一、组件功能描述:
封装了Exceljs的表格导出功能,可以通过传入表格数据、表格列进行数据导出
二、组件参数说明:
- props(属性)
| 参数名 | 参数类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| modelValue | Boolean | 是 | v-model的绑定值 | |
| fileName | String | 数据表 | 导出文件名称 | |
| dimension | Array | [] | 导出表格的可选维度 | |
| tableRef | Object | null | 表格ref,用于获取列名 | |
| operationColumnName | String | operation | 表格列中操作列prop,用于排除列, | |
| tableData | Array | [] | 表格数据 | |
| getTableDataFunc | Function | null | 获取表格数据的方法 | |
| dataTransFunc | Function | null | 数据转换方法 | |
| custom | Boolean | false | 使用自定义表格维度数据 |
- Emits(抛出事件)
| 事件名 | 事件参数 | 说明 |
|---|---|---|
| modelInput | value | 通知父组件更新modelValue |
- Expose(对外暴露事件) 外部组件需要通过 $refs 来进行调用的方法
| 事件名 | 事件参数 | 说明 |
|---|---|---|
| handleInitColumns | Ref 表格的ref | 手动使用ref 初始化可选维度 |
| handleInitColumnsByArray | array 表格的columns | 手动使用数组 初始化可选维度 |
| setColumnsSelected | array 手动设置columns | 手动设置可选列 |
三、组件代码:
<template>
<div>
<!-- 外部使用建议配合v-if进行销毁 -->
<el-dialog
v-if="dialogShow"
title="导出"
:visible.sync="visible"
:close-on-click-modal="false"
width="40%"
append-to-body
>
<el-form :model="exportForm" label-position="right" label-width="80px">
<el-form-item label="文件名">
<el-input v-model="exportForm.fileName" placeholder="请输入文件名" clearable style="width: 200px;" />
</el-form-item>
<el-form-item label="报表维度">
<el-select v-model="exportForm.columns" multiple placeholder="请选择导出维度" style="width: 100%;">
<el-option
v-for="(item,index) in canSelectDimension"
:key="index + item.prop"
:label="item.label"
:value="item.prop"
/>
</el-select>
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="handleClose">关 闭</el-button>
<el-button type="primary" :loading="loading" @click="handleConfirm">确 认</el-button>
</span>
</el-dialog>
<!-- 导出动画,此处可以自己改成el的notify组件 -->
<ExportloadPopup :show="showNotify" title="数据导出" @close="showNotify = false" />
</div>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { flattenDeep } from 'lodash'
import ExportloadPopup from '@/components/ExportloadPopup/index.vue'
import { Message } from 'element-ui'
// 定义props和emit
const props = defineProps({
modelValue: {
type: Boolean
},
fileName: { // 文件名
type: String,
default: '数据表'
},
dimension: { // 可选维度
type: Array,
default: () => []
},
tableRef: { // 表格ref,用于获取列名
type: Object,
default: null
},
operationColumnName: { // 操作列prop,用于排除列,
type: String,
default: 'operation'
},
tableData: { // 表格数据
type: Array,
default: () => []
},
getTableDataFunc: { // 获取表格数据的方法
type: Function,
default: null
},
dataTransFunc: { // 数据转换方法
type: Function,
default: null
},
custom: { // 使用自定义表格维度数据
type: Boolean,
default: false
}
})
const emits = defineEmits('modelInput')
// visible使用computed实现双向绑定
const visible = computed({
get() {
return props.modelValue
},
set(val) {
// 通知父组件更新modelValue
emits('modelInput', val)
}
})
// 表单定义
const exportForm = reactive({
fileName: props.fileName,
columns: []
})
const canSelectDimension = ref([]) // 最后可以选中的维度
/**
* --------------------------- 列处理工具方法 ------------------------------------
* */
// 从ref 获取表格的列 自动切分 | 自动过滤操作列和序号列
const getTableColumnsFromRef = ref => {
let arr = []
if (ref.$children && ref.$children.length > 0) {
ref.$children.forEach(item => {
if (item.label !== undefined && item.prop !== undefined) {
arr.push({
label: item.label,
prop: item.prop
})
}
})
}
arr = splitColumns(arr)
arr = filterColumns(arr)
console.log(arr, '通过ref处理完成')
return arr
}
// 列过滤 去除操作列,#号列
const filterColumns = arr => {
return arr.filter(item => item.prop.indexOf('#') === -1 && item.prop.indexOf(props.operationColumnName) === -1)
}
// 拆分带有 | 的联合列
const splitColumns = arr => {
const temp = arr.map(item => {
// prop内存在 |
if (item.prop.indexOf('|') !== -1) {
const propList = item.prop.split('|')
const labelList = item.label.split('|')
return [
{
label: labelList[0],
prop: propList[0]
},
{
label: labelList[1],
prop: propList[1]
}
]
} else {
return item
}
})
// 平整数组的项,让所有项都不包含数组
return flattenDeep(temp)
}
// 初始化可选维度
const initCanSelectColumns = () => {
if (props.custom) {
return
}
if (props.tableRef !== null) {
// 传入ref 的方式
try {
canSelectDimension.value = getTableColumnsFromRef(props.tableRef)
} catch (e) {
emitError('logic', e)
}
} else if (props.dimension.length > 0) {
// 手动传入维度
canSelectDimension.value = props.dimension
} else {
// 没有任何参数的时候
canSelectDimension.value = []
emitError('prop', 'props.dimension或props.tableRef不能同时为空')
}
// 默认全选
allColumnsSelected()
}
// 设置全部列选中
const allColumnsSelected = () => {
exportForm.columns = canSelectDimension.value.map(item => {
return item.prop
})
}
// 手动设置可选列
const setColumnsSelected = array => {
exportForm.columns = array
}
// 获取选中的维度
const getSelectDimensionList = () => {
// 使用 filter 方法筛选符合条件的项目
return canSelectDimension.value.filter(item => exportForm.columns.includes(item.prop))
}
// 手动使用ref 初始化可选维度
const handleInitColumns = ref => {
// 从参数的ref 获取列配置
canSelectDimension.value = getTableColumnsFromRef(ref)
// 默认全选
allColumnsSelected()
}
// 手动使用数组 初始化可选维度
const handleInitColumnsByArray = array => {
// 从参数的ref 获取列配置
canSelectDimension.value = array
// 默认全选
allColumnsSelected()
}
/**
* --------------------- 数据处理相关 -----------------------
* */
// 提交按钮加载态
const loading = ref(false)
/**
* -------------------- 弹窗操作相关 -----------------------
* **/
const showNotify = ref(false)
const dialogShow = ref(true) // 弹窗隔离 v-if 用于单独控制内部弹窗的渲染 如果不隔离 则内部弹窗的渲染会受外部弹窗的影响 小提示框无法正常展示
// 点击关闭
const handleClose = () => {
// 恢复隔离变量
dialogShow.value = true
// 关闭弹窗
visible.value = false
showNotify.value = false
// 这里重置列的原因是 如果组件没有使用V-if 进行销毁的话,下一次点击则不是默认态,所以需要恢复默认状态
setTimeout(() => {
// 重置选中列
allColumnsSelected()
}, 200)
}
// 点击确认
const handleConfirm = async() => {
try {
let tableData = []
showNotify.value = true
dialogShow.value = false
// 1、 数据获取
// 如果传入的是方法 则调用方法获取数据
if (props.getTableDataFunc && typeof props.getTableDataFunc === 'function') {
const res = await props.getTableDataFunc()
// console.log(res, '获取的数据')
// 如果有数据处理方法则执行判断返回的数据是否为数组
tableData = props.dataTransFunc && typeof props.dataTransFunc === 'function' ? props.dataTransFunc(res) : res
} else {
// 如果传入的是数据,则直接替换
tableData = Array.isArray(props.tableData) && props.tableData.length > 0 ? props.tableData : []
}
// 2、准备excel导出
import('@/vendor/Export2Excel').then(excel => {
const header = []
const filterVal = []
const dimension = getSelectDimensionList()
dimension.forEach(element => {
header.push(element.label)
filterVal.push(element.prop)
})
const data = formatJson(filterVal, tableData)
excel.export_json_to_excel({
header: header, // 标题
data, // 文件数据
filename: props.fileName // 文件名称
})
setTimeout(() => {
visible.value = false
showNotify.value = false
dialogShow.value = true
// 重置选中列
allColumnsSelected()
}, 300)
})
} catch (e) {
emitError('logic', e)
Message.error('导出失败,' + e)
showNotify.value = false
dialogShow.value = true
// visible.value = false
}
}
// 导出过滤
const formatJson = (filterVal, list) => {
try {
return list.map(v => filterVal.map(j => {
return v[j]
}))
} catch (e) {
emitError('logic', e)
}
}
/**
* ---------------------- 组件相关 -------------------------
* */
// 导出方法
defineExpose({handleInitColumns, handleInitColumnsByArray, setColumnsSelected})
// 初始化可选列
onMounted(() => {
initCanSelectColumns()
})
// 错误抛出
const errorMapping = {
'prop': 'prop属性错误',
'func': '方法调用错误',
'logic': '组件内部逻辑错误',
'other': '其他错误'
}
const emitError = (errType = 'other', errorText) => {
Promise.reject(`组件:exportExcel; 错误类型:${errorMapping[errType]}; 错误提示:${errorText}`)
}
</script>
<script>
export default {
model: {
prop: 'modelValue',
event: 'modelInput'
}
}
</script>
<style scoped lang="scss">
</style>
四、组件使用例子:
<export-excel
v-if="exportExcelVisible"
v-model="exportExcelVisible"
:dimension="exportExcelColumns"
file-name="日志追踪数据"
:get-table-data-func="getExportTableList"
/>
// 导出
const exportExcelVisible = ref(false)
const exportExcelColumns = tableColumns
// 获取导出的数据
const getExportTableList = async() => {
const data = {
...queryParams.value,
account_date_range: dateRangeObject(queryParams.value.account_date_range),
event_date_range: dateRangeObject(queryParams.value.event_date_range),
install_date_range: dateRangeObject(queryParams.value.install_date_range),
page: 1,
page_size: 99999 // 默认上限 99999 10w数量
}
try {
const res = await service.post('/api/operation/data/link/list', data)
return transTableData(res.data.list)
} catch (e) {
return []
}
}
// tableColumns -》
export const tableColumns = [
{prop: 'action_type', label: '媒体'},
{prop: 'event_time', label: '时间'},
{prop: 'device_type', label: '系统'},
{prop: 'device_type_ip', label: 'IP'}
]
// transTableData -》
// 转化导出的数据
const transTableData = dataList => {
return dataList.map(item => {
return {
...item
}
})
}
五、其他注意事项:
-
组件使用v-model 传入Boolean 值进行状态控制,当值为 true的时候 会展示导出弹窗,文件名 输入框 可以更改导出文件名, 报表维度 选择器 会根据传入的表格列信息 将列进行默认全选也可手动更改需要导出的列;
-
使用的时候可以选择两种方式处理表格列信息:
- 使用表格ref,如果使用表格ref 则需要传入tableRef props,组件会自动根据传入的tableRef 数据遍历所有的列数据
- 使用自定义列数据,可以通过传入列数据到 dimension props 控制可选列信息,但是需要传入的数据中包含 label 和 prop 两个键
- 两种方式选择一种即可,如果同时存在则优先使用Ref
-
组件初始化流程:
-
组件在mounted 的时候 会执行 initCanSelectColumns 方法 通过ref 或者 传入的列数据 手动初始化可选维度的选择项,如果希望手动传入可选维度并初始化,则可以将 custom props 传入 false,并且通过 暴露的三个方法进行手动处理 handleInitColumns, handleInitColumnsByArray, setColumnsSelected
- handleInitColumns // 手动使用ref 初始化可选维度
- handleInitColumnsByArray // 手动使用数组 初始化可选维度
- setColumnsSelected // 手动设置可选列
-
在 initCanSelectColumns 方法中,
- 优先判断 table Ref 进行处理 调用 getTableColumnsFromRef 方法进行列的细致处理,这一步会取出所有的子列,组合为新数组,同时自动切分列名props中带有 | (由于部分列中是组合列数据,所以可以通过 | 进行分割),且自动过滤操作列和序号列
-
// 从ref 获取表格的列 自动切分 | 自动过滤操作列和序号列 const getTableColumnsFromRef = ref => { let arr = [] if (ref.$children && ref.$children.length > 0) { ref.$children.forEach(item => { if (item.label !== undefined && item.prop !== undefined) { arr.push({ label: item.label, prop: item.prop }) } }) } arr = splitColumns(arr) arr = filterColumns(arr) console.log(arr, '通过ref处理完成') return arr } // 列过滤 去除操作列,#号列 const filterColumns = arr => { return arr.filter(item => item.prop.indexOf('#') === -1 && item.prop.indexOf(props.operationColumnName) === -1) } // 拆分带有 | 的联合列 const splitColumns = arr => { const temp = arr.map(item => { // prop内存在 | if (item.prop.indexOf('|') !== -1) { const propList = item.prop.split('|') const labelList = item.label.split('|') return [ { label: labelList[0], prop: propList[0] }, { label: labelList[1], prop: propList[1] } ] } else { return item } }) // 平整数组的项,让所有项都不包含数组 return flattenDeep(temp) } - 如果table Ref 为空则处理 dimension props 即传入的colums数组,此时会直接使用手动传入的数组,不做处理,所以外部组件传入的时候务必确认数组是正常格式的Columns 至少包含label 和 prop 项
- 如果table Ref 或 dimension 数据 都为空,控制台 会抛出 props.dimension或props.tableRef不能同时为空 的 prop错误
- 如果正常处理完参数,则会调用 allColumnsSelected 方法 将所有的维度默认全选
-
-
关于组件模板结构的说明
- 组件模板结构如下 Div > ( el-dialog + ExportloadPopup )
- 外部组件v-model 同步到visible 变量 且通过v-if 联合控制,dialogShow 变量控制弹窗是展示,showNotify 控制 ExportloadPopup 组件的展示
- 由于在点击导出确认按钮后,需要展示 ExportloadPopup 小弹窗,所以不能将小弹窗 和 dialog做父子关系,又由于需要ExportloadPopup 组件,dialog父组件无法销毁 否则会导致小弹出也一起销毁,所以外层使用div包裹,在完成下载事件后 才通过将 visible 改为false 将整个导出父组件进行销毁。
-
导出函数说明
- 源代码
// 点击确认 const handleConfirm = async() => { try { let tableData = [] showNotify.value = true dialogShow.value = false // 1、 数据获取 // 如果传入的是方法 则调用方法获取数据 if (props.getTableDataFunc && typeof props.getTableDataFunc === 'function') { const res = await props.getTableDataFunc() // console.log(res, '获取的数据') // 如果有数据处理方法则执行判断返回的数据是否为数组 tableData = props.dataTransFunc && typeof props.dataTransFunc === 'function' ? props.dataTransFunc(res) : res } else { // 如果传入的是数据,则直接替换 tableData = Array.isArray(props.tableData) && props.tableData.length > 0 ? props.tableData : [] } // 2、准备excel导出 import('@/vendor/Export2Excel').then(excel => { const header = [] const filterVal = [] const dimension = getSelectDimensionList() dimension.forEach(element => { header.push(element.label) filterVal.push(element.prop) }) const data = formatJson(filterVal, tableData) excel.export_json_to_excel({ header: header, // 标题 data, // 文件数据 filename: props.fileName // 文件名称 }) setTimeout(() => { visible.value = false showNotify.value = false dialogShow.value = true // 重置选中列 allColumnsSelected() }, 300) }) } catch (e) { emitError('logic', e) Message.error('导出失败,' + e) showNotify.value = false dialogShow.value = true // visible.value = false } } ``` 1. 在数据获取中 有两种方式: 1. 优先使用外部组件传入的方法获取数据,且如果存在处理方法 会将返回的数据 进一步调用处理方法处理 1. 如果传入的是数据,则直接替换 1. 使用Export2Excel 库导出文件 1. 在导出成功后 将可选列重置选中,弹窗、小提示、父组件延迟300ms后完全销毁(避免由于导出过快 组件立刻销毁出现闪烁);如果存在异常则直接捕获抛出 -
错误说明,错误类型映射如下,通过 emitError 方法统一调用抛出
// 错误抛出
const errorMapping = {
'prop': 'prop属性错误',
'func': '方法调用错误',
'logic': '组件内部逻辑错误',
'other': '其他错误'
}
const emitError = (errType = 'other', errorText) => {
Promise.reject(`组件:exportExcel; 错误类型:${errorMapping[errType]}; 错误提示:${errorText}`)
}