import React, { useState, useEffect, useCallback } from 'react'; import { App, Table, Form, Input, Select, Button, Modal, Space, DatePicker, Switch, Dropdown, InputNumber, Tree, Tooltip, Checkbox, Popconfirm, Row, Col } from 'antd'; import type { TableColumnsType } from 'antd'; import { SearchOutlined, ReloadOutlined, PlusOutlined, EditOutlined, DeleteOutlined, DownloadOutlined, CheckCircleOutlined, DownOutlined, ExclamationCircleOutlined, QuestionCircleOutlined, UserOutlined } from '@ant-design/icons'; import { listRole, getRole, addRole, updateRole, dataScope, changeRoleStatus, delRole, deptTreeSelect } from '../../api/system/role'; import { treeselect as menuTreeselect, roleMenuTreeselect } from '../../api/system/menu'; import { saveAs } from 'file-saver'; import dayjs from 'dayjs'; import { parseTime } from '../../utils/ruoyi'; // Custom utility import Permission from '@/components/Permission'; import ReadonlyAction from '@/components/Permission/ReadonlyAction'; import { useNavigate } from 'react-router-dom'; import './system-admin.css'; const { RangePicker } = DatePicker; // Mock Dictionaries const sysNormalDisableDict = [ { value: '0', label: '正常' }, { value: '1', label: '停用' }, ]; const dataScopeOptions = [ { value: '1', label: '全部数据权限' }, { value: '2', label: '自定数据权限' }, { value: '3', label: '本部门数据权限' }, { value: '4', label: '本部门及以下数据权限' }, { value: '5', label: '仅本人数据权限' }, ]; type RoleQueryParams = { pageNum?: number; pageSize?: number; roleName?: string; roleKey?: string; status?: string; beginTime?: string; endTime?: string; }; type RoleRecord = { roleId?: string | number; roleName?: string; roleKey?: string; roleSort?: string | number; status?: string; createTime?: string | number | Date; [key: string]: unknown; }; const RolePage: React.FC = () => { const { message, modal } = App.useApp(); const navigate = useNavigate(); const [queryForm] = Form.useForm(); const [roleForm] = Form.useForm(); const [dataScopeForm] = Form.useForm(); const [loading, setLoading] = useState(false); const [roleList, setRoleList] = useState([]); const [total, setTotal] = useState(0); const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [dateRange, setDateRange] = useState<[dayjs.Dayjs | null, dayjs.Dayjs | null] | null>(null); const [addEditModalVisible, setAddEditModalVisible] = useState(false); const [addEditModalTitle, setAddEditModalTitle] = useState(''); const [formType, setFormType] = useState<'add' | 'edit'>('add'); const [dataScopeModalVisible, setDataScopeModalVisible] = useState(false); const [dataScopeModalTitle, setDataScopeModalTitle] = useState(''); const [menuOptions, setMenuOptions] = useState([]); const [deptOptions, setDeptOptions] = useState([]); const [menuExpand, setMenuExpand] = useState(false); const [menuNodeAll, setMenuNodeAll] = useState(false); const [deptExpand, setDeptExpand] = useState(false); const [deptNodeAll, setDeptNodeAll] = useState(false); const [menuExpandedKeys, setMenuExpandedKeys] = useState([]); const [deptExpandedKeys, setDeptExpandedKeys] = useState([]); const [menuCheckedKeys, setMenuCheckedKeys] = useState([]); const [menuHalfCheckedKeys, setMenuHalfCheckedKeys] = useState([]); const [deptCheckedKeys, setDeptCheckedKeys] = useState([]); const [deptHalfCheckedKeys, setDeptHalfCheckedKeys] = useState([]); const [menuCheckStrictly, setMenuCheckStrictly] = useState(true); // For menu tree linkage const [deptCheckStrictly, setDeptCheckStrictly] = useState(true); // For dept tree linkage const [currentRole, setCurrentRole] = useState({}); // For storing current role in modals const [queryParams, setQueryParams] = useState({ pageNum: 1, pageSize: 10, roleName: undefined, roleKey: undefined, status: undefined, }); const extractResponseData = (response: any) => { if (response && typeof response === 'object' && 'data' in response) { return response.data ?? {}; } return response ?? {}; }; const extractTreeNodes = (response: any, key: 'menus' | 'depts') => { if (!response || typeof response !== 'object') { return []; } if (Array.isArray(response[key])) { return response[key]; } if (Array.isArray(response.data)) { return response.data; } if (response.data && typeof response.data === 'object' && Array.isArray(response.data[key])) { return response.data[key]; } return []; }; const extractCheckedKeys = (response: any): React.Key[] => { if (!response || typeof response !== 'object') { return []; } if (Array.isArray(response.checkedKeys)) { return response.checkedKeys; } if (response.data && typeof response.data === 'object' && Array.isArray(response.data.checkedKeys)) { return response.data.checkedKeys; } return []; }; const collectTreeKeys = (nodes: any[]): React.Key[] => { const keys: React.Key[] = []; const visit = (list: any[]) => { list.forEach((node) => { const key = node?.id ?? node?.key; if (key !== undefined) { keys.push(key); } if (Array.isArray(node?.children) && node.children.length > 0) { visit(node.children); } }); }; visit(nodes ?? []); return keys; }; const getList = useCallback(async () => { setLoading(true); try { const formattedQueryParams: RoleQueryParams = { ...queryParams }; if (dateRange && dateRange[0] && dateRange[1]) { formattedQueryParams.beginTime = dateRange[0].format('YYYY-MM-DD'); formattedQueryParams.endTime = dateRange[1].format('YYYY-MM-DD'); } else { formattedQueryParams.beginTime = undefined; formattedQueryParams.endTime = undefined; } const response = await listRole(formattedQueryParams) as { rows?: RoleRecord[]; total?: number }; setRoleList(response.rows ?? []); setTotal(response.total ?? 0); } catch (error) { console.error('Failed to fetch role list:', error); message.error('获取角色列表失败'); } finally { setLoading(false); } }, [queryParams, dateRange]); const getMenuTreeselect = useCallback(async (roleId?: string | number) => { try { const response = roleId !== undefined ? await roleMenuTreeselect(roleId) : await menuTreeselect(); setMenuOptions(extractTreeNodes(response, 'menus')); return response; } catch (error) { console.error('Failed to fetch menu tree:', error); message.error('获取菜单树失败'); return null; } }, []); const getDeptTreeselect = useCallback(async (roleId?: string | number) => { try { const response = await deptTreeSelect(roleId ?? ''); setDeptOptions(extractTreeNodes(response, 'depts')); return response; } catch (error) { console.error('Failed to fetch department tree:', error); message.error('获取部门树失败'); return null; } }, []); useEffect(() => { getList(); }, [getList]); const handleQuery = (values?: Partial) => { const formValues = values ?? queryForm.getFieldsValue(); setQueryParams((prev) => ({ ...prev, pageNum: 1, roleName: formValues.roleName || undefined, roleKey: formValues.roleKey || undefined, status: formValues.status || undefined, })); }; const resetQuery = () => { queryForm.resetFields(); setDateRange(null); setQueryParams({ pageNum: 1, pageSize: 10, roleName: undefined, roleKey: undefined, status: undefined, }); }; const handleSelectionChange = (selectedKeys: React.Key[]) => { setSelectedRowKeys(selectedKeys); }; const handleStatusChange = async (record: any) => { const newStatus = record.status === '0' ? '1' : '0'; const text = record.status === '0' ? '停用' : '启用'; modal.confirm({ title: '确认操作', icon: , content: `确认要"${text}"角色"${record.roleName}"吗?`, onOk: async () => { try { await changeRoleStatus(record.roleId, newStatus); message.success(`${text}成功`); getList(); } catch (error) { message.error(`${text}失败`); setRoleList(prev => prev.map(role => role.roleId === record.roleId ? { ...role, status: record.status } : role)); } }, onCancel() { setRoleList(prev => prev.map(role => role.roleId === record.roleId ? { ...role, status: record.status } : role)); }, }); }; const resetRoleForm = () => { roleForm.resetFields(); dataScopeForm.resetFields(); setMenuOptions([]); // Clear menu options setDeptOptions([]); // Clear dept options setCurrentRole({}); // Clear current role setMenuExpand(false); setMenuNodeAll(false); setDeptExpand(false); setDeptNodeAll(false); setMenuExpandedKeys([]); setDeptExpandedKeys([]); setMenuCheckedKeys([]); setMenuHalfCheckedKeys([]); setDeptCheckedKeys([]); setDeptHalfCheckedKeys([]); setMenuCheckStrictly(true); setDeptCheckStrictly(true); }; const handleAdd = async () => { resetRoleForm(); await getMenuTreeselect(); // Get full menu tree setAddEditModalTitle('添加角色'); setFormType('add'); setAddEditModalVisible(true); roleForm.setFieldsValue({ roleSort: 0, status: '0', menuCheckStrictly: true, deptCheckStrictly: true, }); }; const handleUpdate = async (record?: any) => { resetRoleForm(); const roleId = record?.roleId || selectedRowKeys[0]; if (roleId === undefined) { message.warning('请选择要修改的角色'); return; } try { const roleMenuResponse = await getMenuTreeselect(roleId); // Get menu tree with selected keys const roleResponse = await getRole(roleId); const roleData = extractResponseData(roleResponse); setCurrentRole(roleData); setMenuCheckStrictly(roleData.menuCheckStrictly === undefined ? true : !!roleData.menuCheckStrictly); setDeptCheckStrictly(roleData.deptCheckStrictly === undefined ? true : !!roleData.deptCheckStrictly); setMenuCheckedKeys(extractCheckedKeys(roleMenuResponse)); setMenuHalfCheckedKeys([]); setAddEditModalTitle('修改角色'); setFormType('edit'); roleForm.setFieldsValue({ ...roleData, status: roleData.status?.toString() ?? '0', }); setAddEditModalVisible(true); } catch (error) { message.error('获取角色详情失败'); } }; const handleDelete = (record?: any) => { const roleIds = record?.roleId ? [record.roleId] : selectedRowKeys; if (roleIds.length === 0) { message.warning('请选择要删除的角色'); return; } modal.confirm({ title: '确认删除', icon: , content: `是否确认删除角色编号为"${roleIds.join(',')}"的数据项?`, onOk: async () => { try { await delRole(roleIds.join(',')); message.success('删除成功'); setSelectedRowKeys([]); getList(); } catch (error) { message.error('删除失败'); } }, }); }; const handleExport = async () => { const hide = message.loading('正在导出数据...', 0); try { const response = await listRole({ ...queryParams, pageNum: undefined, pageSize: undefined }) as { rows?: RoleRecord[] }; const header = ['角色编号', '角色名称', '权限字符', '显示顺序', '状态', '创建时间']; const data = (response.rows ?? []).map((role) => [ role.roleId, role.roleName, role.roleKey, role.roleSort, sysNormalDisableDict.find((d) => d.value === String(role.status ?? ''))?.label ?? '', parseTime(role.createTime), ]); const csvContent = [header, ...data] .map((row) => row.map((cell: unknown) => `"${String(cell).replace(/"/g, '""')}"`).join(',')) .join('\n'); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); saveAs(blob, `role_${dayjs().format('YYYYMMDDHHmmss')}.csv`); hide(); message.success('导出成功'); } catch { hide(); message.error('导出失败'); } }; const handleCommand = (key: string, record: any) => { switch (key) { case 'handleDataScope': handleDataScope(record); break; case 'handleAuthUser': handleAuthUser(record); break; default: break; } }; const handleDataScope = async (record: any) => { resetRoleForm(); const roleId = record.roleId; try { const deptTreeResponse = await getDeptTreeselect(roleId); // Get dept tree with selected keys const roleResponse = await getRole(roleId); const roleData = extractResponseData(roleResponse); setCurrentRole(roleData); setDataScopeModalTitle('分配数据权限'); setDataScopeModalVisible(true); dataScopeForm.setFieldsValue({ ...roleData, dataScope: roleData.dataScope?.toString(), }); setDeptCheckedKeys(extractCheckedKeys(deptTreeResponse)); setDeptHalfCheckedKeys([]); } catch (error) { message.error('获取数据权限信息失败'); } }; const handleAuthUser = (record: any) => { navigate(`/system/role-auth/user/${record.roleId}`); }; const submitRoleForm = async (values: any) => { try { const menuIds = [...menuCheckedKeys, ...menuHalfCheckedKeys]; const roleData = { ...currentRole, ...values, menuIds, menuCheckStrictly }; if (formType === 'edit') { await updateRole(roleData); message.success('修改成功'); } else { await addRole(roleData); message.success('新增成功'); } setAddEditModalVisible(false); getList(); } catch (error) { console.error('Submit role form failed:', error); message.error('操作失败'); } }; const submitDataScopeForm = async (values: any) => { try { let deptIds: React.Key[] = []; if (values.dataScope === '2') { deptIds = [...deptCheckedKeys, ...deptHalfCheckedKeys]; } const roleData = { ...currentRole, ...values, deptIds, deptCheckStrictly }; await dataScope(roleData); message.success('分配数据权限成功'); setDataScopeModalVisible(false); getList(); } catch (error) { message.error('分配数据权限失败'); } }; const dataScopeSelectChange = (value: string) => { dataScopeForm.setFieldValue('dataScope', value); if (value !== '2') { setDeptCheckedKeys([]); setDeptHalfCheckedKeys([]); } }; const handleCheckedTreeExpand = (value: boolean, type: 'menu' | 'dept') => { if (type === 'menu') { setMenuExpand(value); setMenuExpandedKeys(value ? collectTreeKeys(menuOptions) : []); return; } setDeptExpand(value); setDeptExpandedKeys(value ? collectTreeKeys(deptOptions) : []); }; const handleCheckedTreeNodeAll = (value: boolean, type: 'menu' | 'dept') => { if (type === 'menu') { const keys = value ? collectTreeKeys(menuOptions) : []; setMenuCheckedKeys(keys); setMenuHalfCheckedKeys([]); setMenuNodeAll(value); return; } const keys = value ? collectTreeKeys(deptOptions) : []; setDeptCheckedKeys(keys); setDeptHalfCheckedKeys([]); setDeptNodeAll(value); }; const handleMenuCheck = (checked: any, info: any) => { if (Array.isArray(checked)) { setMenuCheckedKeys(checked); setMenuHalfCheckedKeys(info?.halfCheckedKeys ?? []); return; } setMenuCheckedKeys(checked?.checked ?? []); setMenuHalfCheckedKeys(checked?.halfChecked ?? []); }; const handleDeptCheck = (checked: any, info: any) => { if (Array.isArray(checked)) { setDeptCheckedKeys(checked); setDeptHalfCheckedKeys(info?.halfCheckedKeys ?? []); return; } setDeptCheckedKeys(checked?.checked ?? []); setDeptHalfCheckedKeys(checked?.halfChecked ?? []); }; const handleCheckedTreeConnect = (value: boolean, type: 'menu' | 'dept') => { if (type === 'menu') setMenuCheckStrictly(value); if (type === 'dept') setDeptCheckStrictly(value); }; const columns: TableColumnsType = [ Table.SELECTION_COLUMN, { title: '角色编号', dataIndex: 'roleId', align: 'center', width: 100 }, { title: '角色名称', dataIndex: 'roleName', align: 'center', ellipsis: true }, { title: '权限字符', dataIndex: 'roleKey', align: 'center', ellipsis: true }, { title: '显示顺序', dataIndex: 'roleSort', align: 'center', width: 80 }, { title: '状态', dataIndex: 'status', align: 'center', width: 100, render: (text: string, record: RoleRecord) => ( } > handleStatusChange(record)} disabled={record.roleId === 1} /> ), }, { title: '创建时间', dataIndex: 'createTime', align: 'center', width: 180, render: (text: string) => parseTime(text) }, { title: '操作', key: 'operation', align: 'center', width: 220, render: (_, record) => ( }>修改}> } danger>删除}> handleDelete(record)} okText="是" cancelText="否" disabled={record.roleId === 1} > }>更多} > , label: '数据权限' }, { key: 'handleAuthUser', icon: , label: '分配用户' }, ], onClick: ({ key }) => handleCommand(String(key), record), }} trigger={['click']} disabled={record.roleId === 1} > ), }, ]; return (
queryForm.submit()} style={{ width: 240 }} /> queryForm.submit()} style={{ width: 240 }} /> setDateRange(dates)} format="YYYY-MM-DD" style={{ width: 240 }} />
`共 ${total} 条`, onChange: (page, pageSize) => { setQueryParams(prev => ({ ...prev, pageNum: page, pageSize: pageSize })); }, }} /> {/* Add/Edit Role Modal */} setAddEditModalVisible(false)} width={500} forceRender >
权限字符 } name="roleKey" rules={[{ required: true, message: '请输入权限字符' }]} // Corrected escaping for " >
handleCheckedTreeExpand(e.target.checked, 'menu')}>展开/折叠 handleCheckedTreeNodeAll(e.target.checked, 'menu')}>全选/全不选 handleCheckedTreeConnect(e.target.checked, 'menu')}>父子联动 setMenuExpandedKeys(expandedKeys)} /> {/* Assign Data Scope Modal */} setDataScopeModalVisible(false)} width={500} forceRender >
prevValues.dataScope !== currentValues.dataScope}> {({ getFieldValue }) => getFieldValue('dataScope') === '2' ? (
handleCheckedTreeExpand(e.target.checked, 'dept')}>展开/折叠 handleCheckedTreeNodeAll(e.target.checked, 'dept')}>全选/全不选 handleCheckedTreeConnect(e.target.checked, 'dept')}>父子联动 setDeptExpandedKeys(expandedKeys)} /> ) : null } ); }; export default RolePage;