当前位置: 首页 > news >正文

微信实名认证组件

效果图:

        

子组件:

<template><view class="face-recognition-container" :style="{backgroundColor: bgColor}" v-if="showModal"><!-- 圆形采集框 --><view class="circle-mask"><camera class="camera" device-position="front" flash="off" resolution="medium" v-if="cameraActive"@ready="initCamera" @error="cameraError" @initdone="initCamera"></camera></view><!-- 状态提示 --><view class="status-panel"><view class="timer" v-if="isRecording">{{ countdown }}s</view><view class="tips">{{ tipsText }}</view></view><!-- 操作按钮 --><view class="control-panel"><button class="btn-record" :class="{recording: isRecording}" @click="handleRecord" :disabled="!isReady">{{ isRecording ? '采集中...' : '开始采集' }}</button><button class="btn-cancel" @click="handleCancel" v-if="showCancel">取消</button></view></view>
</template><script>import ServerConfig from '@/common/utils/config.js';const RECORD_TIME = 15; // 录制时长(秒)const QUALITY_THRESHOLD = 0.8; // 质量合格阈值export default {data() {return {showModal: false,cameraActive: true,isReady: false,isRecording: false,tipsText: '请将人脸对准圆圈',bgColor: 'rgba(0,0,0,0.7)',countdown: RECORD_TIME,showCancel: true,vkSession: null,countdownTimer: null,bestFaceData: {face: null,quality: 0,timestamp: 0},bgColors: ['rgba(0, 255, 0, 0.7)',    // 绿色'rgba(255, 255, 0, 0.7)',   // 黄色'rgba(255, 255, 255, 0.7)',  // 紫色'rgba(0, 255, 255, 0.7)',    // 橙色'rgba(255, 0, 0, 0.7)'      // 红色],cameraContext: null,videoPath: '',faceDetectionCount: 0,listener: null};},methods: {show() {this.showModal = true;this.cameraActive = true;this.cameraContext = wx.createCameraContext();},hide() {this.showModal = false;this.stopDetection();},initCamera() {this.initFaceDetection();},initFaceDetection() {this.stopDetection();try {wx.initFaceDetect();this.listener = this.cameraContext.onCameraFrame((frame) => {if (this.isVerify || this.isRecording) return;wx.faceDetect({frameBuffer: frame.data,width: frame.width,height: frame.height,enablePoint: true,enableConf: true,enableAngle: true,enableMultiFace: true,success: (faceData) => {this.handleFaceDetection(faceData, frame);},fail: (err) => {this.handleFaceError(err);}});});this.listener.start();this.isReady = true;this.tipsText = '请将人脸对准圆圈';} catch (e) {console.error('人脸检测初始化异常:', e);this.tipsText = '人脸识别不可用';this.showCancel = true;}},handleFaceDetection(faceData, frame) {if (!faceData.faceInfo || faceData.faceInfo.length === 0) {this.tipsText = '请正对摄像头';return;}let face = faceData.faceInfo[0];if (face.x == -1 || face.y == -1) {this.tipsText = '检测不到人';return;}// 多人检测if (faceData.faceInfo.length > 1) {this.tipsText = '请保证只有一个人';return;}// 角度检测const { pitch, roll, yaw } = face.angleArray;const standard = 0.5;if (Math.abs(pitch) >= standard || Math.abs(roll) >= standard || Math.abs(yaw) >= standard) {this.tipsText = '请平视摄像头';return;}// 五官遮挡检测if (face.confArray.global <= QUALITY_THRESHOLD || face.confArray.leftEye <= QUALITY_THRESHOLD ||face.confArray.mouth <= QUALITY_THRESHOLD || face.confArray.nose <= QUALITY_THRESHOLD ||face.confArray.rightEye <= QUALITY_THRESHOLD) {this.tipsText = '请勿遮挡五官';return;}// 位置检测const centerWidth = 250;const centerHeight = 250;if (face.x < (frame.width - centerWidth)/2 || face.x > (frame.width + centerWidth)/2 ||face.y < (frame.height - centerHeight)/2 || face.y > (frame.height + centerHeight)/2) {this.tipsText = '请将人脸对准中心位置';return;}// 录制时记录最佳人脸if (this.isRecording) {const currentQuality = face.confArray.global;if (!this.bestFaceData.face || currentQuality > this.bestFaceData.quality) {this.bestFaceData = {face: face,quality: currentQuality,timestamp: Date.now()};}} else {this.tipsText = '位置良好,可以开始采集';}},handleFaceError(err) {if (this.isVerify || this.isRecording) return;this.tipsText = err.x == -1 ? '检测不到人' : (err.errMsg || '网络错误');},handleRecord() {if (this.isRecording) return;// 重置最佳人脸记录this.bestFaceData = {face: null,quality: 0,timestamp: 0};this.showCancel = false;this.isRecording = true;this.countdown = RECORD_TIME;this.bgColor = 'rgba(30,50,80,0.7)';this.cameraContext.startRecord({success: () => {this.countdownTimer = setInterval(() => {this.countdown--;this.bgColor = this.bgColors[Math.floor(Math.random() * this.bgColors.length)];if (this.countdown <= 0) {this.stopRecording();}}, 1000);},fail: (err) => {console.error('录制失败:', err);this.tipsText = '录制失败,请重试';this.isRecording = false;this.showCancel = true;}});},stopRecording() {clearInterval(this.countdownTimer);this.tipsText = '视频处理中...';this.cameraContext.stopRecord({success: (res) => {this.videoPath = res.tempVideoPath;// 检查人脸质量是否合格if (this.bestFaceData.quality >= QUALITY_THRESHOLD) {this.uploadVideo();} else {this.handleUploadError('人脸质量不合格,请重新采集');this.reset();}},fail: (err) => {console.error('停止录制失败:', err);this.handleUploadError('视频处理失败');this.isRecording = false;this.showCancel = true;}});},uploadVideo() {uni.showLoading({title: '上传中...',mask: true});uni.uploadFile({url: ServerConfig.SERVER_URL + '/common/uploadVideo',filePath: this.videoPath,name: 'video',header: {'Authorization': uni.getStorageSync('token')},success: (res) => {uni.hideLoading();try {const data = JSON.parse(res.data);if (data.code === 200) {// this.$emit('success', {// 	videoUrl: data.url,// 	bestFace: this.bestFaceData.face// });this.$emit('success', data.url);this.tipsText = '上传成功';setTimeout(() => {this.reset();}, 1500);} else {this.handleUploadError(data.msg || '上传失败');}} catch (e) {this.handleUploadError('解析响应失败');}},fail: (err) => {this.handleUploadError('上传失败: ' + err.errMsg);}});},handleUploadError(msg) {uni.hideLoading();this.tipsText = msg;this.bgColor = 'rgba(80,30,30,0.7)';this.showCancel = true;this.isRecording = false;console.error(msg);},reset() {this.stopDetection();this.isReady = false;this.isRecording = false;this.tipsText = '请将人脸对准圆圈';this.bgColor = 'rgba(0,0,0,0.7)';this.showCancel = true;this.bestFaceData = {face: null,quality: 0,timestamp: 0};setTimeout(() => {this.initFaceDetection();}, 500);},handleCancel() {this.stopDetection();this.hide();this.$emit('cancel');},stopDetection() {if (this.countdownTimer) clearInterval(this.countdownTimer);if (this.listener) {try {this.listener.stop();} catch (e) {console.warn('停止帧监听异常:', e);}}if (this.isRecording) {this.cameraContext.stopRecord();}this.countdownTimer = null;this.listener = null;},cameraError(e) {console.error('Camera error:', e.detail);this.tipsText = '请允许使用摄像头';this.showCancel = true;}},beforeDestroy() {this.stopDetection();}};
</script><style lang="scss">.face-recognition-container {position: fixed;top: 0;left: 0;width: 100%;height: 100%;display: flex;flex-direction: column;align-items: center;justify-content: center;transition: background-color 0.3s;z-index: 1000;.circle-mask {width: 500rpx;height: 500rpx;border-radius: 50%;overflow: hidden;border: 4rpx solid #07C160;box-shadow: 0 0 30rpx rgba(7, 193, 96, 0.5);margin-top: -200px;.camera {width: 100%;height: 100%;}}.status-panel {margin-top: 60rpx;width: 80%;text-align: center;.timer {font-size: 48rpx;color: #FFFFFF;margin-bottom: 20rpx;font-weight: bold;}.tips {font-size: 32rpx;color: #FFFFFF;margin-bottom: 30rpx;}.quality-indicator {height: 8rpx;background: linear-gradient(to right, #FF4D4F, #FAAD14, #52C41A);border-radius: 4rpx;transition: width 0.3s;}}.control-panel {position: absolute;bottom: 100rpx;width: 100%;display: flex;justify-content: center;gap: 40rpx;button {margin: 0;border: none;color: #FFFFFF;font-size: 32rpx;border-radius: 50rpx;padding: 0 60rpx;height: 80rpx;line-height: 80rpx;&.btn-record {background: #07C160;&.recording {background: #1890FF;}}&.btn-cancel {background: rgba(255, 77, 79, 0.8);}&[disabled] {background: #CCCCCC !important;}}}}
</style>

父组件:

<!-- pages/mine/realnameAuth/realnameAuth.vue -->
<template><view class="container"><view class="auth-card" v-show="isShow"><view class="card-header"><text class="card-title">{{ isVerified ? '已完成认证' : '实名认证' }}</text><text class="card-subtitle">{{ isVerified ? '您已完成实名认证,无需重复认证' : '请填写真实姓名和身份证号码' }}</text></view><view v-if="!isVerified"><view class="form-group"><text class="form-label">真实姓名</text><input class="form-input" v-model="formData.name" placeholder="请输入真实姓名"placeholder-class="placeholder-style" @blur="validateField('name')"/><view class="error-message" v-if="errors.name">{{ errors.name }}</view></view><view class="form-group"><text class="form-label">身份证号</text><input class="form-input" v-model="formData.idCard" placeholder="请输入身份证号码"placeholder-class="placeholder-style" maxlength="18"  @blur="validateField('idCard')"/><view class="error-message" v-if="errors.idCard">{{ errors.idCard }}</view>	</view><view class="form-group"><text class="form-label">人脸识别</text><button class="submit-btn" :class="{ 'disabled': !isStartRecognition }":disabled="!isStartRecognition"@click="startRecognition">开始认证</button></view></view><view v-else class="verified-info"><view class="success-icon">✓</view><text class="success-text">您已完成实名认证</text><button class="back-btn" @click="goBack">返回</button></view></view><!-- 提示信息 --><view class="tips" v-show="isShow"><text class="tip-text" v-if="!isVerified">· 实名认证信息一经提交不可修改</text><text class="tip-text" v-if="!isVerified">· 请确保信息真实有效</text></view><face-recognition ref="faceRecognition" @success="handleSuccess" @cancel="handleCancel" /></view>
</template><script>import {Id2MetaVerify} from '@/common/api/user.js'import {getInfo,checkLogin} from '@/common/api/login.js';import FaceRecognition from '@/component/face.vue'export default {components:{FaceRecognition},data() {return {// 表单数据formData: {name: '',idCard: '',facePath: '' // 存储选择的图片路径},errors: {name: '',idCard: ''},isVerified: false,//是否已实名isStartRecognition : false , //是否可以人脸识别isShow: true}},computed: {// 表单验证isFormValid() {return this.validateField('name') != '' ||this.validateField('idCard') !== '' }},watch: {formData: {handler(newVal) {if (newVal) {this.isStartRecognition = this.validateField('name') && this.validateField('idCard')}},deep: true,immediate: true,},},onShow() {// 检查用户是否已登录checkLogin().then(res => {if (res.code != 200) {uni.clearStorageSync();this.$refs.loginModalRef.onShow()return}})this.isVerified = getApp().globalData.userInfo.isVerified == 1;},methods: {startRecognition() {this.isShow = falsethis.$refs.faceRecognition.show();},handleSuccess(imageUrl) {this.isShow = trueconsole.log('人脸图片URL:', imageUrl);// 更新用户头像等操作this.facePath = imageUrlthis.submitAuth()},handleCancel() {this.isShow = trueconsole.log('用户取消了采集');},validateField(field) {switch (field) {case 'name':if (!this.formData.name) {this.errors.name = '请输入姓名'return false;} else {this.errors.name = ''return true;}case 'idCard':if (!this.formData.idCard) {this.errors.idCard = '请输入身份证号码'return false;} else if (!/^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/.test(this.formData.idCard)) {this.errors.idCard = '请输入有效的身份证号码'return false;} else {this.errors.idCard = ''return true;}case 'facePath':if (!this.formData.facePath) {this.errors.facePath = '人脸识别未通过,请重新识别'return false;} else {this.errors.idCard = ''return true;}}},// 提交认证async submitAuth() {let isSubmit = this.validateField('name') && this.validateField('idCard') && this.validateField("facePath")if(!isSubmit){return}uni.showLoading({title:"认证中..."})try {let data = {idNumber:this.formData.idCard,idName:this.formData.name,url:this.formData.facePath}let response = await Id2MetaVerify(data);if (response.code === 200) {uni.showToast({title: '认证成功',icon: 'success'})uni.reLaunch({url:'/pages/mine/mine'})} else {uni.showToast({title: response.msg || '认证失败,认证信息有误',icon: 'none'})}} catch (error) {uni.showToast({title: '认证请求失败',icon: 'none'})console.error('认证请求失败:', error)} finally {uni.hideLoading();}},// 返回上一页goBack() {uni.navigateBack()}}}
</script><style lang="scss">.container {background-color: #121326;min-height: 100vh;padding: 20rpx 30rpx;}.auth-card {background-color: #1e1f38;border-radius: 20rpx;padding: 40rpx 30rpx;margin-top: 30rpx;box-shadow: 0 10rpx 20rpx rgba(0, 0, 0, 0.2);}.card-header {text-align: center;margin-bottom: 50rpx;}.card-title {font-size: 36rpx;font-weight: 600;color: #C0C2CF;display: block;margin-bottom: 16rpx;}.card-subtitle {font-size: 26rpx;color: #888999;}.form-group {margin-bottom: 40rpx;}.form-label {display: block;font-size: 28rpx;color: #C0C2CF;margin-bottom: 16rpx;}.form-input {width: 100%;height: 80rpx;background-color: #2a2c40;border-radius: 12rpx;padding: 0 24rpx;font-size: 28rpx;color: #C0C2CF;border: 1rpx solid #3a3c58;box-sizing: border-box;}.placeholder-style {color: #888999;}.upload-btn {width: 100%;height: 80rpx;background-color: #2a2c40;border-radius: 12rpx;color: #C0C2CF;font-size: 28rpx;border: 1rpx solid #3a3c58;margin-bottom: 20rpx;}.preview-image {width: 200rpx;height: 200rpx;border-radius: 12rpx;margin-top: 20rpx;}.submit-btn {width: 100%;height: 80rpx;background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);border-radius: 12rpx;color: #fff;font-size: 32rpx;font-weight: 500;margin-top: 20rpx;border: none;&.disabled {background: #3a3c58;color: #888999;}}.tips {margin-top: 40rpx;padding: 0 20rpx;}.tip-text {display: block;font-size: 24rpx;color: #888999;margin-bottom: 10rpx;}.verified-info {text-align: center;padding: 40rpx 0;}.success-icon {width: 100rpx;height: 100rpx;border-radius: 50%;background-color: #4caf50;color: white;font-size: 60rpx;line-height: 100rpx;margin: 0 auto 30rpx;}.success-text {font-size: 32rpx;color: #C0C2CF;display: block;margin-bottom: 40rpx;}.back-btn {width: 100%;height: 80rpx;background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);border-radius: 12rpx;color: #fff;font-size: 32rpx;font-weight: 500;border: none;}.error-message {color: #ff6b6b;font-size: 24rpx;margin-top: 8rpx;display: flex;align-items: center;}.error-message::before {content: "!";display: inline-block;width: 20rpx;height: 20rpx;border-radius: 50%;background-color: #ff6b6b;color: white;font-size: 16rpx;text-align: center;line-height: 20rpx;margin-right: 6rpx;}</style>

http://www.dtcms.com/a/333034.html

相关文章:

  • 二十四、Mybatis-基础操作-删除(预编译SQL)
  • SAP ALV导出excel 报 XML 错误的 /xl/sharedStrings.xml
  • Android协程的用法大全
  • 汽车电子:现代汽车的智能核心
  • Unity_数据持久化_Json
  • 使用原生css实现word目录样式,标题后面的...动态长度并始终在标题后方(生成点线)
  • 第七十章:告别“手写循环”噩梦!Trainer结构搭建:PyTorch Lightning让你“一键炼丹”!
  • Codeforces Deque工艺
  • 用 FreeMarker 动态构造 SQL 实现数据透视分析
  • STM32学习笔记12-串口数据包收发FlyMcuST-LINK Utility
  • Shortest Routes II(Floyd最短路)
  • 管家婆辉煌系列试用版/期限说明
  • Shader开发(十三)理解片元插值
  • 淘米自动签到脚本
  • 大气负氧离子自动监测站:解密空气的科技密码
  • 有红帽认证证书可以0元置换华为openEuler-HCIA/HCIP认证
  • OpenSCA开源社区每日安全漏洞及投毒情报资讯|13th Aug. , 2025
  • MyBatis StatementHandler核心原理详解
  • Nginx反向代理Tomcat实战指南
  • mysql-DDLy语句案例
  • 基于asp.net#C##VUE框架的独居老人物资配送系统的设计与实现#sql server#visual studio
  • OpenZeppelin Contracts 架构分层分析
  • 基于机器学习的赌博网站识别系统设计与实现
  • 【计算机视觉与深度学习实战】02基于形态学的权重自适应图像去噪系统
  • 【车联网kafka】常用参数及其命令总结(第八篇)
  • 【展厅多媒体】数字展厅小知识:实物识别桌是什么?
  • 杂记 02
  • Java研学-SpringCloud(四)
  • YOLO12 改进、魔改|幅度感知线性注意力MALA,提升小目标、遮挡的检测能力
  • FDBus CBaseWork运行在当前线程