笔记,前端导出数据

628 阅读6分钟

需求:针对页面的表头是有分组的。因此在原先的导出的功能的基础上去实现导出也实现分组,并且发现网上少有这种需求技术实现思路,因此自己实现一个

思路

image.png 针对这种分组。初步确定数据结构是是像菜单一样的树结构一样。同时也要考虑到部份场景可能是有三级。四级分组

tablesGroupThree方法参数

  tablesGroupThree(
    field,
    data,
    {
      filename,//导出文件名
      wx_name = `Sheet${new Date().getTime()}`,//excel的工作表名称
      isLab = false,//是否显示字段名
      width = 23,//单元格的宽度
      Group = [],//分组
      isBeforeGroup = true,//是否分组排在最前面
    },
  )
``

#### Group的结构


```js

[{
      title:"一级", //分组名称
      column:['字段A1','字段B1'], //当前一级所属字段
      children:[{ //如果还有分组。则需要
        title:"二级",
        column:['字段C1','字段D1'],
        children:[{title:"三级",column:[],children}]
      }]
    }]

field数组结构,每个字段的具体信息。比如字段的匹配名称。类型是数字还是文字。是否保留小数点

 Filed字段
 FieldText //字段名称
 FieldName //字段名
 FieldType //字段类型
 EnumTypeString, //枚举格式
 FieldMerger // 是否合并上下单元格,值相同且合并
 FieldBackGround //单元格背景颜色
 FieldFontColor //单元格字体颜色
 FieldDefaultColor ////默认合并字段颜色(黄色)

tablesGroupThree的逻辑

  1. 首先要卡控field和Group的参数。同时Group的分组的字段在field字段集合不存在时会报错
 const GetchildColumnError = (Group, field,) => {

      let fieldArr = field.map((item) => {
        return item.FieldName;
      });



      for (let i = 0; i < Group.length; i++) {
        if (Group[i].column && Group[i].column.length) {
          let sliceArr = []
          for (let j = 0; j < Group[i].column.length; j++) {


            if (!fieldArr.includes(Group[i].column[j])) {

              sliceArr.push(j)
              console.log(`当前分组字段${Group[i].column[j]}不存在。已经在分组中删除`)
              // throw Error('当前分组字段在field不存在。请检查');
            }
          }
          sliceArr.map(item => {
            Group[i].column.splice(item, 1)
          })

        }
        if (Group[i].children && Group[i].children.length) {
          for (let k = 0; k < Group[i].children.length; k++) {

            GetchildColumnError(Group[i].children, field,);
          }
        }
      }
    };

2.同时卡控一级分组的title。因为用一级分组标题作为key存诸

 if (
      new Set(
        Group.map((item) => {
          return item.title;
        }),
      ).size !== Group.length
    ) {
      throw new Error('分组标题相同');
    }

3.定义excel的实例对象

    let workbook = new Excel.Workbook();//实例化excel对象
    let worksheet = workbook.addWorksheet(wx_name);//添加工作表
    const columns = []; //获取所有分组列的集合
    const GruopColumnsObj= {}; //所有列的key
    const maxChildArr = []; //每一个分组的最大层级,最终取最大值

4.遍历分组,去获取需要的columns,GruopColumnsObj,maxChildArr

    for (let i = 0; i < Group.length; i++) {
      GruopColumnsObj[Group[i].title] = [];//存诸当前分组的所有列名
      const allColumn = GetchildColumn(Group[i]);//获取当前分组的所有列
      const childreNumber = GetchildColumnTier(Group[i]);//获取分组共有多少层级
      maxChildArr.push(childreNumber);

      allColumn.map((item) => {
        let newItem = field.find((items) => {
          return item == items.FieldName;
        });
        if (newItem) {
          GruopColumnsObj[Group[i].title].push(newItem.FieldName);
          columns.push({
            header: newItem.FieldText,
            key: newItem.FieldName,
            width: width,
            FieldType: newItem.FieldType,
            FieldMerger: newItem.hasOwnProperty('FieldMerger') ? newItem.FieldMerger : false, //合并属性
            FieldBackGround: newItem.hasOwnProperty('FieldBackGround')
              ? newItem.FieldBackGround
              : null, //单元格背影
            FieldFontColor: newItem.hasOwnProperty('FieldFontColor')
              ? newItem.FieldFontColor
              : null, //单元格字体颜色
            FieldDefaultColor: newItem.hasOwnProperty('FieldDefaultColor')
              ? newItem.FieldFontColor
              : false, //默认合并字段颜色(黄色)
          });
        }
      });
    }

4.获取未分组的列并处理信息

   let noGroup = field.filter((item) => {
      //处理未分组的是否启用和隐藏 还有不在分组
      return !columns.find((items) => {
        return items.key == item.FieldName;
      });
    });

    noGroup = noGroup
      .sort((a, b) => {
        //栏目排序
        return a.iColPos - b.iColPos;
      })
      .map((item) => {
        return {
          header: item.FieldText,
          key: item.FieldName,
          width: width,
          FieldType: item.FieldType,
          FieldMerger: item.hasOwnProperty('FieldMerger') ? item.FieldMerger : false, //合并属性
          FieldBackGround: item.hasOwnProperty('FieldBackGround') ? item.FieldBackGround : null, //单元格背影
          FieldFontColor: item.hasOwnProperty('FieldFontColor') ? item.FieldFontColor : null, //单元格字体颜色
          FieldDefaultColor: item.hasOwnProperty('FieldDefaultColor') ? item.FieldFontColor : false, //默认合并字段颜色(黄色)
        };
      });

5.操作worksheet,用图来展示思路

处理表头的列

    let worksheetColumns = [];

    if (noGroup.length) {
      if (isBeforeGroup) {
        worksheetColumns = [...columns, ...noGroup];
      } else {
        worksheetColumns = [...noGroup, ...columns];
      }
    } else {
      worksheetColumns = [...columns];
    }


     worksheet.columns = worksheetColumns;
      //获取最高的层级
    const MaxChild = Math.max(...maxChildArr);

    for (let i = 0; i < MaxChild - 1; i++) {
      //有多少层级就添加多少行
      let rowValue = {};
      columns.map((item) => {
        rowValue[item.key] = item.key;
      });
      noGroup.map((item) => {
        rowValue[item.key] = item.key;
      });
      worksheet.addRow(rowValue);
      if (isLab && i == MaxChild - 1) {
        //如果isLab为true,则添加一段字段
        worksheet.addRow(rowValue);
      }
    }

处理表头的导出效果,此时未合并

image.png

合并表头组件的逻辑代码,这里在处理分组时我们采用的层级遍历。也就是广度优先,通过每次兄弟节点优先遍历,没有兄弟节点时,在遍历子节点的兄弟节点,以此类推。代码逻辑如下

  1. 将组进行层级遍历。也就是说先遍历所有一级分组,再遍历所有二级分级
  2. 在遍历过程中,要判断当前层级是否有上一层级与下一层级的逻辑处理。
条件为如果有上一层级
  1. 如果有上一层级。且需要合并上下的单元格
  2. 如果有上一层级,且有下一层级,先获取当前分组所有列并且拿最后一个,进行左右的合并单元格
  3. 如果有上一层级。且没有下一层级。哪么判定为最后一层转化字段为名称渲染
条件为如果没有上一层级
  1. 如果没有上一层级,且有下一层级,先获取当前分组所有列并且拿最后一个,进行左右的合并单元格
  2. 如果没有上一层级。且没有一层级,哪么判定为最后一层转化字段为名称渲染
  3. 如果没有上一级级。且有下一级但没有当前的列,且合并左右。且右为当前组的最大长度的letter image.png

可以了解下广度优化的大致代码,实现的步聚就是通过这个形式去实现的

     // 广度优先搜索
bfs (uuid) {
    // 1.先初始化一个空队列
    const queue = []
    this.marked[uuid] = true
    // 2.将起始节点放入队列
    queue.push(uuid)
    console.log('bfs', uuid)
    while (queue.length) {
        // 3.弹出队列第一个节点,并且访问它子节点
        const uuid_ = queue.shift()
        this.edges[uuid_].forEach(edge => {
            // 4.如果没有被标记,那就标记它,并放入队列
            if (!this.marked[edge]) {
                this.marked[edge] = true
                console.log('bfs', edge)
                queue.push(edge)
            }
        })
    }
}

具体实现如下,

 function MegerCell2(Group, GruopAllColumns, MaxChild, fieldColumn) {

   if (!Group) return [];
   let res = {};
   let handle_Columns = []; //记录合并防止遍历时又合并
   let queue = [Group];
   let count = 1; //  记录当前层级

   while (queue.length) {
     let countNum = queue.length; //  当前层级的节点数
     while (countNum--) {
       //  遍历当前层级的节点数

       let current = queue.shift(); // 从前开始取当前层级的节点

       //获取上一级的节点

       let beforeGroup = res[count] ? res[count] : null;
       let before_columns = beforeGroup && beforeGroup.column ? beforeGroup.column : []; //上一节点的column
       let cur_columns = current.column && current.column.length ? current.column : []; //当前节点column

       const Columns_end = worksheet.getColumn(
         GruopAllColumns[GruopAllColumns.length - 1],
       ).letter; //获取当前组的最大长度的letter (excel的单元络A,B,C标志)

       if (before_columns && before_columns.length) {
         // 如果有,则需要合并当前的cur_columns与下面共有多少层级就行了
         for (let i = 0; i < before_columns.length; i++) {

           let merge_top = worksheet.getColumn(before_columns[i]).letter;

           let mergeValue = `${merge_top}${count}:${merge_top}${MaxChild}`; //合并的位置
           if (!handle_Columns.includes(mergeValue)) {
             worksheet.mergeCells(mergeValue);
             let columnTitle = fieldColumn.find((item) => {
               return item.FieldName == before_columns[i];
             });

             //修改单元格标题
             worksheet.getCell(`${merge_top}${count}`).value = columnTitle
               ? columnTitle.FieldText
               : '标题';
             cellStyles(merge_top, count);

             handle_Columns.push(mergeValue);
           }
         }

         //如果有cur_columns。则合并二级标题 这里要注意的是你二级还有三级,4级有多少层级 end要获取所有分组column的最后一个
         if (cur_columns.length && current.children && current.children.length) {
           let lastColumn = GetchildColumn(current);
           let start = worksheet.getColumn(cur_columns[0]).letter;
           let end = worksheet.getColumn(lastColumn[lastColumn.length - 1]).letter;

           let mergeValue = `${start}${count}:${end}${count}`; //合并的位置

           if (!handle_Columns.includes(mergeValue)) {
             worksheet.mergeCells(mergeValue);
             worksheet.getCell(`${start}${count}`).value = current.title
               ? current.title
               : '标题';
             //修改单元格样式
             cellStyles(start, count);
             //防止遍历又执行一次
             handle_Columns.push(mergeValue);
           }
         }
         if ((!current.children || !current.children.length) && cur_columns.length) {
           //标题

           let start = worksheet.getColumn(cur_columns[0]).letter;
           let end = worksheet.getColumn(cur_columns[cur_columns.length - 1]).letter;
           let mergeValue = `${start}${count}:${end}${count}`; //合并的位置

           if (!handle_Columns.includes(mergeValue)) {
             worksheet.mergeCells(mergeValue);
             worksheet.getCell(`${start}${count}`).value = current.title
               ? current.title
               : '标题';
             //修改单元格样式
             cellStyles(start, count);
             handle_Columns.push(mergeValue);
           }
           //最后一层转化字段为名称
           let number = count + 1;

           for (let i = 0; i < cur_columns.length; i++) {
             let merge_top = worksheet.getColumn(cur_columns[i]).letter;
             let columnTitle = fieldColumn.find((item) => {
               return item.FieldName == cur_columns[i];
             });
             let mergeValue = `${merge_top}${number}:${merge_top}${MaxChild}`; //合并的位置

             if (!handle_Columns.includes(mergeValue)) {
               worksheet.mergeCells(mergeValue);
               //修改标题

               worksheet.getCell(`${merge_top}${number}`).value = columnTitle
                 ? columnTitle.FieldText
                 : '标题';
               //修改单元格样式
               cellStyles(merge_top, number);
               handle_Columns.push(mergeValue);
             }
           }
         }
       } else {
         //如果没有上一级且没有下一级
         if (!current.children && cur_columns.length) {


           //存在如果分组的字段在field是没有的情况下

           let start = worksheet.getColumn(cur_columns[0]).letter;

           let end = worksheet.getColumn(cur_columns[cur_columns.length - 1]).letter;

           let mergeValue = `${start}${count}:${end}${count}`; //合并的位置

           if (!handle_Columns.includes(mergeValue)) {
             worksheet.mergeCells(mergeValue);
             worksheet.getCell(`${start}${count}`).value = current.title
               ? current.title
               : '标题';
             //修改单元格样式
             cellStyles(start, count);
             handle_Columns.push(mergeValue);
           }
           //最后一层转化字段为名称
           let number = count + 1;

           for (let i = 0; i < cur_columns.length; i++) {
             //如果不存在则跳过 原因在于分组与field有一个不存在会导致合并出错
             // if(!worksheet.getColumn(cur_columns[i])) break;

             let merge_top = worksheet.getColumn(cur_columns[i]).letter;

             let columnTitle = fieldColumn.find((item) => {
               return item.FieldName == cur_columns[i];
             });
             let mergeValue = `${merge_top}${number}:${merge_top}${MaxChild}`; //合并的位置

             if (!handle_Columns.includes(mergeValue)) {
               worksheet.mergeCells(mergeValue);
               //修改标题
               worksheet.getCell(`${merge_top}${number}`).value = columnTitle
                 ? columnTitle.FieldText
                 : '标题';
               //修改单元格样式
               cellStyles(merge_top, number);
               handle_Columns.push(mergeValue);
             }
           }
         } else if (current.children && current.children.length && cur_columns.length) {
           //没有上一级且有下一级
           // let start = worksheet.getColumn(GruopAllColumns[0]).letter;
           // let mergeValue = `${start}${count}:${Columns_end}${count}`;//合并的位置

           let lastColumn = GetchildColumn(current);

           let start = worksheet.getColumn(cur_columns[0]).letter;

           let end = worksheet.getColumn(lastColumn[lastColumn.length - 1]).letter;

           let mergeValue = `${start}${count}:${end}${count}`; //合并的位置
           if (!handle_Columns.includes(mergeValue)) {
             worksheet.mergeCells(mergeValue);
             //合并赋值单元格的值
             worksheet.getCell(`${start}${count}`).value = current.title
               ? current.title
               : '标题';
             //修改单元格样式
             cellStyles(start, count);

             handle_Columns.push(mergeValue);
           }
         } else if (current.children && current.children.length && !cur_columns.length) {
           let start = worksheet.getColumn(GruopAllColumns[0]).letter;
           let mergeValue = `${start}${count}:${Columns_end}${count}`; //合并的位置

           if (!handle_Columns.includes(mergeValue)) {
             worksheet.mergeCells(mergeValue);
             //合并赋值单元格的值
             worksheet.getCell(`${start}${count}`).value = current.title
               ? current.title
               : '标题';
             //修改单元格样式
             cellStyles(start, count);

             handle_Columns.push(mergeValue);
           }
         }
       }

       //这里是层级遍历
       if (current.children && current.children.length) {
         //如果有子级的话。则存当前count的下一位为当前的node;
         let nextCount = count + 1;

         res[nextCount] = current;
         //层级遍历

         for (let i = 0; i < current.children.length; i++) {
           queue.push(current.children[i]);
           // queue.unshift(current.children[i])
         }
       }
     }
     count++; //  层级+1
   }

   return res;
 }

合并后的列头

image.png

之后就是处理单元格式和遍历添加数据的问题了。这里就不一一展开了。代码其实可以在优化的。但目前已经够用