更新记录
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>