【Vue3】前端导入、导出功能

导入导出页(单表):

<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>

*该导入功能暂不支持多表头导入,若【导入模板】为多表头请勿导入数据或改为单表头后再进行导入。
*【导出】虽支持多表头导出,但需要根据需求更改【加入表头数据】和【合并单元格】代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值