<script setup lang="ts">
|
import { computed, ref, watch } from 'vue'
|
import { useRoute, useRouter } from 'vue-router'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { getCurrentUser, logout } from '@/utils/auth'
|
|
const props = defineProps<{ open: boolean; collapsed: boolean }>()
|
const emit = defineEmits<{ close: [] }>()
|
|
const route = useRoute()
|
const router = useRouter()
|
const currentUser = getCurrentUser()
|
|
const menuGroups = [
|
{ title: '首页', icon: 'HomeFilled', path: '/dashboard' },
|
{ title: '测试管理', icon: 'Document', children: [
|
{ path: '/test-list', title: '测试列表' }, { path: '/battery-group', title: '电池组管理' },
|
{ path: '/test-type', title: '测试类型管理' }, { path: '/data-revision', title: '数据修订记录' },
|
{ path: '/exception', title: '异常管理' },
|
]},
|
{ title: '设备管理', icon: 'SetUp', children: [
|
{ path: '/device-cabinet', title: '检测柜' }, { path: '/device-collector', title: '采集器' },
|
]},
|
{ title: '数据分析', icon: 'TrendCharts', children: [
|
{ path: '/report-compare', title: '对比报表' }, { path: '/report-formation', title: '化成报表' },
|
{ path: '/report-splice', title: '拼接报表' },
|
]},
|
{ title: '待办管理', icon: 'List', children: [{ path: '/todo-list', title: '待办列表' }] },
|
{ title: '角色与权限', icon: 'UserFilled', children: [{ path: '/account', title: '账号管理' }, { path: '/role', title: '角色管理' }, { path: '/operation-log', title: '操作日志' }] },
|
]
|
|
const activePath = computed(() => route.path)
|
const expandedGroups = ref(new Set(menuGroups.filter(group => group.children).map(group => group.title)))
|
|
function isPathActive(path: string) {
|
return activePath.value === path || activePath.value.startsWith(`${path}/`)
|
}
|
|
function isGroupActive(group: (typeof menuGroups)[number]) {
|
return group.children?.some(child => isPathActive(child.path)) ?? false
|
}
|
|
function toggleGroup(title: string) {
|
if (props.collapsed) return
|
|
const next = new Set(expandedGroups.value)
|
next.has(title) ? next.delete(title) : next.add(title)
|
expandedGroups.value = next
|
}
|
|
watch(activePath, () => {
|
const activeGroup = menuGroups.find(group => isGroupActive(group))
|
if (activeGroup && !expandedGroups.value.has(activeGroup.title)) {
|
expandedGroups.value = new Set(expandedGroups.value).add(activeGroup.title)
|
}
|
})
|
|
async function navigate(path: string) {
|
await router.push(path)
|
emit('close')
|
}
|
|
async function handleLogout() {
|
try {
|
await ElMessageBox.confirm('退出后需要重新登录,是否继续?', '退出登录', {
|
confirmButtonText: '退出',
|
cancelButtonText: '取消',
|
type: 'warning',
|
})
|
await logout()
|
ElMessage.success('已安全退出')
|
await router.replace('/login')
|
} catch {
|
// 用户取消退出
|
}
|
}
|
</script>
|
|
<template>
|
<div v-if="open" class="sidebar-scrim" @click="$emit('close')"></div>
|
<aside class="sidebar" :class="{ open, collapsed }">
|
<div class="brand">
|
<div class="brand-mark"><el-icon><Monitor /></el-icon></div>
|
<div><strong>检测室数据可视化</strong><span>OPERATIONS CENTER</span></div>
|
<button type="button" class="close-menu" aria-label="关闭导航" @click="$emit('close')"><el-icon><Close /></el-icon></button>
|
</div>
|
|
<section class="environment">
|
<div><span>当前账号</span><strong>{{ currentUser?.username || '—' }}</strong></div>
|
</section>
|
|
<nav class="navigation">
|
<template v-for="group in menuGroups" :key="group.title">
|
<el-tooltip v-if="group.path" :content="group.title" placement="right" :disabled="!collapsed">
|
<button
|
type="button"
|
class="nav-parent"
|
:class="{ active: activePath === group.path }"
|
@click="navigate(group.path)"
|
>
|
<el-icon><component :is="group.icon" /></el-icon><span>{{ group.title }}</span>
|
</button>
|
</el-tooltip>
|
<div v-else class="nav-group">
|
<el-tooltip :content="group.title" placement="right" :disabled="!collapsed">
|
<button
|
type="button"
|
class="nav-parent group-toggle"
|
:class="{ active: isGroupActive(group), expanded: expandedGroups.has(group.title) }"
|
:aria-expanded="expandedGroups.has(group.title) ? 'true' : 'false'"
|
:data-expanded="expandedGroups.has(group.title)"
|
:aria-label="`${expandedGroups.has(group.title) ? '收起' : '展开'}${group.title}`"
|
@click="toggleGroup(group.title)"
|
>
|
<el-icon><component :is="group.icon" /></el-icon>
|
<span>{{ group.title }}</span>
|
<el-icon class="group-arrow"><ArrowRight /></el-icon>
|
</button>
|
</el-tooltip>
|
<div
|
class="nav-children"
|
:class="{ expanded: expandedGroups.has(group.title) }"
|
:aria-hidden="!expandedGroups.has(group.title)"
|
:inert="!expandedGroups.has(group.title)"
|
>
|
<div class="nav-children-inner">
|
<button
|
v-for="child in group.children"
|
:key="child.path"
|
type="button"
|
class="nav-child"
|
:class="{ active: isPathActive(child.path) }"
|
@click="navigate(child.path)"
|
>{{ child.title }}</button>
|
</div>
|
</div>
|
</div>
|
</template>
|
</nav>
|
|
<button class="sidebar-account" type="button" aria-label="退出当前账号" @click="handleLogout">
|
<span class="avatar">{{ currentUser?.displayName.slice(0, 1) || 'A' }}</span>
|
<div><strong>{{ currentUser?.displayName || 'Admin' }}</strong><small>{{ currentUser?.role || '—' }}</small></div>
|
</button>
|
</aside>
|
</template>
|
|
<style scoped>
|
.sidebar {
|
position: fixed;
|
inset: 0 auto 0 0;
|
z-index: 100;
|
width: var(--sidebar-width);
|
display: flex;
|
flex-direction: column;
|
color: #8fa7bf;
|
background: var(--navy);
|
border-right: 1px solid #10335f;
|
transition: width .22s ease, transform .22s ease;
|
}
|
.brand { height: 72px; flex: 0 0 72px; display: flex; align-items: center; gap: 12px; padding: 0 18px; border-bottom: 1px solid #10335f; }
|
.brand-mark { display: grid; place-items: center; width: 32px; height: 32px; color: #fff; background: var(--blue); }
|
.brand-mark .el-icon { font-size: 17px; }
|
.brand strong, .brand span { display: block; }
|
.brand strong { color: #fff; font-size: 14px; font-weight: 600; line-height: 1.3; }
|
.brand span { margin-top: 3px; color: #5f7d9a; font-size: 10px; letter-spacing: .8px; }
|
.close-menu { display: none; margin-left: auto; border: 0; background: none; color: #8fa7bf; font-size: 18px; }
|
.environment { height: 70px; flex: 0 0 70px; display: flex; justify-content: center; flex-direction: column; gap: 7px; padding: 0 18px; border-bottom: 1px solid #10335f; }
|
.environment div { display: flex; justify-content: space-between; font-size: 12px; }
|
.environment strong { color: #fff; font-weight: 500; }
|
.environment p { display: flex; align-items: center; gap: 7px; margin: 0; color: #42c68a; font-size: 11px; }
|
.environment i { width: 6px; height: 6px; border-radius: 50%; background: #42c68a; box-shadow: 0 0 0 3px rgba(66,198,138,.12); }
|
.navigation { flex: 1; min-height: 0; padding: 14px 10px 24px; overflow-x: hidden; overflow-y: auto; scrollbar-gutter: stable; }
|
.nav-parent, .nav-child { width: 100%; border: 0; color: #8fa7bf; background: transparent; cursor: pointer; font-family: inherit; text-align: left; }
|
.nav-parent { height: 38px; display: flex; align-items: center; gap: 10px; padding: 0 9px; font-size: 15px; border-left: 3px solid transparent; }
|
.nav-parent .el-icon { font-size: 16px; }
|
.nav-parent:hover, .nav-parent.active { color: #fff; background: #176fcb; border-left-color: #2188f5; }
|
.group-toggle { color: #a9bbcd; }
|
.group-arrow { margin-left: auto; font-size: 13px !important; transition: transform .18s ease; }
|
.group-toggle.expanded .group-arrow { transform: rotate(90deg); }
|
.nav-children { max-height: 0; overflow: hidden; opacity: 0; transition: max-height .22s ease, opacity .16s ease; }
|
.nav-children.expanded { max-height: 420px; opacity: 1; }
|
.nav-children-inner { overflow: visible; }
|
.nav-child { height: 33px; padding: 0 9px 0 42px; font-size: 14px; border-left: 3px solid transparent; }
|
.nav-child:hover, .nav-child.active { color: #fff; background: rgba(33,136,245,.13); border-left-color: #2188f5; }
|
.nav-group { margin-top: 3px; }
|
.sidebar-account { width: 100%; height: 62px; flex: 0 0 62px; display: flex; align-items: center; gap: 10px; padding: 0 18px; border: 0; border-top: 1px solid #10335f; color: inherit; background: transparent; font-family: inherit; text-align: left; cursor: pointer; }
|
.sidebar-account:hover { background: rgba(33,136,245,.08); }
|
.avatar { display: grid; place-items: center; width: 32px; height: 32px; border: 1px solid var(--blue); color: var(--blue); font-size: 13px; font-weight: 700; }
|
.sidebar-account strong, .sidebar-account small { display: block; }
|
.sidebar-account strong { color: #fff; font-size: 13px; font-weight: 500; }
|
.sidebar-account small { margin-top: 2px; color: #5f7d9a; font-size: 11px; }
|
.sidebar-scrim { display: none; }
|
|
@media (max-width: 900px) {
|
.sidebar { width: var(--sidebar-width); transform: translateX(-100%); box-shadow: 12px 0 35px rgba(0,20,45,.22); }
|
.sidebar.open { transform: translateX(0); }
|
.sidebar-scrim { display: block; position: fixed; inset: 0; z-index: 90; background: rgba(4,20,38,.48); }
|
.close-menu { display: inline-flex; cursor: pointer; }
|
}
|
|
@media (min-width: 901px) {
|
.sidebar.collapsed { width: var(--sidebar-collapsed-width); }
|
.sidebar.collapsed .brand { justify-content: center; gap: 0; padding: 0; }
|
.sidebar.collapsed .brand > div:not(.brand-mark) { display: none; }
|
.sidebar.collapsed .environment { height: 54px; flex-basis: 54px; align-items: center; padding: 0; }
|
.sidebar.collapsed .environment div { display: none; }
|
.sidebar.collapsed .environment p { font-size: 0; }
|
.sidebar.collapsed .environment p i { width: 8px; height: 8px; }
|
.sidebar.collapsed .navigation { padding: 14px 10px; overflow-x: hidden; }
|
.sidebar.collapsed .nav-parent { justify-content: center; gap: 0; padding: 0; border-left-width: 0; }
|
.sidebar.collapsed .nav-parent .el-icon { font-size: 18px; }
|
.sidebar.collapsed .nav-parent span, .sidebar.collapsed .nav-child, .sidebar.collapsed .group-arrow { display: none; }
|
.sidebar.collapsed .nav-children { max-height: 0; opacity: 0; }
|
.sidebar.collapsed .nav-group { margin-top: 3px; }
|
.sidebar.collapsed .group-toggle { cursor: help; }
|
.sidebar.collapsed .sidebar-account { justify-content: center; gap: 0; padding: 0; }
|
.sidebar.collapsed .sidebar-account div { display: none; }
|
}
|
</style>
|