Files
dcalc/frontendNew/src/pages/Dcalc/Character/Index.vue
T
2026-06-02 20:56:07 +08:00

388 lines
12 KiB
Vue

<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { FileInput, PanelRight, WandSparkles } from "lucide-vue-next";
import api from "@/api";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Sheet,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { Textarea } from "@/components/ui/textarea";
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 { useDnfHelperStore } from "@/stores/dnfhelper";
import type { IConfigEmblem, IConfigEquip, IConfigOath } from "@/stores/config";
import type { IHelperInfo } from "@/api/info/type";
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 helperStore = useDnfHelperStore();
const loading = ref(false);
const loadError = ref("");
const isExecuting = ref(false);
const isMobileLayout = ref(false);
const isResultSheetOpen = ref(false);
const helperImportOpen = ref(false);
const skillImportOpen = ref(false);
const helperRoleImportOpen = ref(false);
const helperUid = ref("");
const skillCode = ref("");
const helperImportLoading = ref(false);
const skillImportLoading = ref(false);
const importStatus = ref("");
const importError = ref("");
const helperRole = ref<IHelperInfo["roleList"][number]>();
let mediaQuery: MediaQueryList | null = null;
type AvatarConfig = Record<
string,
{
id: string;
enchant: string;
emblem_0: string;
emblem_1: string;
option: string;
}
>;
interface HelperConfigPayload {
equips?: Record<string, IConfigEquip>;
avatar?: AvatarConfig;
avatars?: AvatarConfig;
avatarLink?: string;
emblems?: Record<string, IConfigEmblem>;
fogOathLv?: unknown;
oaths?: Record<string, IConfigOath>;
}
interface SkillImportPayload {
skills?: typeof configStore.config.skills;
}
const updateMobileLayout = () => {
isMobileLayout.value = mediaQuery?.matches ?? false;
if (!isMobileLayout.value) {
isResultSheetOpen.value = false;
}
};
const tabs = [
{ label: "装备", key: "equipment" },
{ label: "技能", key: "skill" },
{ label: "打造", key: "forge" },
{ label: "计算详情", key: "result" },
] as const;
const activeTab = computed(() => {
const path = route.path;
if (path.endsWith("/skill")) return "skill";
if (path.endsWith("/forge")) return "forge";
if (path.endsWith("/result")) return "result";
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);
prepareHelperRoleImport();
} 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}`);
};
const resetImportMessage = () => {
importStatus.value = "";
importError.value = "";
};
const applyHelperConfig = (payload: HelperConfigPayload) => {
if (payload.equips) configStore.config.equips = payload.equips;
if (payload.avatars || payload.avatar) {
configStore.config.avatar = payload.avatars ?? payload.avatar ?? configStore.config.avatar;
}
if (payload.avatarLink !== undefined) configStore.config.avatarLink = payload.avatarLink;
if (payload.emblems) configStore.config.emblems = payload.emblems;
if (payload.fogOathLv !== undefined) configStore.config.sundry.fog = payload.fogOathLv;
if (payload.oaths) configStore.config.oaths = payload.oaths;
configStore.pushChangeSource("Import", "助手打造配置已导入");
};
const importHelperConfig = async () => {
resetImportMessage();
const uid = helperUid.value.trim();
if (!uid) {
importError.value = "请输入助手社区 ID。";
return;
}
helperImportLoading.value = true;
try {
const payload = (await api.getConfigFromHelper(uid)) as HelperConfigPayload;
applyHelperConfig(payload);
helperImportOpen.value = false;
helperUid.value = "";
importStatus.value = "打造导入成功。";
} catch (error) {
importError.value = error instanceof Error ? error.message : "打造导入失败。";
} finally {
helperImportLoading.value = false;
}
};
const importHelperRoleConfig = async () => {
resetImportMessage();
const uid = helperStore.helperInfo?.id;
const roleId = helperRole.value?.roleId;
if (!uid || !roleId) {
importError.value = "未找到可导入的助手角色。";
return;
}
helperImportLoading.value = true;
try {
const payload = (await api.getConfigFromHelperAlter(uid, roleId)) as HelperConfigPayload;
applyHelperConfig(payload);
helperRoleImportOpen.value = false;
importStatus.value = "助手角色打造导入成功。";
} catch (error) {
importError.value = error instanceof Error ? error.message : "助手角色打造导入失败。";
} finally {
helperImportLoading.value = false;
}
};
const importSkillConfig = async () => {
resetImportMessage();
const code = skillCode.value.trim();
if (!code) {
importError.value = "请输入技能加点代码。";
return;
}
skillImportLoading.value = true;
try {
const payload = (await api.getSkillFromCode(code)) as SkillImportPayload;
if (payload.skills) {
configStore.config.skills = payload.skills;
configStore.pushChangeSource("Import", "技能加点代码已导入");
}
skillImportOpen.value = false;
skillCode.value = "";
importStatus.value = "技能导入成功。";
} catch (error) {
importError.value = error instanceof Error ? error.message : "技能导入失败。";
} finally {
skillImportLoading.value = false;
}
};
const prepareHelperRoleImport = () => {
const roleId = route.query.roleId;
if (typeof roleId !== "string") return;
const target = helperStore.helperInfo?.roleList.find((item) => item.roleId === roleId);
if (!target) return;
helperRole.value = target;
helperRoleImportOpen.value = true;
};
onMounted(() => {
loadCharacter();
window.addEventListener("beforeunload", configStore.saveConfig);
mediaQuery = window.matchMedia("(max-width: 1279px)");
updateMobileLayout();
mediaQuery.addEventListener("change", updateMobileLayout);
});
watch(
() => props.alter,
() => {
loadCharacter();
},
);
watch(
() => route.query.roleId,
() => {
prepareHelperRoleImport();
},
);
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">
<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="flex flex-wrap items-center gap-2">
<Sheet v-model:open="helperImportOpen">
<SheetTrigger as-child>
<Button size="sm" variant="outline" @click="resetImportMessage">
<FileInput class="size-4" />
打造导入
</Button>
</SheetTrigger>
<SheetContent class="w-full overflow-y-auto sm:max-w-md">
<SheetHeader>
<SheetTitle>打造导入</SheetTitle>
<SheetDescription>助手社区 ID</SheetDescription>
</SheetHeader>
<div class="grid gap-3 px-4">
<Input v-model="helperUid" placeholder="社区 ID" @keyup.enter="importHelperConfig" />
<p v-if="importError" class="text-sm text-destructive">{{ importError }}</p>
</div>
<SheetFooter>
<Button :disabled="helperImportLoading" @click="importHelperConfig">
{{ helperImportLoading ? "导入中..." : "导入" }}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
<Sheet v-model:open="skillImportOpen">
<SheetTrigger as-child>
<Button size="sm" variant="outline" @click="resetImportMessage">
<WandSparkles class="size-4" />
技能导入
</Button>
</SheetTrigger>
<SheetContent class="w-full overflow-y-auto sm:max-w-md">
<SheetHeader>
<SheetTitle>技能导入</SheetTitle>
<SheetDescription>技能加点代码</SheetDescription>
</SheetHeader>
<div class="grid gap-3 px-4">
<Textarea v-model="skillCode" class="min-h-28" placeholder="技能代码" />
<p v-if="importError" class="text-sm text-destructive">{{ importError }}</p>
</div>
<SheetFooter>
<Button :disabled="skillImportLoading" @click="importSkillConfig">
{{ skillImportLoading ? "导入中..." : "导入" }}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
<p v-if="importStatus" class="text-xs text-muted-foreground">{{ importStatus }}</p>
</div>
<Sheet v-model:open="helperRoleImportOpen">
<SheetContent class="w-full overflow-y-auto sm:max-w-md">
<SheetHeader>
<SheetTitle>助手角色导入</SheetTitle>
<SheetDescription>{{ helperRole?.roleName || "助手角色" }}</SheetDescription>
</SheetHeader>
<div class="grid gap-2 px-4 text-sm">
<div class="rounded-md border p-3">
<div class="font-medium">{{ helperRole?.roleJob }}</div>
<div class="text-muted-foreground">名望 {{ helperRole?.fame ?? "-" }}</div>
</div>
<p v-if="importError" class="text-sm text-destructive">{{ importError }}</p>
</div>
<SheetFooter>
<Button :disabled="helperImportLoading" @click="importHelperRoleConfig">
{{ helperImportLoading ? "导入中..." : "导入" }}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
<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>