361 lines
9.9 KiB
Vue
361 lines
9.9 KiB
Vue
|
|
<script setup lang="ts">
|
|||
|
|
import { doGetToken } from "@/api/login/HuiBang";
|
|||
|
|
import { loginFaceLogin } from "@/api/login";
|
|||
|
|
definePage({
|
|||
|
|
style: {
|
|||
|
|
navigationBarTitleText: '人脸识别',
|
|||
|
|
navigationStyle: 'custom',
|
|||
|
|
},
|
|||
|
|
})
|
|||
|
|
import { ref, onMounted } from 'vue';
|
|||
|
|
import { HUIBANG_BASE_URL } from '@/config/setting';
|
|||
|
|
import {ensureDecodeURIComponent, parseUrlToObj} from "@/utils";
|
|||
|
|
import {isPageTabbar} from "@/tabbar/store";
|
|||
|
|
import {setToken} from "@/utils/token-util";
|
|||
|
|
import {loginPublic} from "@/utils/login-util";
|
|||
|
|
import {tabbarList} from "@/tabbar/config";
|
|||
|
|
const code = ref<string>('');//用于辅助验证人脸的code
|
|||
|
|
const showFace = ref<boolean>(false);
|
|||
|
|
const facingMode = ref<string>("user");//摄像头类型
|
|||
|
|
const mediaStreamTrack = ref();// 媒体流,用于关闭摄像头
|
|||
|
|
const videoStyle = ref(
|
|||
|
|
{
|
|||
|
|
width: '250px',
|
|||
|
|
height: '250px',
|
|||
|
|
transform: 'rotate(0deg)',
|
|||
|
|
borderRadius: '50%',
|
|||
|
|
}
|
|||
|
|
)
|
|||
|
|
const videoStyle2 = ref(
|
|||
|
|
{
|
|||
|
|
width: '250px',
|
|||
|
|
height: '250px',
|
|||
|
|
borderRadius: '50%',
|
|||
|
|
}
|
|||
|
|
)
|
|||
|
|
const ssoToken = ref<string>('');
|
|||
|
|
const userFaceImage = ref<string>(''); //人脸图片
|
|||
|
|
|
|||
|
|
onMounted(() => {
|
|||
|
|
code.value = uni.getStorageSync("u-code");
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const onCodeConfirm = () => {
|
|||
|
|
if (code.value.length < 4) {
|
|||
|
|
uni.showToast({ title: "请填写完整信息", icon: 'none'})
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
showFace.value = true;
|
|||
|
|
init();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const init = () => {
|
|||
|
|
getToken();
|
|||
|
|
invokingCamera();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
//初始化镜头
|
|||
|
|
const invokingCamera = () => {
|
|||
|
|
showFace.value = true;
|
|||
|
|
// 检查浏览器对摄像头的支持情况
|
|||
|
|
|
|||
|
|
console.log(navigator,'参数数据')
|
|||
|
|
|
|||
|
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|||
|
|
uni.showToast({ title: "您的设备不支持打开摄像头功能", icon: 'none'})
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const constraints = {
|
|||
|
|
audio: false,
|
|||
|
|
video: {
|
|||
|
|
// 前置摄像头
|
|||
|
|
facingMode: {
|
|||
|
|
exact: facingMode.value
|
|||
|
|
},
|
|||
|
|
// 手机端相当于高
|
|||
|
|
width: 200,
|
|||
|
|
// 手机端相当于宽
|
|||
|
|
height: 200,
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
// 请求摄像头权限并获取视频流
|
|||
|
|
navigator.mediaDevices.getUserMedia(constraints)
|
|||
|
|
.then((stream) => {
|
|||
|
|
mediaStreamTrack.value = stream;
|
|||
|
|
// 成功获取到视频流,这里可以对视频流进行处理
|
|||
|
|
// 例如,将视频流显示在页面上的某个元素中
|
|||
|
|
const videoElement = document.querySelector("video");
|
|||
|
|
videoElement!.srcObject = stream;
|
|||
|
|
videoElement!.play();
|
|||
|
|
setTimeout(() => {
|
|||
|
|
handlePhotographClick();
|
|||
|
|
}, 1000);
|
|||
|
|
})
|
|||
|
|
.catch((err) => {
|
|||
|
|
// 获取视频流失败,进行相应提示
|
|||
|
|
console.log('获取摄像头视频流失败:', err);
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
//关闭镜头
|
|||
|
|
const handlePhotographCloseClick = () => {
|
|||
|
|
if (mediaStreamTrack.value) {
|
|||
|
|
// 关闭摄像头
|
|||
|
|
mediaStreamTrack.value.getTracks().forEach(function (track: any) {
|
|||
|
|
track.stop();
|
|||
|
|
});
|
|||
|
|
mediaStreamTrack.value = null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
//拍照
|
|||
|
|
const handlePhotographClick = () => {
|
|||
|
|
const canvas = document.createElement("canvas");
|
|||
|
|
const ctx = canvas.getContext("2d");
|
|||
|
|
const video = document.querySelector("video");
|
|||
|
|
canvas.width = Math.min(video!.videoWidth, video!.videoHeight);
|
|||
|
|
canvas.height = Math.max(video!.videoWidth, video!.videoHeight);
|
|||
|
|
// canvas.width = Math.max(video.videoWidth, video.videoHeight);
|
|||
|
|
// canvas.height = Math.min(video.videoWidth, video.videoHeight);
|
|||
|
|
//console.log(canvas.width);
|
|||
|
|
//console.log(canvas.height);
|
|||
|
|
ctx!.drawImage(video!, 0, 0, canvas.width, canvas.height);
|
|||
|
|
|
|||
|
|
// ****** 镜像处理 ******
|
|||
|
|
function getPixel(imageData: any, row: any, column: any) {
|
|||
|
|
const uint8ClampedArray = imageData.data;
|
|||
|
|
const width = imageData.width;
|
|||
|
|
const height = imageData.height;
|
|||
|
|
const pixel = [];
|
|||
|
|
for (let i = 0; i < 4; i++) {
|
|||
|
|
pixel.push(uint8ClampedArray[row * width * 4 + column * 4 + i]);
|
|||
|
|
}
|
|||
|
|
return pixel;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setPixel(imageData: any, row: any, column: any, pixel: any) {
|
|||
|
|
const uint8ClampedArray = imageData.data;
|
|||
|
|
const width = imageData.width;
|
|||
|
|
const height = imageData.height;
|
|||
|
|
for (let i = 0; i < 4; i++) {
|
|||
|
|
uint8ClampedArray[row * width * 4 + column * 4 + i] = pixel[i];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const mirrorImageData = ctx!.createImageData(canvas.width, canvas.height);
|
|||
|
|
const imageData = ctx!.getImageData(0, 0, canvas.width, canvas.height);
|
|||
|
|
for (let h = 0; h < canvas.height; h++) {
|
|||
|
|
for (let w = 0; w < canvas.width; w++) {
|
|||
|
|
const pixel = getPixel(imageData, h, canvas.width - w - 1);
|
|||
|
|
setPixel(mirrorImageData, h, w, pixel);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
ctx!.putImageData(mirrorImageData, 0, 0);
|
|||
|
|
// ****** 镜像处理 ******
|
|||
|
|
const base64 = canvas.toDataURL("image/jpeg");
|
|||
|
|
userFaceImage.value = base64;
|
|||
|
|
//console.log(self.userFaceImage);
|
|||
|
|
faceLogin();
|
|||
|
|
}
|
|||
|
|
//切换镜头
|
|||
|
|
const handleChangeFacingMode = () => {
|
|||
|
|
facingMode.value = facingMode.value === 'user' ? 'environment' : 'user';
|
|||
|
|
handlePhotographCloseClick();
|
|||
|
|
invokingCamera();
|
|||
|
|
}
|
|||
|
|
//获取token
|
|||
|
|
const getToken = () => {
|
|||
|
|
doGetToken().then((token: string) => {
|
|||
|
|
ssoToken.value = token
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
//人脸登录
|
|||
|
|
const faceLogin = () => {
|
|||
|
|
uni.request({
|
|||
|
|
url: HUIBANG_BASE_URL + '/api/doInterface?code=doStrongFaceLogin',
|
|||
|
|
data: {
|
|||
|
|
token: ssoToken.value,
|
|||
|
|
check_code: code.value,
|
|||
|
|
imgBase64: userFaceImage.value.split(',')[1]
|
|||
|
|
},
|
|||
|
|
method: 'POST',
|
|||
|
|
header: {
|
|||
|
|
'Content-Type': 'application/json'
|
|||
|
|
},
|
|||
|
|
success: (res: any) => {
|
|||
|
|
if (res.data.state === 'ok') {
|
|||
|
|
uni.setStorageSync("u-code", code.value);
|
|||
|
|
loginFaceLogin({ token: res.data.token }).then((res) => {
|
|||
|
|
setToken(res.token)
|
|||
|
|
doLogin()
|
|||
|
|
handlePhotographCloseClick();
|
|||
|
|
})
|
|||
|
|
} else {
|
|||
|
|
handlePhotographClick();
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
fail: (res) => {
|
|||
|
|
uni.showToast({ title: JSON.stringify(res), icon: 'none'})
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
const back = () => {
|
|||
|
|
uni.navigateBack({
|
|||
|
|
delta: 1
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const showKeyboard = ref(false);
|
|||
|
|
const redirectUrl = ref('')
|
|||
|
|
|
|||
|
|
|
|||
|
|
onLoad((options) => {
|
|||
|
|
console.log('login options: ', options)
|
|||
|
|
if (options.redirect) {
|
|||
|
|
redirectUrl.value = ensureDecodeURIComponent(options.redirect)
|
|||
|
|
}
|
|||
|
|
else {
|
|||
|
|
redirectUrl.value = tabbarList[0].pagePath
|
|||
|
|
}
|
|||
|
|
console.log('redirectUrl.value: ', redirectUrl.value)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
function doLogin() {
|
|||
|
|
console.log(redirectUrl.value)
|
|||
|
|
let path = redirectUrl.value
|
|||
|
|
if (!path.startsWith('/')) {
|
|||
|
|
path = `/${path}`
|
|||
|
|
}
|
|||
|
|
const { path: _path, query } = parseUrlToObj(path)
|
|||
|
|
console.log('_path:', _path, 'query:', query, 'path:', path)
|
|||
|
|
console.log('isPageTabbar(_path):', isPageTabbar(_path))
|
|||
|
|
if (isPageTabbar(_path)) {
|
|||
|
|
// 经过我的测试 switchTab 不能带 query 参数, 不管是放到 url 还是放到 query ,
|
|||
|
|
// 最后跳转过去的时候都会丢失 query 信息
|
|||
|
|
uni.switchTab({
|
|||
|
|
url: path,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
else {
|
|||
|
|
console.log('redirectTo:', path)
|
|||
|
|
uni.redirectTo({
|
|||
|
|
url: path,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
</script>
|
|||
|
|
<template>
|
|||
|
|
<view class="content">
|
|||
|
|
<wd-navbar title="人脸识别" left-text="返回" placeholder fixed left-arrow right-text="按钮" @click-left="back">
|
|||
|
|
<template #right>
|
|||
|
|
<wd-icon v-if="showFace" name="camera" size="22px" @click="handleChangeFacingMode"></wd-icon>
|
|||
|
|
</template>
|
|||
|
|
</wd-navbar>
|
|||
|
|
<view class="body">
|
|||
|
|
<view v-if="!showFace" class="code">
|
|||
|
|
<view class="des">
|
|||
|
|
<h3>填写您的手机/身份证后四位</h3>
|
|||
|
|
<p style="margin-top: 15px;color:#909399 ;">请在此处输入</p>
|
|||
|
|
</view>
|
|||
|
|
<view class="code-input">
|
|||
|
|
<!-- 密码输入框 -->
|
|||
|
|
<wd-password-input :length="4" v-model="code" :mask="false" :focused="showKeyboard" @focus="showKeyboard = true" />
|
|||
|
|
<!-- 数字键盘 -->
|
|||
|
|
<wd-number-keyboard v-model="code" extra-key="X" v-model:visible="showKeyboard" :maxlength="4" @blur="showKeyboard = false" />
|
|||
|
|
</view>
|
|||
|
|
<view class="submit">
|
|||
|
|
<view @click="onCodeConfirm" >确定</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
<view v-else class="face">
|
|||
|
|
<view class="face-box">
|
|||
|
|
<video id="video" autoplay :style="facingMode === 'user' ? videoStyle2 : videoStyle" object-fit="fill">
|
|||
|
|
</video>
|
|||
|
|
</view>
|
|||
|
|
<view class="face-tip">
|
|||
|
|
<p>请平视摄像头,并保持光线充足</p>
|
|||
|
|
</view>
|
|||
|
|
<!-- <view class="face-button">
|
|||
|
|
<u-button type="primary" text="开始识别" @click="init()"></u-button>
|
|||
|
|
</view> -->
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.content {
|
|||
|
|
height: 100%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.body {
|
|||
|
|
height: calc(100% - 85px);
|
|||
|
|
background-color: #ffffff;
|
|||
|
|
padding: 20px;
|
|||
|
|
|
|||
|
|
.code {
|
|||
|
|
height: calc(100%);
|
|||
|
|
//display: flex;
|
|||
|
|
//flex-direction: column;
|
|||
|
|
//align-items: center;
|
|||
|
|
|
|||
|
|
.des {
|
|||
|
|
width: 100%;
|
|||
|
|
margin-top: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.code-input {
|
|||
|
|
margin-top: 40px;
|
|||
|
|
margin-bottom: 80px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.submit {
|
|||
|
|
width: 100%;
|
|||
|
|
div {
|
|||
|
|
line-height: 96rpx;
|
|||
|
|
border-radius: 20rpx;
|
|||
|
|
text-align: center;
|
|||
|
|
font-size: 32rpx;
|
|||
|
|
color: #ffffff;
|
|||
|
|
background-color: #409EFF;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.face {
|
|||
|
|
height: calc(100%);
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: center;
|
|||
|
|
|
|||
|
|
.face-tip {
|
|||
|
|
color: #909399;
|
|||
|
|
margin-top: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.face-button {
|
|||
|
|
width: 100%;
|
|||
|
|
margin-top: 20px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
video {
|
|||
|
|
transform: rotateY(180deg);
|
|||
|
|
-webkit-transform: rotateY(180deg);
|
|||
|
|
/* Safari 和 Chrome */
|
|||
|
|
-moz-transform: rotateY(180deg);
|
|||
|
|
border: 1px solid;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 移除:deep()选择器,直接使用类名 */
|
|||
|
|
.uni-video-bar {
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.uni-video-cover {
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|