Export Excel 导入与导出
使用前提供 window.XLSX。当前插件支持普通数据和多级表头,样式导出可选择 xlsx-js-style;无需另外引入 FileSaver。
pnpm add xlsx-js-styleimport * as XLSX from 'xlsx-js-style'
window.XLSX = XLSX文档站已提供 XLSX。导出和读取都返回 Promise,例子会展示成功结果或错误。
Excel 导出
效果预览可直接操作下方示例
下载普通名单或多级表头名单,成功后显示文件名和实际导出条数。
正在加载示例…
<template>
<div class="excel-export-demo">
<div class="excel-export-demo__actions">
<el-button type="primary" :loading="loading === 'single'" :disabled="!!loading" @click="exportExcel('single')">
下载单级表头名单
</el-button>
<el-button :loading="loading === 'grouped'" :disabled="!!loading" @click="exportExcel('grouped')">
下载多级表头名单
</el-button>
</div>
<p>两份文件使用相同的 {{ records.length }} 条数据;多级表头将姓名和年龄归入“基本信息”。</p>
<el-alert v-if="error" :title="error" type="error" :closable="false" show-icon />
<el-alert v-else-if="result" title="文件已生成并触发浏览器下载" type="success" :closable="false" show-icon />
<el-alert v-else title="选择一种表头结构,查看导出结果。" type="info" :closable="false" show-icon />
<dl v-if="result" class="excel-export-demo__result" aria-live="polite">
<dt>文件名</dt><dd>{{ result.filename }}</dd>
<dt>工作表</dt><dd>{{ result.sheetName }}</dd>
<dt>导出条数</dt><dd>{{ result.rows }}</dd>
</dl>
</div>
</template>
<script setup>
import { onBeforeUnmount, ref } from 'vue';
import { $Export } from '@smallwei/avue';
const loading = ref('');
const result = ref(null);
const error = ref('');
const records = [
{ id: '001', name: '张三', age: 26 },
{ id: '002', name: '李四', age: 31 },
{ id: '003', name: '王五', age: 24 },
];
const columns = [
{ label: '编号', prop: 'id' },
{ label: '姓名', prop: 'name' },
{ label: '年龄', prop: 'age' },
];
let disposed = false;
onBeforeUnmount(() => { disposed = true; });
async function exportExcel(kind) {
if (loading.value) return;
loading.value = kind;
result.value = null;
error.value = '';
try {
const exported = await $Export.excel({
title: '成员名单',
filename: kind === 'grouped' ? '成员名单-多级表头' : '成员名单-单级表头',
sheetName: '成员名单',
columns: kind === 'grouped'
? [columns[0], { label: '基本信息', children: columns.slice(1) }]
: columns,
data: records,
});
if (!disposed) result.value = exported;
} catch (cause) {
if (!disposed) error.value = cause instanceof Error ? cause.message : 'Excel 导出失败,请重试。';
} finally {
if (!disposed) loading.value = '';
}
}
</script>
<style scoped>
.excel-export-demo { min-width: 0; }
.excel-export-demo__actions { display: flex; flex-wrap: wrap; gap: 12px; }
.excel-export-demo__actions .el-button { margin-left: 0; }
.excel-export-demo__result {
display: grid;
grid-template-columns: max-content minmax(0, 1fr);
gap: 8px 16px;
padding: 16px;
border-radius: 8px;
background: var(--el-fill-color-light);
font-size: 14px;
}
.excel-export-demo__result dt { color: var(--el-text-color-secondary); }
.excel-export-demo__result dd { margin: 0; overflow-wrap: anywhere; }
</style>
| 参数 | 默认值 | 说明 |
|---|---|---|
columns | [] | 至少一列;每列 label/prop,children 用于多级表头 |
data | [] | 记录数组;按列 prop 取值 |
title | 当前时间文本 | 文档标题与默认文件名 |
filename | title | 自动附加 .xlsx,不需要重复写扩展名 |
sheetName | Sheet1 | 工作表名称 |
返回 { filename, sheetName, rows }。字典编码不会自动转标签,需要导出标签时先转换数据。
Excel 导入
效果预览可直接操作下方示例
先下载本例模板,再选择该文件。文件只在浏览器中解析,不上传;表头「编号 / 姓名 / 年龄」会映射为业务字段。
正在加载示例…
<template>
<div class="excel-import-demo">
<div class="excel-import-demo__actions">
<el-button :loading="downloading" :disabled="busy" @click="downloadTemplate">下载导入模板</el-button>
<el-upload ref="uploadRef" :auto-upload="false" :show-file-list="false" :disabled="busy"
accept=".xlsx,.xls" :on-change="importFile">
<el-button type="primary" :loading="parsing" :disabled="busy">选择 Excel 文件</el-button>
</el-upload>
</div>
<p>首行为“编号 / 姓名 / 年龄”,编号按文本保存。文件仅在浏览器解析,每次导入替换下方结果。</p>
<el-alert :title="notice.message" :type="notice.type" :closable="false" show-icon />
<p v-if="summary" class="excel-import-demo__summary" aria-live="polite">
{{ summary.filename }} · 工作表:{{ summary.sheetName }} · {{ list.length }} 条记录
</p>
<div class="excel-import-demo__content">
<el-skeleton v-if="parsing" :rows="3" animated />
<avue-crud v-else-if="list.length" :option="option" :data="list" />
<el-empty v-else :description="emptyText" :image-size="64" />
</div>
</div>
</template>
<script setup>
import { computed, onBeforeUnmount, ref } from 'vue';
import { $Export } from '@smallwei/avue';
const uploadRef = ref(null);
const list = ref([]);
const parsing = ref(false);
const downloading = ref(false);
const busy = computed(() => parsing.value || downloading.value);
const summary = ref(null);
const emptyText = ref('尚未导入数据');
const notice = ref({ type: 'info', message: '先下载模板,再选择填写后的 Excel 文件。' });
const columns = [
{ label: '编号', prop: 'id' },
{ label: '姓名', prop: 'name' },
{ label: '年龄', prop: 'age' },
];
const option = {
rowKey: 'id',
addBtn: false,
menu: false,
refreshBtn: false,
columnBtn: false,
searchShowBtn: false,
border: true,
column: columns,
};
let disposed = false;
onBeforeUnmount(() => { disposed = true; });
async function downloadTemplate() {
if (busy.value) return;
downloading.value = true;
notice.value = { type: 'info', message: '正在生成导入模板…' };
try {
const exported = await $Export.excel({
title: '成员导入模板',
filename: '成员导入模板',
sheetName: '成员名单',
columns,
data: [{ id: '001', name: '张三', age: 26 }, { id: '002', name: '李四', age: 31 }],
});
if (!disposed) notice.value = { type: 'success', message: `已生成 ${exported.filename},含 ${exported.rows} 条示范数据;可修改或删除示范行。` };
} catch (cause) {
if (!disposed) notice.value = { type: 'error', message: cause instanceof Error ? cause.message : '模板生成失败,请重试。' };
} finally {
if (!disposed) downloading.value = false;
}
}
async function importFile(uploadFile) {
if (busy.value) return;
parsing.value = true;
list.value = [];
summary.value = null;
emptyText.value = '尚无可展示的导入结果';
notice.value = { type: 'info', message: '正在读取并校验本地文件…' };
try {
const file = uploadFile.raw;
if (!(file instanceof File)) throw new Error('未取得本地文件,请重新选择。');
// raw: false 保留格式化文本,例如编号 001;列名来自首行。
const parsed = await $Export.xlsx(file, { headerRow: 0, raw: false, defval: '' });
if (disposed) return;
const required = columns.map((column) => column.label);
const missing = required.filter((label) => !parsed.header.includes(label));
if (missing.length) throw new Error(`缺少必需列:${missing.join('、')}。请使用本例模板。`);
const duplicate = required.filter((label) => parsed.header.filter((value) => value === label).length > 1);
if (duplicate.length) throw new Error(`表头重复:${duplicate.join('、')}。每个必需列只能出现一次。`);
const ids = new Set();
// 插件不会自动将中文表头转成业务 prop,需要显式映射并校验。
const records = parsed.results.map((row, index) => {
const id = String(row['编号'] ?? '').trim();
const name = String(row['姓名'] ?? '').trim();
const ageText = String(row['年龄'] ?? '').trim();
const age = Number(ageText);
if (!id || !name || !ageText) throw new Error(`第 ${index + 1} 条记录的编号、姓名和年龄均需填写。`);
if (!Number.isInteger(age) || age < 0) throw new Error(`第 ${index + 1} 条记录的年龄必须是非负整数。`);
if (ids.has(id)) throw new Error(`编号 ${id} 重复,请修改后重新导入。`);
ids.add(id);
return { id, name, age };
});
list.value = records;
summary.value = { filename: file.name, sheetName: parsed.sheetName };
emptyText.value = '表头正确,工作表中没有数据行';
notice.value = records.length
? { type: 'success', message: `导入成功,已映射并校验 ${records.length} 条记录。` }
: { type: 'info', message: '文件解析成功,但没有数据行。填写模板后可再次导入。' };
} catch (cause) {
if (!disposed) {
emptyText.value = '本次导入未通过校验';
notice.value = { type: 'error', message: cause instanceof Error ? cause.message : 'Excel 读取失败,请检查文件后重试。' };
}
} finally {
if (!disposed) {
parsing.value = false;
uploadRef.value?.clearFiles();
}
}
}
</script>
<style scoped>
.excel-import-demo { min-width: 0; }
.excel-import-demo__actions { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; }
.excel-import-demo__actions .el-button { margin-left: 0; }
.excel-import-demo__content { margin-top: 16px; }
.excel-import-demo__summary { color: var(--el-text-color-secondary); overflow-wrap: anywhere; }
</style>
$Export.xlsx(file, options) 的 file 必须是 File;返回 { header, results, sheetName },results 使用 Excel 表头作为对象键。
| 读取配置 | 默认值 | 说明 |
|---|---|---|
sheetName | 未设置 | 指定工作表名称,优先于 sheetIndex |
sheetIndex | 0 | 工作表下标 |
headerRow | 0 | 表头行下标,从 0 开始 |
raw | false | true 保留原始单元格值 |
defval | 空字符串 | 空单元格默认值 |
导入后由业务代码验证必填值、类型及重复记录。详见全局 API。
