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

✨ feat(permission): add permission management module

- add permission management menu item in admin sidebar
- create Permissions page with CRUD operations for permissions
- add role permission configuration functionality
- register permission and role-permission API clients
- implement permission and role-permission API routes with batch update support

✨ feat(api): add permission and role-permission API endpoints

- create permission entity and CRUD routes
- implement role-permission entity with batch update functionality
- add data scope type support for role permissions
- add custom route for batch updating role permissions

✨ feat(ui): add permission management interface

- design permission list table with filtering and sorting
- create role permission configuration modal
- implement permission tree selector for role permission assignment
- add permission create/edit modal with form validation
yourname 8 месяцев назад
Родитель
Сommit
89e343d503

+ 11 - 2
src/client/admin/menu.tsx

@@ -9,14 +9,16 @@ import {
   InfoCircleOutlined,
   CustomerServiceOutlined,
   DollarOutlined,
-  CalendarOutlined
+  CalendarOutlined,
+  KeyOutlined
 } from '@ant-design/icons';
 import {
   FileTextOutlined,
   DatabaseOutlined,
   FileProtectOutlined,
   HistoryOutlined,
-  AuditOutlined
+  AuditOutlined,
+  SettingOutlined
 } from '@ant-design/icons';
 
 export interface MenuItem {
@@ -175,6 +177,13 @@ export const useMenu = () => {
           icon: <TeamOutlined />,
           path: '/admin/departments',
           permission: 'department:manage'
+        },
+        {
+          key: 'permissions',
+          label: '权限管理',
+          icon: <KeyOutlined />,
+          path: '/admin/permissions',
+          permission: 'permission:manage'
         }
       ]
     },

+ 492 - 0
src/client/admin/pages/Permissions.tsx

@@ -0,0 +1,492 @@
+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 type { InferResponseType, InferRequestType } from 'hono/client';
+import dayjs from 'dayjs';
+
+const { Option } = Select;
+const { TextArea } = Input;
+
+type PermissionListResponse = InferResponseType<typeof permissionClient.$get, 200>;
+type PermissionItem = PermissionListResponse['data'][0];
+type CreatePermissionRequest = InferRequestType<typeof permissionClient.$post>['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, setPermissions] = useState<PermissionItem[]>([]);
+  const [roles, setRoles] = useState<RoleItem[]>([]);
+  const [loading, setLoading] = useState(false);
+  const [modalVisible, setModalVisible] = useState(false);
+  const [rolePermissionModalVisible, setRolePermissionModalVisible] = useState(false);
+  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 [rolePermissionForm] = Form.useForm();
+
+  // 权限类型映射
+  const permissionTypeMap = {
+    module: '模块权限',
+    operation: '操作权限',
+    data: '数据权限'
+  };
+
+  // 获取权限列表
+  const fetchPermissions = async () => {
+    setLoading(true);
+    try {
+      const response = await permissionClient.$get();
+      if (response.status === 200) {
+        const data = await response.json();
+        setPermissions(data.data);
+      }
+    } catch (error) {
+      message.error('获取权限列表失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 获取角色列表
+  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(() => {
+    fetchPermissions();
+    fetchRoles();
+  }, []);
+
+  // 打开编辑模态框
+  const handleEdit = (record: PermissionItem) => {
+    setEditingPermission(record);
+    form.setFieldsValue(record);
+    setModalVisible(true);
+  };
+
+  // 打开新建模态框
+  const handleAdd = () => {
+    setEditingPermission(null);
+    form.resetFields();
+    setModalVisible(true);
+  };
+
+  // 打开角色权限配置模态框
+  const handleRolePermission = (role: RoleItem) => {
+    setSelectedRole(role);
+    fetchRolePermissions(role.id);
+    setRolePermissionModalVisible(true);
+  };
+
+  // 保存权限
+  const handleSave = async (values: any) => {
+    try {
+      if (editingPermission) {
+        await permissionClient[':id'].$put({
+          param: { id: editingPermission.id },
+          json: values as UpdatePermissionRequest
+        });
+        message.success('更新权限成功');
+      } else {
+        await permissionClient.$post({
+          json: values as CreatePermissionRequest
+        });
+        message.success('创建权限成功');
+      }
+      setModalVisible(false);
+      fetchPermissions();
+    } catch (error) {
+      message.error('保存权限失败');
+    }
+  };
+
+  // 删除权限
+  const handleDelete = async (id: number) => {
+    try {
+      await permissionClient[':id'].$delete({ param: { id } });
+      message.success('删除权限成功');
+      fetchPermissions();
+    } catch (error) {
+      message.error('删除权限失败');
+    }
+  };
+
+  // 保存角色权限
+  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 = [
+    {
+      title: '权限编码',
+      dataIndex: 'code',
+      key: 'code',
+      width: 200,
+    },
+    {
+      title: '权限名称',
+      dataIndex: 'name',
+      key: 'name',
+      width: 200,
+    },
+    {
+      title: '类型',
+      dataIndex: 'type',
+      key: 'type',
+      width: 100,
+      render: (type: string) => (
+        <Tag color={type === 'module' ? 'blue' : type === 'operation' ? 'green' : 'orange'}>
+          {permissionTypeMap[type as keyof typeof permissionTypeMap]}
+        </Tag>
+      ),
+    },
+    {
+      title: '所属模块',
+      dataIndex: 'module',
+      key: 'module',
+      width: 120,
+    },
+    {
+      title: '操作类型',
+      dataIndex: 'action',
+      key: 'action',
+      width: 100,
+    },
+    {
+      title: '描述',
+      dataIndex: 'description',
+      key: 'description',
+      ellipsis: true,
+    },
+    {
+      title: '状态',
+      dataIndex: 'isActive',
+      key: 'isActive',
+      width: 80,
+      render: (active: number) => (
+        <Tag color={active === 1 ? 'green' : 'red'}>
+          {active === 1 ? '启用' : '禁用'}
+        </Tag>
+      ),
+    },
+    {
+      title: '排序',
+      dataIndex: 'sortOrder',
+      key: 'sortOrder',
+      width: 80,
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'createdAt',
+      key: 'createdAt',
+      width: 180,
+      render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
+    },
+    {
+      title: '操作',
+      key: 'action',
+      width: 120,
+      fixed: 'right' as const,
+      render: (_, record) => (
+        <Space>
+          <Button
+            type="link"
+            icon={<EditOutlined />}
+            onClick={() => handleEdit(record)}
+          >
+            编辑
+          </Button>
+          <Button
+            type="link"
+            danger
+            icon={<DeleteOutlined />}
+            onClick={() => handleDelete(record.id)}
+          >
+            删除
+          </Button>
+        </Space>
+      ),
+    },
+  ];
+
+  // 角色表格列定义
+  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 (
+    <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>
+
+      {/* 权限编辑模态框 */}
+      <Modal
+        title={editingPermission ? '编辑权限' : '新建权限'}
+        open={modalVisible}
+        onCancel={() => setModalVisible(false)}
+        footer={null}
+        width={600}
+      >
+        <Form form={form} layout="vertical" onFinish={handleSave}>
+          <Form.Item
+            label="权限编码"
+            name="code"
+            rules={[{ required: true, message: '请输入权限编码' }]}
+          >
+            <Input placeholder="如: client:create" />
+          </Form.Item>
+          
+          <Form.Item
+            label="权限名称"
+            name="name"
+            rules={[{ required: true, message: '请输入权限名称' }]}
+          >
+            <Input placeholder="请输入权限名称" />
+          </Form.Item>
+          
+          <Form.Item
+            label="权限类型"
+            name="type"
+            rules={[{ required: true, message: '请选择权限类型' }]}
+          >
+            <Select placeholder="请选择权限类型">
+              <Option value="module">模块权限</Option>
+              <Option value="operation">操作权限</Option>
+              <Option value="data">数据权限</Option>
+            </Select>
+          </Form.Item>
+          
+          <Form.Item
+            label="所属模块"
+            name="module"
+            rules={[{ required: true, message: '请输入所属模块' }]}
+          >
+            <Input placeholder="如: client" />
+          </Form.Item>
+          
+          <Form.Item
+            label="操作类型"
+            name="action"
+            rules={[{ required: true, message: '请输入操作类型' }]}
+          >
+            <Input placeholder="如: create" />
+          </Form.Item>
+          
+          <Form.Item
+            label="描述"
+            name="description"
+          >
+            <TextArea rows={3} placeholder="请输入权限描述" />
+          </Form.Item>
+          
+          <Form.Item
+            label="排序"
+            name="sortOrder"
+            initialValue={0}
+          >
+            <Input type="number" placeholder="请输入排序" />
+          </Form.Item>
+          
+          <Form.Item>
+            <Space>
+              <Button type="primary" htmlType="submit">
+                保存
+              </Button>
+              <Button onClick={() => setModalVisible(false)}>
+                取消
+              </Button>
+            </Space>
+          </Form.Item>
+        </Form>
+      </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>
+  );
+};
+
+export default Permissions;

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

@@ -20,6 +20,7 @@ import LogsPage from './pages/Logs';
 import OrderRecordsPage from './pages/OrderRecords';
 import FollowUpRecordsPage from './pages/FollowUpRecords';
 import DepartmentsPage from './pages/Departments';
+import PermissionsPage from './pages/Permissions';
 import { LoginPage } from './pages/Login';
 
 export const router = createBrowserRouter([
@@ -123,6 +124,11 @@ export const router = createBrowserRouter([
         element: <DepartmentsPage />,
         errorElement: <ErrorPage />
       },
+      {
+        path: 'permissions',
+        element: <PermissionsPage />,
+        errorElement: <ErrorPage />
+      },
       {
         path: '*',
         element: <NotFoundPage />,

+ 11 - 3
src/client/api.ts

@@ -3,7 +3,8 @@ import { hc } from 'hono/client'
 import type {
   AuthRoutes, UserRoutes, RoleRoutes,
   AreaRoutes, ClientRoutes, ExpenseRoutes, FileRoutes, HetongRoutes, HetongRenewRoutes, LinkmanRoutes, LogfileRoutes,
-  OrderRecordRoutes, FollowUpRecordRoutes, DepartmentRoutes, UserDepartmentRoutes
+  OrderRecordRoutes, FollowUpRecordRoutes, DepartmentRoutes, UserDepartmentRoutes,
+  PermissionRoutes, RolePermissionRoutes
 } from '@/server/api';
 
 // 创建 axios 适配器
@@ -42,7 +43,6 @@ const axiosFetch = async (url: RequestInfo | URL, init?: RequestInit) => {
     }
   }
     
-  
   // 处理204 No Content响应,不设置body
   const body = response.status === 204
     ? null
@@ -60,7 +60,6 @@ const axiosFetch = async (url: RequestInfo | URL, init?: RequestInit) => {
   )
 }  
 
-
 export const authClient = hc<AuthRoutes>('/', {
   fetch: axiosFetch,
 }).api.v1.auth;
@@ -120,3 +119,12 @@ export const departmentsClient = hc<DepartmentRoutes>('/', {
 export const userDepartmentsClient = hc<UserDepartmentRoutes>('/', {
   fetch: axiosFetch,
 }).api.v1['user-departments'];
+
+// 权限管理相关客户端
+export const permissionClient = hc<PermissionRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1.permissions;
+
+export const rolePermissionClient = hc<RolePermissionRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1['role-permissions'];

+ 6 - 0
src/server/api.ts

@@ -16,6 +16,8 @@ import orderRecordRoutes from './api/order-records/index'
 import followUpRecordRoutes from './api/follow-up-records/index'
 import departmentsRoute from './api/departments/index'
 import userDepartmentsRoute from './api/user-departments/index'
+import permissionRoutes from './api/permissions/index'
+import rolePermissionRoutes from './api/role-permissions/index'
 import { AuthContext } from './types/context'
 import { AppDataSource } from './data-source'
 
@@ -79,6 +81,8 @@ const orderRecordApiRoutes = api.route('/api/v1/order-records', orderRecordRoute
 const followUpRecordApiRoutes = api.route('/api/v1/follow-up-records', followUpRecordRoutes)
 const departmentsApiRoutes = api.route('/api/v1/departments', departmentsRoute)
 const userDepartmentsApiRoutes = api.route('/api/v1/user-departments', userDepartmentsRoute)
+const permissionsApiRoutes = api.route('/api/v1/permissions', permissionRoutes)
+const rolePermissionsApiRoutes = api.route('/api/v1/role-permissions', rolePermissionRoutes)
 
 export type AuthRoutes = typeof authRoutes
 export type UserRoutes = typeof userRoutes
@@ -96,5 +100,7 @@ export type OrderRecordRoutes = typeof orderRecordApiRoutes
 export type FollowUpRecordRoutes = typeof followUpRecordApiRoutes
 export type DepartmentRoutes = typeof departmentsApiRoutes
 export type UserDepartmentRoutes = typeof userDepartmentsApiRoutes
+export type PermissionRoutes = typeof permissionsApiRoutes
+export type RolePermissionRoutes = typeof rolePermissionsApiRoutes
 
 export default api

+ 15 - 0
src/server/api/permissions/index.ts

@@ -0,0 +1,15 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { Permission, PermissionSchema, CreatePermissionDto, UpdatePermissionDto } from '@/server/modules/permissions/permission.entity';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const permissionRoutes = createCrudRoutes({
+  entity: Permission,
+  createSchema: CreatePermissionDto,
+  updateSchema: UpdatePermissionDto,
+  getSchema: PermissionSchema,
+  listSchema: PermissionSchema,
+  searchFields: ['name', 'code', 'description'],
+  middleware: [authMiddleware]
+});
+
+export default permissionRoutes;

+ 145 - 0
src/server/api/role-permissions/index.ts

@@ -0,0 +1,145 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { RolePermission, RolePermissionSchema, CreateRolePermissionDto, UpdateRolePermissionDto } from '@/server/modules/permissions/permission.entity';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+import { DataScopeType } from '@/server/modules/departments/department.entity';
+
+const rolePermissionRoutes = createCrudRoutes({
+  entity: RolePermission,
+  createSchema: CreateRolePermissionDto,
+  updateSchema: UpdateRolePermissionDto,
+  getSchema: RolePermissionSchema,
+  listSchema: RolePermissionSchema,
+  searchFields: ['roleId', 'permissionId'],
+  middleware: [authMiddleware]
+});
+
+// 使用OpenAPIHono聚合路由,可以添加自定义路由
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { AppDataSource } from '@/server/data-source';
+import { AuthContext } from '@/server/types/context';
+
+
+
+// 添加自定义批量更新路由
+const batchUpdateRoute = createRoute({
+  method: 'post',
+  path: '/batch',
+  middleware: [authMiddleware],
+  request: {
+    body: {
+      content: {
+        'application/json': {
+          schema: z.object({
+            roleId: z.number().int().positive(),
+            permissions: z.array(z.object({
+              permissionId: z.number().int().positive(),
+              dataScopeType: z.enum([DataScopeType.PERSONAL, DataScopeType.DEPARTMENT, DataScopeType.SUB_DEPARTMENT, DataScopeType.COMPANY, DataScopeType.CUSTOM]),
+              customDepartments: z.array(z.number()).optional()
+            }))
+          })
+        }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '更新成功',
+      content: {
+        'application/json': {
+          schema: z.object({
+            message: z.string()
+          })
+        }
+      }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 添加自定义查询路由 - 根据角色ID获取权限
+const listByRoleRoute = createRoute({
+  method: 'get',
+  path: '/by-role',
+  middleware: [authMiddleware],
+  request: {
+    query: z.object({
+      roleId: z.coerce.number().openapi({
+        description: '角色ID',
+        example: 1
+      })
+    })
+  },
+  responses: {
+    200: {
+      description: '成功获取角色权限列表',
+      content: {
+        'application/json': {
+          schema: z.object({
+            data: z.array(RolePermissionSchema)
+          })
+        }
+      }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 注册自定义路由
+const app = new OpenAPIHono<AuthContext>()
+  .route('/', rolePermissionRoutes)
+  .openapi(batchUpdateRoute, async (c) => {
+    try {
+      const { roleId, permissions } = c.req.valid('json');
+      const repository = AppDataSource.getRepository(RolePermission);
+      
+      // 先删除该角色的所有权限
+      await repository.delete({ roleId });
+      
+      // 批量创建新权限
+      const newPermissions = permissions.map(p => 
+        repository.create({
+          roleId,
+          permissionId: p.permissionId,
+          dataScopeType: p.dataScopeType,
+          customDepartments: p.customDepartments
+        })
+      );
+      
+      await repository.save(newPermissions);
+      
+      return c.json({ message: '权限更新成功' }, 200);
+    } catch (error) {
+      return c.json({
+        code: 500,
+        message: error instanceof Error ? error.message : '批量更新权限失败'
+      }, 500);
+    }
+  })
+  .openapi(listByRoleRoute, async (c) => {
+    try {
+      const { roleId } = c.req.valid('query');
+      const repository = AppDataSource.getRepository(RolePermission);
+      
+      const permissions = await repository.find({
+        where: { roleId },
+        relations: ['permission']
+      });
+      
+      return c.json({ data: permissions }, 200);
+    } catch (error) {
+      return c.json({
+        code: 500,
+        message: error instanceof Error ? error.message : '获取权限列表失败'
+      }, 500);
+    }
+  });
+
+export default app;