import { Button, Card, Drawer, Form, Input, message, Popconfirm, Space, Table, Tag, Typography, Tree, Row, Col, Tabs, Empty } from "antd"; import type { DataNode } from "antd/es/tree"; import { useEffect, useMemo, useState } from "react"; import { createRole, listPermissions, listRolePermissions, listRoles, saveRolePermissions, updateRole, deleteRole, fetchUsersByRoleId } from "../api"; import type { SysPermission, SysRole, SysUser } from "../types"; import { usePermission } from "../hooks/usePermission"; import { EditOutlined, PlusOutlined, SafetyCertificateOutlined, SearchOutlined, DeleteOutlined, KeyOutlined, UserOutlined, SaveOutlined } from "@ant-design/icons"; import "./Roles.css"; const { Title, Text } = Typography; const DEFAULT_STATUS = 1; type PermissionNode = SysPermission & { key: number; children?: PermissionNode[] }; const buildPermissionTree = (list: SysPermission[]): PermissionNode[] => { if (!list || list.length === 0) return []; const active = list.filter((p) => p.status !== 0); const map = new Map(); const roots: PermissionNode[] = []; active.forEach((item) => { map.set(item.permId, { ...item, key: item.permId, children: [] }); }); map.forEach((node) => { if (node.parentId && node.parentId !== 0) { const parent = map.get(node.parentId); if (parent) { parent.children!.push(node); } } else { roots.push(node); } }); const sortNodes = (nodes: PermissionNode[]) => { nodes.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)); nodes.forEach((n) => n.children && sortNodes(n.children)); }; sortNodes(roots); return roots; }; const toTreeData = (nodes: PermissionNode[]): DataNode[] => nodes.map((node) => ({ key: node.permId, title: ( {node.name} {node.permType === "button" && 按钮} ), children: node.children && node.children.length > 0 ? toTreeData(node.children) : undefined })); const generateRoleCode = () => `ROLE_${Date.now().toString(36).toUpperCase()}`; export default function Roles() { const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [data, setData] = useState([]); const [permissions, setPermissions] = useState([]); const [selectedRole, setSelectedRole] = useState(null); // Right side states const [selectedPermIds, setSelectedPermIds] = useState([]); const [halfCheckedIds, setHalfCheckedIds] = useState([]); const [roleUsers, setRoleUsers] = useState([]); const [loadingUsers, setLoadingUsers] = useState(false); // Search const [searchText, setSearchText] = useState(""); // Drawer (Only for Add/Edit basic info) const [drawerOpen, setDrawerOpen] = useState(false); const [editing, setEditing] = useState(null); const [form] = Form.useForm(); const { can } = usePermission(); const permissionTreeData = useMemo( () => toTreeData(buildPermissionTree(permissions)), [permissions] ); const loadPermissions = async () => { try { const list = await listPermissions(); setPermissions(list || []); } catch (e) { setPermissions([]); } }; const loadRoles = async () => { setLoading(true); try { const list = await listRoles(); const roles = list || []; setData(roles); if (roles.length > 0 && !selectedRole) { selectRole(roles[0]); } else if (selectedRole) { const updated = roles.find(r => r.roleId === selectedRole.roleId); if (updated) setSelectedRole(updated); } await loadPermissions(); } finally { setLoading(false); } }; const selectRole = async (role: SysRole) => { setSelectedRole(role); try { // Load permissions for this role const ids = await listRolePermissions(role.roleId); const normalized = (ids || []).map((id) => Number(id)).filter((id) => !Number.isNaN(id)); // Filter out parents for Tree回显 const leafIds = normalized.filter(id => { return !permissions.some(p => p.parentId === id); }); setSelectedPermIds(leafIds); setHalfCheckedIds([]); // Load users for this role setLoadingUsers(true); const users = await fetchUsersByRoleId(role.roleId); setRoleUsers(users || []); } catch (e) { message.error("加载角色详情失败"); } finally { setLoadingUsers(false); } }; useEffect(() => { loadRoles(); }, []); // Reload role detail if permissions list loaded later useEffect(() => { if (selectedRole && permissions.length > 0) { // We don't want to infinite loop, but we need to ensure leafIds are correct // after permissions are loaded. const leafIds = selectedPermIds.filter(id => { return !permissions.some(p => p.parentId === id); }); if (leafIds.length !== selectedPermIds.length) { setSelectedPermIds(leafIds); } } }, [permissions]); const filteredData = useMemo(() => { if (!searchText) return data; const lower = searchText.toLowerCase(); return data.filter(r => r.roleName.toLowerCase().includes(lower) || r.roleCode.toLowerCase().includes(lower) ); }, [data, searchText]); const openCreate = () => { setEditing(null); form.resetFields(); form.setFieldsValue({ status: 1 }); setDrawerOpen(true); }; const openEditBasic = (e: React.MouseEvent, record: SysRole) => { e.stopPropagation(); setEditing(record); form.setFieldsValue(record); setDrawerOpen(true); }; const handleRemove = async (e: React.MouseEvent, id: number) => { e.stopPropagation(); try { await deleteRole(id); message.success("角色已删除"); if (selectedRole?.roleId === id) setSelectedRole(null); loadRoles(); } catch (e) { message.error("删除失败"); } }; const submitBasic = async () => { try { const values = await form.validateFields(); setSaving(true); const payload: Partial = { roleCode: editing?.roleCode || values.roleCode || generateRoleCode(), roleName: values.roleName, remark: values.remark, status: values.status ?? DEFAULT_STATUS }; if (editing) { await updateRole(editing.roleId, payload); message.success("角色已更新"); } else { await createRole(payload); message.success("角色已创建"); } setDrawerOpen(false); loadRoles(); } catch (e) { if (e instanceof Error && e.message) message.error(e.message); } finally { setSaving(false); } }; const savePermissions = async () => { if (!selectedRole) return; setSaving(true); try { const allPermIds = Array.from(new Set([...selectedPermIds, ...halfCheckedIds])); await saveRolePermissions(selectedRole.roleId, allPermIds); message.success("权限已保存并生效"); } catch (e) { message.error("保存权限失败"); } finally { setSaving(false); } }; return (
{/* Left: Role List */} } onClick={openCreate}>新增} >
} value={searchText} onChange={e => setSearchText(e.target.value)} allowClear />
({ onClick: () => selectRole(record), className: `cursor-pointer role-row ${selectedRole?.roleId === record.roleId ? 'role-row-selected' : ''}` })} columns={[ { title: '角色', render: (_, record) => (
{record.roleName}
{record.roleCode}
{can("sys_role:update") &&
) } ]} /> {/* Right: Detail Tabs */} {selectedRole ? ( {selectedRole.roleName} {selectedRole.roleCode} } extra={ } > 功能权限} key="permissions" >
{ const checked = Array.isArray(keys) ? keys : keys.checked; const halfChecked = info.halfCheckedKeys || []; setSelectedPermIds(checked.map(k => Number(k))); setHalfCheckedIds(halfChecked.map(k => Number(k))); }} defaultExpandAll />
关联用户 ({roleUsers.length})} key="users" >
(
{r.displayName}
@{r.username}
) }, { title: '手机号', dataIndex: 'phone' }, { title: '邮箱', dataIndex: 'email' }, { title: '状态', dataIndex: 'status', render: s => {s === 1 ? '正常' : '禁用'} } ]} /> ) : ( )} {/* Basic Info Drawer */} setDrawerOpen(false)} width={400} destroyOnClose footer={
} >