This commit was merged in pull request #1.
This commit is contained in:
2026-06-02 20:56:07 +08:00
parent f824177aa5
commit bb3ad71d21
38 changed files with 1304 additions and 532 deletions
@@ -11,12 +11,13 @@ import {
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" | "global">("equip");
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");
@@ -27,6 +28,8 @@ const avatarOptionKeyword = ref("");
const emblemOptionKeyword = ref("");
const onlyConfiguredAvatarParts = ref(false);
const onlyConfiguredEmblemSlots = ref(false);
const equipBatchApply = ref(false);
const oathBatchApply = ref(false);
const noEnchantAvatarParts = ["头部", "帽子", "脸部", "胸部", "上衣", "腰带", "下装", "鞋"];
@@ -49,6 +52,19 @@ const emblemLevels = [
"史诗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;
@@ -71,12 +87,21 @@ const summary = computed(() => {
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,
};
});
@@ -182,9 +207,127 @@ const emblemMaxCount = computed(() => {
return 4;
});
const setNumberField = (
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.defaultOathConfig };
}
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" | "refine" | "adaptation",
field: "reinforce" | "reinforceType" | "refine" | "adaptation" | "precision",
value: string,
) => {
const target = configStore.config.equips[part];
@@ -196,29 +339,45 @@ const setNumberField = (
return;
}
target[field] = Math.max(0, parsed);
setNotice(`已更新 ${part} 的打造参数。`);
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 setOathAdaptation = (part: string, value: string) => {
const target = configStore.config.oaths[part];
const setEquipEnchant = (part: string, 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 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];
target.adaptation = Math.max(0, parsed);
setNotice(`已更新誓约槽位 ${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 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 };
@@ -339,6 +498,12 @@ const resetAllForge = () => {
infoStore.emblemParts.forEach((part) => {
configStore.config.emblems[part] = { ...configStore.defaultEmblemConfig };
});
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 配置已快速重置");
};
@@ -352,7 +517,8 @@ const resetAllForge = () => {
<span>Forge 面板</span>
<span class="text-xs font-normal text-muted-foreground">
装备 {{ summary.equipCount }} · 时装 {{ summary.avatarCount }} · 誓约
{{ summary.oathCount }} · 徽章 {{ summary.emblemCount }}
{{ summary.oathCount }} · 徽章 {{ summary.emblemCount }} · 辟邪玉
{{ summary.jadeCount }} · 其它 {{ summary.sundryCount }}
</span>
</CardTitle>
<CardDescription>Forge 进阶:子模块可编辑,统一浅色/深色模式。</CardDescription>
@@ -400,6 +566,20 @@ const resetAllForge = () => {
>
时装徽章
</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'"
@@ -433,6 +613,18 @@ const resetAllForge = () => {
>时装、徽章、等级与条目配置。</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"
@@ -483,40 +675,121 @@ const resetAllForge = () => {
<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="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"
class="space-y-3 rounded-md border p-2 md:p-3"
>
<div class="font-medium">{{ part }}</div>
<div class="truncate text-muted-foreground">
{{ equipNameById.get(equipCfg(part).id) || "未选择装备(可在装备页选择)" }}
<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
class="h-8 w-full rounded-md border border-input bg-background px-2"
:value="String(equipCfg(part).reinforceType)"
@change="
(event) =>
setEquipNumberField(
part,
'reinforceType',
(event.target as HTMLSelectElement).value,
)
"
>
<option value="0">强化</option>
<option value="1">增幅</option>
</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
class="h-8 w-full min-w-0 rounded-md border border-input bg-background px-2"
:value="equipCfg(part).enchant"
@change="
(event) => setEquipEnchant(part, (event.target as HTMLSelectElement).value)
"
>
<option value="">无</option>
<option
v-for="item in equipEnchantOptions(part)"
:key="item.id"
:value="String(item.id)"
>
{{ item.detail }}
</option>
</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>
<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>
@@ -532,6 +805,12 @@ const resetAllForge = () => {
<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"
@@ -779,6 +1058,104 @@ const resetAllForge = () => {
</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
class="h-8 min-w-0 rounded-md border border-input bg-background px-2"
:value="String(ensureJade(key).id)"
@change="(event) => setJadeId(key, (event.target as HTMLSelectElement).value)"
>
<option value="-1">无</option>
<option v-for="item in infoStore.jades" :key="item.id" :value="String(item.id)">
{{ item.name }}
</option>
</select>
<select
class="h-8 min-w-0 rounded-md border border-input bg-background px-2"
:value="String(ensureJade(key).value)"
:disabled="ensureJade(key).id < 0"
@change="(event) => setJadeValue(key, (event.target as HTMLSelectElement).value)"
>
<option value="0">无</option>
<option
v-for="item in jadeValueOptions(ensureJade(key).id)"
:key="item.id"
:value="String(item.id)"
>
{{ item.value }}
</option>
</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
class="h-8 min-w-0 rounded-md border border-input bg-background px-2"
:value="sundryValue(field.key, field.defaultValue)"
@change="
(event) => setSundryValue(field.key, (event.target as HTMLSelectElement).value)
"
>
<option
v-for="item in sundryOptions(field.key)"
:key="`${field.key}-${item.value}`"
:value="String(item.value)"
>
{{ item.name }}
</option>
</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"
@@ -815,6 +1192,8 @@ const resetAllForge = () => {
<div>誓约槽位: {{ summary.oathCount }}</div>
<div>已配置时装: {{ summary.avatarCount }}</div>
<div>已配置徽章条目: {{ summary.emblemCount }}</div>
<div>已配置辟邪玉: {{ summary.jadeCount }}</div>
<div>其它系统项目: {{ summary.sundryCount }}</div>
</CardContent>
</Card>
</div>