最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 基于xlsx实现通用的前端导出方案

    正文概述 掘金(前端自学驿站)   2021-05-21   499

    前言

    在最近开发报表中碰到了需求需要前端导出,原因是表格中的表头大多数都是动态生成的,后端那边觉得前端做起来更简单,我想着没做过这块的也想学习下, 也没跟他杠啥。上gitHub找了下插件对比下发现前端实现起来也不复杂,性能其实也不差,就统一实现了9张报表的导出,里面内容涉及到:

    • 实现createDynamicColumnByDate通过用户选择的日期动态生成表格的tableHead
    • 根据tableHead表头的层级关系计算出excel的表头的行数动态组建excel的行数
    • 将过滤条件也单独生成一个excel表格
    • 实现通用formatJson来处理列表中特殊的行数据
    • 实现「通用」的汇总方法,表格和导出都能实用该函数来动态生成一行汇总

    效果图?

    基于xlsx实现通用的前端导出方案

    excel的数据结构

    基于xlsx实现通用的前端导出方案

    最后生成excel需要数据结构

    基于xlsx实现通用的前端导出方案

    组件化开发页面

    通过配置快速生成页面

    page

    基于xlsx实现通用的前端导出方案

    template

    基于xlsx实现通用的前端导出方案

    配置项

    这里挑两个配置项讲下

    基于xlsx实现通用的前端导出方案

    其实除了这些简单的配置,还有很多高级的玩法,表格同样支持可编辑,包括不限于:

    • 配置Element-ui的表单项名称单元格生成表单组件(可编辑表格)
    • 也可以配置插槽名称或者插槽render方法来自定义单元格的内容
    • 通过配置rules对象校验表格
    • 配置字典项
    • 。。。

    报表的实现

    报表的数据结构

    基于xlsx实现通用的前端导出方案

    导出

    流程图?

    基于xlsx实现通用的前端导出方案

    列表的实现

    config.js

    // 表头配置项,上面也都解析过
    const initTableHead = [
      {
        label: '开始日期',
        prop: 'rentStartDate',
        query: {
          type: 'date',
          dateType: 'month',
          valueFormat: 'yyyy-MM',
          itemClass: 'filter-item-custom',
        },
        hidden: true
      },
      {
        label: '费项',
        prop: 'feeName',
        minWidth: '160',
        align: 'center',
        query: {
          type: 'select',
          data: [],
          key: 'feeId',
        },
      },
      ...
    ]
    
    // 关键金额字段
    const amountFields = [
      'receivableTaxIncluded',
      'receivableExcludingTax',
    ]
    
    const config = {
      data() {
        return {
          tableHead: initTableHead,
          // 不需要响应式[但我page页面的computed中需要用]
          amountFields: Object.freeze(amountFields); 
        }
      }
    }
    
    export {
      config,
    }
    

    对于我们不需要添加响应式的数据可以通过:导入、Object.freeze冻结对象、在created中初始化对象。达到不添加getter和setter的目的,很好的避免了响应式的滥用

    foramterList

    import {
      config as configMixin,
    } from './config/indexConfig'
    
    export default {
      name: 'A', // page(只要是page都会定义个name)
      mixins: [configMixin], // 将config对象的数据混入进来
      computed: {
        pageList({ list = [], amountFields }) {
          if (!list.length) {
            return []
          } else {
            return this.formaterList(list, amountFields)
          }
        }
      },
      methods: {
       formaterList(list, amountFields) {
         // list数据可以从->「报表的数据结构」看
          return list.map(row => {
            row.costTableEntrys.forEach(i => {
              amountFields.forEach(key => {
                // 后端返回的空值只有null(undefied是js中独有的空)
                i[key] = Number(i[key]).toFixed(2)
              })
            })
            return row
          })
        },
      }
    }
    
    
    

    上面formaterList方法并没有写任何的条件语句也能实现对每一行的costTableEntrys分录中金额字段做初始化处理,所以很多时候的条件判断可以通过「逻辑抽象」达到同样的目的

    createDynamicHead

    config.js

    // 表头配置项,上面也都解析过
    const initTableHead = [...]
    
    // 关键金额字段
    const amountFields = [...]
    
    // 月份
    const monthHash = [
      '01',
      '02',
      '03',
      '04',
      '05',
      '06',
      '07',
      '08',
      '09',
      '10',
      '11',
      '12',
    ]
    
    const config = {
      data() {
        return {
        	...
        }
      }
    }
    
    export {
      config,
      monthHash
    }
    

    page

    export default {
      name: 'A', // page
      ... // 前面的写过的就不贴了
      watch: {
        // list一定是个Array,所以只要watch Array.length就行
        // 并不需要去deep每项
        'list.length': {
          handler(len) {
            if (!len) {
              // 没有数据(还原表头)
              this.tableHead = initTableHead
              return
            }
            
           const {
              rentStartDate, // 开始日期(2022-04)
              rentEndDate // 结束日期(2021-04)
            } = this.$refs.dynamicSearch.searchQuery
    
            this.tableHead = this.createDynamicHead({
              year: startDate.substr(0, 4), // 开始日期选择的年份
              startDate: rentStartDate,
              endDate: rentEndDate,
            })
          }
        }
      },
      methods: {
        // 根据查询的数据获取现有数据的日期
        createDynamicHead({ year, startDate, endDate }) {
          // 获得开始日期和结束相差月份(2022-04 - 2021-04) = 12
          const diffMonthNum = differenceInMonths(startDate, endDate)
          // 根据条件匹配出所选月份范围
          const selectMonthScope = []
          // 开始月份的索引(04 -> 3)
          let startIndex = monthHash.indexOf(startDate.substr(-2))
    
          let n = diffMonthNum + 1
          while (n--) {
            const m = monthHash[startIndex]
            selectMonthScope.push({
              label: `${year}-${m}`,
              prop: `${year}-${m}`,
              isDynamic: true, // 动态标识
            })
            // 跨年的情况
            if (m === '12') {
              year = Number(year) + 1
              startIndex = 0
            } else {
              startIndex++
            }
        }
       	// 生成日期表头
       	return [...initTableHead, ...createDateColumnAction(selectMonthScope)]
      }
    }
    
    

    differenceInMonths方法就是的计算相差的月份,过于简单这里就不解析了

    config.js

    // 根据monthToHash中所选月份动态生成动态列
    const createDateColumn = (dynamicColumns) => {
      return dynamicColumns.map(col => {
        return Object.assign(col, {
          minWidth: '160',
          align: 'center',
          children: [
            {
              label: '含税金额',
              prop: `receivableTaxIncluded`,
              minWidth: '160',
              align: 'center',
              // 「关键」-> 动态生成的列的children都叫这个key名
              // 后续formater列值的时候通过parentKey确定children中的这两个字段是
              // 属于那个日期下的数据
              parentKey: col.prop, 
            },
            {
              label: '不含税金额',
              minWidth: '160',
              prop: `receivableExcludingTax`,
              align: 'center',
              parentKey: col.prop, 
            },
          ]
        })
      })
    }
      
    export {
    	config,
      monthHash,
      createDateColumn as createDateColumnAction,
    }
    

    现在的效果

    基于xlsx实现通用的前端导出方案

    现在动态添加列的两种金额类型数据都是在每行的costTableEntrys中, 所以需要用到表格的formater配置项

    const createDateColumn = (dynamicColumns) => {
      return dynamicColumns.map(col => {
        return Object.assign(col, {
       		...
          children: [
            {
              ...
              parentKey: col.prop, 
    +	  formatter: formatterThousand,
            },
            ..
          ]
        })
      })
    }
    

    formatterThousand

    const formatterThousand = ({ row, column, col }) => {
      // col -> 是tableHead中定义的列, [row,column]是el-table-column插槽抛出的数据
      if (row && row.costTableEntrys && column && col) {
        const curItem = row.costTableEntrys.find(i => {
          return i[key] === col['parentKey'] // 找到当前列对应的日期
        }) || {}
        // 从分录的行中拿到金额字段
        const realValue = Number(curItem[column.property] || 0).toFixed(2)
        // 支持负数千分
        const v = String(realValue).replace('-', '') // 去了负号的数
        return v > 999 ? formatThouPercentile(realValue) : realValue
      }
    }
    

    「效果」

    基于xlsx实现通用的前端导出方案

    getSummaries

    基于xlsx实现通用的前端导出方案

    config.js

    // 不合计的字段
    const noTotalFields = [
      'billNumber',
      'customerName',
      'unitNumber',
      'rentalArea',
      'startDate',
      'endDate',
      'actualEndDate',
      'feeName',
    ]
    
    export {
    	config,
      monthHash,
      createDateColumn as createDateColumnAction,
      noTotalFields
    }
    
     // 汇总
     getSummaries({ columns, data }) {
       const sums = ['', '汇总']
       if (!data.length || !columns.length) return sums
       const {
         tableHead
       } = this
       // 获取展示的所有列
       const cols = getHeaderFields(tableHead)
    
       // 需要合计的列
       const totalCols = cols.filter(c => !noTotalFields.includes(c.prop))
    
       // 将数每行格式化成想要的数据值
       const formatData = formatJson(cols, data)
       
       // 合计计算
       const totalObj = calcArrayTotal(
         formatData,
         totalCols,
         calc.Add
       )
    
       // 如果有[序号|多选框]直接补空
       const diffCol = columns.length - cols.length
       
       totalCols.forEach(c => {
         if (Reflect.has(c, 'index')) {
           sums[c.index + diffCol] = formatThouPercentile(
             totalObj[`${c.parentKey}${c.prop}`].toFixed(2)
           )
         }
       })
       return sums
     },
    

    基于xlsx实现通用的前端导出方案

    基于xlsx实现通用的前端导出方案

    验证

    基于xlsx实现通用的前端导出方案

    导出的实现

    这里需要下载插件

    npm i xlsx 
    npm i xlsx-style // 因为我们需要去对excel进行美化,需要下载这个
    

    引入插件会报错,有个地方需要改下

    导入xlsx-style组件报错Can‘t resolve ‘./cptable‘ in ‘xxxx\nautical-front\node_modules_xlsx

    基于xlsx实现通用的前端导出方案

    setHeaderInfo

     // 给表头加上对应的层级
    const {
      tableHead, // 组合的表头
      cols, // 所有的列
      maxLevel, // 最大的层级(当前表头有几行)
    } = setHeaderInfo(静态表头+动态表头)
    
    const setHeaderInfo = (tableHead, level = 0, cols = []) => {
      for (let i = 0, len = tableHead.length; i < len; i++) {
        const col = tableHead[i]
        // [隐藏的列, 不存在prop的字段] 不展示
        if (col.hidden || !col.prop) {
          continue
        }
    		
        // 设定层级
        col.level = level
    
        if (col.children?.length) {
          setHeaderInfo(col.children, level + 1, cols)
        } else {
          // 父级不需要放在数据列中
          cols.push(Object.assign(col, { property: col.prop }))
        }
      }
      return {
        tableHead,
        cols,
        maxLevel: Math.max(...cols.map(c => c.level))
      }
    }
    

    getMultiHeader

    /*************  组建excel表头 ***************/
    // 根据表的层级结构计算出表头的行数
    const {
      multiHeaders,
    } = getMultiHeader({
      tableHead,
      multiHeaders: Array.from(Array(maxLevel + 1)).map(() => [])
    })
    
    let arrList = Array(3).fill([]) //   [Array(0), Array(0), Array(0)]
    arrList[0][0] = 'beige'
    arrList => // [Array(1), Array(1), Array(1)] 填充的都是同一个引用地址
    

    基于xlsx实现通用的前端导出方案

    
    const getMultiHeader = (params) => {
      const {
        tableHead = [],
        multiHeaders = [], // 存储每一行的表头
      } = params
      for (let i = 0, len = tableHead.length; i < len; i++) {
        const col = tableHead[i]
        // [隐藏的列, 不存在prop的字段] 不展示
        if (col.hidden || !col.prop) {
          continue
        }
    
        // 递归处理子级
        if (col.children?.length) {
          /* 如果有子级,父级只有一个单元格有文字,其他用空串占位 */
          // col.level -当前表头的层级
          // 第一行
          multiHeaders[col.level].push(col.label)
          let j = 1
          while (j < col.children.length) {
            // 留点空格占excel的单元格位置
            multiHeaders[col.level].push('    ')
            j++
          }
    
          // 处理第二行
          !multiHeaders[col.level + 1].length
            ? multiHeaders[col.level + 1].push(...Array(
              multiHeaders[col.level].length - col.children.length
            ).fill(''))
            : null
          getMultiHeader({
            tableHead: col.children,
            multiHeaders
          })
          continue
        } else {
          multiHeaders[col.level].push(col.label)
        }
      }
    
      return { tableHead, multiHeaders }
    }
    

    至此所有前置工作完成,接下来就是引入写好的export2excel.js来实现导出

    async exportHandle() {
      // 校验参数...
    	
      // 拉取数据
    	
      import('@/utils/EXPORT2EXCEL').then((excel) => {
       
        // 给表头加上对应的层级
        const {
          tableHead,
          cols, // 所有的列
          maxLevel, // 最大的层级,当前表头有几行
        } = setHeaderInfo(
          // 动态获取表头
          this.createDynamicHead({
            list,
            year: beginDate.substr(0, 4),
            beginDate,
            finishDate,
          })
        )
    
        /** *****************  组建excel表头 *******************/
        // 根据表的层级结构计算出表头的行数
        const {
          multiHeaders,
        } = getMultiHeader({
          tableHead,
          // fill用的都是同一个引用地址
          multiHeaders: Array.from(Array(maxLevel + 1)).map(() => [])
        })
        // 将表头和字段对应上
        const data = formatJson(cols, list)
        /** *****************  动态生成一行汇总 *******************/
       
        // 将汇总添加最后一行
        data.push(sums)
    
        excel.export_json_to_excel({
          multiHeaders,
          dynamicFilterHeads,
          data,
          filename: `${proName}-项目合同费用表(应收)`,
          merges, // 合并表头
          customRowStyleCallBack, // 自定义行的样式
        })
          .then(res => {
            this.$notify({
              title: res.message,
              type: 'success',
              duration: 2500
            })
          })
          .catch((err) => {
            this.$message.error('导出失败!')
            console.error(err)
          })
          .finally(() => {
            this.$store.state.isShowLoading = false
          })
      })
    }
    

    前端

    • 源码地址?:github.com/it-beige/bo…
    • 历史版本⭐:d70c055

    后端

    • 源码地址?:github.com/it-beige/bo…
    • 历史版本⭐:3b049c5

    写在最后

    如果文章中有那块写的不太好或有问题欢迎大家指出,我也会在后面的文章不停修改。也希望自己进步的同时能跟你们一起成长。喜欢我文章的朋友们也可以关注一下

    我会很感激第一批关注我的人。此时,年轻的我和你,轻装上阵;而后,富裕的你和我,满载而归。

    往期文章

    【建议追更】以模块化的思想来搭建中后台项目

    【前端体系】从一道面试题谈谈对EventLoop的理解 (更新了四道进阶题的解析)

    【前端体系】从地基开始打造一座万丈高楼

    【前端体系】正则在开发中的应用场景可不只是规则校验

    「函数式编程的实用场景 | 掘金技术征文-双节特别篇」

    【建议收藏】css晦涩难懂的点都在这啦


    起源地下载网 » 基于xlsx实现通用的前端导出方案

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元