180 lines
5.4 KiB
Vue
180 lines
5.4 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
||
import { useRoute, useRouter } from "vue-router";
|
||
import { LoaderCircle, PanelRight, RefreshCcw } from "lucide-vue-next";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
|
||
import Tabs from "@/components/ui/tabs/Tabs.vue";
|
||
import TabsList from "@/components/ui/tabs/TabsList.vue";
|
||
import TabsTrigger from "@/components/ui/tabs/TabsTrigger.vue";
|
||
import { useConfigStore, useInfoStore } from "@/stores";
|
||
import ResultPanel from "@/pages/Dcalc/Character/Result/Index.vue";
|
||
const props = defineProps<{
|
||
alter: string;
|
||
}>();
|
||
|
||
const route = useRoute();
|
||
const router = useRouter();
|
||
const infoStore = useInfoStore();
|
||
const configStore = useConfigStore();
|
||
const loading = ref(false);
|
||
const loadError = ref("");
|
||
const isExecuting = ref(false);
|
||
const isMobileLayout = ref(false);
|
||
const isResultSheetOpen = ref(false);
|
||
|
||
let mediaQuery: MediaQueryList | null = null;
|
||
|
||
const updateMobileLayout = () => {
|
||
isMobileLayout.value = mediaQuery?.matches ?? false;
|
||
if (!isMobileLayout.value) {
|
||
isResultSheetOpen.value = false;
|
||
}
|
||
};
|
||
|
||
const tabs = [
|
||
{ label: "装备", key: "equipment" },
|
||
{ label: "技能", key: "skill" },
|
||
{ label: "打造", key: "forge" },
|
||
] as const;
|
||
|
||
const activeTab = computed(() => {
|
||
const path = route.path;
|
||
if (path.endsWith("/skill")) return "skill";
|
||
if (path.endsWith("/forge")) return "forge";
|
||
return "equipment";
|
||
});
|
||
|
||
// Result panel is now embedded inside Equipment/Skill/Forge pages to avoid duplicate rendering.
|
||
const showGlobalResultPanel = computed(() => false);
|
||
|
||
const loadCharacter = async () => {
|
||
loading.value = true;
|
||
loadError.value = "";
|
||
try {
|
||
await infoStore.createCharacter(props.alter);
|
||
} catch (error) {
|
||
loadError.value = error instanceof Error ? error.message : "角色数据加载失败";
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
const switchTab = (tab: (typeof tabs)[number]["key"]) => {
|
||
router.push(`/dcalc/character/${props.alter}/${tab}`);
|
||
};
|
||
|
||
onMounted(() => {
|
||
loadCharacter();
|
||
window.addEventListener("beforeunload", configStore.saveConfig);
|
||
mediaQuery = window.matchMedia("(max-width: 1279px)");
|
||
updateMobileLayout();
|
||
mediaQuery.addEventListener("change", updateMobileLayout);
|
||
});
|
||
|
||
watch(
|
||
() => props.alter,
|
||
() => {
|
||
loadCharacter();
|
||
},
|
||
);
|
||
|
||
watch(showGlobalResultPanel, (show) => {
|
||
if (!show) {
|
||
isResultSheetOpen.value = false;
|
||
}
|
||
});
|
||
|
||
const stopConfigWatch = watch(
|
||
() => JSON.stringify(configStore.config),
|
||
async () => {
|
||
if (isExecuting.value) return;
|
||
isExecuting.value = true;
|
||
try {
|
||
await configStore.result.execute();
|
||
configStore.saveConfig();
|
||
} finally {
|
||
isExecuting.value = false;
|
||
}
|
||
},
|
||
{ deep: true },
|
||
);
|
||
|
||
onUnmounted(() => {
|
||
stopConfigWatch();
|
||
mediaQuery?.removeEventListener("change", updateMobileLayout);
|
||
window.removeEventListener("beforeunload", configStore.saveConfig);
|
||
configStore.saveConfig();
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<section class="mx-auto flex min-h-full w-full max-w-7xl flex-col gap-4 px-4 py-6 md:px-8">
|
||
<Card class="flex-none">
|
||
<CardHeader>
|
||
<CardTitle class="flex items-center justify-between">
|
||
<span>角色配置</span>
|
||
<Button variant="outline" size="sm" :disabled="loading" @click="loadCharacter">
|
||
<RefreshCcw :class="{ 'animate-spin': loading }" />
|
||
刷新
|
||
</Button>
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||
<Tabs :model-value="activeTab" class="gap-0">
|
||
<TabsList>
|
||
<TabsTrigger
|
||
v-for="tab in tabs"
|
||
:key="tab.key"
|
||
:value="tab.key"
|
||
@click="switchTab(tab.key)"
|
||
>
|
||
{{ tab.label }}
|
||
</TabsTrigger>
|
||
</TabsList>
|
||
</Tabs>
|
||
<div class="text-sm text-muted-foreground">
|
||
<span v-if="loading" class="inline-flex items-center gap-2">
|
||
<LoaderCircle class="size-4 animate-spin" />
|
||
正在同步角色数据...
|
||
</span>
|
||
<span v-else-if="loadError">{{ loadError }}</span>
|
||
<span v-else>数据源已就绪,可继续编辑配置。</span>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<div
|
||
:class="[
|
||
'grid min-h-0 flex-1 grid-cols-1 gap-4',
|
||
showGlobalResultPanel ? 'xl:grid-cols-[minmax(0,1fr)_24rem]' : 'xl:grid-cols-1',
|
||
]"
|
||
>
|
||
<div class="min-w-0 overflow-hidden">
|
||
<router-view />
|
||
</div>
|
||
<aside v-if="!isMobileLayout && showGlobalResultPanel" class="min-h-0">
|
||
<ResultPanel />
|
||
</aside>
|
||
</div>
|
||
|
||
<Sheet v-if="isMobileLayout && showGlobalResultPanel" v-model:open="isResultSheetOpen">
|
||
<SheetTrigger as-child>
|
||
<Button class="fixed bottom-5 right-4 z-30 shadow-lg" size="sm">
|
||
<PanelRight class="size-4" />
|
||
查看结果
|
||
</Button>
|
||
</SheetTrigger>
|
||
<SheetContent side="bottom" class="h-[78vh] overflow-hidden px-3 pb-3">
|
||
<SheetHeader class="pb-2">
|
||
<SheetTitle>实时计算结果</SheetTitle>
|
||
</SheetHeader>
|
||
<div class="h-[calc(78vh-4.5rem)] overflow-hidden">
|
||
<ResultPanel />
|
||
</div>
|
||
</SheetContent>
|
||
</Sheet>
|
||
</section>
|
||
</template>
|