本文参考: 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>