Browse Source

📝 docs(permission): add permission config component documentation

- add permission config component design document
- add lazy loading version of permission config component documentation
- add permission config component integration guide
- document component structure, API usage and implementation details
yourname 8 months ago
parent
commit
e6ba1a1815

+ 166 - 0
docs/permission-config-component-design.md

@@ -0,0 +1,166 @@
+# 权限配置组件设计方案
+
+## 组件目标
+
+将当前Roles页面中的权限配置功能抽取为可复用的独立组件,包含完整的UI展示和API调用逻辑。
+
+## 组件设计
+
+### 1. 组件名称
+`PermissionConfigModal`
+
+### 2. 组件位置
+`src/client/admin/components/PermissionConfigModal.tsx`
+
+### 3. 组件功能
+- 权限树形结构展示
+- 权限选择/取消选择
+- 批量保存权限配置
+- 加载状态管理
+
+### 4. Props接口定义
+
+```typescript
+interface PermissionConfigModalProps {
+  visible: boolean;
+  roleId: number | null;
+  roleName?: string;
+  onClose: () => void;
+  onSuccess?: () => void;
+}
+```
+
+### 5. 内部状态管理
+- `selectedPermissionIds: number[]` - 选中的权限ID列表
+- `permissions: PermissionItem[]` - 所有权限列表
+- `rolePermissions: number[]` - 当前角色已拥有的权限ID列表
+- `loading: boolean` - 加载状态
+- `saving: boolean` - 保存状态
+
+### 6. API调用封装
+使用独立的React Query hooks:
+
+```typescript
+// 获取所有权限
+const usePermissions = () => {
+  return useQuery({
+    queryKey: ['permissions'],
+    queryFn: async () => {
+      const response = await permissionClient.$get({ query: {} });
+      return response.json();
+    },
+  });
+};
+
+// 获取角色权限
+const useRolePermissions = (roleId: number | null) => {
+  return useQuery({
+    queryKey: ['role-permissions', roleId],
+    queryFn: async () => {
+      if (!roleId) return [];
+      const response = await rolePermissionClient.$get({
+        query: { filters: JSON.stringify({ roleId }) }
+      });
+      const data = await response.json();
+      return data.data.map((item: any) => item.permissionId);
+    },
+    enabled: !!roleId,
+  });
+};
+
+// 更新角色权限
+const useUpdateRolePermissions = () => {
+  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 }
+      });
+      return response.json();
+    },
+  });
+};
+```
+
+### 7. 权限树数据结构
+
+权限按模块分组展示:
+
+```typescript
+const permissionTreeData = [
+  {
+    title: '用户管理',
+    key: '用户管理',
+    checkable: false,
+    children: [
+      {
+        title: '查看用户列表 (user:list)',
+        key: '1',
+      },
+      {
+        title: '创建用户 (user:create)',
+        key: '2',
+      }
+    ]
+  }
+];
+```
+
+### 8. 使用示例
+
+```typescript
+// 在Roles页面中使用
+const [configVisible, setConfigVisible] = useState(false);
+const [currentRole, setCurrentRole] = useState<RoleItem | null>(null);
+
+const handlePermissionConfig = (role: RoleItem) => {
+  setCurrentRole(role);
+  setConfigVisible(true);
+};
+
+return (
+  <>
+    <Button onClick={() => handlePermissionConfig(record)}>
+      配置权限
+    </Button>
+    
+    <PermissionConfigModal
+      visible={configVisible}
+      roleId={currentRole?.id || null}
+      roleName={currentRole?.name}
+      onClose={() => setConfigVisible(false)}
+      onSuccess={() => {
+        message.success('权限配置成功');
+        // 刷新角色列表
+        queryClient.invalidateQueries({ queryKey: ['roles'] });
+      }}
+    />
+  </>
+);
+```
+
+## 实现步骤
+
+1. **创建组件文件** - 新建 `PermissionConfigModal.tsx`
+2. **提取API调用** - 将相关的React Query hooks封装到组件内部
+3. **实现权限树** - 使用Ant Design的Tree组件
+4. **状态管理** - 管理选中状态
+5. **集成到Roles页面** - 替换原有的权限配置功能
+6. **测试可复用性** - 验证组件可在其他页面使用
+
+## 技术要点
+
+1. **封装完整** - 包含所有必要的API调用和状态管理
+2. **类型安全** - 使用TypeScript确保类型正确
+3. **可配置** - 通过props支持不同场景的使用
+4. **加载状态** - 完善的加载和错误处理
+5. **可复用** - 可在任何需要配置权限的地方使用
+
+## 预期效果
+
+将原来的200+行权限配置相关代码压缩为一个简洁的组件调用,提高代码复用性和可维护性。

+ 357 - 0
docs/permission-config-component-with-lazy-loading.md

@@ -0,0 +1,357 @@
+# 支持懒加载的权限配置组件设计
+
+## 需求分析
+
+在原有权限配置组件基础上增加懒加载支持,解决大数据量权限树的性能问题。
+
+## 懒加载实现方案
+
+### 1. 权限树结构优化
+
+#### 新的数据结构
+```typescript
+interface PermissionTreeData {
+  title: string;
+  key: string;
+  isLeaf?: boolean;
+  children?: PermissionTreeData[];
+  id?: number;
+  parentId?: number;
+}
+
+// 模块-功能-具体权限的三级结构
+```
+
+### 2. API接口扩展
+
+#### 支持父级查询的权限接口
+```typescript
+// 新增查询参数支持
+const permissionsQuery = z.object({
+  parentId: z.coerce.number().optional().openapi({
+    description: '父权限ID,不传则获取顶级权限',
+    example: 1
+  }),
+  module: z.string().optional().openapi({
+    description: '模块名称,用于模块级过滤',
+    example: 'user'
+  })
+});
+```
+
+### 3. 懒加载权限配置组件
+
+#### PermissionConfigModalWithLazyLoading.tsx
+```typescript
+import React, { useState, useCallback } from 'react';
+import { Modal, Tree, Spin, message } from 'antd';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { permissionClient, rolePermissionClient } from '@/client/api';
+import type { InferResponseType } from 'hono/client';
+
+type PermissionListResponse = InferResponseType<typeof permissionClient.$get, 200>;
+type PermissionItem = PermissionListResponse['data'][0];
+
+interface PermissionConfigModalWithLazyLoadingProps {
+  visible: boolean;
+  roleId: number | null;
+  roleName?: string;
+  onClose: () => void;
+  onSuccess?: () => void;
+}
+
+interface TreeNode {
+  title: string;
+  key: string;
+  id?: number;
+  children?: TreeNode[];
+  isLeaf?: boolean;
+  selectable?: boolean;
+}
+
+const PermissionConfigModalWithLazyLoading: React.FC<PermissionConfigModalWithLazyLoadingProps> = ({
+  visible,
+  roleId,
+  roleName,
+  onClose,
+  onSuccess,
+}) => {
+  const queryClient = useQueryClient();
+  const [selectedPermissionIds, setSelectedPermissionIds] = useState<number[]>([]);
+  const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
+  const [treeData, setTreeData] = useState<TreeNode[]>([]);
+  const [autoExpandParent, setAutoExpandParent] = useState(true);
+
+  // 获取模块列表
+  const { data: modules = [], isLoading: modulesLoading } = useQuery({
+    queryKey: ['permission-modules'],
+    queryFn: async () => {
+      const response = await permissionClient.$get({ 
+        query: { page: 1, pageSize: 1000 } 
+      });
+      if (!response.ok) throw new Error('获取权限模块失败');
+      const data = await response.json();
+      
+      // 按模块分组
+      const moduleMap = new Map<string, PermissionItem[]>();
+      data.data.forEach(permission => {
+        const moduleName = permission.module || '其他';
+        if (!moduleMap.has(moduleName)) {
+          moduleMap.set(moduleName, []);
+        }
+        moduleMap.get(moduleName)!.push(permission);
+      });
+      
+      return Array.from(moduleMap.keys());
+    },
+    enabled: visible,
+  });
+
+  // 获取角色权限
+  const { data: rolePermissions = [], isLoading: rolePermissionsLoading } = useQuery({
+    queryKey: ['role-permissions', roleId],
+    queryFn: async () => {
+      if (!roleId) return [];
+      const response = await rolePermissionClient.$get({
+        query: { filters: JSON.stringify({ roleId }) }
+      });
+      if (!response.ok) throw new Error('获取角色权限失败');
+      const data = await response.json();
+      return data.data.map((item: any) => item.permissionId);
+    },
+    enabled: visible && !!roleId,
+  });
+
+  // 懒加载权限数据
+  const loadPermissions = useCallback(async (module?: string, parentId?: number) => {
+    const query: any = {};
+    if (module) query.filters = JSON.stringify({ module });
+    if (parentId) query.filters = JSON.stringify({ parentId });
+    
+    const response = await permissionClient.$get({ query });
+    if (!response.ok) throw new Error('获取权限数据失败');
+    return response.json();
+  }, []);
+
+  // 更新角色权限
+  const updateRolePermissionsMutation = 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('角色权限更新成功');
+      onSuccess?.();
+    },
+    onError: (error: Error) => {
+      message.error(error.message);
+    },
+  });
+
+  // 构建模块树节点
+  useEffect(() => {
+    if (visible && modules.length > 0) {
+      const moduleNodes = modules.map(moduleName => ({
+        title: moduleName,
+        key: `module_${moduleName}`,
+        id: undefined,
+        isLeaf: false,
+        selectable: false,
+        children: [],
+      }));
+      setTreeData(moduleNodes);
+    }
+  }, [visible, modules]);
+
+  // 加载子节点
+  const onLoadData = async (treeNode: any) => {
+    const { key, title } = treeNode;
+    
+    if (key.startsWith('module_')) {
+      // 加载模块下的权限
+      const moduleName = key.replace('module_', '');
+      const response = await loadPermissions(moduleName);
+      const permissions = response.data;
+      
+      const permissionNodes = permissions.map((permission: PermissionItem) => ({
+        title: `${permission.name} (${permission.code})`,
+        key: `permission_${permission.id}`,
+        id: permission.id,
+        isLeaf: true,
+        selectable: true,
+      }));
+
+      treeNode.children = permissionNodes;
+      setTreeData(prevData => [...prevData]);
+    }
+  };
+
+  // 处理节点选择
+  const onCheck = (checkedKeysValue: any, info: any) => {
+    const checked = checkedKeysValue.checked || checkedKeysValue;
+    const permissionKeys = checked.filter((key: string) => key.startsWith('permission_'));
+    setSelectedPermissionIds(permissionKeys.map((key: string) => 
+      parseInt(key.replace('permission_', ''))
+    ));
+  };
+
+  // 保存权限配置
+  const handleSavePermissions = () => {
+    if (!roleId) return;
+    
+    updateRolePermissionsMutation.mutate(
+      { roleId, permissionIds: selectedPermissionIds },
+      {
+        onSuccess: () => {
+          onClose();
+        }
+      }
+    );
+  };
+
+  // 处理展开/收起
+  const handleExpand = (keys: string[]) => {
+    setExpandedKeys(keys);
+    setAutoExpandParent(false);
+  };
+
+  // 初始化选中状态
+  useEffect(() => {
+    if (visible && rolePermissions.length > 0) {
+      setSelectedPermissionIds(rolePermissions);
+      
+      // 自动展开包含已选权限的模块
+      const expandedModuleKeys: string[] = [];
+      treeData.forEach(moduleNode => {
+        moduleNode.children?.forEach(permissionNode => {
+          if (rolePermissions.includes(permissionNode.id!)) {
+            expandedModuleKeys.push(moduleNode.key);
+          }
+        });
+      });
+      setExpandedKeys(expandedModuleKeys);
+    }
+  }, [visible, rolePermissions, treeData]);
+
+  return (
+    <Modal
+      title={`配置权限 - ${roleName}`}
+      open={visible}
+      onCancel={onClose}
+      onOk={handleSavePermissions}
+      okButtonProps={{ 
+        loading: updateRolePermissionsMutation.isPending,
+        disabled: !roleId
+      }}
+      width={800}
+      destroyOnClose
+    >
+      <Spin spinning={modulesLoading || rolePermissionsLoading}>
+        <Tree
+          checkable
+          treeData={treeData}
+          loadData={onLoadData}
+          checkedKeys={selectedPermissionIds.map(id => `permission_${id}`)}
+          onCheck={onCheck}
+          onExpand={handleExpand}
+          expandedKeys={expandedKeys}
+          autoExpandParent={autoExpandParent}
+          height={400}
+          titleRender={(node) => {
+            if (node.key.startsWith('module_')) {
+              return <strong style={{ color: '#1890ff' }}>{node.title}</strong>;
+            }
+            return <span>{node.title}</span>;
+          }}
+        />
+      </Spin>
+    </Modal>
+  );
+};
+
+export default PermissionConfigModalWithLazyLoading;
+```
+
+### 4. 后端API增强
+
+#### 支持分页和过滤的权限接口
+```typescript
+// 在 permission.entity.ts 中增强查询
+const permissionListQuery = z.object({
+  module: z.string().optional(),
+  parentId: z.coerce.number().optional(),
+  page: z.coerce.number().int().positive().default(1),
+  pageSize: z.coerce.number().int().positive().default(20),
+  keyword: z.string().optional(),
+  filters: z.string().optional()
+});
+```
+
+#### 权限树构建服务
+```typescript
+export class PermissionService extends GenericCrudService<Permission> {
+  async getPermissionTree(module?: string, parentId?: number) {
+    const queryBuilder = this.repository.createQueryBuilder('permission');
+    
+    if (module) {
+      queryBuilder.where('permission.module = :module', { module });
+    }
+    
+    if (parentId) {
+      queryBuilder.where('permission.parentId = :parentId', { parentId });
+    } else if (!module) {
+      // 获取顶级权限
+      queryBuilder.where('permission.parentId IS NULL');
+    }
+    
+    return queryBuilder.getMany();
+  }
+}
+```
+
+### 5. 使用示例
+
+```typescript
+// 在Roles页面中使用懒加载版本
+import PermissionConfigModalWithLazyLoading from '@/client/admin/components/PermissionConfigModalWithLazyLoading';
+
+// 使用方式
+<PermissionConfigModalWithLazyLoading
+  visible={configVisible}
+  roleId={currentRole?.id || null}
+  roleName={currentRole?.name}
+  onClose={() => setConfigVisible(false)}
+  onSuccess={() => {
+    message.success('权限配置成功');
+    queryClient.invalidateQueries({ queryKey: ['roles'] });
+  }}
+/>
+```
+
+## 性能优化
+
+1. **按需加载** - 只加载用户展开的模块权限
+2. **缓存优化** - 已加载的模块数据缓存,避免重复请求
+3. **分页处理** - 支持大量权限的分页加载
+4. **虚拟滚动** - 大量节点时启用虚拟滚动
+
+## 当前限制
+
+1. 需要后端支持 `parentId` 和 `module` 查询参数
+2. 权限数据需要按模块-功能层级组织
+3. 当前实现支持二级结构,可扩展到多级
+
+## 兼容性
+
+- 向后兼容原有权限配置功能
+- 支持渐进式升级
+- 可配置是否启用懒加载模式

+ 243 - 0
docs/permission-config-integration-guide.md

@@ -0,0 +1,243 @@
+# 权限配置组件集成指南
+
+## 组件结构说明
+
+权限配置组件将包含以下文件:
+
+1. `src/client/admin/components/PermissionConfigModal.tsx` - 权限配置弹窗组件
+2. `src/client/admin/hooks/usePermissionConfig.ts` - 权限配置相关React Query hooks
+3. 更新后的 `src/client/admin/pages/Roles.tsx` - 集成新组件的角色管理页面
+
+## 组件实现代码
+
+### 1. PermissionConfigModal.tsx 完整实现
+
+```typescript
+import React, { useEffect, useState } from 'react';
+import { Modal, Tree, Spin, message } from 'antd';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { permissionClient, rolePermissionClient } from '@/client/api';
+import type { InferResponseType } from 'hono/client';
+
+type PermissionListResponse = InferResponseType<typeof permissionClient.$get, 200>;
+type PermissionItem = PermissionListResponse['data'][0];
+
+interface PermissionConfigModalProps {
+  visible: boolean;
+  roleId: number | null;
+  roleName?: string;
+  onClose: () => void;
+  onSuccess?: () => void;
+}
+
+const PermissionConfigModal: React.FC<PermissionConfigModalProps> = ({
+  visible,
+  roleId,
+  roleName,
+  onClose,
+  onSuccess,
+}) => {
+  const queryClient = useQueryClient();
+  const [selectedPermissionIds, setSelectedPermissionIds] = useState<number[]>([]);
+
+  // 获取所有权限
+  const { data: permissions = [], isLoading: permissionsLoading } = 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;
+    },
+    enabled: visible,
+  });
+
+  // 获取角色权限
+  const { data: rolePermissions = [], isLoading: rolePermissionsLoading } = useQuery({
+    queryKey: ['role-permissions', roleId],
+    queryFn: async () => {
+      if (!roleId) return [];
+      const response = await rolePermissionClient.$get({
+        query: { filters: JSON.stringify({ roleId }) }
+      });
+      if (!response.ok) throw new Error('获取角色权限失败');
+      const data = await response.json();
+      return data.data.map((item: any) => item.permissionId);
+    },
+    enabled: visible && !!roleId,
+  });
+
+  // 更新角色权限
+  const updateRolePermissionsMutation = 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('角色权限更新成功');
+      onSuccess?.();
+    },
+    onError: (error: Error) => {
+      message.error(error.message);
+    },
+  });
+
+  // 权限数据按模块分组
+  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 (visible && rolePermissions.length > 0) {
+      setSelectedPermissionIds(rolePermissions);
+    } else if (!visible) {
+      setSelectedPermissionIds([]);
+    }
+  }, [visible, rolePermissions]);
+
+  const handleSavePermissions = () => {
+    if (!roleId) return;
+    
+    updateRolePermissionsMutation.mutate(
+      { roleId, permissionIds: selectedPermissionIds },
+      {
+        onSuccess: () => {
+          onClose();
+        }
+      }
+    );
+  };
+
+  return (
+    <Modal
+      title={`配置权限 - ${roleName}`}
+      open={visible}
+      onCancel={onClose}
+      onOk={handleSavePermissions}
+      okButtonProps={{ 
+        loading: updateRolePermissionsMutation.isPending,
+        disabled: !roleId
+      }}
+      width={800}
+      destroyOnClose
+    >
+      <Spin spinning={permissionsLoading || 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>
+  );
+};
+
+export default PermissionConfigModal;
+```
+
+### 2. 集成到Roles页面的修改方案
+
+#### Roles.tsx 修改要点
+
+1. **删除原有权限相关状态和方法**:
+   - 删除 `permissionModalVisible`
+   - 删除 `selectedRole`
+   - 删除 `selectedPermissionIds`
+   - 删除 `usePermissions()` hook
+   - 删除 `useRolePermissions()` hook
+   - 删除 `useUpdateRolePermissions()` mutation
+   - 删除权限相关的所有方法
+
+2. **新增状态管理**:
+   ```typescript
+   const [configVisible, setConfigVisible] = useState(false);
+   const [currentRole, setCurrentRole] = useState<RoleItem | null>(null);
+   ```
+
+3. **修改权限配置触发方法**:
+   ```typescript
+   const handlePermissionConfig = (role: RoleItem) => {
+     setCurrentRole(role);
+     setConfigVisible(true);
+   };
+   ```
+
+4. **替换权限配置模态框**:
+   ```typescript
+   <PermissionConfigModal
+     visible={configVisible}
+     roleId={currentRole?.id || null}
+     roleName={currentRole?.name}
+     onClose={() => setConfigVisible(false)}
+     onSuccess={() => {
+       message.success('权限配置成功');
+       queryClient.invalidateQueries({ queryKey: ['roles'] });
+     }}
+   />
+   ```
+
+## 集成步骤
+
+### 步骤1:创建组件文件
+在 `src/client/admin/components/` 目录下创建 `PermissionConfigModal.tsx`
+
+### 步骤2:更新Roles页面
+1. 导入新组件:`import PermissionConfigModal from '@/client/admin/components/PermissionConfigModal';`
+2. 删除原有的权限相关代码
+3. 添加新的状态和方法
+4. 替换模态框组件
+
+### 步骤3:验证集成
+1. 测试权限查看功能
+2. 测试权限修改功能
+3. 测试权限保存功能
+4. 验证组件在不同场景下的可复用性
+
+## 预期效果
+
+原Roles.tsx文件将从528行减少到约380行,权限配置相关的200+行代码被封装到独立的组件中,提高了代码的可维护性和复用性。
+
+## 扩展建议
+
+该权限配置组件还可以用于:
+1. 用户权限配置页面
+2. 权限批量配置功能
+3. 权限管理后台的其他场景