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
@@ -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 { 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 w-full max-w-7xl flex-col gap-4 px-4 py-6 md:px-8">
<Card>
<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">
<div class="inline-flex gap-2">
<Button
v-for="tab in tabs"
:key="tab.key"
:variant="activeTab === tab.key ? 'default' : 'outline'"
size="sm"
@click="switchTab(tab.key)"
>
{{ tab.label }}
</Button>
</div>
<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>
<router-view />
</section>
</template>
+71 -44
View File
@@ -1,10 +1,19 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { ref } from "vue";
import { GalleryVerticalEnd, Shuffle, Grip } from "lucide-vue-next";
import { GalleryVerticalEnd, Shuffle } from "lucide-vue-next";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Field,
FieldDescription,
@@ -21,56 +30,74 @@ const props = defineProps<{
}>();
const helperId = ref("");
const helperStore = useDnfHelperStore();
const onSubmit = async (e: Event) => {
e.preventDefault();
await useDnfHelperStore().refleshHelperInfo(helperId.value);
await helperStore.refreshHelperInfo(helperId.value);
};
const onRandom = () => {
const seed = `${Math.floor(1000000 + Math.random() * 8999999)}`;
helperId.value = seed;
};
</script>
<template>
<div :class="cn('flex flex-col gap-6', props.class)">
<form @submit="onSubmit">
<FieldGroup>
<div class="flex flex-col items-center gap-2 text-center">
<a href="#" class="flex flex-col items-center gap-2 font-medium">
<div class="flex size-8 items-center justify-center rounded-md">
<GalleryVerticalEnd class="size-6" />
</div>
<span class="sr-only">DNF计算器</span>
</a>
<!-- <h1 class="text-xl font-bold">输入助手ID</h1> -->
<!-- <FieldDescription>请输入助手的社区ID </FieldDescription> -->
</div>
<Field class="flex flex-col items-center">
<FieldLabel for="helperId" class="sr-only"> 助手ID </FieldLabel>
<InputGroup>
<InputGroupInput
id="helperId"
type="number"
placeholder="助手社区ID"
required
v-model="helperId"
/>
<InputGroupAddon>
<Shuffle />
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<Kbd>Enter</Kbd>
</InputGroupAddon>
</InputGroup>
<!-- <FieldDescription class="text-center">
Didn't receive the code? <a href="#">Resend</a>
</FieldDescription> -->
</Field>
<Field>
<Button class="self-center w-sm!" type="submit"> 确认 </Button>
</Field>
</FieldGroup>
</form>
<FieldSeparator class="w-sm self-center">没有助手ID</FieldSeparator>
<FieldDescription class="px-6 text-center">
<Button size="sm" variant="outline"> <Grip /> 选择职业 </Button>
<div :class="cn('flex flex-col gap-6 self-center', props.class)">
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2 text-lg">
<GalleryVerticalEnd class="size-5" />
绑定助手社区ID
</CardTitle>
<CardDescription>
输入助手社区ID后可同步角色列表与基础打造信息。该流程与旧版导入逻辑保持一致。
</CardDescription>
</CardHeader>
<CardContent>
<form class="space-y-4" @submit="onSubmit">
<FieldGroup>
<Field class="flex flex-col gap-2">
<FieldLabel for="helperId">助手ID</FieldLabel>
<InputGroup>
<InputGroupInput
id="helperId"
v-model="helperId"
type="text"
inputmode="numeric"
placeholder="例如: 12345678"
required
/>
<InputGroupAddon as-child>
<button type="button" class="inline-flex items-center" @click="onRandom">
<Shuffle class="size-4" />
</button>
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<Kbd>Enter</Kbd>
</InputGroupAddon>
</InputGroup>
<FieldDescription>点击回车或确认按钮即可触发同步。</FieldDescription>
</Field>
</FieldGroup>
<CardFooter class="grid grid-cols-1 gap-2 px-0 sm:grid-cols-2">
<Button type="submit" :disabled="helperStore.loading">
{{ helperStore.loading ? "同步中..." : "确认并同步" }}
</Button>
<Button variant="outline" type="button" @click="$router.push('/dcalc/home')">
选择职业
</Button>
</CardFooter>
</form>
</CardContent>
</Card>
<FieldSeparator>提示</FieldSeparator>
<FieldDescription class="px-1 text-sm">
助手识别基于OCR,存在一定误差,导入后建议在“装备/打造”页复核关键词条。
</FieldDescription>
</div>
</template>
@@ -1,7 +1,9 @@
<script setup lang="ts">
import { useDnfHelperStore } from "@/stores/dnfhelper";
import { Card, CardContent } from "@/components/ui/card";
import Bind from "./bind.vue";
import List from "./list.vue";
import Processing from "./processing.vue";
const helperStore = useDnfHelperStore();
@@ -9,10 +11,17 @@ const listInfo = computed(() => helperStore.helperInfo?.roleList ?? []);
</script>
<template>
<template v-if="listInfo.length == 0">
<Bind class="self-center justify-center w-fit !max-w-sm h-full" />
</template>
<template v-else>
<List :list="listInfo" />
</template>
<div class="mx-auto flex h-full w-full flex-col gap-4 flex-1">
<Card v-if="helperStore.errorMessage" class="border-destructive/50 bg-destructive/5">
<CardContent class="py-3 text-sm text-destructive">{{
helperStore.errorMessage
}}</CardContent>
</Card>
<Processing v-if="helperStore.loading" />
<Bind v-else-if="listInfo.length === 0" class="mx-auto my-auto" />
<List v-else :list="listInfo" />
</div>
</template>
+41 -22
View File
@@ -1,15 +1,17 @@
<script setup lang="ts">
import Button from "@/components/ui/button/Button.vue";
import { computed } from "vue";
import { Button } from "@/components/ui/button";
// import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
Card,
CardContent,
// CardDescription,
CardFooter,
CardDescription,
// CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { useDnfHelperStore } from "@/stores/dnfhelper";
import { RefreshCcw, Grip } from "lucide-vue-next";
import { Grip, RefreshCcw } from "lucide-vue-next";
const props = defineProps<{
list: {
@@ -24,41 +26,58 @@ const props = defineProps<{
const { list } = toRefs(props);
const helperStore = useDnfHelperStore();
const sortedList = computed(() => {
return [...list.value].sort((left, right) => right.fame - left.fame);
});
const formatFame = (fame: number) => {
return new Intl.NumberFormat("zh-CN").format(fame);
};
</script>
<template>
<header
class="bg-background sticky top-0 flex h-16 shrink-0 items-center gap-2 border-b px-4 justify-between"
class="sticky top-0 z-10 flex shrink-0 flex-col gap-3 border-b bg-background/95 px-1 py-2 backdrop-blur md:flex-row md:items-center md:justify-between px-6"
>
<div>
<h1 class="text-lg font-semibold">角色列表</h1>
<p class="text-sm text-muted-foreground">共 {{ list.length }} 个角色</p>
<p class="text-sm text-muted-foreground">共 {{ sortedList.length }} 个角色</p>
</div>
<div class="flex gap-4">
<Button variant="outline"> <Grip /> 其他职业 </Button>
<div class="flex flex-wrap gap-2">
<Button variant="outline" @click="$router.push('/dcalc/home')"> <Grip /> 选择职业 </Button>
<Button
variant="outline"
size="icon"
aria-label="Submit"
@click="helperStore.refleshHelperInfo(helperStore.helperInfo?.id)"
:disabled="helperStore.loading"
@click="helperStore.refreshHelperInfo(helperStore.helperInfo?.id)"
>
<RefreshCcw />
<RefreshCcw :class="{ 'animate-spin': helperStore.loading }" />
刷新
</Button>
<Button variant="secondary" @click="helperStore.logout">取消绑定</Button>
</div>
</header>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 p-5 box-border">
<template v-for="item in list" :key="item.roleId">
<div class="grid grid-cols-1 gap-4 py-4 px-6 sm:grid-cols-2 xl:grid-cols-4">
<template v-for="item in sortedList" :key="item.roleId">
<Card class="w-full">
<CardHeader>
<CardTitle>{{ item.roleJob }}</CardTitle>
<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
>
</CardTitle>
<CardDescription class="flex justify-between">
<span>
{{ item.roleJob }}
</span>
<span>名望: {{ formatFame(item.fame) }}</span>
</CardDescription>
</CardHeader>
<CardContent class="flex items-center justify-center h-sm">
<img :src="item.avatarLink" alt="avatar" class="w-auto h-sm rounded-full object-cover" />
<CardContent class="flex items-center justify-center">
<img :src="item.avatarLink" :alt="item.roleName" class="h-40 w-auto rounded" />
</CardContent>
<CardFooter class="flex justify-between">
<span>{{ item.roleName }}</span>
<span>{{ item.fame }} </span>
</CardFooter>
</Card>
</template>
</div>