更新记录
1.0.0(2026-09-22) 下载此版本
首个正式版本
- 连拍捕获:一次调起系统相机,用户可连续拍摄多张;返回后按时间窗口批量取回整组照片,
张数上限由
max控制(硬上限 100)。 - 相册扫描双通道:MediaStore 查询 +
DCIM/Camera文件系统扫描,两条通道统一按拍摄时间升序结算,max保留最新的一批;支持连拍文件名IMG_YYYYMMDD_HHMMSS[_N].jpg的时间解析。 - 权限三态处理:自动申请相机与存储权限,区分「临时拒绝 / 永久拒绝 / 未声明」并给出对应引导。
- 双入口:标准 uni-app 组件(模板 1 行 + onShow 1 行)与 JS SDK(完全自控流程),二者能力同源。
- 真机排错:
getLastScanDebug()返回走的哪个通道、扫到几条、是否被max截断、截断前有多少张。 - 平台:App-Android(5.0 / API 21+)、App-HarmonyOS、App-nvue;零原生依赖、无需自定义基座。
完整变更记录见插件包内
changelog.md。
平台兼容性
uni-app(3.8.4)
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| √ | √ | × | × | √ | √ | 5.0 | × | √ |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| × | × | × | × | × | × | × | × | × | × | × | × |
hd-burst-camera · 相机连拍捕获
调起系统相机连拍 → 返回后一次性取回整组照片。纯 Native.js 实现,零原生依赖,无需自定义基座。
在 Android 上,用 JS 调起系统相机拍照后,拿不到 Activity 的返回值(逻辑层没有 onActivityResult)。
本插件把这个「缺陷」变成了优势:不依赖返回值,而是按时间窗口回扫相册 ——
因此窗口内连拍的每一张照片都能被一次性收回,而不只是最后一张。
完整的链路是:申请权限 → 调起相机连拍 → 返回后按时间窗口批量扫描相册 → 返回整组照片路径,
并内置了三态权限处理、文件落盘延迟重试与真机排错能力。
目录
功能简介
| 能力 | 说明 |
|---|---|
| 连拍捕获 | 一次调起相机,用户可连续拍摄多张;返回后按时间窗口批量取回整组照片,由 max 控制张数上限 |
| 调起系统相机 | 三级降级:品牌包名 → ACTION_IMAGE_CAPTURE Intent → 组件名。覆盖华为/荣耀/小米/红米/OPPO/vivo/iQOO/三星/一加/真我/Google |
| 取回照片 | MediaStore 查询 + DCIM/Camera 文件系统扫描双通道,按时间窗口过滤;连拍文件名 IMG_YYYYMMDD_HHMMSS[_N].jpg 自动解析 |
| 权限处理 | 自动申请相机与存储权限,区分「临时拒绝 / 永久拒绝 / 未声明」三态并给出对应引导 |
| 真机排错 | 提供扫描诊断信息(走了哪个通道、扫到几条、时间窗口是多少) |
| 双入口 | 标准 uni-app 组件(3 行接入)与 JS SDK(完全自控流程) |
零依赖:不依赖任何原生插件、.aar,也不需要制作自定义基座。
💡 连拍是怎么实现的? 见「必读:核心机制」。 简单说:插件不取「单张返回值」,而是记录调起相机的时刻,返回后把该时间窗口内 相册里新增的照片全部收集起来 —— 用户拍了几张就能拿回几张。
平台兼容性
| 平台 | 支持 | 说明 |
|---|---|---|
| App-Android | ✅ | 完整支持,Android 5.0 (API 21) 及以上 |
| App-HarmonyOS | ✅ | 经 Native.js 兼容,无需额外配置 |
| App-iOS | ❌ | 未实现(iOS 无 Native.js,需另做 UTS 插件) |
| App-nvue | ✅ | 组件与 SDK 均可在 nvue 页面使用 |
| H5 | ❌ | 组件可渲染,但调用时立即派发 @error(NOT_SUPPORTED) |
| 微信/支付宝/抖音等小程序 | ❌ | 同上;小程序端请改用 uni.chooseImage 等原生 API |
| uni-app x | ❌ | 本插件为 vue2/vue3 通用前端组件,未适配 uni-app x |
判断当前环境是否可用:
burstCamera.isSupported(),或在组件里检查@error的code === 'NOT_SUPPORTED'。
最小接入步骤
1. 安装
将 hd-burst-camera 目录放入项目的 uni_modules/ 下(或在 HBuilderX 中通过「使用插件」导入)。
插件会自动被 easycom 识别,组件无需手动 import。
2. 配置权限(必做,漏配则不弹窗)
⚠️ 本插件是纯 JS 实现,没有原生声明文件,无法自带 Android 权限声明。 权限必须由宿主工程在
manifest.json中静态声明。 Android 对未声明的权限调用plus.android.requestPermissions会 直接判拒绝且不弹窗、不报错(静默失败)。
打开 manifest.json → app-plus.distribute.android.permissions,加入:
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>",
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>"
仅当工程的 targetSdkVersion >= 33 时,再追加:
"<uses-permission android:name=\"android.permission.READ_MEDIA_IMAGES\"/>"
同时建议在 app-plus.modules 中开启模块:
"modules": { "Camera": {}, "Gallery": {}, "Storage": {} }
声明清单必须与
burstCamera.getPermissionList()的返回值严格一一对应, 因为插件会按targetSdkVersion动态决定是否申请READ_MEDIA_IMAGES。
3. 使用
任选一种方式:
- 组件用法(推荐,见下节)—— 放一个标签 + 在
onShow里调一次handlePageShow() - JS SDK 用法 —— 完全自己控制流程
4. 运行
普通运行即可(无需自定义基座)。首次使用会弹出权限申请框。
方式一:组件用法(推荐)
接入成本:模板 1 行 + onShow 1 行。
<template>
<view>
<hd-burst-camera
ref="camera"
:max="20"
:pad="8000"
@success="onSuccess"
@error="onError"
@permission-denied="onPermissionDenied"
/>
<button @click="$refs.camera.open()">连拍</button>
<image v-for="(p, i) in photos" :key="i" :src="p" mode="aspectFill" />
</view>
</template>
<script>
export default {
data() {
return { photos: [] }
},
onShow() {
// ★ 必做的一行:相机返回后由此触发扫描
this.$refs.camera && this.$refs.camera.handlePageShow()
},
methods: {
onSuccess(e) {
console.log('连拍拿到照片', e.count, '张', e.photos, e.debug)
this.photos = e.photos
},
onError(e) {
// e.code / e.message
uni.showToast({ title: e.message, icon: 'none' })
},
onPermissionDenied(e) {
if (e.permanent) {
// 已被永久拒绝,再申请也不会弹窗,只能引导去系统设置
uni.showModal({
title: '相机权限',
content: '请在系统设置中开启相机与存储权限',
confirmText: '去设置',
success: () => this.$refs.camera.openAppSettings(),
})
} else {
uni.showToast({ title: '请点击「允许」后重试', icon: 'none' })
}
},
},
}
</script>
组件说明
- 组件不渲染任何可见 UI,只做逻辑编排。按钮、预览等界面由接入方自己写,保证风格统一。
- 组件通过默认插槽可读取内部状态(
permission-status/capturing/scanning/photos/open), 需要自定义 UI 时可用。 - 组件在
mounted时默认自动申请权限(autoRequestPermission可关闭)。
通过 ref 可调用的方法
| 方法 | 返回 | 说明 |
|---|---|---|
open() |
Promise<boolean> |
打开相机开始连拍(resolve 仅代表启动请求已发出) |
handlePageShow() |
void |
必做:在页面 onShow 中调用,触发连拍照片扫描 |
requestPermission() |
Promise<boolean> |
申请权限(会真正弹窗) |
checkPermission() |
boolean |
只读检测权限(不弹窗) |
scan() |
Promise<string[]> |
手动重扫当前窗口,取回整组连拍照片 |
openAppSettings() |
boolean |
跳转系统权限设置页 |
getScanDebug() |
Object |
获取最近一次扫描诊断信息 |
reset() |
void |
复位组件与 SDK 状态 |
方式二:JS SDK 用法
需要完全自控流程(自定义 UI、多状态并存、嵌入已有页面逻辑)时使用。
import burstCamera from '@/uni_modules/hd-burst-camera/js_sdk/burstCamera.js'
export default {
data() {
return { awaitingPhoto: false, scanTimer: null, photos: [] }
},
onShow() {
// ★ 只认显式状态做守卫,不要用 loading / scanning 之类的中间态
if (this.awaitingPhoto && burstCamera.getStartTime() > 0) {
if (this.scanTimer) clearTimeout(this.scanTimer)
// 延迟扫描:相机写文件与相册建索引都有延迟
this.scanTimer = setTimeout(() => this.scanPhotos(), 900)
}
},
methods: {
async handleCapture() {
const perm = await burstCamera.requestPermissions()
if (!perm.granted) {
// perm.code 可用于区分原因,见「错误码表」
if (perm.deniedAlways && perm.deniedAlways.length) {
burstCamera.openAppSettings()
}
return
}
try {
await burstCamera.openCamera() // 启动请求发出后即 resolve
this.awaitingPhoto = true
uni.showToast({ title: '请连拍后返回', icon: 'none' })
} catch (err) {
console.error(err.code, err.message)
}
},
async scanPhotos() {
// 一次取回该时间窗口内的整组连拍照片(上限由 max 控制)
const photos = await burstCamera.scanNewPhotos(
burstCamera.getStartTime(),
{ max: 20, pad: 8000 }
)
console.log('连拍照片:', photos, '诊断:', burstCamera.getLastScanDebug())
if (photos.length > 0) {
this.photos = photos
this.awaitingPhoto = false
}
// 未命中时建议退避重试 1~2 次(见 FAQ Q1)
},
clearPhotos() {
if (this.scanTimer) clearTimeout(this.scanTimer)
this.photos = []
this.awaitingPhoto = false
burstCamera.reset()
},
},
}
API 说明
相机
| 方法 | 返回 | 说明 |
|---|---|---|
openCamera(options?) |
Promise<number> |
调起系统相机,返回本次 startTime。resolve 仅代表启动请求已发出 |
scanNewPhotos(startTime?, options?) |
Promise<string[]> |
按时间窗口扫描相册,一次返回该窗口内的整组连拍照片(路径数组,按时间倒序) |
captureAndFetch(options?) |
Promise<Object> |
申请权限 + 调起相机的组合封装,返回 {ok, startTime, code, reason} |
fetchAfterCamera(options?) |
Promise<string[]> |
相机返回后取回照片的便捷封装 |
reset() |
void |
复位内部状态(清空 startTime、扫描标志等) |
权限
| 方法 | 返回 | 说明 |
|---|---|---|
requestPermissions(options?) |
Promise<PermResult> |
申请权限(会真实弹窗),返回三态结果 |
isGranted(permissions?) |
boolean |
只读检测,不弹窗 |
getMissingPermissions(permissions?) |
string[] |
列出未授予的权限 |
getPermissionList() |
string[] |
当前 targetSdk 下应申请的权限清单(与 manifest 声明比对用) |
getLastPermissionResult() |
Object |
最近一次权限申请的原始结果 |
openAppSettings() |
boolean |
跳转系统权限设置页 |
状态与调试
| 方法 | 返回 | 说明 |
|---|---|---|
getStartTime() |
number |
最近一次调起相机的时间戳(ms),0 表示无 |
isScanning() |
boolean |
是否正在扫描 |
hasPendingCapture() |
boolean |
是否处于「已调起相机、等待返回扫描」状态 |
getState() |
Object |
一次返回 {startTime, scanning, pending} |
getLastScanDebug() |
Object |
最近一次扫描诊断 {start, end, pad, source, count, at} |
getErrorCodes() |
Object |
获取错误码枚举表 |
isSupported() |
boolean |
当前环境是否可用(是否 App 端) |
参数表
openCamera(options)
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
packages |
string[] |
自动识别 | 自定义相机包名列表,传入后跳过品牌识别 |
scanNewPhotos(startTime, options)
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
startTime |
number |
内部记录值 | 扫描窗口起点。缺省时用 openCamera() 记录的值 |
options.max |
number |
10 |
连拍张数上限,上限 100(超出被钳制),超出按拍摄时间保留最新的一批 |
options.pad |
number |
3000 |
时间窗口缓冲(ms)。真机取回不全时调大,推荐 8000 |
options.useMS |
boolean |
true |
是否启用 MediaStore 通道 |
options.useFS |
boolean |
true |
是否启用文件系统扫描通道 |
requestPermissions(options)
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
permissions |
string[] |
按 targetSdk 自动构建 |
自定义权限清单 |
wait |
number |
3000 |
等待 plus 就绪的最长毫秒数 |
组件 props
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
max |
Number |
20 |
连拍张数上限,上限 100 |
pad |
Number |
8000 |
时间窗口缓冲(ms) |
scanDelay |
Number |
900 |
相机返回后延迟多久开始扫描(ms) |
retry |
Number |
2 |
扫不到时的重试次数,0 表示不重试 |
retryInterval |
Number |
1500 |
重试间隔(ms) |
autoRequestPermission |
Boolean |
true |
是否在 mounted 时自动申请权限 |
packages |
Array |
[] |
自定义相机包名列表 |
PermResult(权限申请返回值)
| 字段 | 类型 | 说明 |
|---|---|---|
granted |
boolean |
是否全部授予 |
deniedPresent |
string[] |
本次被临时拒绝的权限(可再次申请) |
deniedAlways |
string[] |
被永久拒绝的权限(须引导去系统设置) |
reason |
string |
失败原因说明 |
code |
string |
错误码,见「错误码表」 |
事件表
| 事件 | 触发时机 | payload |
|---|---|---|
@success |
扫描成功,拿到连拍照片 | {photos, count, startTime, debug} |
@error |
任何失败路径(统一出口) | {code, message, raw} |
@scan-empty |
重试耗尽仍为 0 张(非错误) | {startTime, retryCount, debug} |
@camera-opened |
相机启动请求已发出 | {startTime} |
@permission-granted |
权限全部授予 | {result} |
@permission-denied |
权限被拒绝 | {permanent, deniedAlways, deniedPresent, result} |
错误码表
code |
含义 | 建议处理 |
|---|---|---|
NOT_SUPPORTED |
当前非 App 环境(H5/小程序) | 前置判断隐藏入口 |
PLUS_NOT_READY |
plus 未在预期时间内就绪 | 重试或提示重启 App |
CAMERA_OPEN_FAILED |
三种启动方式均失败 | 检查是否装有相机 App |
INVALID_OPTIONS |
传入的 options 不是对象 |
修正调用参数 |
INVALID_START_TIME |
startTime 非法或缺失 |
先调 openCamera() |
SCAN_EMPTY |
两条通道均无结果(正常空结果) | 调大 pad 后重扫 |
SCAN_FAILED |
扫描过程抛异常 | 检查存储权限 |
PERMISSION_DENIED |
权限未全部授予 | 看 deniedPresent/deniedAlways |
PERMISSION_NOT_DECLARED |
回调三态全空,多半 manifest 未声明 | 补声明并重装 |
OPEN_SETTINGS_FAILED |
打开系统设置页失败 | 提示用户手动前往 |
必读:核心机制与三条铁律
连拍照片是怎么拿回来的
点击按钮 → openCamera() 发 Intent 启动「外部相机 App」并记录 startTime
→ 宿主 App 进入后台(Hide)
→ 用户在相机里【连续拍摄多张】并保存到相册
→ 用户返回 → 宿主 App 前台(Show)→ 页面 onShow
→ scanNewPhotos(startTime) 按「时间窗口」在相册里找出窗口内新增的【全部】照片
为什么不直接拿返回值?
plus.runtime.launchApplication 与 main.startActivity 启动的都是独立的外部 App,
宿主逻辑层拿不到任何 Activity 返回结果(没有 onActivityResult 可用)。
因此「时间窗口 + 相册扫描」是纯 JS 方案下唯一可靠的取回路径。
这恰好是连拍的最佳实现方式:既然按时间窗口回扫,窗口内拍的所有照片都会落入结果集, 天然支持「一次调起、拍多张、整组取回」。若依赖单张返回值,反而只能拿到最后一张。
需要拿到确定返回值时,必须改用 UTS 插件或原生语言插件,在原生侧用
startActivityForResult。 那不是本插件的实现方式,两者机制不同,不能混用。
三条铁律
-
必须提供
startTimeopenCamera()在启动前记录时间戳,scanNewPhotos()用它划定区间。缺少它无法定位新照片。 -
必须在
onShow里扫描,且要延迟 相机写文件、MediaStore 建索引都有延迟,刚返回就扫大概率扫不到。 组件已内置 900ms 延迟 + 退避重试;SDK 直连时请自行实现。 -
不要用中间态做守卫
if (!loading && !isScanning)这种写法只要有一处路径没复位状态,守卫就会被永久挡死, 表现为「权限正常、相机能起来、返回后什么都拿不到」。 请改用显式业务状态(如awaitingPhoto)。
常见问题(FAQ)
Q1:权限弹窗正常、相机也能起来,但返回后拿不到照片
按顺序排查:
- 看日志有没有「扫描区间」这一行。
- 没有 → 扫描函数根本没被调用。检查
onShow是否调用了handlePageShow()(组件用法), 或是否写了会被卡死的守卫(SDK 用法,见「三条铁律」第 3 条)。 - 有,但 0 张 → 进入第 2 步。
- 没有 → 扫描函数根本没被调用。检查
- 看
getLastScanDebug()的source字段。'none'→ 两条通道都没跑通,多半是存储权限问题或保存目录非DCIM/Camera。'MediaStore'/'FS'→ 通道正常,是时间窗口对不上,把pad调到8000以上。
- 确认照片确实保存到了相册(不是只停留在相机预览界面的缩略图)。
- 用
scan()或组件的「手动重新扫描」再扫一次,验证是否为落盘延迟。
Q2:连拍了好几张,只拿回一部分
这是时间窗口没覆盖住整段连拍过程。连拍前后各留了缓冲(pad),默认值对慢速连拍可能不够:
- 调大
pad(如8000,甚至12000)。pad是窗口起点前推 + 终点后延的缓冲量。 - 调大
max:默认10(SDK)/20(组件),上限100,超出按拍摄时间保留最新的一批。 可用getLastScanDebug()看truncated是否为true、rawCount是多少,判断是否被截断。 - 别在连拍中途切走页面:中途切走会触发一次
onShow,那次扫描会把capturing复位, 后续照片不再被收集。真需要边拍边处理时才用max分批。
Q3:首次安装不弹权限申请框
按顺序排查:
manifest.json是否声明了权限?(最常见原因) Android 对未声明权限直接判拒绝,既不弹窗也不报错。 插件会在PermResult.code返回PERMISSION_NOT_DECLARED帮助识别。- 声明清单是否与
getPermissionList()一致? 例如targetSdk >= 33时清单会追加READ_MEDIA_IMAGES,manifest 里也必须声明。 - 是否已被永久拒绝? 检查
deniedAlways,若是则必须引导去系统设置(openAppSettings())。 - 改过权限声明后是否重装? 权限声明变更需重新打包才生效。
Q4:每次都扫到上一次的旧照片
pad 给得太大导致时间窗口前伸过多。调小 pad(如 3000),
或在连拍前调用 reset() 清掉上一轮的时间戳。
Q5:需要自定义相机界面 / 内置相机
本插件走的是「调起系统相机」,无法自定义相机 UI。
若要内置相机界面,请改用 uni.chooseImage(App 端可设 sourceType: ['camera'])或自行实现 UTS 插件。
Q6:能否用于 iOS?
不能。iOS 没有 Native.js,无法从 JS 直接调用原生相机。 iOS 端需另做 UTS 插件或在原生侧实现。
Q7:openCamera() resolve 了,是不是代表已经拍完照?
不是。 resolve 只代表「相机启动请求已发出」。
连拍的整组照片必须等扫描完成后,从 @success(组件)或 scanNewPhotos()(SDK)拿到。
Q8:非 App 环境(H5 / 小程序)会怎样?
- 组件:正常渲染,但调用
open()时立即派发@error,code为NOT_SUPPORTED。 - SDK:
openCamera()reject(NOT_SUPPORTED);scanNewPhotos()返回[];requestPermissions()返回granted: false+NOT_SUPPORTED。 - 建议在业务层用
burstCamera.isSupported()做前置判断,在非 App 端隐藏入口。

收藏人数:
下载插件并导入HBuilderX
下载插件ZIP
赞赏(0)
下载 1
赞赏 0
下载 12635235
赞赏 1950
赞赏
京公网安备:11010802035340号