refactor: mock清理+安全配置+通用组件+菜单优化
P0安全: SECRET_KEY/ADMIN_PASSWORD/CORS环境变量化, seed仅admin P1Mock: mock.ts 301->88行, 删8数据mock+5generateMock, net-1300行 P2组件: useToast/useFetchList+ConfirmDialog/FormDialog P3菜单: menuSections/menuItems->constants/menu.ts P4API: NotificationsPage/WorkflowPage重写+RolesPage演示 测试: 1050passed, tsc clean
This commit is contained in:
1 parent
ec3a03cf38
commit
8b921c6c83
33 files changed
+1734
-2006
No files matched your search
+175
-342
@@ -1,15 +1,25 @@
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import {
|
||||
Crown, Database, Code, Eye, Shield, ChevronRight, Check, Save,
|
||||
Search, X, Copy, GitCompare, Users, Lock, BarChart3, XCircle,
|
||||
Crown, Database, Code, Eye, ChevronRight, Check, Save,
|
||||
Search, X, Copy, 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';
|
||||
import { permissionTreeData } from '@/lib/mock';
|
||||
|
||||
/* ───────── Types ───────── */
|
||||
|
||||
interface Role {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
iconClass: string;
|
||||
permissions: string[];
|
||||
userCount: number;
|
||||
permissionCount: number;
|
||||
}
|
||||
|
||||
interface PermissionChange {
|
||||
id: string;
|
||||
label: string;
|
||||
@@ -86,38 +96,14 @@ function searchTree(nodes: typeof permissionTreeData, query: string): typeof per
|
||||
}, []);
|
||||
}
|
||||
|
||||
function computeChanges(original: Set<string>, current: Set<string>): PermissionChange[] {
|
||||
const allIds = collectAllIds(permissionTreeData);
|
||||
const idToLabel: Record<string, string> = {};
|
||||
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<Role | null>(mockRoles[0]);
|
||||
const [checkedKeys, setCheckedKeys] = useState<Set<string>>(new Set(mockRoles[0]?.permissions || []));
|
||||
const [originalKeys, setOriginalKeys] = useState<Set<string>>(new Set(mockRoles[0]?.permissions || []));
|
||||
const [checkedKeys, setCheckedKeys] = useState<Set<string>>(new Set());
|
||||
const [originalKeys, setOriginalKeys] = useState<Set<string>>(new Set());
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [showDiff, setShowDiff] = useState(false);
|
||||
const [showPresets, setShowPresets] = useState(false);
|
||||
const [showCloneModal, setShowCloneModal] = useState<Role | null>(null);
|
||||
const [showStats, setShowStats] = useState(false);
|
||||
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null);
|
||||
|
||||
const hasChanges = useMemo(() => {
|
||||
@@ -125,18 +111,29 @@ export default function RolesPage() {
|
||||
return allIds.some(id => checkedKeys.has(id) !== originalKeys.has(id));
|
||||
}, [checkedKeys, originalKeys]);
|
||||
|
||||
const changes = useMemo(() => computeChanges(originalKeys, checkedKeys), [originalKeys, checkedKeys]);
|
||||
const changes = useMemo(() => {
|
||||
const allIds = collectAllIds(permissionTreeData);
|
||||
const idToLabel: Record<string, string> = {};
|
||||
const buildLabelMap = (nodes: typeof permissionTreeData) => {
|
||||
nodes.forEach(n => {
|
||||
idToLabel[n.id] = n.label;
|
||||
if (n.children) buildLabelMap(n.children);
|
||||
});
|
||||
};
|
||||
buildLabelMap(permissionTreeData);
|
||||
const result: PermissionChange[] = [];
|
||||
allIds.forEach(id => {
|
||||
if (checkedKeys.has(id) && !originalKeys.has(id)) {
|
||||
result.push({ id, label: idToLabel[id] || id, action: 'added' });
|
||||
} else if (!checkedKeys.has(id) && originalKeys.has(id)) {
|
||||
result.push({ id, label: idToLabel[id] || id, action: 'removed' });
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}, [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);
|
||||
@@ -161,15 +158,15 @@ export default function RolesPage() {
|
||||
|
||||
const applyPreset = (preset: PermissionPreset) => {
|
||||
setCheckedKeys(new Set(preset.permissions));
|
||||
setOriginalKeys(new Set(preset.permissions));
|
||||
setShowPresets(false);
|
||||
setToast({ message: `已应用「${preset.name}」`, type: 'success' });
|
||||
setToast({ message: `已应用「${preset.name}」预设`, type: 'success' });
|
||||
setTimeout(() => setToast(null), 2000);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!selectedRole) return;
|
||||
setOriginalKeys(new Set(checkedKeys));
|
||||
setToast({ message: '权限已保存', type: 'success' });
|
||||
setToast({ message: '权限配置已保存(演示模式)', type: 'success' });
|
||||
setTimeout(() => setToast(null), 2000);
|
||||
};
|
||||
|
||||
@@ -180,19 +177,6 @@ export default function RolesPage() {
|
||||
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 (
|
||||
<div className="space-y-5">
|
||||
{/* Toast */}
|
||||
@@ -212,258 +196,158 @@ export default function RolesPage() {
|
||||
<h1 className="text-2xl font-semibold">角色权限管理</h1>
|
||||
<p className="text-sm text-[var(--dg-text-secondary)] mt-1">配置 RBAC 角色权限,控制用户对系统功能和数据的访问</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setShowStats(!showStats)}
|
||||
className="flex items-center gap-2 px-4 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm font-medium text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)] transition-all"
|
||||
>
|
||||
<BarChart3 className="w-4 h-4" /> 统计
|
||||
</button>
|
||||
<button className="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg text-sm font-medium text-white hover:opacity-90 transition-opacity">
|
||||
<Shield className="w-4 h-4" /> 新建角色
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Panel */}
|
||||
{showStats && (
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{roleStats.map(({ role, userCount, permissionCount, onlineUsers }) => {
|
||||
const Icon = iconMap[role.icon] || Eye;
|
||||
return (
|
||||
<Card key={role.id} className="dg-card">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className={`w-9 h-9 rounded-lg flex items-center justify-center ${role.iconClass}`}>
|
||||
<Icon className="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-semibold">{role.name}</div>
|
||||
<div className="text-xs text-[var(--dg-text-muted)]">{role.userCount} 用户</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2 text-center">
|
||||
<div className="p-2 bg-[var(--dg-bg-tertiary)] rounded-lg">
|
||||
<div className="text-lg font-bold text-blue-400">{userCount}</div>
|
||||
<div className="text-[10px] text-[var(--dg-text-muted)]">用户</div>
|
||||
</div>
|
||||
<div className="p-2 bg-[var(--dg-bg-tertiary)] rounded-lg">
|
||||
<div className="text-lg font-bold text-purple-400">{permissionCount}</div>
|
||||
<div className="text-[10px] text-[var(--dg-text-muted)]">权限</div>
|
||||
</div>
|
||||
<div className="p-2 bg-[var(--dg-bg-tertiary)] rounded-lg">
|
||||
<div className="text-lg font-bold text-green-400">{onlineUsers}</div>
|
||||
<div className="text-[10px] text-[var(--dg-text-muted)]">在线</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
{/* Empty state info */}
|
||||
<div className="dg-card p-5 flex items-start gap-3 bg-blue-500/5 border-blue-500/20">
|
||||
<Users className="w-5 h-5 text-blue-400 mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm text-[var(--dg-text-secondary)]">
|
||||
角色数据暂未接入后端 API,当前为权限配置演示模式。您可以使用下方的权限树和预设模板来规划角色权限方案。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Role Cards */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{mockRoles.map(role => {
|
||||
const Icon = iconMap[role.icon] || Eye;
|
||||
return (
|
||||
<div
|
||||
key={role.id}
|
||||
onClick={() => 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'}`}
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className={`w-10 h-10 rounded-xl flex items-center justify-center ${role.iconClass}`}>
|
||||
<Icon className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-base font-semibold truncate">{role.name}</div>
|
||||
<div className="text-xs text-[var(--dg-text-muted)] truncate">{role.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4 pt-3 border-t border-[var(--dg-border)]">
|
||||
<div className="text-xs text-[var(--dg-text-secondary)]">
|
||||
<Users className="w-3 h-3 inline mr-1" />
|
||||
<span className="text-[var(--dg-text-primary)] font-semibold">{role.userCount}</span> 用户
|
||||
</div>
|
||||
<div className="text-xs text-[var(--dg-text-secondary)]">
|
||||
<Lock className="w-3 h-3 inline mr-1" />
|
||||
<span className="text-[var(--dg-text-primary)] font-semibold">{role.permissionCount}</span> 权限
|
||||
</div>
|
||||
</div>
|
||||
{/* Clone button */}
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setShowCloneModal(role); }}
|
||||
className="mt-3 flex items-center gap-1 text-xs text-[var(--dg-text-muted)] hover:text-blue-400 transition-colors"
|
||||
>
|
||||
<Copy className="w-3 h-3" /> 克隆角色
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Permission Config */}
|
||||
{selectedRole && (
|
||||
<Card className="dg-card">
|
||||
<CardHeader className="border-b border-[var(--dg-border)] flex flex-row items-center justify-between flex-wrap gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<CardTitle className="text-[15px] font-semibold">{selectedRole.name} - 权限配置</CardTitle>
|
||||
{hasChanges && (
|
||||
<span className="px-2 py-0.5 rounded-full bg-yellow-500/15 text-yellow-400 text-xs font-medium">
|
||||
有未保存的更改 ({changes.length})
|
||||
</span>
|
||||
<Card className="dg-card">
|
||||
<CardHeader className="border-b border-[var(--dg-border)] flex flex-row items-center justify-between flex-wrap gap-3">
|
||||
<CardTitle className="text-[15px] font-semibold">权限配置</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Presets */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowPresets(!showPresets)}
|
||||
className="flex items-center gap-2 px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)] transition-colors"
|
||||
>
|
||||
<Sparkles className="w-4 h-4" /> 预设模板 <ChevronDown className="w-3 h-3" />
|
||||
</button>
|
||||
{showPresets && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-40" onClick={() => setShowPresets(false)} />
|
||||
<div className="absolute right-0 top-full mt-1 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded-lg shadow-xl z-50 py-1 min-w-[260px]">
|
||||
{PERMISSION_PRESETS.map(preset => {
|
||||
const Icon = preset.icon;
|
||||
return (
|
||||
<button
|
||||
key={preset.key}
|
||||
onClick={() => applyPreset(preset)}
|
||||
className="w-full flex items-start gap-3 px-4 py-3 text-left hover:bg-[var(--dg-bg-hover)] transition-colors border-b border-[var(--dg-border-light)] last:border-0"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-lg bg-blue-500/10 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<Icon className="w-4 h-4 text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium">{preset.name}</div>
|
||||
<div className="text-xs text-[var(--dg-text-muted)] mt-0.5">{preset.description}</div>
|
||||
<div className="text-xs text-blue-400 mt-1">{preset.permissions.length} 个权限</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Presets */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowPresets(!showPresets)}
|
||||
className="flex items-center gap-2 px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)] transition-colors"
|
||||
>
|
||||
<Sparkles className="w-4 h-4" /> 预设模板 <ChevronDown className="w-3 h-3" />
|
||||
</button>
|
||||
{showPresets && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-40" onClick={() => setShowPresets(false)} />
|
||||
<div className="absolute right-0 top-full mt-1 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded-lg shadow-xl z-50 py-1 min-w-[260px]">
|
||||
{PERMISSION_PRESETS.map(preset => {
|
||||
const Icon = preset.icon;
|
||||
return (
|
||||
<button
|
||||
key={preset.key}
|
||||
onClick={() => applyPreset(preset)}
|
||||
className="w-full flex items-start gap-3 px-4 py-3 text-left hover:bg-[var(--dg-bg-hover)] transition-colors border-b border-[var(--dg-border-light)] last:border-0"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-lg bg-blue-500/10 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<Icon className="w-4 h-4 text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium">{preset.name}</div>
|
||||
<div className="text-xs text-[var(--dg-text-muted)] mt-0.5">{preset.description}</div>
|
||||
<div className="text-xs text-blue-400 mt-1">{preset.permissions.length} 个权限</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
|
||||
{/* Diff Toggle */}
|
||||
{hasChanges && (
|
||||
<button
|
||||
onClick={() => setShowDiff(!showDiff)}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors ${
|
||||
showDiff ? 'bg-blue-500/15 text-blue-400 border border-blue-500/30' : 'bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]'
|
||||
}`}
|
||||
>
|
||||
变更对比
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Discard */}
|
||||
{hasChanges && (
|
||||
<button
|
||||
onClick={handleDiscard}
|
||||
className="flex items-center gap-2 px-3 py-2 bg-red-500/10 border border-red-500/20 rounded-lg text-sm text-red-400 hover:bg-red-500/20 transition-colors"
|
||||
>
|
||||
<X className="w-4 h-4" /> 撤销
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Save */}
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-blue-500/15 border border-blue-500/30 rounded-lg text-sm font-medium text-blue-400 hover:bg-blue-500/25 transition-all"
|
||||
>
|
||||
<Save className="w-4 h-4" /> 保存权限
|
||||
</button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="p-5">
|
||||
<div className="flex gap-5">
|
||||
{/* Permission Tree */}
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* Search */}
|
||||
<div className="relative mb-4">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--dg-text-muted)]" />
|
||||
<input
|
||||
value={searchQuery}
|
||||
onChange={e => 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 && (
|
||||
<button onClick={() => setSearchQuery('')} className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]">
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Diff Toggle */}
|
||||
{hasChanges && (
|
||||
<button
|
||||
onClick={() => setShowDiff(!showDiff)}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors ${
|
||||
showDiff ? 'bg-blue-500/15 text-blue-400 border border-blue-500/30' : 'bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]'
|
||||
}`}
|
||||
>
|
||||
<GitCompare className="w-4 h-4" /> 变更对比
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Discard */}
|
||||
{hasChanges && (
|
||||
<button
|
||||
onClick={handleDiscard}
|
||||
className="flex items-center gap-2 px-3 py-2 bg-red-500/10 border border-red-500/20 rounded-lg text-sm text-red-400 hover:bg-red-500/20 transition-colors"
|
||||
>
|
||||
<X className="w-4 h-4" /> 撤销
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Save */}
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-blue-500/15 border border-blue-500/30 rounded-lg text-sm font-medium text-blue-400 hover:bg-blue-500/25 transition-all"
|
||||
>
|
||||
<Save className="w-4 h-4" /> 保存权限
|
||||
</button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="p-5">
|
||||
<div className="flex gap-5">
|
||||
{/* Permission Tree */}
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* Search */}
|
||||
<div className="relative mb-4">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--dg-text-muted)]" />
|
||||
<input
|
||||
value={searchQuery}
|
||||
onChange={e => 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"
|
||||
{/* Tree */}
|
||||
<div className="space-y-1">
|
||||
{filteredTree.map(node => (
|
||||
<PermissionNodeComponent
|
||||
key={node.id}
|
||||
node={node}
|
||||
checkedKeys={checkedKeys}
|
||||
onToggle={togglePermission}
|
||||
onToggleChildren={toggleChildren}
|
||||
depth={0}
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button onClick={() => setSearchQuery('')} className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]">
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{filteredTree.length === 0 && (
|
||||
<div className="text-center py-8 text-[var(--dg-text-muted)] text-sm">
|
||||
<Search className="w-8 h-8 mx-auto mb-2 opacity-30" />
|
||||
未找到匹配的权限
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tree */}
|
||||
<div className="space-y-1">
|
||||
{filteredTree.map(node => (
|
||||
<PermissionNodeComponent
|
||||
key={node.id}
|
||||
node={node}
|
||||
checkedKeys={checkedKeys}
|
||||
onToggle={togglePermission}
|
||||
onToggleChildren={toggleChildren}
|
||||
depth={0}
|
||||
/>
|
||||
))}
|
||||
{filteredTree.length === 0 && (
|
||||
<div className="text-center py-8 text-[var(--dg-text-muted)] text-sm">
|
||||
<Search className="w-8 h-8 mx-auto mb-2 opacity-30" />
|
||||
未找到匹配的权限
|
||||
{/* Diff Panel */}
|
||||
{showDiff && hasChanges && (
|
||||
<div className="w-72 border-l border-[var(--dg-border)] pl-5 flex-shrink-0">
|
||||
<h3 className="text-sm font-semibold mb-3">变更详情</h3>
|
||||
<div className="space-y-2 max-h-[500px] overflow-y-auto">
|
||||
{changes.map(change => (
|
||||
<div
|
||||
key={change.id}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-xs ${
|
||||
change.action === 'added' ? 'bg-green-500/10 text-green-400' : 'bg-red-500/10 text-red-400'
|
||||
}`}
|
||||
>
|
||||
{change.action === 'added' ? <CheckCircle2 className="w-3.5 h-3.5 flex-shrink-0" /> : <MinusCircle className="w-3.5 h-3.5 flex-shrink-0" />}
|
||||
<span className="flex-1 min-w-0 truncate">{change.label}</span>
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-3 pt-3 border-t border-[var(--dg-border)] flex items-center gap-3 text-xs text-[var(--dg-text-muted)]">
|
||||
<span className="flex items-center gap-1"><CheckCircle2 className="w-3 h-3 text-green-400" /> 新增 {changes.filter(c => c.action === 'added').length}</span>
|
||||
<span className="flex items-center gap-1"><MinusCircle className="w-3 h-3 text-red-400" /> 移除 {changes.filter(c => c.action === 'removed').length}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Diff Panel */}
|
||||
{showDiff && hasChanges && (
|
||||
<div className="w-72 border-l border-[var(--dg-border)] pl-5 flex-shrink-0">
|
||||
<h3 className="text-sm font-semibold mb-3 flex items-center gap-2">
|
||||
<GitCompare className="w-4 h-4 text-[var(--dg-text-muted)]" />
|
||||
变更详情
|
||||
</h3>
|
||||
<div className="space-y-2 max-h-[500px] overflow-y-auto">
|
||||
{changes.map(change => (
|
||||
<div
|
||||
key={change.id}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-xs ${
|
||||
change.action === 'added' ? 'bg-green-500/10 text-green-400' : 'bg-red-500/10 text-red-400'
|
||||
}`}
|
||||
>
|
||||
{change.action === 'added' ? <CheckCircle2 className="w-3.5 h-3.5 flex-shrink-0" /> : <MinusCircle className="w-3.5 h-3.5 flex-shrink-0" />}
|
||||
<span className="flex-1 min-w-0 truncate">{change.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-3 pt-3 border-t border-[var(--dg-border)] flex items-center gap-3 text-xs text-[var(--dg-text-muted)]">
|
||||
<span className="flex items-center gap-1"><CheckCircle2 className="w-3 h-3 text-green-400" /> 新增 {changes.filter(c => c.action === 'added').length}</span>
|
||||
<span className="flex items-center gap-1"><MinusCircle className="w-3 h-3 text-red-400" /> 移除 {changes.filter(c => c.action === 'removed').length}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Clone Modal */}
|
||||
{showCloneModal && (
|
||||
<CloneRoleModal
|
||||
sourceRole={showCloneModal}
|
||||
onClose={() => setShowCloneModal(null)}
|
||||
/>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -531,54 +415,3 @@ function PermissionNodeComponent({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ───────── 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 (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div className="w-full max-w-md bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded-xl p-6 shadow-xl" onClick={e => e.stopPropagation()}>
|
||||
<div className="flex items-center justify-between mb-5">
|
||||
<h2 className="text-lg font-semibold flex items-center gap-2"><Copy className="w-5 h-5 text-blue-400" /> 克隆角色</h2>
|
||||
<button onClick={onClose} className="text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]"><XCircle className="w-5 h-5" /></button>
|
||||
</div>
|
||||
<div className="p-3 bg-blue-500/5 border border-blue-500/15 rounded-lg mb-4">
|
||||
<p className="text-sm text-[var(--dg-text-secondary)]">基于 <strong className="text-[var(--dg-text-primary)]">{sourceRole.name}</strong> 创建副本,包含 {sourceRole.permissions.length} 个权限</p>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">角色名称 *</label>
|
||||
<input value={name} onChange={e => 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" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">描述</label>
|
||||
<textarea value={description} onChange={e => setDescription(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 resize-none" rows={2} />
|
||||
</div>
|
||||
<div className="flex justify-end gap-3 pt-2">
|
||||
<button onClick={onClose} className="px-4 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm hover:bg-[var(--dg-bg-hover)] transition-colors">取消</button>
|
||||
<button onClick={handleClone} disabled={submitting || !name.trim()} className="px-4 py-2 bg-blue-500 text-white rounded-lg text-sm hover:opacity-90 disabled:opacity-50 transition-opacity">
|
||||
{submitting ? '克隆中...' : '确认克隆'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user