02月25, 2025

支持 Cascader 异步数据源多选回显的方案

在开发需求中需要实现部门的级联多选功能,但目前部门树接口无法支持一次性拉取全量树结构,只能按层级异步加载,并且这个需求中有对已创建数据进行编辑的功能,这就涉及到在编辑的时候需要正常回显到 Cascader 组件中。

方案

目前能想到的方案就是:在编辑时,先将所有已选中的部门数据请求下来作为 Cascader 组件的数据源,这样在编辑初始化时能保证组件能正确回显,在用户对部门进行重新选择时,再按需异步加载未加载完的部门数据。

具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// AsyncCascader.tsx
import React, { useEffect, useState } from "react";
import { Cascader } from "antd";
import { rootDeptCode } from "../config";
import api, { DepartmentItem } from "../api";
import { mergeDeptList, loadDeptTree } from "./utils";

function AsyncCascader(props) {
const [state, setState] = useState({
departmentList: [] as DepartmentItem[],
});

useEffect(() => {
// 组件初始化,先请求第一层级的部门数据
api.deptList({ code: rootDeptCode, level: 1 }).then((deptList) => {
// 编辑模式下再次请求已选中的部门数据
if (props.data) {
loadDeptTree(deptList, props.data).then((res) => {
setState((old) => ({ ...old, departmentList: res }));
});
} else {
setState((old) => ({ ...old, departmentList: deptList }));
}
});
}, [props.data]);

// 异步加载部门数据
const fetchDeptData = (selectedOptions: DepartmentItem[]) => {
const target = selectedOptions[selectedOptions.length - 1];

// 已加载过就不用重新加载了
if (target.children && target.children.length > 0) return;

target.loading = true;
api.deptList({ code: target.code }).then((deptList) => {
target.loading = false;
const newDeptList = mergeDeptList(
state.departmentList,
selectedOptions,
deptList
);
setState((old) => ({ ...old, departmentList: newDeptList }));
});
};

return (
<Cascader
options={state.departmentList}
fieldNames={{ label: "name", value: "code" }}
loadData={fetchDeptData}
placeholder="请选择部门"
changeOnSelect
multiple
maxTagCount={6}
/>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// utils.ts

/**
* 根据 path 将子部门数据添加到部门 tree 中对应的部门下
*/
export const mergeDeptList = (
tree: DepartmentItem[],
path: DepartmentItem[],
children: DepartmentItem[]
) => {
return tree.map((item) => {
if (item.code === path[0].code) {
return {
...item,
children:
path.length === 1
? children
: mergeDeptList(item.children || [], path.slice(1), children),
};
}
return item;
});
};

/**
* 向部门树中指定的部门插入其子部门数据
*/
export const insertToDeptTree = (
tree: DepartmentItem[],
code: string,
children: DepartmentItem[]
) => {
let inserted = false;
for (let item of tree) {
if (item.code === code) {
item.children = children;
inserted = true;
} else if (item.children && item.children.length > 0) {
inserted = insertToDeptTree(item.children, code, children);
}
if (inserted) {
break;
}
}
return inserted;
};

/**
* 请求已选中的所有部门数据
*/
export async function loadDeptTree(deptList, selectedValue) {
// 保存部门 code 对应的子部门数据
const deptMap: { [key: string]: DepartmentItem[] } = {};

// 选出需要加载的部门 code 并去重
let codeArray: string[] = [];
if (Array.isArray(selectedValue)) {
codeArray = [
...new Set(
valueArray
// 按部门层级升序排列,保证按层级从大到小加载部门数据
.sort((a, b) => a.length - b.length)
// 去除末级部门,末级部门不需要加载子部门
.map((item) => item.slice(0, item.length - 1))
.filter((item) => item.length > 0)
.reduce((prev, curr) => prev.concat(curr), [])
),
];
}

// 指加载指定部门 code 的子部门数据
if (codeArray.length > 0) {
// 注意:这里的并发量跟选中的数量有关,如果数量太大需要考虑分批请求
await Promise.all(
codeArray.map((code) =>
api.deptList({ code }).catch((e) => {
console.error(e);
return [];
})
)
).then((resultArray) => {
codeArray.forEach((code, index) => {
deptMap[code] = resultArray[index];
});
});
}

Object.keys(deptMap).forEach((key) => {
insertToDeptTree(deptList, key, deptMap[key]);
});

return deptList;
}

本文链接:https://www.chenliqiang.cn/post/antd-async-cascader-edit.html

-- EOF --