1. 背景介绍
HandleImage.vue
是一个用于拍摄证件照的组件,适用于需要在移动设备上进行图像采集的应用场景。该组件集成了相机功能,并提供了用户友好的界面和交互。
2. 技术栈和工具
- Vue 3:用于构建用户界面。
- TypeScript:提供类型检查和更好的开发体验。
- UniApp:支持多平台开发,特别是小程序。
- @tuniao/tnui-vue3-uniapp:用于 UI 组件的库。
3. 组件结构
模板部分
<template>
<view class="bg-black pb-[80rpx] h-[100vh]">
<camera :device-position="devicePosition" flash="off" resolution="high" @error="error"
style="width: 100%; height: 80%">
<view class="instruction">拍摄时保持稳定,正确对焦,确保画面清晰。</view>
<cover-image style="
position: absolute;
top: 50%;
left: 50%;
width: 480rpx;
height: 720rpx;
transform: translate(-50%, -50%);
" :src="`/static/images/${imagesSrc[photoType] || '框-默认'}.png`" />
</camera>
<view class="flex justify-around items-center h-[20%]">
<view style="width: 80rpx;height: 80rpx;line-height: 80rpx" class="text-#fff text-32rpx font-bold"
@click="setZoom">{{ zoom }}x
</view>
<TnIcon name="circle" type="primary" @click="takePhoto" :custom-style="takePhotoStyle" />
<TnIcon name="refresh" @click="switchPosition" :custom-style="refreshStyle" />
</view>
</view>
</template>
说明
- 相机视图:使用 <camera> 组件实现拍摄功能,支持前后摄像头切换。
- UI 控件:包括缩放按钮、拍照按钮、切换摄像头按钮,提供直观的用户交互。
脚本部分
脚本部分详细说明
<script setup lang="ts" name="证件照拍摄">
import { HideLoading, Loading } from '@/utils/prompt'
import TnIcon from '@tuniao/tnui-vue3-uniapp/components/icon/src/icon.vue'
import type { CSSProperties } from 'vue'
import { ref, reactive, onLoad, onShow, onHide } from 'vue'
说明
- 模块导入:
HideLoading
和Loading
:用于显示和隐藏加载提示。TnIcon
:从@tuniao/tnui-vue3-uniapp
导入的图标组件。CSSProperties
:用于定义样式类型。ref
、reactive
、onLoad
、onShow
、onHide
:Vue 3 的组合式 API,用于管理状态和生命周期。
样式定义
const refreshStyle: CSSProperties = {
fontSize: '80rpx',
color: 'white',
}
const takePhotoStyle: CSSProperties = {
fontSize: '120rpx',
color: 'red',
}
说明
- 样式定义:使用
CSSProperties
类型定义了两个样式对象,分别用于刷新按钮和拍照按钮。
响应式数据
const photoInfo = reactive({
src: '',
})
const photoType = ref('')
const zoom = ref(1) // 设置画面缩放倍数
const devicePosition = ref<'back' | 'front'>('back')
说明
photoInfo
:使用reactive
创建响应式对象,用于存储拍摄的照片路径。photoType
:使用ref
创建响应式变量,存储照片类型。zoom
:存储当前的缩放倍数。devicePosition
:存储当前摄像头位置(前置或后置)。
图片资源
const imagesSrc = {
frontCard: '框-身份证人像面',
reverseCard: '框-身份证国徽面',
handCardSms: '框-手持身份证+卡板',
handCard: '框-手持身份证',
}
说明
imagesSrc
:定义了一个对象,映射不同的照片类型到相应的框架图像路径。
方法实现
错误处理
const error = (e) => {
console.log(e)
uni.showModal({
title: '错误',
content: e.detail.errMsg,
})
}
error
:处理相机错误事件,显示错误信息。
初始化缩放
const setInitialZoom = () => {
if (!cameraContext.value) return
Loading('初始化中...')
const targetZoom = devicePosition.value === 'front' ? 1 : 1.5
try {
cameraContext.value.setZoom({
zoom: targetZoom,
success: (res) => {
console.log('initial zoom', res)
zoom.value = res.zoom
},
fail: (err) => {
console.error('setZoom failed:', err)
zoom.value = 1
},
complete: () => {
HideLoading()
},
})
} catch (error) {
console.error('setZoom error:', error)
HideLoading()
zoom.value = 1
}
}
setInitialZoom
:设置初始缩放倍数,前置摄像头固定为 1 倍,后置为 1.5 倍。iPhone 12 以上机型具有主摄、长焦和广角 3 颗镜头,但 camera 组件仅使用主摄镜头进行拍摄,手机离主体太近无法切换广角镜头拍摄导致图像模糊,所以默认后置摄像头一律为 1.5 倍放大。另外使用Loading
和HideLoading
显示加载状态。
缩放切换
const setZoom = () => {
if (!cameraContext.value) return
Loading('切换中...')
cameraContext.value.setZoom({
zoom: zoom.value === 1.5 ? 1 : 1.5,
success: (res) => {
console.log('zoom', res)
zoom.value = res.zoom
},
complete: () => {
HideLoading()
},
})
}
setZoom
:切换缩放倍数,在1倍和1.5倍之间切换。
摄像头切换
const switchPosition = () => {
devicePosition.value = devicePosition.value === 'back' ? 'front' : 'back'
setInitialZoom()
}
switchPosition
:切换摄像头位置,并重新设置初始缩放。由于需要用户进行上半身人面像拍照,需要开启前置摄像头拍摄。
拍照功能
const takePhoto = () => {
if (!cameraContext.value) {
uni.showToast({
title: '相机未就绪,请稍后重试',
icon: 'none'
})
return
}
cameraContext.value.takePhoto({
quality: 'high',
selfieMirror: false,
success(res) {
console.log(res, '拍照成功')
photoInfo.src = res.tempImagePath
uni.$emit('photoInfo', { tempImagePath: res.tempImagePath, photoType: photoType.value })
uni.navigateBack()
},
fail: (err) => {
console.error('takePhoto failed:', err)
uni.showToast({
title: '拍照失败,请重试',
icon: 'none'
})
},
})
}
takePhoto
:调用相机的takePhoto
方法拍摄照片,成功后存储照片路径并返回上一页。
权限检查
const checkCameraPermission = () => {
uni.authorize({
scope: 'scope.camera',
success() {
createCamera()
},
fail() {
uni.showModal({
title: '提示',
content: '请授权相机权限以继续使用',
success: (res) => {
if (res.confirm) {
uni.openSetting()
}
}
})
}
})
}
checkCameraPermission
:检查相机权限,未授权时提示用户授权。
创建相机上下文
const createCamera = () => {
if (uni.createCameraContext) {
setTimeout(() => {
cameraContext.value = uni.createCameraContext()
setInitialZoom()
}, 200)
} else {
uni.showModal({
title: '提示',
content: '当前微信版本过低,无法使用该功能,请升级到最新微信版本后重试。',
})
}
}
createCamera
:创建相机上下文,并设置初始缩放。使用setTimeout
延迟初始化以确保相机准备就绪。
生命周期钩子
onLoad((params) => {
photoType.value = params.photoType // 获取拍摄证件照类型,替换页面标题和示意图框
const title = imagesSrc[photoType.value] ? imagesSrc[photoType.value].slice(2) : '证件照拍摄'
uni.setNavigationBarTitle({
title: title
})
checkCameraPermission()
})
onShow(() => {
if (!cameraContext.value) {
createCamera()
}
})
onHide(() => {
cameraContext.value = null
})
onLoad
:在页面加载时设置导航栏标题,并检查相机权限。onShow
:在页面显示时创建相机上下文。onHide
:在页面隐藏时释放相机资源。
完整代码
<script setup lang="ts" name="证件照拍摄">
import { HideLoading, Loading } from '@/utils/prompt'
import TnIcon from '@tuniao/tnui-vue3-uniapp/components/icon/src/icon.vue'
import type { CSSProperties } from 'vue'
import { ref, reactive, onLoad, onShow, onHide } from 'vue'
// 样式定义
const style: CSSProperties = {
fontSize: '80rpx',
color: 'white',
}
const takePhotoStyle: CSSProperties = {
fontSize: '120rpx',
color: 'red',
}
// 响应式数据
const cameraContext = ref(null)
const photoInfo = reactive({
src: '',
})
const photoType = ref('')
const zoom = ref(1)
const devicePosition = ref<'back' | 'front'>('back')
// 图片资源
const imagesSrc = {
frontCard: '框-身份证人像面',
reverseCard: '框-身份证国徽面',
handCardSms: '框-手持身份证+卡板',
handCard: '框-手持身份证',
}
// 方法实现
const error = (e) => {
console.log(e)
uni.showModal({
title: '错误',
content: e.detail.errMsg,
})
}
const setInitialZoom = () => {
if (!cameraContext.value) return
Loading('初始化中...')
const targetZoom = devicePosition.value === 'front' ? 1 : 1.5
try {
cameraContext.value.setZoom({
zoom: targetZoom,
success: (res) => {
console.log('initial zoom', res)
zoom.value = res.zoom
},
fail: (err) => {
console.error('setZoom failed:', err)
zoom.value = 1
},
complete: () => {
HideLoading()
},
})
} catch (error) {
console.error('setZoom error:', error)
HideLoading()
zoom.value = 1
}
}
const setZoom = () => {
if (!cameraContext.value) return
Loading('切换中...')
cameraContext.value.setZoom({
zoom: zoom.value === 1.5 ? 1 : 1.5,
success: (res) => {
console.log('zoom', res)
zoom.value = res.zoom
},
complete: () => {
HideLoading()
},
})
}
const switchPosition = () => {
devicePosition.value = devicePosition.value === 'back' ? 'front' : 'back'
setInitialZoom()
}
const takePhoto = () => {
if (!cameraContext.value) {
uni.showToast({
title: '相机未就绪,请稍后重试',
icon: 'none'
})
return
}
cameraContext.value.takePhoto({
quality: 'high',
selfieMirror: false,
success(res) {
console.log(res, '拍照成功')
photoInfo.src = res.tempImagePath
uni.$emit('photoInfo', { tempImagePath: res.tempImagePath, photoType: photoType.value })
uni.navigateBack()
},
fail: (err) => {
console.error('takePhoto failed:', err)
uni.showToast({
title: '拍照失败,请重试',
icon: 'none'
})
},
})
}
const checkCameraPermission = () => {
uni.authorize({
scope: 'scope.camera',
success() {
createCamera()
},
fail() {
uni.showModal({
title: '提示',
content: '请授权相机权限以继续使用',
success: (res) => {
if (res.confirm) {
uni.openSetting()
}
}
})
}
})
}
const createCamera = () => {
if (uni.createCameraContext) {
setTimeout(() => {
cameraContext.value = uni.createCameraContext()
setInitialZoom()
}, 200)
} else {
uni.showModal({
title: '提示',
content: '当前微信版本过低,无法使用该功能,请升级到最新微信版本后重试。',
})
}
}
// 生命周期钩子
onLoad((params) => {
photoType.value = params.photoType
const title = imagesSrc[photoType.value] ? imagesSrc[photoType.value].slice(2) : '证件照拍摄'
uni.setNavigationBarTitle({
title: title
})
checkCameraPermission()
})
onShow(() => {
if (!cameraContext.value) {
createCamera()
}
})
onHide(() => {
cameraContext.value = null
})
</script>