Files
dcalc/frontendNew/src/pages/Dcalc/Character/Forge/index.vue
T
2026-05-28 13:16:24 +08:00

824 lines
30 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, ref } from "vue";
import { Hammer, ShieldCheck, Sparkles } from "lucide-vue-next";
import { useConfigStore, useInfoStore } from "@/stores";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import ResultPanel from "@/pages/Dcalc/Character/Result/Index.vue";
const configStore = useConfigStore();
const infoStore = useInfoStore();
const moduleMode = ref<"equip" | "oath" | "avatar" | "global">("equip");
const forgeViewTab = ref<"config" | "result">("result");
const notice = ref("");
const noticeType = ref<"error" | "info">("info");
const avatarPart = ref("头部");
const emblemPart = ref("彩色");
const avatarOptionKeyword = ref("");
const emblemOptionKeyword = ref("");
const onlyConfiguredAvatarParts = ref(false);
const onlyConfiguredEmblemSlots = ref(false);
const noEnchantAvatarParts = ["头部", "帽子", "脸部", "胸部", "上衣", "腰带", "下装", "鞋"];
const emblemLevels = [
"稀有Lv.1",
"稀有Lv.2",
"稀有Lv.3",
"稀有Lv.4",
"稀有Lv.5",
"神器Lv.1",
"神器Lv.2",
"神器Lv.3",
"神器Lv.4",
"神器Lv.5",
"传说Lv.1",
"传说Lv.2",
"传说Lv.3",
"传说Lv.4",
"传说Lv.5",
"史诗Lv.1",
];
const setNotice = (message: string, type: "error" | "info" = "info") => {
notice.value = message;
noticeType.value = type;
};
const trackSource = (detail: string) => {
configStore.pushChangeSource("Forge", detail);
};
const summary = computed(() => {
const equipCount = Object.values(configStore.config.equips || {}).filter(
(item) => !!item.id,
).length;
const avatarCount = Object.values(configStore.config.avatar || {}).filter(
(item) => !!item.id,
).length;
const oathCount = Object.values(configStore.config.oaths || {}).filter(
(item) => !!item.id,
).length;
const emblemCount = Object.values(configStore.config.emblems || {}).reduce((acc, item) => {
return acc + (item.items?.filter(Boolean).length ?? 0);
}, 0);
return {
equipCount,
avatarCount,
oathCount,
emblemCount,
emblemSlots: infoStore.emblemParts.length,
};
});
const parts = computed(() => infoStore.parts.filter((part) => configStore.config.equips[part]));
const oathParts = computed(() =>
infoStore.oathParts.filter((part) => configStore.config.oaths[part]),
);
const avatarParts = computed(() => {
const all = infoStore.avatarParts.filter((part) => !!configStore.config.avatar[part]);
if (!onlyConfiguredAvatarParts.value) return all;
return all.filter((part) => {
const cfg = configStore.config.avatar[part];
if (!cfg) return false;
return !!cfg.id || !!cfg.enchant || !!cfg.emblem_0 || !!cfg.emblem_1 || cfg.option !== "0";
});
});
const oathNameById = computed(() => {
const map = new Map<string, string>();
infoStore.oaths.forEach((item) => {
if (!map.has(item.id)) {
map.set(item.id, item.name);
}
});
return map;
});
const equipNameById = computed(() => {
const map = new Map<string, string>();
infoStore.equips.forEach((item) => map.set(item.id, item.name));
return map;
});
const avatarEnchantOptions = computed(() => {
const keyword = avatarOptionKeyword.value.trim().toLowerCase();
return infoStore.enchants
.filter((item) => item.position.includes(avatarPart.value))
.filter((item) => !keyword || item.detail.toLowerCase().includes(keyword))
.sort((a, b) => (b.fame ?? 0) - (a.fame ?? 0));
});
const avatarSuitOptions = computed(() => {
return infoStore.avatars[avatarPart.value] ?? [];
});
const avatarEmblemOptions = computed(() => {
const keyword = avatarOptionKeyword.value.trim().toLowerCase();
return infoStore.emblems
.filter((item) => item.type !== "白金")
.filter((item) => {
if (!keyword) return true;
return (
item.value.toLowerCase().includes(keyword) ||
item.rarity.toLowerCase().includes(keyword) ||
item.type.toLowerCase().includes(keyword)
);
})
.sort((a, b) => b.fame - a.fame);
});
const hasAvatarEnchant = computed(() => !noEnchantAvatarParts.includes(avatarPart.value));
const emblemOptions = computed(() => {
const keyword = emblemOptionKeyword.value.trim().toLowerCase();
const selected = new Set((emblemCfg.value.items || []).filter(Boolean));
if (emblemPart.value === "彩色") {
return infoStore.emblems
.filter((a) => a.type !== "白金")
.filter((a) => {
const keywordOk =
!keyword ||
a.value.toLowerCase().includes(keyword) ||
a.rarity.toLowerCase().includes(keyword) ||
a.type.toLowerCase().includes(keyword);
const configuredOk = !onlyConfiguredEmblemSlots.value || selected.has(String(a.id));
return keywordOk && configuredOk;
})
.sort((a, b) => b.fame - a.fame);
}
return infoStore.emblems
.filter((a) => {
const color = emblemPart.value.charAt(0);
return a.type === emblemPart.value || a.type === "彩色" || a.type.includes(color);
})
.filter((a) => {
const keywordOk =
!keyword ||
a.value.toLowerCase().includes(keyword) ||
a.rarity.toLowerCase().includes(keyword) ||
a.type.toLowerCase().includes(keyword);
const configuredOk = !onlyConfiguredEmblemSlots.value || selected.has(String(a.id));
return keywordOk && configuredOk;
})
.sort((a, b) => b.fame - a.fame);
});
const emblemMaxCount = computed(() => {
if (emblemPart.value === "彩色") return 6;
if (emblemPart.value === "白金") return 2;
return 4;
});
const setNumberField = (
part: string,
field: "reinforce" | "refine" | "adaptation",
value: string,
) => {
const target = configStore.config.equips[part];
if (!target) return;
const parsed = Number.parseInt(value || "0", 10);
if (Number.isNaN(parsed)) {
setNotice(`${part} 的输入无效,请输入数字。`, "error");
return;
}
target[field] = Math.max(0, parsed);
setNotice(`已更新 ${part} 的打造参数。`);
};
const setOathAdaptation = (part: string, value: string) => {
const target = configStore.config.oaths[part];
if (!target) return;
const parsed = Number.parseInt(value || "0", 10);
if (Number.isNaN(parsed)) {
setNotice(`誓约槽位 ${part} 的输入无效,请输入数字。`, "error");
return;
}
target.adaptation = Math.max(0, parsed);
setNotice(`已更新誓约槽位 ${part} 的适配值。`);
};
const equipCfg = (part: string) => configStore.config.equips[part] ?? configStore.defaultEqusConfig;
const oathCfg = (part: string) => configStore.config.oaths[part] ?? configStore.defaultOathConfig;
const avatarCfg = (part: string) =>
configStore.config.avatar[part] ?? configStore.defaultAvatarConfig;
const ensureAvatarPart = (part: string) => {
if (!configStore.config.avatar[part]) {
configStore.config.avatar[part] = { ...configStore.defaultAvatarConfig };
}
return configStore.config.avatar[part];
};
const setAvatarField = (
field: "id" | "enchant" | "emblem_0" | "emblem_1" | "option",
value: string,
) => {
const target = ensureAvatarPart(avatarPart.value);
target[field] = value;
if (field === "id") {
const options =
avatarSuitOptions.value.find((item) => item.id.toString() === value)?.options ?? [];
if (options.length === 0) {
target.option = "0";
} else if (!options.includes(target.option)) {
target.option = options[0] ?? "0";
}
}
setNotice(`已更新 ${avatarPart.value} 的配置。`);
trackSource(`时装 ${avatarPart.value} ${field} 已更新`);
};
const resetAvatarPart = () => {
configStore.config.avatar[avatarPart.value] = { ...configStore.defaultAvatarConfig };
setNotice(`已重置 ${avatarPart.value} 配置。`);
trackSource(`时装 ${avatarPart.value} 已重置`);
};
const ensureEmblemPart = (part: string) => {
if (!configStore.config.emblems[part]) {
configStore.config.emblems[part] = { ...configStore.defaultEmblemConfig };
}
return configStore.config.emblems[part];
};
const emblemCfg = computed(() => ensureEmblemPart(emblemPart.value));
const setEmblemLv = (value: string) => {
const parsed = Number.parseInt(value || "0", 10);
if (Number.isNaN(parsed)) {
setNotice("徽章等级输入无效。", "error");
return;
}
const target = ensureEmblemPart(emblemPart.value);
target.lv = Math.max(0, Math.min(parsed, emblemLevels.length - 1));
if (emblemPart.value === "彩色") {
["红色", "蓝色", "黄色", "绿色"].forEach((part) => {
const partCfg = ensureEmblemPart(part);
partCfg.lv = Math.max(partCfg.lv || 0, target.lv);
});
} else {
const colorCfg = ensureEmblemPart("彩色");
colorCfg.lv = Math.min(colorCfg.lv || 0, target.lv);
}
setNotice(`已更新 ${emblemPart.value} 徽章等级。`);
trackSource(`徽章 ${emblemPart.value} 等级已更新`);
};
const setEmblemItem = (index: number, value: string) => {
const target = ensureEmblemPart(emblemPart.value);
const items = [...(target.items ?? configStore.defaultEmblemConfig.items)];
while (items.length < 6) items.push("");
items[index] = value;
target.items = items;
setNotice(`已更新 ${emblemPart.value} 徽章位 ${index + 1}。`);
trackSource(`徽章 ${emblemPart.value} 槽位 ${index + 1} 已更新`);
};
const resetEmblemPart = () => {
configStore.config.emblems[emblemPart.value] = { ...configStore.defaultEmblemConfig };
setNotice(`已重置 ${emblemPart.value} 徽章配置。`);
trackSource(`徽章 ${emblemPart.value} 已重置`);
};
const resetEquipPart = (part: string) => {
configStore.config.equips[part] = {
...configStore.defaultEqusConfig,
id: configStore.config.equips[part]?.id ?? "",
};
setNotice(`已重置 ${part} 的打造参数。`);
trackSource(`装备 ${part} 打造参数已重置`);
};
const resetOathPart = (part: string) => {
configStore.config.oaths[part] = {
...configStore.defaultOathConfig,
id: configStore.config.oaths[part]?.id ?? "",
};
setNotice(`已重置誓约槽位 ${part}。`);
trackSource(`誓约槽位 ${part} 已重置`);
};
const resetAllForge = () => {
parts.value.forEach((part) => {
configStore.config.equips[part] = {
...configStore.defaultEqusConfig,
id: configStore.config.equips[part]?.id ?? "",
};
});
oathParts.value.forEach((part) => {
configStore.config.oaths[part] = {
...configStore.defaultOathConfig,
id: configStore.config.oaths[part]?.id ?? "",
};
});
avatarParts.value.forEach((part) => {
configStore.config.avatar[part] = { ...configStore.defaultAvatarConfig };
});
infoStore.emblemParts.forEach((part) => {
configStore.config.emblems[part] = { ...configStore.defaultEmblemConfig };
});
setNotice("已重置当前角色的打造参数。", "info");
trackSource("当前角色 Forge 配置已快速重置");
};
</script>
<template>
<div class="flex h-full min-h-0 flex-col gap-4 overflow-hidden">
<Card class="border-border/60 bg-gradient-to-b from-background to-muted/30">
<CardHeader>
<CardTitle class="flex flex-wrap items-center justify-between gap-2">
<span>Forge 面板</span>
<span class="text-xs font-normal text-muted-foreground">
装备 {{ summary.equipCount }} · 时装 {{ summary.avatarCount }} · 誓约
{{ summary.oathCount }} · 徽章 {{ summary.emblemCount }}
</span>
</CardTitle>
<CardDescription>Forge 进阶:子模块可编辑,统一浅色/深色模式。</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<div class="inline-flex flex-wrap rounded-md border p-1">
<Button
size="sm"
:variant="forgeViewTab === 'result' ? 'default' : 'ghost'"
@click="forgeViewTab = 'result'"
>
结果面板
</Button>
<Button
size="sm"
:variant="forgeViewTab === 'config' ? 'default' : 'ghost'"
@click="forgeViewTab = 'config'"
>
配置编辑
</Button>
</div>
<div
v-if="forgeViewTab === 'config'"
class="hidden inline-flex flex-wrap rounded-md border p-1 sm:inline-flex"
>
<Button
size="sm"
:variant="moduleMode === 'equip' ? 'default' : 'ghost'"
@click="moduleMode = 'equip'"
>
装备打造
</Button>
<Button
size="sm"
:variant="moduleMode === 'oath' ? 'default' : 'ghost'"
@click="moduleMode = 'oath'"
>
誓约
</Button>
<Button
size="sm"
:variant="moduleMode === 'avatar' ? 'default' : 'ghost'"
@click="moduleMode = 'avatar'"
>
时装徽章
</Button>
<Button
size="sm"
:variant="moduleMode === 'global' ? 'default' : 'ghost'"
@click="moduleMode = 'global'"
>
全局
</Button>
</div>
<div v-if="forgeViewTab === 'config'" class="sm:hidden">
<Accordion
type="single"
:default-value="moduleMode"
class="w-full rounded-md border px-2"
>
<AccordionItem value="equip">
<AccordionTrigger @click="moduleMode = 'equip'">装备打造</AccordionTrigger>
<AccordionContent class="text-xs text-muted-foreground"
>强化、锻造、品级等参数编辑。</AccordionContent
>
</AccordionItem>
<AccordionItem value="oath">
<AccordionTrigger @click="moduleMode = 'oath'">誓约</AccordionTrigger>
<AccordionContent class="text-xs text-muted-foreground"
>誓约槽位适配值编辑。</AccordionContent
>
</AccordionItem>
<AccordionItem value="avatar">
<AccordionTrigger @click="moduleMode = 'avatar'">时装徽章</AccordionTrigger>
<AccordionContent class="text-xs text-muted-foreground"
>时装、徽章、等级与条目配置。</AccordionContent
>
</AccordionItem>
<AccordionItem value="global">
<AccordionTrigger @click="moduleMode = 'global'">全局</AccordionTrigger>
<AccordionContent class="text-xs text-muted-foreground"
>系统奶与组队开关等全局参数。</AccordionContent
>
</AccordionItem>
</Accordion>
</div>
<div class="flex flex-wrap gap-2">
<Button size="sm" variant="outline" @click="resetAllForge">快速重置打造</Button>
<router-link to="/dcalc/dnfhelper">
<Button size="sm" variant="outline">前往助手导入</Button>
</router-link>
</div>
<div
v-if="notice"
class="rounded-md border px-3 py-2 text-xs"
:class="
noticeType === 'error'
? 'border-destructive/40 bg-destructive/10 text-destructive'
: 'border-border/60 bg-muted/40 text-muted-foreground'
"
>
{{ notice }}
</div>
</CardContent>
</Card>
<div class="min-h-0 flex-1 overflow-y-auto pr-1">
<div v-if="forgeViewTab === 'result'" class="h-full min-h-[20rem] overflow-hidden">
<div class="h-full overflow-hidden">
<ResultPanel />
</div>
</div>
<div
v-if="forgeViewTab === 'config' && moduleMode === 'equip'"
class="grid grid-cols-1 gap-4"
>
<Card class="border-border/60 bg-card/90">
<CardHeader>
<CardTitle class="flex items-center gap-2">
<Hammer class="size-4" />
装备打造编辑器
</CardTitle>
<CardDescription>强化、锻造、品级编辑并即时回写配置。</CardDescription>
</CardHeader>
<CardContent class="space-y-2 text-xs sm:space-y-3 sm:text-sm">
<div
v-for="part in parts"
:key="part"
class="grid grid-cols-1 gap-2 rounded-md border p-2 md:grid-cols-[7rem_1fr_6rem_6rem_6rem_auto] md:items-center md:p-3"
>
<div class="font-medium">{{ part }}</div>
<div class="truncate text-muted-foreground">
{{ equipNameById.get(equipCfg(part).id) || "未选择装备(可在装备页选择)" }}
</div>
<Input
:model-value="String(equipCfg(part).reinforce)"
type="number"
min="0"
class="h-8 text-xs"
placeholder="强化"
@update:model-value="(value) => setNumberField(part, 'reinforce', String(value))"
/>
<Input
:model-value="String(equipCfg(part).refine)"
type="number"
min="0"
class="h-8 text-xs"
placeholder="锻造"
@update:model-value="(value) => setNumberField(part, 'refine', String(value))"
/>
<Input
:model-value="String(equipCfg(part).adaptation)"
type="number"
min="0"
class="h-8 text-xs"
placeholder="品级"
@update:model-value="(value) => setNumberField(part, 'adaptation', String(value))"
/>
<Button size="sm" variant="ghost" @click="resetEquipPart(part)">重置</Button>
</div>
</CardContent>
</Card>
</div>
<div v-if="forgeViewTab === 'config' && moduleMode === 'oath'" class="grid grid-cols-1 gap-4">
<Card class="border-border/60 bg-card/90">
<CardHeader>
<CardTitle class="flex items-center gap-2">
<ShieldCheck class="size-4" />
誓约子模块
</CardTitle>
<CardDescription>编辑誓约槽位适配值。</CardDescription>
</CardHeader>
<CardContent class="space-y-2 text-xs sm:space-y-3 sm:text-sm">
<div
v-for="part in oathParts"
:key="part"
class="grid grid-cols-1 gap-2 rounded-md border p-2 md:grid-cols-[6rem_1fr_7rem_auto] md:items-center md:p-3"
>
<div class="font-medium">槽位 {{ part }}</div>
<div class="truncate text-muted-foreground">
{{ oathNameById.get(oathCfg(part).id) || "未选择誓约(可在装备页选择)" }}
</div>
<Input
:model-value="String(oathCfg(part).adaptation)"
type="number"
min="0"
class="h-8 text-xs"
placeholder="适配"
@update:model-value="(value) => setOathAdaptation(part, String(value))"
/>
<Button size="sm" variant="ghost" @click="resetOathPart(part)">重置</Button>
</div>
</CardContent>
</Card>
</div>
<div
v-if="forgeViewTab === 'config' && moduleMode === 'avatar'"
class="grid grid-cols-1 gap-4 xl:grid-cols-2"
>
<Card class="border-border/60 bg-card/90">
<CardHeader>
<CardTitle class="flex items-center gap-2">
<Sparkles class="size-4" />
时装编辑
</CardTitle>
<CardDescription>支持附魔、徽章项、品级和属性值编辑。</CardDescription>
</CardHeader>
<CardContent class="space-y-2 text-xs sm:space-y-3 sm:text-sm">
<div class="grid grid-cols-1 gap-2 md:grid-cols-[7rem_1fr] md:items-center">
<div class="text-muted-foreground">筛选</div>
<div class="flex items-center gap-2">
<Input
v-model="avatarOptionKeyword"
class="h-8 text-xs"
placeholder="搜索附魔/徽章可选项"
/>
<label class="inline-flex items-center gap-1 text-xs whitespace-nowrap">
<input v-model="onlyConfiguredAvatarParts" type="checkbox" class="size-3.5" />
仅已配置部位
</label>
</div>
</div>
<div class="grid grid-cols-1 gap-2 md:grid-cols-[7rem_1fr] md:items-center">
<div class="text-muted-foreground">部位</div>
<select
class="h-8 rounded-md border border-input bg-background px-2"
v-model="avatarPart"
>
<option v-for="part in avatarParts" :key="part" :value="part">{{ part }}</option>
</select>
</div>
<div
v-if="hasAvatarEnchant"
class="grid grid-cols-1 gap-2 md:grid-cols-[7rem_1fr] md:items-center"
>
<div class="text-muted-foreground">附魔</div>
<select
class="h-8 rounded-md border border-input bg-background px-2"
:value="avatarCfg(avatarPart).enchant"
@change="
(event) => setAvatarField('enchant', (event.target as HTMLSelectElement).value)
"
>
<option value="">无</option>
<option
v-for="item in avatarEnchantOptions"
:key="item.id"
:value="String(item.id)"
>
{{ item.detail }}
</option>
</select>
</div>
<template v-else>
<div class="grid grid-cols-1 gap-2 md:grid-cols-[7rem_1fr] md:items-center">
<div class="text-muted-foreground">品级</div>
<select
class="h-8 rounded-md border border-input bg-background px-2"
:value="avatarCfg(avatarPart).id"
@change="
(event) => setAvatarField('id', (event.target as HTMLSelectElement).value)
"
>
<option value="">未选择</option>
<option v-for="item in avatarSuitOptions" :key="item.id" :value="String(item.id)">
{{ item.suit || item.rarity || item.name }}
</option>
</select>
</div>
<div class="grid grid-cols-1 gap-2 md:grid-cols-[7rem_1fr] md:items-center">
<div class="text-muted-foreground">属性</div>
<select
class="h-8 rounded-md border border-input bg-background px-2"
:value="avatarCfg(avatarPart).option"
@change="
(event) => setAvatarField('option', (event.target as HTMLSelectElement).value)
"
>
<option value="0">无</option>
<option
v-for="opt in avatarSuitOptions.find(
(x) => String(x.id) === avatarCfg(avatarPart).id,
)?.options || []"
:key="opt"
:value="opt"
>
{{ opt }}
</option>
</select>
</div>
</template>
<div class="grid grid-cols-1 gap-2 md:grid-cols-[7rem_1fr] md:items-center">
<div class="text-muted-foreground">徽章一</div>
<select
class="h-8 rounded-md border border-input bg-background px-2"
:value="avatarCfg(avatarPart).emblem_0"
@change="
(event) => setAvatarField('emblem_0', (event.target as HTMLSelectElement).value)
"
>
<option value="">无</option>
<option v-for="item in avatarEmblemOptions" :key="item.id" :value="String(item.id)">
[{{ item.rarity }}] {{ item.value }}
</option>
</select>
</div>
<div class="grid grid-cols-1 gap-2 md:grid-cols-[7rem_1fr] md:items-center">
<div class="text-muted-foreground">徽章二</div>
<select
class="h-8 rounded-md border border-input bg-background px-2"
:value="avatarCfg(avatarPart).emblem_1"
@change="
(event) => setAvatarField('emblem_1', (event.target as HTMLSelectElement).value)
"
>
<option value="">无</option>
<option v-for="item in avatarEmblemOptions" :key="item.id" :value="String(item.id)">
[{{ item.rarity }}] {{ item.value }}
</option>
</select>
</div>
<div class="flex justify-end">
<Button size="sm" variant="outline" @click="resetAvatarPart">重置当前部位</Button>
</div>
</CardContent>
</Card>
<Card class="border-border/60 bg-card/90">
<CardHeader>
<CardTitle class="flex items-center gap-2">
<Sparkles class="size-4" />
徽章编辑
</CardTitle>
<CardDescription>支持等级、徽章条目与槽位数量规则。</CardDescription>
</CardHeader>
<CardContent class="space-y-2 text-xs sm:space-y-3 sm:text-sm">
<div class="grid grid-cols-1 gap-2 md:grid-cols-[7rem_1fr] md:items-center">
<div class="text-muted-foreground">筛选</div>
<div class="flex items-center gap-2">
<Input
v-model="emblemOptionKeyword"
class="h-8 text-xs"
placeholder="搜索徽章可选项"
/>
<label class="inline-flex items-center gap-1 text-xs whitespace-nowrap">
<input v-model="onlyConfiguredEmblemSlots" type="checkbox" class="size-3.5" />
仅已配置项
</label>
</div>
</div>
<div class="grid grid-cols-1 gap-2 md:grid-cols-[7rem_1fr] md:items-center">
<div class="text-muted-foreground">槽位类型</div>
<select
class="h-8 rounded-md border border-input bg-background px-2"
v-model="emblemPart"
>
<option v-for="part in infoStore.emblemParts" :key="part" :value="part">
{{ part }}
</option>
</select>
</div>
<div
v-if="emblemPart !== '白金'"
class="grid grid-cols-1 gap-2 md:grid-cols-[7rem_1fr] md:items-center"
>
<div class="text-muted-foreground">等级</div>
<select
class="h-8 rounded-md border border-input bg-background px-2"
:value="String(emblemCfg.lv ?? 0)"
@change="(event) => setEmblemLv((event.target as HTMLSelectElement).value)"
>
<option v-for="(label, index) in emblemLevels" :key="label" :value="String(index)">
{{ label }}
</option>
</select>
</div>
<div class="space-y-2">
<div class="text-muted-foreground">徽章条目</div>
<div class="grid grid-cols-1 gap-2 md:grid-cols-2">
<div
v-for="index in emblemMaxCount"
:key="index"
v-show="!onlyConfiguredEmblemSlots || !!(emblemCfg.items[index - 1] || '')"
class="space-y-1"
>
<div class="text-xs text-muted-foreground">槽位 {{ index }}</div>
<select
class="h-8 w-full rounded-md border border-input bg-background px-2"
:value="emblemCfg.items[index - 1] || ''"
@change="
(event) => setEmblemItem(index - 1, (event.target as HTMLSelectElement).value)
"
>
<option value="">无</option>
<option v-for="item in emblemOptions" :key="item.id" :value="String(item.id)">
[{{ item.rarity }}] {{ item.value }}
</option>
</select>
</div>
</div>
</div>
<div class="flex justify-end">
<Button size="sm" variant="outline" @click="resetEmblemPart">重置当前徽章槽</Button>
</div>
</CardContent>
</Card>
</div>
<div
v-if="forgeViewTab === 'config' && moduleMode === 'global'"
class="grid grid-cols-1 gap-4 lg:grid-cols-2"
>
<Card class="border-border/60 bg-card/90">
<CardHeader>
<CardTitle class="flex items-center gap-2">
<ShieldCheck class="size-4" />
计算开关
</CardTitle>
<CardDescription>从旧版 Character 页底部迁移的全局参数。</CardDescription>
</CardHeader>
<CardContent class="space-y-2 text-sm text-muted-foreground">
<label class="flex items-center justify-between rounded-md border p-2">
<span>系统奶 DSB</span>
<input v-model="configStore.config.DSB" type="checkbox" class="size-4" />
</label>
<label class="flex items-center justify-between rounded-md border p-2">
<span>组队 BUFF</span>
<input v-model="configStore.config.BUFF" type="checkbox" class="size-4" />
</label>
</CardContent>
</Card>
<Card class="border-border/60 bg-card/90">
<CardHeader>
<CardTitle class="flex items-center gap-2">
<ShieldCheck class="size-4" />
当前统计
</CardTitle>
</CardHeader>
<CardContent class="space-y-2 text-sm text-muted-foreground">
<div>已配置装备: {{ summary.equipCount }}</div>
<div>誓约槽位: {{ summary.oathCount }}</div>
<div>已配置时装: {{ summary.avatarCount }}</div>
<div>已配置徽章条目: {{ summary.emblemCount }}</div>
</CardContent>
</Card>
</div>
</div>
</div>
</template>