基于Vue3的H5自定义拍身份证人相框和国徽框

本文参考: vue-h5:在h5中实现相机拍照加上身份证人相框和国徽框_vue前端扫描身份证取景框-CSDN博客 效果:

代码: Camera.vue组件

vue3 复制代码
<template>
  <van-popup
    v-model:show="show"
    :overlay="false"
    position="bottom"
    :style="{ width: '100%', height: '100%' }"
  >
    <div class="container">
      <canvas ref="_canvas" style="display: none"></canvas>
      <video
        ref="video"
        id="video-box"
        autoplay
        muted
        webkit-playsinline
        playsinline
        style="width: 100%"
      ></video>
      <div class="shadow-layer" :style="{ height: videoHeight + 'px' }">
        <div
          ref="rectangle"
          id="capture-rectangle"
          :style="{
            width: '95%',
            height: '5.334rem',
            margin: `4rem auto 0`,
            boxShadow: `0 0 0 ${(1500 / 75) * 1}rem rgba(0, 0, 0, 0.7)`,
          }"
        ></div>
        <div class="hold-tips">
          请将{{ props.photoType == 'head' ? '身份证人像面' : '身份证国徽面' }}完全置于取景框内
        </div>
      </div>
      <div class="footer">
        <div class="foucs-list">
          <div
            class="foucs-one"
            :class="currFoucs == 2 ? 'active' : ''"
            @click="handlerChangeFoucs(2)"
          >
            x2.0
          </div>
          <div
            class="foucs-one"
            :class="currFoucs == 1.5 ? 'active' : ''"
            @click="handlerChangeFoucs(1.5)"
          >
            x1.5
          </div>
          <div
            class="foucs-one"
            :class="currFoucs == 1 ? 'active' : ''"
            @click="handlerChangeFoucs(1)"
          >
            x1.0
          </div>
        </div>
        <div class="left">
          <van-uploader :after-read="onSelectFile">
            <van-icon name="photo-o" style="color: #fff" />
          </van-uploader>
        </div>
        <div class="mid">
          <div id="captureButton" @click="getPhoto">
            <div class="cap-inner"></div>
          </div>
        </div>
        <div class="right">
          <van-icon @click="handlerRotate" name="replay" style="color: #fff" />
        </div>
      </div>
      <div v-if="currImg" class="review-popup">
        <img :src="currImg" class="review-img" />
        <div class="review-footer">
          <van-icon name="revoke" class="review-btn" @click="currImg = ''" />
          <van-icon name="success" class="review-btn" @click="onOk" />
        </div>
      </div>
    </div>
  </van-popup>
</template>

<script setup>
import { ref, defineExpose, nextTick } from 'vue'
import { showToast } from 'vant'
import { getUserMediaStream } from './getUserMediaStream'

const props = defineProps({
  photoType: {
    type: String,
    default: 'head',
  },
})
const emits = defineEmits(['onSuccess'])

const video = ref(null)
const videoHeight = ref()
const rectangle = ref(null)
const _canvas = ref(null)
const currImg = ref('')
const show = ref(false)
let cameraModel = 'environment'

/*
 * 获取video中对应的真实size
 */
function getRealSize() {
  const { videoHeight: vh, videoWidth: vw, offsetHeight: oh, offsetWidth: ow } = video.value
  return {
    getHeight: (height) => {
      return (vh / oh) * height
    },
    getWidth: (width) => {
      return (vw / ow) * width
    },
  }
}

const getPhoto = async () => {
  const { getHeight, getWidth } = getRealSize()
  if (!rectangle.value) {
    return
  }
  /** 获取框的位置 */
  const { left, top, width, height } = rectangle.value.getBoundingClientRect()
  const context = _canvas.value.getContext('2d')
  _canvas.value.width = width * 3 //这里截出来的图比列太小了,做了一个放大3倍的操作
  _canvas.value.height = height * 3
  context?.drawImage(
    video.value,
    getWidth(left + window.scrollX),
    getHeight(top + window.scrollY),
    getWidth(width),
    getHeight(height),
    0,
    0,
    width * 3,
    height * 3,
  )
  const base64 = _canvas.value.toDataURL('image/jpeg')
  currImg.value = base64
}

//选择本地文件
const onSelectFile = (file) => {
  console.log(file)
  currImg.value = file.content
}

// 关闭相机
const closeCamera = () => {
  video.value.srcObject?.getTracks().forEach((track) => track.stop())
}
const openMediaStream = (model) => {
  getUserMediaStream(video.value, model)
    .then(() => {
      setTimeout(() => {
        videoHeight.value = video.value.offsetHeight
        if (video.value.offsetHeight < 400) {
          //解决ios不能获取到实时的offsetHeight的问题
          videoHeight.value = 600
        }
        if (navigator.mediaDevices.getSupportedConstraints().zoom) {
          // showToast("The browser   support zoom.");
        } else {
          showToast('当前游览器不支持调节焦距')
        }
      }, 200)
    })
    .catch(() => {
      showToast('无法调起后置摄像头,请点击相册,手动上传身份证!')
    })
}
// 打开相机
const openCamera = () => {
  show.value = true
  nextTick(() => {
    openMediaStream(cameraModel)
  })
}

const currFoucs = ref(1) //焦点参数
const handlerChangeFoucs = async (num) => {
  try {
    currFoucs.value = num
    //改变焦距
    const videoTracks = video.value.srcObject.getVideoTracks()
    let track = videoTracks[0]
    const constraints = {
      advanced: [
        {
          zoom: num,
        },
      ],
    }
    await track.applyConstraints(constraints)
  } catch (e) {
    showToast('当前游览器不支持此倍数的焦距!')
  }
}
// 旋转摄像头
const handlerRotate = () => {
  if (cameraModel == 'environment') {
    cameraModel = 'front'
  } else {
    cameraModel = 'environment'
  }
  openMediaStream(cameraModel)
}

const onOk = () => {
  emits('onSuccess', currImg.value)
  currImg.value = ''
}
defineExpose({
  openCamera,
  closeCamera,
})
</script>
<style lang="less" scoped>
.container {
  background: #000000;
  width: 100%;
  height: 100%;
  position: relative;
  overflow: hidden;
  #video-box {
    position: absolute;
    top: 0;
    left: 0;
  }
  .shadow-layer {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    overflow: hidden;
    #capture-rectangle {
      border: 1px solid #fff;
      border-radius: (20/75) * 1rem;
      z-index: 2;
      -webkit-appearance: none; //解决ios的cssbug
      box-shadow: 0 0 0 (1500/75) * 1rem rgba(0, 0, 0, 0.7); // 外层阴影
      -webkit-box-shadow: 0 0 0 (1500/75) * 1rem rgba(0, 0, 0, 0.7); // 外层阴影
    }
    .hold-tips {
      color: #e1e1e1;
      font-size: 12px;
      display: flex;
      align-items: center;
      justify-content: center;
      margin: 5px auto 0;
      border-radius: 5px;
    }
  }
  .config {
    position: absolute;
    top: 400px;
    left: 0;
    z-index: 20;
    width: 100%;
    height: 100px;
    color: #d21818;
  }

  .footer {
    position: relative;
    display: flex;
    position: fixed;
    bottom: 0;
    width: 100%;
    height: 135px;
    justify-content: space-around;
    align-items: center;
    .foucs-list {
      z-index: 320;
      position: absolute;
      right: 0;
      color: #fff;
      text-align: left;
      right: 20px;
      bottom: 22vh;
      font-size: 14px;
      div {
        margin: 5px 0;
        padding: 2px 6px;
      }
      .active {
        background: #fff;
        color: #000;
        border-radius: 10px;
      }
    }
    .left,
    .right {
      font-size: 30px;
    }
    .mid {
      display: flex;
      justify-content: center;
      align-items: center;
      width: 80px;
      height: 80px;
      #captureButton:active {
        width: 75px;
        height: 75px;
        .cap-inner {
          background: rgba(255, 255, 255, 0.7) !important;
        }
      }
      #captureButton {
        width: 80px;
        height: 80px;
        border-radius: 50%;
        background: #ffffff;
        display: flex;
        justify-content: center;
        align-items: center;
        .cap-inner {
          background: #fff;
          width: 85%;
          height: 85%;
          border-radius: 50%;
          border: 3px solid #000;
        }
      }
    }
  }
}
.review-popup {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.9);
  z-index: 1000;
  display: flex;
  .review-img {
    width: 95%;
    height: 200px;
    margin: 4rem auto 0;
  }

  .review-footer {
    position: absolute;
    bottom: 40px;
    left: 0;
    width: 100%;
    display: flex;
    justify-content: space-around;
    align-items: center;
    .review-btn {
      font-size: 32px;
      color: #fff;
    }
  }
}
</style>

getUserMediaStream.js

js 复制代码
//访问用户媒体设备的兼容方法
function getUserMedia(constrains) {
  const navigator = window.navigator
  if (navigator.mediaDevices?.getUserMedia) {
    //最新标准API
    return navigator.mediaDevices.getUserMedia(constrains)
  } else if (navigator.webkitGetUserMedia) {
    //webkit内核浏览器
    return navigator.webkitGetUserMedia(constrains)
  } else if (navigator.mozGetUserMedia) {
    //Firefox浏览器
    return navigator.mozGetUserMedia(constrains)
  } else if (navigator.getUserMedia) {
    //旧版API
    return navigator.getUserMedia(constrains)
  }
}

//成功的回调函数
function success(stream, video) {
  return new Promise((resolve) => {
    video.srcObject = stream
    //播放视频
    video.onloadedmetadata = function () {
      video.play()
    }
    resolve()
  })
}

function getUserMediaStream(videoNode, facingMode = 'environment') {
  const rearCamera = { facingMode: { exact: 'environment', width: 1920, height: 1080 } } // 后置摄像头
  let video = true // 前置摄像头
  if (facingMode === 'environment') {
    video = rearCamera
  }
  //调用用户媒体设备,访问摄像头
  return getUserMedia({
    audio: false,
    video,
  })
    .then((res) => {
      return success(res, videoNode)
    })
    .catch((error) => {
      console.log('访问用户媒体设备失败:', error.name, error.message)
      return Promise.reject()
    })
}

export { getUserMediaStream }

演示文件 index.vue

js 复制代码
<template>
  <van-button @click="uploadRef.openCamera()">打开</van-button>
  <Camera ref="uploadRef" @onSuccess="onSuccess" />
</template>

<script setup>
import { ref } from 'vue'
import Camera from '../components/Camera/index.vue'
const uploadRef = ref()

</script>

<style scoped lang="less"></style>
相关推荐
萌萌哒草头将军2 小时前
🚀🚀🚀 不要只知道 Vite 了,可以看看 Farm ,Rust 编写的快速且一致的打包工具
前端·vue.js·react.js
前端南玖4 小时前
深入Vue3响应式:手写实现reactive与ref
前端·javascript·vue.js
不一样的少年_6 小时前
头像组件崩溃、乱序、加载失败?一套队列机制+多级兜底全搞定
前端·vue.js
rookiefishs8 小时前
一个在electron中强制启用触摸模式🤚🏻的方法
前端·javascript·vue.js
深呼吸9939 小时前
如何用div手写一个富文本编辑器(contenteditable="true")
前端·vue.js
BillKu10 小时前
Vue3 axios 请求设置 signal 信号属性,以便 abort 取消请求
前端·javascript·vue.js
灿灿1213813 小时前
Vue 项目路由模式全解析:从 hash 到 history 再到 abstract
javascript·vue.js·哈希算法
BillKu13 小时前
Vue3 + Element Plus中el-table加载状态分析
javascript·vue.js·elementui
大明8813 小时前
滚动锁定技术解析:以Ant Design的useScrollLocker为例
前端·vue.js·前端框架