后端: - 新增 GET /user/permissions 端点返回当前用户权限码 - Login 响应附带 permissions 数组 - collectUserPermissionCodes 用 raw SQL JOIN 避免 GORM many2many 表名问题 - admin 用户返回 ['*'] 通配符 前端: - usePermission hook (hasPermission/hasAnyPermission/hasAllPermissions) - <HasPermission> 组件 (permission/anyOf/allOf + fallback) - zustand store 新增 permissions 字段 - Sidebar 菜单递归过滤 (42 菜单项全部标注 permission) - ServerManager: 添加/导入/批量删除按钮按权限显隐 - ServerTableRow: 终端/编辑/文件/删除按权限显隐 - SystemUserTab: 新建/导入/编辑/删除按钮按权限显隐 - MainLayout: 页面刷新自动拉取权限 - Login: 登录时存储 permissions 验证: - admin: permissions=['*'] 通配符 - viewer: 7 个只读权限 (dashboard:view/server:view/...) - viewer 无 server:create/delete - 10/10 回归测试通过 - npm run build 零错误
205 lines
9.0 KiB
TypeScript
205 lines
9.0 KiB
TypeScript
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import {
|
|
Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import { Shield } from 'lucide-react';
|
|
|
|
export interface RoleFormData {
|
|
name: string;
|
|
code: string;
|
|
description: string;
|
|
permission_ids: number[];
|
|
}
|
|
|
|
export interface PermissionOption {
|
|
id: number;
|
|
name: string;
|
|
code: string;
|
|
type?: string;
|
|
}
|
|
|
|
interface RoleFormDialogProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
editingRole: any | null;
|
|
form: RoleFormData;
|
|
setForm: React.Dispatch<React.SetStateAction<RoleFormData>>;
|
|
saving: boolean;
|
|
onSubmit: () => void;
|
|
permissions: PermissionOption[];
|
|
}
|
|
|
|
// 权限按模块分组
|
|
const MODULE_GROUPS: { key: string; label: string; prefixes: string[] }[] = [
|
|
{ key: 'dashboard', label: '📊 仪表盘', prefixes: ['dashboard:'] },
|
|
{ key: 'server', label: '🖥️ 资产管理', prefixes: ['server:'] },
|
|
{ key: 'cloud', label: '☁️ 云资源', prefixes: ['cloud:'] },
|
|
{ key: 'k8s', label: '☸️ Kubernetes', prefixes: ['k8s:'] },
|
|
{ key: 'deploy', label: '🚀 应用发布', prefixes: ['deploy:'] },
|
|
{ key: 'monitor', label: '📡 监控告警', prefixes: ['monitor:', 'agent:'] },
|
|
{ key: 'auto', label: '⚙️ 自动化运维', prefixes: ['auto:'] },
|
|
{ key: 'aiops', label: '🤖 AIOps智能', prefixes: ['aiops:'] },
|
|
{ key: 'approval', label: '✅ 资源申请', prefixes: ['approval:'] },
|
|
{ key: 'audit', label: '🔍 安全审计', prefixes: ['audit:'] },
|
|
{ key: 'system', label: '🔧 系统管理', prefixes: ['system:'] },
|
|
];
|
|
|
|
function groupPermissions(perms: PermissionOption[]): { key: string; label: string; items: PermissionOption[] }[] {
|
|
const grouped = MODULE_GROUPS.map(g => ({
|
|
key: g.key,
|
|
label: g.label,
|
|
items: perms.filter(p => g.prefixes.some(pre => p.code.startsWith(pre))),
|
|
})).filter(g => g.items.length > 0);
|
|
|
|
// 未分类的权限
|
|
const classified = new Set(grouped.flatMap(g => g.items.map(p => p.id)));
|
|
const uncategorized = perms.filter(p => !classified.has(p.id));
|
|
if (uncategorized.length > 0) {
|
|
grouped.push({ key: 'other', label: '📌 其他', items: uncategorized });
|
|
}
|
|
|
|
return grouped;
|
|
}
|
|
|
|
export function RoleFormDialog({
|
|
open, onOpenChange, editingRole, form, setForm, saving, onSubmit, permissions,
|
|
}: RoleFormDialogProps) {
|
|
const togglePermission = (id: number, checked: boolean) => {
|
|
setForm((prev) => {
|
|
const set = new Set(prev.permission_ids);
|
|
checked ? set.add(id) : set.delete(id);
|
|
return { ...prev, permission_ids: Array.from(set) };
|
|
});
|
|
};
|
|
|
|
// 全选/取消某个分组
|
|
const toggleGroup = (items: PermissionOption[], checked: boolean) => {
|
|
setForm((prev) => {
|
|
const set = new Set(prev.permission_ids);
|
|
items.forEach(p => checked ? set.add(p.id) : set.delete(p.id));
|
|
return { ...prev, permission_ids: Array.from(set) };
|
|
});
|
|
};
|
|
|
|
const grouped = groupPermissions(permissions);
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] max-w-2xl max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
|
<Shield className="h-5 w-5 text-purple-400" />
|
|
{editingRole ? '编辑角色' : '新建角色'}
|
|
</DialogTitle>
|
|
<DialogDescription className="text-[var(--dg-text-secondary)]">
|
|
{editingRole ? '修改角色信息与权限' : '创建新的系统角色并分配权限'}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label className="text-xs text-[var(--dg-text-secondary)]">角色名称</Label>
|
|
<Input
|
|
value={form.name}
|
|
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
|
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
|
|
placeholder="如:运维工程师"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label className="text-xs text-[var(--dg-text-secondary)]">角色编码</Label>
|
|
<Input
|
|
value={form.code}
|
|
onChange={(e) => setForm({ ...form, code: e.target.value })}
|
|
disabled={!!editingRole}
|
|
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)] disabled:opacity-60"
|
|
placeholder="如:ops_engineer"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label className="text-xs text-[var(--dg-text-secondary)]">描述</Label>
|
|
<Textarea
|
|
value={form.description}
|
|
onChange={(e) => setForm({ ...form, description: e.target.value })}
|
|
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)] resize-none"
|
|
placeholder="角色职责描述(可选)"
|
|
rows={2}
|
|
/>
|
|
</div>
|
|
|
|
{/* 权限分配 — 按模块分组 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs text-[var(--dg-text-secondary)]">
|
|
权限分配
|
|
{form.permission_ids.length > 0 && (
|
|
<span className="ml-2 text-purple-400">已选 {form.permission_ids.length} 项</span>
|
|
)}
|
|
</Label>
|
|
<div className="max-h-[40vh] overflow-y-auto rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)] p-3 space-y-3">
|
|
{grouped.length === 0 ? (
|
|
<p className="text-xs text-[var(--dg-text-muted)] text-center py-4">暂无可分配权限</p>
|
|
) : (
|
|
grouped.map(group => {
|
|
const selectedInGroup = group.items.filter(p => form.permission_ids.includes(p.id)).length;
|
|
const allSelected = selectedInGroup === group.items.length && group.items.length > 0;
|
|
return (
|
|
<div key={group.key}>
|
|
<div className="flex items-center justify-between mb-1.5">
|
|
<button
|
|
onClick={() => toggleGroup(group.items, !allSelected)}
|
|
className="text-xs font-medium text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] transition-colors flex items-center gap-1"
|
|
>
|
|
{group.label}
|
|
<span className="text-[var(--dg-text-muted)] text-[10px]">
|
|
({selectedInGroup}/{group.items.length})
|
|
</span>
|
|
</button>
|
|
<button
|
|
onClick={() => toggleGroup(group.items, !allSelected)}
|
|
className="text-[10px] text-blue-400 hover:text-blue-300 transition-colors"
|
|
>
|
|
{allSelected ? '取消全选' : '全选'}
|
|
</button>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-x-3 gap-y-1 pl-1">
|
|
{group.items.map((perm) => (
|
|
<Label
|
|
key={perm.id}
|
|
className="flex items-center gap-2 text-xs text-[var(--dg-text-secondary)] font-normal cursor-pointer hover:text-[var(--dg-text-primary)] py-0.5"
|
|
>
|
|
<Checkbox
|
|
checked={form.permission_ids.includes(perm.id)}
|
|
onCheckedChange={(checked) => togglePermission(perm.id, checked === true)}
|
|
className="border-[var(--dg-border)] data-[state=checked]:bg-blue-600 data-[state=checked]:border-blue-600 h-3.5 w-3.5"
|
|
/>
|
|
<span className="truncate">{perm.name}</span>
|
|
</Label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={() => onOpenChange(false)}>
|
|
取消
|
|
</Button>
|
|
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={onSubmit} disabled={saving}>
|
|
{saving ? '保存中...' : '保存'}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|