diff --git a/bundle/face/face-info.vue b/bundle/face/face-info.vue new file mode 100644 index 0000000..9820524 --- /dev/null +++ b/bundle/face/face-info.vue @@ -0,0 +1,214 @@ + + + + + diff --git a/bundle/face/face-photo.vue b/bundle/face/face-photo.vue new file mode 100644 index 0000000..a2ca619 --- /dev/null +++ b/bundle/face/face-photo.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/bundle/reserve/details.vue b/bundle/reserve/details.vue index 55cfd62..6b83f09 100644 --- a/bundle/reserve/details.vue +++ b/bundle/reserve/details.vue @@ -688,7 +688,7 @@ export default { uni.$off("payment") if (params.result) { uni.redirectTo({ - url: `/bundle/reserve/notice?order_id=${params.order_id}` + url: `/bundle/reserve/notice?order_id=${params.order_id}&ground_type=${self.venue.type_id}` }) } else { uni.redirectTo({ @@ -761,11 +761,11 @@ export default { uni.$off("payment") if (params.result) { uni.redirectTo({ - url: `/bundle/reserve/notice?order_id=${params.order_id}` + url: `/bundle/reserve/notice?order_id=${params.order_id}&ground_type=${self.venue.type_id}` }) } else { uni.redirectTo({ - url: '/pages/order/cg-my-order' + url: '/pages/order/cg-my-order?type=2' }) } }, 500) diff --git a/bundle/reserve/notice.vue b/bundle/reserve/notice.vue index 8eb036c..80113ea 100644 --- a/bundle/reserve/notice.vue +++ b/bundle/reserve/notice.vue @@ -12,18 +12,39 @@ 查看订单 完成 + + + + + + + 提示 + + 为了给您提供更安全、便捷的入场体验,请完成人脸识别认证。只需几秒钟,轻松刷脸入场! + + 去录入 + + @@ -111,4 +145,35 @@ } } } + + .notice-popup { + padding-bottom: 20rpx; + + .title { + font-size: 36rpx; + color: #303133; + line-height: 50rpx; + text-align: center; + } + + .desc { + margin-top: 48rpx; + font-weight: 400; + font-size: 32rpx; + color: #303133; + line-height: 52rpx; + text-align: center; + } + + .btn { + font-size: 32rpx; + background: #365A9A; + border-radius: 8rpx; + margin: 92rpx 44rpx 0; + height: 80rpx; + line-height: 80rpx; + text-align: center; + color: #fff; + } + } diff --git a/manifest.json b/manifest.json index 4d4aebd..fb06817 100644 --- a/manifest.json +++ b/manifest.json @@ -157,6 +157,9 @@ "permission" : { "scope.userLocation" : { "desc" : "获取您与体育场馆的距离" + }, + "scope.camera": { + "desc": "需要获取摄像头权限进行人脸录入" } } }, diff --git a/pages.json b/pages.json index a49ddff..91d7df9 100644 --- a/pages.json +++ b/pages.json @@ -1013,6 +1013,18 @@ "style": { "navigationStyle": "custom" } + }, + { + "path": "face/face-info", + "style": { + "navigationBarTitleText": "" + } + }, + { + "path": "face/face-photo", + "style": { + "navigationBarTitleText": "" + } } ] }], diff --git a/pages/order/cg-my-order.vue b/pages/order/cg-my-order.vue index 9036015..3167377 100644 --- a/pages/order/cg-my-order.vue +++ b/pages/order/cg-my-order.vue @@ -133,6 +133,11 @@ + + + + + @@ -251,6 +256,10 @@ } }, onLoad(e) { + if (e.type) { + this.ballType = e.type; + } + if (typeof e.dataType != 'undefined') { this.dataType = e.dataType; } @@ -572,6 +581,12 @@ uni.navigateTo({ url: `/bundle/reserve/details?id=${ground_id}&typeId=${this.ballType}` }); + }, + + onTakePhoto(order_id) { + uni.navigateTo({ + url: `/bundle/face/face-info?order_id=${order_id}` + }); } } }; diff --git a/uni_modules/face-bio-assay/changelog.md b/uni_modules/face-bio-assay/changelog.md new file mode 100644 index 0000000..ba72fba --- /dev/null +++ b/uni_modules/face-bio-assay/changelog.md @@ -0,0 +1,36 @@ +## 1.1.1(2023-04-13) +文档更新:动作容器的使用案例 +## 1.1.0(2023-04-13) +整理代码,补充文档信息 +## 1.0.14(2023-04-13) +更新文档 +## 1.0.13(2023-04-13) +更新文档信息 +## 1.0.12(2023-04-13) +修复授权弹窗bug +## 1.0.11(2023-04-11) +文档调整 +## 1.0.10(2023-04-11) +更新文档 +## 1.0.9(2023-04-11) +1.优化点头摇头动作 +2.解决页面回退在ios出现空白页 +3.调整ui布局 +## 1.0.8(2023-04-03) +修改文档,并提出在ios中二次调用bug的解决方案 +## 1.0.7(2023-03-28) +未通过核验停留两秒,对未授权拍照bug修复 +## 1.0.6(2023-03-27) +提示用户开启摄像头权限 +## 1.0.5(2023-03-22) +bug修复,文档修订 +## 1.0.4(2023-03-21) +更新文档 +## 1.0.3(2023-03-21) +新增 动作容器(ActionContainer) 类 开发者可通过自定义动作 添加到该容器中管理。也可使用已有动作进行排列组合创建动作组 +## 1.0.2(2023-03-16) +调整通过条件,一次行为就可以通过验证 +## 1.0.1(2023-03-15) +参数调整,提高通过率 +## 1.0.0(2023-03-14) +基于微信小程序的人脸识别做的简易版活体检测 diff --git a/uni_modules/face-bio-assay/components/face-bio-assay/ActionContainer.js b/uni_modules/face-bio-assay/components/face-bio-assay/ActionContainer.js new file mode 100644 index 0000000..c2f2117 --- /dev/null +++ b/uni_modules/face-bio-assay/components/face-bio-assay/ActionContainer.js @@ -0,0 +1,59 @@ +class ActionContainer { + //动作组 actions + //起始动作下标 index + //初始提示 tip + //绑定 完成所有动作的回调 endFun + //绑定 动作进行中 ingFun + //绑定 动作完成时 successFun + //绑定 动作失败时 failFun + constructor(actions, index, tip, endFun, ingFun, successFun, failFun) { + this.actions = actions || [] + this.index = index || 0 + this.tip = tip || '检测不到人脸' + this.endFun = endFun || this.__tempFun + this.ingFun = ingFun || this.__tempFun + this.successFun = successFun || this.__tempFun + this.failFun = failFun || this.__tempFun + } + __tempFun(){ + + } + next(faceData) { + if (this.index >= this.actions.length) { + this.endFun() + return this + } + if (this.actions[this.index].state === 'ing') { + this.tip = this.actions[this.index].tip + this.actions[this.index].check(faceData) + this.ingFun() + return this + } else if (this.actions[this.index].state === 'success') { + this.index++; + this.successFun() + return this + } else if (this.actions[this.index].state === 'fail') { + this.failFun() + return this + } + return this + } + end(fun) { //绑定 完成所有动作的回调 + this.endFun = fun || this.__tempFun + return this + } + ing(fun) { //绑定 动作进行中 + this.ingFun = fun || this.__tempFun + return this + } + success(fun) { //绑定 动作完成时 + this.successFun = fun || this.__tempFun + return this + } + fail(fun) { //绑定 动作失败时 + this.failFun = fun || this.__tempFun + return this + } +} + +export default ActionContainer diff --git a/uni_modules/face-bio-assay/components/face-bio-assay/actions/Action.js b/uni_modules/face-bio-assay/components/face-bio-assay/actions/Action.js new file mode 100644 index 0000000..8190fa5 --- /dev/null +++ b/uni_modules/face-bio-assay/components/face-bio-assay/actions/Action.js @@ -0,0 +1,57 @@ +const STATE = { + ING:'ing', + SUCCESS:'success', + FAIL:'fail' +} +export default class Action { + constructor(second=10,fun,limit,initTip) { + this.second = second + this.endTime = Infinity + this.frames = [] + this.tip = initTip + this.initTip = initTip + this.state = STATE.ING + this.fun = fun + this.limit = limit + } + end(){ + if(this.fun){ + this.fun(this.state) + } + } + check(faceData){ + if(this.endTime === Infinity){ + this.endTime = new Date().getTime() + (this.second*1000) + } + if(this.state !== STATE.ING ){ + return + } + if(new Date().getTime()>this.endTime){ + this.state = STATE.FAIL + this.end() + return + } + if(this.frames.length>=this.limit){ + this.state = STATE.SUCCESS + this.end() + return + } + this.takeFrameAfter(faceData)?.takeFrame(faceData) + } + takeFrame(faceData){ + + } + takeFrameAfter(faceData){ + let face = faceData.faceInfo[0] + this.tip = this.initTip + if(faceData.x == -1 || faceData.y == -1) { + this.tip = '检测不到人脸' + return null + } + if(faceData.faceInfo.length > 1) { + this.tip = '请保证只有一人做核验' + return null + } + return this + } +} \ No newline at end of file diff --git a/uni_modules/face-bio-assay/components/face-bio-assay/actions/NodHead.js b/uni_modules/face-bio-assay/components/face-bio-assay/actions/NodHead.js new file mode 100644 index 0000000..319a2a0 --- /dev/null +++ b/uni_modules/face-bio-assay/components/face-bio-assay/actions/NodHead.js @@ -0,0 +1,24 @@ +import Action from "./Action.js" +class NodHead extends Action { + constructor(second=10,fun) { + super(second,fun,1,'请点头') + this.maxPitch = 0 + this.minPitch = 0 + } + takeFrame(faceData){ + let face = faceData.faceInfo[0] + if(face.angleArray.pitch>this.maxPitch){ + this.maxPitch = face.angleArray.pitch + } + if(face.angleArray.pitch0.45 && (this.minPitch || this.maxPitch) ){ + this.frames.push('点头') + this.maxPitch = 0 + this.minPitch = 0 + } + } +} + +export default NodHead \ No newline at end of file diff --git a/uni_modules/face-bio-assay/components/face-bio-assay/actions/ShakeHead.js b/uni_modules/face-bio-assay/components/face-bio-assay/actions/ShakeHead.js new file mode 100644 index 0000000..fbb7eff --- /dev/null +++ b/uni_modules/face-bio-assay/components/face-bio-assay/actions/ShakeHead.js @@ -0,0 +1,24 @@ +import Action from "./Action.js" +class ShakeHead extends Action { + constructor(second = 10, fun) { + super(second,fun,1,'请摇头') + this.minYaw = 0 + this.maxYaw = 0 + } + takeFrame(faceData) { + let face = faceData.faceInfo[0] + if(face.angleArray.yaw>this.maxYaw){ + this.maxYaw = face.angleArray.yaw + } + if(face.angleArray.yaw0.45 && (this.minYaw || this.maxYaw) ){ + this.frames.push('摇头') + this.minYaw = 0 + this.maxYaw = 0 + } + } +} + +export default ShakeHead diff --git a/uni_modules/face-bio-assay/components/face-bio-assay/actions/StraightenHead.js b/uni_modules/face-bio-assay/components/face-bio-assay/actions/StraightenHead.js new file mode 100644 index 0000000..7266472 --- /dev/null +++ b/uni_modules/face-bio-assay/components/face-bio-assay/actions/StraightenHead.js @@ -0,0 +1,27 @@ +import Action from "./Action.js" +class StraightenHead extends Action { + constructor(second = 10, fun) { + super(second, fun, 10, '请平视摄像头') + } + takeFrame(faceData) { + let face = faceData.faceInfo[0] + if (Math.abs(face.angleArray.pitch) >= 0.3 || Math.abs(face.angleArray.roll) >= 0.2 || Math.abs(face + .angleArray.yaw) >= 0.2) { + this.frames = [] + return + } + if (Math.abs(face.confArray.global) <= 0.8 || Math.abs(face.confArray.leftEye) <= 0.8 || Math.abs( + face.confArray.mouth) <= + 0.8 || Math.abs(face.confArray.nose) <= 0.8 || Math.abs(face.confArray.rightEye) <= 0.8) { + this.tip = '请勿遮挡五官' + this.frames = [] + return + } + this.tip = '正在核验,请保持' + this.frames.push('正') + + } +} + + +export default StraightenHead diff --git a/uni_modules/face-bio-assay/components/face-bio-assay/actions/index.js b/uni_modules/face-bio-assay/components/face-bio-assay/actions/index.js new file mode 100644 index 0000000..3bc3ca3 --- /dev/null +++ b/uni_modules/face-bio-assay/components/face-bio-assay/actions/index.js @@ -0,0 +1,9 @@ +import NodHead from "./NodHead.js" +import ShakeHead from "./ShakeHead.js" +import StraightenHead from "./StraightenHead.js" + +export { + NodHead, + ShakeHead, + StraightenHead, +} \ No newline at end of file diff --git a/uni_modules/face-bio-assay/components/face-bio-assay/face-bio-assay.vue b/uni_modules/face-bio-assay/components/face-bio-assay/face-bio-assay.vue new file mode 100644 index 0000000..aeb30ae --- /dev/null +++ b/uni_modules/face-bio-assay/components/face-bio-assay/face-bio-assay.vue @@ -0,0 +1,436 @@ + + + + + diff --git a/uni_modules/face-bio-assay/package.json b/uni_modules/face-bio-assay/package.json new file mode 100644 index 0000000..50f561d --- /dev/null +++ b/uni_modules/face-bio-assay/package.json @@ -0,0 +1,81 @@ +{ + "id": "face-bio-assay", + "displayName": "face-bio-assay", + "version": "1.1.1", + "description": "纯前端简易版活体检测,易扩展,开发者可通过自定义动作 添加到该容器中管理。也可使用已有动作(点头、摇头、平视)进行排列组合创建动作组", + "keywords": [ + "人脸识别", + "活体检测", + "易扩展" +], + "repository": "", +"engines": { + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "" + }, + "uni_modules": { + "dependencies": [], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "Vue": { + "vue2": "y", + "vue3": "u" + }, + "App": { + "app-vue": "u", + "app-nvue": "u" + }, + "H5-mobile": { + "Safari": "u", + "Android Browser": "u", + "微信浏览器(Android)": "u", + "QQ浏览器(Android)": "u" + }, + "H5-pc": { + "Chrome": "u", + "IE": "u", + "Edge": "u", + "Firefox": "u", + "Safari": "u" + }, + "小程序": { + "微信": "y", + "阿里": "u", + "百度": "u", + "字节跳动": "u", + "QQ": "u" + }, + "快应用": { + "华为": "u", + "联盟": "u" + } + } + } + } +} \ No newline at end of file diff --git a/uni_modules/face-bio-assay/readme.md b/uni_modules/face-bio-assay/readme.md new file mode 100644 index 0000000..aeb3ac3 --- /dev/null +++ b/uni_modules/face-bio-assay/readme.md @@ -0,0 +1,185 @@ +# props +|参数名|类型|默认值|可选择值|描述| +|----|----|----|---|---| +|buildActionContainer|Function|null|""|创建ActionContainer类的函数,要求返回值必需是ActionContainer类| +|actions|Array|[]|""|动作组(目前已有动作:点头,摇头,平视)| +|isDev|boolen|false|true|是否是开发者模式,开启后可显示人脸的三个角度| + +# emits +|方法名|参数|描述| +|----|---|---| +|detectFailed|[]|核验失败| +|photoChange|[url:'照片路径']|拍照后的回调| +|detectOver|[]|检测完成| +|showData|[faceData:'人脸数据']|每一帧的人脸数据| +# slots +|插槽名|参数|描述| +|----|---|---| +|default|无|用户可配合showData钩子展示人脸数据方便调试| + +# 方法 +## initData +开始进行人脸核验 使用案例: +``` +//html + +//js +//调用 +this.$refs.faceDetect.initData() +``` +## takePhoto +拍照获取照片,配合动作使用: + ``` + 在每个动作创建时第二个参数是动作完成的回调 如平视动作的使用: + const fun = (state) => { + //state 有成功success和fail,ing(进行时不会触发该函数,忽略) + if (state === 'success') { + this.$refs.faceDetect.takePhoto() //调用拍照方法 + } + } + let straightenHead = new StraightenHead(10, fun) + ``` + +# 使用建议 +``` +ios中bug解决方案: +在ios中,二次进入使用该组件有问题,解决办法:单独将该组件作为一个页面(或者下载demo查看),代码如下: + +//主页面 + + + + + + +//face.vue页 + + + + +``` + +# 类的使用 +## Action + 动作类,开发者可继承该类重写 takeFrame 方法 如: +``` +//点头动作 +import Action from "./Action.js" +class NodHead extends Action { + constructor(second=10,fun) { + //时间限制(s),结束时回调,完成次数(limit),基本提示 + super(second,fun,1,'请点头') + this.maxPitch = 0 + this.minPitch = 0 + } + takeFrame(faceData){ + let face = faceData.faceInfo[0] + if(face.angleArray.pitch>this.maxPitch){ + this.maxPitch = face.angleArray.pitch + } + if(face.angleArray.pitch0.45 && (this.minPitch || this.maxPitch) ){ + this.frames.push('点头') //frames 完成的帧数组 根据该数组长度和limit判断是否完成 + this.maxPitch = 0 + this.minPitch = 0 + } + } +} + +export default NodHead +``` +## ActionContainer + 动作容器的使用案例 +``` +buildActions() { + if (this.buildActionContainer) { + return this.buildActionContainer() + } + let actions = [] + if (!this.actions?.length) { + let nodHead = new NodHead() + const fun = (state) => { + if (state === 'success') { + this.takePhoto() //拍照 + } + } + let straightenHead = new StraightenHead(10, fun) //平视 结束拍照 + let straightenHead2 = new StraightenHead(10) //平视 + let shakeHead = new ShakeHead() + actions = [straightenHead2, nodHead, shakeHead, straightenHead] + } else { + actions = this.actions + } + //new ActionContainer(actions,...) + //动作组 actions + //起始动作下标 index + //初始提示 tip + //绑定 完成所有动作的回调 endFun + //绑定 动作进行中 ingFun + //绑定 动作完成时 successFun + //绑定 动作失败时 failFun + let act = new ActionContainer(actions) + act.end(() => { //也可用该方法绑定endFun方法 + this.detectOver() + }).fail(() => { //也可用该方法绑定failFun方法 + this.cameraError() + }) + //....其他方法类似 + return act + } +``` \ No newline at end of file diff --git a/uni_modules/face-bio-assay/static/images/cover.png b/uni_modules/face-bio-assay/static/images/cover.png new file mode 100644 index 0000000..b41f26a Binary files /dev/null and b/uni_modules/face-bio-assay/static/images/cover.png differ