This commit is contained in:
2026-05-07 11:29:45 +08:00
parent cf5c178ee4
commit f8607d0b38
10 changed files with 326 additions and 130 deletions
+3
View File
@@ -219,9 +219,12 @@ export interface Attribute {
export interface IHelperInfo { export interface IHelperInfo {
id: string; id: string;
avatar: string; avatar: string;
name: string;
roleList: { roleList: {
roleId: string; roleId: string;
roleName: string; roleName: string;
roleJob: string;
avatarLink: string; avatarLink: string;
fame: number;
}[]; }[];
} }
+12 -6
View File
@@ -31,6 +31,7 @@ import {
SidebarMenuItem, SidebarMenuItem,
SidebarTrigger, SidebarTrigger,
} from "@/components/ui/sidebar"; } from "@/components/ui/sidebar";
import { useDnfHelperStore } from "@/stores/dnfhelper";
const props = withDefaults(defineProps<SidebarProps>(), { const props = withDefaults(defineProps<SidebarProps>(), {
collapsible: "icon", collapsible: "icon",
@@ -43,12 +44,17 @@ const toggleDarkMode = () => {
document.documentElement.classList.toggle("dark", isDark.value); document.documentElement.classList.toggle("dark", isDark.value);
}; };
const dnfHelperStore = useDnfHelperStore();
const userInfo = computed(() => {
return {
name: dnfHelperStore.helperInfo?.name ?? "尚未绑定助手ID",
id: dnfHelperStore.helperInfo?.id ?? "",
avatar: dnfHelperStore.helperInfo?.avatar ?? "",
};
});
const data = { const data = {
user: {
name: "尚未绑定助手ID",
email: "m@example.com",
avatar: "/avatars/shadcn.jpg",
},
navMain: [ navMain: [
{ {
title: "计算器", title: "计算器",
@@ -198,7 +204,7 @@ const data = {
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
</SidebarMenu> </SidebarMenu>
<NavUser :user="data.user" /> <NavUser :user="userInfo" />
</SidebarFooter> </SidebarFooter>
</Sidebar> </Sidebar>
</template> </template>
@@ -0,0 +1,13 @@
<script setup lang="ts">
defineProps<{
visible: boolean;
text?: string;
}>();
</script>
<template>
<div class="v-loading-mask" :class="{ 'is-active': visible }">
<div class="v-loading-spinner" />
<div v-if="text" class="v-loading-text">{{ text }}</div>
</div>
</template>
+20 -14
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { BadgeCheck, Bell, ChevronsUpDown, CreditCard, LogOut, Sparkles } from "lucide-vue-next"; import { Bell, ChevronsUpDown, LogOut } from "lucide-vue-next";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { import {
@@ -17,6 +17,7 @@ import {
SidebarMenuItem, SidebarMenuItem,
useSidebar, useSidebar,
} from "@/components/ui/sidebar"; } from "@/components/ui/sidebar";
import { useDnfHelperStore } from "@/stores/dnfhelper";
const props = defineProps<{ const props = defineProps<{
user: { user: {
@@ -29,6 +30,8 @@ const props = defineProps<{
const { user } = toRefs(props); const { user } = toRefs(props);
const { isMobile } = useSidebar(); const { isMobile } = useSidebar();
const helperStore = useDnfHelperStore();
</script> </script>
<template> <template>
@@ -41,8 +44,8 @@ const { isMobile } = useSidebar();
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground" class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
> >
<Avatar class="h-8 w-8 rounded-lg"> <Avatar class="h-8 w-8 rounded-lg">
<AvatarImage :src="user.avatar" :alt="user.name" /> <AvatarImage :src="user.avatar" :alt="user.id" />
<AvatarFallback class="rounded-lg"> Dcalc </AvatarFallback> <AvatarFallback class="rounded-lg"> DNF </AvatarFallback>
</Avatar> </Avatar>
<div class="grid flex-1 text-left text-sm leading-tight"> <div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">{{ user.name }}</span> <span class="truncate font-medium">{{ user.name }}</span>
@@ -61,7 +64,7 @@ const { isMobile } = useSidebar();
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm"> <div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar class="h-8 w-8 rounded-lg"> <Avatar class="h-8 w-8 rounded-lg">
<AvatarImage :src="user.avatar" :alt="user.name" /> <AvatarImage :src="user.avatar" :alt="user.name" />
<AvatarFallback class="rounded-lg"> Dcalc </AvatarFallback> <AvatarFallback class="rounded-lg"> DNF </AvatarFallback>
</Avatar> </Avatar>
<div class="grid flex-1 text-left text-sm leading-tight"> <div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">{{ user.name }}</span> <span class="truncate font-medium">{{ user.name }}</span>
@@ -69,32 +72,35 @@ const { isMobile } = useSidebar();
</div> </div>
</div> </div>
</DropdownMenuLabel> </DropdownMenuLabel>
<DropdownMenuSeparator /> <!-- <DropdownMenuSeparator />
<DropdownMenuGroup> <DropdownMenuGroup>
<DropdownMenuItem> <DropdownMenuItem :disabled="!helperStore.helperInfo">
<Sparkles /> <Sparkles />
Upgrade to Pro 切换绑定
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuGroup> </DropdownMenuGroup> -->
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuGroup> <DropdownMenuGroup>
<DropdownMenuItem> <!-- <DropdownMenuItem>
<BadgeCheck /> <BadgeCheck />
Account 反馈
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem> <DropdownMenuItem>
<CreditCard /> <CreditCard />
Billing Billing
</DropdownMenuItem> </DropdownMenuItem> -->
<DropdownMenuItem> <DropdownMenuItem>
<Bell /> <Bell />
Notifications 反馈
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuGroup> </DropdownMenuGroup>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem> <DropdownMenuItem
:disabled="!helperStore.helperInfo"
@click="helperStore.helperInfo = null"
>
<LogOut /> <LogOut />
Log out 取消绑定
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
+116
View File
@@ -0,0 +1,116 @@
import { h, render } from "vue";
import type { App, Directive, DirectiveBinding } from "vue";
import LoadingOverlay from "@/components/LoadingOverlay.vue";
type LoadingValue =
| boolean
| string
| {
loading?: boolean;
show?: boolean;
visible?: boolean;
text?: string;
};
type LoadingState = {
container: HTMLDivElement;
props: {
visible: boolean;
text: string;
};
hadRelativePosition: boolean;
};
type LoadingElement = HTMLElement & {
__vLoadingState__?: LoadingState;
};
function normalizeBinding(value: LoadingValue): { loading: boolean; text: string } {
if (typeof value === "string") {
return { loading: true, text: value };
}
if (typeof value === "boolean") {
return { loading: value, text: "" };
}
if (value && typeof value === "object") {
const loading = value.loading ?? value.show ?? value.visible ?? true;
return { loading: Boolean(loading), text: value.text ?? "" };
}
return { loading: false, text: "" };
}
function ensureState(el: LoadingElement): LoadingState {
if (el.__vLoadingState__) {
return el.__vLoadingState__;
}
const computedPosition = window.getComputedStyle(el).position;
const hadRelativePosition = computedPosition !== "static";
if (!hadRelativePosition) {
el.style.position = "relative";
}
const container = document.createElement("div");
el.appendChild(container);
const props = {
visible: false,
text: "",
};
render(h(LoadingOverlay, props), container);
const state: LoadingState = {
container,
props,
hadRelativePosition,
};
el.__vLoadingState__ = state;
return state;
}
function applyState(el: LoadingElement, binding: DirectiveBinding<LoadingValue>): void {
const state = ensureState(el);
const { loading, text } = normalizeBinding(binding.value);
state.props.visible = loading;
state.props.text = text;
render(h(LoadingOverlay, state.props), state.container);
}
const loadingDirective: Directive<HTMLElement, LoadingValue> = {
mounted(el, binding) {
applyState(el as LoadingElement, binding);
},
updated(el, binding) {
applyState(el as LoadingElement, binding);
},
unmounted(el) {
const loadingElement = el as LoadingElement;
const state = loadingElement.__vLoadingState__;
if (!state) {
return;
}
render(null, state.container);
state.container.remove();
if (!state.hadRelativePosition) {
loadingElement.style.position = "";
}
delete loadingElement.__vLoadingState__;
},
};
export const loadingPlugin = {
install(app: App): void {
app.directive("loading", loadingDirective);
},
};
+2
View File
@@ -1,5 +1,6 @@
import { createApp } from "vue"; import { createApp } from "vue";
import { pinia } from "@/stores"; import { pinia } from "@/stores";
import { loadingPlugin } from "@/directives/loading";
import router from "./router"; import router from "./router";
import App from "@/pages/index.vue"; import App from "@/pages/index.vue";
@@ -13,5 +14,6 @@ const app = createApp(App);
app.use(pinia); app.use(pinia);
app.use(router); app.use(router);
app.use(loadingPlugin);
app.mount(appContainer); app.mount(appContainer);
+10 -2
View File
@@ -8,6 +8,7 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { useDnfHelperStore } from "@/stores/dnfhelper";
import { RefreshCcw } from "lucide-vue-next"; import { RefreshCcw } from "lucide-vue-next";
const props = defineProps<{ const props = defineProps<{
@@ -21,6 +22,8 @@ const props = defineProps<{
}>(); }>();
const { list } = toRefs(props); const { list } = toRefs(props);
const helperStore = useDnfHelperStore();
</script> </script>
<template> <template>
@@ -31,7 +34,12 @@ const { list } = toRefs(props);
<h1 class="text-lg font-semibold">角色列表</h1> <h1 class="text-lg font-semibold">角色列表</h1>
<p class="text-sm text-muted-foreground">共 {{ list.length }} 个角色</p> <p class="text-sm text-muted-foreground">共 {{ list.length }} 个角色</p>
</div> </div>
<Button variant="outline" size="icon" aria-label="Submit"> <Button
variant="outline"
size="icon"
aria-label="Submit"
@click="helperStore.refleshHelperInfo(helperStore.helperInfo?.id)"
>
<RefreshCcw /> <RefreshCcw />
</Button> </Button>
</header> </header>
@@ -42,7 +50,7 @@ const { list } = toRefs(props);
<CardTitle>{{ item.roleJob }}</CardTitle> <CardTitle>{{ item.roleJob }}</CardTitle>
</CardHeader> </CardHeader>
<CardContent class="flex items-center justify-center h-sm"> <CardContent class="flex items-center justify-center h-sm">
<!-- <img :src="item.avatarLink" alt="avatar" class="w-auto h-sm rounded-full object-cover" /> --> <img :src="item.avatarLink" alt="avatar" class="w-auto h-sm rounded-full object-cover" />
</CardContent> </CardContent>
<CardFooter class="flex justify-between"> <CardFooter class="flex justify-between">
<span>{{ item.roleName }}</span> <span>{{ item.roleName }}</span>
+2 -2
View File
@@ -10,9 +10,9 @@ export const useDnfHelperStore = defineStore("dnfHelperStore", () => {
}, },
}); });
const refleshHelperInfo = async (uid: string) => { const refleshHelperInfo = async (uid?: string) => {
if (!uid) return;
const data = await api.getHelperList(uid); const data = await api.getHelperList(uid);
console.log("helperInfo", data);
if (data) { if (data) {
helperInfo.value = data; helperInfo.value = data;
console.log(helperInfo.value); console.log(helperInfo.value);
+146 -104
View File
@@ -1,5 +1,4 @@
@import url("https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap");
@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap');
@import "tailwindcss"; @import "tailwindcss";
@@ -10,120 +9,163 @@
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
@theme inline { @theme inline {
--font-sans: 'Geist Variable', sans-serif; --font-sans: "Geist Variable", sans-serif;
--font-heading: var(--font-sans); --font-heading: var(--font-sans);
--color-sidebar-ring: var(--sidebar-ring); --color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border); --color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent); --color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground); --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary); --color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground); --color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar); --color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5); --color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4); --color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3); --color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2); --color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1); --color-chart-1: var(--chart-1);
--color-ring: var(--ring); --color-ring: var(--ring);
--color-input: var(--input); --color-input: var(--input);
--color-border: var(--border); --color-border: var(--border);
--color-destructive: var(--destructive); --color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground); --color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent); --color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground); --color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted); --color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground); --color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary); --color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground); --color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary); --color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground); --color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover); --color-popover: var(--popover);
--color-card-foreground: var(--card-foreground); --color-card-foreground: var(--card-foreground);
--color-card: var(--card); --color-card: var(--card);
--color-foreground: var(--foreground); --color-foreground: var(--foreground);
--color-background: var(--background); --color-background: var(--background);
--radius-sm: calc(var(--radius) - 4px); --radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px); --radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius); --radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px); --radius-xl: calc(var(--radius) + 4px);
} }
:root { :root {
--background: oklch(1 0 0); --background: oklch(1 0 0);
--foreground: oklch(0.145 0 0); --foreground: oklch(0.145 0 0);
--card: oklch(1 0 0); --card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0); --card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0); --popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0); --popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0); --primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0); --primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0); --secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0); --secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0); --muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0); --muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0); --accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0); --accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325); --destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0); --border: oklch(0.922 0 0);
--input: oklch(0.922 0 0); --input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0); --ring: oklch(0.708 0 0);
--chart-1: oklch(0.87 0 0); --chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0); --chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0); --chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0); --chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0); --chart-5: oklch(0.269 0 0);
--radius: 0.625rem; --radius: 0.625rem;
--sidebar: oklch(0.985 0 0); --sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0); --sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0); --sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0); --sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0); --sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0); --sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0); --sidebar-ring: oklch(0.708 0 0);
} }
.dark { .dark {
--background: oklch(0.145 0 0); --background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0); --foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0); --card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0); --card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0); --popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0); --popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0); --primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0); --primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0); --secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0); --secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0); --muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0); --muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0); --accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0); --accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216); --destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%); --border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%); --input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0); --ring: oklch(0.556 0 0);
--chart-1: oklch(0.87 0 0); --chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0); --chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0); --chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0); --chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0); --chart-5: oklch(0.269 0 0);
--sidebar: oklch(0.205 0 0); --sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0); --sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376); --sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0); --sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%); --sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0); --sidebar-ring: oklch(0.556 0 0);
} }
@layer base { @layer base {
* { * {
@apply border-border outline-ring/50; @apply border-border outline-ring/50;
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
@apply font-sans; @apply font-sans;
} }
}
.v-loading-mask {
position: absolute;
inset: 0;
z-index: 30;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.625rem;
background: color-mix(in oklab, var(--background), transparent 28%);
-webkit-backdrop-filter: blur(1.5px);
backdrop-filter: blur(1.5px);
opacity: 0;
pointer-events: none;
transition: opacity 180ms ease;
}
.v-loading-mask.is-active {
opacity: 1;
pointer-events: auto;
}
.v-loading-spinner {
width: 1.625rem;
height: 1.625rem;
border-radius: 9999px;
border: 2px solid color-mix(in oklab, var(--foreground), transparent 84%);
border-top-color: var(--foreground);
animation: v-loading-rotate 700ms linear infinite;
}
.v-loading-text {
font-size: 0.8125rem;
color: color-mix(in oklab, var(--foreground), transparent 22%);
line-height: 1;
}
@keyframes v-loading-rotate {
to {
transform: rotate(360deg);
}
} }