更新记录

1.0.0(2026-10-27)

首个版本:流式写入 xlsx(零三方依赖)、多工作表、列格式(文本/整数/两位小数/金额/日期/日期时间/百分比)、列宽、表头样式、冻结首行、自动筛选、SUM 合计行、统一错误码与半成品清理;目标支持应用私有目录、可写路径与 SAF 文档 URI;UTS→Kotlin 编译通过,126 项核心逻辑与文件往返测试通过。


平台兼容性

uni-app

Vue2 Vue3 Chrome Safari app-vue app-nvue Android iOS 鸿蒙
× √ - - √ × √ - -
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
- - - - - - - - - - - -

企业报表 Excel 导出(流式 · 零依赖)

把业务数据写成标准 .xlsx,几十万行也不会把内存撑爆。不引 POI、不引 sheetjs,安装包体积不变,也不会和别的插件抢同名类。

这个插件解决什么问题

做过 App 端导出的都遇到过这几件事:

  • 用前端库拼 xlsx,几万行就把页面卡死,最后只能限制「最多导出 5000 条」;
  • 引 POI 之类的 Java 库,包体积涨几 MB,还容易和别的原生插件冲突;
  • 写死 /sdcard/xxx.xlsx,在 Android 10 以上直接失败;
  • 导出的表打开一看,金额是文本、日期是字符串,领导和财务还得手工改一遍。

本插件针对的正是这四件事:

老问题 本插件的做法
大表内存爆 边生成 XML 边写进 zip 条目,行数据不驻留内存,内存占用只与单批行数有关
三方库体积/冲突 手写 OOXML + java.util.zip,零三方依赖
分区存储限制 支持应用私有目录(零权限)、任意可写路径,以及调用方已授权的 SAF 目录 URI
打开后要手工改格式 金额、整数、百分比、日期按真正的数字格式写入,Excel 里能直接求和、排序、按日期筛选

适用场景

  • 设备点检、巡检、盘点结果导出给品质/财务做月度分析;
  • 后台任务执行结果、对账单、台账的「一键导出 Excel」;
  • 现场作业 App 把采集数据带走:导出后用微信/邮件发给办公电脑;
  • 需要多张表一起交付:明细一张、汇总一张、异常一张。

主要能力

  • 多工作表:一次导出一个文件内的多个 Sheet,名称自动清洗非法字符、去重校验;
  • 流式追加:appendRows 分批写入,单批上限 20 万行,建议 2000–5000 行一批;
  • 列格式:text / int / number / money / date / datetime / percent;
  • 表头样式:加粗、浅灰底、居中、下边框、行高 26;
  • 列宽:逐列指定,自动夹到 6–80 之间;
  • 冻结首行 + 自动筛选:表头固定,下拉筛选只覆盖数据行(不含合计行);
  • 合计行:写的是 SUM() 公式(不是算好的死值),Excel/WPS 打开后可随数据重算,同时带缓存值,未开启重算的阅读器也能立刻看到数字;
  • 统一错误码:失败返回 { ok:false, code, message },不抛异常给业务层;
  • 半成品清理:abortWorkbook 会关闭流并删除未完成的文件,不会给用户留一个打不开的 xlsx。

快速上手

import { openWorkbook, appendRows, closeWorkbook, getExportDir } from '@/uni_modules/szhzy-xlsx-export'

// 1. 准备目标位置:应用私有目录,不需要任何权限
const dir = getExportDir()
const opened = openWorkbook({
  dir: dir.dir,
  fileName: '设备点检记录_20261027.xlsx',
  freezeHeader: true,
  autoFilter: true,
  sheets: [
    {
      name: '点检明细',
      columns: [
        { title: '点检日期', width: 12, format: 'date' },
        { title: '设备编号', width: 14, format: 'text' },
        { title: '温度(℃)', width: 10, format: 'number' },
        { title: '停机分钟', width: 11, format: 'int' }
      ],
      summaryCols: [3],      // 对第 4 列生成 SUM 合计行
      summaryLabel: '合计'
    },
    {
      name: '异常汇总',
      columns: [
        { title: '设备编号', width: 14, format: 'text' },
        { title: '异常次数', width: 10, format: 'int' }
      ],
      summaryCols: [1]
    }
  ]
})
if (!opened.ok) {
  uni.showToast({ title: opened.message, icon: 'none' })
  return
}

// 2. 分批追加。日期列传 '2026-10-27' 或 '2026-10-27 13:30:00'
const token = opened.token
for (let i = 0; i < list.length; i += 3000) {
  const rows = list.slice(i, i + 3000).map((it) => [it.date, it.code, it.temp, it.downtime])
  const r = appendRows({ token, sheet: '点检明细', rows })
  if (!r.ok) {
    abortWorkbook({ token })
    uni.showToast({ title: r.message, icon: 'none' })
    return
  }
}

// 3. 换到第二张表继续写,然后收尾
appendRows({ token, sheet: '异常汇总', rows: abnormalRows })
const done = closeWorkbook({ token })
console.log(done.target, done.rows, done.bytes)

API

所有同步方法返回统一信封 { ok, code, message, ... }。

openWorkbook(options)

参数 类型 说明
path String 目标文件绝对路径,父目录不存在会自动创建
dir String 目标目录,配合 fileName 使用(与 path、uri 三选一)
uri String 目标文档的 content:// URI,需调用方已取得写权限
fileName String 文件名,缺省 export.xlsx,自动补 .xlsx
sheets Array 工作表定义,至少一个
freezeHeader Boolean 冻结首行,缺省 true
autoFilter Boolean 开启自动筛选,缺省 true

返回:{ ok, code, token, target, sheets }

appendRows(options)

参数 类型 说明
token String openWorkbook 返回的会话标识
rows Array 二维数组,元素顺序与表头列一致;支持字符串/数字/布尔/null
sheet String 可选,写入指定工作表;缺省写当前工作表

返回:{ ok, code, rows, totalRows, sheet }

closeWorkbook(options) / abortWorkbook(options) / getWorkbookInfo(options)

closeWorkbook 补齐合计行、关闭 zip 与文件流,返回 { ok, target, rows, bytes, sheets };调用后 token 失效。 abortWorkbook 中止并清理半成品文件。 getWorkbookInfo 查询当前已写入行数,可用于进度展示。

getExportDir() / buildFileName(prefix, stamp) / getVersion()

getExportDir 返回应用外部私有目录 exports/;buildFileName 生成带时间戳的合法文件名。

错误码

code 含义
NO_TARGET 未提供 path / dir / uri
TARGET_NOT_WRITABLE 目标目录不可写或授权已失效
INVALID_SHEET / DUPLICATE_SHEET 工作表名不合法或重名
TOO_MANY_COLUMNS 单表超过 200 列
SHEET_CLOSED 该工作表已写完,zip 条目只能顺序写
SESSION_NOT_FOUND token 无效(可能已完成或已中止)
ALREADY_FINISHED 会话已结束,不能继续追加
IO_ERROR 写入失败(空间不足、文件被占用等)

导出到「下载」目录怎么做

Android 10 以上不允许直接往 /sdcard/Download 拼路径写文件。要导出到用户可见的位置,正确姿势是让用户选一次目录,拿到目录授权后传 uri:

  1. 用系统目录选择器(ACTION_OPEN_DOCUMENT_TREE)让用户选目标目录并取持久授权;
  2. 在该目录下创建文档,拿到 content:// URI;
  3. 把 URI 传给 openWorkbook({ uri })。

本工作区的 szhzy-filebridge 已经封装了这套目录授权与新建文件,两个插件可以直接配合:filebridge 负责「选目录 + 建文件」,本插件负责「把数据流式写进去」。

平台与验证状态

平台支持:仅 Android App(6.0 / API 23 及以上)。iOS、鸿蒙、H5、小程序均不支持。

已完成的验证:

  • UTS → Kotlin 编译:HBuilderX 5.26 下 编译成功,产出 index.kt 1436 行;
  • 核心逻辑回归:126 项断言全部通过,覆盖日期序列号、数值解析、XML 转义、格式映射、工作表名与列校验、多表写入状态机、文件名处理;
  • 文件可打开性:用同一份生成逻辑在 Node 中拼出真实 .xlsx,交给 openpyxl 解析,核对工作表名、单元格类型(数字/日期/文本)、数字格式(yyyy-mm-dd、0.0%)、冻结窗格、自动筛选范围与 SUM 公式——全部符合预期。

未完成的验证(购买前请知悉):

  • 设备端文件流写入未在真机跑通:本版开发时测试机处于安全锁屏,无法进行真机点击操作,因此「点击导出 → 设备上生成文件 → 用 WPS 打开」这条链路未实测。平台层用的是标准 ZipOutputStream + FileOutputStream / ContentResolver.openOutputStream,逻辑上无特殊依赖,但请务必先在自有工程里试跑一次再上线;
  • 未在 WPS Office、Microsoft Excel 桌面版上做逐一比对(解析器验证用的是 openpyxl);
  • 未做 50 万行以上的极限压测,行数与耗时/体积的关系请以你自己的场景为准。

购买前必读

  1. 本插件只做导出(写),不读 Excel、不做 xlsx 解析;需要读取/导入请另配插件;
  2. 不提供单元格合并、图片、图表、条件格式、数据透视;
  3. 写入在调用线程同步执行,大表请分批 appendRows,并自行放到异步任务里,避免阻塞 UI;
  4. 一个文件内的多个工作表必须按声明顺序写入,写完的表不能再追加(zip 条目顺序限制,已用 SHEET_CLOSED 明确报错);
  5. 目标为 SAF 文档时,abortWorkbook 只关闭流不删除文件(避免误删用户既有文件);
  6. 源码授权版为完整源码,无加密、无联网校验、无授权服务端依赖。

关于数据与权限

  • 插件不联网、不采集任何数据;
  • 不声明任何存储权限:写应用私有目录用标准文件接口,写其它位置必须由用户通过系统选择器授权;
  • 导出的文件只落在调用方指定的位置,插件不复制、不上传。

更新日志

1.0.0

  • 首个版本:流式写入 xlsx(零三方依赖)、多工作表、列格式(文本/整数/两位小数/金额/日期/日期时间/百分比)、列宽、表头样式、冻结首行、自动筛选、SUM 合计行、统一错误码、半成品清理;
  • 目标位置支持应用私有目录、可写路径与 SAF 文档 URI;
  • 126 项核心逻辑与文件往返测试通过。

隐私、权限声明

1. 本插件需要申请的系统权限列表:

无。写入应用私有目录或公共下载目录时使用标准文件接口;写入其它位置需调用方先通过系统存储访问框架取得目录授权,插件不声明任何存储权限。

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

插件不采集、不上传任何数据,不访问网络。导出的表格只写入调用方指定的文件路径或用户通过系统选择器授权的目录,写入过程完全在本机完成。

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

无

暂无用户评论。