addGuest.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071
  1. <template>
  2. <view class="fill-form" v-if="step===STEP.FILL">
  3. <view class="tips">
  4. <text v-text="addGuestTips"></text>
  5. </view>
  6. <view class="input-area">
  7. <view class="info-item">
  8. <view class="item-label">
  9. <text>姓名</text>
  10. </view>
  11. <view class="item-content">
  12. <input v-model="name" type="text" placeholder="请输入姓名" />
  13. </view>
  14. </view>
  15. <view class="info-item">
  16. <view class="item-label">
  17. <text>身份证</text>
  18. </view>
  19. <view class="item-content">
  20. <input v-model="idNumber" type="text" placeholder="请输入身份证" />
  21. </view>
  22. </view>
  23. <view class="info-item">
  24. <view class="item-label">
  25. <text>手机号</text>
  26. </view>
  27. <view class="item-content">
  28. <input v-model="phone" type="text" placeholder="请输入手机号" />
  29. </view>
  30. </view>
  31. </view>
  32. <view class="confirm-button">
  33. <button @click="gotoVerification">立即认证</button>
  34. </view>
  35. </view>
  36. <view class="upload-card" v-else-if="step===STEP.UPLOAD">
  37. <view class="tips" v-if="!userInfo.name">
  38. <text v-text="uploadCardTips"></text>
  39. </view>
  40. <view class="upload-container" @click="uploadCard('portrait')">
  41. <image :src="picPath.portrait"></image>
  42. </view>
  43. <view class="upload-container" @click="uploadCard('emblem')">
  44. <image :src="picPath.emblem"></image>
  45. </view>
  46. <view class="confirm-button">
  47. <button @click="uploadComplete">完成上传</button>
  48. </view>
  49. <canvas id="convertCanvas" canvas-id="convertCanvas"
  50. style="width: 1px; height: 1px; position: absolute; left: -9999px;"></canvas>
  51. </view>
  52. <view class="verification" v-else-if="step===STEP.VERIFICATION">
  53. <view class="face-area">
  54. <view class="tips">
  55. <view class="tips-title">
  56. <text>身份核验</text>
  57. </view>
  58. <view class="tips-content">
  59. <text>请根据页面提示进行操作</text>
  60. </view>
  61. </view>
  62. <view class="separator"></view>
  63. <view class="camera-container">
  64. <camera v-if="showCamera" class="camera" device-position="front" flash="off" resolution="low"></camera>
  65. </view>
  66. <view class="start-info" v-if="showCamera">
  67. <text>{{verificationTips}}</text>
  68. </view>
  69. </view>
  70. <view class="precautions">
  71. <view class="tips">
  72. <view class="tips-title">
  73. <text>注意事项</text>
  74. </view>
  75. <view class="tips-content">
  76. <text>请遵守需注意事项</text>
  77. </view>
  78. </view>
  79. <view class="separator"></view>
  80. <view class="precautions-content">
  81. <text>确认{{name}}本人操作;保持正脸在取景框中根据屏幕指示完成</text>
  82. </view>
  83. <view class="precautions-img">
  84. <view class="precautions-img-item" v-for="img in imgList" :key="img.src">
  85. <view class="image-container">
  86. <image :src="img.src" mode="aspectFit"></image>
  87. </view>
  88. <view class="precautions-img-desc">
  89. <text>{{img.desc}}</text>
  90. </view>
  91. </view>
  92. </view>
  93. </view>
  94. <view class="confirm-button">
  95. <button @click="agreeAndStart">同意并开始验证</button>
  96. </view>
  97. </view>
  98. </template>
  99. <script setup>
  100. import {
  101. onLoad
  102. } from '@dcloudio/uni-app'
  103. import {
  104. computed,
  105. getCurrentInstance,
  106. reactive,
  107. ref,
  108. watch
  109. } from 'vue'
  110. import {
  111. useUserStore
  112. } from '../../store/userStore'
  113. import {
  114. useCheckinInfoStore
  115. } from '../../store/checkinInfoStore'
  116. import {
  117. useHotelStore
  118. } from '../../store/hotelStore'
  119. import {
  120. useIdCardInfoStore
  121. } from '../../store/idCardInfoStore'
  122. import {
  123. useOrderStore
  124. } from '../../store/orderStore'
  125. const STEP = {
  126. FILL: 'FILL',
  127. UPLOAD: 'UPLOAD',
  128. VERIFICATION: 'VERIFICATION'
  129. }
  130. let step = ref(STEP.FILL)
  131. let userStore = useUserStore()
  132. let userInfo = userStore.userInfo
  133. let hotelStore = useHotelStore()
  134. let hotel = hotelStore.hotel
  135. let checkinInfoStore = useCheckinInfoStore()
  136. let checkinInfo = checkinInfoStore.checkinInfo
  137. let idCardInfoStore = useIdCardInfoStore()
  138. let idCardInfo = idCardInfoStore.idCardInfo
  139. let currentIdCardInfo = {}
  140. let VKSession
  141. /**
  142. * addGuest代码区域
  143. */
  144. const addGuestTips = '根据《治安管理处罚法》要求所有入住宾客和访客必须一人一证实名登记'
  145. let name = ref('')
  146. let phone = ref('')
  147. let idNumber = ref('')
  148. let isMainCustomer = false
  149. async function gotoVerification() {
  150. //检查格式是否正确
  151. let nameReg = /\d/
  152. if (!name.value || nameReg.test(name.value)) {
  153. uni.showToast({
  154. icon: 'none',
  155. title: '姓名格式错误!'
  156. })
  157. return
  158. }
  159. let idNumberReg = /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/;
  160. if (!idNumberReg.test(idNumber.value)) {
  161. uni.showToast({
  162. icon: 'none',
  163. title: '身份证号格式错误!'
  164. })
  165. return
  166. }
  167. let phoneReg = /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/;
  168. if (!phoneReg.test(phone.value)) {
  169. uni.showToast({
  170. icon: 'none',
  171. title: '手机号格式错误!'
  172. })
  173. return
  174. }
  175. //判断此顾客信息是否已添加
  176. for (const info of checkinInfo) {
  177. if (info.idNumber === idNumber.value) {
  178. uni.showToast({
  179. icon: 'none',
  180. title: '已添加此顾客,请检查填写信息!'
  181. })
  182. return
  183. }
  184. }
  185. //检查该顾客是否在当前酒店入住
  186. let checkinQueryRes = await uni.request({
  187. url: '/user/isCheckin',
  188. method: 'POST',
  189. data: {
  190. idNumber: idNumber.value,
  191. hotelId: hotel.hotelId
  192. }
  193. })
  194. if (checkinQueryRes.data.data) {
  195. uni.showToast({
  196. icon: 'none',
  197. title: '该顾客已入住!'
  198. })
  199. return
  200. }
  201. //判断本地缓存是否有顾客身份证信息
  202. for (const info of idCardInfo) {
  203. if (info.name === name.value && info.idNumber === idNumber.value) {
  204. //本地有,更新当前使用的身份证信息,跳转至人证比对
  205. currentIdCardInfo = info
  206. step.value = STEP.VERIFICATION
  207. return
  208. }
  209. }
  210. //本地没有顾客身份证信息,先到后台查询
  211. let idCardInfoQueryRes = await uni.request({
  212. url: '/user/idCardInfo',
  213. method: 'POST',
  214. data: {
  215. idNumber: idNumber.value,
  216. name: name.value
  217. }
  218. })
  219. //查询到,写入pinia,跳转至人证比对
  220. if (idCardInfoQueryRes.data.code === 200 && idCardInfoQueryRes.data.data) {
  221. let info = idCardInfoQueryRes.data.data
  222. info.birth = info.birthday
  223. idCardInfoStore.addIdCardInfo(info)
  224. currentIdCardInfo = info
  225. step.value = STEP.VERIFICATION
  226. return
  227. }
  228. //没有查询到,初始化用于识别card的VKSession,跳转至上传页面
  229. step.value = STEP.UPLOAD
  230. }
  231. /**
  232. * uploadCard代码区域
  233. */
  234. const that = getCurrentInstance()
  235. const uploadCardTips = '根据《治安管理处罚法》要求所有入住宾客和访客必须一人一证实名登记'
  236. //照片最大1.5M
  237. const maxPicSize = 1024 * 1024 * 1.5
  238. let picPath = reactive({
  239. portrait: '../../static/idcard_upload_front.png',
  240. emblem: '../../static/idcard_upload_back.png'
  241. })
  242. let detectInfo
  243. let infoList = ['address', 'nation', 'sex', 'birth', 'issuingAuthority', 'issuingDate', 'expiryDate']
  244. function infoComplete() {
  245. for (let info of infoList) {
  246. if (!currentIdCardInfo[info]) {
  247. return false
  248. }
  249. }
  250. return true
  251. }
  252. function initIdCardVKSession() {
  253. VKSession = wx.createVKSession({
  254. track: {
  255. IDCard: {
  256. mode: 2 // 照片模式
  257. }
  258. },
  259. version: 'v1',
  260. gl: that.gl
  261. })
  262. VKSession.start(err => {
  263. VKSession.on('updateAnchors', anchors => {
  264. // 处理返回的身份证信息
  265. if (anchors && anchors[0]) {
  266. // 存在数组,证明存在身份证信息
  267. const anchor = anchors[0]
  268. detectInfo = {
  269. detected: true,
  270. detectSuccess: true,
  271. isComplete: anchor.isComplete,
  272. label: anchor.label,
  273. orientation: anchor.orientation,
  274. box: anchor.box,
  275. }
  276. // 裁剪信息
  277. const affineImgWidth = anchor.affineImgWidth
  278. const affineImgHeight = anchor.affineImgHeight
  279. const affineMat = anchor.affineMat
  280. // 存在裁剪信息,进行身份证裁剪处理
  281. if (affineImgWidth && affineImgHeight && affineMat) {
  282. getCropIDcard(affineImgWidth, affineImgHeight, affineMat)
  283. }
  284. }
  285. })
  286. VKSession.on('removeAnchors', anchors => {
  287. // 图片没有识别到身份证
  288. detectInfo.detected = true
  289. detectInfo.detectSuccess = false
  290. })
  291. })
  292. }
  293. async function uploadCard(direction) {
  294. let chooseImageRes = await uni.chooseImage({
  295. count: 1
  296. })
  297. //检查图片大小
  298. let fileSize = chooseImageRes.tempFiles[0].size
  299. if (fileSize > maxPicSize) {
  300. uni.showModal({
  301. title: '图片超出允许大小(1.5M)',
  302. showCancel: false
  303. })
  304. return
  305. }
  306. let path = chooseImageRes.tempFilePaths[0]
  307. //检查图片格式,仅支持jpg或者png格式
  308. let imageType = path.split('.')[1]
  309. if (imageType !== 'jpg') {
  310. if (imageType !== 'png') {
  311. uni.showModal({
  312. showCancel: false,
  313. title: '仅支持jpg或者png格式的图片'
  314. })
  315. return
  316. }
  317. //png转jpg
  318. console.log('png to jpg')
  319. const canvasId = 'convertCanvas'
  320. const ctx = uni.createCanvasContext(canvasId, that)
  321. let imageInfo = await uni.getImageInfo({
  322. src: path
  323. })
  324. console.log('getImageInfo', imageInfo)
  325. // 绘制图片到canvas
  326. await new Promise((resolve, reject) => {
  327. ctx.drawImage(path, 0, 0, imageInfo.width, imageInfo.height)
  328. ctx.draw(false,
  329. async () => {
  330. // 将canvas内容转换为jpg格式的临时文件路径
  331. let res = await uni.canvasToTempFilePath({
  332. canvasId: canvasId,
  333. fileType: 'jpg',
  334. quality: 1, // 图片质量,可根据需要调整
  335. }, that)
  336. path = res.tempFilePath
  337. console.log('图片转换成功,临时路径为:', res.tempFilePath)
  338. resolve()
  339. },
  340. err => {
  341. reject(err)
  342. })
  343. })
  344. console.log('jpg path', path)
  345. }
  346. await setImageUploadInfo(path)
  347. await detectIdCard()
  348. //图片中不包含身份证
  349. if (!detectInfo.detectSuccess) {
  350. uni.showModal({
  351. title: '图片中不包含身份证,请重新上传',
  352. showCancel: false
  353. })
  354. return
  355. }
  356. if (!detectInfo.isComplete) {
  357. uni.showModal({
  358. title: '身份证照片不完整,请重新上传',
  359. showCancel: false
  360. })
  361. return
  362. }
  363. if ((direction === 'portrait' && detectInfo.label !== 0) ||
  364. (direction === 'emblem' && detectInfo.label !== 1)) {
  365. uni.showModal({
  366. title: '身份证面错误,请重新上传',
  367. showCancel: false
  368. })
  369. return
  370. }
  371. uni.showLoading({
  372. title: '上传中',
  373. mask: true
  374. })
  375. try {
  376. let res = await uni.request({
  377. url: '/user/idCardOcr',
  378. method: 'POST',
  379. data: {
  380. image: imageUploadInfo.imgUrl.split(',')[1],
  381. ocrType: detectInfo.label,
  382. imageType: 'BASE64'
  383. }
  384. })
  385. if (!res.data.success || res.data.data.imageStatus !== 'normal') {
  386. uni.showModal({
  387. content: '获取信息失败,请检查图片并重新上传',
  388. showCancel: false,
  389. })
  390. return
  391. }
  392. if (direction === 'portrait') {
  393. //第一次使用时没有在addGuest页面输入信息,用身份证识别到的信息
  394. if (isMainCustomer && !name.value && !idNumber.value) {
  395. //检查该顾客是否在当前酒店入住
  396. let checkinQueryRes = await uni.request({
  397. url: '/user/isCheckin',
  398. method: 'POST',
  399. data: {
  400. idNumber: res.data.data.cardNum,
  401. hotelId: hotel.hotelId
  402. }
  403. })
  404. if (checkinQueryRes.data.data) {
  405. uni.showToast({
  406. icon: 'none',
  407. title: '该顾客已入住!'
  408. })
  409. return
  410. }
  411. } else {
  412. if (res.data.data.cardNum !== idNumber.value ||
  413. res.data.data.name !== name.value) {
  414. uni.showModal({
  415. content: '填写信息与身份证不一致',
  416. showCancel: false,
  417. complete: () => {
  418. step.value = STEP.FILL
  419. },
  420. })
  421. return
  422. }
  423. currentIdCardInfo = {
  424. ...currentIdCardInfo,
  425. name: res.data.data.name,
  426. sex: res.data.data.sex,
  427. nation: res.data.data.nation,
  428. birth: res.data.data.birth,
  429. address: res.data.data.address,
  430. idNumber: res.data.data.cardNum
  431. }
  432. }
  433. picPath.portrait = imageUploadInfo.imgUrl
  434. } else {
  435. currentIdCardInfo = {
  436. ...currentIdCardInfo,
  437. issuingAuthority: res.data.data.issuingAuthority,
  438. issuingDate: res.data.data.issuingDate,
  439. expiryDate: res.data.data.expiryDate
  440. }
  441. picPath.emblem = imageUploadInfo.imgUrl
  442. }
  443. } catch (err) {
  444. console.log("身份验证出错", err);
  445. uni.showModal({
  446. content: '获取信息失败,请检查图片并重新上传',
  447. showCancel: false,
  448. })
  449. } finally {
  450. uni.hideLoading()
  451. }
  452. }
  453. let imageUploadInfo
  454. async function setImageUploadInfo(url) {
  455. const fixWidth = 300
  456. let imageInfo = await uni.getImageInfo({
  457. src: url
  458. })
  459. console.log('imageInfo', imageInfo)
  460. let height = imageInfo.height
  461. let width = imageInfo.width
  462. imageUploadInfo = {
  463. imgUrl: url,
  464. imgWidth: fixWidth,
  465. imgHeight: (fixWidth / width) * height,
  466. imgOriginWidth: width,
  467. imgOriginHeight: height
  468. }
  469. }
  470. async function detectIdCard() {
  471. const canvas = wx.createOffscreenCanvas({
  472. type: '2d',
  473. width: imageUploadInfo.imgOriginWidth,
  474. height: imageUploadInfo.imgOriginHeight
  475. })
  476. const context = canvas.getContext('2d')
  477. const img = canvas.createImage()
  478. that.img = img
  479. await new Promise(resolve => {
  480. img.onload = resolve
  481. img.src = imageUploadInfo.imgUrl
  482. })
  483. context.clearRect(0, 0, imageUploadInfo.imgOriginWidth, imageUploadInfo.imgOriginHeight)
  484. context.drawImage(img, 0, 0, imageUploadInfo.imgOriginWidth, imageUploadInfo.imgOriginHeight)
  485. // 使用中的 image ArrayBuffer
  486. that.imgData = context.getImageData(0, 0, imageUploadInfo.imgOriginWidth, imageUploadInfo
  487. .imgOriginHeight)
  488. VKSession.detectIDCard({
  489. // 识别身份证图片的信息
  490. frameBuffer: that.imgData.data.buffer,
  491. width: imageUploadInfo.imgOriginWidth,
  492. height: imageUploadInfo.imgOriginHeight,
  493. // 是否获取裁剪图片信息
  494. getAffineImg: true,
  495. })
  496. }
  497. function getCropIDcard(affineImgWidth, affineImgHeight, affineMat) {
  498. const canvas = wx.createOffscreenCanvas({
  499. type: '2d',
  500. width: affineImgWidth,
  501. height: affineImgHeight,
  502. })
  503. const context = canvas.getContext('2d')
  504. context.clearRect(0, 0, affineImgWidth, affineImgHeight);
  505. /*
  506. * affineMat 3x3仿射变换矩阵,行主序
  507. * [0 1 2
  508. * 3 4 5
  509. * 6 7 8]
  510. */
  511. /*
  512. * canvas 2d setTransform
  513. * setTransform(a, b, c, d, e, f)
  514. * [a c e
  515. * b d f
  516. * 0 0 1]
  517. */
  518. context.setTransform(
  519. Number(affineMat[0]), Number(affineMat[3]), Number(affineMat[1]),
  520. Number(affineMat[4]), Number(affineMat[2]), Number(affineMat[5])
  521. );
  522. context.drawImage(that.img, 0, 0, imageUploadInfo.imgOriginWidth, imageUploadInfo.imgOriginHeight)
  523. const imgUrl = canvas.toDataURL()
  524. imageUploadInfo.imgUrl = imgUrl
  525. }
  526. function uploadComplete() {
  527. if (!infoComplete()) {
  528. uni.showToast({
  529. icon: 'none',
  530. title: '信息不完整,请检查身份证'
  531. })
  532. return
  533. }
  534. //保存到本地缓存
  535. idCardInfoStore.addIdCardInfo({
  536. ...currentIdCardInfo
  537. })
  538. //上传至服务器
  539. uni.request({
  540. url: '/user/idCardInfo/add',
  541. method: 'POST',
  542. data: {
  543. ...currentIdCardInfo,
  544. birthday: currentIdCardInfo.birth,
  545. idCardFrontBase64: picPath.portrait.split(',')[1],
  546. idCardBackBase64: picPath.emblem.split(',')[1]
  547. }
  548. })
  549. step.value = STEP.VERIFICATION
  550. }
  551. /**
  552. * verification
  553. */
  554. let cameraAuth = ref(false)
  555. let startVerification = ref(false)
  556. let verificationTips = ref('开始身份核验')
  557. let showCamera = computed(() => {
  558. return cameraAuth.value && startVerification.value
  559. })
  560. const imgList = [{
  561. src: '/static/check-face-phone.png',
  562. desc: '正对手机'
  563. },
  564. {
  565. src: '/static/check-face-light.png',
  566. desc: '光线充足'
  567. },
  568. {
  569. src: '/static/check-face-face.png',
  570. desc: '脸部无遮挡'
  571. }
  572. ]
  573. function agreeAndStart() {
  574. if (!cameraAuth.value) {
  575. getCameraAuth()
  576. return
  577. } else {
  578. VKSession.start(err => {
  579. if (err) {
  580. uni.showModal({
  581. title: '网络错误,请退出后重试',
  582. showCancel: false,
  583. })
  584. }
  585. })
  586. }
  587. startVerification.value = true
  588. }
  589. function getCameraAuth() {
  590. uni.getSetting({
  591. success: (res) => {
  592. if (res.authSetting['scope.camera']) {
  593. cameraAuth.value = true
  594. } else {
  595. uni.authorize({
  596. scope: 'scope.camera',
  597. success: (res) => {
  598. cameraAuth.value = true
  599. },
  600. fail: (res) => {
  601. uni.showModal({
  602. title: '尚未进行授权,功能将无法使用',
  603. showCancel: false
  604. })
  605. }
  606. })
  607. }
  608. }
  609. })
  610. }
  611. let cameraEngine
  612. let listener
  613. function initCameraEngine() {
  614. cameraEngine = wx.createCameraContext()
  615. let count = 0
  616. listener = cameraEngine.onCameraFrame(frame => {
  617. count++
  618. //每十帧分析一次
  619. if (count === 10) {
  620. detectFace(frame)
  621. count = 0
  622. }
  623. })
  624. listener.start()
  625. }
  626. function initFaceVKSession() {
  627. //销毁上传身份证时创建的session
  628. if (VKSession) {
  629. VKSession.destroy()
  630. }
  631. VKSession = wx.createVKSession({
  632. version: 'v1',
  633. track: {
  634. plane: {
  635. mode: 1
  636. },
  637. face: {
  638. mode: 2
  639. }
  640. }
  641. })
  642. VKSession.on('updateAnchors', (anchors) => {
  643. // console.log('anchors', anchors)
  644. // console.log('anchors.length', anchors.length)
  645. anchors.forEach((anchor) => {
  646. // console.log('anchor.points', anchor.points)
  647. // console.log('anchor.origin', anchor.origin)
  648. // console.log('anchor.size', anchor.size)
  649. // console.log('anchor.angle', anchor.angle)
  650. // console.log('anchor', anchor.confidence)
  651. if (anchors.length > 1) {
  652. verificationTips.value = '请保证只有一个人'
  653. } else {
  654. const {
  655. pitch,
  656. roll,
  657. yaw
  658. } = anchor.angle
  659. const standard = 0.3
  660. if (
  661. Math.abs(pitch) >= standard ||
  662. Math.abs(roll) >= standard ||
  663. Math.abs(yaw) >= standard
  664. ) {
  665. verificationTips.value = '请平视摄像头'
  666. } else if (
  667. anchor.origin.x < 0.15 ||
  668. anchor.origin.x > 0.3 ||
  669. anchor.origin.y < 0.2 ||
  670. anchor.origin.y > 0.45
  671. ) {
  672. verificationTips.value = '请将人脸对准中心位置'
  673. } else if (
  674. anchor.confidence[0] <= 0.8 ||
  675. anchor.confidence[1] <= 0.8 ||
  676. anchor.confidence[2] <= 0.8 ||
  677. anchor.confidence[3] <= 0.8 ||
  678. anchor.confidence[4] <= 0.8
  679. ) {
  680. verificationTips.value = '请勿遮挡五官'
  681. } else {
  682. listener.stop()
  683. verificationTips.value = '即将拍照,请保持!'
  684. setTimeout(function() {
  685. takePhoto()
  686. }, 1000)
  687. }
  688. }
  689. })
  690. })
  691. }
  692. function takePhoto() {
  693. verificationTips.value = ''
  694. uni.showLoading({
  695. title: '正在身份核验,请稍后',
  696. })
  697. cameraEngine.takePhoto({
  698. quality: 'normal',
  699. success: async ({
  700. tempImagePath
  701. }) => {
  702. let base64 = wx
  703. .getFileSystemManager()
  704. .readFileSync(tempImagePath, 'base64')
  705. startVerification.value = false
  706. await checkFace(base64)
  707. },
  708. })
  709. }
  710. async function detectFace(frame) {
  711. VKSession.detectFace({
  712. frameBuffer: frame.data,
  713. width: frame.width,
  714. height: frame.height,
  715. scoreThreshold: 0.8,
  716. sourceType: 0,
  717. modelMode: 1,
  718. })
  719. }
  720. async function checkFace(base64) {
  721. uni.showLoading({
  722. title: '正在身份核验,请稍后',
  723. mask: true
  724. })
  725. try {
  726. let res = await uni.request({
  727. url: '/user/faceCheckAll',
  728. method: 'POST',
  729. data: {
  730. hotelId: hotel.hotelId,
  731. name: name.value,
  732. idNumber: idNumber.value,
  733. imageBase64: base64
  734. }
  735. })
  736. if (res.data.success) {
  737. //如果是主入住人,且当前账号姓名和身份证号为空,则更新账号信息为主入住人信息
  738. if (isMainCustomer) {
  739. if (!userInfo.name || !userInfo.idNumber) {
  740. //修改本地缓存中的信息
  741. userInfo.name = name.value
  742. userInfo.idNumber = idNumber.value
  743. userStore.updateUserInfo(userInfo)
  744. //修改服务器中的信息
  745. uni.request({
  746. url: `/user`,
  747. method: 'PUT',
  748. data: {
  749. name: name.value,
  750. idNumber: idNumber.value
  751. }
  752. })
  753. }
  754. //更新订单中预定人信息为主入住人信息
  755. let orderStore = useOrderStore()
  756. let orderInfo = orderStore.orderInfo
  757. orderStore.updateOrderInfo({
  758. ...orderInfo,
  759. name: name.value,
  760. phone: phone.value
  761. })
  762. }
  763. //更新缓存中的入住信息
  764. checkinInfoStore.addCheckinInfo({
  765. phone: phone.value,
  766. faceData: base64,
  767. ...currentIdCardInfo
  768. })
  769. uni.showToast({
  770. icon: 'none',
  771. title: '身份核验通过!',
  772. success: () => {
  773. setTimeout(() => {
  774. uni.reLaunch({
  775. url: '/subpkg_checkin/confirmOrder/confirmOrder'
  776. })
  777. }, 1500)
  778. }
  779. })
  780. } else {
  781. uni.showModal({
  782. title: '身份核验失败,请重试',
  783. showCancel: false
  784. })
  785. }
  786. } catch (err) {
  787. console.log('err', err)
  788. uni.showModal({
  789. title: '身份核验失败,请重试',
  790. showCancel: false
  791. })
  792. uni.hideLoading()
  793. } finally {
  794. //finally中调用uni.hideLoading()会关闭上面的uni.showToast导致toast一闪而过
  795. //调用uni.showToast时会覆盖掉uni.hideLoading,因此只在catch中调用uni.hideLoading即可
  796. // uni.hideLoading()
  797. }
  798. }
  799. watch(step, (newValue, oldValue) => {
  800. if (newValue === STEP.UPLOAD) {
  801. console.log('initIdCardVKSession')
  802. initIdCardVKSession()
  803. } else if (newValue === STEP.VERIFICATION) {
  804. console.log('initCameraEngine')
  805. initCameraEngine()
  806. console.log('initFaceVKSession')
  807. initFaceVKSession()
  808. }
  809. })
  810. onLoad(() => {
  811. /**
  812. * addGuest
  813. */
  814. let pages = getCurrentPages()
  815. let beforePage = pages[pages.length - 2]
  816. if (beforePage.route === 'subpkg_checkin/selectRoom/selectRoom') {
  817. name.value = userInfo.name
  818. phone.value = userInfo.phone
  819. idNumber.value = userInfo.idNumber
  820. isMainCustomer = true
  821. }
  822. //是主入住人且姓名和身份证号为空,判断为新用户,直接跳转至上传身份证照片页面
  823. if (isMainCustomer && (!name.value || !idNumber.value)) {
  824. step.value = STEP.UPLOAD
  825. }
  826. /**
  827. * uploadCard
  828. */
  829. let isVKSupport = wx.isVKSupport('v1')
  830. if (!isVKSupport) {
  831. uni.showModal({
  832. title: '微信版本过低,请升级至最新版本',
  833. showCancel: false,
  834. success() {
  835. uni.navigateBack()
  836. }
  837. })
  838. return
  839. }
  840. /**
  841. * verification
  842. */
  843. //获取相机权限
  844. getCameraAuth()
  845. })
  846. </script>
  847. <style lang="scss">
  848. .fill-form {
  849. .tips {
  850. margin: 30rpx;
  851. color: #333333;
  852. font-size: 28rpx;
  853. }
  854. .input-area {
  855. background-color: #ffffff;
  856. .info-item {
  857. height: 100rpx;
  858. display: flex;
  859. align-items: center;
  860. margin: 0 40rpx;
  861. border-bottom: #E5E5E5 2rpx solid;
  862. .item-label {
  863. width: 20vw;
  864. }
  865. .item-content {
  866. input {
  867. padding: 10rpx;
  868. width: 50vw;
  869. }
  870. }
  871. }
  872. }
  873. .confirm-button {
  874. position: fixed;
  875. bottom: 90rpx;
  876. left: 30rpx;
  877. right: 30rpx;
  878. button {
  879. background-color: #a09cc4;
  880. color: #ffffff;
  881. }
  882. }
  883. }
  884. .upload-card {
  885. .tips {
  886. margin: 30rpx;
  887. color: #333333;
  888. font-size: 28rpx;
  889. }
  890. .upload-container {
  891. width: 500rpx;
  892. margin: 30rpx auto;
  893. image {
  894. width: 500rpx;
  895. height: 350rpx;
  896. margin: 0 auto;
  897. }
  898. }
  899. .confirm-button {
  900. position: fixed;
  901. bottom: 90rpx;
  902. left: 30rpx;
  903. right: 30rpx;
  904. button {
  905. background-color: #a09cc4;
  906. color: #ffffff;
  907. }
  908. }
  909. }
  910. .verification {
  911. .tips {
  912. display: flex;
  913. margin: 20rpx;
  914. align-items: flex-end;
  915. .tips-title {
  916. color: #333333;
  917. font-weight: bold;
  918. font-size: 34rpx;
  919. margin-right: 20rpx;
  920. }
  921. .tips-content {
  922. color: #CBCBCB;
  923. font-size: 28rpx;
  924. }
  925. }
  926. .separator {
  927. margin-left: 40rpx;
  928. margin-bottom: 20rpx;
  929. width: 30rpx;
  930. border-bottom: solid 6rpx #9e97c3;
  931. }
  932. .face-area {
  933. display: flex;
  934. flex-direction: column;
  935. background-color: #FFFFFF;
  936. border-radius: 50rpx;
  937. overflow: hidden;
  938. .camera-container {
  939. width: 80%;
  940. height: 80vw;
  941. margin: 0 auto;
  942. margin-bottom: 30rpx;
  943. camera {
  944. width: 100%;
  945. height: 80vw;
  946. margin: 0 auto;
  947. margin-bottom: 30rpx;
  948. border: 2rpx solid black;
  949. border-radius: 50%;
  950. // box-sizing: border-box;
  951. }
  952. }
  953. .start-info {
  954. margin: 0 auto
  955. }
  956. }
  957. .precautions {
  958. .precautions-content {
  959. color: #666666;
  960. font-size: 24rpx;
  961. margin: 20rpx;
  962. }
  963. .precautions-img {
  964. display: flex;
  965. justify-content: space-around;
  966. .precautions-img-item {
  967. display: flex;
  968. flex-direction: column;
  969. align-items: center;
  970. width: 20%;
  971. .image-container {
  972. width: 120rpx;
  973. height: 120rpx;
  974. border-radius: 20rpx;
  975. background-color: #dcdce9;
  976. display: flex;
  977. justify-content: center;
  978. align-items: center;
  979. image {
  980. width: 80rpx;
  981. height: 80rpx;
  982. }
  983. }
  984. image {
  985. width: 80rpx;
  986. height: 80rpx;
  987. }
  988. }
  989. }
  990. }
  991. .confirm-button {
  992. position: fixed;
  993. bottom: 90rpx;
  994. left: 30rpx;
  995. right: 30rpx;
  996. button {
  997. background-color: #a09cc4;
  998. color: #ffffff;
  999. }
  1000. }
  1001. }
  1002. </style>