导入导出页(单表):
<template>
<!-- 导入弹窗 -->
<uploadExcel :drColumns="drColumns" @import-success="finishImport" ref="importRef" />
<!-- 导入按钮 -->
<n-button type="primary" @click="openImport">
<template #icon>
<n-icon>
<ImportantDevicesOutlined />
</n-icon>
</template>
导入
</n-button>
<!-- 下载导入模板(导出)按钮 -->
<n-button type="info" @click="downloadTemplate" :disabled="exportLoading">
<template #icon>
<n-icon>
<CloudDownloadOutlined />
</n-icon>
</template>
<!-- {{ exportLoading ? '导出中...' : '导出'}} -->
{{ exportLoading ? '下载导入模板中...' : '下载导入模板'}}
</n-button>
<!-- 表格数据 -->
<n-data-table striped :columns="columns" :data="data" :single-line="false" />
</template>
<script lang="ts" setup>
// 引入 vue 组件
import { ref, onMounted } from 'vue';
import { useMessage } from 'naive-ui'
import { ImportantDevicesOutlined, CloudDownloadOutlined } from '@vicons/material'
// 引入导入弹窗组件
import uploadExcel from '@/components/uploadExcel/uploadGlobal.vue'
// 引入导出组件(下载导入模板)
import * as XLSX from 'xlsx';
import { saveAs } from 'file-saver';
// 导入成功、失败提示消息
const message = useMessage()
// 表格数据
const data = ref([
{
bt: '该列必须要填,否则导入报错',
fbt: '该列可以不填,且该列导出后隐藏',
rq: '2024-12-31',
sj: '2024-12-31 17:41:56',
yf: '2024-12',
},
])
const columns = ref < any > ([
{ title: '必填', key: 'bt', width: 120, ellipsis: { tooltip: true } },
{ title: '非必填', key: 'fbt', width: 120, ellipsis: { tooltip: true } },
{
title: '父节点',
key: 'father',
align: 'center',
children: [
{ title: '日期', key: 'rq', width: 100 },
{ title: '时间', key: 'sj', width: 100 },
{ title: '月份', key: 'yf', width: 80 }
]
}
])
// 导入弹窗
const importRef = ref();
// 导入字段
const drColumns = ref < any > ([
{ title: '*必填', key: 'bt', width: 120, ellipsis: { tooltip: true }, required: true },
{ title: '非必填', key: 'fbt', width: 120, ellipsis: { tooltip: true }, required: false },
// 加入表头数据(若需导出两行表头的情况,注释以下代码)
{ title: '*日期', key: 'rq', width: 100, required: true, type: 'date' },
{ title: '*时间', key: 'sj', width: 100, required: true, type: 'datetime' },
{ title: '*月份', key: 'yf', width: 80, required: true, type: 'month' }
// 若需要导出两行表头的情况打开以下注释(打开后导出方法【downloadTemplate】需要 *合并单元格* )
// {
// title: '父节点',
// key: 'father',
// align: 'center',
// children: [
// { title: '*日期', key: 'rq', width: 100, required: true, type: 'date' },
// { title: '*时间', key: 'sj', width: 100, required: true, type: 'datetime' },
// { title: '*月份', key: 'yf', width: 80, required: true, type: 'month' }
// ]
// }
])
// 导出字段键名
const exportKeys = ref<any>({
cnKeys: [], // 中文键名
enKeys: [] // 英文键名
})
// 导出加载
const exportLoading = ref(false)
// 初始化加载
onMounted(() => {
getKeysData(drColumns.value)
})
// 获取导出表格字段的键名
function getKeysData(arr: any){
arr.forEach( (item: any) => {
if(item.children){
getKeysData(item.children)
}else{
exportKeys.value.cnKeys.push(item.title)
exportKeys.value.enKeys.push(item.key)
}
})
}
// 打开导入弹窗
function openImport() {
importRef.value.importModal = true
}
// 导入成功关闭弹窗并刷新表格
function finishImport(arr: any) {
// 每条数据是否有要更改的字段
arr.forEach((item: any) => {
item.fbt = '我可以是空数据'
})
// 新增接口(可根据后端需要的数据打开注释后进行更改)
// saveBasicData({ changedRows: arr }).then((res: any) => {
// // 判断是否有报错(可根据后端传的数据进行更改或注释该行)
// if (!res.data.success) return message.error(res.data.msg)
// 上传成功,清空上传列表、关闭弹窗
importRef.value.fileList = [];
importRef.value.importModal = false;
// 并刷新表格(根据需求重新调查询接口)
// refreshTable()
message.success('导入成功')
// }).catch((err:any) => {
// message.error('导入失败')
// })
}
// 下载导入模板(导出)
function downloadTemplate() {
// 查询表格数据
exportLoading.value = true;
// 导出数组
const exportArr: any = [];
// 加入表头数据(若需导出两行表头的情况,注释以下代码)
exportArr.push(exportKeys.value.cnKeys)
// 若需要导出两行表头的情况打开以下注释(后打开 *合并单元格数据* 注释)
// let title1: any = [];
// let title2: any = [];
// drColumns.value.forEach((item: any) => {
// if(item.children){
// // 有子节点,第一行加入父节点标题并加入【子节点长度-1】的空值,第二行加入子节点标题
// item.children.forEach( (value: any, index: any) => {
// index == 0 ? title1.push(item.title) : title1.push('')
// title2.push(value.title)
// })
// }else{
// // 无子节点,第一行加入标题,第二行加入空值
// title1.push(item.title)
// title2.push('')
// }
// })
// exportArr.push(title1)
// exportArr.push(title2)
// 加入表格数据(若需导出两行的情况,注释以下代码)
data.value.forEach( (item:any) => {
let dataArr: any = [];
exportKeys.value.enKeys.forEach( (key: any) => {
dataArr.push(item[key])
})
exportArr.push(dataArr)
})
// 将数据转换为工作表
const worksheet = XLSX.utils.aoa_to_sheet(exportArr);
// 合并单元格数据(若表格有多行表头【父子节点】,打开下列代码)
// worksheet['!merges'] = [
// // 静态合并
// { s: { r: 0, c: 0 }, e: { r: 1, c: 0 } }, // 合并范围从 A1 到 A2(必填)
// { s: { r: 0, c: 1 }, e: { r: 1, c: 1 } }, // 合并范围从 B1 到 B2(非必填)
// { s: { r: 0, c: 2 }, e: { r: 0, c: 4 } }, // 合并范围从 C1 到 E1(父节点)
// ]
// 隐藏非必填列(若无需隐藏列,注释下列代码)
worksheet['!cols'] = [
{}, // 第一列(必填)不隐藏
{hidden: true}, // 第二列(非必填)隐藏
{},{},{} // 父节点不隐藏
];
// 创建工作簿并添加工作表
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
// 生成Excel文件
const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
// 使用blob和FileReader创建一个Blob URL
const dataBlob = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8' });
const blobUrl = window.URL.createObjectURL(dataBlob);
// 使用saveAs下载文件
saveAs(dataBlob, '导入模板.xlsx');
// 清理
window.URL.revokeObjectURL(blobUrl);
exportLoading.value = false;
}
</script>
导入组件(路径要和上方保持一致,此处为【@/components/uploadExcel.vue】)
<template>
<!------------------------------ 【单表页面】导入Excel文件 ------------------------------->
<n-modal title="导入" v-model:show="importModal" preset="card" style="width: 1200px">
<!-- 文件上传列表 -->
<n-upload
max="1"
accept=".xlsx, .xls"
:show-retry-button="false"
v-model:file-list="fileList"
@change="beforeUpload"
@remove="errmsgList = []"
>
<n-upload-dragger>
<div style="margin-bottom: 12px">
<n-icon size="48" :depth="3">
<ArchiveOutlined />
</n-icon>
</div>
<n-text style="font-size: 16px">
点击或拖动Excel(.xlsx、.xls)文件到该区域
</n-text>
<n-p depth="3" style="margin: 8px 0 0 0">
请确保Excel文件的文件大小不超过100MB
</n-p>
</n-upload-dragger>
</n-upload>
<!-- 错误信息列表 -->
<n-card title="校验报错数据" v-if="errmsgList.length > 0">
<n-data-table style="height: 460px;" :columns="drColumns" :data="errmsgList" max-height="calc(100% - 40px)" size="small" :scroll-x="scrollX" />
</n-card>
</n-modal>
</template>
<script lang="ts" setup>
import { defineEmits, defineProps, defineExpose, ref, nextTick } from 'vue';
import { useMessage, UploadFileInfo } from 'naive-ui'
import { ArchiveOutlined } from '@vicons/material'
// Excel文件解析
import * as XLSX from 'xlsx';
import dayjs from "dayjs";
const message = useMessage()
// 表格字段
const props = defineProps(['drColumns'])
// 上传成功,关闭弹窗
const emit = defineEmits(['importSuccess'])
// 导入窗口
const importModal = ref(false)
// 上传文件列表
const fileList = ref<UploadFileInfo[]>([])
// 错误信息列表
const errmsgList = ref<Array<any>>([])
// 表格宽度
const scrollX = ref(getTableWidth(props.drColumns, 0))
// 计算表格宽度
function getTableWidth(arr: any, preWidth: any){
var addWidth = preWidth;
arr.forEach((item: any) => {
if(item.children){
addWidth += getTableWidth(item.children, 0);
}else{
addWidth += item.width;
}
})
return addWidth;
}
// 上传文件之前检查文件类型和大小
function beforeUpload(e: any){
// console.log(e.file, e.fileList)
const isExcel = /\.(xlsx|xls)$/.test(e.file.name);
const limitSize = e.file.file.size / 1024 / 1024 < 100;
if ( !isExcel ) {
message.error( "请上传Excel(.xlsx、.xls)文件")
nextTick(() => {
fileList.value = [];
})
}else if ( !limitSize ) {
message.error( "文件大小不能超过100MB")
nextTick(() => {
fileList.value[0].status = 'error'
})
}else if(e.fileList.length > 0){
if (e.file && e.file.file && e.file.file.size > 0) {
const reader = new FileReader();
reader.onload = (event:any) => {
const data = new Uint8Array(event.target.result);
const workbook = XLSX.read(data, { type: 'array' });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet);
// jsonData现在是Excel文件的JSON表示,可以根据需要进行处理
const updateArr:any = []; // 导入数据
const errmsgArr: any = []; // 错误数据
jsonData.forEach((item:any) => {
// 判断【示例数据】直接跳过当前循环(若模板中不存在示例数据可注释该行)
if(item["备注"] && item["备注"].includes('示例')) return;
// 判断是否有错误数据
let flag = true;
const rowData = props.drColumns.reduce((obj:any, value:any) => {
// 判断空值和日期(日期需要判断是否为数字类型),其他都转成字符串
if(!item[value.title] && item[value.title]!=0){
obj[value.key] = null;
}else if(item[value.title] && (value.type=='date' || value.type=='datetime' || value.type == 'month') && typeof(item[value.title]) === "number"){
// excel表导出的时间为天数,需要转换为时间戳 --js日期从1970年开始,Excel表从1900年开始算[1900-1970年有25567+2天错位]
var timeStamp = (item[value.title] - 25569) * 24 * 60 * 60 * 1000;
if(value.type == 'date'){
obj[value.key] = dayjs(timeStamp).format('YYYY-MM-DD');
}else if(value.type == 'datetime'){
obj[value.key] = dayjs(timeStamp).format('YYYY-MM-DD HH:mm:ss');
}else if(value.type == 'month'){
obj[value.key] = dayjs(timeStamp).format('YYYY-MM');
}
}else{
obj[value.key] = item[value.title].toString().trim()
}
// 判断是否为必填项
if(value.required && !obj[value.key]){
flag = false
}
return obj;
}, {});
flag ? updateArr.push(rowData) : errmsgArr.push(rowData)
})
// 若有错误数据,展示错误数据
if(errmsgArr.length > 0){
message.error('导入数据出错,请检查表格字段是否填写完整!');
fileList.value[0].status = 'error';
errmsgList.value = errmsgArr;
}else if(updateArr.length > 0){
emit('importSuccess', updateArr)
}else{
message.warning('没有检测到导入数据,请检查文件!');
}
};
reader.readAsArrayBuffer(e.file.file);
}
}
}
defineExpose({ importModal, fileList })
</script>
*该导入功能暂不支持多表头导入,若【导入模板】为多表头请勿导入数据或改为单表头后再进行导入。
*【导出】虽支持多表头导出,但需要根据需求更改【加入表头数据】和【合并单元格】代码。