558 lines
21 KiB
Vue
558 lines
21 KiB
Vue
<template>
|
||
<div class="admin-shell">
|
||
<!-- ===== 侧边栏:树形菜单 ===== -->
|
||
<aside class="sidebar" :class="{ open: mobileSidebarOpen }">
|
||
<!-- Logo -->
|
||
<div class="sidebar-logo" @click="router.push('/admin/dashboard')">
|
||
<svg class="logo-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
|
||
</svg>
|
||
<span class="logo-text">Mokee</span>
|
||
</div>
|
||
|
||
<!-- 菜单树 -->
|
||
<nav class="sidebar-nav">
|
||
<!-- 首页 -->
|
||
<div
|
||
class="menu-item single"
|
||
:class="{ active: activeMenu === '/admin/dashboard' }"
|
||
@click="openPage('/admin/dashboard', '首页')"
|
||
>
|
||
<el-icon :size="18"><HomeFilled /></el-icon>
|
||
<span>首页</span>
|
||
</div>
|
||
|
||
<!-- 系统管理 -->
|
||
<div class="menu-group" :class="{ open: menuOpen.system }">
|
||
<div class="menu-item parent" :class="{ active: activeGroup === 'system' }" @click="menuOpen.system = !menuOpen.system">
|
||
<el-icon :size="18"><Monitor /></el-icon>
|
||
<span>系统管理</span>
|
||
<el-icon :size="14" class="arrow" :class="{ rotated: menuOpen.system }"><ArrowRight /></el-icon>
|
||
</div>
|
||
<div class="sub-items" v-show="menuOpen.system">
|
||
<div class="menu-item child" :class="{ active: activeMenu === '/admin/system' }" @click="openPage('/admin/system', '系统列表')">
|
||
<span>系统列表</span>
|
||
</div>
|
||
<div class="menu-item child" :class="{ active: activeMenu === '/admin/api' }" @click="openPage('/admin/api', '接口管理')">
|
||
<span>接口管理</span>
|
||
</div>
|
||
<div class="menu-item child" :class="{ active: activeMenu === '/admin/dict' }" @click="openPage('/admin/dict', '数据字典')">
|
||
<span>数据字典</span>
|
||
</div>
|
||
<div class="menu-item child" :class="{ active: activeMenu === '/admin/file' }" @click="openPage('/admin/file', '文件管理')">
|
||
<span>文件管理</span>
|
||
</div>
|
||
<div class="menu-item child" :class="{ active: activeMenu === '/admin/email' }" @click="openPage('/admin/email', '邮件管理')">
|
||
<span>邮件管理</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 用户权限 -->
|
||
<div class="menu-group" :class="{ open: menuOpen.user }">
|
||
<div class="menu-item parent" :class="{ active: activeGroup === 'user' }" @click="menuOpen.user = !menuOpen.user">
|
||
<el-icon :size="18"><UserFilled /></el-icon>
|
||
<span>用户权限</span>
|
||
<el-icon :size="14" class="arrow" :class="{ rotated: menuOpen.user }"><ArrowRight /></el-icon>
|
||
</div>
|
||
<div class="sub-items" v-show="menuOpen.user">
|
||
<div class="menu-item child" :class="{ active: activeMenu === '/admin/user' }" @click="openPage('/admin/user', '用户管理')">
|
||
<span>用户管理</span>
|
||
</div>
|
||
<div class="menu-item child" :class="{ active: activeMenu === '/admin/role' }" @click="openPage('/admin/role', '角色管理')">
|
||
<span>角色管理</span>
|
||
</div>
|
||
<div class="menu-item child" :class="{ active: activeMenu === '/admin/menu' }" @click="openPage('/admin/menu', '菜单管理')">
|
||
<span>菜单管理</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 日志审计 -->
|
||
<div class="menu-group" :class="{ open: menuOpen.log }">
|
||
<div class="menu-item parent" :class="{ active: activeGroup === 'log' }" @click="menuOpen.log = !menuOpen.log">
|
||
<el-icon :size="18"><Document /></el-icon>
|
||
<span>日志审计</span>
|
||
<el-icon :size="14" class="arrow" :class="{ rotated: menuOpen.log }"><ArrowRight /></el-icon>
|
||
</div>
|
||
<div class="sub-items" v-show="menuOpen.log">
|
||
<div class="menu-item child" :class="{ active: activeMenu === '/admin/log/login' }" @click="openPage('/admin/log/login', '登录日志')">
|
||
<span>登录日志</span>
|
||
</div>
|
||
<div class="menu-item child" :class="{ active: activeMenu === '/admin/log/operation' }" @click="openPage('/admin/log/operation', '操作日志')">
|
||
<span>操作日志</span>
|
||
</div>
|
||
<div class="menu-item child" :class="{ active: activeMenu === '/admin/log/token' }" @click="openPage('/admin/log/token', 'Token日志')">
|
||
<span>Token日志</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- 底部 -->
|
||
<div class="sidebar-footer">
|
||
<div class="menu-item single" @click="router.push('/select')">
|
||
<el-icon :size="18"><Switch /></el-icon>
|
||
<span>切换系统</span>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- 移动端遮罩 -->
|
||
<div :class="['sidebar-overlay', { show: mobileSidebarOpen }]" @click="mobileSidebarOpen = false" />
|
||
|
||
<!-- ===== 右侧主体 ===== -->
|
||
<div class="main-area">
|
||
<!-- 顶栏 -->
|
||
<header class="topbar">
|
||
<div class="topbar-left">
|
||
<span class="hamburger" @click="mobileSidebarOpen = !mobileSidebarOpen">
|
||
<el-icon :size="20"><component :is="mobileSidebarOpen ? Fold : Expand" /></el-icon>
|
||
</span>
|
||
<el-breadcrumb separator="/" class="breadcrumb-desk">
|
||
<el-breadcrumb-item :to="{ path: '/admin/dashboard' }">首页</el-breadcrumb-item>
|
||
<el-breadcrumb-item v-if="breadcrumbParent">{{ breadcrumbParent }}</el-breadcrumb-item>
|
||
<el-breadcrumb-item v-if="breadcrumbCurrent">{{ breadcrumbCurrent }}</el-breadcrumb-item>
|
||
</el-breadcrumb>
|
||
</div>
|
||
<div class="topbar-right">
|
||
<el-dropdown trigger="hover" placement="bottom-end">
|
||
<div class="user-chip">
|
||
<el-avatar :size="28" :src="authStore.userInfo?.avatar">
|
||
{{ authStore.userInfo?.realName?.charAt(0) || 'U' }}
|
||
</el-avatar>
|
||
<span class="user-name">{{ authStore.userInfo?.realName || authStore.userInfo?.username || '用户' }}</span>
|
||
<el-icon :size="12" class="user-chevron"><ArrowDown /></el-icon>
|
||
</div>
|
||
<template #dropdown>
|
||
<el-dropdown-menu>
|
||
<el-dropdown-item @click="router.push('/admin/personal')">
|
||
<el-icon><User /></el-icon>个人中心
|
||
</el-dropdown-item>
|
||
<el-dropdown-item @click="router.push('/select')">
|
||
<el-icon><Switch /></el-icon>切换系统
|
||
</el-dropdown-item>
|
||
<el-dropdown-item divided @click="handleLogout">
|
||
<el-icon><SwitchButton /></el-icon>退出登录
|
||
</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</template>
|
||
</el-dropdown>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- 标签栏 -->
|
||
<div class="tab-bar" v-if="tabsStore.tabs.length > 0">
|
||
<div class="tab-list" ref="tabListRef">
|
||
<div
|
||
v-for="tab in tabsStore.tabs"
|
||
:key="tab.path"
|
||
:class="['tab-chip', { active: tabsStore.activeTab === tab.path }]"
|
||
@click="switchTab(tab)"
|
||
@contextmenu.prevent="showMenu($event, tab)"
|
||
>
|
||
<span class="tab-title">{{ tab.title }}</span>
|
||
<el-icon
|
||
v-if="tabsStore.tabs.length > 1"
|
||
class="tab-close"
|
||
:size="12"
|
||
@click.stop="closeTab(tab.path)"
|
||
><Close /></el-icon>
|
||
</div>
|
||
</div>
|
||
<el-dropdown trigger="click" @command="handleTabCmd" class="tab-more">
|
||
<span class="tab-more-btn">
|
||
<el-icon :size="14"><ArrowDown /></el-icon>
|
||
</span>
|
||
<template #dropdown>
|
||
<el-dropdown-menu>
|
||
<el-dropdown-item command="closeCurrent">
|
||
<el-icon><Close /></el-icon>关闭当前
|
||
</el-dropdown-item>
|
||
<el-dropdown-item command="closeOthers">
|
||
<el-icon><Remove /></el-icon>关闭其他
|
||
</el-dropdown-item>
|
||
<el-dropdown-item command="closeAll">
|
||
<el-icon><CircleClose /></el-icon>关闭全部
|
||
</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</template>
|
||
</el-dropdown>
|
||
</div>
|
||
|
||
<!-- 右键菜单 -->
|
||
<Teleport to="body">
|
||
<div
|
||
v-if="ctxMenu.visible"
|
||
class="ctx-overlay"
|
||
@click="ctxMenu.visible = false"
|
||
>
|
||
<div
|
||
class="ctx-menu"
|
||
:style="{ left: ctxMenu.x + 'px', top: ctxMenu.y + 'px' }"
|
||
@click.stop
|
||
>
|
||
<div class="ctx-item" @click="closeTab(ctxMenu.tab?.path); ctxMenu.visible = false">
|
||
<el-icon :size="14"><Close /></el-icon>关闭
|
||
</div>
|
||
<div class="ctx-item" @click="closeOthers(ctxMenu.tab?.path); ctxMenu.visible = false">
|
||
<el-icon :size="14"><Remove /></el-icon>关闭其他
|
||
</div>
|
||
<div class="ctx-item" @click="closeAll(); ctxMenu.visible = false">
|
||
<el-icon :size="14"><CircleClose /></el-icon>关闭全部
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Teleport>
|
||
|
||
<!-- 内容区 -->
|
||
<main class="content">
|
||
<router-view :key="tabsStore.activeTab" />
|
||
</main>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, reactive, watch, onMounted, ref } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import {
|
||
HomeFilled, Monitor, UserFilled, Document,
|
||
ArrowRight, Switch, ArrowDown, SwitchButton,
|
||
User, Close, Remove, CircleClose, Expand, Fold,
|
||
} from '@element-plus/icons-vue'
|
||
import { useAuthStore } from '@/stores/auth'
|
||
import { useTabsStore } from '@/stores/tabs'
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const authStore = useAuthStore()
|
||
const tabsStore = useTabsStore()
|
||
const tabListRef = ref<HTMLElement>()
|
||
const mobileSidebarOpen = ref(false)
|
||
|
||
// 菜单展开状态
|
||
const menuOpen = reactive({ system: true, user: true, log: true })
|
||
|
||
// 当前高亮
|
||
const activeMenu = computed(() => {
|
||
const p = route.path
|
||
const known = ['/admin/dashboard', '/admin/system', '/admin/api', '/admin/dict', '/admin/file', '/admin/email',
|
||
'/admin/user', '/admin/role', '/admin/menu', '/admin/log/login', '/admin/log/operation', '/admin/log/token']
|
||
return known.find(k => p.startsWith(k)) || p
|
||
})
|
||
|
||
const activeGroup = computed(() => {
|
||
const p = route.path
|
||
if (p.startsWith('/admin/system') || p.startsWith('/admin/api') || p.startsWith('/admin/dict') || p.startsWith('/admin/file') || p.startsWith('/admin/email')) return 'system'
|
||
if (p.startsWith('/admin/user') || p.startsWith('/admin/role') || p.startsWith('/admin/menu')) return 'user'
|
||
if (p.startsWith('/admin/log')) return 'log'
|
||
return ''
|
||
})
|
||
|
||
// 面包屑
|
||
const breadcrumbMap: Record<string, { parent?: string; current: string }> = {
|
||
'/admin/dashboard': { current: '首页' },
|
||
'/admin/system': { parent: '系统管理', current: '系统列表' },
|
||
'/admin/api': { parent: '系统管理', current: '接口管理' },
|
||
'/admin/dict': { parent: '系统管理', current: '数据字典' },
|
||
'/admin/file': { parent: '系统管理', current: '文件管理' },
|
||
'/admin/email': { parent: '系统管理', current: '邮件管理' },
|
||
'/admin/user': { parent: '用户权限', current: '用户管理' },
|
||
'/admin/role': { parent: '用户权限', current: '角色管理' },
|
||
'/admin/menu': { parent: '用户权限', current: '菜单管理' },
|
||
'/admin/log/login': { parent: '日志审计', current: '登录日志' },
|
||
'/admin/log/operation': { parent: '日志审计', current: '操作日志' },
|
||
'/admin/log/token': { parent: '日志审计', current: 'Token日志' },
|
||
'/admin/user/form': { parent: '用户权限', current: '用户表单' },
|
||
'/admin/role/form': { parent: '用户权限', current: '角色表单' },
|
||
'/admin/menu/form': { parent: '用户权限', current: '菜单表单' },
|
||
'/admin/system/form': { parent: '系统管理', current: '系统表单' },
|
||
'/admin/api/form': { parent: '系统管理', current: '接口表单' },
|
||
'/admin/dict/form': { parent: '系统管理', current: '字典表单' },
|
||
'/admin/personal': { current: '个人中心' },
|
||
}
|
||
|
||
const breadcrumbParent = computed(() => {
|
||
const key = Object.keys(breadcrumbMap).find(k => route.path.startsWith(k))
|
||
return key ? breadcrumbMap[key].parent : undefined
|
||
})
|
||
const breadcrumbCurrent = computed(() => {
|
||
const key = Object.keys(breadcrumbMap).find(k => route.path.startsWith(k))
|
||
return key ? breadcrumbMap[key].current : undefined
|
||
})
|
||
|
||
// ===== 标签页 =====
|
||
function openPage(path: string, title: string) {
|
||
tabsStore.addTab({ path, meta: { title }, query: {} } as any)
|
||
router.push(path)
|
||
mobileSidebarOpen.value = false
|
||
}
|
||
|
||
function switchTab(tab: any) {
|
||
router.push(tab.path)
|
||
}
|
||
|
||
function closeTab(path: string) {
|
||
tabsStore.removeTab(path)
|
||
if (tabsStore.activeTab) {
|
||
router.push(tabsStore.activeTab)
|
||
} else {
|
||
router.push('/admin/dashboard')
|
||
tabsStore.addTab({ path: '/admin/dashboard', meta: { title: '首页' }, query: {} } as any)
|
||
}
|
||
}
|
||
|
||
function closeOthers(path?: string) {
|
||
if (path) tabsStore.closeOthers(path)
|
||
}
|
||
|
||
function closeAll() {
|
||
tabsStore.closeAll()
|
||
router.push('/admin/dashboard')
|
||
tabsStore.addTab({ path: '/admin/dashboard', meta: { title: '首页' }, query: {} } as any)
|
||
}
|
||
|
||
function handleTabCmd(cmd: string) {
|
||
const cur = tabsStore.activeTab
|
||
if (cmd === 'closeCurrent' && cur) closeTab(cur)
|
||
else if (cmd === 'closeOthers' && cur) closeOthers(cur)
|
||
else if (cmd === 'closeAll') closeAll()
|
||
}
|
||
|
||
// 右键菜单
|
||
const ctxMenu = reactive({ visible: false, x: 0, y: 0, tab: null as any })
|
||
function showMenu(e: MouseEvent, tab: any) {
|
||
ctxMenu.visible = true
|
||
ctxMenu.x = e.clientX
|
||
ctxMenu.y = e.clientY
|
||
ctxMenu.tab = tab
|
||
}
|
||
|
||
// 路由变化自动加标签
|
||
watch(() => route.path, (p) => {
|
||
if (p.startsWith('/admin') && p !== '/admin') {
|
||
tabsStore.addTab(route)
|
||
}
|
||
})
|
||
|
||
onMounted(async () => {
|
||
// 跨域名跳转后 Pinia 状态丢失,重新从服务端加载用户信息
|
||
await authStore.fetchUserInfo()
|
||
if (route.path !== '/admin' && route.path !== '/admin/dashboard') {
|
||
tabsStore.addTab(route)
|
||
}
|
||
// 首页默认标签
|
||
if (!tabsStore.tabs.find((t: any) => t.path === '/admin/dashboard')) {
|
||
tabsStore.addTab({ path: '/admin/dashboard', meta: { title: '首页' }, query: {} } as any)
|
||
}
|
||
if (route.path === '/admin') {
|
||
router.push('/admin/dashboard')
|
||
}
|
||
})
|
||
|
||
async function handleLogout() {
|
||
try { await authStore.logout() } catch {}
|
||
tabsStore.closeAll()
|
||
// 直接完整跳转登录页,避免 router.push 触发组件 API 请求
|
||
window.location.href = import.meta.env.VITE_LOGIN_URL || '/login'
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* ===== 整体布局 ===== */
|
||
.admin-shell { display:flex; height:100vh; overflow:hidden; background:var(--bg-page); }
|
||
|
||
/* ===== 侧边栏 ===== */
|
||
.sidebar {
|
||
width: 240px; flex-shrink:0;
|
||
background: var(--bg-surface);
|
||
border-right: 1px solid var(--border-color);
|
||
display:flex; flex-direction:column; overflow:hidden; z-index:50;
|
||
}
|
||
|
||
.sidebar-logo {
|
||
display:flex; align-items:center; gap:10px;
|
||
height: 58px; padding:0 20px;
|
||
border-bottom: 1px solid var(--border-light);
|
||
cursor:pointer; flex-shrink:0;
|
||
}
|
||
.logo-svg { width:22px; height:22px; color:var(--color-primary); flex-shrink:0; }
|
||
.logo-text { font-size:18px; font-weight:700; color:var(--text-primary); letter-spacing:-0.3px; }
|
||
|
||
.sidebar-nav { flex:1; overflow-y:auto; padding:16px 12px; }
|
||
.sidebar-footer { padding:8px 12px 16px; border-top:1px solid var(--border-light); flex-shrink:0; }
|
||
|
||
/* ===== 菜单项 ===== */
|
||
.menu-item {
|
||
display:flex; align-items:center; gap:10px;
|
||
height:42px; padding:0 14px;
|
||
border-radius: var(--radius-md);
|
||
cursor:pointer; font-size:14px; font-weight:500;
|
||
color: var(--text-secondary);
|
||
transition: all var(--transition-fast);
|
||
white-space:nowrap;
|
||
}
|
||
.menu-item:hover { background:var(--bg-hover); color:var(--text-primary); }
|
||
.menu-item.active { background:var(--color-primary-bg); color:var(--color-primary); font-weight:600; }
|
||
|
||
.menu-item.single { margin-bottom:4px; }
|
||
|
||
.menu-item.parent {
|
||
font-weight:600; font-size:14px; color:var(--text-primary);
|
||
margin-top:2px;
|
||
}
|
||
|
||
.menu-item.child {
|
||
padding-left:48px; height:38px;
|
||
font-size:14px; font-weight:400;
|
||
color: var(--text-muted);
|
||
margin:1px 0;
|
||
}
|
||
.menu-item.child:hover { color:var(--text-primary); }
|
||
.menu-item.child.active { color:var(--color-primary); font-weight:500; }
|
||
|
||
/* 展开箭头 */
|
||
.arrow { margin-left:auto; transition:transform var(--transition-fast); color:var(--text-muted); flex-shrink:0; }
|
||
.arrow.rotated { transform:rotate(90deg); }
|
||
|
||
.sub-items { overflow:hidden; padding-bottom:4px; }
|
||
|
||
/* ===== 分隔线(菜单组之间) ===== */
|
||
.menu-group { padding:4px 0; }
|
||
.menu-group + .menu-group { border-top:1px solid var(--border-light); }
|
||
|
||
/* ===== 主体区域 ===== */
|
||
.main-area { flex:1; display:flex; flex-direction:column; overflow:hidden; min-width:0; }
|
||
|
||
/* ===== 顶栏 ===== */
|
||
.topbar {
|
||
display:flex; align-items:center; justify-content:space-between;
|
||
height:58px; background:var(--bg-surface);
|
||
border-bottom:1px solid var(--border-color);
|
||
padding:0 24px; flex-shrink:0;
|
||
}
|
||
.topbar-left { display:flex; align-items:center; }
|
||
.topbar-right { display:flex; align-items:center; }
|
||
|
||
.user-chip {
|
||
display:flex; align-items:center; gap:8px;
|
||
padding:5px 12px 5px 5px; border-radius:24px;
|
||
cursor:pointer; transition:all var(--transition-fast);
|
||
border:1px solid transparent;
|
||
}
|
||
.user-chip:hover { background:var(--bg-hover); border-color:var(--border-color); }
|
||
.user-name {
|
||
font-size:14px; font-weight:500; color:var(--text-secondary);
|
||
max-width:100px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
|
||
}
|
||
.user-chevron { color:var(--text-muted); flex-shrink:0; }
|
||
|
||
/* ===== 标签栏 ===== */
|
||
.tab-bar {
|
||
display:flex; align-items:center;
|
||
height:42px; background:var(--bg-surface);
|
||
border-bottom:1px solid var(--border-color);
|
||
padding:0 12px; flex-shrink:0; gap:4px;
|
||
}
|
||
.tab-list { display:flex; flex:1; overflow-x:auto; overflow-y:hidden; height:100%; align-items:center; gap:2px; }
|
||
.tab-list::-webkit-scrollbar { height:2px; }
|
||
|
||
.tab-chip {
|
||
display:flex; align-items:center; gap:6px;
|
||
height:34px; padding:0 12px;
|
||
border-radius:var(--radius-sm);
|
||
cursor:pointer; font-size:13px;
|
||
color:var(--text-muted);
|
||
transition:all var(--transition-fast);
|
||
white-space:nowrap; flex-shrink:0;
|
||
}
|
||
.tab-chip:hover { background:var(--bg-hover); color:var(--text-secondary); }
|
||
.tab-chip.active { background:var(--color-primary-bg); color:var(--color-primary); font-weight:500; }
|
||
.tab-title { max-width:120px; overflow:hidden; text-overflow:ellipsis; }
|
||
|
||
.tab-close { border-radius:3px; padding:1px; opacity:0; transition:all var(--transition-fast); }
|
||
.tab-chip:hover .tab-close { opacity:1; }
|
||
.tab-close:hover { background:var(--border-color); color:var(--text-primary); }
|
||
|
||
.tab-more { flex-shrink:0; }
|
||
.tab-more-btn {
|
||
display:flex; align-items:center; justify-content:center;
|
||
width:30px; height:30px; border-radius:var(--radius-sm);
|
||
cursor:pointer; color:var(--text-muted);
|
||
transition:all var(--transition-fast);
|
||
}
|
||
.tab-more-btn:hover { background:var(--bg-hover); color:var(--text-secondary); }
|
||
|
||
/* ===== 右键菜单 ===== */
|
||
.ctx-overlay { position:fixed; inset:0; z-index:9998; }
|
||
.ctx-menu {
|
||
position:fixed; z-index:9999;
|
||
background:var(--bg-surface); border:1px solid var(--border-color);
|
||
border-radius:var(--radius-md); box-shadow:var(--shadow-lg);
|
||
min-width:150px; padding:4px;
|
||
}
|
||
.ctx-item {
|
||
display:flex; align-items:center; gap:8px;
|
||
padding:9px 14px; font-size:14px; cursor:pointer;
|
||
color:var(--text-secondary); border-radius:var(--radius-sm);
|
||
transition:all var(--transition-fast);
|
||
}
|
||
.ctx-item:hover { background:var(--bg-hover); color:var(--text-primary); }
|
||
|
||
/* ===== 内容区 ===== */
|
||
.content { flex:1; overflow-y:auto; padding:24px; }
|
||
|
||
/* ===== 汉堡按钮 ===== */
|
||
.hamburger {
|
||
display: none; align-items: center; justify-content: center;
|
||
width: 36px; height: 36px; border-radius: var(--radius-md);
|
||
cursor: pointer; color: var(--text-secondary);
|
||
margin-right: 8px; flex-shrink: 0;
|
||
}
|
||
.hamburger:hover { background: var(--bg-hover); color: var(--color-primary); }
|
||
|
||
/* ===== 侧边栏遮罩 ===== */
|
||
.sidebar-overlay {
|
||
display: none;
|
||
position: fixed; inset: 0; z-index: 99;
|
||
background: rgba(0,0,0,0.4);
|
||
}
|
||
|
||
/* ===== 移动端适配 ===== */
|
||
@media (max-width: 768px) {
|
||
.hamburger { display: flex; }
|
||
.breadcrumb-desk { display: none; }
|
||
|
||
/* 侧边栏滑出 */
|
||
.sidebar {
|
||
position: fixed; top: 0; left: 0; bottom: 0; z-index: 100;
|
||
width: 260px !important;
|
||
transform: translateX(-100%);
|
||
transition: transform 0.25s ease;
|
||
box-shadow: none;
|
||
}
|
||
.sidebar.open {
|
||
transform: translateX(0);
|
||
}
|
||
|
||
.sidebar-overlay.show { display: block; }
|
||
|
||
/* 主体全宽 */
|
||
.main-area { width: 100%; }
|
||
|
||
/* 顶栏 */
|
||
.topbar { padding: 0 12px; height: 50px; }
|
||
.user-chip { padding: 4px 8px 4px 4px; }
|
||
.user-name { display: none; }
|
||
|
||
/* 标签栏 */
|
||
.tab-bar { height: 36px; padding: 0 6px; }
|
||
.tab-chip { height: 28px; padding: 0 8px; font-size: 12px; }
|
||
.tab-title { max-width: 80px; }
|
||
|
||
/* 内容区 */
|
||
.content { padding: 12px; }
|
||
}
|
||
</style>
|