赞
踩
<template> <div class="face-verify-approve-index"> <div class="loading-wrap" v-if="loading"> <div class="loading"> <img src="@/assets/img/setting/loading.png" alt=""/> <p>{{ $t("approveText.text7") }}</p> </div> </div> <Header1 class="header" headerId="35"></Header1> <section class="all-header1-index"> <div class="page"> <p class="title" v-if="currentTaskId">{{ $t("faceVerify.tasks" + currentTaskId) }}</p> <div class="video-page"> <van-circle v-model="currentRate" size="232" :stroke-width="30" :rate="rate" :color="gradientColor" layer-color="#F1F1F6" class="circle-container" > <video class="video" id="video" playsinline autoplay x5-video-player-type="h5"></video> <!-- <video ref="videoElement" id="video" class="video" autoplay></video> --> </van-circle> <!-- <input id="imgFile" class="imgFile" type="file" accept="video/*" capture="camcorder" @change="uploadVideo()" /> --> <!-- 检测结果 --> <p class="text" v-if="notFace">{{ notFaceText }}</p> </div> </div> </section> <VerifyResultDialog @showDialog="showDialog" :isShowDialog="isShowDialog" :type="type" :url="url"></VerifyResultDialog> </div> </template> <script> import {mapState} from "vuex"; import {generateRandomNumber} from "@/utils/index"; import COS from "cos-js-sdk-v5"; import md5 from "@/service/md5.js"; import * as faceapi from "face-api.js"; export default { data() { return { verifyType: "", //认证类型 1为认证 2为校验 url: "", generateRandomNumber, md5, currentRate: 0, rate: 100, gradientColor: { "0%": "#95B8FA", "100%": "#3B80FF", }, timer: null, currentTaskId: null, selectedTasks: [], notFace: false, notFaceText: "", tasks: [ //1左右摇头 2点头 3眨眼 {id: 1, type: false}, {id: 2, type: false}, {id: 3, type: false}, ], tasks4: {id: 4, type: false}, stillTimer: null, stillTimeThreshold: 3000, // 3秒 cos: null, // COS实例 file: null, // 选择的文件 imageUrl: "", // 上传后的图片URL isShowDialog: false, //人脸认证结果弹框 type: 3, //认证失败弹框是否可以重新认证 1,不可以 2,可以 ,3,认证成功 4,显示去认证弹框 countdownTimer: null, // 用于倒计时的计时器 loading: false, nodCounter: 0, // 点头计数器 nodThreshold: 2, // 点头动作的阈值 isStill: false, // 是否保持不动 lastPitch: 0, // 上一次的头部倾斜角度 shakeCounter: 0, // 摇头计数器 shakeThreshold: 2, // 摇头动作的阈值 isStillshake: false, // 摇头是否保持不动 shakelastPitch: 0, // 摇头上一次的头部倾斜角度 isFrontCamera: true, videoStream: null, }; }, components: { Header1: () => import("@/components/header/header1.vue"), VerifyResultDialog: () => import("@/components/faceVerify/verifyResult-dialog.vue"), }, mounted() { this.verifyType = this.$route.query.type; this.url = this.$route.query.url; this.startTasks(); // 初始化COS实例 }, computed: { ...mapState(["lang", "userInfo"]), }, methods: { // 关于navigator.mediaDevices.getUserMedia兼容问题 // 有一部分老的浏览器不能兼容navigator.mediaDevices.getUserMedia来获取麦克风和摄像头,可用如下来获取 getAudio() { if ( navigator.mediaDevices.getUserMedia || navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia ) { this.getUserMediaFun({video: true}); // 调用用户媒体设备,访问摄像头、录音、 } else { console.log("你的浏览器不支持访问用户媒体设备"); } }, getUserMediaFun(constrains) { let that = this; if (navigator.mediaDevices.getUserMedia) { // 最新标准API、 navigator.mediaDevices .getUserMedia(constrains) .then((stream) => { that.success(stream); }) .catch((err) => { that.error(err); }); } else if (navigator.webkitGetUserMedia || navigator.mozGetUserMedia) { // webkit内核浏览器 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方法包裹一个Promise return new Promise(function (resolve, reject) { getUserMedia.call(navigator, constraints, resolve, reject); }); }; } navigator.mediaDevices .getUserMedia(constrains) .then((stream) => { that.success(stream); }) .catch((err) => { that.error(err); }); } else if (navigator.getUserMedia) { // 旧版API navigator .getUserMedia(constrains) .then((stream) => { that.success(stream); }) .catch((err) => { that.error(err); }); } }, // 成功的回调函数 success(stream) { const videoElement = document.getElementById("video"); // 获取视频元素 this.videoStream = stream; // 保存视频流,以便在组件销毁时停止摄像头 videoElement.srcObject = stream; // 如果是前置摄像头,添加翻转类 videoElement.classList.add("flipped"); }, // 异常的回调函数 error(error) { console.log("访问用户媒体设备失败:", error.name, error.message); }, startTasks() { // 随机选择两个任务 this.selectedTasks = this.shuffleArray(this.tasks).slice(0, 2); this.completeTask(); }, shuffleArray(array) { // 随机打乱数组顺序 for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } return array; }, completeTask() { // console.log("循环了", this.timer); // 如果当前任务是保持不动的任务,完成后直接调用接口 // console.log(this.selectedTasks, "剩余任务"); if (!this.timer) { this.startCamera() .then(() => { this.timer = window.setInterval(this.detectFace, 1000); }) .catch((err) => { console.log("startCamera函数报错了", err); }); } const allTasksCompleted = this.selectedTasks.every((task) => task.type === true); // console.log("完成了所有的任务:", allTasksCompleted); if (allTasksCompleted) { //随机两项全部通过,开始最后静止不动3s this.currentTaskId = 4; if (this.timer) { clearInterval(this.timer); this.timer = null; // Add this line to set timer to null } } else { if (!this.selectedTasks[0].type) { this.currentTaskId = this.selectedTasks[0].id; } else if (!this.selectedTasks[1].type) { this.currentTaskId = this.selectedTasks[1].id; } else if (!this.selectedTasks[2].type) { this.currentTaskId = this.selectedTasks[2].id; } else { //全部通过 this.currentTaskId = null; if (this.timer) { clearInterval(this.timer); this.timer = null; // Add this line to set timer to null } } } }, // 在 async 函数中初始化摄像头和 face-api.js async startCamera() { // try { // const stream = await navigator.mediaDevices.getUserMedia({ video: true }); // this.videoStream = stream; // 保存视频流,以便在组件销毁时停止摄像头 // this.$refs.videoElement.srcObject = stream; // console.log(this.$refs.videoElement.srcObject); // } catch (error) { // console.error("Error starting camera:", error); // } this.getAudio(); // 初始化 face-api.js 模型 await Promise.all([ faceapi.nets.tinyFaceDetector.loadFromUri(`${process.env.BASE_URL}/models`), faceapi.nets.faceLandmark68Net.loadFromUri(`${process.env.BASE_URL}/models`), ]); }, // 开始实时捕获 async detectFace() { const video = document.getElementById("video"); if (!video) { return; // 视频元素不存在,直接返回,避免报错 } const detections = await faceapi .detectSingleFace(video, new faceapi.TinyFaceDetectorOptions({inputSize: 128})) .withFaceLandmarks(); // const detections = await faceapi.detectSingleFace(video, new faceapi.TinyFaceDetectorOptions()).withFaceLandmarks(); if (detections) { // 如果检测到人脸 this.notFace = false; // 重置未检测到人脸的标志 this.notFaceText = ""; // 清空提示文本 if (this.currentTaskId) { // 如果当前有任务正在进行 if (this.currentTaskId === 1) { this.shakeHeadfunc(detections); } else if (this.currentTaskId === 2) { this.nodHeadfunc(detections); } else if (this.currentTaskId === 3) { this.winkfunc(detections); } else if (this.currentTaskId === 4) { // 静止不动任务 if (!this.stillTimer) { this.stillTimer = setTimeout(() => { this.stillTimer = null; this.captureStillImage(); }, this.stillTimeThreshold); } if (!this.countdownTimer) { this.countdownTimer = setTimeout(() => { this.currentTaskId = null; // 完成所有任务,重置当前任务 this.countdownTimer = null; // 清除计时器 }, this.stillTimeThreshold); } } } else { // 没有任务正在进行,进行下一个任务 this.completeTask(); } } else { // 没有检测到人脸 this.notFace = true; this.notFaceText = this.$t("approveText.text8"); } }, //循环任务摇头中的处理 shakeHeadfunc(detections) { console.log("摇头"); if (detections) { const landmarks = detections.landmarks; const leftEye = landmarks.getLeftEye()[0]; const rightEye = landmarks.getRightEye()[3]; const pitch = this.calculateYaw(leftEye, rightEye); // 判断用户是否摇头 if (Math.abs(pitch - this.shakelastPitch) < 1) { console.log("用户保持不动"); } else { if (this.isShaking(pitch)) { this.shakeCounter++; if (this.shakeCounter >= this.shakeThreshold) { this.shakeCounter = 0; this.notFace = false; this.notFaceText = this.$t("approveText.text9"); this.selectedTasks = this.selectedTasks.filter((task) => task.id !== this.currentTaskId); } } else { this.shakeCounter = 0; this.notFace = true; this.notFaceText = this.$t("approveText.text10"); } } setTimeout(() => { this.completeTask(); }, 1000); this.shakelastPitch = pitch; } }, //循环点头中的处理 nodHeadfunc(detections) { console.log("点头"); if (detections) { const landmarks = detections.landmarks; // const nose = landmarks.getNose(); const leftEye = landmarks.getLeftEye(); const rightEye = landmarks.getRightEye(); // 计算头部倾斜角度 const pitch = this.calculatePitch(leftEye, rightEye); // 判断点头动作 if (Math.abs(pitch - this.lastPitch) < 1) { console.log("用户保持不动"); } else { if (this.isNodding(pitch)) { this.nodCounter++; if (this.nodCounter >= this.nodThreshold) { this.nodCounter = 0; this.notFace = false; this.notFaceText = this.$t("approveText.text11"); this.selectedTasks = this.selectedTasks.filter((task) => task.id !== this.currentTaskId); } } else { this.nodCounter = 0; this.notFace = true; this.notFaceText = this.$t("approveText.text12"); } } setTimeout(() => { this.completeTask(); }, 1000); this.lastPitch = pitch; } }, //眨眼中的处理 winkfunc(detections) { console.log("眨眼"); if (detections) { const landmarks = detections.landmarks; const leftEye = landmarks.getLeftEye(); const rightEye = landmarks.getRightEye(); // 计算左眼和右眼的闭合程度 const leftEyeOpenness = this.calculateEyeOpenness(leftEye); const rightEyeOpenness = this.calculateEyeOpenness(rightEye); // 判断是否眨眼(根据闭合程度) const isBlinking = leftEyeOpenness < 10 && rightEyeOpenness < 10; if (isBlinking) { this.notFace = false; this.notFaceText = this.$t("approveText.text13"); //删除任务数组对应项 this.selectedTasks = this.selectedTasks.filter((task) => task.id !== this.currentTaskId); } else { this.notFace = true; this.notFaceText = this.$t("approveText.text14"); } setTimeout(() => { this.completeTask(); }, 1000); } }, //摇头计算方法 calculateYaw(leftEye, rightEye) { const deltaX = rightEye.x - leftEye.x; const deltaY = rightEye.y - leftEye.y; const angle = Math.atan2(deltaY, deltaX) * (180 / Math.PI); return angle; }, //判断摇头动作 isShaking(pitch) { const mediumRange = {min: -10, max: 10}; // 范围可根据实际情况调整 return pitch >= mediumRange.min && pitch <= mediumRange.max; }, //点头计算方法 calculatePitch(leftEye, rightEye) { // 在这里计算头部倾斜角度 const leftEyeCenter = { x: (leftEye[0].x + leftEye[3].x) / 2, y: (leftEye[0].y + leftEye[3].y) / 2, }; const rightEyeCenter = { x: (rightEye[0].x + rightEye[3].x) / 2, y: (rightEye[0].y + rightEye[3].y) / 2, }; const deltaX = rightEyeCenter.x - leftEyeCenter.x; const deltaY = rightEyeCenter.y - leftEyeCenter.y; const radians = Math.atan2(deltaY, deltaX); // 将弧度转换为角度 const degrees = radians * (180 / Math.PI); return degrees; }, //判断点头动作 isNodding(pitch) { // console.log("角度:", pitch); // 在这里判断点头动作 // 根据头部倾斜角度进行判断 // 返回 true 或 false const mediumRange = {min: -10, max: 10}; // 范围可根据实际情况调整 return pitch >= mediumRange.min && pitch <= mediumRange.max; }, //眨眼计算方法 calculateEyeOpenness(eyeLandmarks) { // 计算眼睛的闭合程度 const eyeTop = eyeLandmarks[1].y; const eyeBottom = eyeLandmarks[5].y; const eyeHeight = eyeBottom - eyeTop; // console.log(eyeHeight, "eyeHeight"); const eyeOpenness = eyeHeight / 3; // 根据实际情况进行调整 return eyeOpenness; }, // 保存当前帧作为图像 async captureStillImage() { if (!this.imageUrl) { const video = document.getElementById("video"); const canvas = document.createElement("canvas"); canvas.width = video.videoWidth; canvas.height = video.videoHeight; const context = canvas.getContext("2d"); context.drawImage(video, 0, 0, canvas.width, canvas.height); const imgDataUrl = canvas.toDataURL("image/jpeg"); // 现在你可以将imgDataUrl用作捕获的静态图像。 // console.log("捕获的图像数据URL:", imgDataUrl); // 如果需要,你还可以在界面上显示捕获的图像。 // 例如,你可以创建一个<img>元素,并将其src属性设置为imgDataUrl。 this.getOssObject(imgDataUrl); } }, //调后台接口获取腾讯云上传临时参数 getOssObject(imgDataUrl) { this.loading = true; this.$api .tencentOss({}) .then((res) => { this.cos = new COS({ SecretId: res.data.cred.Credentials.TmpSecretId, SecretKey: res.data.cred.Credentials.TmpSecretKey, SecurityToken: res.data.cred.Credentials.Token, }); // 上传文件到腾讯云对象存储 this.cos.putObject( { Bucket: res.data.bucket, Region: res.data.region, Key: `/resource/being_dev/user_face/${this.userInfo.user_info.uid}_${Date.now()}.jpg`, // 设置文件在COS中的路径 Body: this.dataURLtoBlob(imgDataUrl), // ContentLength: base64Image.length, }, (err, data) => { if (err) { console.error("Upload error:", err); } else { this.imageUrl = data.Location; // 获取上传后的图片URL if (this.verifyType == "1") { this.personalBindorCheck(this.imageUrl, "1"); } else if (this.verifyType == "2") { this.personalBindorCheck(this.imageUrl, "2"); } } } ); }) .catch((err) => { clearInterval(this.timer); console.log(err); }); }, // 替换域名方法 replaceDomainWithCom(originalUrl, newDomain) { const regex = /^(https?:\/\/)?(www\.)?[^\/]+/; const replacedUrl = originalUrl.replace(regex, newDomain); return replacedUrl; }, // 人脸绑定/校验接口 personalBindorCheck(imageUrl, val) { const newDomain = 'https://b1.being.com'; const replacedUrl = this.replaceDomainWithCom(imageUrl, newDomain); //处理后的新的url const currentDate = new Date(); currentDate.setHours(0, 0, 0, 0); // 设置时分秒为0 const currentDateTimestamp = Math.floor(currentDate.getTime() / 1000); // 转换为秒级时间戳 let ramdomCount = this.generateRandomNumber(32); let deviceModel = `${this.userInfo.user_info.uid}-${currentDateTimestamp}-${this.userInfo.user_info.uid}-${currentDateTimestamp}`; let md5Count = `${replacedUrl}${ramdomCount.slice(-5)}`; let md5param = md5.hexMD5(md5Count); let actionType = ""; if (val == "1") { actionType = "personnel_bind"; } else if (val == "2") { actionType = "face_verify"; } let params = { url: replacedUrl, // 图 device: deviceModel, // 设备号, rand_str: ramdomCount, //随机数 md5: md5param, //加密数 action: actionType, //方法名 }; let apiMethod = null; if (val === "1") { apiMethod = this.$api.personnelBind; } else if (val === "2") { apiMethod = this.$api.faceVerify; } apiMethod(params) .then((res) => { this.loading = false; this.isShowDialog = true; if (!res.error_code) { if (val === "1") { this.type = 3; } else if (val === "2") { this.type = 4; } } else { this.type = res.error_code; } }) .catch((err) => { this.loading = false; console.log(err); }); }, // 去掉Base64头部 dataURLtoBlob(dataurl) { var arr = dataurl.split(","); var mime = arr[0].match(/:(.*?);/)[1]; var bstr = atob(arr[1]); var n = bstr.length; var u8arr = new Uint8Array(n); while (n--) { u8arr[n] = bstr.charCodeAt(n); } return new Blob([u8arr], {type: mime}); }, showDialog() { this.isShowDialog = false; }, }, beforeDestroy() { if (this.timer) { clearInterval(this.timer); this.timer = null; // Add this line to set timer to null } if (this.stillTimer) { clearTimeout(this.stillTimer); this.stillTimer = null; } if (this.countdownTimer) { clearTimeout(this.countdownTimer); this.countdownTimer = null; } if (this.videoStream) { this.videoStream.getTracks().forEach((track) => track.stop()); } }, }; </script> <style> /* 在这里可以添加样式 */ </style> <style scoped lang="less"> .face-verify-approve-index { flex: 1; display: flex; flex-direction: column; background: #fff; position: relative; .loading-wrap { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: #d9d9d9; z-index: 99999; display: flex; /* 使用Flex布局 */ justify-content: center; /* 水平居中 */ align-items: center; /* 垂直居中 */ .loading { width: 1.26rem; height: 1.26rem; background: #ffffff; border-radius: 0.12rem; z-index: 99999; display: flex; flex-direction: column; justify-content: center; align-items: center; & > img { width: 0.44rem; height: 0.44rem; animation: rotate 1s linear infinite; } @keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } & > p { font-size: 0.16rem; font-weight: bold; color: #000000; margin-top: 0.1rem; } } } .header { background-color: #fff; border-bottom: 1px solid #f6f7f8; color: #000; } section { flex: 1; display: flex; flex-direction: column; padding: 0.4rem 0.2rem; box-sizing: border-box; .page { width: 100%; display: flex; flex-direction: column; align-items: center; .title { font-size: 0.24rem; font-weight: bold; color: #000000; line-height: 0.22rem; text-align: center; } .video-page { display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; margin-top: 0.24rem; width: 232px; height: 232px; overflow: hidden; border-radius: 232px; .circle-container { width: 232px; height: 232px; display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden; } /* 使用深度选择器,确保样式作用到 .circle-container 内的 svg 元素 */ /deep/ .circle-container > svg { z-index: 11 !important; } video { width: 215px; height: 215px; border-radius: 50%; // position: absolute; // top: 50%; // left: 50%; // transform: translate(-50%, -50%); overflow: hidden; display: flex; align-items: center; justify-content: center; object-fit: cover; /* 保持视频比例,填满容器 */ // z-index: 99; } .flipped { width: 215px; height: 215px; border-radius: 50%; // position: absolute; // top: 50%; // left: 50%; transform: translate(-50%, -50%); overflow: hidden; display: flex; align-items: center; justify-content: center; object-fit: cover; /* 保持视频比例,填满容器 */ transform: scaleX(-1); } .text { position: absolute; bottom: 0.07rem; left: 50%; transform: translate(-50%, 0); width: 215px; height: 68px; background: rgba(0, 0, 0, 0.5); font-size: 0.16rem; color: #fff; line-height: 0.18rem; text-align: center; padding: 0.16rem 0.35rem; box-sizing: border-box; // z-index: 999; } } } } } </style>
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。