feat: 前端侧边栏和全部工单页面改为基于permissions数组控制显隐

- Sidebar: isAdmin boolean → permissions string[],所有导航项按 perm 属性过滤
- 侧边栏 navItems 新增 perm 字段,canSee() 检查 tickets:read/tickets:create/tickets:import/reports:read
- "全部工单"仅 permissions.includes('*') 可见,"系统设置"区由 hasAnyAdminPerm() 控制
- 设置子项按各自 perm 过滤(users:read/roles:read/api-keys:read)
- /tickets/all 页面权限检查同步改为 permissions.includes('*')
This commit is contained in:
gitadmin 2026-05-14 17:01:22 +08:00
parent 48f8084b9b
commit 152241e666
2 changed files with 27 additions and 16 deletions

View File

@ -12,7 +12,7 @@ export default function AllTicketsPage() {
fetch('/api/auth/me') fetch('/api/auth/me')
.then(r => r.json()) .then(r => r.json())
.then(u => { .then(u => {
if (u.user?.role !== 'admin') { if (!u.user?.permissions?.includes('*')) {
router.replace('/tickets/pending') router.replace('/tickets/pending')
} else { } else {
setReady(true) setReady(true)

View File

@ -5,42 +5,53 @@ import { usePathname } from 'next/navigation'
import { LayoutDashboard, FileText, Settings, Users, Shield, Key, Clock, CheckCircle, PlusSquare, Upload, List } from 'lucide-react' import { LayoutDashboard, FileText, Settings, Users, Shield, Key, Clock, CheckCircle, PlusSquare, Upload, List } from 'lucide-react'
const navItems = [ const navItems = [
{ href: '/dashboard', label: '仪表盘', icon: LayoutDashboard }, { href: '/dashboard', label: '仪表盘', icon: LayoutDashboard, perm: null },
{ href: '/tickets/pending', label: '待办工单', icon: Clock }, { href: '/tickets/pending', label: '待办工单', icon: Clock, perm: 'tickets:read' },
{ href: '/tickets/completed', label: '已办工单', icon: CheckCircle }, { href: '/tickets/completed', label: '已办工单', icon: CheckCircle, perm: 'tickets:read' },
{ href: '/tickets/create', label: '手动建单', icon: PlusSquare }, { href: '/tickets/create', label: '手动建单', icon: PlusSquare, perm: 'tickets:create' },
{ href: '/tickets/import', label: '导入工单', icon: Upload }, { href: '/tickets/import', label: '导入工单', icon: Upload, perm: 'tickets:import' },
{ href: '/reports', label: '报告管理', icon: FileText }, { href: '/reports', label: '报告管理', icon: FileText, perm: 'reports:read' },
] ]
const settingsItems = [ const settingsItems = [
{ href: '/settings/users', label: '用户管理', icon: Users }, { href: '/settings/users', label: '用户管理', icon: Users, perm: 'users:read' },
{ href: '/settings/roles', label: '角色权限', icon: Shield }, { href: '/settings/roles', label: '角色权限', icon: Shield, perm: 'roles:read' },
{ href: '/settings/api-keys', label: 'API Key', icon: Key }, { href: '/settings/api-keys', label: 'API Key', icon: Key, perm: 'api-keys:read' },
] ]
function hasAnyAdminPerm(permissions: string[]): boolean {
return permissions.includes('*') || permissions.some(p => p.startsWith('users:') || p.startsWith('roles:') || p.startsWith('api-keys:'))
}
export default function Sidebar() { export default function Sidebar() {
const pathname = usePathname() const pathname = usePathname()
const [isAdmin, setIsAdmin] = useState(false) const [permissions, setPermissions] = useState<string[]>([])
useEffect(() => { useEffect(() => {
fetch('/api/auth/me') fetch('/api/auth/me')
.then(r => r.json()) .then(r => r.json())
.then(u => { if (u.user?.role === 'admin') setIsAdmin(true) }) .then(u => { if (u.user?.permissions) setPermissions(u.user.permissions) })
.catch(() => {}) .catch(() => {})
}, []) }, [])
const canSee = (perm: string | null) => {
if (perm === null) return true
if (permissions.includes('*')) return true
return permissions.includes(perm)
}
return ( return (
<aside className="fixed left-0 top-0 bottom-0 w-60 bg-white dark:bg-slate-900 border-r border-slate-200 dark:border-slate-800 flex flex-col z-40"> <aside className="fixed left-0 top-0 bottom-0 w-60 bg-white dark:bg-slate-900 border-r border-slate-200 dark:border-slate-800 flex flex-col z-40">
<div className="h-14 flex items-center px-5 border-b border-slate-200 dark:border-slate-800"> <div className="h-14 flex items-center px-5 border-b border-slate-200 dark:border-slate-800">
<span className="text-lg font-semibold text-blue-600 dark:text-blue-400">IT工单跟踪系统</span> <span className="text-lg font-semibold text-blue-600 dark:text-blue-400">IT工单跟踪系统</span>
</div> </div>
<nav className="flex-1 py-3 px-3 space-y-1 overflow-y-auto"> <nav className="flex-1 py-3 px-3 space-y-1 overflow-y-auto">
{navItems.map((item) => { {navItems.filter(item => canSee(item.perm)).map((item) => {
const isActive = pathname === item.href || pathname.startsWith(item.href + '/') const isActive = pathname === item.href || pathname.startsWith(item.href + '/')
const Icon = item.icon const Icon = item.icon
return (<Link key={item.href} href={item.href} className={`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 ${isActive ? 'bg-blue-50 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400' : 'text-slate-600 hover:bg-slate-50 dark:text-slate-400 dark:hover:bg-slate-800'}`}><Icon size={18} />{item.label}</Link>) return (<Link key={item.href} href={item.href} className={`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 ${isActive ? 'bg-blue-50 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400' : 'text-slate-600 hover:bg-slate-50 dark:text-slate-400 dark:hover:bg-slate-800'}`}><Icon size={18} />{item.label}</Link>)
})} })}
{isAdmin && ( {permissions.includes('*') && (
<Link <Link
href="/tickets/all" href="/tickets/all"
className={`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 ${pathname === '/tickets/all' ? 'bg-blue-50 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400' : 'text-slate-600 hover:bg-slate-50 dark:text-slate-400 dark:hover:bg-slate-800'}`} className={`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 ${pathname === '/tickets/all' ? 'bg-blue-50 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400' : 'text-slate-600 hover:bg-slate-50 dark:text-slate-400 dark:hover:bg-slate-800'}`}
@ -48,12 +59,12 @@ export default function Sidebar() {
<List size={18} /> <List size={18} />
</Link> </Link>
)} )}
{isAdmin && ( {hasAnyAdminPerm(permissions) && (
<div className="pt-3 border-t border-slate-200 dark:border-slate-800 mt-3"> <div className="pt-3 border-t border-slate-200 dark:border-slate-800 mt-3">
<div className="flex items-center gap-3 px-3 py-2 text-xs font-semibold text-slate-400 dark:text-slate-500 uppercase tracking-wider"> <div className="flex items-center gap-3 px-3 py-2 text-xs font-semibold text-slate-400 dark:text-slate-500 uppercase tracking-wider">
<Settings size={14} /> <Settings size={14} />
</div> </div>
{settingsItems.map((item) => { {settingsItems.filter(item => canSee(item.perm)).map((item) => {
const isActive = pathname === item.href const isActive = pathname === item.href
const Icon = item.icon const Icon = item.icon
return (<Link key={item.href} href={item.href} className={`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 ${isActive ? 'bg-blue-50 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400' : 'text-slate-600 hover:bg-slate-50 dark:text-slate-400 dark:hover:bg-slate-800'}`}><Icon size={18} />{item.label}</Link>) return (<Link key={item.href} href={item.href} className={`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 ${isActive ? 'bg-blue-50 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400' : 'text-slate-600 hover:bg-slate-50 dark:text-slate-400 dark:hover:bg-slate-800'}`}><Icon size={18} />{item.label}</Link>)