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.
 
 
 
 
 
 

201 lines
10 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 />
<!-- 页面内容 -->
<div class="p-6">
<slot />
</div>
</main>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import { useAuthStore } from '~/stores/auth'
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())
// 菜单分组(严格对齐 common.js renderSidebar,含新增 svcconfig/calltranslate)
const menuGroups = [
{
label: '数据概览',
items: [
{ id: 'dashboard', label: '仪表盘', to: '/dashboard', icon: 'M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z' },
],
},
{
label: '生产管理',
items: [
{ id: 'factory', label: '厂家管理', to: '/factory', icon: 'M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4' },
{ id: 'product', label: '产品管理', to: '/product', icon: 'M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4' },
{ id: 'code', label: '公码管理', to: '/code', icon: 'M4 4h6v6H4V4zm10 0h6v6h-6V4zM4 14h6v6H4v-6zm10 2h2m-2 4h6m-4-4v4m4-8h2m-2 0v4' },
{ id: 'device', label: '生产管理', to: '/device', icon: 'M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z' },
],
},
{
label: '用户管理',
items: [
{ id: 'user', label: '用户查询', to: '/users', icon: 'M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z' },
],
},
{
label: '服务配置',
items: [
{ id: 'globalconfig', label: '全局环境配置', to: '/globalconfig', icon: 'M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9' },
{ id: 'mcpconfig', label: 'MCP配置', to: '/mcpconfig', icon: 'M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-3-4h.01M16 16h.01' },
{ id: 'meettemplate', label: '会议模板', to: '/meettemplates', icon: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z' },
{ id: 'svcconfig', label: '依赖服务配置', to: '/serviceconfig', icon: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z' },
{ id: 'calltranslate', label: '通话翻译配置', to: '/calltranslate', icon: 'M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129' },
],
},
{
label: '系统管理',
items: [
{ id: 'apps', label: '应用注册', to: '/apps', icon: 'M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10' },
{ id: 'statlog', label: '统计日志', to: '/statlog', icon: 'M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4' },
{ id: 'admin', label: '后台账号', to: '/accounts', icon: 'M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z' },
],
},
]
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>