|
|
@@ -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>
|