参考:
https://blog.csdn.net/weixin_45148022/article/details/135696629
https://juejin.cn/post/7327353533618978842?searchId=20241101133433B2BB37A081FD6A02DA60
https://www.freesion.com/article/67641324321/
https://github.com/AlexKratky/vue-camera-lib
效果:
调用组件的
主要组件方法:openCamera,closeCamera
Upload.vue组件
<template><div id="cameraContainer"><div ref="takePhotoDiv" class="take-photo" style="display: none"><video ref="video" id="video-fix" :width="width" :height="height" autoplay webkit-playsinline playsinline></video><div class="frame-container"><div class="mask" >
<!-- 头像页图标--><img v-if="props.currPhotoType=='head'" class="img-head" src="../assets/image/idcard1.svg">
<!-- 国徽页图标--><img v-if="props.currPhotoType=='mark'" class="img-mark" src="../assets/image/idcard2.svg"><div class="tips">请将{{props.currPhotoType=='head'?'身份证人像面':'身份证国徽面'}}完全置于取景框内</div></div></div></div>
<!-- 拍照按钮--><div id="captureButton" @click="takePhoto"><div class="cap-inner"></div></div></div><canvas ref="canvas" style="display: none"></canvas><img ref="photo" id="photo" alt="入职文件" style="display: none" /></template>
<script setup lang="ts">
import { showToast } from "vant/lib/toast";
import { nextTick, onMounted, ref,inject } from "vue";
import {base64ToBlob, base64ToFile, putFile} from "@/common/services/OSSFile.ts";
import {FileUploadType} from "@/common/enum/FileUploadType.ts";
import {ElLoading} from "element-plus";
const props=defineProps({currPhotoType:String
})
const emit=defineEmits(['okUploadImg'])
const video = ref<HTMLVideoElement | null>(null);
// const frame = ref<HTMLDivElement | null>(null);
const photo = ref<HTMLImageElement | null>(null);
const canvas = ref<HTMLCanvasElement | null>(null);
const mediaStream = ref<any>();
const takePhotoDiv = ref<HTMLDivElement | null>(null);const width=ref()
const height=ref()
onMounted(()=>{//设置摄像头宽高width.value=window.innerHeightheight.value=window.innerWidth})const getVideoMedia = () => {if (video.value) {// ----------兼容性代码------------// 老的浏览器可能根本没有实现 mediaDevices,所以我们可以先设置一个空的对象if (navigator.mediaDevices === undefined) {navigator.mediaDevices = {};}// 一些浏览器部分支持 mediaDevices。我们不能直接给对象设置 getUserMedia
// 因为这样可能会覆盖已有的属性。这里我们只会在没有 getUserMedia 属性的时候添加它。if (navigator.mediaDevices.getUserMedia === undefined) {navigator.mediaDevices.getUserMedia = function (constraints) {// 首先,如果有 getUserMedia 的话,就获得它var getUserMedia =navigator.webkitGetUserMedia || navigator.mozGetUserMedia;// 一些浏览器根本没实现它 - 那么就返回一个 error 到 promise 的 reject 来保持一个统一的接口if (!getUserMedia) {return Promise.reject(new Error("getUserMedia is not implemented in this browser"),);}// 否则,为老的 navigator.getUserMedia 方法包裹一个 Promisereturn new Promise(function (resolve, reject) {getUserMedia.call(navigator, constraints, resolve, reject);});};}// ----------兼容性代码------------// 获取用户媒体设备权限navigator.mediaDevices// 强制使用后置摄像头.getUserMedia({ video: { facingMode: { exact: "environment" } }, audio: false })//前置// .getUserMedia({ video: true, audio: false }).then((stream) => {// if (video.value) {// video.value.srcObject = stream;// mediaStream.value = stream;// }//兼容性写法if ("srcObject" in video.value) {video.value.srcObject = stream;} else {// 防止在新的浏览器里使用它,应为它已经不再支持了video.value.src = window.URL.createObjectURL(stream);}video.value.onloadedmetadata = function (e) {video.value.play();};}).catch((error) => {console.error("获取相机权限失败:", error);showToast('获取相机权限失败');});}
}const takePhoto = () => {nextTick(async () => {console.log(video.value)if (canvas.value && video.value && photo.value) {const context = canvas.value.getContext("2d");// 设置画布尺寸与取景框相同canvas.value.width = video.value.videoWidth;canvas.value.height = video.value.videoHeight;// 绘制取景框内的画面到画布if (context) {context.drawImage(video.value, 0, 0);// 将画布内容转为图片并显示photo.value.src = canvas.value.toDataURL();photo.value.style.display = "block";// 关闭videoconsole.log('video', video.value);video.value.pause();// 关闭摄像头mediaStream.value?.getTracks().forEach((track: any) => track.stop());video.value=null}}console.log(photo.value)// console.log(photo.value.src) 将文件流传给后台上传,下列代码根据实际情况自定let file:any=photo.value.srclet idtype=props.currPhotoType=='head'?FileUploadType.BIZ_TYPE_IDCARD2:FileUploadType.BIZ_TYPE_IDCARD1//文件名:时间戳+1000以内的随机数let fileName=new Date().getTime()+ Math.floor(Math.random()*1000)+'.jpg'const loadingInstance = ElLoading.service({ fullscreen: true, background: 'rgba(0,0,0,0.1)', text: '请求中...' });let data = await putFile(fileName,idtype, base64ToFile(file,fileName));if(data){loadingInstance.close()sendValue({file:file,type:props.currPhotoType,url:data})showToast('上传成功!')emit('okUploadImg',{status:1})}else{loadingInstance.close()showToast('上传失败!')emit('okUploadImg',{status:2})}})
}const passValue:any = inject("getIdFile")
//3.孙组件在函数中调用爷爷传递过来的函数,并在()中传递要传递的数据
const sendValue = (file) => {passValue(file)
}
//4.调用这个函数(也可以使用点击事件等方式触发)//关闭相机
const closeCamera=()=>{// 关闭摄像头mediaStream.value?.getTracks().forEach((track: any) => track.stop());video.value=null
}
//dakai相机
const openCamera=()=>{console.log('打开相机')//打开相机if (takePhotoDiv.value) {takePhotoDiv.value.style.display = 'block'getVideoMedia()}
}defineExpose({openCamera,closeCamera
})
</script>
<style scoped lang="less"></style>
#cameraContainer {position: relative;//width: 324px;//height: 216px;width:100vw;height: 100vh;background: #000;overflow: hidden;.take-photo{//height:85.6*6px;//width: 53.98*6px;height: 70%;width: 90%;overflow: hidden;background: #000;position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%) ;}#video-fix{position: absolute;top: 50%;left: 50%;//transform: translate(-50%, -50%) rotate(90deg);transform: translate(-50%, -50%);}
}#video {object-fit: cover;}.frame-container {position: absolute;top: 0;left: 0;width: 100%;height: 100%;
}.mask {position: absolute;height:85.6*5px;width: 53.98*5px;border: 1px solid #fdfdfd;border-radius: 5px;top: 50%;left: 50%;transform: translate(-50%, -50%);.img-head{position: absolute;bottom: 4.5%;right: 13.7%;height: 28%;width: 53%;transform: rotate(90deg);}.img-mark{position: absolute;top:7%;right: 9%;width: 37%;height: 22.5%;transform: rotate(90deg);}.tips{position: absolute;left: -50%;top: 50%;color: #fff;transform: rotate(90deg);font-size: 14px;background: #555657;border-radius: 5px;}}#frame {position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);width: 200px;height: 90px;z-index: 10;background-color: transparent;
}#photo {display: none;}
#captureButton{width: 100px;height: 100px;border-radius: 50%;background: #ffffff;position: absolute;bottom: 50px;left: 50%;transform: translateX(-50%);display: flex;justify-content: center;align-items: center;.cap-inner{background: #fff;width: 85%;height: 85%;border-radius: 50%;border: 3px solid #000;}
}
base64转文件流
/*** @description: Base64 转 File* @param {string} base64 base64格式的字符串* @param {string} fileName 文件名* @return {File}*/
export const base64ToFile = (base64: string, fileName: string): File => {const arr: string[] = base64.split(',');const type = (arr[0].match(/:(.*?);/) as string[])[1];const bstr = atob(arr[1]);let n = bstr.length;const u8arr = new Uint8Array(n);while (n--) {u8arr[n] = bstr.charCodeAt(n);}return new File([u8arr], fileName, { type });
};
调用组件:
<script setup lang="ts">
import {onMounted, ref} from "vue";
import Upload from "@/components/Upload.vue";const props=defineProps({currPhotoType:String
})
const _show=ref(false)
const uploadRef=ref()const goBack =()=> {// window.history.back() // 删掉van-popup打开时添加的history_show.value = false//关闭相机uploadRef.value.closeCamera()
}const openModal=()=>{_show.value=truesetTimeout(()=>{//打开相机uploadRef.value.openCamera()},500)}
onMounted(()=>{})const okUpload=(e)=>{if(e.status==1){//上传成功,关闭弹框,关闭相机goBack()}if(e.status==2){//上传失败,关闭弹框,关闭相机goBack()}
}defineExpose({openModal
})
</script><template>
<!--全屏弹框组件--><!-- @close="selectProjectCloseHandler" @open="selectProjectOpenHandler"--><van-popup v-model:show="_show" :overlay="false" position="bottom" :style="{ width: '100%', height: '100%'}"><div class="header"><van-nav-bar class="title" left-arrow title="身份证头像页上传" :safe-area-inset-top="true" :fixed="true"@click-left="goBack" /></div><div style="color: red">{{props}}</div><Upload ref="uploadRef" @okUploadImg="okUpload" :currPhotoType="props.currPhotoType"></Upload></van-popup>
</template><style scoped lang="less"></style>