实现移动端证件照拍摄功能的最佳实践

482 阅读3分钟

1. 背景介绍

HandleImage.vue 是一个用于拍摄证件照的组件,适用于需要在移动设备上进行图像采集的应用场景。该组件集成了相机功能,并提供了用户友好的界面和交互。

拍摄截图.jpg

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'

说明

  • 模块导入
    • HideLoadingLoading:用于显示和隐藏加载提示。
    • TnIcon:从 @tuniao/tnui-vue3-uniapp 导入的图标组件。
    • CSSProperties:用于定义样式类型。
    • refreactiveonLoadonShowonHide: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 倍放大。另外使用 LoadingHideLoading 显示加载状态。

缩放切换

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>