feat(rbac): F3 前端 RBAC 按钮级控制
后端: - 新增 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 零错误
This commit is contained in:
1 parent
77c234b060
commit
be2a377f01
30 files changed
+2328
-462
No files matched your search
@@ -4,12 +4,7 @@ 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,
|
||||
Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Shield } from 'lucide-react';
|
||||
|
||||
@@ -38,37 +33,69 @@ interface RoleFormDialogProps {
|
||||
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,
|
||||
open, onOpenChange, editingRole, form, setForm, saving, onSubmit, permissions,
|
||||
}: RoleFormDialogProps) {
|
||||
const togglePermission = (id: number, checked: boolean) => {
|
||||
setForm((prev) => {
|
||||
const set = new Set(prev.permission_ids);
|
||||
if (checked) {
|
||||
set.add(id);
|
||||
} else {
|
||||
set.delete(id);
|
||||
}
|
||||
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-slate-900 border-slate-800">
|
||||
<DialogContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100 flex items-center gap-2">
|
||||
<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-slate-500">
|
||||
<DialogDescription className="text-[var(--dg-text-secondary)]">
|
||||
{editingRole ? '修改角色信息与权限' : '创建新的系统角色并分配权限'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
@@ -76,68 +103,95 @@ export function RoleFormDialog({
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-slate-500">角色名称</Label>
|
||||
<Label className="text-xs text-[var(--dg-text-secondary)]">角色名称</Label>
|
||||
<Input
|
||||
value={form.name}
|
||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500"
|
||||
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-slate-500">角色编码</Label>
|
||||
<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-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500 disabled:opacity-60"
|
||||
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-slate-500">描述</Label>
|
||||
<Label className="text-xs text-[var(--dg-text-secondary)]">描述</Label>
|
||||
<Textarea
|
||||
value={form.description}
|
||||
onChange={(e) => setForm({ ...form, description: e.target.value })}
|
||||
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500 resize-none"
|
||||
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-slate-500">
|
||||
<Label className="text-xs text-[var(--dg-text-secondary)]">
|
||||
权限分配
|
||||
{form.permission_ids.length > 0 && (
|
||||
<span className="ml-2 text-slate-400">已选 {form.permission_ids.length} 项</span>
|
||||
<span className="ml-2 text-purple-400">已选 {form.permission_ids.length} 项</span>
|
||||
)}
|
||||
</Label>
|
||||
<div className="max-h-48 overflow-y-auto rounded-lg border border-slate-800 bg-slate-800/30 p-3">
|
||||
{permissions.length === 0 ? (
|
||||
<p className="text-xs text-slate-500 text-center py-4">暂无可分配权限</p>
|
||||
<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>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{permissions.map((perm) => (
|
||||
<Label
|
||||
key={perm.id}
|
||||
className="flex items-center gap-2 text-xs text-slate-300 font-normal cursor-pointer hover:text-slate-100"
|
||||
>
|
||||
<Checkbox
|
||||
checked={form.permission_ids.includes(perm.id)}
|
||||
onCheckedChange={(checked) => togglePermission(perm.id, checked === true)}
|
||||
className="border-slate-600 data-[state=checked]:bg-blue-600 data-[state=checked]:border-blue-600"
|
||||
/>
|
||||
<span className="truncate">{perm.name}</span>
|
||||
</Label>
|
||||
))}
|
||||
</div>
|
||||
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-slate-700 text-slate-300" onClick={() => onOpenChange(false)}>
|
||||
<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}>
|
||||
|
||||
@@ -4,61 +4,81 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Plus, Shield, Edit, Eye, Trash2 } from 'lucide-react';
|
||||
import { Plus, Shield, ShieldCheck, Edit, Eye, Trash2, Lock, Star } from 'lucide-react';
|
||||
import { userApi } from '@/services/api';
|
||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||
import type { Role } from '@/types';
|
||||
import {
|
||||
RoleFormDialog,
|
||||
type RoleFormData,
|
||||
type PermissionOption,
|
||||
} from './RoleFormDialog';
|
||||
import { RoleFormDialog, type RoleFormData, type PermissionOption } from './RoleFormDialog';
|
||||
|
||||
interface SystemRolesTabProps {
|
||||
selectedRole: Role | null;
|
||||
setSelectedRole: (role: Role | null) => void;
|
||||
}
|
||||
|
||||
// 将后端角色数据规范化为前端 Role 类型 // Logan.Li
|
||||
function normalizeRole( raw: any): Role {
|
||||
// 角色图标映射
|
||||
const ROLE_ICONS: Record<string, string> = {
|
||||
super_admin: '👑',
|
||||
k8s_admin: '☸️',
|
||||
resource_admin: '🖥️',
|
||||
monitor_admin: '📡',
|
||||
security_auditor: '🔍',
|
||||
release_admin: '🚀',
|
||||
viewer: '👁️',
|
||||
};
|
||||
|
||||
// 权限按模块分组
|
||||
const PERM_MODULE_GROUPS: { label: string; prefixes: string[] }[] = [
|
||||
{ label: '仪表盘', prefixes: ['dashboard:'] },
|
||||
{ label: '资产管理', prefixes: ['server:'] },
|
||||
{ label: '云资源', prefixes: ['cloud:'] },
|
||||
{ label: 'K8s', prefixes: ['k8s:'] },
|
||||
{ label: '发布', prefixes: ['deploy:'] },
|
||||
{ label: '监控', prefixes: ['monitor:', 'agent:'] },
|
||||
{ label: '自动化', prefixes: ['auto:'] },
|
||||
{ label: 'AI', prefixes: ['aiops:'] },
|
||||
{ label: '审批', prefixes: ['approval:'] },
|
||||
{ label: '审计', prefixes: ['audit:'] },
|
||||
{ label: '系统', prefixes: ['system:'] },
|
||||
];
|
||||
|
||||
function groupPermNames(perms: string[], codes: string[]): { label: string; count: number }[] {
|
||||
const grouped = PERM_MODULE_GROUPS.map(g => {
|
||||
const matched = perms.filter((_, i) => g.prefixes.some(pre => (codes[i] || '').startsWith(pre)));
|
||||
return { label: g.label, count: matched.length };
|
||||
}).filter(g => g.count > 0);
|
||||
return grouped;
|
||||
}
|
||||
|
||||
// 将后端角色数据规范化为前端 Role 类型
|
||||
function normalizeRole(raw: any): Role {
|
||||
const rawPerms = Array.isArray(raw.permissions) ? raw.permissions : [];
|
||||
return {
|
||||
id: String(raw.id ?? ''),
|
||||
name: raw.name ?? '',
|
||||
code: raw.code ?? '',
|
||||
description: raw.description ?? '',
|
||||
// 后端返回权限对象数组(含 name/code/type),映射为显示名数组
|
||||
permissions: rawPerms.map(( p: any) => (typeof p === 'string' ? p : p?.name ?? p?.code ?? '')).filter(Boolean),
|
||||
permissionCodes: rawPerms.map(( p: any) => (typeof p === 'string' ? p : p?.code ?? '')).filter(Boolean),
|
||||
permissions: rawPerms.map((p: any) => (typeof p === 'string' ? p : p?.name ?? p?.code ?? '')).filter(Boolean),
|
||||
permissionCodes: rawPerms.map((p: any) => (typeof p === 'string' ? p : p?.code ?? '')).filter(Boolean),
|
||||
createdAt: raw.createdAt ?? raw.created_at ?? '',
|
||||
// 保留原始权限 ID 供编辑使用
|
||||
isSystem: raw.is_system ?? raw.isSystem ?? false,
|
||||
_permissionIds: rawPerms
|
||||
.map(( p: any) => (typeof p === 'object' ? Number(p.id) : 0))
|
||||
.map((p: any) => (typeof p === 'object' ? Number(p.id) : 0))
|
||||
.filter(Boolean),
|
||||
} as Role & { _permissionIds: number[]; permissionCodes: string[] };
|
||||
_raw: raw,
|
||||
} as Role & { _permissionIds: number[]; permissionCodes: string[]; isSystem: boolean; _raw: any };
|
||||
}
|
||||
|
||||
const emptyForm: RoleFormData = {
|
||||
name: '',
|
||||
code: '',
|
||||
description: '',
|
||||
permission_ids: [],
|
||||
name: '', code: '', description: '', permission_ids: [],
|
||||
};
|
||||
|
||||
export function SystemRolesTab({ selectedRole, setSelectedRole }: SystemRolesTabProps) {
|
||||
const confirm = useConfirm();
|
||||
const [roles, setRoles] = useState<Role[]>([]);
|
||||
const [roles, setRoles] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [permissions, setPermissions] = useState<PermissionOption[]>([]);
|
||||
|
||||
// 新建 / 编辑弹窗
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [editingRole, setEditingRole] = useState<any | null>(null);
|
||||
const [form, setForm] = useState<RoleFormData>({ ...emptyForm });
|
||||
@@ -82,11 +102,8 @@ export function SystemRolesTab({ selectedRole, setSelectedRole }: SystemRolesTab
|
||||
const resp = await userApi.getPermissions();
|
||||
const list = resp?.list ?? [];
|
||||
setPermissions(
|
||||
list.map(( p: any) => ({
|
||||
id: Number(p.id),
|
||||
name: p.name ?? p.code ?? '',
|
||||
code: p.code ?? '',
|
||||
type: p.type,
|
||||
list.map((p: any) => ({
|
||||
id: Number(p.id), name: p.name ?? p.code ?? '', code: p.code ?? '', type: p.type,
|
||||
}))
|
||||
);
|
||||
} catch {
|
||||
@@ -105,8 +122,8 @@ export function SystemRolesTab({ selectedRole, setSelectedRole }: SystemRolesTab
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const openEdit = ( role: any) => {
|
||||
setEditingRole(role);
|
||||
const openEdit = (role: any) => {
|
||||
setEditingRole(role._raw || role);
|
||||
setForm({
|
||||
name: role.name ?? '',
|
||||
code: role.code ?? '',
|
||||
@@ -117,14 +134,8 @@ export function SystemRolesTab({ selectedRole, setSelectedRole }: SystemRolesTab
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!form.name.trim()) {
|
||||
toast.error('请输入角色名称');
|
||||
return;
|
||||
}
|
||||
if (!form.code.trim()) {
|
||||
toast.error('请输入角色编码');
|
||||
return;
|
||||
}
|
||||
if (!form.name.trim()) { toast.error('请输入角色名称'); return; }
|
||||
if (!form.code.trim()) { toast.error('请输入角色编码'); return; }
|
||||
setSaving(true);
|
||||
try {
|
||||
const payload = {
|
||||
@@ -149,7 +160,11 @@ export function SystemRolesTab({ selectedRole, setSelectedRole }: SystemRolesTab
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async ( role: any) => {
|
||||
const handleDelete = async (role: any) => {
|
||||
if (role.isSystem) {
|
||||
toast.error('系统默认角色不可删除');
|
||||
return;
|
||||
}
|
||||
const ok = await confirm({
|
||||
title: '删除角色',
|
||||
description: `确认删除角色「${role.name}」吗?该操作不可恢复。`,
|
||||
@@ -162,16 +177,17 @@ export function SystemRolesTab({ selectedRole, setSelectedRole }: SystemRolesTab
|
||||
toast.success('角色已删除');
|
||||
await loadRoles();
|
||||
} catch (err: any) {
|
||||
toast.error(err?.message || '删除失败');
|
||||
const msg = err?.response?.data?.message || err?.message || '删除失败';
|
||||
toast.error(msg);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<span className="text-xs text-[var(--dg-text-muted)]">{roles.length} 个角色</span>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={openCreate}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
新建角色
|
||||
<Plus className="h-4 w-4 mr-2" />新建角色
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -180,77 +196,83 @@ export function SystemRolesTab({ selectedRole, setSelectedRole }: SystemRolesTab
|
||||
<div className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
) : roles.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-slate-500">
|
||||
<div className="flex flex-col items-center justify-center py-16 text-[var(--dg-text-muted)]">
|
||||
<Shield className="h-10 w-10 mb-3 opacity-40" />
|
||||
<p className="text-sm">暂无角色数据</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{roles.map(( role: any) => (
|
||||
<Card key={role.id} className="bg-slate-900 border-slate-800">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-purple-500/10 flex items-center justify-center">
|
||||
<Shield className="h-5 w-5 text-purple-400" />
|
||||
{roles.map((role: any) => {
|
||||
const perms = role.permissions as string[];
|
||||
const codes = role.permissionCodes as string[];
|
||||
const groupedPerms = groupPermNames(perms, codes);
|
||||
const icon = ROLE_ICONS[role.code] || '🛡️';
|
||||
return (
|
||||
<Card key={role.id} className={`bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] ${!role.isSystem ? '' : 'border-purple-500/20'}`}>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-lg flex items-center justify-center text-lg ${
|
||||
role.isSystem ? 'bg-purple-500/10' : 'bg-[var(--dg-bg-tertiary)]'
|
||||
}`}>
|
||||
{icon}
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base text-[var(--dg-text-primary)] flex items-center gap-1.5">
|
||||
{role.name}
|
||||
{role.isSystem && (
|
||||
<Badge className="bg-purple-500/15 text-purple-400 border-purple-500/30 text-[10px] px-1 py-0">
|
||||
<Lock className="h-2.5 w-2.5 mr-0.5" />系统
|
||||
</Badge>
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-[var(--dg-text-muted)] text-xs">{role.code}</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base text-slate-100">{role.name}</CardTitle>
|
||||
<CardDescription className="text-slate-500">{role.code}</CardDescription>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost" size="icon"
|
||||
className="h-8 w-8 text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]"
|
||||
onClick={() => setSelectedRole(role)}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost" size="icon"
|
||||
className="h-8 w-8 text-[var(--dg-text-muted)] hover:text-blue-400"
|
||||
onClick={() => openEdit(role)}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost" size="icon"
|
||||
className="h-8 w-8 text-[var(--dg-text-muted)] hover:text-red-400"
|
||||
onClick={() => handleDelete(role)}
|
||||
disabled={role.isSystem}
|
||||
title={role.isSystem ? '系统默认角色不可删除' : '删除角色'}
|
||||
>
|
||||
<Trash2 className={`h-4 w-4 ${role.isSystem ? 'opacity-30' : ''}`} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-slate-400 hover:text-slate-100"
|
||||
onClick={() => setSelectedRole(role)}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-slate-400 hover:text-slate-100"
|
||||
onClick={() => openEdit(role)}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-slate-400 hover:text-red-400"
|
||||
onClick={() => handleDelete(role)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-[var(--dg-text-secondary)] mb-3">{role.description || '暂无描述'}</p>
|
||||
{/* 按模块显示权限数 */}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{groupedPerms.map(g => (
|
||||
<Badge key={g.label} variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-muted)] text-[10px]">
|
||||
{g.label}·{g.count}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-slate-400 mb-3">{role.description || '暂无描述'}</p>
|
||||
{/* 权限按模块分组显示 // Logan.Li */}
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{(role.permissions as string[]).slice(0, 8).map((permission, idx) => (
|
||||
<Badge key={idx} variant="outline" className="border-slate-700 text-slate-400 text-xs">
|
||||
{permission}
|
||||
</Badge>
|
||||
))}
|
||||
{(role.permissions as string[]).length > 8 && (
|
||||
<Badge variant="outline" className="border-blue-500/30 text-blue-400 text-xs">
|
||||
+{(role.permissions as string[]).length - 8} 项
|
||||
</Badge>
|
||||
{perms.length === 0 && (
|
||||
<span className="text-xs text-[var(--dg-text-muted)]">未分配权限</span>
|
||||
)}
|
||||
{(role.permissions as string[]).length === 0 && (
|
||||
<span className="text-xs text-slate-600">未分配权限</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-slate-600">
|
||||
共 {role.permissions?.length || 0} 项权限
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -259,14 +281,9 @@ export function SystemRolesTab({ selectedRole, setSelectedRole }: SystemRolesTab
|
||||
|
||||
{/* 新建 / 编辑角色 Dialog */}
|
||||
<RoleFormDialog
|
||||
open={dialogOpen}
|
||||
onOpenChange={setDialogOpen}
|
||||
editingRole={editingRole}
|
||||
form={form}
|
||||
setForm={setForm}
|
||||
saving={saving}
|
||||
onSubmit={handleSubmit}
|
||||
permissions={permissions}
|
||||
open={dialogOpen} onOpenChange={setDialogOpen}
|
||||
editingRole={editingRole} form={form} setForm={setForm}
|
||||
saving={saving} onSubmit={handleSubmit} permissions={permissions}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -274,50 +291,50 @@ export function SystemRolesTab({ selectedRole, setSelectedRole }: SystemRolesTab
|
||||
|
||||
// 角色详情查看 Dialog
|
||||
function RoleDetailDialog({
|
||||
role,
|
||||
onClose,
|
||||
onEdit,
|
||||
}: {
|
||||
role: Role | null;
|
||||
onClose: () => void;
|
||||
onEdit: (role: Role) => void;
|
||||
}) {
|
||||
role, onClose, onEdit,
|
||||
}: { role: Role | null; onClose: () => void; onEdit: (role: Role) => void }) {
|
||||
const rawRole = role as any;
|
||||
const isSystem = rawRole?.isSystem ?? false;
|
||||
return (
|
||||
<Dialog open={!!role} onOpenChange={(v) => !v && onClose()}>
|
||||
<DialogContent className="bg-slate-900 border-slate-800">
|
||||
<DialogContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100 flex items-center gap-2">
|
||||
<Shield className="h-5 w-5 text-purple-400" />
|
||||
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
<ShieldCheck className="h-5 w-5 text-purple-400" />
|
||||
{role?.name}
|
||||
{isSystem && (
|
||||
<Badge className="bg-purple-500/15 text-purple-400 border-purple-500/30 text-[10px]">
|
||||
<Lock className="h-2.5 w-2.5 mr-0.5" />系统
|
||||
</Badge>
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">编码:{role?.code}</DialogDescription>
|
||||
<DialogDescription className="text-[var(--dg-text-muted)]">编码:{role?.code}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg bg-slate-800/50 p-4">
|
||||
<p className="text-xs text-slate-500 mb-1">描述</p>
|
||||
<p className="text-sm text-slate-200">{role?.description || '暂无描述'}</p>
|
||||
<div className="rounded-lg bg-[var(--dg-bg-tertiary)] p-4">
|
||||
<p className="text-xs text-[var(--dg-text-muted)] mb-1">描述</p>
|
||||
<p className="text-sm text-[var(--dg-text-primary)]">{role?.description || '暂无描述'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-300 mb-2">权限列表 ({role?.permissions?.length || 0} 项)</h4>
|
||||
{role && role.permissions.length > 0 ? (
|
||||
<h4 className="text-sm font-medium text-[var(--dg-text-primary)] mb-2">
|
||||
权限列表 ({role?.permissions?.length || 0} 项)
|
||||
</h4>
|
||||
{role && (role.permissions as string[]).length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2 max-h-60 overflow-y-auto">
|
||||
{role.permissions.map((p: string, idx: number) => (
|
||||
<Badge key={idx} variant="outline" className="border-slate-700 text-slate-300">
|
||||
{(role.permissions as string[]).map((p, idx) => (
|
||||
<Badge key={idx} variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
|
||||
{p}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-slate-500">未分配权限</p>
|
||||
<p className="text-xs text-[var(--dg-text-muted)]">未分配权限</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={() => role && onEdit(role)}>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
编辑
|
||||
<Edit className="h-4 w-4 mr-2" />编辑
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -25,6 +25,7 @@ import { UserFormDialog, type UserFormData } from './UserFormDialog';
|
||||
import { UserDetailDialog } from './UserDetailDialog';
|
||||
import { CsvImportDialog } from './CsvImportDialog';
|
||||
import { LdapImportDialog } from './LdapImportDialog';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
|
||||
interface SystemUserTabProps {
|
||||
// Search
|
||||
@@ -108,6 +109,7 @@ export function SystemUserTab({
|
||||
handleLdapImport,
|
||||
setLdapUsers,
|
||||
}: SystemUserTabProps) {
|
||||
const { hasPermission } = usePermission();
|
||||
return (
|
||||
<>
|
||||
{/* Filters */}
|
||||
@@ -122,18 +124,24 @@ export function SystemUserTab({
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{hasPermission('system:user') && (
|
||||
<Button variant="outline" className="border-slate-700 text-slate-300 hover:text-slate-100 hover:bg-slate-800" onClick={() => setImportDialogOpen('csv')}>
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
批量导入
|
||||
</Button>
|
||||
)}
|
||||
{hasPermission('system:user') && (
|
||||
<Button variant="outline" className="border-slate-700 text-slate-300 hover:text-slate-100 hover:bg-slate-800" onClick={() => setImportDialogOpen('ldap')}>
|
||||
<Network className="h-4 w-4 mr-2" />
|
||||
LDAP 导入
|
||||
</Button>
|
||||
)}
|
||||
{hasPermission('system:user') && (
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={openCreate}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
新建用户
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -232,6 +240,7 @@ export function SystemUserTab({
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
{hasPermission('system:user') && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -240,6 +249,8 @@ export function SystemUserTab({
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
{hasPermission('system:user') && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -248,6 +259,7 @@ export function SystemUserTab({
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
Reference in new issue
Block a user