树形数据转二维数据的算法知识
by flygo on 2021/01/10
前言
要实现如下表格:
其中一级菜单二级菜单是动态生成的,可能有N级,中间页同样,操作是固定的只有一列。
代码
所谓Talk is cheap, show me the code!,直接来吧~
/*
* @Author: laifeipeng
* @Date: 2020-12-28 14:01:12
* @Last Modified by: laifeipeng
* @Last Modified time: 2021-01-10 20:48:26
*/
/**
* 树形数据转成二维数组数据,才能满足table的数据格式,最终生成如下格式:
* [
{
'L1-type': 1,
'L1-pid': undefined,
'L1-id': '1',
'L1-name': '权限管理',
'L1-enable': true,
'L1-checked': false,
'L1-serialNum': 1,
'L2-type': 1,
'L2-pid': undefined,
'L2-id': '5',
'L2-name': '角色配置',
'L2-enable': true,
'L2-checked': false,
'L2-serialNum': 2,
'M1-type': 4,
'M1-pid': undefined,
'M1-id': '101',
'M1-name': '编辑页签',
'M1-enable': true,
'M1-checked': false,
'M1-serialNum': 1,
'M2-type': 3,
'M2-pid': undefined,
'M2-id': '101001',
'M2-name': '编辑中间页',
'M2-enable': true,
'M2-checked': false,
'M2-serialNum': 1,
functionVOs: [ [Object], [Object], [Object] ],
id: 'ed4f94-a7b-b56-bff-8b3bf9e93'
},
* ]
*/
/** *******************************************************************
************************** 一点说明 **********************************
1、非叶子菜单没有中间页,也没有按钮,只可以有子级菜单。
2、如果子级为菜单,则自己为非叶子菜单,则不会有中间页,也不会有操作按钮,如果出现这些属性则忽略。
3、叶子菜单可以有中间页、页签和自己的操作按钮。
4、中间页包括中间页和页签,他们可以有自己的操作按钮。
buttons代表各自的functionVOs,也可以为空,但是为空也显示这一行!
|一级菜单|二级菜单|中间页|中间页|操作按钮|
|首页 | | | | | // 为空也显示
|首页 | |查看 | | buttons|
|配置 |用户配置| | | buttons|
|配置 |用户配置| 编辑 | | buttons|
|配置 |系统配置| | | buttons|
|配置 |系统配置| 应用 | | | // 为空也显示
|配置 |系统配置| 应用 |功能管理| buttons|
*/
const data = [
{
checked: false,
code: 'app_manager',
createTime: '2020-12-04 09:11:39',
createUser: 'admin',
enable: true,
icon: null,
id: '1',
name: '权限管理',
pageCache: null,
parentId: null,
serialNum: 1,
type: 1,
updateTime: '2020-12-23 10:49:04',
updateUser: 'c3843977198142589f139ef3f5133333',
url: '/app',
busSys: 'bsp',
children: [
{
checked: false,
code: '',
createTime: '2020-12-04 09:11:39',
createUser: 'admin',
enable: true,
icon: '',
id: '4',
name: '操作管理',
pageCache: null,
parentId: '1',
serialNum: 1,
type: 1,
updateTime: '2020-12-04 09:11:45',
updateUser: 'admin',
url: '/tenant',
busSys: 'bsp',
children: null,
functionVOs: null,
},
{
checked: false,
code: 'tenant_manager',
createTime: '2020-12-04 09:11:39',
createUser: 'admin',
enable: true,
icon: '',
id: '2',
name: '菜单管理',
pageCache: null,
parentId: '1',
serialNum: 2,
type: 1,
updateTime: '2020-12-04 09:11:45',
updateUser: 'admin',
url: '/tenant',
busSys: 'bsp',
children: null,
functionVOs: null,
},
{
checked: false,
code: '',
createTime: '2020-12-04 09:11:39',
createUser: 'admin',
enable: true,
icon: '',
id: '5',
name: '角色配置',
pageCache: null,
parentId: '1',
serialNum: 2,
type: 1,
updateTime: '2020-12-04 09:11:45',
updateUser: 'admin',
url: '/role',
busSys: 'bsp',
children: [
{
checked: false,
code: 'BJZJS',
createTime: '2020-12-04 09:11:39',
createUser: 'admin',
enable: true,
icon: '',
id: '101',
name: '编辑页签',
pageCache: null,
parentId: '1',
serialNum: 1,
type: 4,
updateTime: '2020-12-04 09:11:45',
updateUser: 'admin',
url: '/tenant',
busSys: 'bsp',
children: [
{
checked: false,
code: 'BJZJS',
createTime: '2020-12-04 09:11:39',
createUser: 'admin',
enable: true,
icon: '',
id: '101001',
name: '编辑中间页',
pageCache: null,
parentId: '101',
serialNum: 1,
type: 3,
updateTime: '2020-12-04 09:11:45',
updateUser: 'admin',
url: '/tenant',
busSys: 'bsp',
children: null,
functionVOs: [
{
checked: false,
createTime: '2020-12-04 09:11:39',
createUser: 'admin',
enable: true,
funcCode: 'BJZJS',
funcName: '保存222',
funcUrl: '/tenant',
id: '1071',
rescId: '101',
serialNum: 1,
updateTime: '2020-12-04 09:11:45',
updateUser: 'admin',
busSys: 'bsp',
},
{
checked: false,
createTime: '2020-12-04 09:11:39',
createUser: 'admin',
enable: true,
funcCode: 'CODE_BDIS',
funcName: '禁用222',
funcUrl: '/tenant',
id: '1081',
rescId: '101',
serialNum: 2,
updateTime: '2020-12-04 09:11:45',
updateUser: 'admin',
busSys: 'bsp',
},
{
checked: false,
createTime: '2020-12-04 09:11:39',
createUser: 'admin',
enable: true,
funcCode: 'CODE_BEN',
funcName: '禁用222',
funcUrl: '/tenant',
id: '1091',
rescId: '101',
serialNum: 3,
updateTime: '2020-12-04 09:11:45',
updateUser: 'admin',
busSys: 'bsp',
},
],
},
],
functionVOs: [
{
checked: false,
createTime: '2020-12-04 09:11:39',
createUser: 'admin',
enable: true,
funcCode: 'BJZJS',
funcName: '保存',
funcUrl: '/tenant',
id: '1071',
rescId: '101',
serialNum: 1,
updateTime: '2020-12-04 09:11:45',
updateUser: 'admin',
busSys: 'bsp',
},
// {
// checked: false,
// createTime: '2020-12-04 09:11:39',
// createUser: 'admin',
// enable: true,
// funcCode: 'CODE_BDIS',
// funcName: '禁用',
// funcUrl: '/tenant',
// id: '1081',
// rescId: '101',
// serialNum: 2,
// updateTime: '2020-12-04 09:11:45',
// updateUser: 'admin',
// busSys: 'bsp',
// },
{
checked: false,
createTime: '2020-12-04 09:11:39',
createUser: 'admin',
enable: true,
funcCode: 'CODE_BEN',
funcName: '禁用',
funcUrl: '/tenant',
id: '1091',
rescId: '101',
serialNum: 3,
updateTime: '2020-12-04 09:11:45',
updateUser: 'admin',
busSys: 'bsp',
},
],
},
],
functionVOs: null,
},
{
checked: false,
code: '',
createTime: '2020-12-04 09:11:39',
createUser: 'admin',
enable: true,
icon: '',
id: '3',
name: '中间页管理',
pageCache: null,
parentId: '1',
serialNum: 3,
type: 1,
updateTime: '2020-12-04 09:11:45',
updateUser: 'admin',
url: '/tenant',
busSys: 'bsp',
children: [
{
checked: false,
code: 'BJZJS',
createTime: '2020-12-04 09:11:39',
createUser: 'admin',
enable: true,
icon: '',
id: '106',
name: '编辑中间页',
pageCache: null,
parentId: '3',
serialNum: 1,
type: 3,
updateTime: '2020-12-04 09:11:45',
updateUser: 'admin',
url: '/tenant',
busSys: 'bsp',
children: null,
functionVOs: [
{
checked: false,
createTime: '2020-12-04 09:11:39',
createUser: 'admin',
enable: true,
funcCode: 'BJZJS',
funcName: '保存',
funcUrl: '/tenant',
id: '107',
rescId: '106',
serialNum: 1,
updateTime: '2020-12-04 09:11:45',
updateUser: 'admin',
busSys: 'bsp',
},
{
checked: false,
createTime: '2020-12-04 09:11:39',
createUser: 'admin',
enable: true,
funcCode: 'CODE_BDIS',
funcName: '禁用',
funcUrl: '/tenant',
id: '108',
rescId: '106',
serialNum: 2,
updateTime: '2020-12-04 09:11:45',
updateUser: 'admin',
busSys: 'bsp',
},
{
checked: false,
createTime: '2020-12-04 09:11:39',
createUser: 'admin',
enable: true,
funcCode: 'CODE_BEN',
funcName: '禁用',
funcUrl: '/tenant',
id: '109',
rescId: '106',
serialNum: 3,
updateTime: '2020-12-04 09:11:45',
updateUser: 'admin',
busSys: 'bsp',
},
],
},
],
functionVOs: null,
},
],
functionVOs: null,
},
{
checked: false,
code: '',
createTime: '2020-12-04 09:11:39',
createUser: 'admin',
enable: true,
icon: '',
id: '6',
name: '应用管理',
pageCache: null,
parentId: null,
serialNum: 2,
type: 1,
updateTime: '2020-12-04 09:11:45',
updateUser: 'admin',
url: '/role',
busSys: 'bsp',
children: [
{
checked: false,
code: '',
createTime: '2020-12-04 09:11:39',
createUser: 'admin',
enable: true,
icon: '',
id: '7',
name: '应用配置',
pageCache: null,
parentId: '6',
serialNum: 2,
type: 1,
updateTime: '2020-12-04 09:11:45',
updateUser: 'admin',
url: '/role',
busSys: 'bsp',
children: null,
functionVOs: null,
},
{
checked: false,
code: '_YHPZ',
createTime: '2020-12-22 09:16:56',
createUser: 'c3843977198142589f139ef3f5133333',
enable: false,
icon: null,
id: '9820413ccd1f46ba875f8c2adab3fd66',
name: '用户配置',
pageCache: null,
parentId: '6',
serialNum: 2,
type: 1,
updateTime: '2020-12-23 03:32:15',
updateUser: 'c3843977198142589f139ef3f5133333',
url: '/userset',
busSys: 'bsp',
children: null,
functionVOs: null,
},
{
checked: false,
code: '_GLEJ',
createTime: '2020-12-23 02:12:53',
createUser: 'c3843977198142589f139ef3f5133333',
enable: true,
icon: null,
id: 'd1f85843442147e6accd28c8cf30b50e',
name: '管理二级',
pageCache: null,
parentId: '6',
serialNum: 4,
type: 1,
updateTime: '2020-12-23 02:12:53',
updateUser: 'c3843977198142589f139ef3f5133333',
url: '/manager2',
busSys: 'bsp',
children: [
{
checked: false,
code: '_GLEJ_GLSJ',
createTime: '2020-12-23 10:52:40',
createUser: 'c3843977198142589f139ef3f5133333',
enable: true,
icon: null,
id: 'b7923be0ec8c47e4ba62cdeb24d4ab34',
name: '管理三级',
pageCache: null,
parentId: 'd1f85843442147e6accd28c8cf30b50e',
serialNum: 1,
type: 1,
updateTime: '2020-12-23 10:52:40',
updateUser: 'c3843977198142589f139ef3f5133333',
url: '/manager3',
busSys: 'bsp',
children: null,
functionVOs: null,
},
],
functionVOs: null,
},
],
functionVOs: null,
},
{
checked: false,
code: '',
createTime: '2020-12-04 09:11:39',
createUser: 'admin',
enable: true,
icon: '',
id: '8',
name: '应用视角',
pageCache: null,
parentId: null,
serialNum: 3,
type: 1,
updateTime: '2020-12-04 09:11:45',
updateUser: 'admin',
url: '/role',
busSys: 'bsp',
children: [
{
checked: false,
code: '',
createTime: '2020-12-04 09:11:39',
createUser: 'admin',
enable: true,
icon: '',
id: '10',
name: '资源管理',
pageCache: null,
parentId: '8',
serialNum: 2,
type: 1,
updateTime: '2020-12-04 09:11:45',
updateUser: 'admin',
url: '/role',
busSys: 'bsp',
children: null,
functionVOs: null,
},
{
checked: false,
code: '',
createTime: '2020-12-04 09:11:39',
createUser: 'admin',
enable: true,
icon: '',
id: '11',
name: '操作管理',
pageCache: null,
parentId: '8',
serialNum: 3,
type: 1,
updateTime: '2020-12-04 09:11:45',
updateUser: 'admin',
url: '/role',
busSys: 'bsp',
children: null,
functionVOs: null,
},
{
checked: false,
code: '',
createTime: '2020-12-04 09:11:39',
createUser: 'admin',
enable: true,
icon: '',
id: '12',
name: '订购管理',
pageCache: null,
parentId: '8',
serialNum: 4,
type: 1,
updateTime: '2020-12-04 09:11:45',
updateUser: 'admin',
url: '/role',
busSys: 'bsp',
children: null,
functionVOs: null,
},
],
functionVOs: [
{
checked: false,
createTime: '2020-12-04 09:11:39',
createUser: 'admin',
enable: true,
funcCode: '',
funcName: '角色新增',
funcUrl: '/role',
id: '14',
rescId: '8',
serialNum: 1,
updateTime: '2020-12-04 09:11:45',
updateUser: 'admin',
busSys: 'bsp',
},
],
},
];
// 生成uuid,形如'b41e8c-122-f43-ef4-ad339cb76'
function uuid () {
function S4 () {
return Math.floor((1 + Math.random()) * 10000)
.toString(16)
.substring(1);
}
return `${S4()}${S4()}-${S4()}-${S4()}-${S4()}-${S4()}${S4()}${S4()}`;
}
const TYPE_MAP = {
menu: 1, // 菜单类型
button: 2, // 按钮类型
middle: 3, // 中间页
tab: 4, // 页签(也属于中间页的一类)
};
const NEED_CLONE_PROPS = ['type', 'pid', 'id', 'name', 'enable', 'checked', 'serialNum'];
const isMenu = item => item.type === TYPE_MAP.menu;
const isMiddle = item => item.type === TYPE_MAP.middle;
const isTab = item => item.type === TYPE_MAP.tab;
const isMiddleTab = item => isMiddle(item) || isTab(item);
function isChildrenMenu (children) {
// 注意:空数组 some 方法永远返回 false
return children.some(item => isMenu(item));
}
function isChildrenMiddleTab (children) {
// 注意:空数组 some 方法永远返回 false
return children.some(item => isMiddleTab(item));
}
/* 判断是否为最后一级菜单 */
function isLastMenuLevel (item) {
if (!isMenu(item)) {
return false;
}
if (!item.children) {
return true;
}
const childrenIsMenu = isChildrenMenu(item.children);
return !childrenIsMenu;
}
// 得到菜单和中间页的最大层数
function getDepth (arr) {
const menuDepthArr = []; // 代表所有菜单的列表(即菜单叶子节点深度)
// 注意:中间页包括中间页+页签,且中间页只能在最后一层,前面可以有多层页签
const totalDepthArr = []; // 代表所有中间页的深度(即各自菜单的中间页深度)
function dfs (item, level) {
const isLastMenu = isLastMenuLevel(item);
if (isLastMenu) {
menuDepthArr.push(level);
}
const { children } = item;
if (!children) {
totalDepthArr.push(level);
return;
}
level++;
children.forEach(item => dfs(item, level));
}
// 因为第一层一定是菜单,所以为 true
arr.forEach(item => dfs(item, 1));
const middleTabDepthArr = [];
totalDepthArr.forEach((item, idx) => {
middleTabDepthArr.push(item - menuDepthArr[idx]);
});
console.log({ menuDepthArr });
console.log({ totalDepthArr });
console.log({ middleTabDepthArr });
const depth = {
menu: Math.max(...menuDepthArr),
middleTab: Math.max(...middleTabDepthArr),
};
return depth;
}
// 树形数据 --> 二维表格数据
function flatData (list) {
const res = [];
// 把数据copy过来,只需要NEED_CLONE_PROPS里的属性
function cloneItem (item, level, isMenu) {
const res = {};
const LETTER = isMenu ? 'L' : 'M';
NEED_CLONE_PROPS.forEach(key => {
res[`${LETTER}${level}-${key}`] = item[key];
});
return res;
}
function cloneMenuItem (item, level) {
return cloneItem(item, level, true);
}
function cloneMiddleTabItem (item, level) {
return cloneItem(item, level, false);
}
/**
* 把数据铺平
*
* @param {*} item 当前要处理的元素
* @param {*} level 当前的层级
* @param {*} currRes 累积到当前层级的
* @param {*} allRes 得到的所有结果,主要是加上了叶子结点的信息
* @return {*}
*/
function dfs (item, menuLevel, middleTabLevel, currRes, allRes) {
let res = {};
if (isMenu(item)) {
menuLevel++;
res = cloneMenuItem(item, menuLevel);
} else {
middleTabLevel++;
res = cloneMiddleTabItem(item, middleTabLevel);
}
const localCurrRes = { ...currRes, ...res };
// 1-无children
if (!item.children) {
const res = Object.assign(localCurrRes, {
functionVOs: item.functionVOs,
id: uuid(),
});
allRes.push(res);
return;
}
// 2-有children
/* 儿子为中间页(自己为上级中间页或者叶子菜单,不管怎样都需要增加到列表中) */
if (isChildrenMiddleTab(item.children)) {
const res = Object.assign(localCurrRes, {
functionVOs: item.functionVOs,
id: uuid(),
});
allRes.push(res);
}
item.children.forEach(item => {
dfs(item, menuLevel, middleTabLevel, localCurrRes, allRes);
});
}
list.forEach(item => dfs(item, 0, 0, {}, res));
console.log(res);
return res;
}
/**
* 计算每层菜单的数量,以此来计算表格 rowSpan,计算合并的个数
*
* @param { Array } data 树形数据平铺后得到的二维表格数据
* @param { object } depth { menu: number, middleTab: number, } 包括菜单和中间页的深度
* @return Map结构
* key: xx-name
* value: [{ name:xx-name, idx:xx, count:xx, xx-id:xx}] 可以有多个,因为可能同名,通过id区分
*/
function getNameMap (data, depth) {
const nameList = [];
for (let i = 0; i < depth.menu; i++) {
nameList.push(`L${i + 1}-name`);
}
for (let i = 0; i < depth.middleTab; i++) {
nameList.push(`M${i + 1}-name`);
}
console.log({ nameList });
const nameMap = new Map();
data.forEach((item, idx) => {
nameList.forEach(name => {
if (item[name] === undefined) {
return;
}
const prefix = name.slice(0, 3);
if (nameMap.has(item[name])) {
const list = nameMap.get(item[name]);
const obj = list.find(e => e[`${prefix}id`] === item[`${prefix}id`]);
if (obj) {
obj.count++;
} else {
const obj = { idx, count: 1 };
obj[`${prefix}id`] = item[`${prefix}id`];
obj[name] = item[name];
list.push(obj);
}
} else {
const obj = { idx, count: 1 };
obj[`${prefix}id`] = item[`${prefix}id`];
obj[name] = item[name];
nameMap.set(item[name], [obj]);
}
});
});
console.log({ nameMap });
return nameMap;
}
/* 处理数据 */
function handleData (arr) {
const depth = getDepth(arr); // 通过计算得出
const data = flatData(arr);
const nameMap = getNameMap(data, depth);
// 处理
return {
data,
depth,
nameMap,
};
}
console.log(handleData(data));
下一步
下一步是合并操作,这个已经完成。
再下一步是处理选择和取消选择的父子节点联动关系。