Files
OCS_service/app/components/layout/AppHeader.vue
T
2026-05-23 22:23:29 +08:00

137 lines
3.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- app/components/layout/AppHeader.vue - 顶部导航和页面切换入口 -->
<script lang="ts" setup>
import {
Activity,
LayoutDashboard,
LogIn,
LogOut,
Moon,
Search,
Sun
} from "lucide-vue-next";
import SettingsDialog from "@/components/layout/SettingsDialog.vue";
import { Button } from "@/components/ui/button";
import { useAuthStore } from "@/stores/auth";
import { useGlobalStore } from "@/stores/global";
/** 主题状态由 themeStore 统一管理,持久化由 @nuxtjs/color-mode 处理 */
const themeStore = useGlobalStore();
const authStore = useAuthStore();
/** 顶部导航项,icon 使用 lucide,和项目里 shadcn 的图标体系保持一致 */
const navItems = [
{
label: "答题",
to: "/",
icon: Search
},
{
label: "Dashboard",
to: "/dashboard",
icon: LayoutDashboard
}
];
const route = useRoute();
/** 当前路由高亮,Dashboard 的子路径也归到 Dashboard 导航项 */
const isActive = (to: string) => {
if (to === "/") return route.path === "/";
return route.path.startsWith(to);
};
// 退出登录
const handleSignOut = async () => {
await authStore.signOut();
await navigateTo("/login");
};
</script>
<template>
<header
class="border-b bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/80"
>
<div
class="mx-auto flex min-h-16 w-full max-w-7xl items-center justify-between gap-4 px-4 md:px-6"
>
<NuxtLink to="/" class="flex min-w-0 items-center gap-3">
<span
class="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-primary text-primary-foreground"
>
<Activity class="size-5" />
</span>
<span class="min-w-0">
<span class="block truncate text-base font-semibold text-foreground">
OCS AI 答题服务
</span>
<span class="block truncate text-xs text-muted-foreground">
Nuxt API Runtime
</span>
</span>
</NuxtLink>
<nav class="flex shrink-0 items-center gap-1 overflow-x-auto">
<Button
v-for="item in navItems"
:key="item.to"
as-child
:variant="isActive(item.to) ? 'secondary' : 'ghost'"
size="sm"
>
<NuxtLink :to="item.to" class="gap-2">
<component :is="item.icon" class="size-4" />
<span>{{ item.label }}</span>
</NuxtLink>
</Button>
<!-- 深色/浅色主题切换按钮 -->
<Button
variant="ghost"
size="icon"
aria-label="切换主题"
@click="themeStore.toggle"
>
<Sun v-if="themeStore.isDark" class="size-4" />
<Moon v-else class="size-4" />
</Button>
<!--
已登录/未登录状态由客户端 auth 插件决定.client.ts),
SSR 阶段 isOnline 始终为 false
整个认证区块必须放在 ClientOnly 内避免水合不匹配
-->
<ClientOnly>
<template v-if="authStore.isOnline">
<span class="max-sm:hidden text-xs text-muted-foreground">
{{ authStore.user!.email }}
</span>
<SettingsDialog />
<Button
variant="ghost"
size="icon"
aria-label="退出登录"
@click="handleSignOut"
>
<LogOut class="size-4" />
</Button>
</template>
<template v-else>
<Button as-child variant="ghost" size="sm">
<NuxtLink to="/login" class="gap-2">
<LogIn class="size-4" />
<span>登录</span>
</NuxtLink>
</Button>
</template>
<!-- SSR 阶段占位避免布局闪烁 -->
<template #fallback>
<div class="size-8 rounded-md" />
</template>
</ClientOnly>
</nav>
</div>
</header>
</template>