index.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  1. <template>
  2. <view class="container">
  3. <!-- <up-notify type="success" ref="uNotifyRef" duration="0" :show="true" message="Hi uview-plus"
  4. :safeAreaInsetTop="true">审核中</up-notify> -->
  5. <view class="upload-section">
  6. <!-- 状态展示 -->
  7. <view v-if="verifyInfo" class="verify-status">
  8. <view>认证状态:{{ statusText }}</view>
  9. </view>
  10. <!-- 身份证正面上传 -->
  11. <view class="upload-box">
  12. <view class="upload-title">上传身份证</view>
  13. <view class="upload-box-list">
  14. <up-upload width="335rpx" height="208rpx" :fileList="frontImageList" @afterRead="afterReadFront"
  15. @delete="deletePicFront" :deletable="deletable" :previewImage="true" imageMode="aspectFill" name="front"
  16. :maxCount="1">
  17. <template #trigger>
  18. <view class="upload-block">
  19. <image mode="" v-if="frontImageList.length === 0" src="/static/img/IDFront.png" class="IDFront" />
  20. </view>
  21. </template>
  22. </up-upload>
  23. <up-upload width="335rpx" height="208rpx" :fileList="backImageList" @afterRead="afterReadBack"
  24. @delete="deletePicBack" :previewImage="true" :deletable="deletable" imageMode="aspectFill" name="back"
  25. :maxCount="1">
  26. <template #trigger>
  27. <view class="upload-block">
  28. <image mode="aspectFill" v-if="backImageList.length === 0" src="/static/img/IDReverse.png"
  29. class="IDBack" />
  30. </view>
  31. </template>
  32. </up-upload>
  33. </view>
  34. </view>
  35. <!-- 身份信息输入 -->
  36. <view class="info-section">
  37. <view class="info-title">身份信息</view>
  38. <view class="info-form">
  39. <view class="form-item">
  40. <up-input v-model="realName" placeholder="请输入真实姓名" border="surround" clearable :customStyle="inputStyle"
  41. :disabled="isReadonly" inputAlign="right">
  42. <template #prefix>
  43. <view class="">姓名</view>
  44. </template>
  45. </up-input>
  46. </view>
  47. <view class="form-item">
  48. <up-input v-model="idCardNumber" placeholder="请输入身份证号码" border="surround" clearable
  49. :customStyle="inputStyle" type="idcard" @change="onIdCardChange" maxlength="18" :disabled="isReadonly"
  50. inputAlign="right">
  51. <template #prefix>
  52. <view class="">身份证号</view>
  53. </template>
  54. </up-input>
  55. </view>
  56. </view>
  57. </view>
  58. <view class="info-tips">
  59. <image src="/static/img/tips@2x.png" mode=""></image>
  60. <view class="">
  61. <view class="">上传说明:</view>
  62. <view class="">1.请确保身份证信息清晰可见</view>
  63. <view class="">2.请上传真实的身份证照片</view>
  64. <view class="">3.请确保光线充足,避免反光</view>
  65. <view class="">4.请确保姓名和身份证号与身份证照片一致</view>
  66. </view>
  67. </view>
  68. </view>
  69. <view class="kong"></view>
  70. <view class="footer">
  71. <up-button class="btn" @click="submitDetect" :disabled="!canSubmit || isProcessing || isReadonly">
  72. {{
  73. isProcessing ? "认证中..." : isRejected ? "重新提交认证" : "提交认证"
  74. }}
  75. </up-button>
  76. </view>
  77. </view>
  78. </template>
  79. <script setup>
  80. import {
  81. ref,
  82. computed
  83. } from "vue";
  84. import {
  85. onLoad
  86. } from "@dcloudio/uni-app";
  87. import {
  88. init,
  89. query
  90. } from "@/api/faceVerify.js";
  91. import {
  92. useToast
  93. } from "@/hooks/useToast";
  94. import {
  95. useImageUpload
  96. } from "@/hooks/useImageUpload";
  97. import {
  98. faceVerify,
  99. getFaceVerify
  100. } from "@/api/user";
  101. import {
  102. useAppStore
  103. } from "@/stores/app";
  104. const {
  105. Toast
  106. } = useToast();
  107. const appStore = useAppStore();
  108. const idCardPicUrl = ref("");
  109. const isProcessing = ref(false);
  110. const realName = ref("");
  111. const idCardNumber = ref("");
  112. const emit = defineEmits(["fetchUserInfo"]);
  113. const verifyInfo = ref(null);
  114. const deletable = ref(true);
  115. // #ifdef APP
  116. const aliyunVerify = uni.requireNativePlugin("AP-FaceDetectModule");
  117. // #endif
  118. const certifyId = ref("");
  119. const metaInfo = ref("");
  120. // 正面上传
  121. const {
  122. imageList: frontImageList,
  123. afterRead: afterReadFront,
  124. deletePic: deletePicFront,
  125. } = useImageUpload({
  126. pid: 7,
  127. model: "user",
  128. });
  129. console.log("frontImageList", frontImageList.value);
  130. // 反面上传
  131. const {
  132. imageList: backImageList,
  133. afterRead: afterReadBack,
  134. deletePic: deletePicBack,
  135. } = useImageUpload({
  136. pid: 5,
  137. model: "user",
  138. });
  139. // 输入框样式
  140. const inputStyle = {
  141. backgroundColor: "#F9F7F0",
  142. borderRadius: "16rpx",
  143. padding: "28rpx 16rpx",
  144. fontSize: "28rpx",
  145. border: 'none'
  146. };
  147. const isReadonly = computed(() => {
  148. return (
  149. verifyInfo.value &&
  150. (verifyInfo.value.status === 0 || verifyInfo.value.status === 1)
  151. );
  152. });
  153. const showUploadImg = computed(() => {
  154. // 如果是被拒绝状态,不显示已上传的图片,允许重新上传
  155. if (isRejected.value) {
  156. return false;
  157. }
  158. return verifyInfo.value || frontImageList.value[0]?.info?.url;
  159. });
  160. const isRejected = computed(() => {
  161. return verifyInfo.value && verifyInfo.value.status === 2;
  162. });
  163. const statusText = computed(() => {
  164. if (!verifyInfo.value) return "";
  165. switch (verifyInfo.value.status) {
  166. case 0:
  167. return "审核中";
  168. case 1:
  169. return "已通过";
  170. case 2:
  171. return verifyInfo.value?.approveMsg || "认证被拒绝,请重新提交";
  172. default:
  173. return "";
  174. }
  175. });
  176. // 计算是否可以提交(基础校验)
  177. const canSubmit = computed(() => {
  178. return (
  179. frontImageList.value.length > 0 &&
  180. backImageList.value.length > 0 &&
  181. realName.value.trim() &&
  182. idCardNumber.value.trim()
  183. );
  184. });
  185. onLoad(() => {
  186. fetchFaceVerify();
  187. });
  188. // 获取实名状态
  189. async function fetchFaceVerify() {
  190. try {
  191. const {
  192. data
  193. } = await getFaceVerify({
  194. uid: appStore.uid
  195. });
  196. if (data?.list.length > 0) {
  197. verifyInfo.value = data.list[0];
  198. realName.value = verifyInfo.value.realName || "";
  199. idCardNumber.value = verifyInfo.value.cardId || "";
  200. frontImageList.value[0] = {
  201. url: verifyInfo.value.idCardFront,
  202. };
  203. backImageList.value[0] = {
  204. url: verifyInfo.value.idCardBack,
  205. };
  206. if (verifyInfo.value.status === 0 || verifyInfo.value.status === 1) {
  207. deletable.value = false;
  208. }
  209. // 如果是被拒绝状态,清空上传列表,允许重新上传
  210. // if (verifyInfo.value.status === 2) {
  211. // frontImageList.value = [];
  212. // backImageList.value = [];
  213. // }
  214. } else {
  215. verifyInfo.value = null;
  216. realName.value = "";
  217. idCardNumber.value = "";
  218. frontImageList.value = [];
  219. backImageList.value = [];
  220. }
  221. } catch (error) {
  222. console.error("getFaceVerify", error);
  223. }
  224. }
  225. function submitDetect() {
  226. if (isReadonly.value) {
  227. Toast({
  228. title: "当前状态不可提交"
  229. });
  230. return;
  231. }
  232. if (!frontImageList.value.length || !backImageList.value.length) {
  233. Toast({
  234. title: "请先上传身份证正反面照片"
  235. });
  236. return;
  237. } else if (!realName.value.trim()) {
  238. Toast({
  239. title: "请输入真实姓名"
  240. });
  241. return;
  242. } else if (!idCardNumber.value.trim()) {
  243. Toast({
  244. title: "请输入身份证号码"
  245. });
  246. return;
  247. } else if (!validateIdCard(idCardNumber.value.trim())) {
  248. Toast({
  249. title: "请输入正确的身份证号码"
  250. });
  251. return;
  252. }
  253. // 检查图片上传状态
  254. const frontImage = frontImageList.value[0];
  255. const backImage = backImageList.value[0];
  256. console.log(frontImage, backImage);
  257. if (!frontImage || !backImage) {
  258. Toast({
  259. title: "请先上传身份证正反面照片"
  260. });
  261. return;
  262. }
  263. if (frontImage?.status === "failed" || backImage?.status === "failed") {
  264. Toast({
  265. title: "有图片上传失败,请重新上传"
  266. });
  267. return;
  268. }
  269. if (frontImage?.status === "uploading" || backImage?.status === "uploading") {
  270. Toast({
  271. title: "图片正在上传中,请稍候"
  272. });
  273. return;
  274. }
  275. try {
  276. isProcessing.value = true;
  277. uni.showLoading({
  278. title: "开始认证"
  279. });
  280. console.log("frontImage?.info", frontImage?.info);
  281. const frontImageUrl = frontImage.info?.url || frontImage.url;
  282. const backImageUrl = backImage.info?.url || backImage.url;
  283. if (!frontImageUrl || !backImageUrl) {
  284. Toast({
  285. title: "图片上传失败,请重新上传"
  286. });
  287. uni.hideLoading();
  288. isProcessing.value = false;
  289. return;
  290. }
  291. const params = {
  292. uid: appStore.uid,
  293. idCardFront: frontImageUrl,
  294. idCardBack: backImageUrl,
  295. realName: realName.value.trim(),
  296. cardId: idCardNumber.value.trim(),
  297. };
  298. faceVerify(params)
  299. .then((res) => {
  300. uni.hideLoading();
  301. Toast({
  302. title: "认证已提交,正在审核..."
  303. });
  304. emit("fetchUserInfo");
  305. // 认证成功后清空所有数据
  306. frontImageList.value = [];
  307. backImageList.value = [];
  308. realName.value = "";
  309. idCardNumber.value = "";
  310. isProcessing.value = false;
  311. fetchFaceVerify(); // 重新拉取状态
  312. })
  313. .catch((err) => {
  314. console.log("submitDetect-err", err);
  315. uni.hideLoading();
  316. isProcessing.value = false;
  317. Toast({
  318. title: err?.msg || "认证失败,请重试"
  319. });
  320. });
  321. } catch (err) {
  322. console.log("submitDetect-err", err);
  323. uni.hideLoading();
  324. isProcessing.value = false;
  325. Toast({
  326. title: "认证失败,请重试"
  327. });
  328. }
  329. }
  330. // 身份证号码验证
  331. function validateIdCard(idCard) {
  332. const reg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
  333. return reg.test(idCard);
  334. }
  335. // 身份证号码输入处理
  336. function onIdCardChange(value) {
  337. if (value) {
  338. idCardNumber.value = value.toUpperCase();
  339. }
  340. }
  341. // 重试上传
  342. function retryUpload(type) {
  343. if (type === "front") {
  344. frontImageList.value = [];
  345. } else if (type === "back") {
  346. backImageList.value = [];
  347. }
  348. }
  349. // 重新上传(清空后重新选择)
  350. function reUpload(type) {
  351. if (type === "front") {
  352. frontImageList.value = [];
  353. } else if (type === "back") {
  354. backImageList.value = [];
  355. }
  356. }
  357. //调用getMetaInfo获取MetaInfo数据
  358. function startVerify() {
  359. try {
  360. console.log("aliyunVerify", aliyunVerify);
  361. metaInfo.value = aliyunVerify.getMetaInfo();
  362. let p = uni.getSystemInfoSync().platform;
  363. if (p === "ios") {
  364. metaInfo.value = JSON.stringify(metaInfo);
  365. }
  366. console.log("metaInfo", metaInfo.value);
  367. initFn();
  368. } catch (error) {
  369. console.log("startVerify-error", error);
  370. }
  371. }
  372. function initFn() {
  373. console.log("initFn-start");
  374. try {
  375. uni.showLoading({
  376. title: "开始认证"
  377. });
  378. init({
  379. idCardPicUrl: idCardPicUrl.value,
  380. metaInfo: metaInfo.value,
  381. })
  382. .then((res) => {
  383. console.log("initFn-----", res);
  384. certifyId.value = res.data.certifyId || "";
  385. uni.hideLoading();
  386. aliyunVerify.verify({
  387. certifyId: certifyId.value,
  388. extParams: {},
  389. },
  390. function(response) {
  391. console.log("aliyunVerify", response, response?.code);
  392. if (response?.code == 1000) {
  393. queryFn();
  394. }
  395. }
  396. );
  397. })
  398. .catch((err) => {
  399. console.log("initFn-err", err);
  400. certifyId.value = "";
  401. });
  402. } catch (err) {
  403. console.log("initFn-err", err);
  404. }
  405. }
  406. function queryFn() {
  407. console.log("initFn-start");
  408. query({
  409. certifyId: certifyId.value,
  410. })
  411. .then((res) => {
  412. const passed = res.data && res.data.passed;
  413. if (passed == "T") {
  414. Toast({
  415. title: "认证成功"
  416. });
  417. emit("fetchUserInfo");
  418. } else {
  419. Toast({
  420. title: "认证失败,请确认身份证和人脸信息是否一致?"
  421. });
  422. }
  423. console.log("queryFn", res);
  424. })
  425. .catch((err) => {
  426. console.log("queryFn-err", err);
  427. });
  428. }
  429. defineExpose({
  430. startVerify,
  431. });
  432. </script>
  433. <style lang="scss" scoped>
  434. page {
  435. background-color: #F9F7F0;
  436. }
  437. .upload-section {
  438. padding: 16rpx;
  439. }
  440. .upload-box {
  441. padding: 16rpx;
  442. border-radius: 16rpx;
  443. background: #FFFFFF;
  444. &:last-child {
  445. margin-bottom: 0;
  446. }
  447. ::v-deep .u-upload__wrap {
  448. // width: 100%;
  449. .u-upload__wrap__preview {
  450. // width: 100%;
  451. margin: 0;
  452. .u-upload__wrap__preview__image {
  453. // width: 100% !important;
  454. }
  455. }
  456. view:last-child {
  457. flex: 1;
  458. }
  459. }
  460. }
  461. .upload-title {
  462. font-size: 32rpx;
  463. font-weight: 600;
  464. color: #333;
  465. margin-bottom: 16rpx;
  466. }
  467. .upload-box-list {
  468. display: flex;
  469. }
  470. .upload-block {
  471. width: 335rpx;
  472. }
  473. .IDFront,
  474. .IDBack {
  475. width: 100%;
  476. height: 206rpx;
  477. object-fit: cover;
  478. border-radius: 8rpx;
  479. }
  480. .status-text {
  481. font-size: 22rpx;
  482. font-weight: 500;
  483. &.uploading {
  484. color: #fa8013;
  485. }
  486. &.failed {
  487. color: #ff4444;
  488. }
  489. &.success {
  490. color: #52c41a;
  491. }
  492. }
  493. .retry-btn {
  494. color: #fa8013;
  495. text-decoration: underline;
  496. margin-left: 10rpx;
  497. font-size: 20rpx;
  498. }
  499. .upload-show {
  500. padding: 15rpx;
  501. background-color: #f8f9fa;
  502. border-radius: 12rpx;
  503. }
  504. .info-section {
  505. margin: 16rpx 0;
  506. background-color: #fff;
  507. border-radius: 16rpx;
  508. padding: 16rpx;
  509. }
  510. .info-tips {
  511. padding: 10rpx 16rpx;
  512. display: flex;
  513. background: #FEF2CE;
  514. border-radius: 16rpx;
  515. border: 1rpx solid #F8C008;
  516. image {
  517. top: 4rpx;
  518. width: 32rpx;
  519. height: 32rpx;
  520. margin-right: 8rpx;
  521. }
  522. view {
  523. flex: 1;
  524. color: #333333;
  525. font-size: 24rpx;
  526. line-height: 40rpx;
  527. }
  528. }
  529. .info-title {
  530. font-size: 32rpx;
  531. font-weight: 600;
  532. color: #333;
  533. margin-bottom: 16rpx;
  534. }
  535. .info-form {
  536. .form-item {
  537. margin-bottom: 16rpx;
  538. &:last-child {
  539. margin-bottom: 0;
  540. }
  541. }
  542. }
  543. .verify-status {
  544. background-color: #fff;
  545. border-radius: 16rpx;
  546. padding: 16rpx;
  547. font-size: 28rpx;
  548. font-weight: 600;
  549. color: #F8C008;
  550. margin-bottom: 16rpx;
  551. }
  552. .kong {
  553. height: calc(132rpx + constant(safe-area-inset-bottom));
  554. height: calc(132rpx + env(safe-area-inset-bottom));
  555. }
  556. .footer {
  557. width: 100%;
  558. background: #FFFFFF;
  559. box-shadow: inset 0rpx 1rpx 0rpx 0rpx #F1F3F8;
  560. padding: 22rpx 32rpx calc(22rpx + constant(safe-area-inset-bottom));
  561. padding: 22rpx 32rpx calc(22rpx + env(safe-area-inset-bottom));position: fixed;
  562. bottom: 0;
  563. .btn {
  564. font-size: 32rpx;
  565. color: #333333;
  566. font-weight: bold;
  567. background-color: #F8C008;
  568. border-radius: 16rpx;
  569. text-align: center;
  570. line-height: 88rpx;
  571. &:disabled {
  572. background-image: linear-gradient(to right, #ccc 0%, #ccc 100%);
  573. color: #999;
  574. }
  575. }
  576. }
  577. </style>