更新记录

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 重启后仍然有效;之后所有读写、新建、删除都在该目录内完成,权限边界清晰、可撤销。


⚠️ 购买前必读

  1. 仅支持 Android App。iOS、鸿蒙、小程序、H5 均未实现(本插件不做跨端模拟)。
  2. 文件操作依赖系统 DocumentsProvider。目录浏览、新建、重命名、删除、复制由系统文件管理器(或网盘 App)的提供方实现,不同提供方对「复制」「重命名」的支持程度不同。失败时插件返回明确错误码(NOT_SUPPORTED / IO_ERROR),不会静默失败。
  3. 不提供路径式 API。所有操作基于 content:// 文档 URI。这是分区存储下的正确做法,也意味着不能拿一个 /sdcard/... 字符串直接调用;如果业务强依赖绝对路径(例如把文件交给只认路径的三方 SDK),需要先用 readText 读内容或自行在应用私有目录落盘。
  4. 文本读取有上限。默认 256KB、最大 4MB,超限返回 truncated: true。本插件服务于「配置文件 / CSV / JSON 等业务数据」,不承担大文件传输;二进制大文件请用 copyFile 让系统复制,不要读进内存。
  5. 本版本的验证状态见文末「验证状态」一节,请按实际情况评估,不要仅凭宣传语判断。

环境要求

  • 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。

隐私、权限声明

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

无。插件不声明任何存储权限,文件访问范围严格由用户在系统选择器中授权,符合 Android 10+ 分区存储要求。

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

插件不采集、不上传任何数据。文件读写全部通过系统存储访问框架在本机完成,不访问网络;授权的目录由系统记录,本插件仅在调用时读取该授权列表。

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

无

暂无用户评论。