This commit is contained in:
2026-05-28 13:16:24 +08:00
parent 6f07c8bc15
commit b539cdb42f
34 changed files with 1118 additions and 779 deletions
@@ -4,6 +4,12 @@ import type { IResultSkill } from "@/api/calc/type";
import { useConfigStore, useInfoStore } from "@/stores";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Input } from "@/components/ui/input";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import Tabs from "@/components/ui/tabs/Tabs.vue";
@@ -240,6 +246,7 @@ const equippedParts = computed(() =>
/* ─── Change sources ─── */
const changeSources = computed(() => configStore.changeSourceLogs.slice(0, 8));
const clearChangeSources = () => configStore.clearChangeSource();
const leftPanelOpenSections = ["equipped", "stats", "suit", "equipped-list"];
</script>
<template>
@@ -248,152 +255,166 @@ const clearChangeSources = () => configStore.clearChangeSource();
<div
class="flex w-full flex-none flex-col gap-3 lg:w-72 lg:overflow-y-auto lg:overflow-x-hidden"
>
<!-- Equipment / Oath grid -->
<Card>
<CardContent class="space-y-2 p-3">
<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>
<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>
</CardContent>
</Card>
<CardContent class="py-2">
<Accordion type="multiple" :default-value="leftPanelOpenSections" class="w-full">
<AccordionItem value="equipped">
<AccordionTrigger>穿戴概览</AccordionTrigger>
<AccordionContent>
<div class="space-y-2 px-1">
<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>
<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>
</AccordionContent>
</AccordionItem>
<!-- Character stats -->
<Card v-if="resultState?.info?.length">
<CardHeader class="pb-1 pt-3">
<CardTitle class="text-xs text-muted-foreground">角色属性</CardTitle>
</CardHeader>
<CardContent class="pb-3">
<div class="grid grid-cols-2 gap-x-3 gap-y-0.5 text-xs">
<template v-for="info in resultState.info" :key="info.id">
<div
class="flex justify-between gap-1"
:class="
typeof info.value === 'string' && info.value.includes('/') ? 'col-span-2' : ''
"
>
<span class="shrink-0 text-muted-foreground">{{ info.name }}</span>
<span class="text-right font-medium text-emerald-600">{{ info.value }}</span>
</div>
</template>
</div>
</CardContent>
</Card>
<AccordionItem v-if="resultState?.info?.length" value="stats">
<AccordionTrigger>角色属性</AccordionTrigger>
<AccordionContent>
<div class="grid grid-cols-2 gap-x-3 gap-y-0.5 px-1 text-xs">
<template v-for="info in resultState.info" :key="info.id">
<div
class="flex justify-between gap-1"
:class="
typeof info.value === 'string' && info.value.includes('/')
? 'col-span-2'
: ''
"
>
<span class="shrink-0 text-muted-foreground">{{ info.name }}</span>
<span class="text-right font-medium text-emerald-600">{{ info.value }}</span>
</div>
</template>
</div>
</AccordionContent>
</AccordionItem>
<!-- Suit info + oath skills -->
<Card v-if="suit || oathSuit">
<CardContent class="space-y-1 px-3 py-2 text-xs">
<div v-if="suit" class="flex items-center gap-1.5">
<img
:src="getImageURL(`/equipment${suit.imageUrl}`)"
alt=""
class="size-5 object-contain"
/>
<span :class="rarityColor(suit.rarity)" class="flex-1">{{ suit.name }} 套装</span>
<span class="shrink-0 text-muted-foreground">{{ suit.rarity }}</span>
<span class="shrink-0 font-medium">{{ suit.point }}点</span>
</div>
<div v-if="oathSuit" class="flex items-center gap-1.5">
<img
:src="getImageURL(`/equipment${oathSuit.imageUrl}`)"
alt=""
class="size-5 object-contain"
/>
<span :class="rarityColor(oathSuit.rarity)" class="flex-1"
>{{ oathSuit.name }} 誓约</span
>
<span class="shrink-0 text-muted-foreground">{{ oathSuit.rarity }}</span>
<span class="shrink-0 font-medium">{{ oathSuit.point }}点</span>
</div>
<!-- Oath skill selector -->
<div v-if="oathSkillsSorted.length" class="flex items-center justify-center gap-4 pt-1">
<button
v-for="skill in oathSkillsSorted"
:key="skill.id"
type="button"
:disabled="!skill.enable"
class="rounded-full transition-opacity"
:class="[
skill.enable ? 'cursor-pointer opacity-100' : 'cursor-not-allowed opacity-40',
skill.active ? 'ring-2 ring-amber-400 ring-offset-1' : '',
]"
@click="selectOathSkill(skill)"
>
<img
:src="getImageURL(skill.imageUrl)"
:alt="skill.name"
class="size-8 rounded-full object-contain"
/>
</button>
</div>
</CardContent>
</Card>
<AccordionItem v-if="suit || oathSuit" value="suit">
<AccordionTrigger>套装与誓约</AccordionTrigger>
<AccordionContent>
<div class="space-y-1 px-1 text-xs">
<div v-if="suit" class="flex items-center gap-1.5">
<img
:src="getImageURL(`/equipment${suit.imageUrl}`)"
alt=""
class="size-5 object-contain"
/>
<span :class="rarityColor(suit.rarity)" class="flex-1"
>{{ suit.name }} 套装</span
>
<span class="shrink-0 text-muted-foreground">{{ suit.rarity }}</span>
<span class="shrink-0 font-medium">{{ suit.point }}点</span>
</div>
<div v-if="oathSuit" class="flex items-center gap-1.5">
<img
:src="getImageURL(`/equipment${oathSuit.imageUrl}`)"
alt=""
class="size-5 object-contain"
/>
<span :class="rarityColor(oathSuit.rarity)" class="flex-1"
>{{ oathSuit.name }} 誓约</span
>
<span class="shrink-0 text-muted-foreground">{{ oathSuit.rarity }}</span>
<span class="shrink-0 font-medium">{{ oathSuit.point }}点</span>
</div>
<div
v-if="oathSkillsSorted.length"
class="flex items-center justify-center gap-4 pt-1"
>
<button
v-for="skill in oathSkillsSorted"
:key="skill.id"
type="button"
:disabled="!skill.enable"
class="rounded-full transition-opacity"
:class="[
skill.enable
? 'cursor-pointer opacity-100'
: 'cursor-not-allowed opacity-40',
skill.active ? 'ring-2 ring-amber-400 ring-offset-1' : '',
]"
@click="selectOathSkill(skill)"
>
<img
:src="getImageURL(skill.imageUrl)"
:alt="skill.name"
class="size-8 rounded-full object-contain"
/>
</button>
</div>
</div>
</AccordionContent>
</AccordionItem>
<!-- Equipped items list -->
<Card v-if="equippedParts.length">
<CardHeader class="pb-1 pt-3">
<CardTitle class="text-xs text-muted-foreground">穿戴装备信息</CardTitle>
</CardHeader>
<CardContent class="pb-2">
<ScrollArea class="max-h-64">
<div class="space-y-0.5 text-xs">
<div
v-for="part in equippedParts"
:key="part"
class="flex items-center gap-1.5 rounded px-1 py-0.5 hover:bg-muted/40"
>
<img
:src="getImageURL(`/equipment/part/${part}.png`)"
:alt="part"
class="size-4 shrink-0 object-contain"
/>
<img
v-if="adaptation(part) > 0"
:src="getImageURL(`/equipment/adaptation/${adaptation(part)}.png`)"
alt=""
class="h-2.5 w-3 shrink-0 object-contain"
/>
<span
v-if="(configStore.config.equips[part]?.reinforce ?? 0) > 0"
class="shrink-0 font-medium"
:class="
configStore.config.equips[part]?.reinforceType === 1
? 'text-[#ef28c1]'
: 'text-[#68d5ed]'
"
>+{{ configStore.config.equips[part]?.reinforce }}</span
>
<span
v-if="(configStore.config.equips[part]?.refine ?? 0) > 0"
class="shrink-0 text-[#9879da]"
>({{ configStore.config.equips[part]?.refine }})</span
>
<span
class="truncate"
:class="
rarityColor(equById(configStore.config.equips[part]?.id ?? '')?.rarity ?? '')
"
>{{ equById(configStore.config.equips[part]?.id ?? "")?.name ?? "—" }}</span
>
</div>
</div>
<template #scrollbar>
<ScrollBar orientation="vertical" />
</template>
</ScrollArea>
<AccordionItem v-if="equippedParts.length" value="equipped-list">
<AccordionTrigger>穿戴装备信息</AccordionTrigger>
<AccordionContent>
<ScrollArea class="max-h-64 px-1">
<div class="space-y-0.5 text-xs">
<div
v-for="part in equippedParts"
:key="part"
class="flex items-center gap-1.5 rounded px-1 py-0.5 hover:bg-muted/40"
>
<img
:src="getImageURL(`/equipment/part/${part}.png`)"
:alt="part"
class="size-4 shrink-0 object-contain"
/>
<img
v-if="adaptation(part) > 0"
:src="getImageURL(`/equipment/adaptation/${adaptation(part)}.png`)"
alt=""
class="h-2.5 w-3 shrink-0 object-contain"
/>
<span
v-if="(configStore.config.equips[part]?.reinforce ?? 0) > 0"
class="shrink-0 font-medium"
:class="
configStore.config.equips[part]?.reinforceType === 1
? 'text-[#ef28c1]'
: 'text-[#68d5ed]'
"
>+{{ configStore.config.equips[part]?.reinforce }}</span
>
<span
v-if="(configStore.config.equips[part]?.refine ?? 0) > 0"
class="shrink-0 text-[#9879da]"
>({{ configStore.config.equips[part]?.refine }})</span
>
<span
class="truncate"
:class="
rarityColor(
equById(configStore.config.equips[part]?.id ?? '')?.rarity ?? '',
)
"
>{{ equById(configStore.config.equips[part]?.id ?? "")?.name ?? "—" }}</span
>
</div>
</div>
<template #scrollbar>
<ScrollBar orientation="vertical" />
</template>
</ScrollArea>
</AccordionContent>
</AccordionItem>
</Accordion>
</CardContent>
</Card>
</div>