Просмотр исходного кода

✨ feat(admin): 实现角色管理功能

- 添加角色管理菜单选项
- 创建角色管理页面,支持角色的增删改查
- 实现角色权限配置功能,支持树形权限选择

♻️ refactor(permissions): 重构权限管理页面

- 移除权限页面中的角色相关功能
- 清理未使用的导入和状态变量
- 优化权限列表获取逻辑,添加空查询参数处理

🔧 chore(routes): 添加角色管理路由配置

- 在管理后台路由中添加角色管理页面路由
yourname 8 месяцев назад
Родитель
Сommit
ffe8765a29
4 измененных файлов с 479 добавлено и 219 удалено
  1. 7 0
      src/client/admin/menu.tsx
  2. 26 219
      src/client/admin/pages/Permissions.tsx
  3. 440 0
      src/client/admin/pages/Roles.tsx
  4. 6 0
      src/client/admin/routes.tsx

+ 7 - 0
src/client/admin/menu.tsx

@@ -178,6 +178,13 @@ export const useMenu = () => {
           path: '/admin/departments',
           path: '/admin/departments',
           permission: 'department:manage'
           permission: 'department:manage'
         },
         },
+        {
+          key: 'roles',
+          label: '角色管理',
+          icon: <KeyOutlined />,
+          path: '/admin/roles',
+          permission: 'role:manage'
+        },
         {
         {
           key: 'permissions',
           key: 'permissions',
           label: '权限管理',
           label: '权限管理',

+ 26 - 219
src/client/admin/pages/Permissions.tsx

@@ -1,7 +1,7 @@
 import React, { useState, useEffect } from 'react';
 import React, { useState, useEffect } from 'react';
-import { Card, Table, Button, Modal, Form, Input, Select, Space, Tag, message, Row, Col, Tree, Checkbox, Radio } from 'antd';
-import { EditOutlined, DeleteOutlined, PlusOutlined, KeyOutlined } from '@ant-design/icons';
-import { permissionClient, rolePermissionClient, roleClient } from '@/client/api';
+import { Card, Table, Button, Modal, Form, Input, Select, Space, Tag, message } from 'antd';
+import { EditOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons';
+import { permissionClient } from '@/client/api';
 import type { InferResponseType, InferRequestType } from 'hono/client';
 import type { InferResponseType, InferRequestType } from 'hono/client';
 import dayjs from 'dayjs';
 import dayjs from 'dayjs';
 
 
@@ -13,21 +13,12 @@ type PermissionItem = PermissionListResponse['data'][0];
 type CreatePermissionRequest = InferRequestType<typeof permissionClient.$post>['json'];
 type CreatePermissionRequest = InferRequestType<typeof permissionClient.$post>['json'];
 type UpdatePermissionRequest = InferRequestType<typeof permissionClient[':id']['$put']>['json'];
 type UpdatePermissionRequest = InferRequestType<typeof permissionClient[':id']['$put']>['json'];
 
 
-type RoleListResponse = InferResponseType<typeof roleClient.$get, 200>;
-type RoleItem = RoleListResponse['data'][0];
-
 const Permissions: React.FC = () => {
 const Permissions: React.FC = () => {
   const [permissions, setPermissions] = useState<PermissionItem[]>([]);
   const [permissions, setPermissions] = useState<PermissionItem[]>([]);
-  const [roles, setRoles] = useState<RoleItem[]>([]);
   const [loading, setLoading] = useState(false);
   const [loading, setLoading] = useState(false);
   const [modalVisible, setModalVisible] = useState(false);
   const [modalVisible, setModalVisible] = useState(false);
-  const [rolePermissionModalVisible, setRolePermissionModalVisible] = useState(false);
   const [editingPermission, setEditingPermission] = useState<PermissionItem | null>(null);
   const [editingPermission, setEditingPermission] = useState<PermissionItem | null>(null);
-  const [selectedRole, setSelectedRole] = useState<RoleItem | null>(null);
-  const [rolePermissions, setRolePermissions] = useState<number[]>([]);
-  const [rolePermissionsData, setRolePermissionsData] = useState<any[]>([]);
   const [form] = Form.useForm();
   const [form] = Form.useForm();
-  const [rolePermissionForm] = Form.useForm();
 
 
   // 权限类型映射
   // 权限类型映射
   const permissionTypeMap = {
   const permissionTypeMap = {
@@ -40,7 +31,7 @@ const Permissions: React.FC = () => {
   const fetchPermissions = async () => {
   const fetchPermissions = async () => {
     setLoading(true);
     setLoading(true);
     try {
     try {
-      const response = await permissionClient.$get();
+      const response = await permissionClient.$get({ query: {} });
       if (response.status === 200) {
       if (response.status === 200) {
         const data = await response.json();
         const data = await response.json();
         setPermissions(data.data);
         setPermissions(data.data);
@@ -52,38 +43,8 @@ const Permissions: React.FC = () => {
     }
     }
   };
   };
 
 
-  // 获取角色列表
-  const fetchRoles = async () => {
-    try {
-      const response = await roleClient.$get();
-      if (response.status === 200) {
-        const data = await response.json();
-        setRoles(data.data);
-      }
-    } catch (error) {
-      message.error('获取角色列表失败');
-    }
-  };
-
-  // 获取角色的权限
-  const fetchRolePermissions = async (roleId: number) => {
-    try {
-      const response = await rolePermissionClient.$get({
-        query: { roleId }
-      });
-      if (response.status === 200) {
-        const data = await response.json();
-        setRolePermissionsData(data.data);
-        setRolePermissions(data.data.map((item: any) => item.permissionId));
-      }
-    } catch (error) {
-      message.error('获取角色权限失败');
-    }
-  };
-
   useEffect(() => {
   useEffect(() => {
     fetchPermissions();
     fetchPermissions();
-    fetchRoles();
   }, []);
   }, []);
 
 
   // 打开编辑模态框
   // 打开编辑模态框
@@ -100,13 +61,6 @@ const Permissions: React.FC = () => {
     setModalVisible(true);
     setModalVisible(true);
   };
   };
 
 
-  // 打开角色权限配置模态框
-  const handleRolePermission = (role: RoleItem) => {
-    setSelectedRole(role);
-    fetchRolePermissions(role.id);
-    setRolePermissionModalVisible(true);
-  };
-
   // 保存权限
   // 保存权限
   const handleSave = async (values: any) => {
   const handleSave = async (values: any) => {
     try {
     try {
@@ -140,30 +94,6 @@ const Permissions: React.FC = () => {
     }
     }
   };
   };
 
 
-  // 保存角色权限
-  const handleSaveRolePermissions = async (values: any) => {
-    if (!selectedRole) return;
-
-    try {
-      const permissions = values.permissions.map((p: any) => ({
-        permissionId: p.permissionId,
-        dataScopeType: p.dataScopeType,
-        customDepartments: p.customDepartments || []
-      }));
-
-      await rolePermissionClient['batch'].$post({
-        json: {
-          roleId: selectedRole.id,
-          permissions
-        }
-      });
-      message.success('角色权限更新成功');
-      setRolePermissionModalVisible(false);
-    } catch (error) {
-      message.error('保存角色权限失败');
-    }
-  };
-
   // 表格列定义
   // 表格列定义
   const columns = [
   const columns = [
     {
     {
@@ -236,7 +166,7 @@ const Permissions: React.FC = () => {
       key: 'action',
       key: 'action',
       width: 120,
       width: 120,
       fixed: 'right' as const,
       fixed: 'right' as const,
-      render: (_, record) => (
+      render: (_: any, record: PermissionItem) => (
         <Space>
         <Space>
           <Button
           <Button
             type="link"
             type="link"
@@ -258,103 +188,29 @@ const Permissions: React.FC = () => {
     },
     },
   ];
   ];
 
 
-  // 角色表格列定义
-  const roleColumns = [
-    {
-      title: '角色名称',
-      dataIndex: 'name',
-      key: 'name',
-    },
-    {
-      title: '描述',
-      dataIndex: 'description',
-      key: 'description',
-    },
-    {
-      title: '创建时间',
-      dataIndex: 'createdAt',
-      key: 'createdAt',
-      render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
-    },
-    {
-      title: '操作',
-      key: 'action',
-      render: (_, record) => (
-        <Button
-          type="primary"
-          icon={<KeyOutlined />}
-          onClick={() => handleRolePermission(record)}
-        >
-          配置权限
-        </Button>
-      ),
-    },
-  ];
-
-  // 权限数据按模块分组
-  const permissionTreeData = React.useMemo(() => {
-    const grouped = permissions.reduce((acc, permission) => {
-      const module = permission.module || '其他';
-      if (!acc[module]) {
-        acc[module] = [];
-      }
-      acc[module].push(permission);
-      return acc;
-    }, {} as Record<string, PermissionItem[]>);
-
-    return Object.entries(grouped).map(([module, items]) => ({
-      title: module,
-      key: module,
-      children: items.map(item => ({
-        title: `${item.name} (${item.code})`,
-        key: item.id.toString(),
-        permission: item,
-      })),
-    }));
-  }, [permissions]);
-
   return (
   return (
     <div className="p-6">
     <div className="p-6">
-      <Row gutter={[24, 24]}>
-        <Col span={24}>
-          <Card
-            title="权限管理"
-            extra={
-              <Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
-                新建权限
-              </Button>
-            }
-          >
-            <Table
-              columns={columns}
-              dataSource={permissions}
-              loading={loading}
-              rowKey="id"
-              scroll={{ x: 1200 }}
-              pagination={{
-                showSizeChanger: true,
-                showQuickJumper: true,
-                showTotal: (total) => `共 ${total} 条记录`,
-              }}
-            />
-          </Card>
-        </Col>
-        
-        <Col span={24}>
-          <Card title="角色权限配置">
-            <Table
-              columns={roleColumns}
-              dataSource={roles}
-              rowKey="id"
-              pagination={{
-                showSizeChanger: true,
-                showQuickJumper: true,
-                showTotal: (total) => `共 ${total} 条记录`,
-              }}
-            />
-          </Card>
-        </Col>
-      </Row>
+      <Card
+        title="权限管理"
+        extra={
+          <Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
+            新建权限
+          </Button>
+        }
+      >
+        <Table
+          columns={columns}
+          dataSource={permissions}
+          loading={loading}
+          rowKey="id"
+          scroll={{ x: 1200 }}
+          pagination={{
+            showSizeChanger: true,
+            showQuickJumper: true,
+            showTotal: (total: number) => `共 ${total} 条记录`,
+          }}
+        />
+      </Card>
 
 
       {/* 权限编辑模态框 */}
       {/* 权限编辑模态框 */}
       <Modal
       <Modal
@@ -436,55 +292,6 @@ const Permissions: React.FC = () => {
           </Form.Item>
           </Form.Item>
         </Form>
         </Form>
       </Modal>
       </Modal>
-
-      {/* 角色权限配置模态框 */}
-      <Modal
-        title={`配置权限 - ${selectedRole?.name}`}
-        open={rolePermissionModalVisible}
-        onCancel={() => setRolePermissionModalVisible(false)}
-        width={800}
-        footer={null}
-      >
-        <Form
-          form={rolePermissionForm}
-          layout="vertical"
-          onFinish={handleSaveRolePermissions}
-          initialValues={{ permissions: rolePermissionsData }}
-        >
-          <Form.Item label="选择权限">
-            <Tree
-              checkable
-              treeData={permissionTreeData}
-              checkedKeys={rolePermissions.map(String)}
-              onCheck={(checkedKeys) => {
-                const checkedIds = checkedKeys.map(Number);
-                setRolePermissions(checkedIds);
-              }}
-              titleRender={(node) => (
-                <span>
-                  {node.title}
-                  {node.permission && (
-                    <span style={{ marginLeft: 8, color: '#666', fontSize: 12 }}>
-                      {node.permission.description}
-                    </span>
-                  )}
-                </span>
-              )}
-            />
-          </Form.Item>
-          
-          <Form.Item>
-            <Space>
-              <Button type="primary" htmlType="submit">
-                保存权限配置
-              </Button>
-              <Button onClick={() => setRolePermissionModalVisible(false)}>
-                取消
-              </Button>
-            </Space>
-          </Form.Item>
-        </Form>
-      </Modal>
     </div>
     </div>
   );
   );
 };
 };

+ 440 - 0
src/client/admin/pages/Roles.tsx

@@ -0,0 +1,440 @@
+import React, { useState, useEffect } from 'react';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { Card, Table, Button, Modal, Form, Input, Space, Tag, message, Tree, Spin } from 'antd';
+import { EditOutlined, DeleteOutlined, PlusOutlined, KeyOutlined } from '@ant-design/icons';
+import { roleClient, permissionClient, rolePermissionClient } from '@/client/api';
+import type { InferResponseType, InferRequestType } from 'hono/client';
+import dayjs from 'dayjs';
+
+const { TextArea } = Input;
+
+type RoleListResponse = InferResponseType<typeof roleClient.$get, 200>;
+type RoleItem = RoleListResponse['data'][0];
+type CreateRoleRequest = InferRequestType<typeof roleClient.$post>['json'];
+type UpdateRoleRequest = InferRequestType<typeof roleClient[':id']['$put']>['json'];
+
+type PermissionListResponse = InferResponseType<typeof permissionClient.$get, 200>;
+type PermissionItem = PermissionListResponse['data'][0];
+
+// React Query hooks
+const useRoles = () => {
+  return useQuery({
+    queryKey: ['roles'],
+    queryFn: async () => {
+      const response = await roleClient.$get({ query: {} });
+      if (!response.ok) throw new Error('获取角色列表失败');
+      const data = await response.json();
+      return data.data;
+    },
+  });
+};
+
+const usePermissions = () => {
+  return useQuery({
+    queryKey: ['permissions'],
+    queryFn: async () => {
+      const response = await permissionClient.$get({ query: {} });
+      if (!response.ok) throw new Error('获取权限列表失败');
+      const data = await response.json();
+      return data.data;
+    },
+  });
+};
+
+const useRolePermissions = (roleId: number | null) => {
+  return useQuery({
+    queryKey: ['role-permissions', roleId],
+    queryFn: async () => {
+      if (!roleId) return [];
+      const response = await rolePermissionClient.$get({
+        query: { roleId: roleId as any }
+      });
+      if (!response.ok) throw new Error('获取角色权限失败');
+      const data = await response.json();
+      return data.data.map((item: any) => item.permissionId);
+    },
+    enabled: !!roleId,
+  });
+};
+
+const useCreateRole = () => {
+  const queryClient = useQueryClient();
+  return useMutation({
+    mutationFn: async (data: CreateRoleRequest) => {
+      const response = await roleClient.$post({
+        json: { ...data, permissions: [] as string[] }
+      });
+      if (!response.ok) throw new Error('创建角色失败');
+      return response.json();
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ['roles'] });
+      message.success('创建角色成功');
+    },
+    onError: (error: Error) => {
+      message.error(error.message);
+    },
+  });
+};
+
+const useUpdateRole = () => {
+  const queryClient = useQueryClient();
+  return useMutation({
+    mutationFn: async ({ id, data }: { id: number; data: UpdateRoleRequest }) => {
+      const response = await roleClient[':id'].$put({
+        param: { id: id.toString() },
+        json: data
+      });
+      if (!response.ok) throw new Error('更新角色失败');
+      return response.json();
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ['roles'] });
+      message.success('更新角色成功');
+    },
+    onError: (error: Error) => {
+      message.error(error.message);
+    },
+  });
+};
+
+const useDeleteRole = () => {
+  const queryClient = useQueryClient();
+  return useMutation({
+    mutationFn: async (id: number) => {
+      const response = await roleClient[':id'].$delete({ 
+        param: { id: id.toString() } 
+      });
+      if (!response.ok) throw new Error('删除角色失败');
+      return response.json();
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ['roles'] });
+      message.success('删除角色成功');
+    },
+    onError: (error: Error) => {
+      message.error(error.message);
+    },
+  });
+};
+
+const useUpdateRolePermissions = () => {
+  const queryClient = useQueryClient();
+  return useMutation({
+    mutationFn: async ({ roleId, permissionIds }: { roleId: number; permissionIds: number[] }) => {
+      const permissions = permissionIds.map(permissionId => ({
+        permissionId,
+        dataScopeType: 'COMPANY' as const,
+        customDepartments: [] as number[]
+      }));
+
+      const response = await rolePermissionClient.batch.$post({
+        json: { roleId, permissions }
+      });
+      if (!response.ok) throw new Error('更新角色权限失败');
+      return response.json();
+    },
+    onSuccess: () => {
+      message.success('角色权限更新成功');
+    },
+    onError: (error: Error) => {
+      message.error(error.message);
+    },
+  });
+};
+
+const Roles: React.FC = () => {
+  const queryClient = useQueryClient();
+  const [modalVisible, setModalVisible] = useState(false);
+  const [permissionModalVisible, setPermissionModalVisible] = useState(false);
+  const [editingRole, setEditingRole] = useState<RoleItem | null>(null);
+  const [selectedRole, setSelectedRole] = useState<RoleItem | null>(null);
+  const [form] = Form.useForm();
+  const [selectedPermissionIds, setSelectedPermissionIds] = useState<number[]>([]);
+
+  // React Query hooks
+  const { data: roles = [], isLoading: rolesLoading } = useRoles();
+  const { data: permissions = [], isLoading: permissionsLoading } = usePermissions();
+  const { data: rolePermissions = [], isLoading: rolePermissionsLoading } = useRolePermissions(selectedRole?.id ?? null);
+
+  // Mutations
+  const createRoleMutation = useCreateRole();
+  const updateRoleMutation = useUpdateRole();
+  const deleteRoleMutation = useDeleteRole();
+  const updateRolePermissionsMutation = useUpdateRolePermissions();
+
+  // 打开编辑模态框
+  const handleEdit = (record: RoleItem) => {
+    setEditingRole(record);
+    form.setFieldsValue({
+      name: record.name,
+      description: record.description
+    });
+    setModalVisible(true);
+  };
+
+  // 打开新建模态框
+  const handleAdd = () => {
+    setEditingRole(null);
+    form.resetFields();
+    setModalVisible(true);
+  };
+
+  // 打开权限配置模态框
+  const handlePermissionConfig = (role: RoleItem) => {
+    setSelectedRole(role);
+    setSelectedPermissionIds(rolePermissions);
+    setPermissionModalVisible(true);
+  };
+
+  // 保存角色
+  const handleSave = async (values: any) => {
+    if (editingRole) {
+      updateRoleMutation.mutate({ id: editingRole.id, data: values });
+    } else {
+      createRoleMutation.mutate(values);
+    }
+    setModalVisible(false);
+  };
+
+  // 删除角色
+  const handleDelete = (record: RoleItem) => {
+    Modal.confirm({
+      title: '确认删除',
+      content: `确定要删除角色 "${record.name}" 吗?`,
+      onOk: () => {
+        deleteRoleMutation.mutate(record.id);
+      },
+    });
+  };
+
+  // 保存角色权限
+  const handleSavePermissions = () => {
+    if (!selectedRole) return;
+    
+    updateRolePermissionsMutation.mutate(
+      { roleId: selectedRole.id, permissionIds: selectedPermissionIds },
+      {
+        onSuccess: () => {
+          setPermissionModalVisible(false);
+          queryClient.invalidateQueries({ queryKey: ['roles'] });
+        }
+      }
+    );
+  };
+
+  // 权限数据按模块分组
+  const permissionTreeData = React.useMemo(() => {
+    const grouped: Record<string, PermissionItem[]> = {};
+    permissions.forEach(permission => {
+      const module = permission.module || '其他';
+      if (!grouped[module]) {
+        grouped[module] = [];
+      }
+      grouped[module].push(permission);
+    });
+
+    return Object.entries(grouped).map(([module, items]) => ({
+      title: module,
+      key: module,
+      checkable: false,
+      children: items.map(item => ({
+        title: `${item.name} (${item.code})`,
+        key: item.id.toString(),
+      })),
+    }));
+  }, [permissions]);
+
+  // 在权限模态框打开时更新选中状态
+  useEffect(() => {
+    if (permissionModalVisible && rolePermissions.length > 0) {
+      setSelectedPermissionIds(rolePermissions);
+    }
+  }, [permissionModalVisible, rolePermissions]);
+
+  const columns = [
+    {
+      title: '角色名称',
+      dataIndex: 'name',
+      key: 'name',
+      width: 150,
+    },
+    {
+      title: '描述',
+      dataIndex: 'description',
+      key: 'description',
+      ellipsis: true,
+    },
+    {
+      title: '权限数量',
+      key: 'permissionCount',
+      width: 100,
+      render: (_: any, record: RoleItem) => (
+        <Tag color="blue">{record.permissions?.length || 0}</Tag>
+      ),
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'createdAt',
+      key: 'createdAt',
+      width: 180,
+      render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
+      sorter: (a: RoleItem, b: RoleItem) => 
+        dayjs(a.createdAt).valueOf() - dayjs(b.createdAt).valueOf(),
+    },
+    {
+      title: '更新时间',
+      dataIndex: 'updatedAt',
+      key: 'updatedAt',
+      width: 180,
+      render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
+      sorter: (a: RoleItem, b: RoleItem) => 
+        dayjs(a.updatedAt).valueOf() - dayjs(b.updatedAt).valueOf(),
+    },
+    {
+      title: '操作',
+      key: 'action',
+      width: 200,
+      fixed: 'right' as const,
+      render: (_: any, record: RoleItem) => (
+        <Space>
+          <Button
+            type="link"
+            icon={<EditOutlined />}
+            onClick={() => handleEdit(record)}
+          >
+            编辑
+          </Button>
+          <Button
+            type="link"
+            icon={<KeyOutlined />}
+            onClick={() => handlePermissionConfig(record)}
+          >
+            配置权限
+          </Button>
+          <Button
+            type="link"
+            danger
+            icon={<DeleteOutlined />}
+            onClick={() => handleDelete(record)}
+            disabled={record.name === 'admin'}
+          >
+            删除
+          </Button>
+        </Space>
+      ),
+    },
+  ];
+
+  return (
+    <div className="p-6">
+      <Spin spinning={rolesLoading || permissionsLoading}>
+        <Card
+          title="角色管理"
+          extra={
+            <Button 
+              type="primary" 
+              icon={<PlusOutlined />} 
+              onClick={handleAdd}
+              loading={createRoleMutation.isPending}
+            >
+              新建角色
+            </Button>
+          }
+        >
+          <Table
+            columns={columns}
+            dataSource={roles}
+            rowKey="id"
+            scroll={{ x: 1000 }}
+            pagination={{
+              showSizeChanger: true,
+              showQuickJumper: true,
+              showTotal: (total: number) => `共 ${total} 条记录`,
+              defaultPageSize: 10,
+              pageSizeOptions: [10, 20, 50, 100],
+            }}
+          />
+        </Card>
+      </Spin>
+
+      {/* 角色编辑模态框 */}
+      <Modal
+        title={editingRole ? '编辑角色' : '新建角色'}
+        open={modalVisible}
+        onCancel={() => setModalVisible(false)}
+        onOk={() => form.submit()}
+        okButtonProps={{ 
+          loading: editingRole ? updateRoleMutation.isPending : createRoleMutation.isPending 
+        }}
+        width={500}
+      >
+        <Form
+          form={form}
+          layout="vertical"
+          onFinish={handleSave}
+        >
+          <Form.Item
+            label="角色名称"
+            name="name"
+            rules={[
+              { required: true, message: '请输入角色名称' },
+              { min: 2, max: 50, message: '角色名称长度必须在2-50个字符之间' }
+            ]}
+          >
+            <Input 
+              placeholder="请输入角色名称" 
+              maxLength={50}
+              showCount
+            />
+          </Form.Item>
+          
+          <Form.Item
+            label="描述"
+            name="description"
+            rules={[{ max: 500, message: '描述最多500个字符' }]}
+          >
+            <TextArea
+              rows={3}
+              placeholder="请输入角色描述"
+              maxLength={500}
+              showCount
+            />
+          </Form.Item>
+        </Form>
+      </Modal>
+
+      {/* 权限配置模态框 */}
+      <Modal
+        title={`配置权限 - ${selectedRole?.name}`}
+        open={permissionModalVisible}
+        onCancel={() => setPermissionModalVisible(false)}
+        onOk={handleSavePermissions}
+        okButtonProps={{ loading: updateRolePermissionsMutation.isPending }}
+        width={800}
+        destroyOnClose
+      >
+        <Spin spinning={rolePermissionsLoading}>
+          <Tree
+            checkable
+            treeData={permissionTreeData}
+            checkedKeys={selectedPermissionIds.map(String)}
+            onCheck={(checkedKeys) => {
+              const checked = checkedKeys as string[];
+              setSelectedPermissionIds(checked.map(Number));
+            }}
+            height={400}
+            titleRender={(node) => {
+              if (node.children) {
+                return <strong>{node.title}</strong>;
+              }
+              return <span>{node.title}</span>;
+            }}
+          />
+        </Spin>
+      </Modal>
+    </div>
+  );
+};
+
+export default Roles;

+ 6 - 0
src/client/admin/routes.tsx

@@ -21,6 +21,7 @@ import OrderRecordsPage from './pages/OrderRecords';
 import FollowUpRecordsPage from './pages/FollowUpRecords';
 import FollowUpRecordsPage from './pages/FollowUpRecords';
 import DepartmentsPage from './pages/Departments';
 import DepartmentsPage from './pages/Departments';
 import PermissionsPage from './pages/Permissions';
 import PermissionsPage from './pages/Permissions';
+import RolesPage from './pages/Roles';
 import { LoginPage } from './pages/Login';
 import { LoginPage } from './pages/Login';
 
 
 export const router = createBrowserRouter([
 export const router = createBrowserRouter([
@@ -124,6 +125,11 @@ export const router = createBrowserRouter([
         element: <DepartmentsPage />,
         element: <DepartmentsPage />,
         errorElement: <ErrorPage />
         errorElement: <ErrorPage />
       },
       },
+      {
+        path: 'roles',
+        element: <RolesPage />,
+        errorElement: <ErrorPage />
+      },
       {
       {
         path: 'permissions',
         path: 'permissions',
         element: <PermissionsPage />,
         element: <PermissionsPage />,