在企业级应用场景中,除了数据导出,模板化导入是另一个核心需求。本文将深入讲解如何基于Vue3 + Ant Design Vue + xlsx技术栈,实现以下高级导入功能:
通过模板引擎设计,解决传统导入功能存在的三大痛点:
|--- 模板文件(template.xlsx)
|--- 元数据工作表(MetaSheet)
| A1: 模板版本号
| A2: 字段映射规则
|--- 数据工作表(DataSheet)
| A1: 姓名(必填)
| B1: 部门(下拉选择:技术部/市场部/财务部)
| C1: 薪资(数字格式,保留两位小数)
| D1: 入职日期(日期格式:YYYY-MM-DD)
const FIELD_MAP = {
姓名: 'name',
部门: 'department',
薪资: 'salary',
入职日期: 'joinDate'
}
<template>
<a-upload-dragger
:before-upload="handleBeforeUpload"
:custom-request="handleCustomRequest"
accept=".xlsx,.xls"
>
<p class="ant-upload-drag-icon">
<InboxOutlined />
p>
<p class="ant-upload-text">点击或拖拽文件上传p>
<p class="ant-upload-hint">
仅支持.xlsx和.xls格式的自定义模板文件
p>
a-upload-dragger>
<a-table
v-if="errorList.length > 0"
:columns="errorColumns"
:data-source="errorList"
row-key="rowIndex"
style="margin-top: 24px"
/>
template>
<script setup>
import { ref } from 'vue'
import * as XLSX from 'xlsx/xlsx.mjs'
import { InboxOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
const errorList = ref([])
const errorColumns = [
{ title: '行号', dataIndex: 'rowIndex' },
{ title: '字段', dataIndex: 'field' },
{ title: '错误信息', dataIndex: 'message' }
]
const handleCustomRequest = async ({ file }) => {
try {
const workbook = XLSX.read(await file.arrayBuffer(), { type: 'array' })
validateTemplate(workbook)
const parsedData = parseDataSheet(workbook)
await validateData(parsedData)
message.success('文件校验通过,准备提交数据')
// 提交数据到服务端...
} catch (error) {
handleParseError(error)
}
}
script>
const validateTemplate = (workbook) => {
// 校验元数据工作表
if (!workbook.SheetNames.includes('MetaSheet')) {
throw new Error('缺少元数据工作表')
}
const metaSheet = workbook.Sheets.MetaSheet
const templateVersion = getCellValue(metaSheet, 'A1')
if (templateVersion !== '1.0.0') {
throw new Error(`模板版本不匹配,当前版本:${templateVersion},需要版本:1.0.0`)
}
// 校验数据工作表
if (!workbook.SheetNames.includes('DataSheet')) {
throw new Error('缺少数据工作表')
}
}
const getCellValue = (sheet, cellRef) => {
const cellAddress = XLSX.utils.decode_cell(cellRef)
const cell = sheet[XLSX.utils.encode_cell(cellAddress)]
return cell ? cell.v : undefined
}
const parseDataSheet = (workbook) => {
const dataSheet = workbook.Sheets.DataSheet
const headerRow = XLSX.utils.sheet_to_json(dataSheet, { header: 1, range: 1 })[0]
const dataRows = XLSX.utils.sheet_to_json(dataSheet, { header: 1, range: 2, defval: '' })
return dataRows.map((row, index) => {
const record = {}
headerRow.forEach((field, colIndex) => {
record[FIELD_MAP[field] || field] = row[colIndex]
})
return { ...record, rowIndex: index + 2 } // 记录原始行号
})
}
const validateData = async (data) => {
const schema = {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string', minLength: 2 },
department: { enum: ['技术部', '市场部', '财务部'] },
salary: { type: 'number', minimum: 3000 },
joinDate: { type: 'string', format: 'date' }
},
required: ['name', 'department', 'salary']
}
}
// 使用ajv进行校验
const Ajv = require('ajv')
const ajv = new Ajv()
const validate = ajv.compile(schema)
for (const [index, item] of data.entries()) {
const valid = validate(item)
if (!valid) {
const errors = validate.errors.map(err => ({
rowIndex: item.rowIndex,
field: err.instancePath.slice(1),
message: err.message
}))
errorList.value.push(...errors)
}
}
if (errorList.value.length > 0) {
throw new Error('存在校验错误')
}
}
// 模板版本升级策略
const handleTemplateUpgrade = (workbook) => {
const currentVersion = getCellValue(workbook.Sheets.MetaSheet, 'A1')
if (currentVersion === '1.0.0') return workbook
// 添加新字段列
const dataSheet = workbook.Sheets.DataSheet
XLSX.utils.sheet_add_aoa(dataSheet, [['邮箱']], { origin: 'E1' })
// 更新元数据
dataSheet['E1'].v = '邮箱'
metaSheet['A1'].v = '1.1.0'
return workbook
}
// 流式读取配置
const readOptions = {
sheetRows: 100, // 每次读取100行
cellDates: true // 保持日期格式
}
const streamParse = (workbookReader) => {
return new Promise((resolve, reject) => {
const data = []
let currentSheet = null
workbookReader.on('sheet', (sheetName) => {
currentSheet = sheetName
})
workbookReader.on('row', (row) => {
if (currentSheet === 'DataSheet') {
data.push(row)
}
})
workbookReader.on('end', () => {
resolve(data)
})
workbookReader.on('error', (err) => {
reject(err)
})
})
}
<template>
<a-table
:columns="previewColumns"
:data-source="previewData"
row-key="rowIndex"
:scroll="{ y: 400 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'status'">
<a-tag :color="record.status === 'valid' ? 'green' : 'red'">
{{ record.status === 'valid' ? '通过' : '失败' }}
a-tag>
template>
<template v-else-if="column.dataIndex === 'error'">
<a-tooltip :title="record.error">
<ExclamationCircleOutlined v-if="record.error" />
a-tooltip>
template>
template>
a-table>
template>
<script setup>
const previewColumns = [
{ title: '行号', dataIndex: 'rowIndex' },
{ title: '姓名', dataIndex: 'name' },
{ title: '状态', dataIndex: 'status' },
{ title: '错误信息', dataIndex: 'error', width: 200 }
]
script>
通过本文实现的自定义模板导入方案,可获得以下提升: