更新记录

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) 失败(errUniError,含 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 最多读取的行数,超出时 truncatedtrue
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 / booleannull 保持空白格。 后缀为 .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) 中的 errUniError 对象:

字段 说明
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 的读取权限。

使用须知

  1. 相对路径落盘位置:相对文件名(如 demo.xlsx)会读写应用私有目录 Android/data/<包名>/files/excel,卸载应用时会被清除。
  2. 追加模式mode: 'append' 时按 Sheet 名称定位已有 Sheet(未指定名称时写第一个 Sheet), 从最后一行继续往下写;目标文件不存在时会新建。
  3. 覆盖写入是整体重写overwrite 模式会重新生成文件,原有内容全部丢弃。
  4. 写一半不会损坏原文件:写入过程中若失败,目标文件保持原样。
  5. maxRows 截断:读取被截断时该 Sheet 的 truncatedtruerowCount 为实际读到的行数。
  6. 大文件:读写在后台线程执行,不会阻塞界面;文件越大耗时越长。

常见问题

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。

隐私、权限声明

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

android.permission.READ_EXTERNAL_STORAGE

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

Excel 文件仅在本机读写,不会上传至任何服务器

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

暂无用户评论。