<script setup lang="ts">
|
import { computed, onMounted } from 'vue'
|
import { useRoute } from 'vue-router'
|
import { getUserInfo } from '@/api/auth'
|
import { useDashboard, clearDashboard } from '@/stores/dashboard'
|
import { formatBusinessTime } from '@/utils/businessTime'
|
import GlobalSearch from '@/components/GlobalSearch.vue'
|
import NotificationCenter from '@/components/NotificationCenter.vue'
|
|
defineProps<{ sidebarCollapsed: boolean }>()
|
defineEmits<{ toggleMenu: []; toggleSidebar: [] }>()
|
|
const { snapshot, loadedAt, error, load } = useDashboard()
|
onMounted(async () => {
|
try {
|
const permissions = (await getUserInfo()).permissions || []
|
if (permissions.some(value => ['*:*:*', 'dashboard:workbench:list', 'dashboard:workbench:query'].includes(value))) await load()
|
else clearDashboard()
|
} catch { /* 共享查询错误由顶部状态和首页错误提示展示。 */ }
|
})
|
const route = useRoute()
|
const pageTitle = computed(() => String(route.meta.title || '工作台'))
|
</script>
|
|
<template>
|
<header class="topbar">
|
<div class="title-group">
|
<button
|
class="sidebar-toggle"
|
type="button"
|
:aria-label="sidebarCollapsed ? '展开导航' : '收起导航'"
|
:title="sidebarCollapsed ? '展开导航' : '收起导航'"
|
@click="$emit('toggleSidebar')"
|
>
|
<el-icon><Expand v-if="sidebarCollapsed" /><Fold v-else /></el-icon>
|
</button>
|
<button class="mobile-menu" type="button" aria-label="打开导航" title="打开导航" @click="$emit('toggleMenu')">
|
<el-icon><Menu /></el-icon>
|
</button>
|
<h1>{{ pageTitle }}</h1>
|
<span>/ WORKBENCH-01</span>
|
</div>
|
|
<div class="topbar-actions">
|
<div v-if="loadedAt" class="sync-state">
|
<el-icon><Refresh /></el-icon>
|
<span>{{ formatBusinessTime(loadedAt) }} 更新</span>
|
</div>
|
<GlobalSearch />
|
<NotificationCenter />
|
</div>
|
</header>
|
|
<div v-if="!route.path.startsWith('/test-list')" class="system-strip">
|
<div v-if="error"><span role="alert">概览加载失败</span></div>
|
<template v-else-if="snapshot">
|
<div><span>在线检测柜</span><strong class="healthy">{{ snapshot.cabinets.filter(item => item.onlineStatus === '在线').length }} / {{ snapshot.cabinets.length }}</strong></div>
|
<div><span>检测中测试</span><strong class="active">{{ snapshot.tests.filter(item => item.status === 'RUNNING').length }}</strong></div>
|
<div><span>开放异常</span><strong class="warning">{{ snapshot.warnings.length }}</strong></div>
|
</template>
|
<div v-else><span>暂无已加载概览</span></div>
|
</div>
|
</template>
|
|
<style scoped>
|
.topbar {
|
position: sticky;
|
top: 0;
|
z-index: 50;
|
height: 58px;
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
padding: 0 20px;
|
background: #fff;
|
border-bottom: 1px solid var(--line);
|
}
|
|
.title-group, .topbar-actions, .sync-state {
|
display: flex;
|
align-items: center;
|
}
|
|
.title-group { gap: 12px; min-width:0; }
|
.title-group h1 { overflow:hidden;white-space:nowrap;text-overflow:ellipsis;margin: 0; font-size: 20px; line-height: 1; font-weight: 600; color: var(--ink); }
|
.title-group > span { color: #7b8794; font-size: 10px; font-weight: 500; }
|
.topbar-actions { gap: 14px; flex-shrink:0; }
|
.sync-state { gap: 6px; color: #52606d; font-size: 12px; }
|
.sync-state .el-icon { color: var(--blue); font-size: 14px; }
|
|
.mobile-menu, .sidebar-toggle {
|
border: 0;
|
padding: 0;
|
background: transparent;
|
color: #52606d;
|
cursor: pointer;
|
}
|
.sidebar-toggle { width: 28px; height: 28px; display: inline-grid; place-items: center; font-size: 19px; }
|
.sidebar-toggle:hover { color: var(--blue); background: #edf4fa; }
|
.mobile-menu { display: none; font-size: 19px; }
|
|
.system-strip {
|
height: 38px;
|
display: flex;
|
align-items: center;
|
gap: 24px;
|
padding: 0 20px;
|
overflow-x: auto;
|
white-space: nowrap;
|
background: #edf4fa;
|
border-bottom: 1px solid #d8e6f1;
|
}
|
.system-strip div { display: flex; align-items: center; gap: 7px; font-size: 12px; color: #6f7b86; }
|
.system-strip strong { color: #52606d; font-size: 13px; font-weight: 600; }
|
.system-strip .healthy { color: #23865e; }
|
.system-strip .active { color: var(--blue); }
|
.system-strip .warning { color: #b35a25; }
|
|
@media (max-width: 900px) {
|
.sidebar-toggle { display: none; }
|
.mobile-menu { display: inline-flex; }
|
}
|
|
@media (max-width: 620px) {
|
.title-group > span, .sync-state span { display: none; }
|
.system-strip { gap: 18px; padding: 0 14px; }
|
}
|
</style>
|