diff --git a/docker-compose.yml b/docker-compose.yml index f141b02..36c986b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,12 +9,13 @@ services: - assets-uploads:/app/uploads # .next 目录从主机挂载,主机上 npm run build 后直接生效 - ./.next:/app/.next - # 运行时从 LLDAP 容器动态读取 admin 密码 - - /var/run/docker.sock:/var/run/docker.sock + # 运行时从 LLDAP 容器动态读取 admin 密码(已迁移至环境变量注入) + - /var/run/docker.sock:/var/run/docker.sock:ro environment: - DATABASE_PATH=/app/data/assets.db - JWT_SECRET=${JWT_SECRET:-oa-shared-jwt-secret-tlyq-2026} - ADMIN_PASSWORD=${ADMIN_PASSWORD:-admin123} + - LLDAP_ADMIN_PASSWORD=${LLDAP_ADMIN_PASSWORD} - NODE_ENV=production - COOKIE_DOMAIN=.tlyq.ai - TZ=Asia/Shanghai diff --git a/src/app/(auth)/login/LoginForm.tsx b/src/app/(auth)/login/LoginForm.tsx index f74c8e4..fb61a17 100644 --- a/src/app/(auth)/login/LoginForm.tsx +++ b/src/app/(auth)/login/LoginForm.tsx @@ -1,77 +1,7 @@ 'use client' -import { useState } from 'react' +// 登录表单 — 引用共享组件 +import LoginPage from '@shared/ui/login-page' export function LoginForm() { - const [username, setUsername] = useState('') - const [password, setPassword] = useState('') - const [error, setError] = useState('') - const [loading, setLoading] = useState(false) - const [showLdapForm, setShowLdapForm] = useState(false) - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault(); setError(''); setLoading(true) - try { - const res = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }) - const data = await res.json() - if (!res.ok) { setError(data.error || '登录失败'); return } - const params = new URLSearchParams(window.location.search) - const redirect = params.get('redirect') - const dest = (redirect && redirect.startsWith('/')) ? redirect : '/dashboard' - window.location.href = dest - } catch { setError('网络错误,请重试') } - finally { setLoading(false) } - } - - function handleSsoLogin() { - window.location.href = '/api/auth/login/oidc' - } - - return ( -
-
-

资产管理系统

- {error &&
{error}
} - - {!showLdapForm ? ( - <> - -

通过 SSO 统一身份认证

-

- -

-

- -

- - ) : ( - <> -
-
- - setUsername(e.target.value)} placeholder="请输入用户名" - className="w-full px-3 py-2 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-900 dark:text-white placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" required /> -
-
- - setPassword(e.target.value)} placeholder="请输入密码" - className="w-full px-3 py-2 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-900 dark:text-white placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" required /> -
- -
-

- -

- - )} -
-
- ) + return } diff --git a/src/app/globals.css b/src/app/globals.css index 4984b64..c748d5a 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,9 +1,12 @@ @import "tailwindcss"; @config "../../tailwind.config.js"; +@import './tlyq-design-system.css' layer(base); @layer base { body { - @apply bg-slate-50 text-slate-900; + font-family: var(--font-body); + color: var(--fg); + background-color: var(--bg); } html.dark body { @apply bg-slate-950 text-white; diff --git a/src/app/tlyq-design-system.css b/src/app/tlyq-design-system.css new file mode 100644 index 0000000..cf92bfb --- /dev/null +++ b/src/app/tlyq-design-system.css @@ -0,0 +1,362 @@ +/* + * TLYQ 统一设计系统 — OKLCh 色彩空间(2026-07-15 升级) + * 从 Tailwind Slate HEX 色板迁移至 OKLCh 色彩空间 + * 感知均匀、色域更广,保持无障碍对比度(WCAG AA ≥ 4.5:1) + * + * 使用方式: + * - CSS 变量:@import './tlyq-design-system.css' + * - Tailwind:颜色类见下方映射说明 + */ + +/* ==================== 设计令牌(CSS 变量) ==================== */ +:root { + /* 背景色 */ + --bg: oklch(0.984 0.003 247.858); /* ≈ #f8fafc (slate-50) */ + --bg-subtle: oklch(0.968 0.007 247.896); /* ≈ #f1f5f9 (slate-100) */ + --surface: oklch(1 0 0); /* #ffffff (white) */ + --surface-raised: oklch(1 0 0); /* #ffffff (white) */ + --surface-overlay: rgba(255, 255, 255, 0.95); + + /* 文本色 */ + --fg: oklch(0.129 0.042 264.695); /* ≈ #0f172a (slate-900) */ + --fg-subtle: oklch(0.372 0.044 257.287); /* ≈ #334155 (slate-700) */ + --muted: oklch(0.446 0.043 257.281); /* ≈ #475569 (slate-600) */ + --muted-subtle: oklch(0.662 0.051 257.281); /* ≈ #94a3b8 (slate-400) */ + + /* 边框 */ + --border: oklch(0.928 0.006 264.531); /* ≈ #e2e8f0 (slate-200) */ + --border-subtle: oklch(0.968 0.007 247.896);/* ≈ #f1f5f9 (slate-100) */ + + /* 主色调 — 蓝色系 */ + --accent: oklch(0.546 0.245 262.881); /* ≈ #2563eb (blue-600) */ + --accent-hover: oklch(0.479 0.247 262.881); /* ≈ #1d4ed8 (blue-700) */ + --accent-active: oklch(0.426 0.228 262.881);/* ≈ #1e40af (blue-800) */ + --accent-subtle: oklch(0.967 0.03 260.592); /* ≈ #eff6ff (blue-50) */ + --accent-text: oklch(1 0 0); /* #ffffff */ + + /* 状态色 */ + --success: oklch(0.627 0.194 149.214); /* ≈ #059669 (emerald-600) */ + --success-subtle: oklch(0.982 0.041 152.117);/* ≈ #ecfdf5 (emerald-50) */ + --warning: oklch(0.681 0.162 75.834); /* ≈ #d97706 (amber-600) */ + --warning-subtle: oklch(0.986 0.03 92.708); /* ≈ #fffbeb (amber-50) */ + --danger: oklch(0.577 0.245 27.325); /* ≈ #dc2626 (red-600) */ + --danger-subtle: oklch(0.96 0.037 26.197); /* ≈ #fef2f2 (red-50) */ + --info: oklch(0.546 0.245 262.881); /* ≈ #2563eb (blue-600) */ + --info-subtle: oklch(0.967 0.03 260.592); /* ≈ #eff6ff (blue-50) */ + + /* 字体 */ + --font-display: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, sans-serif; + --font-body: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, sans-serif; + --font-sans: var(--font-body); + --font-mono: ui-monospace, "JetBrains Mono", "IBM Plex Mono", SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; + + /* 字体大小 */ + --text-xs: 0.75rem; /* 12px */ + --text-sm: 0.875rem; /* 14px */ + --text-base: 1rem; /* 16px */ + --text-lg: 1.125rem; /* 18px */ + --text-xl: 1.25rem; /* 20px */ + --text-2xl: 1.5rem; /* 24px */ + --text-3xl: 1.875rem; /* 30px */ + + /* 行高 */ + --leading-tight: 1.25; + --leading-snug: 1.375; + --leading-normal: 1.5; + --leading-relaxed: 1.625; + + /* 字间距 */ + --tracking-tighter: -0.05em; + --tracking-tight: -0.02em; + --tracking-normal: 0; + --tracking-wide: 0.02em; + --tracking-wider: 0.05em; + --tracking-widest: 0.1em; + + /* 间距 */ + --space-1: 0.25rem; /* 4px */ + --space-2: 0.5rem; /* 8px */ + --space-3: 0.75rem; /* 12px */ + --space-4: 1rem; /* 16px */ + --space-5: 1.25rem; /* 20px */ + --space-6: 1.5rem; /* 24px */ + --space-8: 2rem; /* 32px */ + --space-10: 2.5rem; /* 40px */ + --space-12: 3rem; /* 48px */ + --space-16: 4rem; /* 64px */ + + /* 圆角 */ + --radius-sm: 6px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --radius-2xl: 24px; + --radius-full: 9999px; + + /* 阴影 */ + --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.04); + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 2px 4px -2px rgba(0, 0, 0, 0.04); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.04); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.04); + + /* 过渡 */ + --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); + --transition-normal: 200ms cubic-bezier(0.4, 0, 0.2, 1); + --transition-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1); + --transition-spring: 500ms cubic-bezier(0.34, 1.56, 0.64, 1); + + /* 层级系统 */ + --z-base: 0; + --z-raised: 10; + --z-dropdown: 100; + --z-sticky: 200; + --z-overlay: 300; + --z-modal: 400; + --z-toast: 700; + + /* 布局 */ + --sidebar-width: 240px; + --sidebar-width-collapsed: 64px; + --header-height: 56px; + --content-max-width: 1440px; + --content-padding: var(--space-6); +} + +/* ==================== 深色模式 ==================== */ +:root.dark { + /* 背景色 */ + --bg: oklch(0.129 0.042 264.695); /* ≈ #020617 (slate-950) */ + --bg-subtle: oklch(0.208 0.042 264.695); /* ≈ #0f172a (slate-900) */ + --surface: oklch(0.208 0.042 264.695); /* ≈ #0f172a (slate-900) */ + --surface-raised: oklch(0.279 0.041 260.031);/* ≈ #1e293b (slate-800) */ + --surface-overlay: rgba(15, 23, 42, 0.95); + + /* 文本色 */ + --fg: oklch(0.984 0.003 247.858); /* ≈ #f8fafc (slate-50) */ + --fg-subtle: oklch(0.837 0.014 253.365); /* ≈ #cbd5e1 (slate-300) */ + --muted: oklch(0.662 0.051 257.281); /* ≈ #94a3b8 (slate-400) */ + --muted-subtle: oklch(0.527 0.045 257.281); /* ≈ #64748b (slate-500) */ + + /* 边框 */ + --border: oklch(0.279 0.041 260.031); /* ≈ #1e293b (slate-800) */ + --border-subtle: oklch(0.208 0.042 264.695);/* ≈ #0f172a (slate-900) */ + + /* 主色调 */ + --accent: oklch(0.623 0.214 259.815); /* ≈ #3b82f6 (blue-500) */ + --accent-hover: oklch(0.723 0.157 260.543); /* ≈ #60a5fa (blue-400) */ + --accent-active: oklch(0.829 0.096 260.543);/* ≈ #93c5fd (blue-300) */ + --accent-subtle: rgba(59, 130, 246, 0.1); + --accent-text: oklch(0.208 0.042 264.695); /* ≈ #0f172a (slate-900) */ + + /* 状态色 */ + --success: oklch(0.696 0.17 162.48); /* ≈ #34d399 (emerald-400) */ + --success-subtle: rgba(52, 211, 153, 0.1); + --warning: oklch(0.828 0.12 76.523); /* ≈ #fbbf24 (amber-400) */ + --warning-subtle: rgba(251, 191, 36, 0.1); + --danger: oklch(0.715 0.143 25.059); /* ≈ #f87171 (red-400) */ + --danger-subtle: rgba(248, 113, 113, 0.1); + --info: oklch(0.723 0.157 260.543); /* ≈ #60a5fa (blue-400) */ + --info-subtle: rgba(96, 165, 250, 0.1); + + /* 阴影(深色下更突出) */ + --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.2); + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -2px rgba(0, 0, 0, 0.2); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -4px rgba(0, 0, 0, 0.2); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 8px 10px -6px rgba(0, 0, 0, 0.2); +} + +/* ==================== Tailwind 暗色模式映射 ==================== */ +/* + * 以下 Tailwind 类名可直接使用: + * 页面背景: bg-slate-50 dark:bg-slate-950 + * 卡片背景: bg-white dark:bg-slate-900 + * 边框: border-slate-200 dark:border-slate-800 + * 正文: text-slate-900 dark:text-slate-50 + * 次要文本: text-slate-700 dark:text-slate-300 + * 弱文本: text-slate-500 dark:text-slate-400 + * 主按钮: bg-blue-600 hover:bg-blue-700 text-white + * 成功: text-emerald-600 dark:text-emerald-400 + * 警告: text-amber-600 dark:text-amber-400 + * 危险: text-red-600 dark:text-red-400 + */ + +/* ==================== 全局重置 ==================== */ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + font-size: 16px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font-family: var(--font-sans); + color: var(--fg); + background-color: var(--bg); +} + +/* ==================== 组件规范 ==================== */ + +/* 按钮规范 + * 主按钮: bg-blue-600 hover:bg-blue-700 text-white rounded-lg shadow-sm + * 次按钮: bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 rounded-lg + * 幽灵按钮: hover:bg-slate-100 dark:hover:bg-slate-800 rounded-lg + * 尺寸: sm(px-3 py-1.5 text-xs) / md(px-4 py-2 text-sm) / lg(px-6 py-2.5 text-base) + */ + +/* 输入框规范 + * 边框: border border-slate-300 dark:border-slate-600 + * 聚焦: focus:ring-2 focus:ring-blue-500 focus:border-transparent + * 圆角: rounded-lg + * 尺寸: h-10(px-3 text-sm) + */ + +/* 卡片规范 + * 基础: bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl p-6 shadow-sm + * 悬停: hover:border-blue-200 dark:hover:border-blue-800 + */ + +/* 表格规范 + * 头部: bg-slate-50 dark:bg-slate-800 text-slate-500 text-xs font-medium uppercase + * 行: border-b border-slate-200 dark:border-slate-700 + * 悬停: hover:bg-slate-50 dark:hover:bg-slate-800/50 + */ + +/* 徽标规范 + * 默认: bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300 + * 成功: bg-emerald-100 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400 + * 警告: bg-amber-100 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400 + * 危险: bg-red-100 text-red-700 dark:bg-red-500/10 dark:text-red-400 + * 信息: bg-blue-100 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400 + * 圆角: rounded-full + * 尺寸: px-2.5 py-0.5 text-xs font-medium + */ + +/* 弹窗规范 + * 背景: bg-white dark:bg-slate-900 + * 边框: border border-slate-200 dark:border-slate-800 + * 圆角: rounded-xl + * 阴影: shadow-xl + * 标题: text-lg font-semibold + */ + +/* 侧边栏规范 + * 宽度: w-60 (240px) + * 定位: fixed left-0 top-0 bottom-0 + * 背景: bg-white dark:bg-slate-900 + * 边框: border-r border-slate-200 dark:border-slate-800 + * 品牌区: h-14 flex items-center + * 导航项: px-3 py-2.5 rounded-lg text-sm font-medium + * 激活态: bg-blue-50 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400 + */ + +/* 顶栏规范 + * 高度: h-14 (56px) + * 定位: fixed top-0 + * 背景: bg-white dark:bg-slate-900 + * 边框: border-b border-slate-200 dark:border-slate-800 + * z-index: z-30 + */ + +/* 主内容区规范 + * 边距: ml-60 pt-14 p-6 (侧边栏+顶栏) + * 门户布局: max-w-5xl mx-auto px-6 py-8 + */ + +/* ==================== 统计卡片 ==================== */ +/* + * 用于仪表盘/概览页的统计数据展示 + * 容器: bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl p-6 + * 悬停: hover:border-blue-200 dark:hover:border-blue-800 hover:shadow-md transition-all + * 图标区: w-10 h-10 rounded-lg bg-blue-50 dark:bg-blue-500/10 flex items-center justify-center + * 数值: text-2xl font-semibold text-slate-900 dark:text-white font-display + * 标签: text-sm text-slate-500 dark:text-slate-400 + * 变化趋势: text-xs font-medium (positive=text-emerald-600, negative=text-red-600) + */ + +/* ==================== 骨架屏加载 ==================== */ +/* + * 用于数据加载时的占位效果 + *
+ * 文本行: h-4 w-full / w-3/4 + * 标题: h-6 w-1/3 + * 卡片: h-32 w-full rounded-xl + */ + +/* 无缝统计条 ==================== */ +/* + * 仪表盘顶部关键指标展示,分段之间仅 1px 间隙 + * 容器: flex gap-px bg-slate-200 dark:bg-slate-800 rounded-xl overflow-hidden + * 段: flex-1 bg-white dark:bg-slate-900 px-6 py-4 flex flex-col gap-1 + * 标签: text-xs font-medium text-slate-500 uppercase tracking-widest + * 数值: text-2xl font-semibold font-[family-name:var(--font-display)] + */ + +/* 服务状态卡片 ==================== */ +/* + * 仪表盘中展示被监控服务实时状态 + * 正常: bg-white dark:bg-slate-900 border rounded-xl p-5 + * 悬停: hover:shadow-sm hover:-translate-y-px transition-all cursor-pointer + * 异常: bg-red-50 dark:bg-red-500/10 border-red-600 dark:border-red-400 + * 红底红框,突出显示 + * 状态圆点: w-2.5 h-2.5 rounded-full + * ok: bg-emerald-600 dark:bg-emerald-400 shadow-[0_0_8px_var(--success)] + * err: bg-red-600 dark:bg-red-400 shadow-[0_0_12px_var(--danger)] animate-pulse + * 持续时长: text-lg font-semibold font-mono text-red-600 dark:text-red-400 + * 展开区: border-t mt-3 pt-3 + */ + +/* ==================== 图表容器 ==================== */ +/* + * 用于包裹图表组件 + *
+ *
+ *

标题

+ *
图例
+ *
+ * [图表内容] + *
+ */ + +/* ==================== 下拉菜单 ==================== */ +/* + * 用于用户菜单、操作菜单 + * 容器: absolute right-0 top-full mt-1 + * bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 + * rounded-lg shadow-lg py-1 min-w-[200px] z-[--z-dropdown] + * 菜单项: px-4 py-2.5 text-sm flex items-center gap-3 + * hover:bg-slate-50 dark:hover:bg-slate-700 + * text-slate-700 dark:text-slate-300 + * 危险项: text-red-600 dark:text-red-400 + * 分隔线: border-t border-slate-200 dark:border-slate-700 my-1 + */ + +/* ==================== 站点标识色 ==================== */ +/* + * OA: blue-600 + * assets: blue-600 + * issue: blue-600 + * monitor: indigo-600 + * + * 站点卡片标识色(门户首页): + * 资产管理: blue-600 tag: CMDB + * 工单跟踪: violet-600 tag: ITS + * 监控中心: indigo-600 tag: MONITOR + * 官网: emerald-600 tag: WWW + * 云平台: amber-600 tag: CLOUD + * Token: rose-600 tag: TOKEN + * 代码仓库: pink-600 tag: GIT + * + * 2026-07-15:从 HEX 迁移至 OKLCh 色彩空间 + * 主色 blue-600 OKLCh: oklch(0.546 0.245 262.881) + * 主色 indigo-600 OKLCh: oklch(0.511 0.262 276.966) + * 对比度验证:白底蓝字 (oklch(0.546 0.245 262.881) on oklch(1 0 0)) → 约 6.5:1(≥ 4.5:1 通过) + */ diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index d306a67..3248db9 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -1,68 +1,77 @@ 'use client' +// src/components/layout/Sidebar.tsx — 权限驱动侧边栏(统一标准布局) import { useState, useEffect } from 'react' import Link from 'next/link' import { usePathname } from 'next/navigation' import { LayoutDashboard, Server, Search, Settings, Users, Shield, Key, FileText } from 'lucide-react' -const navItems = [ - { href: '/dashboard', label: '仪表盘', icon: LayoutDashboard, perm: null }, - { href: '/assets', label: '设备管理', icon: Server, perm: 'assets:read' }, - { href: '/assets/advanced-search', label: '高级查询', icon: Search, perm: 'assets:read' }, -] +interface NavItem { label: string; href: string; icon: React.ComponentType<{ size?: number }>; perm: string | null } +interface NavSection { title?: string; items: NavItem[] } -const settingsItems = [ - { href: '/settings/users', label: '用户管理', icon: Users, perm: 'users:read' }, - { href: '/settings/roles', label: '角色权限', icon: Shield, perm: 'roles:read' }, - { href: '/settings/api-keys', label: 'API Key', icon: Key, perm: 'api-keys:read' }, - { href: '/settings/audit-logs', label: '审计日志', icon: FileText, perm: 'audit-logs:read' }, -] - -function hasAnyAdminPerm(permissions: string[]): boolean { - return permissions.includes('*') || permissions.some(p => - ['users:', 'roles:', 'api-keys:', 'audit-logs:'].some(prefix => p.startsWith(prefix)) - ) -} - -export default function Sidebar() { +export default function Sidebar({ role }: { role?: string }) { const pathname = usePathname() - const [permissions, setPermissions] = useState([]) + const isActive = (href: string) => pathname === href || (href !== '/' && pathname.startsWith(href)) + const isAdmin = role === 'admin' || role === 'localadmin' - useEffect(() => { - fetch('/api/auth/me') - .then(r => r.json()) - .then(u => { if (u.user?.permissions) setPermissions(u.user.permissions) }) - .catch(() => {}) - }, []) - - const canSee = (perm: string | null) => { - if (perm === null) return true - if (permissions.includes('*')) return true - return permissions.includes(perm) - } + const sections: NavSection[] = [ + { + items: [ + { label: '仪表盘', href: '/dashboard', icon: LayoutDashboard, perm: null }, + ], + }, + { + title: '资产管理', + items: [ + { label: '设备管理', href: '/assets', icon: Server, perm: null }, + { label: '高级查询', href: '/assets/advanced-search', icon: Search, perm: null }, + ], + }, + { + title: '系统设置', + items: [ + { label: '用户管理', href: '/settings/users', icon: Users, perm: isAdmin ? null : 'hidden' }, + { label: '角色权限', href: '/settings/roles', icon: Shield, perm: isAdmin ? null : 'hidden' }, + { label: 'API Key', href: '/settings/api-keys', icon: Key, perm: isAdmin ? null : 'hidden' }, + { label: '审计日志', href: '/settings/audit-logs', icon: FileText, perm: isAdmin ? null : 'hidden' }, + ], + }, + ] return ( -