import { Button, Card, Drawer, Form, Input, message, Popconfirm, Space, Table, Tag, Typography, Tree, Row, Col, Tabs, Empty, Select, Modal } from "antd"; import type { DataNode } from "antd/es/tree"; import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { createRole, listPermissions, listRolePermissions, listRoles, saveRolePermissions, updateRole, deleteRole, fetchUsersByRoleId, bindUsersToRole, unbindUserFromRole, listUsers } from "../api"; import type { SysPermission, SysRole, SysUser } from "../types"; import { usePermission } from "../hooks/usePermission"; import { EditOutlined, PlusOutlined, SafetyCertificateOutlined, SearchOutlined, DeleteOutlined, KeyOutlined, UserOutlined, SaveOutlined, UserAddOutlined } 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[], t: any): DataNode[] => nodes.map((node) => ({ key: node.permId, title: ( {node.name} {node.permType === "button" && {t('permissions.permType') === '按钮' ? '按钮' : 'Button'}} ), children: node.children && node.children.length > 0 ? toTreeData(node.children, t) : undefined })); const generateRoleCode = () => `ROLE_${Date.now().toString(36).toUpperCase()}`; export default function Roles() { const { t } = useTranslation(); 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); // User selection states const [allUsers, setAllUsers] = useState([]); const [userModalOpen, setUserModalOpen] = useState(false); const [selectedUserKeys, setSelectedUserKeys] = useState([]); const [userSearchText, setUserSearchText] = useState(""); // 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), t), [permissions, t] ); const loadAllUsers = async () => { try { const list = await listUsers(); setAllUsers(list || []); } catch (e) { console.error(e); } }; const openUserModal = () => { loadAllUsers(); setSelectedUserKeys([]); setUserModalOpen(true); }; const handleAddUsers = async () => { if (!selectedRole || selectedUserKeys.length === 0) return; try { await bindUsersToRole(selectedRole.roleId, selectedUserKeys); message.success(t('common.success')); setUserModalOpen(false); selectRole(selectedRole); } catch (e) { message.error(t('common.error')); } }; const handleUnbindUser = async (userId: number) => { if (!selectedRole) return; try { await unbindUserFromRole(selectedRole.roleId, userId); message.success(t('common.success')); selectRole(selectedRole); } catch (e) { message.error(t('common.error')); } }; const filteredModalUsers = useMemo(() => { const existingIds = new Set(roleUsers.map(u => u.userId)); return allUsers.filter(u => !existingIds.has(u.userId) && (u.username.toLowerCase().includes(userSearchText.toLowerCase()) || u.displayName.toLowerCase().includes(userSearchText.toLowerCase())) ); }, [allUsers, roleUsers, userSearchText]); 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(t('common.error')); } finally { setLoadingUsers(false); } }; useEffect(() => { loadRoles(); }, []); // Reload role detail if permissions list loaded later useEffect(() => { if (selectedRole && permissions.length > 0) { 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(t('common.success')); if (selectedRole?.roleId === id) setSelectedRole(null); loadRoles(); } catch (e) { message.error(t('common.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(t('common.success')); } else { await createRole(payload); message.success(t('common.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(t('common.success')); } catch (e) { message.error(t('common.error')); } finally { setSaving(false); } }; return (
{/* Left: Role List */}