最近做业务的时候,有个树穿梭器的需求,这个业务所用的技术栈是vue2+element Ui,由于element UI的穿梭器仅支持checkbox形式,所以自己去开发这个组件了,在这个组件中用到的一些逻辑还是涉及到了很多知识点的,所以在此记录一下。
首先,这个组件的UI大概是这样的:
这里仅记录一下其中的核心逻辑。
从UI上看很显而易见,大致就是操作一个接口返回来的树数据,通过v-model传入的选中id集合 selectIds,将原始树数据拆分为左边的 leftTreeData 和右边的 rightTreeData 并渲染到对应区域中即可,两个拆分出来的树数据,相互不能包含对方的末级子数据,穿梭器嘛,实际上就是把左边的数据剪切到右边来,单纯的数据还好说,放在树结构当中,就比较复杂了。
这里我的做法是先把整个树数据扁平化,多维数据转化为一维数据,其中包含所有的父子级数据,具体方法是:
const treeData = [
{
title: "001",
id: 1000,
children: [
{
title: "001-01",
id: 1100,
children: [
{ title: "001-01-01", id: 1101 },
{ title: "001-01-01", id: 1102 },
{ title: "001-01-03", id: 1103 },
{ title: "001-01-04", id: 1104 },
],
},
{
title: "001-02",
id: 1200,
children: [
{ title: "001-02-01", id: 1201 },
{ title: "001-02-02", id: 1202 },
],
},
{
title: "001-03",
id: 1300,
children: [{ title: "001-03-01", id: 1301 }],
},
{
title: "001-04",
id: 1400,
children: [{ title: "001-04-01", id: 1401 }],
},
],
},
{
title: "002",
id: 2000,
children: [
{
title: "002-01",
id: 2100,
children: [
{ title: "002-01-01", id: 2101 },
{ title: "002-01-01", id: 2102 },
{ title: "002-01-03", id: 2103 },
{ title: "002-01-04", id: 2104 },
],
},
{
title: "002-02",
id: 2200,
children: [
{ title: "002-02-01", id: 2201 },
{ title: "002-02-02", id: 2202 },
{ title: "002-02-03", id: 2203 },
],
},
{
title: "002-03",
id: 2300,
children: [
{ title: "002-03-01", id: 2301 },
{ title: "002-03-02", id: 2302 },
],
},
{
title: "002-04",
id: 2400,
children: [{ title: "002-04-01", id: 2401 }],
},
],
},
{
title: "003",
id: 3000,
children: [
{
title: "003-01",
id: 3100,
children: [
{ title: "003-01-01", id: 3101 },
{ title: "003-01-01", id: 3102 },
{ title: "003-01-03", id: 3103 },
{ title: "003-01-04", id: 3104 },
{ title: "003-01-05", id: 3105 },
],
},
],
},
];
// 将树数据扁平化
// 这里利用reduce的特性对原始的树数据进行递归遍历,最终返回一个map结构数据,并主动为数据添加一个父级标识pid
// 这里也可以修改成返回一个array类型数据,之所以用map是为了方便后续的数据获取和去重
const childrenKey = 'children'
const _nodeKey = 'id'
const rootPid ='0'
const treeToFlat = (arr, pid, map) =>
arr.reduce((prev, cur) => {
const cur_children = cur[childrenKey];
const cur_id = cur[_nodeKey];
const newCur = JSON.parse(
JSON.stringify({ pid: pid || rootPid, ...cur })
);
Reflect.deleteProperty(newCur, childrenKey);
prev.set(cur_id, newCur);
if (Array.isArray(cur_children) && cur_children.length)
treeToFlat(cur_children, cur_id, prev);
return prev;
}, map || new Map());
const flatArrayMap = treeToFlat(treeData)
获取到扁平化后的一维数据 flatArrayMap 之后,再遍历选中的 id 集合,根据每一个 id 从 flatArrayMap 中获取这个 id 的对象数据,并获取这个 id 的父级数据 item1,父级的父级 item2···直至最顶级 item[n],再将这些数据组装成数组 [item1,item2…item[n]],最后将这些数据添加到一个新的map集合中,至于为什么是map,其实还是为了减少递归和去重,具体实现方法是:
// 通过keys集合,获取扁平化后的选中数组集合
const selectIdsToFlat = (keys, flatMap) => {
const KeyFlatArrMap = new Map();
if (Array.isArray(keys) && keys.length)
keys.forEach((key) => {
const keyToFlat = (key, map) => {
return [key].reduce((prev, cur) => {
const cur_obj = map.get(cur);
const { pid } = cur_obj || {};
const cur_arr =
pid && pid !== rootPid ? keyToFlat(pid, map) : [];
return prev.concat([cur_obj], cur_arr);
}, []);
};
const keyFlatArr = keyToFlat(key, flatMap)
.reverse()
.filter((k) => !!k);
keyFlatArr.forEach((item) => {
KeyFlatArrMap.set(item[_nodeKey], item);
});
});
return KeyFlatArrMap;
};
const selectIdsToArrayMap = selectIdsToFlat([1101,1201,1301,2101,3101],flatArrayMap);
const resultItems = [...selectIdsToArrayMap.values()]
最后将获取到的resultItems再组装成新的树,就可以得到选中的id集合汇成的新树数据了:
const _nodeKey = 'id'
const childrenKey = 'children'
//格式化扁平化数据,将扁平化数据转化为树数据
const formatFlagToTree = (baseArr, parentId) => {
const map = baseArr.reduce((prev, cur) => {
prev[cur[_nodeKey]] = cur;
return prev;
}, {});
let result = [];
for (let i = 0; i < baseArr.length; i++) {
const item = baseArr[i];
if (item.pid === parentId) {
result.push(item);
continue;
}
const parent = map[item.pid];
if (parent) {
parent[childrenKey] = parent[childrenKey] || [];
parent[childrenKey].push(item);
}
}
return result;
}
const newTree = formatFlagToTree(resultItems , "0");
以上便是树穿梭器的核心逻辑啦,既然能通过选中的id集合获取到对应的选中树rightTreeData渲染右边区域;
那也能通过选中的id,在所有id集合中过滤一下,就能得到所有未选中的id集合,再生成未选中的树leftTreeData渲染到左边区域;
至此穿梭器的逻辑起差不多完成了。