import React, { useState, useMemo, useCallback } from 'react'; import { Table, Modal, Form, Input, Space, Tag, Popconfirm, Card, Checkbox, Empty } from 'antd'; import { PlusOutlined, EditOutlined, StopOutlined } from '@ant-design/icons'; import api from '../../api'; import PermissionButton from '../../components/PermissionButton'; import EditableCell from '../../components/EditableCell'; import { message } from '../../ui/app-message'; import { useQuery } from '@tanstack/react-query'; import { useApiMutation } from '../../hooks/useApiMutation'; import { validateResponse } from '../../utils/validate'; import { permissionTreeSchema, rolesSchema } from '../../api/schemas'; import { getErrorMessage } from '../../utils/error'; interface PermissionItem { id: number; code: string; name: string; group: string; } interface RoleItem { id: number; name: string; description: string; isSystem: boolean; status: number; permissions: PermissionItem[]; } const RolesPage: React.FC = () => { const [modalOpen, setModalOpen] = useState(false); const [editing, setEditing] = useState(null); const [form] = Form.useForm(); const [selectedPermIds, setSelectedPermIds] = useState([]); const [saving, setSaving] = useState(false); const { data: fetchResult = { roles: [], permTree: [] }, isLoading, isFetching, } = useQuery<{ roles: RoleItem[]; permTree: { group: string; permissions: PermissionItem[] }[]; }>({ queryKey: ['rbac', 'roles', 'permission-tree'], queryFn: async () => { try { const [roles, permTree] = await Promise.all([ api.get('/rbac/roles') as Promise, api.get('/rbac/permissions/tree') as Promise< { group: string; permissions: PermissionItem[] }[] >, ]); return { roles: validateResponse(rolesSchema, roles), permTree: validateResponse<{ group: string; permissions: PermissionItem[] }[]>( permissionTreeSchema, permTree, ), }; } catch (e: unknown) { message.error(getErrorMessage(e, '加载失败,请稍后重试')); return { roles: [], permTree: [] }; } }, }); const data = fetchResult.roles; const allPerms = fetchResult.permTree; const loading = isLoading || isFetching; const saveMutation = useApiMutation( async (values: { name: string; description?: string; permissionIds: number[] }) => editing ? api.put(`/rbac/roles/${editing.id}`, values) : api.post('/rbac/roles', values), { invalidate: [['rbac', 'roles', 'permission-tree']] }, ); const disableMutation = useApiMutation( async (id: number) => api.delete(`/rbac/roles/${id}`), { invalidate: [['rbac', 'roles', 'permission-tree']] }, ); const saveCellMutation = useApiMutation( async ({ record, field, value }: { record: RoleItem; field: string; value: unknown }) => api.put(`/rbac/roles/${record.id}`, { [field]: value }), { invalidate: [['rbac', 'roles', 'permission-tree']] }, ); const handleAdd = () => { setEditing(null); form.resetFields(); setSelectedPermIds([]); setModalOpen(true); }; const handleEdit = useCallback( (record: RoleItem) => { setEditing(record); form.setFieldsValue({ name: record.name, description: record.description }); setSelectedPermIds(record.permissions.map((p) => p.id)); setModalOpen(true); }, [form], ); const handleSubmit = async () => { setSaving(true); const values = await form.validateFields(); try { await saveMutation.mutateAsync({ name: values.name, description: values.description, permissionIds: selectedPermIds, }); message.success(editing ? '角色更新成功' : '角色创建成功'); setModalOpen(false); } catch { // 错误提示由 useApiMutation 统一处理 } finally { setSaving(false); } }; const handleDisable = useCallback( async (id: number) => { try { await disableMutation.mutateAsync(id); message.success('角色已停用'); } catch { // 错误提示由 useApiMutation 统一处理 } }, [disableMutation], ); const groupNames: Record = { dashboard: '数据面板', notification: '通知中心', student: '学生管理', 'student-scope': '学生范围权限', teacher: '教师管理', 'teacher-workspace': '教师工作台', room: '宿舍管理', occupancy: '入住管理', expense: '费用管理', bill: '账单管理', deposit: '押金管理', wallet: '学生余额', classroom: '教室管理', organization: '机构管理', rental: '租赁订单', class: '班级管理', schedule: '排课管理', attendance: '考勤管理', 'attendance-scope': '考勤范围权限', log: '操作日志', user: '用户管理', role: '角色管理', sync: '数据同步', integration: '集成配置', ai: 'AI 配置', 'ai-chat': 'AI 助手', }; const permissionOptions = useMemo( () => allPerms .flatMap((group) => group.permissions) .map((item) => ({ value: item.id, label: `${item.group} · ${item.name}` })), [allPerms], ); const saveCell = useCallback( async (record: RoleItem, field: string, value: unknown) => { try { await saveCellMutation.mutateAsync({ record, field, value }); message.success('已保存'); } catch { // 错误提示由 useApiMutation 统一处理 } }, [saveCellMutation], ); const columns = useMemo( () => [ { title: 'ID', dataIndex: 'id', width: 80 }, { title: '名称', dataIndex: 'name', width: 120, render: (v: string, r: RoleItem) => ( saveCell(r, 'name', next)} > {v} ), }, { title: '描述', dataIndex: 'description', width: 200, render: (v: string, r: RoleItem) => ( saveCell(r, 'description', next)} > {v || '-'} ), }, { title: '权限标签', dataIndex: 'permissions', width: 150, render: (perms: PermissionItem[], record: RoleItem) => ( item.id) || []} editor="multi-select" options={permissionOptions} permission="role:edit" onSave={(next) => saveCell(record, 'permissionIds', next)} > {perms?.length > 0 ? ( {perms.length} 个权限 ) : ( 无权限 )} ), }, { title: '系统', dataIndex: 'isSystem', width: 80, render: (v: boolean) => (v ? 系统 : null), }, { title: '操作', width: 180, fixed: 'right' as const, render: (_: any, record: RoleItem) => ( } onClick={() => handleEdit(record)} > 编辑 {!record.isSystem && ( handleDisable(record.id)}> } > 停用 )} ), }, ], [permissionOptions, saveCell, handleEdit, handleDisable], ); const handleGroupCheckAll = (group: string, checked: boolean) => { const groupPermIds = allPerms.find((g) => g.group === group)?.permissions.map((p) => p.id) || []; if (checked) { setSelectedPermIds((prev) => [...new Set([...prev, ...groupPermIds])]); } else { setSelectedPermIds((prev) => prev.filter((id) => !groupPermIds.includes(id))); } }; const isGroupAllChecked = (group: string) => { const groupPermIds = allPerms.find((g) => g.group === group)?.permissions.map((p) => p.id) || []; return groupPermIds.length > 0 && groupPermIds.every((id) => selectedPermIds.includes(id)); }; const isGroupIndeterminate = (group: string) => { const groupPermIds = allPerms.find((g) => g.group === group)?.permissions.map((p) => p.id) || []; const checkedCount = groupPermIds.filter((id) => selectedPermIds.includes(id)).length; return checkedCount > 0 && checkedCount < groupPermIds.length; }; return (

角色管理

} onClick={handleAdd} > 新增角色
}} scroll={{ x: 900 }} pagination={false} /> setModalOpen(false)} width={700} destroyOnHidden confirmLoading={saving} >
{allPerms.map((group) => ( handleGroupCheckAll(group.group, e.target.checked)} > {groupNames[group.group] || group.group} } style={{ marginBottom: 8 }} > setSelectedPermIds(vals as number[])} > {group.permissions.map((p) => ( {p.name} ))} ))}
); }; export default RolesPage;