Files
aiops/frontend/src/components/system/SystemRolesTab.tsx
T

322 lines
11 KiB
TypeScript

import { useState, useEffect, useCallback } from 'react';
import { toast } from 'sonner';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Plus, Shield, Edit, Eye, Trash2 } 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';
interface SystemRolesTabProps {
selectedRole: Role | null;
setSelectedRole: (role: Role | null) => void;
}
// 将后端角色数据规范化为前端 Role 类型 // Logan.Li
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 ?? '',
// 后端可能返回权限对象数组或字符串数组,统一映射为编码字符串数组
permissions: rawPerms.map((p: any) => (typeof p === 'string' ? p : p?.code ?? '')).filter(Boolean),
createdAt: raw.createdAt ?? raw.created_at ?? '',
// 保留原始权限 ID 供编辑使用
_permissionIds: Array.isArray(raw.permissions)
? raw.permissions.map((p: any) => (typeof p === 'object' ? Number(p.id) : 0)).filter(Boolean)
: [],
} as Role & { _permissionIds: number[] };
}
const emptyForm: RoleFormData = {
name: '',
code: '',
description: '',
permission_ids: [],
};
export function SystemRolesTab({ selectedRole, setSelectedRole }: SystemRolesTabProps) {
const confirm = useConfirm();
const [roles, setRoles] = useState<Role[]>([]);
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 });
const [saving, setSaving] = useState(false);
const loadRoles = useCallback(async () => {
setLoading(true);
try {
const resp = await userApi.getRoles();
const list = resp?.list ?? [];
setRoles(list.map(normalizeRole));
} catch (err) {
setRoles([]);
} finally {
setLoading(false);
}
}, []);
const loadPermissions = useCallback(async () => {
try {
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,
}))
);
} catch {
setPermissions([]);
}
}, []);
useEffect(() => {
loadRoles();
loadPermissions();
}, [loadRoles, loadPermissions]);
const openCreate = () => {
setEditingRole(null);
setForm({ ...emptyForm });
setDialogOpen(true);
};
const openEdit = (role: any) => {
setEditingRole(role);
setForm({
name: role.name ?? '',
code: role.code ?? '',
description: role.description ?? '',
permission_ids: Array.isArray(role._permissionIds) ? [...role._permissionIds] : [],
});
setDialogOpen(true);
};
const handleSubmit = async () => {
if (!form.name.trim()) {
toast.error('请输入角色名称');
return;
}
if (!form.code.trim()) {
toast.error('请输入角色编码');
return;
}
setSaving(true);
try {
const payload = {
name: form.name.trim(),
code: form.code.trim(),
description: form.description.trim(),
permission_ids: form.permission_ids,
};
if (editingRole) {
await userApi.updateRole(editingRole.id, payload);
toast.success('角色更新成功');
} else {
await userApi.createRole(payload);
toast.success('角色创建成功');
}
setDialogOpen(false);
await loadRoles();
} catch (err: any) {
toast.error(err?.message || '保存失败');
} finally {
setSaving(false);
}
};
const handleDelete = async (role: any) => {
const ok = await confirm({
title: '删除角色',
description: `确认删除角色「${role.name}」吗?该操作不可恢复。`,
confirmText: '删除',
variant: 'destructive',
});
if (!ok) return;
try {
await userApi.deleteRole(role.id);
toast.success('角色已删除');
await loadRoles();
} catch (err: any) {
toast.error(err?.message || '删除失败');
}
};
return (
<>
<div className="flex items-center justify-end">
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={openCreate}>
<Plus className="h-4 w-4 mr-2" />
新建角色
</Button>
</div>
{loading ? (
<div className="flex items-center justify-center py-16">
<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">
<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) => (
<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" />
</div>
<div>
<CardTitle className="text-base text-slate-100">{role.name}</CardTitle>
<CardDescription className="text-slate-500">{role.code}</CardDescription>
</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>
</div>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-slate-400 mb-3">{role.description || '暂无描述'}</p>
<div className="flex flex-wrap gap-1">
{role.permissions.slice(0, 5).map((permission) => (
<Badge key={permission} variant="outline" className="border-slate-700 text-slate-400 text-xs">
{permission}
</Badge>
))}
{role.permissions.length > 5 && (
<Badge variant="outline" className="border-slate-700 text-slate-400 text-xs">
+{role.permissions.length - 5}
</Badge>
)}
{role.permissions.length === 0 && (
<span className="text-xs text-slate-600">未分配权限</span>
)}
</div>
</CardContent>
</Card>
))}
</div>
)}
{/* 角色详情 Dialog */}
<RoleDetailDialog role={selectedRole} onClose={() => setSelectedRole(null)} onEdit={(r) => { setSelectedRole(null); openEdit(r); }} />
{/* 新建 / 编辑角色 Dialog */}
<RoleFormDialog
open={dialogOpen}
onOpenChange={setDialogOpen}
editingRole={editingRole}
form={form}
setForm={setForm}
saving={saving}
onSubmit={handleSubmit}
permissions={permissions}
/>
</>
);
}
// 角色详情查看 Dialog
function RoleDetailDialog({
role,
onClose,
onEdit,
}: {
role: Role | null;
onClose: () => void;
onEdit: (role: Role) => void;
}) {
return (
<Dialog open={!!role} onOpenChange={(v) => !v && onClose()}>
<DialogContent className="bg-slate-900 border-slate-800">
<DialogHeader>
<DialogTitle className="text-slate-100 flex items-center gap-2">
<Shield className="h-5 w-5 text-purple-400" />
{role?.name}
</DialogTitle>
<DialogDescription className="text-slate-500">编码:{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>
<div>
<h4 className="text-sm font-medium text-slate-300 mb-2">权限列表</h4>
{role && role.permissions.length > 0 ? (
<div className="flex flex-wrap gap-2">
{role.permissions.map((p) => (
<Badge key={p} variant="outline" className="border-slate-700 text-slate-400">
{p}
</Badge>
))}
</div>
) : (
<p className="text-xs text-slate-500">未分配权限</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" />
编辑
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}