更新记录

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 组件可渲染,但调用时立即派发 @errorNOT_SUPPORTED
微信/支付宝/抖音等小程序 同上;小程序端请改用 uni.chooseImage 等原生 API
uni-app x 本插件为 vue2/vue3 通用前端组件,未适配 uni-app x

判断当前环境是否可用:burstCamera.isSupported(),或在组件里检查 @errorcode === 'NOT_SUPPORTED'


最小接入步骤

1. 安装

hd-burst-camera 目录放入项目的 uni_modules/ 下(或在 HBuilderX 中通过「使用插件」导入)。

插件会自动被 easycom 识别,组件无需手动 import

2. 配置权限(必做,漏配则不弹窗

⚠️ 本插件是纯 JS 实现,没有原生声明文件,无法自带 Android 权限声明。 权限必须由宿主工程manifest.json 中静态声明。 Android 对未声明的权限调用 plus.android.requestPermissions直接判拒绝且不弹窗、不报错(静默失败)

打开 manifest.jsonapp-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> 调起系统相机,返回本次 startTimeresolve 仅代表启动请求已发出
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.launchApplicationmain.startActivity 启动的都是独立的外部 App, 宿主逻辑层拿不到任何 Activity 返回结果(没有 onActivityResult 可用)。 因此「时间窗口 + 相册扫描」是纯 JS 方案下唯一可靠的取回路径。

这恰好是连拍的最佳实现方式:既然按时间窗口回扫,窗口内拍的所有照片都会落入结果集, 天然支持「一次调起、拍多张、整组取回」。若依赖单张返回值,反而只能拿到最后一张。

需要拿到确定返回值时,必须改用 UTS 插件或原生语言插件,在原生侧用 startActivityForResult。 那不是本插件的实现方式,两者机制不同,不能混用。

三条铁律

  1. 必须提供 startTime openCamera() 在启动前记录时间戳,scanNewPhotos() 用它划定区间。缺少它无法定位新照片。

  2. 必须在 onShow 里扫描,且要延迟 相机写文件、MediaStore 建索引都有延迟,刚返回就扫大概率扫不到。 组件已内置 900ms 延迟 + 退避重试;SDK 直连时请自行实现。

  3. 不要用中间态做守卫 if (!loading && !isScanning) 这种写法只要有一处路径没复位状态,守卫就会被永久挡死, 表现为「权限正常、相机能起来、返回后什么都拿不到」。 请改用显式业务状态(如 awaitingPhoto)。


常见问题(FAQ)

Q1:权限弹窗正常、相机也能起来,但返回后拿不到照片

按顺序排查:

  1. 看日志有没有「扫描区间」这一行。
    • 没有 → 扫描函数根本没被调用。检查 onShow 是否调用了 handlePageShow()(组件用法), 或是否写了会被卡死的守卫(SDK 用法,见「三条铁律」第 3 条)。
    • 有,但 0 张 → 进入第 2 步。
  2. getLastScanDebug()source 字段。
    • 'none' → 两条通道都没跑通,多半是存储权限问题或保存目录非 DCIM/Camera
    • 'MediaStore' / 'FS' → 通道正常,是时间窗口对不上,把 pad 调到 8000 以上。
  3. 确认照片确实保存到了相册(不是只停留在相机预览界面的缩略图)。
  4. scan() 或组件的「手动重新扫描」再扫一次,验证是否为落盘延迟。

Q2:连拍了好几张,只拿回一部分

这是时间窗口没覆盖住整段连拍过程。连拍前后各留了缓冲(pad),默认值对慢速连拍可能不够:

  1. 调大 pad(如 8000,甚至 12000)。pad 是窗口起点前推 + 终点后延的缓冲量。
  2. 调大 max:默认 10(SDK)/ 20(组件),上限 100,超出按拍摄时间保留最新的一批。 可用 getLastScanDebug()truncated 是否为 truerawCount 是多少,判断是否被截断。
  3. 别在连拍中途切走页面:中途切走会触发一次 onShow,那次扫描会把 capturing 复位, 后续照片不再被收集。真需要边拍边处理时才用 max 分批。

Q3:首次安装不弹权限申请框

按顺序排查:

  1. manifest.json 是否声明了权限?(最常见原因) Android 对未声明权限直接判拒绝,既不弹窗也不报错。 插件会在 PermResult.code 返回 PERMISSION_NOT_DECLARED 帮助识别。
  2. 声明清单是否与 getPermissionList() 一致? 例如 targetSdk >= 33 时清单会追加 READ_MEDIA_IMAGES,manifest 里也必须声明。
  3. 是否已被永久拒绝? 检查 deniedAlways,若是则必须引导去系统设置(openAppSettings())。
  4. 改过权限声明后是否重装? 权限声明变更需重新打包才生效。

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() 时立即派发 @errorcodeNOT_SUPPORTED
  • SDK:openCamera() reject(NOT_SUPPORTED);scanNewPhotos() 返回 []requestPermissions() 返回 granted: false + NOT_SUPPORTED
  • 建议在业务层用 burstCamera.isSupported() 做前置判断,在非 App 端隐藏入口。

许可证

MIT

隐私、权限声明

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

android.permission.CAMERA —— 调起系统相机拍摄 android.permission.READ_EXTERNAL_STORAGE —— 扫描相册,取回刚拍摄的照片 android.permission.WRITE_EXTERNAL_STORAGE —— 相机写入照片文件 android.permission.READ_MEDIA_IMAGES —— 仅设备 targetSdkVersion ≥ 33 时申请,用于读取相册图片 说明:本插件为纯 JS 实现,无原生声明文件,上述权限需自行在 manifest.json 中静态声明。 Android 对未声明的权限调用 requestPermissions 会直接判拒绝且不弹窗,插件会在返回值中给出 PERMISSION_NOT_DECLARED 便于识别。

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

插件不采集任何数据。相机调用与相册读取全部在设备本地完成,不涉及任何网络请求、不上传任何数据到服务器。

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

许可协议

MIT License

Copyright (c) 2026 慧鼎科技

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.