1
This commit was merged in pull request #1.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { defineRequest } from "../common";
|
||||
import type { IAdventureInfo, ICharacterInfo, IHelperInfo, ISkillInfo } from "./type";
|
||||
import type { IAdventureInfo, IAlterInfo, ICharacterInfo, IHelperInfo, ISkillInfo } from "./type";
|
||||
|
||||
export default defineRequest((request) => {
|
||||
return {
|
||||
@@ -7,6 +7,10 @@ export default defineRequest((request) => {
|
||||
const res = await request.get<IAdventureInfo[]>("/adventure");
|
||||
return res.data;
|
||||
},
|
||||
async alterByName(name: string) {
|
||||
const res = await request.get<IAlterInfo>("/adventure", { params: { name } });
|
||||
return res.data;
|
||||
},
|
||||
async createToken(alter: string, equVersion?: string) {
|
||||
const res = await request.get<string>(`/token/get/${alter}`, { params: { equVersion } });
|
||||
return res.data;
|
||||
@@ -23,6 +27,10 @@ export default defineRequest((request) => {
|
||||
const res = await request.get(`/dnfhelper/${uid}/`);
|
||||
return res.data;
|
||||
},
|
||||
async getConfigFromHelperAlter(uid: string, alter: string) {
|
||||
const res = await request.get(`/dnfhelper/${uid}/${alter}/`);
|
||||
return res.data;
|
||||
},
|
||||
async getSkillFromCode(code: string) {
|
||||
const res = await request.post(`/skillTree/`, { code });
|
||||
return res.data;
|
||||
|
||||
@@ -572,7 +572,7 @@ const leftPanelOpenSections = ["equipped", "stats", "suit", "equipped-list"];
|
||||
<td class="px-1.5 py-1.5">
|
||||
<div class="flex items-center gap-2">
|
||||
<img
|
||||
:src="skill.icon"
|
||||
:src="getImageURL(skill.icon)"
|
||||
:alt="skill.name"
|
||||
class="size-4 rounded-sm object-cover sm:size-5"
|
||||
/>
|
||||
|
||||
@@ -6,12 +6,21 @@ import {
|
||||
ChevronRight,
|
||||
ChevronsLeft,
|
||||
ChevronsRight,
|
||||
Settings2,
|
||||
Sparkles,
|
||||
} from "lucide-vue-next";
|
||||
import { useConfigStore, useInfoStore } from "@/stores";
|
||||
import type { IEquipment, ISuit } from "@/api/info/type";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/components/ui/sheet";
|
||||
import Tabs from "@/components/ui/tabs/Tabs.vue";
|
||||
import TabsList from "@/components/ui/tabs/TabsList.vue";
|
||||
import TabsTrigger from "@/components/ui/tabs/TabsTrigger.vue";
|
||||
@@ -54,7 +63,8 @@ const suitId = ref("");
|
||||
const oathSuitId = ref("");
|
||||
const suitRarity = ref<string>("");
|
||||
const oathSuitRarity = ref<string>("");
|
||||
const sidePanelTab = ref<SidePanelTab>("detail");
|
||||
const sidePanelTab = ref<SidePanelTab>("equipped");
|
||||
const triggerOptionsOpen = ref(false);
|
||||
const equippedPart = ref("");
|
||||
const equippedOathPart = ref("0");
|
||||
const previewEquipKey = ref("");
|
||||
@@ -75,7 +85,8 @@ const panelLayoutVars = computed(() => ({
|
||||
|
||||
const iconWrapStyle = computed(() => ({
|
||||
...panelLayoutVars.value,
|
||||
width: "var(--equip-grid-width)",
|
||||
maxWidth: "var(--equip-grid-width)",
|
||||
width: "100%",
|
||||
}));
|
||||
|
||||
const isOathEquipment = (equip?: IEquipment) =>
|
||||
@@ -805,25 +816,29 @@ const toggleOathRarity = (value: unknown) => {
|
||||
const next = normalizeRarityValue(value);
|
||||
oathSuitRarity.value = !next || oathSuitRarity.value === next ? "" : next;
|
||||
};
|
||||
|
||||
const setTriggerOption = (id: number, value: string) => {
|
||||
configStore.config.options[id.toString()] = Number.parseInt(value, 10);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="grid min-h-full min-w-0 gap-4 pb-6 md:h-full md:pb-0 md:grid-cols-[minmax(0,1fr)_320px]"
|
||||
class="grid min-h-full min-w-0 gap-4 pb-6 lg:h-full lg:pb-0 lg:grid-cols-[minmax(0,1fr)_300px] xl:grid-cols-[minmax(0,1fr)_320px]"
|
||||
>
|
||||
<div
|
||||
class="min-h-0 min-w-0 grid items-stretch gap-4 md:h-full [grid-template-columns:repeat(auto-fit,minmax(min(100%,18rem),1fr))]"
|
||||
class="min-h-0 min-w-0 grid items-stretch gap-4 lg:h-full [grid-template-columns:repeat(auto-fit,minmax(min(100%,18rem),1fr))]"
|
||||
:style="panelLayoutVars"
|
||||
>
|
||||
<div class="min-h-0 min-w-0 md:h-full">
|
||||
<div class="min-h-0 min-w-0 lg:h-full">
|
||||
<Card
|
||||
class="w-full md:flex md:h-full md:flex-col md:overflow-hidden"
|
||||
class="w-full lg:flex lg:h-full lg:flex-col lg:overflow-hidden"
|
||||
:style="panelLayoutVars"
|
||||
>
|
||||
<!-- <CardHeader class="pb-3">
|
||||
<CardTitle>武器/称号/宠物/秘宝</CardTitle>
|
||||
</CardHeader> -->
|
||||
<CardContent class="space-y-4 md:min-h-0 md:flex-1 md:overflow-y-auto">
|
||||
<CardContent class="space-y-4 overflow-x-hidden lg:min-h-0 lg:flex-1 lg:overflow-y-auto">
|
||||
<Tabs v-model="weaponTab" class="gap-0" size="sm">
|
||||
<TabsList>
|
||||
<TabsTrigger value="weapon">武器</TabsTrigger>
|
||||
@@ -832,7 +847,7 @@ const toggleOathRarity = (value: unknown) => {
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<div class="overflow-x-hidden">
|
||||
<div class="mx-auto flex flex-wrap content-start gap-1" :style="iconWrapStyle">
|
||||
<template v-if="weaponTab === 'weapon'">
|
||||
<EquipmentCell
|
||||
@@ -878,6 +893,50 @@ const toggleOathRarity = (value: unknown) => {
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
<Sheet v-model:open="triggerOptionsOpen">
|
||||
<SheetTrigger as-child>
|
||||
<Button
|
||||
v-if="infoStore.options.length"
|
||||
size="icon-sm"
|
||||
variant="outline"
|
||||
title="装备触发条件"
|
||||
>
|
||||
<Settings2 class="size-4" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent class="w-full overflow-y-auto sm:max-w-md">
|
||||
<SheetHeader>
|
||||
<SheetTitle>装备触发条件</SheetTitle>
|
||||
<SheetDescription>配置会立即写回当前角色计算条件。</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div class="grid gap-3 px-4">
|
||||
<label
|
||||
v-for="option in infoStore.options"
|
||||
:key="option.id"
|
||||
class="grid gap-1 text-sm"
|
||||
>
|
||||
<span class="text-muted-foreground">{{ option.name }}</span>
|
||||
<select
|
||||
class="h-9 rounded-md border border-input bg-background px-2"
|
||||
:value="String(configStore.config.options[option.id.toString()] ?? 0)"
|
||||
@change="
|
||||
(event) =>
|
||||
setTriggerOption(option.id, (event.target as HTMLSelectElement).value)
|
||||
"
|
||||
>
|
||||
<option
|
||||
v-for="(item, index) in option.options"
|
||||
:key="`${option.id}-${index}`"
|
||||
:value="String(index)"
|
||||
>
|
||||
{{ item }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
<ColorSwatchPickerRoot
|
||||
v-if="equipTab === 'suit'"
|
||||
class="ml-auto flex items-center gap-2"
|
||||
@@ -908,7 +967,7 @@ const toggleOathRarity = (value: unknown) => {
|
||||
|
||||
<template v-if="equipTab === 'suit'">
|
||||
<div
|
||||
class="mx-auto flex w-max max-w-full flex-nowrap overflow-x-auto pb-1"
|
||||
class="mx-auto flex flex-wrap content-start gap-1 overflow-x-hidden pb-1"
|
||||
:style="iconWrapStyle"
|
||||
>
|
||||
<button
|
||||
@@ -941,7 +1000,7 @@ const toggleOathRarity = (value: unknown) => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<div class="overflow-x-hidden">
|
||||
<div class="mx-auto flex flex-wrap content-start gap-1" :style="iconWrapStyle">
|
||||
<EquipmentCell
|
||||
v-for="(equip, index) in suitGrid"
|
||||
@@ -966,7 +1025,7 @@ const toggleOathRarity = (value: unknown) => {
|
||||
>
|
||||
{{ section.title }}
|
||||
</div> -->
|
||||
<div class="overflow-x-auto">
|
||||
<div class="overflow-x-hidden">
|
||||
<div class="mx-auto flex flex-wrap content-start gap-1" :style="iconWrapStyle">
|
||||
<EquipmentCell
|
||||
v-for="(equip, index) in section.items"
|
||||
@@ -991,21 +1050,21 @@ const toggleOathRarity = (value: unknown) => {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="min-h-0 min-w-0 md:h-full">
|
||||
<div class="min-h-0 min-w-0 lg:h-full">
|
||||
<Card
|
||||
class="w-full md:flex md:h-full md:flex-col md:overflow-hidden"
|
||||
class="w-full lg:flex lg:h-full lg:flex-col lg:overflow-hidden"
|
||||
:style="panelLayoutVars"
|
||||
>
|
||||
<!-- <CardHeader>
|
||||
<CardTitle>誓约</CardTitle>
|
||||
</CardHeader> -->
|
||||
<CardContent class="space-y-4 md:min-h-0 md:flex-1 md:overflow-y-auto">
|
||||
<CardContent class="space-y-4 overflow-x-hidden lg:min-h-0 lg:flex-1 lg:overflow-y-auto">
|
||||
<div class="flex items-center gap-2">
|
||||
<Tabs v-model="oathTab" class="gap-0">
|
||||
<TabsList>
|
||||
<TabsTrigger value="suit">誓约</TabsTrigger>
|
||||
<!-- <TabsTrigger value="universal">通宝</TabsTrigger>
|
||||
<TabsTrigger value="other">其他</TabsTrigger> -->
|
||||
<TabsTrigger value="universal">通宝</TabsTrigger>
|
||||
<TabsTrigger value="other">其他</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
@@ -1039,7 +1098,7 @@ const toggleOathRarity = (value: unknown) => {
|
||||
|
||||
<template v-if="oathTab === 'suit'">
|
||||
<div
|
||||
class="mx-auto flex w-max max-w-full flex-nowrap overflow-x-auto pb-1"
|
||||
class="mx-auto flex flex-wrap content-start gap-1 overflow-x-hidden pb-1"
|
||||
:style="iconWrapStyle"
|
||||
>
|
||||
<button
|
||||
@@ -1084,7 +1143,7 @@ const toggleOathRarity = (value: unknown) => {
|
||||
>
|
||||
{{ section.title }}
|
||||
</div> -->
|
||||
<div class="overflow-x-auto">
|
||||
<div class="overflow-x-hidden">
|
||||
<div class="mx-auto flex flex-wrap content-start gap-1" :style="iconWrapStyle">
|
||||
<EquipmentCell
|
||||
v-for="(equip, index) in section.items"
|
||||
@@ -1109,7 +1168,7 @@ const toggleOathRarity = (value: unknown) => {
|
||||
</template>
|
||||
|
||||
<template v-else-if="oathTab === 'universal'">
|
||||
<div class="overflow-x-auto">
|
||||
<div class="overflow-x-hidden">
|
||||
<div class="mx-auto flex flex-wrap content-start gap-1" :style="iconWrapStyle">
|
||||
<EquipmentCell
|
||||
v-for="(equip, index) in oathUniversalGrid"
|
||||
@@ -1123,10 +1182,16 @@ const toggleOathRarity = (value: unknown) => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!oathUniversalGrid.some(Boolean)"
|
||||
class="py-6 text-center text-xs text-muted-foreground"
|
||||
>
|
||||
当前暂无誓约通宝数据
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="overflow-x-auto">
|
||||
<div class="overflow-x-hidden">
|
||||
<div class="mx-auto flex flex-wrap content-start gap-1" :style="iconWrapStyle">
|
||||
<EquipmentCell
|
||||
v-for="(equip, index) in oathOtherGrid"
|
||||
@@ -1140,14 +1205,20 @@ const toggleOathRarity = (value: unknown) => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!oathOtherGrid.some(Boolean)"
|
||||
class="py-6 text-center text-xs text-muted-foreground"
|
||||
>
|
||||
当前暂无其它誓约数据
|
||||
</div>
|
||||
</template>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="min-h-0 space-y-4 md:h-full md:self-stretch">
|
||||
<Card class="md:flex md:h-full md:flex-col md:overflow-hidden">
|
||||
<div class="min-h-0 space-y-4 lg:h-full lg:self-stretch">
|
||||
<Card class="lg:flex lg:h-full lg:flex-col lg:overflow-hidden">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-base">
|
||||
<Tabs v-model="sidePanelTab" class="gap-0">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { PanelRight } from "lucide-vue-next";
|
||||
import { FileInput, PanelRight, WandSparkles } from "lucide-vue-next";
|
||||
import api from "@/api";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/components/ui/sheet";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import Tabs from "@/components/ui/tabs/Tabs.vue";
|
||||
import TabsList from "@/components/ui/tabs/TabsList.vue";
|
||||
import TabsTrigger from "@/components/ui/tabs/TabsTrigger.vue";
|
||||
import { useConfigStore, useInfoStore } from "@/stores";
|
||||
import { useDnfHelperStore } from "@/stores/dnfhelper";
|
||||
import type { IConfigEmblem, IConfigEquip, IConfigOath } from "@/stores/config";
|
||||
import type { IHelperInfo } from "@/api/info/type";
|
||||
import ResultPanel from "@/pages/Dcalc/Character/Result/Index.vue";
|
||||
const props = defineProps<{
|
||||
alter: string;
|
||||
@@ -17,14 +31,50 @@ const route = useRoute();
|
||||
const router = useRouter();
|
||||
const infoStore = useInfoStore();
|
||||
const configStore = useConfigStore();
|
||||
const helperStore = useDnfHelperStore();
|
||||
const loading = ref(false);
|
||||
const loadError = ref("");
|
||||
const isExecuting = ref(false);
|
||||
const isMobileLayout = ref(false);
|
||||
const isResultSheetOpen = ref(false);
|
||||
const helperImportOpen = ref(false);
|
||||
const skillImportOpen = ref(false);
|
||||
const helperRoleImportOpen = ref(false);
|
||||
const helperUid = ref("");
|
||||
const skillCode = ref("");
|
||||
const helperImportLoading = ref(false);
|
||||
const skillImportLoading = ref(false);
|
||||
const importStatus = ref("");
|
||||
const importError = ref("");
|
||||
const helperRole = ref<IHelperInfo["roleList"][number]>();
|
||||
|
||||
let mediaQuery: MediaQueryList | null = null;
|
||||
|
||||
type AvatarConfig = Record<
|
||||
string,
|
||||
{
|
||||
id: string;
|
||||
enchant: string;
|
||||
emblem_0: string;
|
||||
emblem_1: string;
|
||||
option: string;
|
||||
}
|
||||
>;
|
||||
|
||||
interface HelperConfigPayload {
|
||||
equips?: Record<string, IConfigEquip>;
|
||||
avatar?: AvatarConfig;
|
||||
avatars?: AvatarConfig;
|
||||
avatarLink?: string;
|
||||
emblems?: Record<string, IConfigEmblem>;
|
||||
fogOathLv?: unknown;
|
||||
oaths?: Record<string, IConfigOath>;
|
||||
}
|
||||
|
||||
interface SkillImportPayload {
|
||||
skills?: typeof configStore.config.skills;
|
||||
}
|
||||
|
||||
const updateMobileLayout = () => {
|
||||
isMobileLayout.value = mediaQuery?.matches ?? false;
|
||||
if (!isMobileLayout.value) {
|
||||
@@ -55,6 +105,7 @@ const loadCharacter = async () => {
|
||||
loadError.value = "";
|
||||
try {
|
||||
await infoStore.createCharacter(props.alter);
|
||||
prepareHelperRoleImport();
|
||||
} catch (error) {
|
||||
loadError.value = error instanceof Error ? error.message : "角色数据加载失败";
|
||||
} finally {
|
||||
@@ -66,6 +117,103 @@ const switchTab = (tab: (typeof tabs)[number]["key"]) => {
|
||||
router.push(`/dcalc/character/${props.alter}/${tab}`);
|
||||
};
|
||||
|
||||
const resetImportMessage = () => {
|
||||
importStatus.value = "";
|
||||
importError.value = "";
|
||||
};
|
||||
|
||||
const applyHelperConfig = (payload: HelperConfigPayload) => {
|
||||
if (payload.equips) configStore.config.equips = payload.equips;
|
||||
if (payload.avatars || payload.avatar) {
|
||||
configStore.config.avatar = payload.avatars ?? payload.avatar ?? configStore.config.avatar;
|
||||
}
|
||||
if (payload.avatarLink !== undefined) configStore.config.avatarLink = payload.avatarLink;
|
||||
if (payload.emblems) configStore.config.emblems = payload.emblems;
|
||||
if (payload.fogOathLv !== undefined) configStore.config.sundry.fog = payload.fogOathLv;
|
||||
if (payload.oaths) configStore.config.oaths = payload.oaths;
|
||||
configStore.pushChangeSource("Import", "助手打造配置已导入");
|
||||
};
|
||||
|
||||
const importHelperConfig = async () => {
|
||||
resetImportMessage();
|
||||
const uid = helperUid.value.trim();
|
||||
if (!uid) {
|
||||
importError.value = "请输入助手社区 ID。";
|
||||
return;
|
||||
}
|
||||
|
||||
helperImportLoading.value = true;
|
||||
try {
|
||||
const payload = (await api.getConfigFromHelper(uid)) as HelperConfigPayload;
|
||||
applyHelperConfig(payload);
|
||||
helperImportOpen.value = false;
|
||||
helperUid.value = "";
|
||||
importStatus.value = "打造导入成功。";
|
||||
} catch (error) {
|
||||
importError.value = error instanceof Error ? error.message : "打造导入失败。";
|
||||
} finally {
|
||||
helperImportLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const importHelperRoleConfig = async () => {
|
||||
resetImportMessage();
|
||||
const uid = helperStore.helperInfo?.id;
|
||||
const roleId = helperRole.value?.roleId;
|
||||
if (!uid || !roleId) {
|
||||
importError.value = "未找到可导入的助手角色。";
|
||||
return;
|
||||
}
|
||||
|
||||
helperImportLoading.value = true;
|
||||
try {
|
||||
const payload = (await api.getConfigFromHelperAlter(uid, roleId)) as HelperConfigPayload;
|
||||
applyHelperConfig(payload);
|
||||
helperRoleImportOpen.value = false;
|
||||
importStatus.value = "助手角色打造导入成功。";
|
||||
} catch (error) {
|
||||
importError.value = error instanceof Error ? error.message : "助手角色打造导入失败。";
|
||||
} finally {
|
||||
helperImportLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const importSkillConfig = async () => {
|
||||
resetImportMessage();
|
||||
const code = skillCode.value.trim();
|
||||
if (!code) {
|
||||
importError.value = "请输入技能加点代码。";
|
||||
return;
|
||||
}
|
||||
|
||||
skillImportLoading.value = true;
|
||||
try {
|
||||
const payload = (await api.getSkillFromCode(code)) as SkillImportPayload;
|
||||
if (payload.skills) {
|
||||
configStore.config.skills = payload.skills;
|
||||
configStore.pushChangeSource("Import", "技能加点代码已导入");
|
||||
}
|
||||
skillImportOpen.value = false;
|
||||
skillCode.value = "";
|
||||
importStatus.value = "技能导入成功。";
|
||||
} catch (error) {
|
||||
importError.value = error instanceof Error ? error.message : "技能导入失败。";
|
||||
} finally {
|
||||
skillImportLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const prepareHelperRoleImport = () => {
|
||||
const roleId = route.query.roleId;
|
||||
if (typeof roleId !== "string") return;
|
||||
|
||||
const target = helperStore.helperInfo?.roleList.find((item) => item.roleId === roleId);
|
||||
if (!target) return;
|
||||
|
||||
helperRole.value = target;
|
||||
helperRoleImportOpen.value = true;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadCharacter();
|
||||
window.addEventListener("beforeunload", configStore.saveConfig);
|
||||
@@ -81,6 +229,13 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => route.query.roleId,
|
||||
() => {
|
||||
prepareHelperRoleImport();
|
||||
},
|
||||
);
|
||||
|
||||
watch(showGlobalResultPanel, (show) => {
|
||||
if (!show) {
|
||||
isResultSheetOpen.value = false;
|
||||
@@ -125,6 +280,79 @@ onUnmounted(() => {
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Sheet v-model:open="helperImportOpen">
|
||||
<SheetTrigger as-child>
|
||||
<Button size="sm" variant="outline" @click="resetImportMessage">
|
||||
<FileInput class="size-4" />
|
||||
打造导入
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent class="w-full overflow-y-auto sm:max-w-md">
|
||||
<SheetHeader>
|
||||
<SheetTitle>打造导入</SheetTitle>
|
||||
<SheetDescription>助手社区 ID</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div class="grid gap-3 px-4">
|
||||
<Input v-model="helperUid" placeholder="社区 ID" @keyup.enter="importHelperConfig" />
|
||||
<p v-if="importError" class="text-sm text-destructive">{{ importError }}</p>
|
||||
</div>
|
||||
<SheetFooter>
|
||||
<Button :disabled="helperImportLoading" @click="importHelperConfig">
|
||||
{{ helperImportLoading ? "导入中..." : "导入" }}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
<Sheet v-model:open="skillImportOpen">
|
||||
<SheetTrigger as-child>
|
||||
<Button size="sm" variant="outline" @click="resetImportMessage">
|
||||
<WandSparkles class="size-4" />
|
||||
技能导入
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent class="w-full overflow-y-auto sm:max-w-md">
|
||||
<SheetHeader>
|
||||
<SheetTitle>技能导入</SheetTitle>
|
||||
<SheetDescription>技能加点代码</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div class="grid gap-3 px-4">
|
||||
<Textarea v-model="skillCode" class="min-h-28" placeholder="技能代码" />
|
||||
<p v-if="importError" class="text-sm text-destructive">{{ importError }}</p>
|
||||
</div>
|
||||
<SheetFooter>
|
||||
<Button :disabled="skillImportLoading" @click="importSkillConfig">
|
||||
{{ skillImportLoading ? "导入中..." : "导入" }}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
<p v-if="importStatus" class="text-xs text-muted-foreground">{{ importStatus }}</p>
|
||||
</div>
|
||||
|
||||
<Sheet v-model:open="helperRoleImportOpen">
|
||||
<SheetContent class="w-full overflow-y-auto sm:max-w-md">
|
||||
<SheetHeader>
|
||||
<SheetTitle>助手角色导入</SheetTitle>
|
||||
<SheetDescription>{{ helperRole?.roleName || "助手角色" }}</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div class="grid gap-2 px-4 text-sm">
|
||||
<div class="rounded-md border p-3">
|
||||
<div class="font-medium">{{ helperRole?.roleJob }}</div>
|
||||
<div class="text-muted-foreground">名望 {{ helperRole?.fame ?? "-" }}</div>
|
||||
</div>
|
||||
<p v-if="importError" class="text-sm text-destructive">{{ importError }}</p>
|
||||
</div>
|
||||
<SheetFooter>
|
||||
<Button :disabled="helperImportLoading" @click="importHelperRoleConfig">
|
||||
{{ helperImportLoading ? "导入中..." : "导入" }}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
<div
|
||||
:class="[
|
||||
'grid min-h-0 flex-1 grid-cols-1 gap-4',
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import type { IResultSkill } from "@/api/calc/type";
|
||||
import { useConfigStore, useInfoStore } from "@/stores";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||
import Tabs from "@/components/ui/tabs/Tabs.vue";
|
||||
import TabsList from "@/components/ui/tabs/TabsList.vue";
|
||||
import TabsTrigger from "@/components/ui/tabs/TabsTrigger.vue";
|
||||
import EquList from "@/components/dnf/Equipment/List/index.vue";
|
||||
import OathList from "@/components/dnf/Equipment/Oath/index.vue";
|
||||
import { useConfigStore, useInfoStore } from "@/stores";
|
||||
|
||||
const configStore = useConfigStore();
|
||||
const infoStore = useInfoStore();
|
||||
@@ -26,8 +18,6 @@ const equippedOathPart = ref("0");
|
||||
|
||||
const resultState = computed(() => configStore.result.state);
|
||||
const isLoading = computed(() => configStore.result.isLoading);
|
||||
const isBufferClass = computed(() => !!resultState.value?.buffer);
|
||||
const standard = computed(() => infoStore.standard);
|
||||
|
||||
const resultError = computed(() => {
|
||||
const error = configStore.result.error;
|
||||
@@ -35,57 +25,26 @@ const resultError = computed(() => {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
});
|
||||
|
||||
const retryCalc = async () => {
|
||||
await configStore.result.execute();
|
||||
};
|
||||
|
||||
const setStandard = () => {
|
||||
if (!resultState.value?.uuid) return;
|
||||
infoStore.setStandard(resultState.value, configStore.skillCountConfig);
|
||||
};
|
||||
|
||||
const clearStandard = () => {
|
||||
infoStore.setStandard(undefined);
|
||||
};
|
||||
|
||||
const getSkillCountEntry = (skill: IResultSkill) => {
|
||||
const name = skill.name;
|
||||
const mode = skill.mode;
|
||||
const existed = configStore.skillCountConfig.find(
|
||||
(item) => item.name === name && item.mode === mode,
|
||||
);
|
||||
if (existed) return existed;
|
||||
const created = { name, mode, count: 1 };
|
||||
configStore.skillCountConfig.push(created);
|
||||
return created;
|
||||
};
|
||||
|
||||
const getSkillCount = (skill: IResultSkill) =>
|
||||
Math.max(0, Number(getSkillCountEntry(skill).count) || 0);
|
||||
|
||||
const getStandardSkill = (skill: IResultSkill) =>
|
||||
standard.value?.skills.find((item) => item.name === skill.name && item.mode === skill.mode);
|
||||
|
||||
const getStandardSkillCount = (skill: IResultSkill) => {
|
||||
const count = standard.value?.skillCount.find(
|
||||
(item) => item.name === skill.name && item.mode === skill.mode,
|
||||
)?.count;
|
||||
return Number(count ?? 0);
|
||||
};
|
||||
|
||||
const skills = computed(() => resultState.value?.skills ?? []);
|
||||
|
||||
const totalWeightedDamage = computed(() =>
|
||||
skills.value.reduce((sum, item) => sum + item.damage * getSkillCount(item), 0),
|
||||
skills.value.reduce((sum, item) => {
|
||||
const count = configStore.skillCountConfig.find(
|
||||
(config) => config.name === item.name && config.mode === item.mode,
|
||||
)?.count;
|
||||
return sum + item.damage * Math.max(0, Number(count ?? 1) || 0);
|
||||
}, 0),
|
||||
);
|
||||
|
||||
const totalStandardDamage = computed(() => {
|
||||
if (!standard.value) return 0;
|
||||
return skills.value.reduce((sum, item) => {
|
||||
const base = getStandardSkill(item);
|
||||
if (!base) return sum;
|
||||
return sum + base.damage * getStandardSkillCount(item);
|
||||
}, 0);
|
||||
const hasVisibleResult = computed(
|
||||
() => (resultState.value?.skills?.length ?? 0) > 0 || (resultState.value?.info?.length ?? 0) > 0,
|
||||
);
|
||||
|
||||
const statusText = computed(() => {
|
||||
if (isLoading.value) return "计算中...";
|
||||
if (resultError.value) return resultError.value;
|
||||
if (!hasVisibleResult.value) return "暂无结果";
|
||||
return "";
|
||||
});
|
||||
|
||||
const formatNumber = (value: number) => {
|
||||
@@ -93,152 +52,54 @@ const formatNumber = (value: number) => {
|
||||
if (Math.abs(value) >= 100000000) return `${(value / 100000000).toFixed(2)}e`;
|
||||
return value.toLocaleString("zh-CN", { maximumFractionDigits: 2 });
|
||||
};
|
||||
|
||||
const totalDiffLabel = computed(() => {
|
||||
if (!totalStandardDamage.value) return formatNumber(totalWeightedDamage.value);
|
||||
const diff =
|
||||
((totalWeightedDamage.value - totalStandardDamage.value) / totalStandardDamage.value) * 100;
|
||||
return `${diff >= 0 ? "+" : ""}${diff.toFixed(2)}%`;
|
||||
});
|
||||
|
||||
const totalDiffClass = computed(() => {
|
||||
if (!totalStandardDamage.value) return "text-foreground";
|
||||
return totalWeightedDamage.value >= totalStandardDamage.value
|
||||
? "text-emerald-600"
|
||||
: "text-red-600";
|
||||
});
|
||||
|
||||
const hasVisibleResult = computed(
|
||||
() => (resultState.value?.skills?.length ?? 0) > 0 || (resultState.value?.info?.length ?? 0) > 0,
|
||||
);
|
||||
|
||||
const changeSources = computed(() => configStore.changeSourceLogs.slice(0, 8));
|
||||
|
||||
const clearChangeSources = () => {
|
||||
configStore.clearChangeSource();
|
||||
};
|
||||
|
||||
const defaultOpenSections = ["overview", "equipped"];
|
||||
|
||||
const statusText = computed(() => {
|
||||
if (isLoading.value) return "计算中...";
|
||||
if (resultError.value) return resultError.value;
|
||||
if (!hasVisibleResult.value) return "暂无结果,请先编辑配置。";
|
||||
if (isBufferClass.value) return "请前往计算详情查看。";
|
||||
return "";
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full min-h-0 flex-col gap-3">
|
||||
<Card>
|
||||
<CardContent class="py-2">
|
||||
<Accordion type="multiple" :default-value="defaultOpenSections" class="w-full">
|
||||
<AccordionItem value="overview">
|
||||
<AccordionTrigger>结果概览</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div class="space-y-3 px-1">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button size="sm" variant="outline" class="h-8 px-2.5" @click="setStandard"
|
||||
>设为基准</Button
|
||||
>
|
||||
<Button size="sm" variant="outline" class="h-8 px-2.5" @click="clearStandard"
|
||||
>清空基准</Button
|
||||
>
|
||||
</div>
|
||||
<Card class="shrink-0">
|
||||
<CardContent class="p-3">
|
||||
<div class="rounded-md border border-border/60 px-3 py-2">
|
||||
<div class="text-xs text-muted-foreground">总伤害</div>
|
||||
<div
|
||||
v-if="statusText"
|
||||
class="mt-1 text-sm"
|
||||
:class="resultError ? 'text-destructive' : 'text-muted-foreground'"
|
||||
>
|
||||
{{ statusText }}
|
||||
</div>
|
||||
<div v-else class="mt-1 truncate text-xl font-semibold tabular-nums">
|
||||
{{ formatNumber(totalWeightedDamage) }}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div
|
||||
v-if="isLoading || resultError || !hasVisibleResult || isBufferClass"
|
||||
class="rounded-md border border-border/60 px-3 py-2 text-center text-sm"
|
||||
:class="resultError ? 'text-destructive' : 'text-muted-foreground'"
|
||||
>
|
||||
{{ statusText }}
|
||||
</div>
|
||||
<Card class="min-h-0 flex-1">
|
||||
<CardContent class="flex h-full min-h-0 flex-col gap-2 p-3 text-xs">
|
||||
<Tabs :model-value="displayTab" class="shrink-0">
|
||||
<TabsList class="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="equ" @click="displayTab = 'equ'">装备</TabsTrigger>
|
||||
<TabsTrigger value="oath" @click="displayTab = 'oath'">誓约</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="flex items-center justify-between rounded-md border border-border/60 px-3 py-2"
|
||||
>
|
||||
<span class="text-sm text-muted-foreground">合计伤害</span>
|
||||
<span class="text-sm font-semibold" :class="totalDiffClass">{{
|
||||
totalDiffLabel
|
||||
}}</span>
|
||||
</div>
|
||||
<ScrollArea class="min-h-0 flex-1 rounded-md border p-2">
|
||||
<div v-if="displayTab === 'equ'">
|
||||
<EquList
|
||||
v-model:part="equippedPart"
|
||||
:detail="configStore.config.equips"
|
||||
:with-sub-weapon="infoStore.infos?.alter?.includes('vegabond') ?? false"
|
||||
:with-pet="true"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<OathList v-model:part="equippedOathPart" :detail="configStore.config.oaths" />
|
||||
</div>
|
||||
|
||||
<div v-if="resultError" class="flex justify-center">
|
||||
<Button size="sm" variant="outline" @click="retryCalc">重试计算</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="equipped">
|
||||
<AccordionTrigger>穿戴信息</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div class="space-y-2 px-1 text-xs">
|
||||
<Tabs :model-value="displayTab">
|
||||
<TabsList class="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="equ" @click="displayTab = 'equ'">装备</TabsTrigger>
|
||||
<TabsTrigger value="oath" @click="displayTab = 'oath'">誓约</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<ScrollArea class="max-h-56 rounded-md border p-2">
|
||||
<div v-if="displayTab === 'equ'">
|
||||
<EquList
|
||||
v-model:part="equippedPart"
|
||||
:detail="configStore.config.equips"
|
||||
:with-sub-weapon="infoStore.infos?.alter?.includes('vegabond') ?? false"
|
||||
:with-pet="true"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<OathList v-model:part="equippedOathPart" :detail="configStore.config.oaths" />
|
||||
</div>
|
||||
<template #scrollbar>
|
||||
<ScrollBar orientation="vertical" />
|
||||
</template>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="changes">
|
||||
<AccordionTrigger>配置变更来源</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div class="space-y-2 px-1">
|
||||
<div class="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>最近触发计算的操作记录</span>
|
||||
<Button size="sm" variant="ghost" class="h-7 px-2" @click="clearChangeSources"
|
||||
>清空</Button
|
||||
>
|
||||
</div>
|
||||
|
||||
<div v-if="changeSources.length === 0" class="text-xs text-muted-foreground">
|
||||
暂无变更来源记录。
|
||||
</div>
|
||||
|
||||
<ScrollArea v-else class="max-h-40 rounded-md border border-border/60 p-2">
|
||||
<div class="space-y-1">
|
||||
<div
|
||||
v-for="item in changeSources"
|
||||
:key="item.id"
|
||||
class="rounded-md border border-border/60 bg-muted/20 px-2 py-1 text-xs"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="font-medium">{{ item.source }}</span>
|
||||
<span class="text-muted-foreground">{{ item.time }}</span>
|
||||
</div>
|
||||
<div class="text-muted-foreground">{{ item.detail }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #scrollbar>
|
||||
<ScrollBar orientation="vertical" />
|
||||
</template>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
<template #scrollbar>
|
||||
<ScrollBar orientation="vertical" />
|
||||
</template>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,7 @@ const noticeType = ref<"error" | "info">("info");
|
||||
const openUpPickerSlot = ref<number | null>(null);
|
||||
const rightPanelTab = ref<"detail" | "result">("result");
|
||||
const leftPanelWidthClass = computed(() =>
|
||||
rightPanelTab.value === "result" ? "xl:w-[460px]" : "xl:w-[550px]",
|
||||
rightPanelTab.value === "result" ? "xl:w-[360px]" : "xl:w-[550px]",
|
||||
);
|
||||
|
||||
/* ─── Constants ─── */
|
||||
@@ -193,6 +193,19 @@ const upOptionsForSlot = (slotIdx: number) => {
|
||||
.sort((a, b) => a.learnLv - b.learnLv);
|
||||
};
|
||||
|
||||
const firstAwakeSkill = computed(() =>
|
||||
infoStore.skills.find((item) => item.learnLv === 50 && item.type === "active" && item.bind),
|
||||
);
|
||||
const secondAwakeSkill = computed(() =>
|
||||
infoStore.skills.find((item) => item.learnLv === 85 && item.type === "active" && item.bind),
|
||||
);
|
||||
const canBindAwake = computed(() => !!firstAwakeSkill.value || !!secondAwakeSkill.value);
|
||||
|
||||
const setBindAwake = (level: 50 | 85) => {
|
||||
configStore.config.bindAwake = level;
|
||||
track(`觉醒绑定切换为 ${level === 50 ? "一觉" : "二觉"}`);
|
||||
};
|
||||
|
||||
const setUPSlotSkill = (slotIdx: number, nextId: string) => {
|
||||
const slot = upSlots.value[slotIdx];
|
||||
if (!slot) return;
|
||||
@@ -337,6 +350,33 @@ const resetAllSkills = () => {
|
||||
: '',
|
||||
]"
|
||||
/>
|
||||
<div
|
||||
v-if="skill.learnLv === 100 && skill.bind && canBindAwake"
|
||||
class="awake-bind-panel"
|
||||
@click.stop
|
||||
>
|
||||
<button
|
||||
v-if="firstAwakeSkill"
|
||||
class="awake-bind-btn"
|
||||
:class="{ active: configStore.config.bindAwake === 50 }"
|
||||
title="绑定一觉"
|
||||
@click.stop="setBindAwake(50)"
|
||||
>
|
||||
<img :src="getImageURL(firstAwakeSkill.icon)" :alt="firstAwakeSkill.name" />
|
||||
</button>
|
||||
<button
|
||||
v-if="secondAwakeSkill"
|
||||
class="awake-bind-btn"
|
||||
:class="{ active: configStore.config.bindAwake === 85 }"
|
||||
title="绑定二觉"
|
||||
@click.stop="setBindAwake(85)"
|
||||
>
|
||||
<img
|
||||
:src="getImageURL(secondAwakeSkill.icon)"
|
||||
:alt="secondAwakeSkill.name"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="activeSkillId === skill.id.toString()"
|
||||
class="action-row"
|
||||
@@ -399,6 +439,36 @@ const resetAllSkills = () => {
|
||||
: '',
|
||||
]"
|
||||
/>
|
||||
<div
|
||||
v-if="cell.learnLv === 100 && cell.bind && canBindAwake"
|
||||
class="awake-bind-panel"
|
||||
@click.stop
|
||||
>
|
||||
<button
|
||||
v-if="firstAwakeSkill"
|
||||
class="awake-bind-btn"
|
||||
:class="{ active: configStore.config.bindAwake === 50 }"
|
||||
title="绑定一觉"
|
||||
@click.stop="setBindAwake(50)"
|
||||
>
|
||||
<img
|
||||
:src="getImageURL(firstAwakeSkill.icon)"
|
||||
:alt="firstAwakeSkill.name"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
v-if="secondAwakeSkill"
|
||||
class="awake-bind-btn"
|
||||
:class="{ active: configStore.config.bindAwake === 85 }"
|
||||
title="绑定二觉"
|
||||
@click.stop="setBindAwake(85)"
|
||||
>
|
||||
<img
|
||||
:src="getImageURL(secondAwakeSkill.icon)"
|
||||
:alt="secondAwakeSkill.name"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="activeSkillId === cell.id.toString()"
|
||||
class="action-row"
|
||||
@@ -844,6 +914,41 @@ const resetAllSkills = () => {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.awake-bind-panel {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 50%;
|
||||
z-index: 8;
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
transform: translateX(-50%);
|
||||
border-radius: 4px;
|
||||
background: color-mix(in oklab, var(--background) 88%, transparent);
|
||||
padding: 2px;
|
||||
box-shadow: 0 0 0 1px color-mix(in oklab, var(--border) 80%, transparent);
|
||||
}
|
||||
|
||||
.awake-bind-btn {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 1px solid color-mix(in oklab, var(--border) 70%, transparent);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
opacity: 0.62;
|
||||
}
|
||||
|
||||
.awake-bind-btn.active {
|
||||
border-color: var(--primary);
|
||||
opacity: 1;
|
||||
box-shadow: 0 0 0 1px var(--primary);
|
||||
}
|
||||
|
||||
.awake-bind-btn img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* 4 action buttons at the bottom */
|
||||
.action-row {
|
||||
position: absolute;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import api from "@/api";
|
||||
import { Button } from "@/components/ui/button";
|
||||
// import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import {
|
||||
@@ -26,6 +28,7 @@ const props = defineProps<{
|
||||
const { list } = toRefs(props);
|
||||
|
||||
const helperStore = useDnfHelperStore();
|
||||
const router = useRouter();
|
||||
|
||||
const sortedList = computed(() => {
|
||||
return [...list.value].sort((left, right) => right.fame - left.fame);
|
||||
@@ -34,6 +37,12 @@ const sortedList = computed(() => {
|
||||
const formatFame = (fame: number) => {
|
||||
return new Intl.NumberFormat("zh-CN").format(fame);
|
||||
};
|
||||
|
||||
const importRole = async (item: (typeof props.list)[number]) => {
|
||||
const alter = await api.alterByName(item.roleJob);
|
||||
if (!alter?.value) return;
|
||||
await router.push(`/dcalc/character/${alter.value}/equipment?roleId=${item.roleId}`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -64,7 +73,7 @@ const formatFame = (fame: number) => {
|
||||
<CardHeader class="pb-3">
|
||||
<CardTitle class="text-base flex">
|
||||
{{ item.roleName }}
|
||||
<Button class="ml-auto" size="sm" variant="outline" @click="$router.push('/dcalc/home')"
|
||||
<Button class="ml-auto" size="sm" variant="outline" @click="importRole(item)"
|
||||
>导入</Button
|
||||
>
|
||||
</CardTitle>
|
||||
|
||||
@@ -102,7 +102,7 @@ onMounted(() => {
|
||||
:disabled="!child.open"
|
||||
@click="openAlter(child)"
|
||||
>
|
||||
<item class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<img
|
||||
:src="
|
||||
getImageURL(`characters/${adventure.name}/${child.name}/material/Seicing_3.png`)
|
||||
@@ -111,7 +111,7 @@ onMounted(() => {
|
||||
class="size-8 rounded-sm object-cover"
|
||||
/>
|
||||
<span class="truncate">{{ child.title }}</span>
|
||||
</item>
|
||||
</div>
|
||||
<ArrowRight class="size-3" />
|
||||
</Button>
|
||||
</CardContent>
|
||||
|
||||
Reference in New Issue
Block a user