Files
dashboard/frontend/src/layout/index.vue
T

290 lines
7.5 KiB
Vue
Raw Normal View History

<template>
<div class="lux-shell">
<div class="lux-shell-bg" aria-hidden="true">
<div class="lux-orb lux-orb-1" />
<div class="lux-orb lux-orb-2" />
<div class="lux-orb lux-orb-3" />
</div>
<el-container class="lux-shell-body">
<el-aside
class="sidebar-glass"
:class="{ 'is-collapsed': isSidebarCollapsed }"
@mouseenter="isSidebarCollapsed = false"
@mouseleave="isSidebarCollapsed = true"
>
<div class="sidebar-inner">
<div class="sidebar-logo">
<SidebarLogo :collapsed="isSidebarCollapsed" />
</div>
<div class="sidebar-nav-wrap">
<nav
class="sidebar-rail"
:class="{ 'is-active': isSidebarCollapsed }"
aria-label="主导航"
>
<router-link
v-for="item in railNavItems"
:key="item.path"
:to="item.path"
class="sidebar-rail-item"
:class="{ 'is-active': activeMenu === item.path }"
:title="item.title"
>
<el-icon><component :is="item.icon" /></el-icon>
</router-link>
</nav>
<el-menu
class="lux-menu lux-menu--expanded"
:class="{ 'is-active': !isSidebarCollapsed }"
:default-active="activeMenu"
v-model:openeds="openedMenus"
background-color="transparent"
text-color="#cbd5e1"
active-text-color="#7dd3fc"
router
>
<el-sub-menu index="1">
<template #title>
<el-icon><Headset /></el-icon>
<span>耳机管理</span>
</template>
<el-menu-item index="/brand">
<el-icon><Collection /></el-icon>
<span>品牌管理</span>
</el-menu-item>
<el-menu-item index="/model">
<el-icon><List /></el-icon>
<span>型号管理</span>
</el-menu-item>
</el-sub-menu>
<el-sub-menu index="upgrade">
<template #title>
<el-icon><Upload /></el-icon>
<span>升级管理</span>
</template>
<el-menu-item index="/ota">
<el-icon><Document /></el-icon>
<span>OTA 管理</span>
</el-menu-item>
</el-sub-menu>
<el-sub-menu index="share-code">
<template #title>
<el-icon><Share /></el-icon>
<span>分享码</span>
</template>
<el-menu-item index="/share-code/log">
<el-icon><Document /></el-icon>
<span>分享日志</span>
</el-menu-item>
</el-sub-menu>
</el-menu>
</div>
</div>
</el-aside>
<el-container class="lux-right-col">
<el-header class="lux-header">
<el-breadcrumb class="lux-breadcrumb" separator="/">
<el-breadcrumb-item>首页</el-breadcrumb-item>
<el-breadcrumb-item v-if="breadcrumbSection">{{ breadcrumbSection }}</el-breadcrumb-item>
<el-breadcrumb-item v-if="breadcrumbTitle">{{ breadcrumbTitle }}</el-breadcrumb-item>
</el-breadcrumb>
<el-button
circle
type="primary"
class="lux-header-power-btn"
title="退出登录"
aria-label="退出登录"
@click="handleLogout"
>
<el-icon :size="18"><SwitchButton /></el-icon>
</el-button>
</el-header>
<el-main class="lux-main">
<TabsView />
</el-main>
<el-footer class="lux-footer" height="auto">
<span class="lux-footer-copy">&copy;Powered By EafonYang</span>
</el-footer>
</el-container>
</el-container>
</div>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { Headset, Collection, List, Upload, Document, SwitchButton, Share } from '@element-plus/icons-vue'
import { clearToken } from '@/utils/auth'
import TabsView from '@/components/TabsView.vue'
import SidebarLogo from '@/components/SidebarLogo.vue'
const route = useRoute()
const router = useRouter()
const activeMenu = computed(() => route.path)
const isSidebarCollapsed = ref(true)
const openedMenus = ref([])
function resolveOpenedMenus(path) {
if (path.startsWith('/brand') || path.startsWith('/model')) return ['1']
if (path.startsWith('/ota')) return ['upgrade']
if (path.startsWith('/share-code')) return ['share-code']
return []
}
watch(
() => route.path,
(path) => {
openedMenus.value = resolveOpenedMenus(path)
},
{ immediate: true }
)
const railNavItems = [
{ path: '/brand', title: '品牌管理', icon: Collection },
{ path: '/model', title: '型号管理', icon: List },
{ path: '/ota', title: 'OTA 管理', icon: Document },
{ path: '/share-code/log', title: '分享日志', icon: Share }
]
const breadcrumbTitle = computed(() => {
const t = route.meta?.title
return typeof t === 'string' ? t : ''
})
const breadcrumbSection = computed(() => {
const path = route.path || ''
if (path.startsWith('/brand') || path.startsWith('/model')) return '耳机管理'
if (path.startsWith('/ota')) return '升级管理'
if (path.startsWith('/share-code')) return '分享码'
return ''
})
const handleLogout = () => {
clearToken()
ElMessage.success('已退出登录')
router.push('/login')
}
</script>
<style scoped>
.lux-right-col {
flex: 1;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
}
.sidebar-inner {
width: 220px;
min-width: 220px;
height: 100%;
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.sidebar-nav-wrap {
position: relative;
flex: 1;
min-height: 0;
}
.sidebar-rail,
.lux-menu--expanded {
position: absolute;
inset: 0;
overflow-x: hidden;
overflow-y: auto;
}
.sidebar-rail:not(.is-active),
.lux-menu--expanded:not(.is-active) {
visibility: hidden;
pointer-events: none;
opacity: 0;
}
.sidebar-rail.is-active,
.lux-menu--expanded.is-active {
visibility: visible;
pointer-events: auto;
opacity: 1;
}
.sidebar-logo {
position: relative;
flex-shrink: 0;
}
.sidebar-glass.is-collapsed .sidebar-logo {
overflow: visible;
}
.sidebar-rail {
padding: 8px 0 16px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.sidebar-rail-item {
width: 64px;
height: 44px;
border-radius: 10px;
display: inline-flex;
align-items: center;
justify-content: center;
color: #cbd5e1;
text-decoration: none;
transition: background-color 0.15s ease, color 0.15s ease;
}
.sidebar-rail-item:hover {
color: #e2e8f0;
background: rgba(255, 255, 255, 0.07);
}
.sidebar-rail-item.is-active {
color: #e0f2fe;
background: linear-gradient(
90deg,
rgba(56, 189, 248, 0.22),
rgba(99, 102, 241, 0.14)
);
box-shadow: 0 0 0 1px rgba(56, 189, 248, 0.25) inset;
}
.sidebar-rail-item .el-icon {
font-size: 18px;
}
.lux-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.lux-breadcrumb {
min-width: 0;
padding-right: 8px;
}
.lux-breadcrumb :deep(.el-breadcrumb__inner) {
color: #cbd5e1;
}
.lux-breadcrumb :deep(.el-breadcrumb__inner.is-link:hover) {
color: #7dd3fc;
}
</style>