dev
This commit is contained in:
File diff suppressed because it is too large
Load Diff
+77
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from "vue-router";
|
||||
import { ArrowRight, Search } from "lucide-vue-next";
|
||||
import api from "@/api";
|
||||
import type { IAdventureInfo, IAlterInfo } from "@/api/info/type";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
// CardDescription,
|
||||
// CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { getImageURL } from "@/utils";
|
||||
const router = useRouter();
|
||||
const adventures = ref<IAdventureInfo[]>([]);
|
||||
const loading = ref(false);
|
||||
const helperId = ref("");
|
||||
|
||||
const loadAdventures = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
adventures.value = await api.adventures();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const openAlter = (alter: IAlterInfo) => {
|
||||
if (alter.comment === "首页") {
|
||||
window.open("https://dnftools.com", "_blank", "noopener");
|
||||
return;
|
||||
}
|
||||
if (alter.name === "sponsor") {
|
||||
if (alter.url) {
|
||||
window.open(alter.url, "_blank", "noopener");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!alter.open) {
|
||||
return;
|
||||
}
|
||||
const target = alter.value || alter.name;
|
||||
router.push(`/dcalc/character/${target}/equipment`);
|
||||
};
|
||||
|
||||
const gotoHelper = () => {
|
||||
if (!helperId.value.trim()) {
|
||||
router.push("/dcalc/dnfhelper");
|
||||
return;
|
||||
}
|
||||
router.push("/dcalc/dnfhelper");
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadAdventures();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="mx-auto flex min-h-full w-full max-w-7xl flex-col gap-6 px-4 py-6 md:px-8">
|
||||
<Card class="h-auto flex-none">
|
||||
<CardContent class="flex min-h-14 flex-col gap-3 justify-center items-center">
|
||||
<img src="/favicon.ico" alt="DNF Tools" class="size-30 rounded-sm object-cover" />
|
||||
<div class="text-sm text-muted-foreground">
|
||||
点击下方职业卡片进入配置,或使用助手导入快速同步角色。
|
||||
</div>
|
||||
|
||||
<Button type="submit" variant="outline" @click="gotoHelper">
|
||||
<Search />
|
||||
助手导入
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div v-if="loading" class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<Card v-for="idx in 6" :key="idx">
|
||||
<CardHeader>
|
||||
<Skeleton class="h-5 w-40" />
|
||||
<Skeleton class="h-4 w-24" />
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-2">
|
||||
<Skeleton class="h-8 w-full" v-for="row in 4" :key="row" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div v-else class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<Card v-for="adventure in adventures" :key="adventure.name">
|
||||
<CardHeader>
|
||||
<CardTitle>{{ adventure.title }}</CardTitle>
|
||||
<!-- <CardDescription>{{ adventure.children.length }} 个分支职业</CardDescription> -->
|
||||
</CardHeader>
|
||||
<CardContent class="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
v-for="child in adventure.children.filter((item) => item.title)"
|
||||
:key="child.name"
|
||||
variant="outline"
|
||||
class="justify-between p-1.5 !h-auto"
|
||||
:disabled="!child.open"
|
||||
@click="openAlter(child)"
|
||||
>
|
||||
<item class="flex items-center gap-2">
|
||||
<img
|
||||
:src="
|
||||
getImageURL(`characters/${adventure.name}/${child.name}/material/Seicing_3.png`)
|
||||
"
|
||||
:alt="child.title"
|
||||
class="size-8 rounded-sm object-cover"
|
||||
/>
|
||||
<span class="truncate">{{ child.title }}</span>
|
||||
</item>
|
||||
<ArrowRight class="size-3" />
|
||||
</Button>
|
||||
</CardContent>
|
||||
<!-- <CardFooter class="text-xs text-muted-foreground">
|
||||
{{ adventure.name }}
|
||||
</CardFooter> -->
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="text-center text-xs text-muted-foreground">
|
||||
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">
|
||||
赣ICP备2023003492号
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
@@ -1,16 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import AppSidebar from "@/components/AppSidebar.vue";
|
||||
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
||||
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
|
||||
// import DcalcBreadcrumb from "@/components/DcalcBreadcrumb.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SidebarProvider>
|
||||
<div class="flex w-full">
|
||||
<AppSidebar />
|
||||
<SidebarInset>
|
||||
<router-view />
|
||||
<!-- router-view -->
|
||||
</SidebarInset>
|
||||
<div class="flex h-svh w-full overflow-hidden">
|
||||
<div class="flex w-full min-h-0">
|
||||
<AppSidebar />
|
||||
<SidebarInset class="flex min-w-0 flex-1 min-h-0 flex-col overflow-hidden">
|
||||
<!-- <DcalcBreadcrumb /> -->
|
||||
<div class="flex items-center border-b px-2 py-2 md:hidden">
|
||||
<SidebarTrigger class="h-8 w-8" />
|
||||
</div>
|
||||
<div class="flex flex-1 overflow-y-auto overflow-x-hidden">
|
||||
<router-view />
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user