更新记录
1.0.0(2026-10-26)
首个版本:目录级持久授权(重启后仍可读写)、文件选择(多选+扩展名两段式过滤)、目录浏览(下钻/排序/过滤)、文本读写(超限截断)、目录内新建/重命名/复制/删除、批量删除逐项结果、统一错误码与文件条目结构;UTS→Kotlin 编译通过,43 项核心逻辑自动化测试通过。
平台兼容性
uni-app
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| × | √ | - | - | √ | × | √ | - | - |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| - | - | - | - | - | - | - | - | - | - | - | - |
szhzy-filebridge 文件存取与批量导入导出插件
Android 端的文件存取层:基于系统存储访问框架(SAF),不需要任何存储权限,一次授权目录、重启后仍可读写,支持目录浏览、文本读写、新建/重命名/复制/删除。面向业务表单附件、批量数据文件导入导出、归档目录管理等真实场景。
适配 Android 6.0–15 的分区存储(Scoped Storage)限制,不使用 MANAGE_EXTERNAL_STORAGE 这类需要商店特殊审核、且会被系统标记为高风险的权限。
为什么需要它
Android 10 起分区存储生效,Android 11+ 上 READ_EXTERNAL_STORAGE 已经无法访问公共目录,很多老项目里「拼一个 /sdcard/xxx 路径直接读写」的写法在新系统上必然失败。常见的三条路各有问题:
| 方案 | 问题 |
|---|---|
| 继续用存储权限 + 拼路径 | Android 11+ 直接失效;即便在旧系统可用,Android 13+ 的媒体权限拆分也让代码分支越来越乱 |
申请 MANAGE_EXTERNAL_STORAGE |
需要应用商店特殊审核,企业内部分发也会被系统提示为高风险权限 |
| 只做「选一个文件」 | 拿到一次性 URI,应用重启后失效;无法浏览目录、无法写入指定目录 |
本插件走第三条路的升级版:目录级持久授权 + 文档 URI 操作。用户在系统选择器里选一次目录,授权由系统持久保存,App 重启后仍然有效;之后所有读写、新建、删除都在该目录内完成,权限边界清晰、可撤销。
⚠️ 购买前必读
- 仅支持 Android App。iOS、鸿蒙、小程序、H5 均未实现(本插件不做跨端模拟)。
- 文件操作依赖系统 DocumentsProvider。目录浏览、新建、重命名、删除、复制由系统文件管理器(或网盘 App)的提供方实现,不同提供方对「复制」「重命名」的支持程度不同。失败时插件返回明确错误码(
NOT_SUPPORTED/IO_ERROR),不会静默失败。 - 不提供路径式 API。所有操作基于
content://文档 URI。这是分区存储下的正确做法,也意味着不能拿一个/sdcard/...字符串直接调用;如果业务强依赖绝对路径(例如把文件交给只认路径的三方 SDK),需要先用readText读内容或自行在应用私有目录落盘。 - 文本读取有上限。默认 256KB、最大 4MB,超限返回
truncated: true。本插件服务于「配置文件 / CSV / JSON 等业务数据」,不承担大文件传输;二进制大文件请用copyFile让系统复制,不要读进内存。 - 本版本的验证状态见文末「验证状态」一节,请按实际情况评估,不要仅凭宣传语判断。
环境要求
- HBuilderX 3.99 及以上
- uni-app(Vue3)Android App,最低 Android 6.0(API 23)
- 使用自定义基座运行(UTS 插件不在标准基座内),云打包正常
- 无需在 manifest 中配置任何存储权限
快速开始
1. 安装
把 szhzy-filebridge 放入项目 uni_modules/ 目录,HBuilderX 会自动识别。
2. 选择目录并拿到持久授权
import { pickDirectory, listDirectory } from '@/uni_modules/szhzy-filebridge'
// 选择目录:用户在系统选择器里确认后回调
pickDirectory({}, (res) => {
if (!res.ok) {
if (res.code !== 'USER_CANCEL') {
uni.showToast({ title: res.message, icon: 'none' })
}
return
}
// res.treeUri 是后续所有操作的入口,建议存到本地缓存,重启后可直接复用
console.log('已授权目录:', res.name, res.treeUri, res.readable, res.writable)
})
// 列出根目录下的直接子项
const listed = listDirectory({ treeUri: 'content://.../tree/primary%3A归档' })
if (listed.ok) {
listed.entries.forEach((item) => {
console.log(item.name, item.isDirectory, item.sizeText, item.lastModifiedText)
})
}
3. 浏览子目录
每个条目都带 documentId,把它传给 listDirectory 即可下钻;返回上级就把 documentId 换成上一级的值(根目录传空串)。
const sub = listDirectory({
treeUri,
documentId: 'primary:归档/2026-设备巡检',
order: 'time', // name | size | time
desc: true
})
4. 新建、读取、写入
import { createFile, readText, writeText } from '@/uni_modules/szhzy-filebridge'
// 在目录内新建文本文件(同名时系统自动追加序号,返回实际文件名)
const created = createFile({
treeUri,
documentId: '',
name: '导入说明.txt',
mimeType: 'text/plain',
content: '第一行\n第二行'
})
// 读取(默认上限 256KB)
const read = readText({ uri: created.uri })
if (read.ok) {
console.log(read.content, read.bytes, read.truncated)
}
// 覆盖写入(不会在旧内容后面追加)
writeText({ uri: created.uri, content: '新的内容' })
5. 批量删除
import { deleteFiles } from '@/uni_modules/szhzy-filebridge'
const res = deleteFiles({ uris: [uri1, uri2, uri3] })
// res.summary = { total, succeeded, failed, allSucceeded }
// res.items 逐项给出 ok / code / message,便于定位哪一条失败
API 参考
所有同步 API 返回统一信封:{ ok, code, message, ... }。判断成功请用 ok,分支判断请用 code(错误码稳定,文案可能随版本优化)。选择类 API 通过回调返回结果,回调在主线程触发。
pickFiles(options, callback)
打开系统文档选择器选择文件。
| 参数 | 类型 | 说明 |
|---|---|---|
| options.maxCount | number | 最多选择数量,1–100,默认 1 |
| options.extensions | string[] | 扩展名白名单,如 ['pdf','xlsx'];空数组表示不限 |
| callback | (res) => void | res = { ok, code, message, files, persistable, filteredOut } |
files 中每一项为文件条目(见下方「文件条目结构」)。
返回值是启动结果,不代表用户已完成选择;用户点了返回键会得到 code: 'USER_CANCEL'。
系统选择器只认 MIME 不认扩展名,因此插件做两段式过滤:先用扩展名推导 MIME 提示给系统,用户选定后再按扩展名精确过滤一次(filteredOut 是被过滤掉的条数)。
pickDirectory(options, callback)
打开系统目录选择器并申请持久授权。
res = { ok, code, message, treeUri, documentId, name, readable, writable }。treeUri 必须保存下来,它是后续所有操作的入口。
listDirectory(options)
| 参数 | 类型 | 说明 |
|---|---|---|
| options.treeUri | string | 必填,目录树 URI |
| options.documentId | string | 子目录 ID,空串表示根目录 |
| options.order | string | name(默认)/ size / time |
| options.desc | boolean | 是否倒序,默认 false |
| options.extensions | string[] | 只保留这些扩展名的文件;目录始终保留以便继续浏览 |
返回 { ok, entries }。排序规则固定为目录恒在文件之前,同类型内再按指定字段排序。
readText(options)
options.uri 必填,options.maxBytes 可选(默认 256KB,上限 4MB)。返回 { ok, content, bytes, truncated }。
writeText(options)
options.uri、options.content 必填。覆盖写入(内部使用截断模式,不会在旧内容尾部追加)。返回 { ok, bytes }。
createFile(options)
options.treeUri、options.name 必填;options.documentId、options.mimeType、options.content 可选。文件名会被清洗非法字符。返回 { ok, uri, name, mimeType },name 是系统实际采用的文件名(同名时可能被追加序号)。
deleteFiles(options)
options.uris 必填(字符串数组)。返回 { ok, summary, items }。ok: true 表示批处理执行完毕,具体某一条是否成功看 items[i].ok。
删除目录会连同其内容一起删除。业务侧必须先做二次确认。
renameFile(options)
options.uri、options.newName 必填。返回 { ok, uri, name },uri 是重命名后的新 URI(部分提供方会改变 URI,请用返回值替换旧值)。
copyFile(options)
options.sourceUri、options.treeUri 必填,options.documentId 可选。返回 { ok, uri, name }。部分提供方不支持复制,此时返回 NOT_SUPPORTED。
getGrantedDirectories()
返回 { ok, directories: [{ uri, name, documentId, readable, writable, isTree }] },即当前应用持有的全部持久授权。典型用途:App 启动时恢复「上次使用的目录」。
releaseDirectory(options)
options.uri 必填。释放该 URI 的持久授权(用户主动取消授权时调用)。
getStatus() / getCapabilities() / getPluginVersion()
getStatus() 返回 { ok, pickerPending, timezoneOffsetMinutes, grantedCount, pluginVersion },用于排查与 UI 防重复点击。
getCapabilities() 返回平台、最低系统版本、是否需要权限以及能力清单,便于做版本兼容判断。
文件条目结构
{
name: '设备台账.xlsx',
uri: 'content://com.android.externalstorage.documents/...',
documentId: 'primary:归档/设备台账.xlsx', // listDirectory 返回的条目带此项,用于下钻或再次定位
size: 2048,
sizeText: '2.0 KB', // 目录为 '-'
mimeType: 'application/vnd.openxmlformats-...',
extension: 'xlsx',
lastModified: 1793001543000,
lastModifiedText: '2026-10-26 15:59', // 时间未知时为空串
isDirectory: false
}
错误码
| code | 含义 | 常见原因 |
|---|---|---|
SUCCESS |
成功 | — |
INVALID_PARAM |
参数不合法 | 必填项为空、数量越界、扩展名含非法字符 |
USER_CANCEL |
用户取消 | 用户在选择器里按了返回 |
NO_ACTIVITY |
没有可用 Activity | 在界面创建前调用,或运行环境不是 App |
PICKER_BUSY |
已有选择器在途 | 上一次选择还未返回就再次调用 |
NO_PERMISSION |
授权失效或不足 | 用户在系统设置里撤销了授权、目录被删除 |
NO_MATCH |
所选文件均不符合扩展名白名单 | 用户切到「所有文件」后选了其他类型 |
IO_ERROR |
读写失败 | 目标只读、空间不足、提供方内部错误 |
NOT_FOUND |
目标不存在 | 文件已被移动或删除 |
NOT_SUPPORTED |
提供方不支持该操作 | 部分网盘/文件管理器不支持复制、重命名 |
授权模型
- 授权发生在系统选择器里,用户明确指定哪个目录,插件只能看到这个目录及其子目录;
- 授权是持久的:系统记录在
UriPermission中,App 重启、进程被杀后依然有效,直到用户主动撤销或应用卸载; - 用户可以随时在「系统设置 → 应用 → 权限」或文件管理器侧边栏里取消授权,插件下次操作会返回
NO_PERMISSION; - 插件不申请也不持有任何存储权限,
package.json的权限声明为空。
授权丢失是业务侧必须处理的正常分支,不要当成异常崩溃。建议在页面加载时先调 getGrantedDirectories() 判断再决定是否引导用户重新选择。
与其他文件类插件的定位差异
市面上多数文件插件是「文件选择器」:选一个文件,返回路径或 URI。本插件的定位是文件存取层:
- 目录级持久授权,而不是一次性文件 URI;
- 提供目录浏览、新建、重命名、删除、复制,能真正做「归档管理」而不只是「上传附件」;
- 零存储权限,规避 Android 11+ 的分区存储改造。
如果你只需要「选一张图片拿到路径」,用 uni.chooseImage 或轻量选择器更合适,不必为此付费。
常见问题
Q:为什么插件不声明存储权限,也能读写文件? A:SAF 的授权不走权限体系,而是由系统选择器把用户选中的目录授权给应用。这既是 Google 推荐做法,也让应用不需要申请敏感权限。
Q:为什么列不出某个目录? A:只能访问用户已授权的目录树。若该目录在系统选择器里不可见(例如被其他应用隔离的应用私有目录),插件也无法访问。
Q:重启后 treeUri 还能用吗?
A:能,只要用户没有撤销授权。建议把 treeUri 持久化到本地缓存,并在启动时用 getGrantedDirectories() 校验一次。
Q:copyFile 返回 NOT_SUPPORTED 怎么办?
A:说明当前目录的提供方不支持跨目录复制。可以改用 readText + createFile 自己搬运文本内容,或引导用户换用其他目录(如系统「文档」目录)。
Q:能读大文件吗?
A:readText 只面向文本,上限 4MB。大文件请用 copyFile 让系统完成复制,不要把内容读进 JS 内存。
Q:可以拿绝对路径去调用别的 SDK 吗?
A:不行。content:// URI 不能转换成稳定的文件路径。需要给三方 SDK 传路径时,请先把内容写到应用私有目录(如 _doc/)再把该路径交给 SDK。
验证状态
如实说明本版本的验证范围,便于你评估风险:
| 项目 | 状态 |
|---|---|
| UTS → Kotlin 编译(HBuilderX 5.26,Android 平台) | 已通过 |
| 纯逻辑层自动化测试(文件名清洗、扩展名、时间/体积格式化、过滤排序、参数校验等) | 43 项全部通过 |
| 真机全流程(选择目录、浏览、读写、删除) | 未完成,需在真实 Android 设备上用自定义基座逐项验证 |
| 不同文件管理器 / 网盘的提供方差异 | 未做横向验证 |
已知限制:copyFile 与 renameFile 的行为依赖提供方实现;跨进程并发修改同一目录时,列表可能短暂不一致(重新调用 listDirectory 即可)。
技术支持
插件文档、接入示例、更新记录随插件一同发布。使用中遇到问题请在插件市场留言,附上:设备型号、Android 版本、目录来源(系统文件管理器 / 哪个网盘)、返回的 code 与 message。
更新记录
见 changelog.md。

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