"智能识别工具箱"将不仅仅是一个简单的拍照应用,它将融合普通拍照、文档扫描(高质量拍照)、二维码/条形码扫描 等多种功能于一体。通过在这个单一、强大的场景中切换不同模式和设置,学习者可以深刻理解 <camera>
组件的每一个属性和事件是如何协同工作,以构建一个健壮、用户体验优秀的原生功能模块。
核心场景:智能识别工具箱
用户故事: 我们要开发一个内置于公司主应用的功能模块。用户可以在此模块中:
- 进行常规拍照,并可以切换前后摄像头、控制闪光灯。
- 切换到文档扫描模式,此时相机会以高分辨率进行拍摄,以确保文档文字清晰可辨。
- 切换到扫码模式,相机将自动识别画面中的二维码或条形码,并返回结果。
- 整个过程需要有清晰的状态反馈,例如:相机初始化状态、用户未授权时的友好提示、被系统中断后的处理等。
完整代码示例 (pages/toolbox/camera.vue
)
这是一个包含了所有关键属性和事件的完整页面代码。你可以直接在支持 <camera>
组件的小程序平台(如微信小程序)上运行它。
html
<template>
<view class="container">
<!-- 错误状态:当用户拒绝授权时显示 -->
<view v-if="isError" class="error-container">
<text class="error-title">摄像头授权失败</text>
<text class="error-message">{{ errorMessage }}</text>
<button @click="openSettings" class="setting-btn">前往设置</button>
</view>
<!-- 正常工作状态 -->
<view v-else class="camera-wrapper">
<!--
camera 组件本身
我们将所有可动态修改的属性都绑定到了 data 中的变量上。
这样我们就可以通过用户交互来改变相机的行为。
-->
<camera
:mode="mode"
:device-position="devicePosition"
:flash="flash"
:resolution="resolution"
frame-size="large"
class="camera-component"
@ready="handleReady"
@initdone="handleInitDone"
@scancode="handleScanCode"
@error="handleError"
@stop="handleStop"
></camera>
<!--
覆盖在 camera 上方的操作界面 (必须使用 cover-view, cover-image)
这是由 camera 组件是原生组件,层级最高的特性决定的。
-->
<cover-view class="controls-container">
<!-- 顶部状态栏 -->
<cover-view class="top-status">
<cover-view class="flash-status">闪光灯: {{ flash }}</cover-view>
<cover-view class="mode-status">模式: {{ mode === 'normal' ? '拍照' : '扫码' }}</cover-view>
</cover-view>
<!-- 扫码模式下的取景框 -->
<cover-view v-if="mode === 'scanCode'" class="scan-box"></cover-view>
<!-- 底部主控制栏 -->
<cover-view class="bottom-controls">
<!-- 切换模式按钮 -->
<cover-view class="control-btn" @click="switchMode">
<cover-image class="icon" src="/static/switch-mode.png"></cover-image>
<cover-view class="text">切换模式</cover-view>
</cover-view>
<!-- 拍照按钮 (核心功能) -->
<cover-view class="control-btn take-photo-btn" @click="takePhoto">
<cover-view class="outer-ring"></cover-view>
</cover-view>
<!-- 更多设置按钮 -->
<cover-view class="control-btn" @click="showMoreSettings">
<cover-image class="icon" src="/static/settings.png"></cover-image>
<cover-view class="text">设置</cover-view>
</cover-view>
</cover-view>
</cover-view>
<!-- 更多设置面板 -->
<cover-view class="more-settings" v-if="settingsVisible">
<cover-view class="setting-item" @click="switchDevice">切换摄像头</cover-view>
<cover-view class="setting-item" @click="switchFlash">切换闪光灯</cover-view>
<cover-view class="setting-item" @click="toggleResolution">
{{ resolution === 'high' ? '切换为普通画质' : '切换为文档画质(高)' }}
</cover-view>
</cover-view>
<!-- 预览拍摄的照片 -->
<image v-if="photoSrc" :src="photoSrc" class="preview-image" @click="photoSrc = ''"></image>
<!-- 状态提示 -->
<cover-view v-if="statusMessage" class="status-overlay">{{ statusMessage }}</cover-view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
// --- 核心属性绑定 ---
mode: 'normal', // 'normal' 或 'scanCode'
devicePosition: 'back', // 'front' 或 'back'
flash: 'auto', // 'auto', 'on', 'off', 'torch'
resolution: 'medium', // 'low', 'medium', 'high'
// --- 状态管理 ---
isError: false,
errorMessage: '',
isReady: false, // 相机是否初始化完成
ctx: null, // 相机上下文
photoSrc: '', // 拍摄的照片临时路径
settingsVisible: false,
statusMessage: '相机初始化中...'
}
},
onReady() {
// 在页面准备好后,创建 camera 上下文与组件实例关联
this.ctx = uni.createCameraContext();
},
methods: {
// --- 事件处理 ---
handleReady(e) {
console.log('相机组件在支付宝小程序初始化完成', e);
this.isReady = true;
this.statusMessage = '';
},
handleInitDone(e) {
console.log('相机初始化完成', e);
// 微信小程序等平台通过这个事件回调
if (e.detail && e.detail.maxZoom) {
console.log(`相机支持的最大变焦倍数: ${e.detail.maxZoom}`);
}
this.isReady = true;
this.statusMessage = '';
},
handleError(e) {
console.error('相机错误:', e.detail);
this.isError = true;
this.errorMessage = e.detail.errMsg || '无法启动相机,请检查应用权限。';
if (this.errorMessage.includes('auth deny')) {
this.errorMessage = '您已拒绝摄像头授权,请在小程序设置中重新开启。'
}
},
handleStop() {
console.log('相机被非正常终止,例如退到后台。');
// 可以给用户一个提示
this.statusMessage = '相机已暂停';
// 当用户返回页面时,相机通常会自动恢复
setTimeout(() => { if(this.statusMessage === '相机已暂停') this.statusMessage = '' }, 2000);
},
handleScanCode(e) {
console.log('扫码成功:', e.detail);
uni.showToast({
title: '扫码成功',
icon: 'success'
});
// 震动一下,提供反馈
uni.vibrateShort();
// 将结果展示出来
this.statusMessage = `扫描结果: ${e.detail.result}`;
// 2秒后清除提示
setTimeout(() => { this.statusMessage = '' }, 2000);
},
// --- 用户操作 ---
takePhoto() {
if (!this.isReady) {
uni.showToast({ title: '相机未准备好', icon: 'none' });
return;
}
this.ctx.takePhoto({
quality: 'high', // 拍照质量
success: (res) => {
this.photoSrc = res.tempImagePath;
},
fail: (err) => {
console.error('拍照失败', err);
uni.showToast({ title: '拍照失败,请重试', icon: 'none' });
}
});
},
switchMode() {
if (!this.isReady) return;
this.mode = this.mode === 'normal' ? 'scanCode' : 'normal';
uni.showToast({ title: `已切换到 ${this.mode === 'normal' ? '拍照' : '扫码'}模式`, icon: 'none' });
},
showMoreSettings() {
this.settingsVisible = !this.settingsVisible;
},
switchDevice() {
this.devicePosition = this.devicePosition === 'back' ? 'front' : 'back';
},
switchFlash() {
const flashModes = ['auto', 'on', 'off', 'torch'];
let currentIndex = flashModes.indexOf(this.flash);
let nextIndex = (currentIndex + 1) % flashModes.length;
this.flash = flashModes[nextIndex];
},
toggleResolution() {
// 模拟切换到"文档扫描"模式
this.resolution = this.resolution === 'medium' ? 'high' : 'medium';
uni.showToast({ title: `画质已切换为: ${this.resolution}`, icon: 'none' });
},
openSettings() {
// 引导用户打开授权设置页面
uni.openSetting({
success(res) {
console.log(res.authSetting)
}
});
}
}
}
</script>
<style>
/* 整体布局 */
.container, .camera-wrapper { width: 100vw; height: 100vh; }
.camera-component { width: 100%; height: 100%; }
/* 错误状态 */
.error-container { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 50rpx; height: 100%; }
.error-title { font-size: 40rpx; font-weight: bold; }
.error-message { font-size: 28rpx; color: #888; margin: 20rpx 0; }
.setting-btn { margin-top: 40rpx; }
/* 覆盖层控件 */
.controls-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: space-between; }
.top-status { display: flex; justify-content: space-between; padding: 20rpx; color: white; background-color: rgba(0,0,0,0.2); font-size: 24rpx; }
.bottom-controls { display: flex; justify-content: space-around; align-items: center; width: 100%; padding: 40rpx 0; background-color: rgba(0,0,0,0.4); }
.control-btn { display: flex; flex-direction: column; align-items: center; color: white; }
.control-btn .icon { width: 64rpx; height: 64rpx; }
.control-btn .text { font-size: 20rpx; margin-top: 8rpx; }
.take-photo-btn .outer-ring { width: 120rpx; height: 120rpx; border-radius: 50%; border: 8rpx solid white; display: flex; justify-content: center; align-items: center; }
/* 扫码框 */
.scan-box { position: absolute; top: 50%; left: 50%; width: 500rpx; height: 500rpx; transform: translate(-50%, -60%); border: 2rpx solid #00ff00; }
/* 更多设置面板 */
.more-settings { position: absolute; bottom: 200rpx; right: 20rpx; background-color: rgba(0,0,0,0.7); color: white; border-radius: 16rpx; }
.setting-item { padding: 24rpx 30rpx; font-size: 28rpx; border-bottom: 1rpx solid rgba(255,255,255,0.2); }
.setting-item:last-child { border-bottom: none; }
/* 预览图和状态 */
.preview-image { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
.status-overlay { position: absolute; top: 45%; left: 50%; transform: translateX(-50%); background-color: rgba(0,0,0,0.6); color: white; padding: 20rpx 40rpx; border-radius: 16rpx; font-size: 30rpx; }
</style>
(请自行准备 switch-mode.png
和 settings.png
图标文件,或使用文字代替)
代码与属性详解 (Architect's Breakdown)
1. 核心属性 (<camera>
标签)
-
mode
:{{ mode }}
- 作用: 定义相机的核心工作模式。
- 场景应用 : 我们通过
switchMode
方法动态改变this.mode
的值('normal'
或'scanCode'
)。这使得用户可以在"拍照"和"扫码"功能间无缝切换,UI 也会随之变化(如显示/隐藏扫码框),这是构建多功能相机的基础。
-
device-position
:{{ devicePosition }}
- 作用: 控制使用前置或后置摄像头。
- 场景应用 :
switchDevice
方法在'front'
和'back'
之间切换this.devicePosition
的值,实现了标准的"切换摄像头"功能,满足自拍或拍摄他物的需求。
-
flash
:{{ flash }}
- 作用 : 控制闪光灯状态 (
auto
,on
,off
,torch
- 常亮)。 - 场景应用 :
switchFlash
方法在一个数组中循环切换闪光灯模式,满足用户在不同光线条件下的拍摄需求。UI上的状态显示也同步更新。
- 作用 : 控制闪光灯状态 (
-
resolution
:{{ resolution }}
- 作用: 设置相机分辨率,直接影响画面质量。
- 场景应用 : 在我们的"智能识别工具箱"中,这是区分普通拍照和文档扫描 的关键。
toggleResolution
方法在'medium'
和'high'
之间切换。当用户需要扫描合同时,可以切换到high
模式,确保takePhoto
捕获的图像足够清晰,便于后续的 OCR 识别。 - 注意点 :
output-dimension
是支付宝小程序的特有属性,功能类似,用于控制输出分辨率。在跨平台开发时,需要注意这种平台差异。
-
frame-size
:"large"
- 作用: 指定从相机获取的原始帧数据尺寸,主要用于需要实时处理相机画面的高级场景。
- 场景应用 : 虽然本示例未直接处理帧数据,但设置
frame-size="large"
是一个面向未来的架构决策。如果我们下一步要增加"实时美颜"或"实时物体识别"功能,就需要从相机获取高质量的原始数据流。在这里设置好,就为未来的功能扩展铺平了道路。
2. 关键事件 (@
事件绑定)
-
@initdone
/@ready
- 作用: 标志着相机硬件和软件已成功初始化,可以接收 API 指令(如拍照、变焦)。
- 场景应用 : 在
handleInitDone
方法中,我们将this.isReady
设为true
,并清除"初始化中..."的提示。在takePhoto
等操作前,我们检查this.isReady
,这是防止应用在相机未就绪时调用 API 而崩溃的关键保护性编程实践。
-
@error
- 作用: 当相机启动失败时触发,最常见的原因是用户未授权。
- 场景应用 :
handleError
是用户体验的生命线 。它会捕获错误,将isError
设为true
,从而隐藏相机界面,显示一个友好的错误提示和"前往设置"的按钮。这避免了给用户一个白屏或无响应的界面,而是清晰地引导他们解决问题。
-
@stop
- 作用: 当相机被系统非正常中断(如小程序退到后台)时触发。
- 场景应用 :
handleStop
方法可以用来更新 UI 状态,例如显示"相机已暂停"的提示。这是一个细节,但能让用户感知到应用的当前状态,提升应用的专业度和健壮性。
-
@scancode
- 作用 : 仅在
mode="scanCode"
时生效,当相机成功识别到二维码/条形码时触发。 - 场景应用 :
handleScanCode
方法接收到识别结果后,立即通过 Toast 和震动给予用户即时反馈,并将结果显示在屏幕上。这是扫码功能的核心交互闭环。
- 作用 : 仅在
3. 相关 API (uni.createCameraContext
)
- 作用 : 创建并返回一个
camera
组件的上下文对象,通过这个对象,我们才能命令相机执行动作。 - 场景应用 : 在
onReady
生命周期中,我们执行this.ctx = uni.createCameraContext()
。然后在takePhoto
方法中,我们调用this.ctx.takePhoto({...})
来执行拍照。ctx
是我们与相机组件进行程序化交互的唯一桥梁。
总结与最佳实践
- 原生组件层级问题 :
<camera>
是原生组件,层级最高。所有UI控件都必须使用<cover-view>
和<cover-image>
,这是开发前必须了解的核心知识点。 - 状态驱动的UI : 整个示例的核心是状态管理 (
mode
,isError
,isReady
等)。通过改变data
中的状态,UI 自动地、响应式地进行更新。这是现代前端开发的标准范式,能让复杂交互逻辑变得清晰可控。 - 完备的异常处理 : 成功的应用不仅功能强大,更能优雅地处理各种异常。
@error
和@stop
的处理,以及对isReady
状态的判断,共同构成了一个健壮的、不易崩溃的相机模块。 - 清晰的用户引导: 从"初始化中"的提示,到"授权失败"的引导,再到"扫码成功"的反馈,每一步都应该给用户清晰的指示和反馈。这是决定产品体验好坏的关键。
- 考虑平台差异和未来扩展 : 在使用
resolution
等属性时,要了解其在不同平台的兼容性。在设计组件时,像frame-size
这样的属性可以预先设置,为未来的高级功能(如AI识别)留下扩展空间。