768 lines
28 KiB
Vue
768 lines
28 KiB
Vue
<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 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="inline-flex flex-wrap rounded-md border p-1">
|
||
<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 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-3">
|
||
<div
|
||
v-for="part in parts"
|
||
:key="part"
|
||
class="grid grid-cols-1 gap-2 rounded-md border p-3 text-sm md:grid-cols-[7rem_1fr_6rem_6rem_6rem_auto] md:items-center"
|
||
>
|
||
<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"
|
||
placeholder="强化"
|
||
@update:model-value="(value) => setNumberField(part, 'reinforce', String(value))"
|
||
/>
|
||
<Input
|
||
:model-value="String(equipCfg(part).refine)"
|
||
type="number"
|
||
min="0"
|
||
class="h-8"
|
||
placeholder="锻造"
|
||
@update:model-value="(value) => setNumberField(part, 'refine', String(value))"
|
||
/>
|
||
<Input
|
||
:model-value="String(equipCfg(part).adaptation)"
|
||
type="number"
|
||
min="0"
|
||
class="h-8"
|
||
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-3">
|
||
<div
|
||
v-for="part in oathParts"
|
||
:key="part"
|
||
class="grid grid-cols-1 gap-2 rounded-md border p-3 text-sm md:grid-cols-[6rem_1fr_7rem_auto] md:items-center"
|
||
>
|
||
<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"
|
||
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-3 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" 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-3 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" 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>
|