FastAPI backend with auth, user, instance, ticket, query, dashboard APIs. React + TypeScript frontend with mock data UI (10 pages). SQLite + async SQLAlchemy, JWT auth, role-based access control. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
160 lines
6.5 KiB
TypeScript
160 lines
6.5 KiB
TypeScript
import { useState } from 'react';
|
|
import { Crown, Database, Code, Eye, Shield, ChevronRight, Check, Save } from 'lucide-react';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { roles, permissionTreeData } from '@/lib/mock';
|
|
import type { Role } from '@/types';
|
|
|
|
const iconMap: Record<string, React.ElementType> = { Crown, Database, Code, Eye };
|
|
|
|
export default function RolesPage() {
|
|
const [selectedRole, setSelectedRole] = useState<Role | null>(roles[0]);
|
|
const [checkedKeys, setCheckedKeys] = useState<Set<string>>(new Set(roles[0]?.permissions || []));
|
|
|
|
const handleRoleSelect = (role: Role) => {
|
|
setSelectedRole(role);
|
|
setCheckedKeys(new Set(role.permissions));
|
|
};
|
|
|
|
const togglePermission = (id: string) => {
|
|
const next = new Set(checkedKeys);
|
|
if (next.has(id)) next.delete(id);
|
|
else next.add(id);
|
|
setCheckedKeys(next);
|
|
};
|
|
|
|
const toggleChildren = (node: typeof permissionTreeData[0], checked: boolean) => {
|
|
const next = new Set(checkedKeys);
|
|
const toggleNode = (n: typeof node) => {
|
|
if (checked) next.add(n.id);
|
|
else next.delete(n.id);
|
|
n.children?.forEach(toggleNode);
|
|
};
|
|
toggleNode(node);
|
|
setCheckedKeys(next);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-5">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-semibold">角色权限管理</h1>
|
|
<p className="text-sm text-[var(--dg-text-secondary)] mt-1">配置 RBAC 角色权限,控制用户对系统功能和数据的访问</p>
|
|
</div>
|
|
<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 className="grid grid-cols-4 gap-4">
|
|
{roles.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)]"><span className="text-[var(--dg-text-primary)] font-semibold">{role.userCount}</span> 用户</div>
|
|
<div className="text-xs text-[var(--dg-text-secondary)]"><span className="text-[var(--dg-text-primary)] font-semibold">{role.permissionCount}</span> 权限</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* 权限配置 */}
|
|
{selectedRole && (
|
|
<Card className="dg-card">
|
|
<CardHeader className="border-b border-[var(--dg-border)] flex flex-row items-center justify-between">
|
|
<CardTitle className="text-[15px] font-semibold">{selectedRole.name} - 权限配置</CardTitle>
|
|
<button 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>
|
|
</CardHeader>
|
|
<CardContent className="p-5">
|
|
<div className="space-y-2">
|
|
{permissionTreeData.map(node => (
|
|
<PermissionNodeComponent
|
|
key={node.id}
|
|
node={node}
|
|
checkedKeys={checkedKeys}
|
|
onToggle={togglePermission}
|
|
onToggleChildren={toggleChildren}
|
|
depth={0}
|
|
/>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function PermissionNodeComponent({
|
|
node, checkedKeys, onToggle, onToggleChildren, depth
|
|
}: {
|
|
node: typeof permissionTreeData[0];
|
|
checkedKeys: Set<string>;
|
|
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 (
|
|
<div>
|
|
<div
|
|
className="flex items-center gap-2 py-2 px-3 rounded-lg hover:bg-[var(--dg-bg-hover)] transition-colors cursor-pointer"
|
|
style={{ paddingLeft: `${depth * 20 + 12}px` }}
|
|
onClick={() => { if (hasChildren) setExpanded(!expanded); }}
|
|
>
|
|
{hasChildren && (
|
|
<ChevronRight className={`w-4 h-4 text-[var(--dg-text-muted)] transition-transform ${expanded ? 'rotate-90' : ''}`} />
|
|
)}
|
|
{!hasChildren && <div className="w-4" />}
|
|
<div
|
|
className={`w-4 h-4 rounded border flex items-center justify-center transition-all ${isChecked || allChildrenChecked ? 'bg-blue-500 border-blue-500' : someChildrenChecked ? 'bg-blue-500/50 border-blue-500' : 'border-[var(--dg-text-muted)]'}`}
|
|
onClick={e => {
|
|
e.stopPropagation();
|
|
if (hasChildren) onToggleChildren(node, !allChildrenChecked);
|
|
else onToggle(node.id);
|
|
}}
|
|
>
|
|
{(isChecked || allChildrenChecked || someChildrenChecked) && <Check className="w-3 h-3 text-white" />}
|
|
</div>
|
|
<span className="text-sm">{node.label}</span>
|
|
</div>
|
|
{hasChildren && expanded && (
|
|
<div>
|
|
{node.children!.map(child => (
|
|
<PermissionNodeComponent
|
|
key={child.id}
|
|
node={child}
|
|
checkedKeys={checkedKeys}
|
|
onToggle={onToggle}
|
|
onToggleChildren={onToggleChildren}
|
|
depth={depth + 1}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|