You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

165 lines
7.0 KiB

<template>
<div class="flex min-h-screen">
<!-- ── 侧边栏 ── -->
<aside
class="fixed top-0 left-0 bottom-0 z-30 overflow-y-auto flex flex-col"
style="width:256px; background:#1E293B;"
>
<!-- Logo 区 -->
<div style="padding:20px 20px 16px; border-bottom:1px solid rgba(255,255,255,0.06);">
<div style="display:flex; align-items:center; gap:12px;">
<div style="width:40px; height:40px; background:linear-gradient(135deg,#4F46E5,#7C3AED); border-radius:12px; display:flex; align-items:center; justify-content:center; box-shadow:0 6px 20px rgba(79,70,229,0.35); flex-shrink:0;">
<svg style="width:22px; height:22px; color:#fff;" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<div>
<div style="color:#fff; font-weight:700; font-size:15px; letter-spacing:-0.02em;">{{ auth.siteTitle || 'Admin' }}</div>
<div style="color:#64748B; font-size:11px; font-weight:500;">Admin Console · v1.0.1</div>
</div>
</div>
</div>
<!-- 导航 -->
<nav style="padding:12px 0; flex:1;">
<template v-for="group in menuGroups">
<div v-if="hasVisibleItems(group)" :key="group.label" class="group-title">{{ group.label }}</div>
<template v-for="item in group.items">
<NuxtLink
v-if="auth.hasAccess(item.id)"
:key="item.id"
:to="item.to"
class="sidebar-link"
active-class="active"
>
<svg class="icon" width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="item.icon" />
</svg>
{{ item.label }}
</NuxtLink>
</template>
</template>
</nav>
<!-- 底部账号信息卡 -->
<div style="margin-top:auto; padding:14px 18px; border-top:1px solid rgba(255,255,255,0.06);">
<div style="display:flex; align-items:center; gap:10px;">
<div style="width:32px; height:32px; border-radius:9px; background:linear-gradient(135deg,#4F46E5,#7C3AED); color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700; flex-shrink:0;">
{{ avatarChar }}
</div>
<div style="min-width:0;">
<div style="color:#E2E8F0; font-size:13px; font-weight:600; line-height:1.2; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ auth.account || '-' }}</div>
<div style="color:#64748B; font-size:11px; margin-top:2px;">{{ identityLabel }}</div>
</div>
</div>
</div>
</aside>
<!-- ── 主内容区 ── -->
<main class="flex-1" style="margin-left:256px;">
<!-- 顶部栏 -->
<header class="bg-white shadow-sm sticky top-0 z-20 h-16 flex items-center justify-between px-6">
<!-- 面包屑 -->
<div class="flex items-center gap-2 text-sm">
<span class="text-slate-400">首页</span>
<span class="text-slate-300">/</span>
<span class="text-slate-700 font-medium">{{ pageTitle }}</span>
</div>
<!-- 右侧操作 -->
<div class="flex items-center gap-2">
<!-- 通知 -->
<div class="dropdown dropdown-end">
<div tabindex="0" class="btn btn-ghost btn-sm btn-circle relative">
<svg class="h-5 w-5 text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
</div>
<div tabindex="0" class="dropdown-content z-[2] shadow-xl bg-white rounded-xl w-80 border" style="max-height:400px;">
<div class="flex items-center justify-between px-4 py-3 border-b">
<span class="text-sm font-semibold text-slate-700">系统通知</span>
</div>
<div class="px-3 py-6 text-center text-slate-400 text-xs">暂无新通知</div>
</div>
</div>
<!-- 用户菜单 -->
<div class="dropdown dropdown-end">
<div tabindex="0" class="flex items-center gap-2 cursor-pointer hover:bg-slate-50 rounded-lg px-3 py-1.5">
<div class="avatar placeholder">
<div class="bg-indigo-600 text-white rounded-full w-8">
<span class="text-sm">{{ avatarChar }}</span>
</div>
</div>
<div>
<div class="text-sm text-slate-700 leading-tight">{{ auth.account }}</div>
<div class="text-[10px] text-slate-400">{{ identityLabelShort }}</div>
</div>
</div>
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow-lg bg-white rounded-lg w-44 border">
<li><a class="text-sm text-red-500 cursor-pointer" @click="logout">退出登录</a></li>
</ul>
</div>
</div>
</header>
<!-- Toast -->
<AppToast />
<!-- 全局处理中遮罩(写操作期间显示,防重复点击) -->
<AppLoading />
<!-- 页面内容 -->
<div class="p-6">
<slot />
</div>
</main>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import { useAuthStore } from '~/stores/auth'
import { MENU_GROUPS } from '~/utils/menus'
const auth = useAuthStore()
const router = useRouter()
const route = useRoute()
function logout() {
auth.logout()
router.push('/')
}
// 身份标签(侧栏用全称,顶栏用简称,与 common.js 一致)
const identityLabel = computed(() => {
const map: Record<number, string> = { 1: '超级管理员', 2: '管理员', 3: '代理', 4: '运营' }
return map[auth.identity] ?? ''
})
const identityLabelShort = computed(() => {
const map: Record<number, string> = { 1: '超管', 2: '管理员', 3: '代理', 4: '运营' }
return map[auth.identity] ?? ''
})
const avatarChar = computed(() => (auth.account || 'A').charAt(0).toUpperCase())
// 菜单分组:统一取自 utils/menus.ts(权限编辑器/路由守卫共用同一份目录)
const menuGroups = MENU_GROUPS
function hasVisibleItems(group: typeof menuGroups[number]) {
return group.items.some((item) => auth.hasAccess(item.id))
}
const pageTitle = computed(() => {
if (route.meta?.title) return route.meta.title as string
for (const g of menuGroups) {
const item = g.items.find((m) => route.path.startsWith(m.to))
if (item) return item.label
}
return 'Console'
})
onMounted(() => {
if (!auth.token) router.push('/')
})
</script>