Vue3实现动态导入Excel表格数据
1. 前言
在开发工作过程中,我们会遇到各种各样的表格数据导入,大部分我们的解决方案:提供一个模板前端进行下载,然后按照这个模板要求进行数据填充,最后上传导入,这是其中一种解决方案。个人认为还有另外一种解决方案,不一定会前面的方案好,方便,但是可以减少人为操作,减少出错,更为通用,就是进行动态数据导入,前端进行自定义导入数据,配置数据对应关系,最后提交导入结果。
2. 如何实现
2.1使用技术
后台使用.Net6.0,前端使用Vue3
2.2实现思路
- 前端通过Excel表格数据导入,导入后客户可以进行数据编辑(未实现)客户数据确认后进行数据回写主页面
- 设置数据对应关系,也就是后台所需数据格式和前台数据格式进行绑定
- 数据校验,进行数据提交
2.3具体实现方案
2.3.1导入Excel数据
创建列表页面,页面主要布局如下
Table需要绑定的列是未知的,所以el-table绑定的el-table-column是需要动态进行加载,动态定义表格以及变量
<el-table :data="state.tableData.data"> <el-table-column v-for="item in state.colunm" :prop="item.key" :key="item.key" :label="item.lable" > </el-table-column> </el-table>
const state = reactive({ colunm: [{key: "", lable: ""}], });
点击导入报名数据,弹出上传数据页面
这一块的功能在之前的一篇文章有写过:https://www.cnblogs.com/wuyongfu/p/16651107.html
子组件的核心代码
页面布局:
<template> <el-dialog :close-on-click-modal="false" v-model="state.dialogVisible" :title="title" width="70%" > <el-upload class="upload-demo" :auto-upload="false" :on-change="uploadChange" style="text-align: left;" > <el-button type="primary">上传文件</el-button> </el-upload> <el-form-item> <el-button @click='submit'>确认导入</el-button> </el-form-item> <el-table :data="state.tableData.data" max-height="500px"> <el-table-column v-for="item in state.colunm" :prop="item.key" :key="item.key" :label="item.lable"> </el-table-column> </el-table> <div class='block flex justify-end' v-if='state.tableData.total > 0'> <el-pagination v-model:currentPage="state.searchInput.PageIndex" v-model:page-size="state.searchInput.PageSize" :page-sizes="[10, 50, 200, 1000]" layout="total, sizes, prev, pager, next, jumper" @size-change="getData" @current-change="getData" :total="state.tableData.total" /> </div> </el-dialog> </template>
变量定义:
const state = reactive({ tempTableData: [{}],//临时存储全部数据 searchInput: { PageIndex: 1, PageSize: 10 }, tableData: { data: [{}], total: 0 },//表格加载当前页面数据 dialogVisible: false, colunm: [{ key: '', lable: '' }] });
父页面调用方法,进行表格列头加载,初始化表格数据:
const childMethod = (data) => { state.colunm = data; state.tableData.data = []; state.dialogVisible = true; }
绑定表格方法,前端进行分页数据处理:(这里可能会有性能问题,暂时没有仔细探究)
const getData = () => { const tempData: any = []; state.tempTableData.forEach((value, index) => { if (index >= ((state.searchInput.PageIndex - 1) * state.searchInput.PageSize) && index < ((state.searchInput.PageIndex) * state.searchInput.PageSize)) { tempData.push(value); } }); state.tableData.data = tempData; state.tableData.total = state.tempTableData.length; }
提交数据回写父组件方法
const submit = () => { console.log(state.tempTableData); context.emit('childClick', state.tempTableData,state.colunm) } 上传Excel读取数据方法,主要动态绑定列以及导入的数据 const uploadChange = async (file) => { let dataBinary = await readFile(file.raw) let workBook = XLSX.read(dataBinary, { type: 'binary', cellDates: true }) let workSheet = workBook.Sheets[workBook.SheetNames[0]] let data: any = XLSX.utils.sheet_to_json(workSheet) let mycolunm={}; Object.setPrototypeOf(mycolunm,data[0]); state.colunm=[]; for(let key in mycolunm){ state.colunm.push( { lable: key, key: key }) } let tHeader = state.colunm.map(obj => obj.lable) let filterVal = state.colunm.map(obj => obj.key) tHeader.map(val => filterVal.map(obj => val[obj])) const tempData: any = []; data.forEach((value) => { const ob = {}; tHeader.forEach((item, index) => { ob[filterVal[index]] = value[item].toString(); }) tempData.push(ob); }) state.tempTableData = tempData; getData(); }
这里导入数据后,会调用主组件方法(dataUploadchildClick)用于回写主组件表格数据,列头等数据
const dataUploadchildClick = (data, colunm) => { state.colunm = colunm; state.tempData = data; getData(); dataUpload.value.cancel(); };
2.3.2设置数据对应关系
定义后台需要的列
SelectData: [ {key: 'zkzh', type: '', value: '', selectValue: '', title: '准考证号'}, {key: 'zjlb', type: '', value: '', selectValue: '', title: '证件类别'}, {key: 'zjhm', type: '', value: '', selectValue: '', title: '证件号码'}, {key: 'ksxm', type: '', value: '', selectValue: '', title: '考生姓名'}, {key: 'xb', type: '', value: '', selectValue: '', title: '性别'}, {key: 'ss', type: '', value: '', selectValue: '', title: '省市'}, {key: 'kq', type: '', value: '', selectValue: '', title: '考区'}, {key: 'kdh', type: '', value: '', selectValue: '', title: '考点号'}, {key: 'kdmc', type: '', value: '', selectValue: '', title: '考点名称'}, {key: 'kch', type: '', value: '', selectValue: '', title: '考场号'}, {key: 'kchdz', type: '', value: '', selectValue: '', title: '考场地址'}, {key: 'zwh', type: '', value: '', selectValue: '', title: '座位号'}, {key: 'chc', type: '', value: '', selectValue: '', title: '场次'} ]
创建Select组件,组件核心代码
页面布局部分
<label style="width: 50px;display: inline-block;">{{ itemKey }}:</label> <el-select v-model="state.type" :placeholder="name" size="large" clearable> <el-option v-for="item in state.options" :key="item.value" :label="item.label" :value="item.value" /> </el-select> <el-input v-model="state.value" style="width: 200px;" :placeholder="name" size="large" v-if="state.type=='0'"/> <el-select v-model="state.selectValue" style="width: 200px;" :placeholder="name" size="large" clearable v-if="state.type=='1'||state.type=='2'"> <el-option v-for="item in state.ValueOptions" :key="item.key" :label="item.lable" :value="item.key" /> </el-select>
接受参数定义
props: {
itemKey: String,
name: String,
type: String,
value: String,
selectValue: String,
colunm: Object
},
页面变量定义,这里定义的默认option主要有三个
- 0 固定值,这一列为固定值,不从表格中读取
- 1 下拉框,从导入数据中选择
- 2 自动生成,这里主要是特殊业务,可以进行自定义扩展
const state = reactive({ value: ref(''), type: ref(''), selectValue: ref([]), ValueOptions:[{}], options: [ { value: '0', label: '固定值', }, { value: '1', label: '下拉框', }, { value: '2', label: '自动生成', }, ], });
监听下拉框选择类型:
watch(() => state.type, (newVal) => { context.emit('update:type', newVal); } );
监听下拉框选择固定,文本框输入的值:
watch(() => state.value, (newVal) => { context.emit('update:value', newVal) })
监听下拉框选择下拉框,后面下拉框选择的值:
watch(() => state.selectValue, (newVal) => { context.emit('update:selectValue', newVal) })
监听表格的列,动态加载下拉框绑定的值:
watch(() => props.colunm, (newVal: any) => { state.ValueOptions=newVal; })
最终的效果:
父页面进行引用
<el-row :gutter="24" justify="start" style="text-align: left;"> <div v-for="(item,index) in state.SelectData" :key='index' style="margin-top: 5px;width: 100%;"> <el-col :span="24"><Select :itemKey="item.key" v-model:type="item.type" v-model:value="item.value" v-model:selectValue="item.selectValue" :name="item.title" :colunm="state.colunm"/></el-col> </div> </el-row>
2.3.3进行数据提交
提交数据到后台进行处理,这里根据自己的业务进行验证,或则进行其它扩展
const Save = () => { if (state.tempData.length == 0) { state.active=1; ElMessage.warning('请导入考生数据'); return; } let CheckSelectData = true; state.SelectData.forEach((value, index) => { if (!value.type) { CheckSelectData = false } }); if (!CheckSelectData) { state.active=2; ElMessage.warning('请设置完成数据对应关系'); return; } if (state.tableTimeData.data.length==0){ state.active=3; ElMessage.warning('请添加场次数据'); return; } if (!state.ExamData.jiancheng||!state.ExamData.kaikaonianyue||!state.ExamData.quancheng){ state.active=4; ElMessage.warning('请设置任务相关信息'); return; } axios.post('/GenerateCheckTemplate', state) .then(function (response) { if (response.status == 200) { ElMessage.success(response.data); dataUpload.value.cancel(); getData(); } else { ElMessage.error(response.data) } }) }
后台定义接口:/GenerateCheckTemplate
定义实体对前台导入数据进行接收:
public class GenerateCheckTemplate { /// <summary> /// 考试任务对象 /// </summary> public ExamData ExamData { get; set; } /// <summary> /// 数据对应关系对象 /// </summary> public List<Correspondence> SelectData { get; set; } /// <summary> /// 导入数据 /// </summary> public List<Dictionary<string, string>> tempData { get; set; } /// <summary> /// 导入数据列集合 /// </summary> public List<Colunm> colunm { get; set; } /// <summary> /// 场次对象 /// </summary> public tableTimeData tableTimeData { get; set; } } /// <summary> /// 考试任务对象 /// </summary> public class ExamData { /// <summary> /// 简称 /// </summary> public string jiancheng { get; set; } /// <summary> /// 考试年月 /// </summary> public string kaikaonianyue { get; set; } /// <summary> /// 简称 /// </summary> public string quancheng { get; set; } } /// <summary> /// 数据对应关系对象 /// </summary> public class Correspondence { /// <summary> /// key /// </summary> public string key { get; set; } /// <summary> /// 下拉选择数据 /// </summary> public string selectValue { get; set; } /// <summary> /// 标题 /// </summary> public string title { get; set; } /// <summary> /// 类型 0 固定值 1下拉框选择 /// </summary> public string type { get; set; } /// <summary> /// 固定值数据 /// </summary> public string value { get; set; } } /// <summary> /// 表格对象 /// </summary> public class tableTimeData { /// <summary> /// 提交表格数据 /// </summary> public List<ExamTime> data { get; set; } /// <summary> /// 总数 /// </summary> public int total { get; set; } } public class ExamTime { /// <summary> /// 场次编码 /// </summary> public int? ExamTimeCode { get; set; } /// <summary> /// 场次名称 /// </summary> public string ExamTimeName { get; set; } /// <summary> /// 开始时间 /// </summary> public string startTime { get; set; } /// <summary> /// 结束时间 /// </summary> public string endTime { get; set; } /// <summary> /// 是否编辑状态 /// </summary> public bool Edit { get; set; } } /// <summary> /// 导入数据列对象 /// </summary> public class Colunm { /// <summary> /// 键值 /// </summary> public string key { get; set; } /// <summary> /// 值 /// </summary> public string lable { get; set; } }
然后处理前端传过来的数据转换成需要的数据,这里type需要修改成枚举类型,类型可以根据需求进行扩展
/// <summary> /// 处理前端传过来的数据 /// </summary> /// <param name="companiesInput"></param> private void ProcessingData(GenerateCheckTemplate companiesInput, DataTable dataTable) { Dictionary<string, string> keyValuePairs = new Dictionary<string, string>(); foreach (var data in companiesInput.tempData) { DataRow dataRow = dataTable.NewRow(); foreach (Correspondence correspondence in companiesInput.SelectData) { if (correspondence.type == "0") { dataRow[correspondence.key] = correspondence.value; } else if (correspondence.type == "1") { dataRow[correspondence.key] = data[correspondence.selectValue]; } else if (correspondence.type == "2") { string value = data[correspondence.selectValue]; if (keyValuePairs.ContainsKey(value)) { dataRow[correspondence.key] = keyValuePairs[value]; } else { keyValuePairs.Add(value, (keyValuePairs.Count + 1).ToString().PadLeft(2, '0')); dataRow[correspondence.key] = keyValuePairs[value]; } } } dataTable.Rows.Add(dataRow); } }
整体的功能到这里基本上实现了,具体的细节可能需要根据不同的项目进行优化,有更多的方案可以一起进行交流
附上源码地址:https://gitee.com/wyf854861085/file-upload.git