<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>
“相关推荐”对你有帮助么?
-
非常没帮助
-
没帮助
-
一般
-
有帮助
-
非常有帮助
提交