更新记录
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:
- 用系统目录选择器(
ACTION_OPEN_DOCUMENT_TREE)让用户选目标目录并取持久授权; - 在该目录下创建文档,拿到
content://URI; - 把 URI 传给
openWorkbook({ uri })。
本工作区的 szhzy-filebridge 已经封装了这套目录授权与新建文件,两个插件可以直接配合:filebridge 负责「选目录 + 建文件」,本插件负责「把数据流式写进去」。
平台与验证状态
平台支持:仅 Android App(6.0 / API 23 及以上)。iOS、鸿蒙、H5、小程序均不支持。
已完成的验证:
- UTS → Kotlin 编译:HBuilderX 5.26 下
编译成功,产出index.kt1436 行; - 核心逻辑回归:126 项断言全部通过,覆盖日期序列号、数值解析、XML 转义、格式映射、工作表名与列校验、多表写入状态机、文件名处理;
- 文件可打开性:用同一份生成逻辑在 Node 中拼出真实
.xlsx,交给 openpyxl 解析,核对工作表名、单元格类型(数字/日期/文本)、数字格式(yyyy-mm-dd、0.0%)、冻结窗格、自动筛选范围与SUM公式——全部符合预期。
未完成的验证(购买前请知悉):
- 设备端文件流写入未在真机跑通:本版开发时测试机处于安全锁屏,无法进行真机点击操作,因此「点击导出 → 设备上生成文件 → 用 WPS 打开」这条链路未实测。平台层用的是标准
ZipOutputStream+FileOutputStream/ContentResolver.openOutputStream,逻辑上无特殊依赖,但请务必先在自有工程里试跑一次再上线; - 未在 WPS Office、Microsoft Excel 桌面版上做逐一比对(解析器验证用的是 openpyxl);
- 未做 50 万行以上的极限压测,行数与耗时/体积的关系请以你自己的场景为准。
购买前必读
- 本插件只做导出(写),不读 Excel、不做 xlsx 解析;需要读取/导入请另配插件;
- 不提供单元格合并、图片、图表、条件格式、数据透视;
- 写入在调用线程同步执行,大表请分批
appendRows,并自行放到异步任务里,避免阻塞 UI; - 一个文件内的多个工作表必须按声明顺序写入,写完的表不能再追加(zip 条目顺序限制,已用
SHEET_CLOSED明确报错); - 目标为 SAF 文档时,
abortWorkbook只关闭流不删除文件(避免误删用户既有文件); - 源码授权版为完整源码,无加密、无联网校验、无授权服务端依赖。
关于数据与权限
- 插件不联网、不采集任何数据;
- 不声明任何存储权限:写应用私有目录用标准文件接口,写其它位置必须由用户通过系统选择器授权;
- 导出的文件只落在调用方指定的位置,插件不复制、不上传。
更新日志
1.0.0
- 首个版本:流式写入 xlsx(零三方依赖)、多工作表、列格式(文本/整数/两位小数/金额/日期/日期时间/百分比)、列宽、表头样式、冻结首行、自动筛选、SUM 合计行、统一错误码、半成品清理;
- 目标位置支持应用私有目录、可写路径与 SAF 文档 URI;
- 126 项核心逻辑与文件往返测试通过。

收藏人数:
购买源码授权版(
试用
赞赏(0)
下载 13
赞赏 0
下载 12718207
赞赏 1958
赞赏
京公网安备:11010802035340号