import { useState, useMemo, useCallback } from 'react'; import { Crown, Database, Code, Eye, Shield, ChevronRight, Check, Save, Search, X, Copy, GitCompare, Users, Lock, BarChart3, XCircle, CheckCircle2, MinusCircle, Sparkles, ChevronDown, } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { roles as mockRoles, permissionTreeData, users as mockUsers } from '@/lib/mock'; import type { Role } from '@/types'; /* ───────── Types ───────── */ interface PermissionChange { id: string; label: string; action: 'added' | 'removed'; } interface PermissionPreset { key: string; name: string; description: string; icon: React.ElementType; permissions: string[]; } /* ───────── Constants ───────── */ const iconMap: Record = { Crown, Database, Code, Eye }; const PERMISSION_PRESETS: PermissionPreset[] = [ { key: 'dba', name: 'DBA 模板', description: '数据库管理员:完整的数据源管理、工单审批、查询审计权限', icon: Database, permissions: ['dashboard', 'mysql', 'postgres', 'redis', 'mongo', 'tickets', 'ticket_create', 'ticket_approve', 'ticket_execute', 'ticket_rollback', 'queries', 'query_create', 'query_export', 'query_view_all', 'audit-rules', 'audit-logs', 'workflow'], }, { key: 'developer', name: '开发模板', description: '开发人员:工单提交、查询执行、数据导出', icon: Code, permissions: ['dashboard', 'tickets', 'ticket_create', 'queries', 'query_create', 'query_export'], }, { key: 'auditor', name: '审计模板', description: '审计人员:查看查询日志、审计规则、审批流程', icon: Eye, permissions: ['dashboard', 'queries', 'query_view_all', 'audit-rules', 'audit-logs', 'workflow'], }, { key: 'admin', name: '管理员模板', description: '超级管理员:系统全部权限', icon: Crown, permissions: ['dashboard', 'mysql', 'postgres', 'redis', 'mongo', 'tickets', 'ticket_create', 'ticket_approve', 'ticket_execute', 'ticket_rollback', 'queries', 'query_create', 'query_export', 'query_view_all', 'roles', 'users', 'audit-rules', 'audit-logs', 'workflow', 'auto-task', 'announcement', 'config-platform', 'notifications'], }, ]; /* ───────── Helpers ───────── */ function collectAllIds(nodes: typeof permissionTreeData): string[] { const ids: string[] = []; const walk = (list: typeof permissionTreeData) => { list.forEach(n => { ids.push(n.id); if (n.children) walk(n.children); }); }; walk(nodes); return ids; } function searchTree(nodes: typeof permissionTreeData, query: string): typeof permissionTreeData { if (!query.trim()) return nodes; const lower = query.toLowerCase(); return nodes.reduce((acc, node) => { const matchesSelf = node.label.toLowerCase().includes(lower); const filteredChildren = node.children ? searchTree(node.children, query) : []; if (matchesSelf || filteredChildren.length > 0) { acc.push({ ...node, children: filteredChildren.length > 0 ? filteredChildren : node.children }); } return acc; }, []); } function computeChanges(original: Set, current: Set): PermissionChange[] { const allIds = collectAllIds(permissionTreeData); const idToLabel: Record = {}; const buildLabelMap = (nodes: typeof permissionTreeData) => { nodes.forEach(n => { idToLabel[n.id] = n.label; if (n.children) buildLabelMap(n.children); }); }; buildLabelMap(permissionTreeData); const changes: PermissionChange[] = []; allIds.forEach(id => { if (current.has(id) && !original.has(id)) { changes.push({ id, label: idToLabel[id] || id, action: 'added' }); } else if (!current.has(id) && original.has(id)) { changes.push({ id, label: idToLabel[id] || id, action: 'removed' }); } }); return changes; } /* ───────── Main Component ───────── */ export default function RolesPage() { const [selectedRole, setSelectedRole] = useState(mockRoles[0]); const [checkedKeys, setCheckedKeys] = useState>(new Set(mockRoles[0]?.permissions || [])); const [originalKeys, setOriginalKeys] = useState>(new Set(mockRoles[0]?.permissions || [])); const [searchQuery, setSearchQuery] = useState(''); const [showDiff, setShowDiff] = useState(false); const [showPresets, setShowPresets] = useState(false); const [showCloneModal, setShowCloneModal] = useState(null); const [showStats, setShowStats] = useState(false); const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null); const hasChanges = useMemo(() => { const allIds = collectAllIds(permissionTreeData); return allIds.some(id => checkedKeys.has(id) !== originalKeys.has(id)); }, [checkedKeys, originalKeys]); const changes = useMemo(() => computeChanges(originalKeys, checkedKeys), [originalKeys, checkedKeys]); const filteredTree = useMemo(() => searchTree(permissionTreeData, searchQuery), [searchQuery]); const handleRoleSelect = useCallback((role: Role) => { setSelectedRole(role); const newSet = new Set(role.permissions); setCheckedKeys(newSet); setOriginalKeys(newSet); setShowDiff(false); }, []); const togglePermission = useCallback((id: string) => { setCheckedKeys(prev => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }, []); const toggleChildren = useCallback((node: typeof permissionTreeData[0], checked: boolean) => { setCheckedKeys(prev => { const next = new Set(prev); const toggleNode = (n: typeof node) => { if (checked) next.add(n.id); else next.delete(n.id); n.children?.forEach(toggleNode); }; toggleNode(node); return next; }); }, []); const applyPreset = (preset: PermissionPreset) => { setCheckedKeys(new Set(preset.permissions)); setShowPresets(false); setToast({ message: `已应用「${preset.name}」`, type: 'success' }); setTimeout(() => setToast(null), 2000); }; const handleSave = () => { if (!selectedRole) return; setOriginalKeys(new Set(checkedKeys)); setToast({ message: '权限已保存', type: 'success' }); setTimeout(() => setToast(null), 2000); }; const handleDiscard = () => { setCheckedKeys(new Set(originalKeys)); setShowDiff(false); setToast({ message: '已撤销未保存的更改', type: 'error' }); setTimeout(() => setToast(null), 2000); }; // Role statistics const roleStats = useMemo(() => { return mockRoles.map(role => { const roleUsers = mockUsers.filter(u => u.role === role.id); return { role, userCount: roleUsers.length, permissionCount: role.permissions.length, onlineUsers: roleUsers.filter(u => u.status === 'active').length, }; }); }, []); return (
{/* Toast */} {toast && (
{toast.type === 'success' ? : } {toast.message}
)} {/* Header */}

角色权限管理

配置 RBAC 角色权限,控制用户对系统功能和数据的访问

{/* Stats Panel */} {showStats && (
{roleStats.map(({ role, userCount, permissionCount, onlineUsers }) => { const Icon = iconMap[role.icon] || Eye; return (
{role.name}
{role.userCount} 用户
{userCount}
用户
{permissionCount}
权限
{onlineUsers}
在线
); })}
)} {/* Role Cards */}
{mockRoles.map(role => { const Icon = iconMap[role.icon] || Eye; return (
handleRoleSelect(role)} className={`dg-card p-5 cursor-pointer transition-all ${selectedRole?.id === role.id ? 'border-blue-500/50 bg-blue-500/5' : 'dg-card-hover'}`} >
{role.name}
{role.description}
{role.userCount} 用户
{role.permissionCount} 权限
{/* Clone button */}
); })}
{/* Permission Config */} {selectedRole && (
{selectedRole.name} - 权限配置 {hasChanges && ( 有未保存的更改 ({changes.length}) )}
{/* Presets */}
{showPresets && ( <>
setShowPresets(false)} />
{PERMISSION_PRESETS.map(preset => { const Icon = preset.icon; return ( ); })}
)}
{/* Diff Toggle */} {hasChanges && ( )} {/* Discard */} {hasChanges && ( )} {/* Save */}
{/* Permission Tree */}
{/* Search */}
setSearchQuery(e.target.value)} placeholder="搜索权限名称..." className="w-full pl-10 pr-4 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm placeholder:text-[var(--dg-text-muted)] focus:border-blue-400 focus:outline-none" /> {searchQuery && ( )}
{/* Tree */}
{filteredTree.map(node => ( ))} {filteredTree.length === 0 && (
未找到匹配的权限
)}
{/* Diff Panel */} {showDiff && hasChanges && (

变更详情

{changes.map(change => (
{change.action === 'added' ? : } {change.label}
))}
新增 {changes.filter(c => c.action === 'added').length} 移除 {changes.filter(c => c.action === 'removed').length}
)}
)} {/* Clone Modal */} {showCloneModal && ( setShowCloneModal(null)} /> )}
); } /* ───────── Permission Node Component ───────── */ function PermissionNodeComponent({ node, checkedKeys, onToggle, onToggleChildren, depth }: { node: typeof permissionTreeData[0]; checkedKeys: Set; onToggle: (id: string) => void; onToggleChildren: (node: typeof permissionTreeData[0], checked: boolean) => void; depth: number; }) { const [expanded, setExpanded] = useState(true); const isChecked = checkedKeys.has(node.id); const hasChildren = node.children && node.children.length > 0; const allChildrenChecked = hasChildren && node.children!.every(c => checkedKeys.has(c.id)); const someChildrenChecked = hasChildren && node.children!.some(c => checkedKeys.has(c.id)); return (
{ if (hasChildren) setExpanded(!expanded); }} > {hasChildren && ( )} {!hasChildren &&
}
{ e.stopPropagation(); if (hasChildren) onToggleChildren(node, !allChildrenChecked); else onToggle(node.id); }} > {(isChecked || allChildrenChecked || someChildrenChecked) && }
{node.label}
{hasChildren && expanded && (
{node.children!.map(child => ( ))}
)}
); } /* ───────── Clone Role Modal ───────── */ function CloneRoleModal({ sourceRole, onClose }: { sourceRole: Role; onClose: () => void }) { const [name, setName] = useState(`${sourceRole.name} (副本)`); const [description, setDescription] = useState(sourceRole.description); const [submitting, setSubmitting] = useState(false); const handleClone = async () => { setSubmitting(true); try { // In real implementation, call API await new Promise(r => setTimeout(r, 500)); onClose(); } catch { // handle error } finally { setSubmitting(false); } }; return (
e.stopPropagation()}>

克隆角色

基于 {sourceRole.name} 创建副本,包含 {sourceRole.permissions.length} 个权限

setName(e.target.value)} className="w-full px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none" />