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>
This commit is contained in:
commit
439155c92e
290 files changed
+61950
No files matched your search
@@ -0,0 +1,159 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user