snapshot: AlertCenter拆分前快照

This commit is contained in:
attacker committed 2026-06-15 13:01:56 +08:00
1 parent 6a40956afa
commit 805cb767a8
23 files changed
+1560 -404

No files matched your search

@@ -0,0 +1,150 @@
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[];
}
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);
if (checked) {
set.add(id);
} else {
set.delete(id);
}
return { ...prev, permission_ids: Array.from(set) };
});
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<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" />
{editingRole ? '编辑角色' : '新建角色'}
</DialogTitle>
<DialogDescription className="text-slate-500">
{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-slate-500">角色名称</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"
placeholder="如:运维工程师"
/>
</div>
<div className="space-y-2">
<Label className="text-xs text-slate-500">角色编码</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"
placeholder="如:ops_engineer"
/>
</div>
</div>
<div className="space-y-2">
<Label className="text-xs text-slate-500">描述</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"
placeholder="角色职责描述(可选)"
rows={2}
/>
</div>
<div className="space-y-2">
<Label className="text-xs text-slate-500">
权限分配
{form.permission_ids.length > 0 && (
<span className="ml-2 text-slate-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="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>
)}
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" className="border-slate-700 text-slate-300" onClick={() => onOpenChange(false)}>
取消
</Button>
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={onSubmit} disabled={saving}>
{saving ? '保存中...' : '保存'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+226 -25
View File
@@ -1,10 +1,25 @@
import { useState, useEffect } from 'react';
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 { Plus, Shield, Edit, Eye } from 'lucide-react';
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;
@@ -13,45 +28,147 @@ interface SystemRolesTabProps {
// 将后端角色数据规范化为前端 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/createdAt,给予默认值以避免渲染崩溃
permissions: Array.isArray(raw.permissions) ? raw.permissions : [],
// 后端可能返回权限对象数组或字符串数组,统一映射为编码字符串数组
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(() => {
let cancelled = false;
(async () => {
setLoading(true);
try {
const resp = await userApi.getRoles();
if (cancelled) return;
const list = resp?.list ?? [];
setRoles(list.map(normalizeRole));
} catch (err) {
if (!cancelled) setRoles([]);
} finally {
if (!cancelled) setLoading(false);
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('角色创建成功');
}
})();
return () => {
cancelled = true;
};
}, []);
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">
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={openCreate}>
<Plus className="h-4 w-4 mr-2" />
新建角色
</Button>
@@ -90,14 +207,27 @@ export function SystemRolesTab({ selectedRole, setSelectedRole }: SystemRolesTab
>
<Eye className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-slate-100">
<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>
<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">
@@ -109,12 +239,83 @@ export function SystemRolesTab({ selectedRole, setSelectedRole }: SystemRolesTab
+{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>
);
}