yeg
2026-09-09 528d52f2c7c6f911c54cae6e3f25500eab4384fe
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
<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>