更新记录
1.0.0(2026-09-22)
- 初始版本
- 支持 Android 平台
- 支持读取
.xlsx/.xls - 支持覆盖写入与追加写入(
overwrite/append)
平台兼容性
uni-app(4.72)
| Vue2 | Vue2插件版本 | Vue3 | Vue3插件版本 | Chrome | Safari | app-vue | app-vue插件版本 | app-nvue | app-nvue插件版本 | Android | Android插件版本 | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| √ | 1.0.0 | √ | 1.0.0 | × | × | √ | 1.0.0 | √ | 1.0.0 | 5.0 | 1.0.0 | × | × |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| × | × | × | × | × | × | × | × | × | - | × | × |
uni-app x(4.72)
| Chrome | Safari | Android | Android插件版本 | iOS | 鸿蒙 | 微信小程序 |
|---|---|---|---|---|---|---|
| × | × | 5.0 | 1.0.0 | × | × | × |
zy-read-write-excel
读写 Excel(.xlsx / .xls)UTS 插件(Android)。支持读取各 Sheet 的二维数据、按名称或下标筛选、
覆盖写入与追加写入、多 Sheet 写入、自定义日期格式。文件默认保存在应用私有目录,无需任何存储权限。
目录
特性
- 读取:
.xlsx/.xls读取,返回各 Sheet 的二维行数据(rows[r][c]对应第 r 行第 c 列) - 筛选:可按 Sheet 名称或下标只读一个 Sheet,不指定时读取全部
- 读取选项:
asText全部转显示文本、maxRows限制行数、dateFormat自定义日期格式 - 写入:覆盖(
overwrite)与追加(append)两种模式;支持多 Sheet,也支持rows单 Sheet 简写 - 格式:按文件后缀自动选择格式,
.xls与.xlsx均可 - 路径:支持绝对路径、
file://前缀、content://URI(读取)、相对文件名(应用私有目录) - 无需权限:不传路径时自动保存到应用私有目录,读写都不需要存储权限
- 统一回调:全部 API 支持
success/fail/complete,失败为UniError
平台支持
| 平台 | 支持 |
|---|---|
| Android | ✓ |
| iOS | ✗ |
| Harmony | ✗ |
| H5 | ✗ |
| 小程序 | ✗ |
兼容性
| 项目 | 要求 |
|---|---|
| 最低系统版本 | Android 5.0(API 21) |
| 支持的宿主 | uni-app(vue2 / vue3)、uni-app x |
安装与引入
插件目录名 / 插件 id 为 zy-read-write-excel,通过 uni_modules 引入:
import {
readExcel,
writeExcel,
getExcelWorkDir
} from '@/uni_modules/zy-read-write-excel'
示例代码
<template>
<view class="container">
<view class="header">
<text class="title">Excel 读写</text>
<text class="subtitle">zy-read-write-excel · xlsx / xls</text>
</view>
<!-- 工作目录 -->
<view class="section">
<view class="section-title">工作目录</view>
<view class="path-text">{{ workDir || '未获取' }}</view>
<button type="default" size="mini" @click="loadWorkDir">获取工作目录</button>
</view>
<!-- 写入 -->
<view class="section">
<text class="section-title">写入 Excel</text>
<view class="form-row">
<text class="label">文件名</text>
<input class="input" v-model="fileName" placeholder="demo.xlsx" />
</view>
<view class="form-row">
<text class="label">模式</text>
<radio-group class="radio-group" @change="onModeChange">
<label class="radio-label">
<radio value="overwrite" :checked="mode === 'overwrite'" />覆盖
</label>
<label class="radio-label">
<radio value="append" :checked="mode === 'append'" />追加
</label>
</radio-group>
</view>
<view class="button-group">
<button type="primary" size="mini" :loading="writing" @click="doWrite">写入</button>
<button type="default" size="mini" :loading="reading" @click="doRead">读取</button>
</view>
</view>
<!-- 读取结果 -->
<view class="section" v-if="sheets.length > 0">
<text class="section-title">读取结果({{ sheets.length }} 个 Sheet)</text>
<view class="sheet-block" v-for="(sheet, si) in sheets" :key="si">
<text class="sheet-name">[{{ sheet.index }}] {{ sheet.name }}</text>
<text class="sheet-meta">{{ sheet.rowCount }} 行 × {{ sheet.columnCount }} 列
<text v-if="sheet.truncated" class="truncated">(已截断)</text>
</text>
<view class="table" v-for="(row, ri) in sheet.rows" :key="ri">
<text class="cell" v-for="(cell, ci) in row" :key="ci">{{ cell }}</text>
</view>
</view>
</view>
<!-- 日志 -->
<view class="section">
<view class="section-title">日志</view>
<view class="log-text">{{ log }}</view>
</view>
</view>
</template>
<script>
import {
readExcel,
writeExcel,
getExcelWorkDir
} from '@/uni_modules/zy-read-write-excel'
export default {
data() {
return {
workDir: '',
fileName: 'demo.xlsx',
mode: 'overwrite',
writing: false,
reading: false,
sheets: [],
log: ''
}
},
onLoad() {
this.loadWorkDir()
},
methods: {
appendLog(msg) {
this.log = msg
},
loadWorkDir() {
getExcelWorkDir({
success: (res) => {
this.workDir = res.dirPath
this.appendLog('工作目录: ' + res.dirPath)
},
fail: (err) => this.appendLog(`获取失败 [${err.errCode}] ${err.errMsg}`)
})
},
onModeChange(e) {
this.mode = e.detail.value
},
resolvePath() {
const name = this.fileName && this.fileName.length > 0 ? this.fileName : 'demo.xlsx'
return name
},
doWrite() {
this.writing = true
const path = this.resolvePath()
const append = this.mode === 'append'
const sheets = append ? undefined : [{
name: '成绩单',
rows: [
['姓名', '科目', '分数', '通过', '日期'],
['张三', '语文', 92.5, true, '2026-09-22 10:00:00'],
['李四', '数学', 88, true, '2026-09-22 10:05:00'],
['王五', '英语', 59, false, '2026-09-22 10:10:00']
]
}, {
name: '汇总',
rows: [
['指标', '数值'],
['总人数', 3],
['平均分', 79.8]
]
}]
const options = {
path: path,
mode: this.mode,
success: (res) => {
this.writing = false
this.appendLog(`${res.mode === 'append' ? '追加' : '写入'}成功: ${res.path}\n` +
`${res.sheetCount} 个 Sheet,本次 ${res.rowCount} 行 / ${res.cellCount} 格,大小 ${res.size} 字节`)
},
fail: (err) => {
this.writing = false
this.appendLog(`写入失败 [${err.errCode}] ${err.errMsg}`)
}
}
if (append) {
options.sheetName = '成绩单'
options.rows = [
['赵六', '物理', 91, true, '2026-09-22 11:00:00']
]
} else {
options.sheets = sheets
}
console.log(options)
writeExcel(options)
},
doRead() {
this.reading = true
readExcel({
path: this.resolvePath(),
success: (res) => {
console.log(res)
this.reading = false
this.sheets = res.sheets
this.appendLog(`读取成功: ${res.fileName},共 ${res.sheetCount} 个 Sheet`)
},
fail: (err) => {
this.reading = false
this.sheets = []
this.appendLog(`读取失败 [${err.errCode}] ${err.errMsg}`)
}
})
}
}
}
</script>
<style>
.container {min-height: 100vh;background: #f5f6fa;}
.header {padding: 60rpx 40rpx 40rpx;background: linear-gradient(135deg, #07c160 0%, #10b981 100%);}
.title {font-size: 44rpx;font-weight: 700;color: #fff;}
.subtitle {display: block;font-size: 24rpx;color: rgba(255, 255, 255, .8);margin-top: 8rpx;}
.section {background: #fff;margin: 24rpx 30rpx;border-radius: 20rpx;padding: 30rpx;flex-direction: column;}
.section-title {font-size: 30rpx;font-weight: 600;color: #1a1a2e;margin-bottom: 16rpx;}
.path-text {font-size: 24rpx;color: #667085;word-break: break-all;margin-bottom: 16rpx;}
.form-row {flex-direction: row;align-items: center;margin-bottom: 20rpx;}
.label {font-size: 26rpx;color: #475467;width: 120rpx;}
.input {flex: 1;height: 72rpx;border: 1rpx solid #e4e7ec;border-radius: 12rpx;padding: 0 20rpx;font-size: 26rpx;}
.radio-group {flex-direction: row;}
.radio-label {margin-right: 30rpx;font-size: 26rpx;color: #475467;}.button-group {flex-direction: row;}
.sheet-block {margin-top: 20rpx;flex-direction: column;}
.sheet-name {font-size: 28rpx;font-weight: 600;color: #07c160;}
.sheet-meta {font-size: 22rpx;color: #98a2b3;margin: 6rpx 0 12rpx;}
.truncated {color: #f79009;}
.table {flex-direction: row;flex-wrap: wrap;border-left: 1rpx solid #e4e7ec;border-top: 1rpx solid #e4e7ec;margin-bottom: 8rpx;}
.cell {min-width: 140rpx;max-width: 300rpx;padding: 10rpx 14rpx;font-size: 22rpx;color: #344054;border-right: 1rpx solid #e4e7ec;border-bottom: 1rpx solid #e4e7ec;}
.log-text {font-size: 24rpx;color: #667085;line-height: 1.6;}
</style>
快速开始
写入一个 Excel 并读回内容:
import {
readExcel,
writeExcel,
getExcelWorkDir
} from '@/uni_modules/zy-read-write-excel'
export default {
data() {
return {
workDir: '',
sheets: []
}
},
onLoad() {
getExcelWorkDir({
success: (res) => {
this.workDir = res.dirPath
console.log('工作目录:', res.dirPath)
}
})
},
methods: {
writeDemo() {
writeExcel({
// 不传 path 时自动保存到应用私有目录,这里指定文件名便于演示
path: 'demo.xlsx',
mode: 'overwrite',
sheets: [
{
name: '成绩单',
rows: [
['姓名', '科目', '分数', '日期'],
['张三', '语文', 92.5, '2026-09-22 10:00:00'],
['李四', '数学', 88, true]
]
}
],
success: (res) => {
console.log('写入成功:', res.path, res.rowCount, '行', res.cellCount, '格')
this.readDemo(res.path)
},
fail: (err) => console.error(`写入失败 [${err.errCode}] ${err.errMsg}`)
})
},
readDemo(path) {
readExcel({
path: path,
success: (res) => {
console.log(`共 ${res.sheetCount} 个 Sheet`)
res.sheets.forEach((sheet) => {
console.log(`[${sheet.name}] ${sheet.rowCount} 行 x ${sheet.columnCount} 列`)
this.sheets = res.sheets
})
},
fail: (err) => console.error(`读取失败 [${err.errCode}] ${err.errMsg}`)
})
},
appendDemo() {
writeExcel({
path: 'demo.xlsx',
mode: 'append',
rows: [
['王五', '英语', 95, false]
],
sheetName: '成绩单',
success: (res) => console.log('追加成功,共', res.rowCount, '行'),
fail: (err) => console.error(err.errCode, err.errMsg)
})
}
}
}
API 一览
| API | 说明 |
|---|---|
readExcel |
读取 Excel,返回各 Sheet 的二维数据 |
writeExcel |
写入 / 追加 Excel |
getExcelWorkDir |
获取默认工作目录绝对路径 |
通用约定:
- 回调:
success(res)成功、fail(err)失败(err为UniError,含errCode/errMsg/errSubject)、complete(res)无论成败都会执行; - 路径不传时使用应用私有目录(
.../files/excel),相对文件名也落在该目录; - 所有 API 均为异步,读写不阻塞界面线程。
API 详解
readExcel(options)
读取 Excel 文件内容。
参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
path |
string |
是* | - | 文件路径或 content:// URI(*与 uri 二选一,path 优先) |
uri |
string |
否 | - | 兼容字段,等价于 path |
sheetName |
string |
否 | - | 只读指定名称的 Sheet;与 sheetIndex 二选一 |
sheetIndex |
number |
否 | - | 只读指定下标的 Sheet(从 0 开始) |
asText |
boolean |
否 | false |
true 时所有单元格按显示文本返回 |
maxRows |
number |
否 | 不限 | 每个 Sheet 最多读取的行数,超出时 truncated 为 true |
dateFormat |
string |
否 | yyyy-MM-dd HH:mm:ss |
日期单元格格式化模板 |
不传
sheetName/sheetIndex时读取全部 Sheet。
success 回调(ExcelReadResult):
| 字段 | 类型 | 说明 |
|---|---|---|
path |
string |
实际读取的文件展示路径 |
fileName |
string |
文件名 |
asText |
boolean |
本次是否按显示文本读取 |
sheetCount |
number |
读取到的 Sheet 数量 |
sheets |
ExcelSheetInfo[] |
Sheet 数据列表 |
readExcel({
path: 'demo.xlsx',
sheetName: '成绩单',
maxRows: 100,
success: (res) => {
const sheet = res.sheets[0]
console.log(sheet.name, sheet.rowCount, '行')
console.log('第一行:', sheet.rows[0])
},
fail: (err) => console.error(err.errCode, err.errMsg)
})
writeExcel(options)
写入(覆盖)或追加 Excel 文件。
参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
sheets |
ExcelSheetWriteConfig[] |
二选一 | - | 多 Sheet 配置,优先于 rows |
rows |
ExcelCellValue[][] |
二选一 | - | 单 Sheet 简写二维行数据 |
sheetName |
string |
否 | 自动命名 | rows 简写时的 Sheet 名 |
path |
string |
否 | 自动生成 | 目标路径;不传时生成 excel_时间戳.xlsx |
mode |
string |
否 | overwrite |
overwrite 覆盖 / append 追加 |
单元格值支持
string/number/boolean;null保持空白格。 后缀为.xls按.xls格式写入,其它(含.xlsx)按.xlsx写入。
success 回调(ExcelWriteResult):
| 字段 | 类型 | 说明 |
|---|---|---|
path |
string |
实际写入的文件绝对路径 |
fileName |
string |
文件名 |
size |
number |
文件大小(字节) |
mode |
string |
实际模式:overwrite / append |
sheetCount |
number |
文件最终 Sheet 总数 |
rowCount |
number |
本次实际写入的行数 |
cellCount |
number |
本次实际写入的单元格数 |
// 覆盖写入多 Sheet
writeExcel({
path: 'report.xlsx',
sheets: [
{ name: '汇总', rows: [['指标', '数值'], ['总量', 100]] },
{ name: '明细', rows: [['编号', '名称'], [1, '甲'], [2, '乙']] }
],
success: (res) => console.log('已写入', res.path),
fail: (err) => console.error(err.errCode, err.errMsg)
})
// 在已有文件末尾追加一行
writeExcel({
path: 'report.xlsx',
mode: 'append',
sheetName: '明细',
rows: [[3, '丙']],
success: (res) => console.log('追加成功'),
fail: (err) => console.error(err.errCode, err.errMsg)
})
getExcelWorkDir(options?))
获取默认工作目录绝对路径(应用私有外部目录下的 excel 子目录)。
参数:无(options 可省略)
success 回调:{ dirPath: string }
getExcelWorkDir({
success: (res) => console.log(res.dirPath)
})
数据类型
ExcelCellValue
string | number | boolean
ExcelRow
ExcelCellValue[],一行单元格数据。
ExcelSheetInfo
| 字段 | 类型 | 说明 |
|---|---|---|
name |
string |
Sheet 名称 |
index |
number |
Sheet 下标(从 0 开始) |
rowCount |
number |
实际读取到的行数 |
columnCount |
number |
最大列数 |
rows |
ExcelRow[] |
二维行数据,rows[r][c] 对应第 r 行第 c 列 |
truncated |
boolean |
因 maxRows 截断时为 true(未截断不返回) |
ExcelSheetWriteConfig
| 字段 | 类型 | 说明 |
|---|---|---|
name |
string |
Sheet 名,缺省自动 Sheet1… |
rows |
ExcelCellValue[][] |
二维行数据 |
错误码
失败回调 fail(err) 中的 err 为 UniError 对象:
| 字段 | 说明 |
|---|---|
errCode |
错误码,取值见下表 |
errMsg |
错误描述 |
errSubject |
固定为 zy-read-write-excel |
| 错误码 | 说明 |
|---|---|
9040001 |
参数错误(缺少 path / sheets,或 Sheet 名称、下标不存在) |
9040002 |
文件不存在 |
9040003 |
读写失败(IO 异常、权限不足等) |
9040004 |
文件格式不正确,仅支持 .xlsx / .xls,或文件已加密 |
9040005 |
应用上下文获取失败(建议重启应用) |
权限与要求
- 读写应用私有目录(不传
path或使用相对文件名)不需要任何存储权限; - 读取
/storage等公共路径下的文件(Android 12 及以下)需要READ_EXTERNAL_STORAGE,插件已声明; - 在 Android 11+ 上写入公共目录需由 App 自行申请「所有文件访问权限」(
MANAGE_EXTERNAL_STORAGE); - 通过
content://URI 读取时,需由选择文件的一方授予该 URI 的读取权限。
使用须知
- 相对路径落盘位置:相对文件名(如
demo.xlsx)会读写应用私有目录Android/data/<包名>/files/excel,卸载应用时会被清除。 - 追加模式:
mode: 'append'时按 Sheet 名称定位已有 Sheet(未指定名称时写第一个 Sheet), 从最后一行继续往下写;目标文件不存在时会新建。 - 覆盖写入是整体重写:
overwrite模式会重新生成文件,原有内容全部丢弃。 - 写一半不会损坏原文件:写入过程中若失败,目标文件保持原样。
maxRows截断:读取被截断时该 Sheet 的truncated为true,rowCount为实际读到的行数。- 大文件:读写在后台线程执行,不会阻塞界面;文件越大耗时越长。
常见问题
Q:读取报「文件格式不正确」(9040004)?
A:请确认文件确实是 .xlsx / .xls,且未被加密。加密文件需先解密。
Q:读取报「文件不存在」(9040002)?
A:相对文件名以应用私有 excel 目录为基准;绝对路径请确认拼写。content:// URI 需确保授予了读取权限。
Q:truncated 一直是 true?
A:说明该 Sheet 行数超过了 maxRows。去掉 maxRows 或调大即可读全。
Q:追加没有写到我指定的 Sheet?
A:追加按 sheetName 匹配已有 Sheet;名称不匹配(或不传)时会写入文件的第一个 Sheet。请确保名称完全一致。
Q:写到公共目录(如 /sdcard/Download)失败? A:Android 11+ 写公共目录需要「所有文件访问权限」,请由 App 引导用户在系统设置中开启。推荐直接使用默认应用私有目录。
Q:iOS 能用吗? A:不能,本插件仅支持 Android。

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