更新记录

0.0.1(2026-09-08)

功能

  • 相机实时预览(内嵌在页面任意区域)
  • 拍照,返回照片文件路径(taken 事件)
  • 前后摄像头切换(facing 属性 / switchCamera 方法)
  • 闪光灯 / 手电筒(setFlash 方法)
  • 运行时权限自动申请,被拒时通过 error 事件通知
  • 预览就绪 ready 事件、错误码规范化

平台兼容性

uni-app(4.0)

Vue2 Vue3 Chrome Safari app-vue app-nvue Android iOS 鸿蒙
× × × × × 5.0 × ×
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
× × × × × × × × × × × ×

uts-ccamera 自定义相机(Camera2)

基于 Android Camera2 API 的自定义相机 UTS 组件插件。以组件方式内嵌到页面 template 中,无需三方依赖,标准基座即可真机运行。

功能

  • 相机实时预览(内嵌在页面任意区域)
  • 拍照,返回照片文件路径(taken 事件)
  • 前后摄像头切换(facing 属性 / switchCamera 方法)
  • 闪光灯 / 手电筒(setFlash 方法)
  • 运行时权限自动申请,被拒时通过 error 事件通知
  • 预览就绪 ready 事件、错误码规范化

平台兼容性

平台 支持 说明
app-android Android 5.0+(API 21),uni-app 需 HBuilderX 3.6.18+,uni-app x 需 HBuilderX 3.9+
app-ios × 未实现,请勿在 iOS 页面引入
app-harmony × 不支持(uni-app 兼容模式组件限制)
H5 / 小程序 × 未实现,请用条件编译隔离

⚠️ 重要:本插件为 uts 组件(uni-app 兼容模式)。uni-app 项目请在 nvue 页面中使用;uni-app x 项目请在 uvue 页面(VDOM 模式)中使用,不支持 uni-app x 蒸汽模式

权限与原生配置

插件已在 utssdk/app-android/AndroidManifest.xml 中声明:

<uses-permission android:name="android.permission.CAMERA" />
  • 运行时权限:组件在 NVLoaded 阶段自动调用 UTSAndroid.requestSystemPermission 申请相机权限,无需前端再调用 uni.authorize。用户拒绝后会触发 error 事件(errCode=1001),可在此时提示用户前往系统设置(UTSAndroid.gotoSystemPermissionActivity)。
  • 标准基座:如真机运行时报无相机权限,请在项目 manifest.json → App 模块配置中勾选 Camera 权限,或制作自定义基座(涉及 AndroidManifest/原生配置的变更需自定义基座才能生效)。
  • 云打包:插件 manifest 会自动合并,无需额外配置。

快速开始

⚠️ 组件标签必须使用显式闭合></uts-ccamera>),不要用自闭合 />——uni-app x 的模板编译器对自定义组件自闭合支持不完整,会报「Element is missing end tag」。

<template>
  <view>
    <!-- ⚠️ 必须用 style 指定组件宽高,否则组件不显示 -->
    <uts-ccamera
      ref="cam"
      :facing="facing"
      style="width:375px;height:500px;"
      @ready="onReady"
      @error="onError"
      @taken="onTaken"></uts-ccamera>
    <button @tap="shoot">拍照</button>
  </view>
</template>

组件属性

属性 类型 默认值 说明
facing String "back" 摄像头朝向:"back" 后置 / "front" 前置。动态修改会自动切换摄像头

组件事件

事件 参数(Map) 说明
ready facing: string 预览会话就绪,可以开始拍照
error errCode: number, errMsg: string 发生错误,见错误码表
taken filePath: string, width: number, height: number 拍照完成,照片已写入磁盘

错误码

errCode 含义
1001 相机权限被用户拒绝
1002 相机打开失败(openCamera / 设备错误)
1003 捕获会话配置失败
1004 拍照请求失败
1005 相机未就绪 / 未找到指定朝向的摄像头 / 上下文缺失
1006 照片写入磁盘失败

组件方法

通过 ref 调用。拍照结果仅通过 taken 事件异步返回(nvue 不支持组件方法同步返回值)。

方法 参数 说明
takePhoto() 拍照,结果经 taken 事件返回;进行中重复调用会被忽略
switchCamera(facing : string) "back" / "front" 切换摄像头
setFlash(mode : string) "off" / "on" / "torch" "off" 关闭;"on" 拍照瞬间自动闪光;"torch" 手电筒常亮(预览中即时生效)
startPreview() 开始/恢复预览(权限被拒后重试、停止后恢复)
stopPreview() 停止预览并释放相机,建议退后台时调用

uni-app 项目调用示例(nvue 页面)

⚠️ 必须将页面文件命名为 xxx.nvue(不是 .vue。uts 组件只能在 nvue 页面使用;放在 .vue 页面时组件不被识别,$refs 上没有组件方法,会报 takePhoto is not a function

⚠️ 本节示例仅适用于 uni-app 项目(页面后缀 .nvue,JS 代码)。如果你是 uni-app x 项目(页面后缀 .uvue,请直接使用下一节的 uvue 示例——两套代码不通用,把 JS 示例粘贴进 uvue 会报「An explicit type is required on a value parameter」等类型错误。

<template>
  <view style="flex:1;">
    <uts-ccamera
      ref="cam"
      :facing="facing"
      style="width:375px;height:500px;background-color:#000000;"
      @ready="onReady"
      @error="onError"
      @taken="onTaken"></uts-ccamera>
    <view style="flex-direction:row;">
      <button @tap="shoot">拍照</button>
      <button @tap="flip">翻转</button>
      <button @tap="torch">手电筒</button>
    </view>
    <text>{{ status }}</text>
  </view>
</template>

<script>
export default {
  data() {
    return {
      facing: "back",
      status: ""
    }
  },
  methods: {
    // 统一调用组件方法的辅助函数:
    // 兼容 ref 直调、ref 为数组包装、以及部分版本(vue3)需要 $callMethod 桥接的情况
    callCam(name, ...args) {
      let ref = this.$refs["cam"] || this.$refs.cam
      if (Array.isArray(ref)) {
        ref = ref[0]
      }
      if (!ref) {
        console.log("未找到相机组件 ref,请确认页面为 .nvue")
        return
      }
      if (typeof ref[name] === "function") {
        ref[name].apply(ref, args)
      } else if (typeof ref.$callMethod === "function") {
        ref.$callMethod(name, ...args)
      } else {
        console.log("组件方法不可用:请确认页面文件后缀为 .nvue,且 HBuilderX ≥ 3.6.18")
      }
    },
    shoot() {
      this.callCam("takePhoto")
    },
    flip() {
      this.facing = this.facing === "back" ? "front" : "back"
      // 也可以用组件方法:this.callCam("switchCamera", "front")
    },
    torch() {
      this.callCam("setFlash", "torch")
    },
    onReady() {
      this.status = "相机就绪"
    },
    onError(e) {
      // 事件参数在不同环境下可能直接是 Map/对象,也可能包在 e.detail 中,做兼容处理
      const d = e.detail || e
      this.status = "错误 " + d.errCode + ": " + d.errMsg
    },
    onTaken(e) {
      const d = e.detail || e
      this.status = "拍照完成"
      uni.previewImage({
        urls: [d.filePath]
      })
    }
  }
}
</script>

事件参数在 uni-app(JS 环境)中经桥接转换,可能通过 e.detail 访问字段,也可能直接是对象(示例已做兼容)。

进阶:自定义相机页完整示例(nvue 页面)

组件只负责出预览画面和拍照,页面 UI 完全由你用 vue 模板自由设计——取景框、快门按钮、遮罩、水印、提示文字都可以任意叠加。下面是一个完整的身份证取景风格相机页(pages/camera/camera.nvue,记得在 pages.json 中注册页面):

<template>
  <view style="flex:1;background-color:#000000;">
    <!-- 顶部栏 -->
    <view style="height:88px;flex-direction:row;align-items:center;justify-content:space-between;padding-left:24px;padding-right:24px;">
      <text style="color:#ffffff;font-size:30px;" @click="goBack">关闭</text>
      <text style="color:#ffffff;font-size:30px;" @click="toggleFlash">{{ flashText }}</text>
    </view>

    <!-- 预览区 + 取景遮罩 -->
    <view style="flex:1;align-items:center;justify-content:center;">
      <view style="width:600px;height:800px;">
        <uts-ccamera
          ref="cam"
          :facing="facing"
          style="width:600px;height:800px;background-color:#111111;"
          @ready="onReady" @error="onError" @taken="onTaken"></uts-ccamera>
        <!-- 四角取景框(叠加在组件上层) -->
        <view style="position:absolute;left:0;top:0;width:60px;height:60px;border-left-width:4px;border-top-width:4px;border-color:#00FF88;"></view>
        <view style="position:absolute;right:0;top:0;width:60px;height:60px;border-right-width:4px;border-top-width:4px;border-color:#00FF88;"></view>
        <view style="position:absolute;left:0;bottom:0;width:60px;height:60px;border-left-width:4px;border-bottom-width:4px;border-color:#00FF88;"></view>
        <view style="position:absolute;right:0;bottom:0;width:60px;height:60px;border-right-width:4px;border-bottom-width:4px;border-color:#00FF88;"></view>
      </view>
      <text style="color:#ffffff;font-size:26px;margin-top:24px;">{{ tipText }}</text>
    </view>

    <!-- 底部操作栏 -->
    <view style="height:180px;flex-direction:row;align-items:center;justify-content:space-around;">
      <text style="color:#ffffff;font-size:28px;width:120px;text-align:center;" @click="flip">翻转</text>
      <view @click="shoot" style="width:140px;height:140px;border-radius:70px;background-color:#ffffff;align-items:center;justify-content:center;">
        <view style="width:110px;height:110px;border-radius:55px;background-color:#FF3B30;"></view>
      </view>
      <text style="color:#ffffff;font-size:28px;width:120px;text-align:center;" @click="torch">手电</text>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      facing: "back",
      flashMode: "off",
      tipText: "正在启动相机..."
    }
  },
  computed: {
    flashText() {
      return this.flashMode === "torch" ? "⚡ 已开" : "⚡ 关闭"
    }
  },
  methods: {
    callCam(name, ...args) {
      let ref = this.$refs["cam"] || this.$refs.cam
      if (Array.isArray(ref)) { ref = ref[0] }
      if (!ref) { return }
      if (typeof ref[name] === "function") {
        ref[name].apply(ref, args)
      } else if (typeof ref.$callMethod === "function") {
        ref.$callMethod(name, ...args)
      }
    },
    shoot() {
      this.tipText = "拍照中..."
      this.callCam("takePhoto")
    },
    flip() {
      this.facing = this.facing === "back" ? "front" : "back"
      this.tipText = "切换摄像头..."
    },
    torch() {
      this.flashMode = this.flashMode === "torch" ? "off" : "torch"
      this.callCam("setFlash", this.flashMode)
    },
    toggleFlash() {
      this.torch()
    },
    goBack() {
      uni.navigateBack()
    },
    onReady() {
      this.tipText = "请将证件放入取景框"
    },
    onError(e) {
      const d = e.detail || e
      this.tipText = "错误 " + d.errCode + ":" + (d.errCode === 1001 ? "请授予相机权限后重试" : d.errMsg)
      if (d.errCode === 1001) {
        this.callCam("startPreview") // 可在此弹出引导后重试
      }
    },
    onTaken(e) {
      const d = e.detail || e
      this.tipText = "已保存:" + d.filePath
      // 实际业务中可在此上传:uni.uploadFile({ filePath: d.filePath, ... })
    }
  }
}
</script>

nvue 布局提示:nvue 仅支持 flex 布局,默认 flex-direction: column;尺寸单位 px 为逻辑像素(默认 750px 等于屏宽);叠加层用 position: absolute。更多 nvue 样式约束见官方文档。

uni-app x 项目调用示例(uvue 页面,VDOM 模式)

⚠️ 本节示例仅适用于 uni-app x 项目(页面后缀 .uvuescript lang="uts" 强类型代码)。uni-app 项目(.nvue 页面)请使用前面的 nvue 示例,两套代码不通用。

<template>
  <view style="flex:1;">
    <uts-ccamera
      id="camera"
      ref="cam"
      :facing="facing"
      style="width:375px;height:500px;background-color:#000000;"
      @ready="onReady"
      @error="onError"
      @taken="onTaken"></uts-ccamera>
    <button @click="shoot">拍照</button>
  </view>
</template>

<script lang="uts">
  // Element 类名 = 组件名转 upper camel case + Element,模块名 = 插件目录名转 lower camel case
  // 若 HBuilderX 自动提示的名称不同,以实际生成为准
  import { UtsCcameraElement } from 'uts.sdk.modules.utsCcamera'

  export default {
    data() {
      return {
        facing: "back"
      }
    },
    methods: {
      shoot() {
        (this.$refs["cam"] as UtsCcameraElement).takePhoto()
        // 也可以: (uni.getElementById("camera") as UtsCcameraElement).takePhoto()
      },
      onReady() {
        console.log("相机就绪")
      },
      onError(e : Map<string, any>) {
        console.log("错误码:", e.get('errCode'), "信息:", e.get('errMsg'))
      },
      onTaken(e : Map<string, any>) {
        let path = e.get('filePath') as string
        uni.previewImage({
          urls: [path]
        })
      }
    }
  }
</script>

uni-app x 的 Android 端事件参数为 Map<string, any>,用 e.get('字段名') 取值。

注意事项 / 已知限制

  1. 必须用 style 指定组件宽高,否则组件不显示(uts 组件通用要求)。
  2. 同一页面同时仅支持一个实例(v1 限制)。多个实例时后创建的会覆盖前一个的内部状态。
  3. 照片保存在应用外部私有目录 Android/data/<包名>/files/Pictures/uts-ccamera/无需存储权限,卸载应用即删除。返回的 filePath 为绝对路径,可直接用于 image 组件 src、uni.previewImageuni.uploadFile 等。
  4. 拍照方向按竖屏场景计算(JPEG_ORIENTATION 已处理传感器方向与屏幕旋转);横屏场景请自行扩展 computeJpegOrientation
  5. 暂不支持:视频录制、手动对焦/变焦/曝光调节、EXIF 信息写入。
  6. 退后台建议调用 stopPreview(),回前台调用 startPreview(),避免相机被系统回收或与其他应用冲突。
  7. iOS / 小程序 / H5 未实现,请在这些平台用条件编译移除相关代码。
  8. uni-app x 蒸汽模式不支持 uni-app 兼容模式组件,请使用 VDOM 模式项目。

FAQ

Q:调用组件方法报 takePhoto is not a function A:按顺序排查:1) 页面文件后缀必须是 .nvue(uts 组件不支持 .vue 页面,这是最常见原因,此时组件标签也不会被识别渲染);2) 若 console.log(this.$refs["cam"]) 输出 undefined,检查 ref 名称拼写;3) vue3 项目部分版本 ref 代理不直通原生方法,改用 ref.$callMethod('takePhoto')(readme 示例中的 callCam 辅助函数已自动兼容);4) 确认 HBuilderX ≥ 3.6.18。

Q:预览黑屏? A:按顺序排查:1) 监听 error 事件,errCode=1001 是权限被拒;2) 确认页面是 nvue(uni-app 项目);3) 确认组件 style 指定了非零宽高;4) 模拟器/设备是否有摄像头;5) 在 ready 触发前调用 takePhoto 会返回 1005,属正常保护。

Q:标准基座运行报权限错误? A:插件 manifest 中的权限声明需自定义基座才能完整生效。临时方案:在项目 manifest.json 中勾选 Camera 权限;正式方案:制作自定义基座。

Q:与 uni.createCameraContext(camera 组件)的区别? A:官方 camera 组件功能固定(受系统相机封装限制);本插件直接基于 Camera2,可完全控制预览输出尺寸、拍照请求参数(闪光灯、方向等),适合需要深度定制取景框、水印、连拍等场景的开发者。

Q:.vue 页面能用组件吗?想要完全自定义的相机 UI 怎么做? A:不能。.vue 页面是 WebView 渲染,放不进原生相机画面,uts 组件只支持 nvue 页面(uni-app)——这是平台架构限制。需要完全自定义的相机界面(取景框、快门、水印等自由排版)时,新建一个 nvue 页面承载 <uts-ccamera> 组件即可,页面 UI 用 vue 模板随意设计,见上文「进阶:自定义相机页完整示例」;从 .vue 页面 uni.navigateTo 跳转到该 nvue 相机页是标准用法。

Q:照片方向不对? A:本插件按竖屏优化。横屏使用时请修改 index.vuecomputeJpegOrientation 的 rotation 处理逻辑。 A:本插件按竖屏优化。横屏使用时请修改 index.vuecomputeJpegOrientation 的 rotation 处理逻辑。

隐私、权限声明

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

<uses-permission android:name="android.permission.CAMERA" />

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

插件不采集任何数据

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

暂无用户评论。