AvueAvue
首页
  • 开发指南
  • Skill开发
  • 在线测试工具
  • Form组件
  • Crud组件
  • Default组件
  • Data组件
  • Component组件
产品
工作台
授权
联系
2.x文档
个人支付接口
首页
  • 开发指南
  • Skill开发
  • 在线测试工具
  • Form组件
  • Crud组件
  • Default组件
  • Data组件
  • Component组件
产品
工作台
授权
联系
2.x文档
个人支付接口
  • Article 文章
  • Group 分组容器
  • Title 标题文本
  • CountUp 数字动画
  • Comment 评论
  • Chat 客服聊天
  • Captcha 验证码
  • Contextmenu 右键菜单
  • Clipboard 复制文本
  • DialogForm 弹窗表单
  • Draggable 拖拽
  • Export Excel 导入与导出
  • Flow 流程
  • FilterBuilder 高级筛选
  • FileManager 文件管理
  • QRCode 二维码
  • Iframe 嵌入页面
  • ImagePreview 图片预览
  • Login 登录组件
  • License 授权书
  • Print 打印
  • Screenshot 页面截图
  • Search 标签搜索
  • Sign 电子签名
  • TextEllipsis 超出文本省略
  • Tabs 选项卡
  • Tree 树型
  • Video 摄像头
  • Calendar 日历组合示例

Export Excel 导入与导出

使用前提供 window.XLSX。当前插件支持普通数据和多级表头,样式导出可选择 xlsx-js-style;无需另外引入 FileSaver。

pnpm add xlsx-js-style
import * 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当前时间文本文档标题与默认文件名
filenametitle自动附加 .xlsx,不需要重复写扩展名
sheetNameSheet1工作表名称

返回 { 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
sheetIndex0工作表下标
headerRow0表头行下标,从 0 开始
rawfalsetrue 保留原始单元格值
defval空字符串空单元格默认值

导入后由业务代码验证必填值、类型及重复记录。详见全局 API。

最后更新:
贡献者: smallwei
Prev
Draggable 拖拽
Next
Flow 流程