This commit is contained in:
2026-05-11 00:32:03 +08:00
parent 2535bec2ed
commit 10022ae048
32 changed files with 2450 additions and 203 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,77 @@
<script setup lang="ts">
import { computed } from "vue";
import type { IEquipment } from "@/api/info/type";
import { getImageURL } from "@/utils";
import { cn } from "@/lib/utils";
const props = defineProps<{
equipment?: IEquipment;
inactive?: boolean;
selected?: boolean;
useDisplayUrl?: boolean;
}>();
defineEmits<{
click: [];
preview: [];
}>();
const rarityClass = computed(() => {
switch (props.equipment?.rarity) {
case "稀有":
return "border-violet-400";
case "神器":
return "border-fuchsia-500";
case "传说":
return "border-orange-500";
case "史诗":
return "border-amber-400";
case "太初":
return "border-emerald-400";
default:
return "border-muted";
}
});
const imageUrl = computed(() => {
if (!props.equipment) return "";
return getImageURL(
props.useDisplayUrl
? props.equipment.displayUrl || props.equipment.imageUrl
: props.equipment.imageUrl,
);
});
const cellStyle = {
width: "var(--equip-cell-size, 1.75rem)",
height: "var(--equip-cell-size, 1.75rem)",
};
</script>
<template>
<button
v-if="equipment"
type="button"
:class="
cn(
'relative shrink-0 overflow-hidden rounded-sm border bg-background p-0 leading-none transition-colors',
rarityClass,
selected
? 'after:pointer-events-none after:absolute after:inset-0 after:rounded-[3px] after:shadow-[inset_0_0_0_2px_hsl(var(--primary)),0_0_0_1px_hsl(var(--background))]'
: 'hover:border-primary/60',
inactive ? 'opacity-60' : '',
)
"
:style="cellStyle"
@click="$emit('click')"
@mouseenter="$emit('preview')"
>
<img :src="imageUrl" :alt="equipment.name" class="size-full object-contain" />
<div v-if="inactive" class="absolute inset-0 bg-black/45" />
</button>
<div
v-else
class="shrink-0 rounded-sm border border-dashed border-muted-foreground/30 bg-muted/15"
:style="cellStyle"
/>
</template>
@@ -0,0 +1,76 @@
<script setup lang="ts">
import { computed } from "vue";
import { Hammer, ShieldCheck, Sparkles } from "lucide-vue-next";
import { useConfigStore, useInfoStore } from "@/stores";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
const configStore = useConfigStore();
const infoStore = useInfoStore();
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;
return {
equipCount,
avatarCount,
oathCount,
emblemSlots: infoStore.emblemParts.length,
};
});
</script>
<template>
<div class="grid grid-cols-1 gap-4 lg:grid-cols-3">
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2">
<Hammer class="size-4" />
装备打造
</CardTitle>
<CardDescription>已迁移基础统计,后续补齐增幅/强化/锻造编辑器。</CardDescription>
</CardHeader>
<CardContent class="space-y-2 text-sm text-muted-foreground">
<div>已配置装备: {{ summary.equipCount }}</div>
<div>誓约槽位: {{ summary.oathCount }}</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2">
<Sparkles class="size-4" />
时装与徽章
</CardTitle>
<CardDescription>保留旧版数据结构,逐步替换为 shadcn-vue 控件。</CardDescription>
</CardHeader>
<CardContent class="space-y-2 text-sm text-muted-foreground">
<div>已配置时装: {{ summary.avatarCount }}</div>
<div>徽章位类型: {{ summary.emblemSlots }}</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2">
<ShieldCheck class="size-4" />
助手导入
</CardTitle>
<CardDescription>沿用助手识别能力快速回填打造数据。</CardDescription>
</CardHeader>
<CardContent>
<router-link to="/dcalc/dnfhelper">
<Button variant="outline">前往助手导入</Button>
</router-link>
</CardContent>
</Card>
</div>
</template>
@@ -0,0 +1,105 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { LoaderCircle, RefreshCcw } from "lucide-vue-next";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
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 { useInfoStore } from "@/stores";
const props = defineProps<{
alter: string;
}>();
const route = useRoute();
const router = useRouter();
const infoStore = useInfoStore();
const loading = ref(false);
const loadError = ref("");
const tabs = [
{ label: "装备", key: "equipment" },
{ label: "技能", key: "skill" },
{ label: "打造", key: "forge" },
] as const;
const activeTab = computed(() => {
const path = route.path;
if (path.endsWith("/skill")) return "skill";
if (path.endsWith("/forge")) return "forge";
return "equipment";
});
const loadCharacter = async () => {
loading.value = true;
loadError.value = "";
try {
await infoStore.createCharacter(props.alter);
} catch (error) {
loadError.value = error instanceof Error ? error.message : "角色数据加载失败";
} finally {
loading.value = false;
}
};
const switchTab = (tab: (typeof tabs)[number]["key"]) => {
router.push(`/dcalc/character/${props.alter}/${tab}`);
};
onMounted(() => {
loadCharacter();
});
watch(
() => props.alter,
() => {
loadCharacter();
},
);
</script>
<template>
<section class="mx-auto flex min-h-full w-full max-w-7xl flex-col gap-4 px-4 py-6 md:px-8">
<Card class="flex-none">
<CardHeader>
<CardTitle class="flex items-center justify-between">
<span>角色配置 · {{ alter }}</span>
<Button variant="outline" size="sm" :disabled="loading" @click="loadCharacter">
<RefreshCcw :class="{ 'animate-spin': loading }" />
刷新
</Button>
</CardTitle>
<CardDescription
>此页面承接旧版 character 模块,逐步迁移装备/技能/打造功能。</CardDescription
>
</CardHeader>
<CardContent class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<Tabs :model-value="activeTab" class="gap-0">
<TabsList>
<TabsTrigger
v-for="tab in tabs"
:key="tab.key"
:value="tab.key"
@click="switchTab(tab.key)"
>
{{ tab.label }}
</TabsTrigger>
</TabsList>
</Tabs>
<div class="text-sm text-muted-foreground">
<span v-if="loading" class="inline-flex items-center gap-2">
<LoaderCircle class="size-4 animate-spin" />
正在同步角色数据...
</span>
<span v-else-if="loadError">{{ loadError }}</span>
<span v-else>数据源已就绪,可继续编辑配置。</span>
</div>
</CardContent>
</Card>
<div class="min-w-0 flex-1">
<router-view />
</div>
</section>
</template>
@@ -0,0 +1,54 @@
<script setup lang="ts">
import { computed, ref } from "vue";
import { Search } from "lucide-vue-next";
import { useInfoStore } from "@/stores";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
const infoStore = useInfoStore();
const keyword = ref("");
const skills = computed(() => {
const source = infoStore.skills ?? [];
const text = keyword.value.trim().toLowerCase();
if (!text) return source;
return source.filter((skill) => {
return skill.name.toLowerCase().includes(text) || skill.type.toLowerCase().includes(text);
});
});
</script>
<template>
<Card>
<CardHeader>
<CardTitle>技能面板</CardTitle>
<CardDescription>与旧版技能页等价的基础浏览能力,后续补齐 VP/符文配置。</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<div class="relative">
<Search class="text-muted-foreground absolute left-2.5 top-2.5 size-4" />
<Input v-model="keyword" class="pl-8" placeholder="搜索技能名 / 类型" />
</div>
<div class="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3">
<Card v-for="skill in skills" :key="skill.id" size="sm">
<CardHeader>
<CardTitle class="text-sm">{{ skill.name }}</CardTitle>
<CardDescription>{{ skill.type }} · 最高等级 {{ skill.maxLv }}</CardDescription>
</CardHeader>
<CardContent class="flex items-center gap-3 pt-0">
<img :src="skill.icon" :alt="skill.name" class="size-11 rounded-md object-cover" />
<div class="text-xs text-muted-foreground">
<div>可学等级: {{ skill.maxLearnLv }}</div>
<div>学习等级: {{ skill.learnLv }}</div>
</div>
</CardContent>
</Card>
</div>
<div v-if="skills.length === 0" class="py-10 text-center text-sm text-muted-foreground">
当前筛选条件没有匹配技能。
</div>
</CardContent>
</Card>
</template>