树形数据转二维数据的算法知识

283 阅读10分钟

树形数据转二维数据的算法知识

by flygo on 2021/01/10

前言

要实现如下表格: 1 其中一级菜单二级菜单是动态生成的,可能有N级,中间页同样,操作是固定的只有一列。 2

代码

所谓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));

下一步

下一步是合并操作,这个已经完成。

再下一步是处理选择和取消选择的父子节点联动关系。

2