Files
dcalc/frontendNew/src/pages/Dcalc/Character/Forge/index.vue
T
2026-06-10 12:13:24 +08:00

1269 lines
47 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 {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import type { IJade } from "@/api/info/type";
import ResultPanel from "@/pages/Dcalc/Character/Result/Index.vue";
const configStore = useConfigStore();
const infoStore = useInfoStore();
const moduleMode = ref<"equip" | "oath" | "avatar" | "jade" | "sundry" | "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 equipBatchApply = ref(false);
const oathBatchApply = 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 jadeKeys = ["0", "1", "2", "3"] as const;
const sundryFields = [
{ key: "fog", label: "迷雾系统", defaultValue: 10 },
{ key: "adventure", label: "冒险团", defaultValue: 30 },
{ key: "marriage_house", label: "结婚房屋", defaultValue: 4 },
{ key: "marriage_ring", label: "结婚戒指", defaultValue: 2 },
{ key: "contract", label: "晶体契约", defaultValue: 1 },
{ key: "collection_type", label: "收集箱类型", defaultValue: 1 },
{ key: "collection_num_0", label: "收集箱数量一", defaultValue: 5 },
{ key: "collection_num_1", label: "收集箱数量二", defaultValue: 5 },
{ key: "costume_card", label: "名称装扮", defaultValue: 1 },
] as const;
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);
const jadeCount = Object.values(configStore.config.jades || {}).filter(
(item) => Number(item.id) >= 0,
).length;
const sundryCount = sundryFields.filter((item) => {
const value = configStore.config.sundry?.[item.key] ?? item.defaultValue;
return value !== 0 && value !== "" && value !== undefined;
}).length;
return {
equipCount,
avatarCount,
oathCount,
emblemCount,
jadeCount,
sundryCount,
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 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;
}
const partsToApply = oathBatchApply.value ? infoStore.oathParts : [part];
partsToApply.forEach((item) => {
if (!configStore.config.oaths[item]) {
configStore.config.oaths[item] = configStore.createDefaultOathConfig();
}
configStore.config.oaths[item].adaptation = Math.max(0, parsed);
});
setNotice(
oathBatchApply.value ? "已批量更新全部誓约槽位适配值。" : `已更新誓约槽位 ${part} 的适配值。`,
);
trackSource(oathBatchApply.value ? "誓约适配已批量更新" : `誓约槽位 ${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 canUpgradeEquip = (part: string) => !["称号", "宠物"].includes(part);
const canSetSecret = (part: string) => ["武器", "辅助装备", "魔法石", "耳环"].includes(part);
const equipEnchantOptions = (part: string) =>
infoStore.enchants
.filter(
(item) =>
item.position.includes(part) &&
!item.position.includes("武器装扮") &&
!item.position.includes("宠物装备"),
)
.sort((a, b) => (b.fame ?? 0) - (a.fame ?? 0));
const formatFloat = (value: number, digits = 1) => Number(value.toFixed(digits)).toString();
const ensureJade = (key: string) => {
if (!configStore.config.jades[key]) {
configStore.config.jades[key] = { id: -1, value: 0 };
}
return configStore.config.jades[key];
};
const jadeValueOptions = (id: number) => {
if (id < 0) return [];
const current = infoStore.jades.find((item) => item.id === id) as IJade | undefined;
if (!current) return [];
const result: { id: number; value: string }[] = [];
const max = Number(current.max);
const min = Number(current.min);
const step = Number(current.pre);
if (current.unit.includes("等级")) {
result.push({ id: 1, value: current.unit });
} else if (step > 0) {
for (let value = max; value >= min; value = Number((value - step).toFixed(1))) {
if (value !== 0) {
result.push({
id: value,
value: `${value > 0 ? "+" : ""}${formatFloat(value, 1)}${current.unit}`,
});
}
}
}
return result;
};
const setJadeId = (key: string, value: string) => {
const target = ensureJade(key);
const id = Number.parseInt(value, 10);
target.id = Number.isNaN(id) ? -1 : id;
target.value = jadeValueOptions(target.id)[0]?.id ?? 0;
setNotice(`已更新辟邪玉槽位 ${Number(key) + 1}。`);
trackSource(`辟邪玉槽位 ${Number(key) + 1} 已更新`);
};
const setJadeValue = (key: string, value: string) => {
const target = ensureJade(key);
const parsed = Number(value);
target.value = Number.isNaN(parsed) ? 0 : parsed;
setNotice(`已更新辟邪玉槽位 ${Number(key) + 1} 数值。`);
trackSource(`辟邪玉槽位 ${Number(key) + 1} 数值已更新`);
};
const resetJades = () => {
jadeKeys.forEach((key) => {
configStore.config.jades[key] = { id: -1, value: 0 };
});
setNotice("已重置辟邪玉配置。");
trackSource("辟邪玉配置已重置");
};
const sundryOptions = (key: string) => infoStore.sundries[key]?.options ?? [];
const sundryValue = (key: string, defaultValue: number) => {
return String(configStore.config.sundry?.[key] ?? defaultValue);
};
const setSundryValue = (key: string, value: string) => {
const parsed = Number(value);
configStore.config.sundry[key] = Number.isNaN(parsed) ? value : parsed;
setNotice("已更新其它系统配置。");
trackSource(`其它系统 ${key} 已更新`);
};
const resetSundry = () => {
sundryFields.forEach((field) => {
configStore.config.sundry[field.key] = field.defaultValue;
});
setNotice("已重置其它系统配置。");
trackSource("其它系统配置已重置");
};
const setEquipNumberField = (
part: string,
field: "reinforce" | "reinforceType" | "refine" | "adaptation" | "precision",
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;
}
const normalized =
field === "precision" ? Math.max(0, Math.min(parsed, 100)) : Math.max(0, parsed);
const canBatch =
equipBatchApply.value && ["reinforceType", "reinforce", "adaptation"].includes(field);
const partsToApply = canBatch ? infoStore.parts.filter(canUpgradeEquip) : [part];
partsToApply.forEach((item) => {
if (!configStore.config.equips[item]) {
configStore.config.equips[item] = { ...configStore.defaultEqusConfig };
}
configStore.config.equips[item][field] = normalized;
});
setNotice(canBatch ? "已批量更新装备打造参数。" : `已更新 ${part} 的打造参数。`);
trackSource(canBatch ? `装备 ${field} 已批量更新` : `装备 ${part} ${field} 已更新`);
};
const setEquipEnchant = (part: string, value: string) => {
const target = configStore.config.equips[part];
if (!target) return;
const enchant = infoStore.enchants.find((item) => String(item.id) === value);
const partsToApply =
equipBatchApply.value && enchant?.position.length
? enchant.position.filter((item) => configStore.config.equips[item])
: [part];
partsToApply.forEach((item) => {
if (!configStore.config.equips[item]) {
configStore.config.equips[item] = { ...configStore.defaultEqusConfig };
}
configStore.config.equips[item].enchant = value;
});
setNotice(
equipBatchApply.value && partsToApply.length > 1
? "已批量更新装备附魔。"
: `已更新 ${part} 的附魔。`,
);
trackSource(
equipBatchApply.value && partsToApply.length > 1
? "装备附魔已批量更新"
: `装备 ${part} 附魔已更新`,
);
};
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.createDefaultEmblemConfig();
}
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.createDefaultEmblemConfig();
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.createDefaultOathConfig(),
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.createDefaultOathConfig(),
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.createDefaultEmblemConfig();
});
jadeKeys.forEach((key) => {
configStore.config.jades[key] = { id: -1, value: 0 };
});
sundryFields.forEach((field) => {
configStore.config.sundry[field.key] = field.defaultValue;
});
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 }} · 辟邪玉
{{ summary.jadeCount }} · 其它 {{ summary.sundryCount }}
</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 === 'jade' ? 'default' : 'ghost'"
@click="moduleMode = 'jade'"
>
辟邪玉
</Button>
<Button
size="sm"
:variant="moduleMode === 'sundry' ? 'default' : 'ghost'"
@click="moduleMode = 'sundry'"
>
其它
</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="jade">
<AccordionTrigger @click="moduleMode = 'jade'">辟邪玉</AccordionTrigger>
<AccordionContent class="text-xs text-muted-foreground"
>4 个辟邪玉词条与数值配置。</AccordionContent
>
</AccordionItem>
<AccordionItem value="sundry">
<AccordionTrigger @click="moduleMode = 'sundry'">其它</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">
<label
class="flex flex-wrap items-center justify-between gap-2 rounded-md border bg-muted/30 p-2 text-muted-foreground"
>
<span>批量应用到同类部位</span>
<input v-model="equipBatchApply" type="checkbox" class="size-4" />
</label>
<div v-for="part in parts" :key="part" class="space-y-3 rounded-md border p-2 md:p-3">
<div class="grid grid-cols-[5rem_minmax(0,1fr)_auto] items-center gap-2">
<div class="font-medium">{{ part }}</div>
<div class="truncate text-muted-foreground">
{{ equipNameById.get(equipCfg(part).id) || "未选择装备(可在装备页选择)" }}
</div>
<Button size="sm" variant="ghost" @click="resetEquipPart(part)">重置</Button>
</div>
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2 xl:grid-cols-4">
<label v-if="canUpgradeEquip(part)" class="space-y-1">
<span class="text-muted-foreground">强化类型</span>
<Select
:model-value="String(equipCfg(part).reinforceType)"
@update:model-value="
(value) => setEquipNumberField(part, 'reinforceType', String(value))
"
>
<SelectTrigger class="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="0">强化</SelectItem>
<SelectItem value="1">增幅</SelectItem>
</SelectContent>
</Select>
</label>
<label v-if="canUpgradeEquip(part)" class="space-y-1">
<span class="text-muted-foreground">强化/增幅</span>
<Input
:model-value="String(equipCfg(part).reinforce)"
type="number"
min="0"
max="30"
class="h-8 text-xs"
@update:model-value="
(value) => setEquipNumberField(part, 'reinforce', String(value))
"
/>
</label>
<label class="space-y-1">
<span class="text-muted-foreground">锻造</span>
<Input
:model-value="String(equipCfg(part).refine)"
type="number"
min="0"
max="8"
class="h-8 text-xs"
@update:model-value="
(value) => setEquipNumberField(part, 'refine', String(value))
"
/>
</label>
<label v-if="canUpgradeEquip(part)" class="space-y-1">
<span class="text-muted-foreground">调适</span>
<Input
:model-value="String(equipCfg(part).adaptation)"
type="number"
min="0"
max="3"
class="h-8 text-xs"
@update:model-value="
(value) => setEquipNumberField(part, 'adaptation', String(value))
"
/>
</label>
<label v-if="equipEnchantOptions(part).length" class="space-y-1 sm:col-span-2">
<span class="text-muted-foreground">附魔</span>
<Select
:model-value="equipCfg(part).enchant"
@update:model-value="(value) => setEquipEnchant(part, String(value))"
>
<SelectTrigger class="h-8 min-w-0 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="">无</SelectItem>
<SelectItem
v-for="item in equipEnchantOptions(part)"
:key="item.id"
:value="String(item.id)"
>
{{ item.detail }}
</SelectItem>
</SelectContent>
</Select>
</label>
<label v-if="canSetSecret(part)" class="space-y-1">
<span class="text-muted-foreground">秘宝精度</span>
<Input
:model-value="String(equipCfg(part).precision)"
type="number"
min="0"
max="100"
class="h-8 text-xs"
@update:model-value="
(value) => setEquipNumberField(part, 'precision', String(value))
"
/>
</label>
</div>
</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">
<label
class="flex flex-wrap items-center justify-between gap-2 rounded-md border bg-muted/30 p-2 text-muted-foreground"
>
<span>批量应用到全部誓约槽位</span>
<input v-model="oathBatchApply" type="checkbox" class="size-4" />
</label>
<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 v-model="avatarPart">
<SelectTrigger class="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="part in avatarParts" :key="part" :value="part">
{{ part }}
</SelectItem>
</SelectContent>
</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
:model-value="avatarCfg(avatarPart).enchant"
@update:model-value="(value) => setAvatarField('enchant', String(value))"
>
<SelectTrigger class="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="">无</SelectItem>
<SelectItem
v-for="item in avatarEnchantOptions"
:key="item.id"
:value="String(item.id)"
>
{{ item.detail }}
</SelectItem>
</SelectContent>
</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
:model-value="avatarCfg(avatarPart).id"
@update:model-value="(value) => setAvatarField('id', String(value))"
>
<SelectTrigger class="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="">未选择</SelectItem>
<SelectItem
v-for="item in avatarSuitOptions"
:key="item.id"
:value="String(item.id)"
>
{{ item.suit || item.rarity || item.name }}
</SelectItem>
</SelectContent>
</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
:model-value="avatarCfg(avatarPart).option"
@update:model-value="(value) => setAvatarField('option', String(value))"
>
<SelectTrigger class="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="0">无</SelectItem>
<SelectItem
v-for="opt in avatarSuitOptions.find(
(x) => String(x.id) === avatarCfg(avatarPart).id,
)?.options || []"
:key="opt"
:value="opt"
>
{{ opt }}
</SelectItem>
</SelectContent>
</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
:model-value="avatarCfg(avatarPart).emblem_0"
@update:model-value="(value) => setAvatarField('emblem_0', String(value))"
>
<SelectTrigger class="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="">无</SelectItem>
<SelectItem
v-for="item in avatarEmblemOptions"
:key="item.id"
:value="String(item.id)"
>
[{{ item.rarity }}] {{ item.value }}
</SelectItem>
</SelectContent>
</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
:model-value="avatarCfg(avatarPart).emblem_1"
@update:model-value="(value) => setAvatarField('emblem_1', String(value))"
>
<SelectTrigger class="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="">无</SelectItem>
<SelectItem
v-for="item in avatarEmblemOptions"
:key="item.id"
:value="String(item.id)"
>
[{{ item.rarity }}] {{ item.value }}
</SelectItem>
</SelectContent>
</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 v-model="emblemPart">
<SelectTrigger class="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="part in infoStore.emblemParts" :key="part" :value="part">
{{ part }}
</SelectItem>
</SelectContent>
</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
:model-value="String(emblemCfg.lv ?? 0)"
@update:model-value="(value) => setEmblemLv(String(value))"
>
<SelectTrigger class="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="(label, index) in emblemLevels"
:key="label"
:value="String(index)"
>
{{ label }}
</SelectItem>
</SelectContent>
</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
:model-value="emblemCfg.items[index - 1] || ''"
@update:model-value="(value) => setEmblemItem(index - 1, String(value))"
>
<SelectTrigger class="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="">无</SelectItem>
<SelectItem
v-for="item in emblemOptions"
:key="item.id"
:value="String(item.id)"
>
[{{ item.rarity }}] {{ item.value }}
</SelectItem>
</SelectContent>
</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 === 'jade'" class="grid grid-cols-1 gap-4">
<Card class="border-border/60 bg-card/90">
<CardHeader>
<CardTitle class="flex items-center gap-2">
<Sparkles class="size-4" />
辟邪玉
</CardTitle>
<CardDescription>从旧版 Forge 迁移的 4 个辟邪玉词条与数值配置。</CardDescription>
</CardHeader>
<CardContent class="space-y-2 text-xs sm:space-y-3 sm:text-sm">
<div
v-for="key in jadeKeys"
:key="key"
class="grid grid-cols-1 gap-2 rounded-md border p-2 md:grid-cols-[6rem_1fr_12rem] md:items-center md:p-3"
>
<div class="font-medium">槽位 {{ Number(key) + 1 }}</div>
<Select
:model-value="String(ensureJade(key).id)"
@update:model-value="(value) => setJadeId(key, String(value))"
>
<SelectTrigger class="h-8 min-w-0 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="-1">无</SelectItem>
<SelectItem
v-for="item in infoStore.jades"
:key="item.id"
:value="String(item.id)"
>
{{ item.name }}
</SelectItem>
</SelectContent>
</Select>
<Select
:model-value="String(ensureJade(key).value)"
:disabled="ensureJade(key).id < 0"
@update:model-value="(value) => setJadeValue(key, String(value))"
>
<SelectTrigger class="h-8 min-w-0 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="0">无</SelectItem>
<SelectItem
v-for="item in jadeValueOptions(ensureJade(key).id)"
:key="item.id"
:value="String(item.id)"
>
{{ item.value }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div class="flex justify-end">
<Button size="sm" variant="outline" @click="resetJades">重置辟邪玉</Button>
</div>
</CardContent>
</Card>
</div>
<div
v-if="forgeViewTab === 'config' && moduleMode === 'sundry'"
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 class="grid grid-cols-1 gap-2 md:grid-cols-2 xl:grid-cols-3">
<div
v-for="field in sundryFields"
:key="field.key"
class="grid grid-cols-1 gap-2 rounded-md border p-2 sm:grid-cols-[6rem_1fr] sm:items-center sm:p-3"
>
<div class="font-medium">{{ field.label }}</div>
<Select
:model-value="sundryValue(field.key, field.defaultValue)"
@update:model-value="(value) => setSundryValue(field.key, String(value))"
>
<SelectTrigger class="h-8 min-w-0 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="item in sundryOptions(field.key)"
:key="`${field.key}-${item.value}`"
:value="String(item.value)"
>
{{ item.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div class="flex justify-end">
<Button size="sm" variant="outline" @click="resetSundry">重置其它系统</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>
<div>已配置辟邪玉: {{ summary.jadeCount }}</div>
<div>其它系统项目: {{ summary.sundryCount }}</div>
</CardContent>
</Card>
</div>
</div>
</div>
</template>