diff --git a/docker-compose.yml b/docker-compose.yml
index 3346ace..05f5afd 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -10,12 +10,13 @@ services:
- issue-reports:/app/reports
# .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/issue.db
- JWT_SECRET=${JWT_SECRET:-oa-shared-jwt-secret-tlyq-2026}
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-admin123}
+ - LLDAP_ADMIN_PASSWORD=${LLDAP_ADMIN_PASSWORD}
- ASSETS_API_URL=${ASSETS_API_URL:-https://assets.tlyq.ai/api}
- ASSETS_API_KEY=${ASSETS_API_KEY}
- ALLOWED_API_KEYS=${ALLOWED_API_KEYS}
diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx
index b3f929a..a7fe5d8 100644
--- a/src/app/(auth)/login/page.tsx
+++ b/src/app/(auth)/login/page.tsx
@@ -1,79 +1,7 @@
'use client'
-import { useState } from 'react'
+// 登录页 — 引用共享组件
+import LoginPage from '@shared/ui/login-page'
-export default function LoginPage() {
- const [username, setUsername] = useState('')
- const [password, setPassword] = useState('')
- const [error, setError] = useState('')
- const [loading, setLoading] = useState(false)
- const [showLdapForm, setShowLdapForm] = useState(false)
-
- const handleSubmit = async (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 (
-
-
-
IT 工单跟踪系统
- {error &&
{error}
}
-
- {!showLdapForm ? (
- <>
-
-
通过 SSO 统一身份认证
-
-
-
-
-
-
- >
- ) : (
- <>
-
-
-
-
- >
- )}
-
-
- )
+export default function Page() {
+ return
}
diff --git a/src/app/globals.css b/src/app/globals.css
index b961b63..559d27b 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -1,14 +1,16 @@
@import "tailwindcss";
@config "../../tailwind.config.js";
+@import './tlyq-design-system.css' layer(base);
@layer base {
body {
- background-color: #f8fafc;
- color: #0f172a;
+ font-family: var(--font-body);
+ color: var(--fg);
+ background-color: var(--bg);
}
html.dark body {
- background-color: #020617;
- color: #ffffff;
+ background-color: var(--bg);
+ color: var(--fg);
}
}
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/AppShell.tsx b/src/components/layout/AppShell.tsx
index 7e59542..583e811 100644
--- a/src/components/layout/AppShell.tsx
+++ b/src/components/layout/AppShell.tsx
@@ -12,7 +12,7 @@ export default function AppShell({ children }: AppShellProps) {
}, [])
return (
diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx
index c715a28..1f6d04d 100644
--- a/src/components/layout/Sidebar.tsx
+++ b/src/components/layout/Sidebar.tsx
@@ -1,82 +1,81 @@
'use client'
-import { useState, useEffect } from 'react'
+// src/components/layout/Sidebar.tsx — 权限驱动侧边栏(统一标准布局)
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { LayoutDashboard, FileText, Settings, Users, Shield, Key, Clock, CheckCircle, PlusSquare, Upload, List, Mail, Bell } from 'lucide-react'
-const navItems = [
- { href: '/dashboard', label: '仪表盘', icon: LayoutDashboard, perm: null },
- { href: '/tickets/pending', label: '待办工单', icon: Clock, perm: 'tickets:read' },
- { href: '/tickets/completed', label: '已办工单', icon: CheckCircle, perm: 'tickets:read' },
- { href: '/tickets/create', label: '手动建单', icon: PlusSquare, perm: 'tickets:create' },
- { href: '/tickets/import', label: '导入工单', icon: Upload, perm: 'tickets:import' },
- { href: '/reports', label: '报告管理', icon: FileText, perm: 'reports: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' },
- { href: '/settings/monitor', label: '邮件监控', icon: Mail, perm: 'monitor:read' },
- { href: '/settings/wechat', label: '微信推送', icon: Bell, perm: 'monitor:read' },
-]
-
-function hasAnyAdminPerm(permissions: string[]): boolean {
- return permissions.includes('*') || permissions.some(p =>
- p.startsWith('users:') || p.startsWith('roles:') || p.startsWith('api-keys:') ||
- p.startsWith('audit-logs:') || p.startsWith('monitor:')
- )
-}
-
-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: '/tickets/pending', icon: Clock, perm: null },
+ { label: '已办工单', href: '/tickets/completed', icon: CheckCircle, perm: null },
+ { label: '手动建单', href: '/tickets/create', icon: PlusSquare, perm: null },
+ { label: '导入工单', href: '/tickets/import', icon: Upload, perm: null },
+ { label: '全部工单', href: '/tickets/all', icon: List, perm: isAdmin ? null : 'hidden' },
+ ],
+ },
+ {
+ title: '报告',
+ items: [{ label: '报告管理', href: '/reports', icon: FileText, 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' },
+ { label: '邮件监控', href: '/settings/monitor', icon: Mail, perm: isAdmin ? null : 'hidden' },
+ { label: '微信推送', href: '/settings/wechat', icon: Bell, perm: isAdmin ? null : 'hidden' },
+ ],
+ },
+ ]
return (
-