Files
opsxsql/frontend/src/pages/RolesPage.tsx
T
loganandClaude Opus 4.7 439155c92e Initial commit: SQL audit platform v0.1.0
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>
2026-05-22 11:11:57 +08:00

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>
);
}