فهرست منبع

修复角色分配菜单父子不联动问题

pm 4 ماه پیش
والد
کامیت
dea59d3ec4
1فایلهای تغییر یافته به همراه108 افزوده شده و 17 حذف شده
  1. 108 17
      src/components/RoleAssignMenuForm.tsx

+ 108 - 17
src/components/RoleAssignMenuForm.tsx

@@ -65,6 +65,91 @@ const RoleAssignMenuForm = forwardRef<RoleAssignMenuFormRef, RoleAssignMenuFormP
     return parents;
   };
 
+  // 获取某个节点的所有子节点 key(递归)
+  const getChildrenKeys = (node: DataNode): React.Key[] => {
+    let keys: React.Key[] = [];
+    if (node.children && node.children.length > 0) {
+      node.children.forEach((child) => {
+        keys.push(child.key);
+        keys = keys.concat(getChildrenKeys(child));
+      });
+    }
+    return keys;
+  };
+
+  // 在树中查找节点
+  const findNode = (nodes: DataNode[], key: React.Key): DataNode | null => {
+    for (const node of nodes) {
+      if (node.key === key) {
+        return node;
+      }
+      if (node.children) {
+        const found = findNode(node.children, key);
+        if (found) return found;
+      }
+    }
+    return null;
+  };
+
+  // 根据接口返回的ID列表,计算正确的回显状态
+  // 规则:
+  // 1. 如果父节点的所有子节点都在返回列表中,父节点全选,所有子节点全选
+  // 2. 如果父节点只有部分子节点在返回列表中,父节点半选,只有那些子节点选中
+  // 3. 如果接口返回了父节点ID,但子节点没有全部返回,不能把所有子节点都渲染为选中
+  const calculateDisplayState = (returnedIds: number[], treeNodes: DataNode[]) => {
+    const returnedIdSet = new Set(returnedIds);
+    const checked: React.Key[] = [];
+    const halfChecked: React.Key[] = [];
+    const parentKeySet = getParentKeys(treeNodes);
+
+    // 遍历返回的ID列表
+    returnedIdSet.forEach((id) => {
+      const node = findNode(treeNodes, id);
+      if (!node) return;
+
+      // 如果是父节点
+      if (parentKeySet.has(id)) {
+        const childrenKeys = getChildrenKeys(node);
+        const allChildrenReturned = childrenKeys.every((childKey) => returnedIdSet.has(Number(childKey)));
+
+        if (allChildrenReturned) {
+          // 所有子节点都在返回列表中,父节点全选
+          checked.push(id);
+          // 所有子节点也加入 checked(如果还没加入的话)
+          childrenKeys.forEach((childKey) => {
+            if (!checked.includes(childKey)) {
+              checked.push(childKey);
+            }
+          });
+        } else {
+          // 只有部分子节点在返回列表中,父节点半选
+          halfChecked.push(id);
+        }
+      } else {
+        // 如果是子节点,且不在 checked 中(避免重复),加入 checked
+        if (!checked.includes(id)) {
+          checked.push(id);
+        }
+      }
+    });
+
+    // 处理半选父节点的子节点:只有那些在返回列表中的子节点才应该被选中
+    halfChecked.forEach((parentKey) => {
+      const parentNode = findNode(treeNodes, parentKey);
+      if (parentNode) {
+        const childrenKeys = getChildrenKeys(parentNode);
+        childrenKeys.forEach((childKey) => {
+          // 只有返回列表中包含的子节点才加入 checked
+          if (returnedIdSet.has(Number(childKey)) && !checked.includes(childKey)) {
+            checked.push(childKey);
+          }
+        });
+      }
+    });
+
+    return { checked, halfChecked };
+  };
+
   // 将菜单数据转换为 Tree 组件需要的格式
   // 注意:handleTree 返回的节点类型不是 MenuVO(会带 children),这里用更宽松的类型兼容
   const convertMenuToTreeData = (menus: any[]): DataNode[] => {
@@ -103,16 +188,15 @@ const RoleAssignMenuForm = forwardRef<RoleAssignMenuFormRef, RoleAssignMenuFormP
           const menuIds = await roleApi.getRoleMenuList(row.id!);
           const menuIdsData = (menuIds as any)?.data || menuIds;
           setFormData(prev => ({ ...prev, menuIds: menuIdsData || [] }));
-          // 回显:后端可能返回父节点ID(如 3127)。
-          // 若使用父子联动(checkStrictly=false),父节点会导致所有子节点被渲染为选中。
-          // 这里改用 checkStrictly=true(父子不联动),并将“父节点ID”放入 halfCheckedKeys,
-          // “叶子节点/实际选中的节点ID”放入 checkedKeys,避免回显误全选子节点。
-          const rawKeys = (menuIdsData || []).map((id: number) => id);
-          const parentKeySet = getParentKeys(treeNodes);
-          const checked = rawKeys.filter((k: number) => !parentKeySet.has(k));
-          const half = rawKeys.filter((k: number) => parentKeySet.has(k));
+          
+          // 回显:根据接口返回的ID列表,计算正确的选中状态
+          // 规则:
+          // 1. 如果父节点的所有子节点都在返回列表中,父节点全选,所有子节点全选
+          // 2. 如果父节点只有部分子节点在返回列表中,父节点半选,只有那些子节点选中
+          const returnedIds = (menuIdsData || []).map((id: number) => id);
+          const { checked, halfChecked } = calculateDisplayState(returnedIds, treeNodes);
           setCheckedKeys(checked);
-          setHalfCheckedKeys(half);
+          setHalfCheckedKeys(halfChecked);
         } catch (error: any) {
           console.error(t('role.getRoleMenuFailed'), error);
         } finally {
@@ -164,6 +248,7 @@ const RoleAssignMenuForm = forwardRef<RoleAssignMenuFormRef, RoleAssignMenuFormP
       menuIds: [],
     });
     setCheckedKeys([]);
+    setHalfCheckedKeys([]);
     setExpandedKeys([]);
     form.resetFields();
   };
@@ -192,24 +277,30 @@ const RoleAssignMenuForm = forwardRef<RoleAssignMenuFormRef, RoleAssignMenuFormP
   };
 
   // 树节点选中变化
-  // 注意:Ant Design Tree 的 onCheck:
-  // - checkStrictly=false(父子联动):checkedKeysValue 为数组,半选节点在 info.halfCheckedKeys
-  // - checkStrictly=true(父子不联动):checkedKeysValue 为 { checked, halfChecked }
+  // 注意:当 checkStrictly=false(父子联动)时,onCheck 接收的参数是数组,半选节点在 info.halfCheckedKeys
   const onCheck = (
     checkedKeysValue: React.Key[] | { checked: React.Key[]; halfChecked: React.Key[] },
     info: any
   ) => {
+    // 父子联动模式下(checkStrictly=false),checkedKeysValue 是数组
+    // 包含所有选中的节点(包括父节点和子节点)
+    // info.halfCheckedKeys 包含半选中的父节点(部分子节点被选中)
     let checked: React.Key[] = [];
     let half: React.Key[] = [];
+    
     if (Array.isArray(checkedKeysValue)) {
+      // 父子联动模式
       checked = checkedKeysValue;
       half = info?.halfCheckedKeys || [];
     } else {
+      // 父子不联动模式(虽然我们用的是联动模式,但类型定义需要兼容)
       checked = checkedKeysValue.checked || [];
       half = checkedKeysValue.halfChecked || [];
     }
+    
     setCheckedKeys(checked);
     setHalfCheckedKeys(half);
+    
     // 如果全部选中,设置全选状态
     const allKeys = getAllKeys(menuOptions);
     setTreeNodeAll(checked.length === allKeys.length && allKeys.length > 0);
@@ -276,15 +367,15 @@ const RoleAssignMenuForm = forwardRef<RoleAssignMenuFormRef, RoleAssignMenuFormP
             >
               <Tree
                 checkable
-                // checkStrictly=true 时,checkedKeys 需要传 { checked, halfChecked }
-                checkedKeys={{ checked: checkedKeys, halfChecked: halfCheckedKeys } as any}
+                checkedKeys={checkedKeys}
                 expandedKeys={expandedKeys}
-                onCheck={onCheck}
+                onCheck={onCheck as any}
                 onExpand={setExpandedKeys}
                 treeData={menuOptions}
                 defaultExpandAll={false}
-                // 父子不联动:避免“回显包含父节点ID”时把所有子节点渲染为选中
-                checkStrictly
+                // 父子联动:点击父节点可以批量操作所有子节点,提高效率
+                // 半选状态由 Tree 组件自动计算(根据子节点选中情况)
+                checkStrictly={false}
               />
             </Card>
           </Form.Item>