更新记录

1.0.0(2026-05-04)

  • 发布插件

平台兼容性

uni-app x(5.07)

Chrome Safari Android Android插件版本 iOS 鸿蒙 微信小程序
- - 5.0 1.0.0 × × -

yuange-camera 自定义相机组件

uvue 原生相机组件,支持拍照、录像、手动对焦、双指缩放、曝光调节、闪光灯控制、取色等功能。

快速开始

1. 模板中放置相机组件

<template>
  <view class="camera-wrapper">
    <native-camerapreview
      id="cameraPreview"
      :width="cameraWidth"
      :height="cameraHeight"
    ></native-camerapreview>
  </view>
</template>

2. 初始化相机上下文

import {
  CreateNativeCameraContext,
  INativeCameraContext,
  OnCallBackOptions,
  CameraStartOptions,
  TakePhotoOptions,
  StartRecordOptions
} from "@/uni_modules/yuange-camera";

let context: INativeCameraContext | null = null;

// 初始化(建议在页面 onLoad 中调用)
context = CreateNativeCameraContext("cameraPreview", this);

3. 启动相机预览

context?.start({
  scale: '16:9',   // '16:9' / '4:3' / '1:1'
  hd: true,        // 是否高清
  cameraId: 0,     // 0=后摄,1=前摄
  noRecordPermissions: false,
  showSetting: true,
} as CameraStartOptions, {
  success(res) { console.log("启动成功", res); },
  fail(err)    { console.log("启动失败", err); }
} as OnCallBackOptions);

start 参数说明:

参数 类型 默认值 说明
scale String '16:9' 成像比例:'16:9' / '4:3' / '1:1'
hd Boolean false 是否高清(true=1080p,false=720p)
cameraId Int 0 摄像头:0=后置,1=前置
noRecordPermissions Boolean true 是否跳过录音权限申请
showSetting Boolean true 无权限时是否提示去设置

API 列表

设置摄像头

// true = 后置摄像头(默认),false = 前置
context?.setIsbackCamera(true);

启动预览

context?.start(options as CameraStartOptions, callback as OnCallBackOptions);

停止预览

// 建议在 onHide 中调用
context?.stop();

销毁相机

// 建议在 onUnload 中调用
context?.destroy();

拍照

context?.takePhoto({
  quality: 90,       // 图像质量 1-100
  width: 0,          // 输出宽度(px),0=原图
  returnFile: true,  // true=返回文件路径,false=返回 base64
  savePath: '',      // 保存路径(绝对路径),空=保存到 DCIM
  fileName: 'my.jpg',
  crop: [0, 0, 100, 100],  // [x, y, w, h],百分比
} as TakePhotoOptions, {
  success(res) {
    const url = res.get("url");   // returnFile=true 时
    // const b64 = res.get("base64"); // returnFile=false 时
  },
  fail(err) { console.log(err); }
} as OnCallBackOptions);

takePhoto 参数说明:

参数 类型 默认值 说明
quality Int 90 JPEG 质量 1-100
width Int 0 输出宽度(像素),0=原始尺寸
returnFile Boolean false true=返回 url,false=返回 base64
savePath String '' 文件保存目录(绝对路径)
fileName String '' 文件名,空=自动生成时间戳文件名
crop Array\<Int> [0,0,100,100] 裁剪区域 [x,y,w,h],单位为百分比

开始录像

context?.startRecord({
  savePath: '',
  fileName: 'video.mp4',
} as StartRecordOptions, {
  success(res) {
    const path = res.get("path"); // 文件保存路径
  },
  fail(err) { console.log(err); }
} as OnCallBackOptions);

startRecord 参数说明:

参数 类型 默认值 说明
savePath String '' 视频保存路径(绝对路径),空=保存到 DCIM
fileName String '' 视频文件名,空=自动生成

停止录像

context?.stopRecord({
  success(res) { console.log("停止成功"); },
  fail(err)    { console.log(err); }
} as OnCallBackOptions);

设置缩放

// zoom 范围:1.0 ~ maxZoom(通过 getMaxZoom 获取)
context?.setZoom(2.0, {
  success(res) {
    const actualZoom = res.get("zoom"); // 实际生效的缩放倍数
  },
  fail(err) { console.log(err); }
} as OnCallBackOptions);

获取最大缩放倍数

context?.getMaxZoom({
  success(res) {
    const maxZoom = res.get("maxZoom"); // Float
    console.log("最大缩放:", maxZoom);
  },
  fail(err) { console.log(err); }
} as OnCallBackOptions);

手动对焦

// x, y 为预览区域的百分比坐标(0-100)
context?.setFocus(50, 50, {
  success(res) { console.log("对焦成功"); },
  fail(err)    { console.log(err); }
} as OnCallBackOptions);

设置曝光

// exposure: 0.0(最暗)~ 1.0(最亮),0.5 为正常
context?.setExposure(0.5, {
  success(res) {
    const compensation = res.get("compensation"); // AE 补偿等级
  },
  fail(err) { console.log(err); }
} as OnCallBackOptions);

设置闪光灯模式

// mode: "off" | "on" | "torch" | "auto"
// - off:   关闭(默认)
// - on:    拍照时触发闪光
// - torch: 手电筒常亮
// - auto:  自动闪光
context?.setFlashMode("torch", {
  success(res) { console.log("设置成功"); },
  fail(err)    { console.log(err); }
} as OnCallBackOptions);

获取颜色

// x, y 为预览区域的百分比坐标(0-100)
context?.getColor(50, 50, {
  success(res) {
    const color = res.get("color"); // HEX 颜色值,如 "#FF3A2C"
    const r = res.get("r");         // 红色分量 0-255
    const g = res.get("g");         // 绿色分量 0-255
    const b = res.get("b");         // 蓝色分量 0-255
  },
  fail(err) { console.log(err); }
} as OnCallBackOptions);

完整使用示例 camera-demo.uvue


<template>
  <view class="container">
    <!-- 相机预览区 -->
    <view class="camera-wrapper" @tap="onCameraClick">
      <native-camerapreview
        id="cameraPreview"
        :width="cameraWidth"
        :height="cameraHeight"
      ></native-camerapreview>

      <!-- 对焦框 -->
      <view
        v-if="focusVisible"
        class="focus-box"
        :style="{ left: focusLeft + 'px', top: focusTop + 'px' }"
      ></view>

      <!-- 取色块 -->
      <view
        v-if="colorVisible && colorValue.length > 0"
        class="color-badge"
        :style="{ backgroundColor: colorValue }"
      >
        <text class="color-text">{{ colorValue }}</text>
      </view>
    </view>

    <!-- 闪光灯 + 切换摄像头 -->
    <view class="top-bar">
      <button size="mini" :type="flashMode === 'torch' ? 'warn' : 'default'" @tap="toggleFlash">
        {{ flashMode === 'torch' ? '手电筒: 开' : flashMode === 'on' ? '闪光: 拍照' : flashMode === 'auto' ? '闪光: 自动' : '闪光: 关' }}
      </button>
      <button size="mini" type="default" @tap="cycleFlashMode">切换闪光模式</button>
      <button size="mini" type="primary" @tap="switchCamera">切换摄像头</button>
    </view>

    <!-- 缩放滑块 -->
    <view class="zoom-bar">
      <text class="zoom-label">缩放 {{ currentZoom.toFixed(1) }}x</text>
      <slider
        class="zoom-slider"
        :min="10"
        :max="maxZoom * 10"
        :value="currentZoom * 10"
        @change="onZoomChange"
      />
      <text class="zoom-max">{{ maxZoom.toFixed(0) }}x</text>
    </view>

    <!-- 曝光滑块 -->
    <view class="zoom-bar">
      <text class="zoom-label">曝光 {{ (currentExposure * 100).toFixed(0) }}%</text>
      <slider
        class="zoom-slider"
        :min="0"
        :max="100"
        :value="currentExposure * 100"
        @change="onExposureChange"
      />
    </view>

    <!-- 控制按钮区 -->
    <view class="controls">
      <view class="btn-row">
        <button type="primary" size="mini" @tap="initCamera">初始化相机</button>
        <button type="primary" size="mini" @tap="startCamera">启动相机</button>
        <button type="warn" size="mini" @tap="stopCamera">停止相机</button>
      </view>

      <view class="btn-row">
        <button type="primary" size="mini" @tap="takePhoto">拍照</button>
        <button type="default" size="mini" @tap="toggleColorMode">
          {{ colorMode ? '关闭取色' : '开启取色' }}
        </button>
      </view>

      <view class="btn-row">
        <button type="primary" size="mini" @tap="startRecord" :disabled="isRecording">开始录像</button>
        <button type="warn" size="mini" @tap="stopRecord" :disabled="!isRecording">停止录像</button>
      </view>
    </view>

    <!-- 拍照预览 -->
    <view class="preview" v-if="photoPath">
      <text class="section-title">拍照预览</text>
      <image :src="photoPath" mode="aspectFit" class="preview-image"></image>
    </view>

    <!-- 操作日志 -->
    <view class="log-wrapper">
      <view class="log-header">
        <text class="section-title">操作日志</text>
        <button size="mini" type="default" @tap="clearLogs">清空</button>
      </view>
      <scroll-view scroll-y class="log-scroll">
        <text v-for="(log, index) in logs" :key="index" :class="log.startsWith('[ERR]') ? 'log-item log-err' : 'log-item'">{{ log }}</text>
      </scroll-view>
    </view>
  </view>
</template>

<script>
  import {
    CreateNativeCameraContext,
    INativeCameraContext,
    OnCallBackOptions,
    CameraStartOptions,
    TakePhotoOptions,
    StartRecordOptions
  } from "@/uni_modules/yuange-camera";

  let context: INativeCameraContext | null = null;
  let isBackCamera = true;
  let currentZoomVal = 1.0;
  let currentExposureVal = 0.5;

  export default {
    data() {
      return {
        cameraWidth: 1080,
        cameraHeight: 1920,
        photoPath: '',
        logs: [] as string[],
        isRecording: false,
        // 缩放
        currentZoom: 1.0,
        maxZoom: 10.0,
        // 曝光
        currentExposure: 0.5,
        // 闪光灯模式:off / on / torch / auto
        flashMode: 'off',
        // 对焦动画
        focusVisible: false,
        focusLeft: 0,
        focusTop: 0,
        // 取色模式
        colorMode: false,
        colorVisible: false,
        colorValue: ''
      }
    },

    onLoad() {
      const systemInfo = uni.getSystemInfoSync();
      this.cameraWidth = systemInfo.windowWidth;
      this.cameraHeight = Math.floor(systemInfo.windowHeight * 0.45);
      this.addLog("页面加载完成,点击「初始化相机」后启动");
    },

    onUnload() {
      if (context != null) {
        context?.destroy();
        context = null;
        this.addLog("相机已销毁");
      }
    },

    onHide() {
      if (context != null) {
        context?.stop();
        this.addLog("相机已停止(onHide)");
      }
    },

    methods: {
      addLog(msg: string) {
        const time = new Date().toLocaleTimeString();
        this.logs.unshift(`[${time}] ${msg}`);
        if (this.logs.length > 80) {
          this.logs.pop();
        }
      },

      clearLogs() {
        this.logs = [];
      },

      // ---- 初始化 & 启停 ----
      initCamera() {
        context = CreateNativeCameraContext("cameraPreview", this);
        if (context != null) {
          this.addLog("相机初始化成功");
          // 启动后查询最大缩放
          this.fetchMaxZoom();
        } else {
          this.addLog("[ERR] 相机初始化失败,请检查组件 id 是否匹配");
        }
      },

      fetchMaxZoom() {
        if (context == null) return;
        context?.getMaxZoom({
          success: (res) => {
            const mz = res.get("maxZoom");
            if (mz != null) {
              this.maxZoom = parseFloat(mz as string);
              this.addLog("最大缩放倍数: " + this.maxZoom + "x");
            }
          },
          fail: (err) => {
            // 相机未启动时正常,忽略
          }
        } as OnCallBackOptions);
      },

      startCamera() {
        if (context == null) {
          this.addLog("[ERR] 请先点击「初始化相机」");
          return;
        }

        context?.start({
          scale: '16:9',
          hd: true,
          cameraId: isBackCamera ? 0 : 1,
          noRecordPermissions: false,
          showSetting: true,
        } as CameraStartOptions, {
          success: (res) => {
            this.addLog("启动相机成功");
            setTimeout(() => { this.fetchMaxZoom(); }, 500);
          },
          fail: (err) => {
            this.addLog("[ERR] 启动相机失败: " + JSON.stringify(err));
          }
        } as OnCallBackOptions);
      },

      stopCamera() {
        if (context == null) {
          this.addLog("相机未初始化");
          return;
        }
        context?.stop();
        this.addLog("相机已停止");
      },

      switchCamera() {
        if (context == null) {
          this.addLog("[ERR] 相机未初始化");
          return;
        }
        isBackCamera = !isBackCamera;
        context?.setIsbackCamera(isBackCamera);
        this.addLog("切换到" + (isBackCamera ? "后置" : "前置") + "摄像头");
      },

      // ---- 拍照 ----
      takePhoto() {
        if (context == null) {
          this.addLog("[ERR] 相机未初始化");
          return;
        }

        const fileName = 'photo_' + Date.now() + '.jpg';
        this.addLog("正在拍照...");

        context?.takePhoto({
          quality: 90,
          width: 0,
          returnFile: true,
          savePath: '',
          fileName: fileName,
          crop: [0, 0, 100, 100],
        } as TakePhotoOptions, {
          success: (res) => {
            const url = res.get("url") as string | null;
            if (url != null) {
              this.photoPath = url;
              this.addLog("拍照成功: " + url);
            }
          },
          fail: (err) => {
            this.addLog("[ERR] 拍照失败: " + JSON.stringify(err));
          }
        } as OnCallBackOptions);
      },

      // ---- 录像 ----
      startRecord() {
        if (context == null) {
          this.addLog("[ERR] 相机未初始化");
          return;
        }

        const fileName = 'video_' + Date.now() + '.mp4';
        this.addLog("开始录像...");

        context?.startRecord({
          savePath: '',
          fileName: fileName,
        } as StartRecordOptions, {
          success: (res) => {
            this.isRecording = true;
            const path = res.get("path") as string | null;
            this.addLog("开始录像成功,保存至: " + (path ?? "DCIM"));
          },
          fail: (err) => {
            this.addLog("[ERR] 开始录像失败: " + JSON.stringify(err));
          }
        } as OnCallBackOptions);
      },

      stopRecord() {
        if (context == null) {
          this.addLog("[ERR] 相机未初始化");
          return;
        }

        this.addLog("停止录像...");
        context?.stopRecord({
          success: (res) => {
            this.isRecording = false;
            this.addLog("停止录像成功");
          },
          fail: (err) => {
            this.isRecording = false;
            this.addLog("[ERR] 停止录像失败: " + JSON.stringify(err));
          }
        } as OnCallBackOptions);
      },

      // ---- 缩放 ----
      onZoomChange(e: UniSliderChangeEvent) {
        if (context == null) return;
        const zoom = e.detail.value / 10.0;
        currentZoomVal = zoom;
        this.currentZoom = zoom;

        context?.setZoom(zoom.toFloat(), {
          success: (res) => {
            const actualZoom = res.get("zoom");
            if (actualZoom != null) {
              this.currentZoom = parseFloat(actualZoom as string);
            }
          },
          fail: (err) => {
            this.addLog("[ERR] 设置缩放失败: " + JSON.stringify(err));
          }
        } as OnCallBackOptions);
      },

      // ---- 曝光 ----
      onExposureChange(e: UniSliderChangeEvent) {
        if (context == null) return;
        const exposure = e.detail.value / 100.0;
        currentExposureVal = exposure;
        this.currentExposure = exposure;

        context?.setExposure(exposure.toFloat(), {
          success: (res) => {},
          fail: (err) => {
            this.addLog("[ERR] 设置曝光失败: " + JSON.stringify(err));
          }
        } as OnCallBackOptions);
      },

      // ---- 闪光灯 ----
      cycleFlashMode() {
        const modes = ['off', 'on', 'torch', 'auto'];
        const idx = modes.indexOf(this.flashMode);
        this.flashMode = modes[(idx + 1) % modes.length];
        this.applyFlashMode();
      },

      toggleFlash() {
        if (this.flashMode === 'torch') {
          this.flashMode = 'off';
        } else {
          this.flashMode = 'torch';
        }
        this.applyFlashMode();
      },

      applyFlashMode() {
        if (context == null) {
          this.addLog("[ERR] 相机未初始化");
          return;
        }
        context?.setFlashMode(this.flashMode, {
          success: (res) => {
            this.addLog("闪光灯模式: " + this.flashMode);
          },
          fail: (err) => {
            this.addLog("[ERR] 设置闪光灯失败: " + JSON.stringify(err));
          }
        } as OnCallBackOptions);
      },

      // ---- 点击对焦 / 取色 ----
      onCameraClick(e: UniPointerEvent) {
        if (context == null) return;

        const clickX = e.x as number;
        const clickY = e.y as number;

        // 显示对焦框
        this.focusLeft = clickX - 30;
        this.focusTop = clickY - 30;
        this.focusVisible = true;
        setTimeout(() => { this.focusVisible = false; }, 1500);

        // 计算百分比坐标
        const pctX = Math.round((clickX / this.cameraWidth) * 100) as Int;
        const pctY = Math.round((clickY / this.cameraHeight) * 100) as Int;

        if (this.colorMode) {
          // 取色模式:获取点击位置的颜色
          context?.getColor(pctX, pctY, {
            success: (res) => {
              const color = res.get("color") as string | null;
              if (color != null) {
                this.colorValue = color;
                this.colorVisible = true;
                this.addLog("取色成功: " + color + " at (" + pctX + "%, " + pctY + "%)");
              }
            },
            fail: (err) => {
              this.addLog("[ERR] 取色失败: " + JSON.stringify(err));
            }
          } as OnCallBackOptions);
        } else {
          // 普通模式:手动对焦
          context?.setFocus(pctX, pctY, {
            success: (res) => {
              this.addLog("对焦成功: (" + pctX + "%, " + pctY + "%)");
            },
            fail: (err) => {
              this.addLog("[ERR] 对焦失败: " + JSON.stringify(err));
            }
          } as OnCallBackOptions);
        }
      },

      toggleColorMode() {
        this.colorMode = !this.colorMode;
        if (!this.colorMode) {
          this.colorVisible = false;
          this.colorValue = '';
        }
        this.addLog(this.colorMode ? "已开启取色模式(点击预览区取色)" : "已关闭取色模式");
      }
    }
  }
</script>

<style>
  .container {
    background-color: #1a1a1a;
    min-height: 100vh;
  }

  /* 相机预览区 */
  .camera-wrapper {
    width: 100%;
    background-color: #000000;
    position: relative;
    overflow: hidden;
  }

  /* 对焦框 */
  .focus-box {
    position: absolute;
    width: 60px;
    height: 60px;
    border: 2px solid #ffff00;
    border-radius: 4rpx;
    pointer-events: none;
  }

  /* 取色badge */
  .color-badge {
    position: absolute;
    right: 20rpx;
    top: 20rpx;
    padding: 8rpx 20rpx;
    border-radius: 30rpx;
    flex-direction: row;
    align-items: center;
  }

  .color-text {
    font-size: 24rpx;
    color: #ffffff;
    font-weight: bold;
    text-shadow: 0 0 4rpx #000;
  }

  /* 顶部快捷栏 */
  .top-bar {
    flex-direction: row;
    justify-content: space-around;
    align-items: center;
    background-color: #2c2c2c;
    padding: 16rpx 20rpx;
  }

  /* 缩放/曝光条 */
  .zoom-bar {
    flex-direction: row;
    align-items: center;
    background-color: #242424;
    padding: 10rpx 20rpx;
  }

  .zoom-label {
    font-size: 24rpx;
    color: #aaaaaa;
    width: 130rpx;
  }

  .zoom-slider {
    flex: 1;
    margin: 0 10rpx;
  }

  .zoom-max {
    font-size: 24rpx;
    color: #888888;
    width: 60rpx;
  }

  /* 按钮控制区 */
  .controls {
    background-color: #2c2c2c;
    padding: 20rpx;
    margin-top: 2rpx;
  }

  .btn-row {
    flex-direction: row;
    justify-content: space-around;
    margin-bottom: 16rpx;
  }

  .btn-row:last-child {
    margin-bottom: 0;
  }

  /* 拍照预览 */
  .preview {
    background-color: #2c2c2c;
    padding: 20rpx;
    margin-top: 2rpx;
  }

  .section-title {
    font-size: 28rpx;
    font-weight: bold;
    color: #eeeeee;
    margin-bottom: 16rpx;
  }

  .preview-image {
    width: 100%;
    height: 400rpx;
  }

  /* 日志区 */
  .log-wrapper {
    background-color: #1e1e1e;
    padding: 20rpx;
    margin-top: 2rpx;
    padding-bottom: 40rpx;
  }

  .log-header {
    flex-direction: row;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 12rpx;
  }

  .log-scroll {
    max-height: 360rpx;
  }

  .log-item {
    font-size: 22rpx;
    color: #aaaaaa;
    line-height: 44rpx;
    display: block;
  }

  .log-err {
    color: #ff6b6b;
  }
</style>

隐私、权限声明

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

android.permission.CAMERA,android.permission.RECORD_AUDIO,android.permission.WRITE_EXTERNAL_STORAGE,android.permission.READ_EXTERNAL_STORAGE

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

插件不采集任何数据

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

暂无用户评论。