前言
在最近开发报表中碰到了需求需要前端导出,原因是表格中的表头大多数都是动态生成的,后端那边觉得前端做起来更简单,我想着没做过这块的也想学习下, 也没跟他杠啥。上gitHub找了下插件对比下发现前端实现起来也不复杂,性能其实也不差,就统一实现了9张报表的导出,里面内容涉及到:
- 实现
createDynamicColumnByDate
通过用户选择的日期动态生成表格的tableHead
- 根据
tableHead
表头的层级关系计算出excel的表头的行数动态组建excel的行数 - 将过滤条件也单独生成一个excel表格
- 实现通用
formatJson
来处理列表中特殊的行数据 - 实现「通用」的汇总方法,表格和导出都能实用该函数来动态生成一行汇总
效果图?
excel的数据结构
最后生成excel需要数据结构
组件化开发页面
通过配置快速生成页面
page
template
配置项
这里挑两个配置项讲下
其实除了这些简单的配置,还有很多高级的玩法,表格同样支持可编辑,包括不限于:
- 配置Element-ui的表单项名称单元格生成表单组件(可编辑表格)
- 也可以配置插槽名称或者插槽render方法来自定义单元格的内容
- 通过配置rules对象校验表格
- 配置字典项
- 。。。
报表的实现
报表的数据结构
导出
流程图?
列表的实现
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,
}
现在的效果
现在动态添加列的两种金额类型数据都是在每行的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
}
}
「效果」
getSummaries
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
},
验证
导出的实现
这里需要下载插件
npm i xlsx
npm i xlsx-style // 因为我们需要去对excel进行美化,需要下载这个
引入插件会报错,有个地方需要改下
导入xlsx-style组件报错Can‘t resolve ‘./cptable‘ in ‘xxxx\nautical-front\node_modules_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)] 填充的都是同一个引用地址
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晦涩难懂的点都在这啦
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!