|
|
@@ -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. 当前实现支持二级结构,可扩展到多级
|
|
|
+
|
|
|
+## 兼容性
|
|
|
+
|
|
|
+- 向后兼容原有权限配置功能
|
|
|
+- 支持渐进式升级
|
|
|
+- 可配置是否启用懒加载模式
|