388 lines
12 KiB
Vue
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>
|