更新记录
0.1.4(2026-08-13)
- 修复视频保存到相册后不排在最新位置:0.1.3 的 EXIF 改写只对 JPEG 生效,MP4 没有 EXIF,媒体扫描器改从
moov/mvhd里的 creation_time 取拍摄时间,于是仍按原始拍摄时间排序。 - 新增 MP4/MOV 的
mvhd时间改写:写入相册后、清除IS_PENDING之前,按 ISO-BMFF 盒结构定位moov→mvhd,把 creation_time / modification_time 就地改写成「现在」(version 0 写 32 位、version 1 写 64 位,起点为 1904-01-01 UTC)。只改这 8/16 个字节,文件长度和其余内容完全不动,也不产生大文件拷贝。 - 覆盖
video/mp4、video/quicktime、video/x-m4v;其它容器不改写,退回原有行为。改写失败只记日志,不影响保存结果。 - Android 9- 的公共目录路径同样改写,并同步文件修改时间。
平台兼容性
uni-app(4.25)
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| √ | √ | - | - | √ | √ | 5.0 | √ | × |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| - | - | - | - | - | - | - | - | - | - | - | - |
uni-app x(4.25)
| Chrome | Safari | Android | iOS | 鸿蒙 | 微信小程序 |
|---|---|---|---|---|---|
| - | - | - | - | - | - |
zoo-media-saver
把 App 端本地文件真正写进系统相册 / 系统下载目录的 UTS 插件。
为什么需要它:uni-app 的 uni.saveImageToPhotosAlbum、uni.saveVideoToPhotosAlbum 和 plus.gallery.save 在部分机型上存在「接口返回成功、但相册里找不到文件」的假成功问题(媒体库没有正确收录,或收录后按文件自带的拍摄时间排序,混在旧照片里找不到)。本插件直接调用原生 API 写入:
- Android 走 MediaStore(API 29+)或公共目录 + 媒体扫描(API 21-28);
- iOS 走 PhotoKit
PHAssetCreationRequest,并把creationDate显式写成「现在」,保证保存的内容排在相册最新位置。
特性
- 图片 / 视频写入系统相册,支持自定义文件名;
- 重名自动追加
(1)(2)序号,不覆盖已有文件; - 保存后排在相册最新位置(不沿用文件自带的拍摄时间);
- 任意类型文件写入系统下载目录(Android
Download/); - iOS 无公共下载目录,文件落进应用
Documents/Download,配合宿主开启UIFileSharingEnabled后可在系统「文件」App 的「我的 iPhone」中查看、分享; - 只申请最小权限:Android 10+ 零权限,iOS 只申请「添加照片」级别;
- 统一错误码,成功/失败都有明确回调,不存在假成功。
快速开始
import { saveToAlbum, saveToDownloads, getMediaSaverVersion } from '@/uni_modules/zoo-media-saver'
console.log('插件版本:', getMediaSaverVersion())
// 保存图片/视频到相册
saveToAlbum({
filePath: '/storage/emulated/0/Android/data/<包名>/doc/download/a.jpg', // 沙盒绝对路径
fileName: '巡检报告-20260811.jpg', // 可选,传空沿用源文件名
mediaType: 'image', // 可选,'image' | 'video',传空按扩展名推断
success: (res) => {
console.log('已保存到相册:', res.fileName, res.target)
},
fail: (err) => {
console.error('保存失败:', err.errCode, err.errMsg)
}
})
// 保存任意文件到下载目录
saveToDownloads({
filePath: '/var/mobile/Containers/Data/Application/<uuid>/Documents/a.pdf',
fileName: '巡检报告.pdf',
success: (res) => {
console.log('已保存到下载目录:', res.fileName, res.filePath)
},
fail: (err) => {
console.error('保存失败:', err.errCode, err.errMsg)
}
})
注意:filePath 必须是本地沙盒文件(uni.downloadFile 的 tempFilePath 可用 plus.io.convertLocalFileSystemURL 转成绝对路径),不支持直接传网络 URL。
兼容版本与基座边界
- Android:minSdkVersion 21(Android 5.0)及以上;
- iOS:deploymentTarget 12.0 及以上;
- 仅支持 uni-app App 端(vue2 / vue3 / nvue 均可),不支持小程序与 H5;
- 插件含原生代码,必须制作自定义基座或云打包后才能生效,标准基座运行会报「当前运行环境不支持保存文件」类错误。
参数
saveToAlbum(options)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| filePath | string | 是 | 待保存的本地文件路径,沙盒绝对路径(可带 file:// 前缀,插件内会自行归一化) |
| fileName | string | 否 | 期望的落地文件名(含扩展名);传空沿用源文件名;系统内重名时自动追加序号 |
| mediaType | string | 否 | 'image' 或 'video';传空按扩展名推断,推断失败返回 9011006 |
| success | function | 否 | 保存成功回调,参数为 ZooSaveResult |
| fail | function | 否 | 保存失败回调,参数为 ZooSaveFail |
| complete | function | 否 | 完成回调(成功失败都会触发) |
saveToDownloads(options)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| filePath | string | 是 | 待保存的本地文件路径,沙盒绝对路径 |
| fileName | string | 否 | 期望的落地文件名(含扩展名);传空沿用源文件名;重名自动追加序号 |
| success / fail / complete | function | 否 | 同上 |
返回
ZooSaveResult
| 字段 | 类型 | 说明 |
|---|---|---|
| fileName | string | 实际落地的文件名(重名追加序号后以这里返回的为准) |
| filePath | string | Android 为媒体库 content:// URI;iOS 相册保存为 asset localIdentifier,下载目录保存为沙盒绝对路径 |
| target | string | 落地位置:album-image / album-video / downloads |
错误码
| errCode | 含义 |
|---|---|
| 9011001 | 当前运行环境不支持保存文件(如未打自定义基座) |
| 9011002 | 源文件不存在或读取失败 |
| 9011003 | 写入系统相册失败 |
| 9011004 | 相册或存储权限被拒绝,请在系统设置中允许后重试 |
| 9011005 | 保存参数错误(如 filePath 为空) |
| 9011006 | 无法识别的媒体类型,请显式传入 mediaType |
| 9011007 | 写入系统下载目录失败 |
宿主权限声明
Android
- Android 10(API 29)及以上:无需任何权限(MediaStore 分区存储);
- Android 9 及以下:需在 manifest 中声明
"permissions": [
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>"
]
iOS
- 保存相册需配置隐私描述(缺失会导致保存失败):
"NSPhotoLibraryAddUsageDescription": "需要保存图片和视频到您的相册"
- 若使用
saveToDownloads并希望在系统「文件」App 中查看,需开启文件共享:
"UIFileSharingEnabled": true,
"LSSupportsOpeningDocumentsInPlace": true
各平台行为
Android
| 系统版本 | 相册保存 | 下载目录保存 |
|---|---|---|
| API 29+ | MediaStore 写入 DCIM/(图片)、Movies/(视频) |
MediaStore 写入 Download/ |
| API 21-28 | 复制到公共 DCIM/、Movies/ 后触发媒体扫描 |
复制到公共 Download/ 目录 |
保存时会显式写入 DATE_ADDED、DATE_MODIFIED、datetaken 为当前时间,相册按最新排序。
iOS
- 相册:PhotoKit 写入,
creationDate = Date(),排在最新;只申请「添加照片」权限(iOS 14+ 为.addOnly),不需要读取整个相册; - 下载目录:iOS 没有公共下载目录,文件复制到应用
Documents/Download/;宿主开启UIFileSharingEnabled后,在系统「文件」App →「我的 iPhone」→ 对应应用文件夹中可见,也可在「文件」App 里分享出去。
FAQ
Q:调用后 success 了,但相册里看不到?
本插件的写路径就是为解决该问题设计的:Android 走 MediaStore 显式插入并写好时间字段,iOS 走 PhotoKit 并显式设置 creationDate。如果仍看不到,先确认 getMediaSaverVersion() 能取到版本号(取不到说明自定义基座里没有打进本插件)。
Q:标准基座可以直接运行吗? 不可以。插件含原生代码,必须「运行 → 运行到手机或模拟器 → 制作自定义调试基座」后再运行,或云打包。
Q:fileName 传中文、特殊字符可以吗? 可以。Android MediaStore 与 iOS 下载目录都支持中文文件名;iOS 相册不暴露文件名概念(相册按时间排序),传入的 fileName 会原样回传便于业务记录。
Q:权限被用户拒绝过怎么办?
返回 9011004。业务侧可引导用户去系统设置开启(Android 9- 的存储权限、iOS 的照片「添加」权限)。
授权说明
插件不采集、不上传任何数据,只把宿主指定的本地文件写入系统相册或下载目录。

收藏人数:
购买源码授权版(
试用
使用 HBuilderX 导入示例项目
赞赏(0)
下载 2
赞赏 0
下载 12504411
赞赏 1941
赞赏
京公网安备:11010802035340号