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 {
id: string;
avatar: string;
name: string;
roleList: {
roleId: string;
roleName: string;
roleJob: string;
avatarLink: string;
fame: number;
}[];
}
+12 -6
View File
@@ -31,6 +31,7 @@ import {
SidebarMenuItem,
SidebarTrigger,
} from "@/components/ui/sidebar";
import { useDnfHelperStore } from "@/stores/dnfhelper";
const props = withDefaults(defineProps<SidebarProps>(), {
collapsible: "icon",
@@ -43,12 +44,17 @@ const toggleDarkMode = () => {
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 = {
user: {
name: "尚未绑定助手ID",
email: "m@example.com",
avatar: "/avatars/shadcn.jpg",
},
navMain: [
{
title: "计算器",
@@ -198,7 +204,7 @@ const data = {
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
<NavUser :user="data.user" />
<NavUser :user="userInfo" />
</SidebarFooter>
</Sidebar>
</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">
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 {
@@ -17,6 +17,7 @@ import {
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
import { useDnfHelperStore } from "@/stores/dnfhelper";
const props = defineProps<{
user: {
@@ -29,6 +30,8 @@ const props = defineProps<{
const { user } = toRefs(props);
const { isMobile } = useSidebar();
const helperStore = useDnfHelperStore();
</script>
<template>
@@ -41,8 +44,8 @@ const { isMobile } = useSidebar();
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar class="h-8 w-8 rounded-lg">
<AvatarImage :src="user.avatar" :alt="user.name" />
<AvatarFallback class="rounded-lg"> Dcalc </AvatarFallback>
<AvatarImage :src="user.avatar" :alt="user.id" />
<AvatarFallback class="rounded-lg"> DNF </AvatarFallback>
</Avatar>
<div class="grid flex-1 text-left text-sm leading-tight">
<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">
<Avatar class="h-8 w-8 rounded-lg">
<AvatarImage :src="user.avatar" :alt="user.name" />
<AvatarFallback class="rounded-lg"> Dcalc </AvatarFallback>
<AvatarFallback class="rounded-lg"> DNF </AvatarFallback>
</Avatar>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">{{ user.name }}</span>
@@ -69,32 +72,35 @@ const { isMobile } = useSidebar();
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<!-- <DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<DropdownMenuItem :disabled="!helperStore.helperInfo">
<Sparkles />
Upgrade to Pro
切换绑定
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuGroup> -->
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<!-- <DropdownMenuItem>
<BadgeCheck />
Account
反馈
</DropdownMenuItem>
<DropdownMenuItem>
<CreditCard />
Billing
</DropdownMenuItem>
</DropdownMenuItem> -->
<DropdownMenuItem>
<Bell />
Notifications
反馈
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<DropdownMenuItem
:disabled="!helperStore.helperInfo"
@click="helperStore.helperInfo = null"
>
<LogOut />
Log out
取消绑定
</DropdownMenuItem>
</DropdownMenuContent>
</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 { pinia } from "@/stores";
import { loadingPlugin } from "@/directives/loading";
import router from "./router";
import App from "@/pages/index.vue";
@@ -13,5 +14,6 @@ const app = createApp(App);
app.use(pinia);
app.use(router);
app.use(loadingPlugin);
app.mount(appContainer);
+10 -2
View File
@@ -8,6 +8,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { useDnfHelperStore } from "@/stores/dnfhelper";
import { RefreshCcw } from "lucide-vue-next";
const props = defineProps<{
@@ -21,6 +22,8 @@ const props = defineProps<{
}>();
const { list } = toRefs(props);
const helperStore = useDnfHelperStore();
</script>
<template>
@@ -31,7 +34,12 @@ const { list } = toRefs(props);
<h1 class="text-lg font-semibold">角色列表</h1>
<p class="text-sm text-muted-foreground">共 {{ list.length }} 个角色</p>
</div>
<Button variant="outline" size="icon" aria-label="Submit">
<Button
variant="outline"
size="icon"
aria-label="Submit"
@click="helperStore.refleshHelperInfo(helperStore.helperInfo?.id)"
>
<RefreshCcw />
</Button>
</header>
@@ -42,7 +50,7 @@ const { list } = toRefs(props);
<CardTitle>{{ item.roleJob }}</CardTitle>
</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" /> -->
<img :src="item.avatarLink" alt="avatar" class="w-auto h-sm rounded-full object-cover" />
</CardContent>
<CardFooter class="flex justify-between">
<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);
console.log("helperInfo", data);
if (data) {
helperInfo.value = data;
console.log(helperInfo.value);
+45 -3
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";
@@ -10,7 +9,7 @@
@custom-variant dark (&:is(.dark *));
@theme inline {
--font-sans: 'Geist Variable', sans-serif;
--font-sans: "Geist Variable", sans-serif;
--font-heading: var(--font-sans);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
@@ -127,3 +126,46 @@
@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);
}
}