270 lines
5.8 KiB
Vue
270 lines
5.8 KiB
Vue
<script setup lang="ts">
|
|
import { Bell, ChevronDown } from "@lucide/vue";
|
|
import { computed } from "vue";
|
|
import { storeToRefs } from "pinia";
|
|
import { useRoute, useRouter } from "vue-router";
|
|
|
|
import { useAppStore } from "@/stores/app.store";
|
|
import { useAuthStore } from "@/stores/auth.store";
|
|
|
|
const appStore = useAppStore();
|
|
const { isOnline } = storeToRefs(appStore);
|
|
const auth = useAuthStore();
|
|
const { user } = storeToRefs(auth);
|
|
const router = useRouter();
|
|
const route = useRoute();
|
|
|
|
const navigation = ["对话", "看板", "供应商", "标准库", "报告", "风险项管理"];
|
|
const navigationRoutes: Record<string, string> = {
|
|
对话: "assessment",
|
|
看板: "overview",
|
|
供应商: "supplier",
|
|
标准库: "standards",
|
|
报告: "report",
|
|
风险项管理: "review",
|
|
};
|
|
const visibleNavigation = computed(() =>
|
|
navigation.filter(
|
|
(item) => item !== "风险项管理" || auth.hasPermission("risk_item.review"),
|
|
),
|
|
);
|
|
|
|
const routeChunks: Record<string, () => Promise<unknown>> = {
|
|
看板: () => import("@/pages/OverviewPage.vue"),
|
|
供应商: () => import("@/pages/SupplierProfilePage.vue"),
|
|
标准库: () => import("@/pages/StandardsPage.vue"),
|
|
报告: () => import("@/pages/ReportPage.vue"),
|
|
风险项管理: () => import("@/pages/ReviewPage.vue"),
|
|
};
|
|
const prefetched = new Set<string>();
|
|
function prefetch(item: string) {
|
|
if (prefetched.has(item)) return;
|
|
const loader = routeChunks[item];
|
|
if (!loader) return;
|
|
prefetched.add(item);
|
|
void loader();
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<header class="app-header">
|
|
<RouterLink class="brand" :to="{ name: 'assessment' }">
|
|
<span class="brand__mark">蝶</span>
|
|
<span class="brand__name">风险预警助手</span>
|
|
<span class="engine-badge">双引擎</span>
|
|
</RouterLink>
|
|
|
|
<nav class="top-navigation" aria-label="顶部导航">
|
|
<RouterLink
|
|
v-for="item in visibleNavigation"
|
|
:key="item"
|
|
:to="{
|
|
name: navigationRoutes[item] || 'assessment',
|
|
}"
|
|
:class="{
|
|
active:
|
|
(item === '看板' && route.name === 'overview') ||
|
|
(item === '供应商' && route.name === 'supplier') ||
|
|
(item === '标准库' && route.name === 'standards') ||
|
|
(item === '报告' && route.name === 'report') ||
|
|
(item === '风险项管理' && route.name === 'review') ||
|
|
(item === '对话' && route.name === 'assessment'),
|
|
}"
|
|
@mouseenter="prefetch(item)"
|
|
@focus="prefetch(item)"
|
|
>
|
|
{{ item }}
|
|
</RouterLink>
|
|
</nav>
|
|
|
|
<div class="header-actions">
|
|
<span
|
|
class="service-dot"
|
|
:class="{ 'service-dot--online': isOnline }"
|
|
:title="isOnline ? '服务正常' : '服务离线'"
|
|
></span>
|
|
<button class="header-icon" type="button" title="通知" aria-label="通知">
|
|
<Bell :size="16" />
|
|
</button>
|
|
<button
|
|
class="user-pill"
|
|
type="button"
|
|
title="退出登录"
|
|
@click="
|
|
auth.logout();
|
|
router.push({ name: 'login' });
|
|
"
|
|
>
|
|
<span class="user-avatar">{{ user?.name?.slice(0, 1) ?? "访" }}</span>
|
|
<span>{{ user?.name ?? "访客" }}</span>
|
|
<ChevronDown :size="13" />
|
|
</button>
|
|
</div>
|
|
</header>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.app-header {
|
|
z-index: 10;
|
|
display: flex;
|
|
height: var(--header-height);
|
|
flex: 0 0 var(--header-height);
|
|
align-items: center;
|
|
gap: 16px;
|
|
border-bottom: 1px solid var(--color-border);
|
|
padding: 0 20px;
|
|
background: var(--color-surface);
|
|
}
|
|
|
|
.brand {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
color: var(--color-text);
|
|
text-decoration: none;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.brand__mark {
|
|
display: grid;
|
|
width: 30px;
|
|
height: 30px;
|
|
place-items: center;
|
|
border-radius: 6px;
|
|
background: var(--color-text);
|
|
color: white;
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.brand__name {
|
|
font-size: 14px;
|
|
font-weight: 650;
|
|
}
|
|
|
|
.engine-badge {
|
|
border: 1px solid var(--color-accent-soft);
|
|
border-radius: 999px;
|
|
padding: 1px 6px;
|
|
background: var(--color-accent-subtle);
|
|
color: var(--color-accent);
|
|
font-size: 9px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
nav {
|
|
display: flex;
|
|
min-width: 0;
|
|
flex: 1;
|
|
gap: 2px;
|
|
margin-left: 16px;
|
|
}
|
|
|
|
nav a {
|
|
border-radius: 6px;
|
|
padding: 6px 12px;
|
|
color: var(--color-text-muted);
|
|
font-size: 13px;
|
|
text-decoration: none;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
nav a:hover,
|
|
nav a.active {
|
|
background: var(--color-bg-warm);
|
|
color: var(--color-text);
|
|
}
|
|
|
|
.header-actions,
|
|
.user-pill {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.header-actions {
|
|
gap: 8px;
|
|
}
|
|
|
|
.service-dot {
|
|
width: 6px;
|
|
height: 6px;
|
|
border-radius: 50%;
|
|
background: var(--color-danger);
|
|
}
|
|
|
|
.service-dot--online {
|
|
background: var(--color-success);
|
|
}
|
|
|
|
.header-icon {
|
|
display: grid;
|
|
width: 30px;
|
|
height: 30px;
|
|
place-items: center;
|
|
border: 0;
|
|
border-radius: 50%;
|
|
background: transparent;
|
|
color: var(--color-text-muted);
|
|
}
|
|
|
|
.header-icon:hover {
|
|
background: var(--color-bg-warm);
|
|
color: var(--color-text);
|
|
}
|
|
|
|
.user-pill {
|
|
min-height: 34px;
|
|
gap: 7px;
|
|
border: 0;
|
|
border-radius: 999px;
|
|
padding: 4px 10px 4px 4px;
|
|
background: var(--color-bg-warm);
|
|
color: var(--color-text-secondary);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.user-avatar {
|
|
display: grid;
|
|
width: 26px;
|
|
height: 26px;
|
|
place-items: center;
|
|
border-radius: 50%;
|
|
background: var(--color-text);
|
|
color: white;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
@media (max-width: 960px) {
|
|
nav a:nth-child(n + 4) {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 680px) {
|
|
.app-header {
|
|
padding-inline: 12px;
|
|
}
|
|
|
|
.engine-badge,
|
|
.top-navigation,
|
|
.header-icon,
|
|
.user-pill > span:not(.user-avatar),
|
|
.user-pill svg {
|
|
display: none;
|
|
}
|
|
|
|
.header-actions {
|
|
margin-left: auto;
|
|
}
|
|
|
|
.brand__name {
|
|
font-size: 13px;
|
|
}
|
|
|
|
.user-pill {
|
|
padding: 4px;
|
|
}
|
|
}
|
|
</style>
|