bm-bmt/pages/assets/bmt-sign-info.vue

946 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="asset-page asset-theme bmt-info-page">
<asset-page-shell title="补充签约信息" />
<view class="asset-scroll bmt-info-scroll">
<view class="glass-panel bmt-info-notice">
<text class="bmt-info-notice__title">温馨提示</text>
<text class="bmt-info-notice__text">如核对后仍然审核失败,可联系客服处理。</text>
<text v-if="noticeText" class="bmt-info-notice__error">审核失败:{{ noticeText }}</text>
</view>
<view class="glass-panel bmt-info-section">
<text class="bmt-info-section__title">身份证认证</text>
<text class="bmt-info-section__desc">请上传本人身份证正反面照片,并核对识别后的身份信息。</text>
<view class="bmt-upload-row">
<view v-for="item in uploadList" :key="item.key" class="bmt-upload-col">
<view class="bmt-upload-box" @click="handleUpload(item.key)">
<image class="bmt-upload-image" :src="item.image || item.placeholder" mode="aspectFit" />
<view v-if="item.uploading" class="bmt-upload-mask">{{ item.loadingText || "上传中..." }}</view>
</view>
<text class="bmt-upload-label">{{ item.label }}</text>
</view>
</view>
</view>
<view class="glass-panel bmt-info-section">
<text class="bmt-info-section__title">身份信息</text>
<view class="bmt-form">
<view class="bmt-form-item">
<text class="bmt-form-label">姓名</text>
<input class="bmt-form-input" type="text" v-model="form.sign_name" placeholder="识别后自动回填"
placeholder-class="bmt-form-placeholder" />
</view>
<view class="bmt-form-item">
<text class="bmt-form-label">身份证号</text>
<input class="bmt-form-input" type="text" v-model="form.id_card" maxlength="18" placeholder="识别后自动回填"
placeholder-class="bmt-form-placeholder" />
</view>
<view class="bmt-form-item">
<text class="bmt-form-label">手机号</text>
<input class="bmt-form-input" type="number" v-model="form.sign_mobile" maxlength="11" placeholder="请输入手机号"
placeholder-class="bmt-form-placeholder" />
</view>
<view class="bmt-form-item bmt-form-item--textarea">
<text class="bmt-form-label">身份证地址</text>
<textarea class="bmt-form-textarea" v-model="form.sign_address" auto-height maxlength="120"
placeholder="识别后自动回填" placeholder-class="bmt-form-placeholder" />
</view>
<view class="bmt-shoot">
<text class="bmt-shoot__title">拍摄身份证要求:</text>
<text class="bmt-shoot__item">1、持有的本人有效二代身份证;</text>
<text class="bmt-shoot__item">2、拍摄时确保身份证边框完整、字体清晰、亮度均匀。</text>
</view>
</view>
</view>
</view>
<view class="bmt-info-bottom">
<view class="bmt-info-bottom__row">
<view class="bmt-info-button bmt-info-button--ghost" @click="handleCancel">取消</view>
<view class="bmt-info-button bmt-info-button--primary" :class="{ 'is-disabled': isBusy }"
@click="handleSubmit">提交</view>
</view>
</view>
</view>
</template>
<script>
import AssetPageShell from "../../components/asset-page-shell.vue";
import {
uploadBmtImage,
fetchBmtOcrResult,
submitBmtSignInfo,
fetchBmtContractStatus,
checkBmtSignCondition,
startBmtSigning,
} from "../../api/bmt";
const ID_CARD_REG = /^(\d{15}|\d{17}[\dXx])$/;
const OCR_TYPE_ID_CARD = "idcard_ocr";
export default {
components: {
AssetPageShell,
},
data() {
return {
info: {},
form: {
back_img: "",
id_card: "",
positive_img: "",
sign_address: "",
sign_mobile: "",
sign_name: "",
},
uploadList: [
{
key: "positive_img",
label: "人像面",
placeholder: "https://imgs.agrimedia.cn/shop/sfz-2.png",
image: "",
uploading: false,
loadingText: "",
ocrValid: false,
},
{
key: "back_img",
label: "国徽面",
placeholder: "https://imgs.agrimedia.cn/shop/sfz-1.png",
image: "",
uploading: false,
loadingText: "",
ocrValid: false,
},
],
isSubmitting: false,
isIdCardExpired: false,
};
},
computed: {
noticeText() {
return String(this.info.jzq_audit_msg || "");
},
isBusy() {
return (
this.isSubmitting ||
this.uploadList.some((item) => item.uploading)
);
},
},
onLoad(options) {
if (options && options.data) {
try {
this.info = JSON.parse(decodeURIComponent(options.data)) || {};
} catch (error) {
this.info = {};
}
}
this.initForm();
},
methods: {
initForm() {
const data = this.info || {};
this.form = {
back_img: data.back_img || data.reverse_img || "",
id_card: data.id_card || data.card_id || data.number || "",
positive_img: data.positive_img || data.front_img || "",
sign_address:
data.sign_address ||
data.card_address ||
data.id_card_address ||
data.address ||
"",
sign_mobile:
data.sign_mobile || data.mobile || data.realname_mobile || "",
sign_name:
data.sign_name ||
data.legal_representative_name ||
data.legal_person ||
data.name ||
"",
};
this.syncUploadList();
},
syncUploadList() {
this.uploadList.forEach((item) => {
item.image = this.form[item.key] || "";
item.loadingText = "";
item.ocrValid = !!item.image;
});
},
getUploadItem(key) {
return this.uploadList.find((item) => item.key === key);
},
getRequiredUploads() {
return ["positive_img", "back_img"];
},
showTip(title) {
uni.showToast({
title: title,
icon: "none",
});
},
formatError(error, fallback) {
if (!error) {
return fallback;
}
if (typeof error === "string") {
return error;
}
if (error.message) {
return error.message;
}
if (error.msg) {
return error.msg;
}
if (error.errMsg) {
return error.errMsg;
}
if (error.data && error.data.msg) {
return error.data.msg;
}
return fallback;
},
handleUpload(key) {
if (this.isBusy) {
return;
}
const target = this.getUploadItem(key);
if (!target || target.uploading) {
return;
}
uni.chooseImage({
count: 1,
sizeType: ["original", "compressed"],
sourceType: ["album", "camera"],
success: (result) => {
const filePath =
result && result.tempFilePaths && result.tempFilePaths[0];
const fileSize =
result && result.tempFiles && result.tempFiles[0]
? Number(result.tempFiles[0].size || 0)
: 0;
if (!filePath) {
return;
}
this.uploadAndRecognize(key, filePath, fileSize);
},
});
},
async uploadAndRecognize(key, filePath, fileSize) {
const target = this.getUploadItem(key);
if (!target) {
return;
}
const previous = {
image: target.image || "",
value: this.form[key] || "",
ocrValid: target.ocrValid,
};
target.uploading = true;
target.loadingText = "上传中...";
try {
// 超出 2MB 时会先压缩再上传
const uploadResult = await uploadBmtImage(filePath, {
size: fileSize,
});
const url = uploadResult && uploadResult.url ? uploadResult.url : "";
if (!url) {
throw new Error("上传失败,请重试");
}
target.image = url;
target.ocrValid = false;
this.form[key] = url;
target.loadingText = "识别中...";
console.log("[BMT签约] 图片上传成功,开始 OCR:", url);
// OCR:/api/merchant/ocr_img,type 固定 idcard_ocr
const ocrResult = await fetchBmtOcrResult({
type: OCR_TYPE_ID_CARD,
url: url,
});
console.log("[BMT签约] OCR 返回:", ocrResult);
if (!this.handleOcrResult(key, ocrResult)) {
this.restoreUpload(key, previous);
}
} catch (error) {
console.error("[BMT签约] 上传/识别失败", error);
this.restoreUpload(key, previous);
this.showTip(this.formatError(error, "识别失败,请重新上传清晰照片"));
} finally {
target.uploading = false;
target.loadingText = "";
}
},
restoreUpload(key, previous) {
const target = this.getUploadItem(key);
if (!target) {
return;
}
target.image = previous.image || "";
target.ocrValid = !!previous.ocrValid;
this.form[key] = previous.value || "";
},
getOcrData(result) {
if (!result || typeof result !== "object") {
return {};
}
if (result.data && result.data.data && result.data.data.info) {
return result.data.data.info;
}
if (result.data && result.data.info) {
return result.data.info;
}
if (result.data && result.data.data) {
return result.data.data;
}
if (result.data) {
return result.data;
}
return result;
},
handleOcrResult(key, result) {
if (key === "positive_img") {
return this.handleIdCardFrontOcr(result);
}
if (key === "back_img") {
return this.handleIdCardBackOcr(result);
}
return true;
},
handleIdCardFrontOcr(result) {
const info = this.getOcrData(result);
const name = String(info.name || info.sign_name || "").trim();
const number = String(
info.number || info.id_card || info.card_id || "",
).trim();
const address = String(
info.address || info.sign_address || info.card_address || "",
).trim();
const target = this.getUploadItem("positive_img");
if (!name && !number) {
if (target) {
target.ocrValid = false;
}
this.showTip("身份证人像面识别失败,请重新上传清晰照片");
return false;
}
if (!name) {
if (target) {
target.ocrValid = false;
}
this.showTip("未识别到姓名,请重新上传清晰照片");
return false;
}
if (!number) {
if (target) {
target.ocrValid = false;
}
this.showTip("未识别到身份证号,请重新上传清晰照片");
return false;
}
if (!ID_CARD_REG.test(number)) {
if (target) {
target.ocrValid = false;
}
this.showTip("身份证号识别异常,请重新上传清晰照片");
return false;
}
this.form.sign_name = name;
this.form.id_card = number;
if (address) {
this.form.sign_address = address;
}
if (target) {
target.ocrValid = true;
}
this.showTip("身份证识别成功,请核对信息");
return true;
},
handleIdCardBackOcr(result) {
const info = this.getOcrData(result);
const timelimit = String(
info.timelimit || info.valid_date || info.validity || "",
).trim();
const validity = this.checkIdCardTimelimit(timelimit);
const target = this.getUploadItem("back_img");
this.isIdCardExpired = false;
if (!validity.valid) {
if (target) {
target.ocrValid = false;
}
this.showTip("身份证国徽面识别失败,请重新上传清晰照片");
return false;
}
if (validity.expired) {
this.isIdCardExpired = true;
if (target) {
target.ocrValid = false;
}
this.showTip("身份证已过期,请上传最新身份证");
return false;
}
if (target) {
target.ocrValid = true;
}
this.showTip("身份证国徽面识别成功");
return true;
},
checkIdCardTimelimit(timelimit) {
const text = String(timelimit || "").trim();
if (!text) {
return { valid: false, expired: true, text: "" };
}
if (text.indexOf("长期") > -1) {
return { valid: true, expired: false, text: text };
}
const digits = text.replace(/\D/g, "");
if (digits.length < 16) {
return { valid: false, expired: true, text: text };
}
const parseDate = (value) => {
if (!value || value.length < 8) {
return null;
}
const year = parseInt(value.slice(0, 4), 10);
const month = parseInt(value.slice(4, 6), 10) - 1;
const day = parseInt(value.slice(6, 8), 10);
const dateObj = new Date(year, month, day);
return isNaN(dateObj.getTime()) ? null : dateObj;
};
const startDate = parseDate(digits.slice(0, 8));
const endDate = parseDate(digits.slice(digits.length - 8));
if (!startDate || !endDate) {
return { valid: false, expired: true, text: text };
}
const currentDate = new Date();
return {
valid: true,
expired: currentDate > endDate || currentDate < startDate,
text: text,
};
},
validateUploads() {
const labels = {
positive_img: "身份证人像面",
back_img: "身份证国徽面",
};
const required = this.getRequiredUploads();
for (let i = 0; i < required.length; i += 1) {
const key = required[i];
const target = this.getUploadItem(key);
if (!this.form[key]) {
this.showTip(`请上传${labels[key]}`);
return false;
}
if (!target || !target.ocrValid) {
this.showTip(`请重新上传${labels[key]}`);
return false;
}
}
return true;
},
validateForm() {
if (!this.validateUploads()) {
return false;
}
const validations = [
{ field: "sign_name", message: "请输入姓名" },
{ field: "id_card", message: "请输入身份证号" },
{ field: "sign_mobile", message: "请输入手机号" },
{ field: "sign_address", message: "请输入身份证地址" },
];
for (let i = 0; i < validations.length; i += 1) {
const item = validations[i];
if (!String(this.form[item.field] || "").trim()) {
this.showTip(item.message);
return false;
}
}
if (!ID_CARD_REG.test(String(this.form.id_card || "").trim())) {
this.showTip("请输入正确的身份证号");
return false;
}
return true;
},
buildPayload() {
const address = String(this.form.sign_address || "").trim();
return {
sign_name: String(this.form.sign_name || "").trim(),
id_card: String(this.form.id_card || "").trim(),
sign_mobile: String(this.form.sign_mobile || "").trim(),
sign_address: address,
contact_address: address,
positive_img: this.form.positive_img || "",
back_img: this.form.back_img || "",
};
},
handleCancel() {
if (getCurrentPages().length > 1) {
uni.navigateBack();
return;
}
uni.reLaunch({
url: "/pages/index/index",
});
},
navigateToExternalUrl(targetUrl) {
if (!targetUrl) {
return;
}
if (typeof window !== "undefined" && window.location) {
window.location.href = targetUrl;
return;
}
if (
typeof plus !== "undefined" &&
plus.webview &&
plus.webview.currentWebview
) {
const currentWebview = plus.webview.currentWebview();
if (currentWebview && typeof currentWebview.loadURL === "function") {
currentWebview.loadURL(targetUrl);
return;
}
}
uni.navigateTo({
url: targetUrl,
});
},
backAfterSubmit() {
setTimeout(() => {
this.handleCancel();
}, 800);
},
async handleSubmit() {
if (this.isBusy) {
return;
}
if (!this.validateForm()) {
return;
}
this.isSubmitting = true;
try {
await submitBmtSignInfo(this.buildPayload(), {
showLoading: true,
loadingText: "提交中",
});
uni.showToast({
title: "提交成功",
icon: "none",
});
await this.handleAfterSignInfoSubmit();
} catch (error) {
uni.showToast({
title: this.formatError(error, "提交失败"),
icon: "none",
});
} finally {
this.isSubmitting = false;
}
},
/**
* 提交成功后复查签约状态:仍未签约则自动发起签约,
* 拿到 saasShortLink 后跳转去认证;否则回到上一页。
*/
async handleAfterSignInfoSubmit() {
let saasShortLink = "";
try {
const status = (await fetchBmtContractStatus()) || {};
const contractStatus = Number(status.contract_status || 0);
if (contractStatus !== 0) {
this.backAfterSubmit();
return;
}
let contractType = Number(status.contract_type || 0);
let contractId = "";
let recordId = String(status.id || "");
try {
const checkData =
(await checkBmtSignCondition(
{},
{
showLoading: true,
loadingText: "校验中",
},
)) || {};
contractId = String(checkData.c_id || "");
recordId = String(checkData.id || recordId || "");
const checkType = checkData.contract_type;
if (
checkType !== undefined &&
checkType !== null &&
checkType !== ""
) {
contractType = Number(checkType) === 1 ? 1 : 0;
}
} catch (error) {
// 前置校验失败不阻断,继续尝试发起签约
}
const result =
(await startBmtSigning(
{
contract_type: contractType,
c_id: contractId,
id: recordId,
},
{
showLoading: true,
loadingText: "发起签约",
},
)) || {};
console.log("[BMT签约] start_signing_bmt_v2 返回:", result.raw);
if (Number(result.status) === 0 && result.saasShortLink) {
saasShortLink = String(result.saasShortLink);
} else {
uni.showToast({
title: result.message || "发起签约失败,请稍后重试",
icon: "none",
});
}
} catch (error) {
console.error("[BMT签约] 提交后自动签约失败", error);
}
if (saasShortLink) {
this.navigateToExternalUrl(saasShortLink);
return;
}
this.backAfterSubmit();
},
},
};
</script>
<style lang="scss" scoped>
@import "../../styles/tokens.scss";
@import "../../styles/common.scss";
.bmt-info-page {
min-height: 100vh;
}
.bmt-info-scroll {
padding: 12rpx 24rpx calc(env(safe-area-inset-bottom) + 180rpx);
}
.bmt-info-notice {
padding: 24rpx 26rpx;
}
.bmt-info-notice__title {
display: block;
font-size: 28rpx;
font-weight: 700;
color: $asset-warning;
}
.bmt-info-notice__text {
display: block;
margin-top: 10rpx;
font-size: 24rpx;
line-height: 1.7;
color: $asset-text-muted;
}
.bmt-info-notice__error {
display: block;
margin-top: 10rpx;
font-size: 24rpx;
line-height: 1.7;
color: $asset-danger;
}
.bmt-info-section {
margin-top: 24rpx;
padding: 28rpx 26rpx;
}
.bmt-info-section__title {
display: block;
font-size: 32rpx;
font-weight: 700;
color: $asset-text-main;
}
.bmt-info-section__desc {
display: block;
margin-top: 12rpx;
font-size: 24rpx;
line-height: 1.7;
color: $asset-text-subtle;
}
.bmt-upload-row {
display: flex;
align-items: flex-start;
margin-top: 24rpx;
}
.bmt-upload-col {
flex: 1;
min-width: 0;
text-align: center;
}
.bmt-upload-col+.bmt-upload-col {
margin-left: 16rpx;
}
.bmt-upload-box {
position: relative;
width: 100%;
height: 176rpx;
border-radius: 14rpx;
overflow: hidden;
background: rgba(18, 25, 48, 0.72);
border: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
}
.bmt-upload-image {
width: 100%;
height: 100%;
}
.bmt-upload-mask {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(5, 10, 22, 0.62);
font-size: 24rpx;
color: #ffffff;
}
.bmt-upload-label {
display: block;
margin-top: 12rpx;
min-height: 44rpx;
font-size: 22rpx;
line-height: 32rpx;
color: $asset-text-muted;
}
.bmt-form {
margin-top: 16rpx;
}
.bmt-form-item {
display: flex;
align-items: center;
min-height: 96rpx;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.bmt-form-item:last-child {
border-bottom: 0;
}
.bmt-form-item--textarea {
align-items: flex-start;
padding: 26rpx 0;
}
.bmt-form-label {
width: 200rpx;
flex-shrink: 0;
font-size: 28rpx;
color: $asset-text-muted;
}
.bmt-form-input {
flex: 1;
height: 72rpx;
padding: 0;
border: 0;
text-align: right;
font-size: 30rpx;
color: $asset-text-main;
background: transparent;
}
.bmt-form-textarea {
flex: 1;
width: auto;
min-height: 84rpx;
padding: 0;
border: 0;
text-align: right;
font-size: 28rpx;
line-height: 42rpx;
color: $asset-text-main;
background: transparent;
}
.bmt-form-placeholder {
font-size: 26rpx;
color: rgba(143, 167, 207, 0.72);
}
.bmt-shoot {
margin-top: 26rpx;
}
.bmt-shoot__title {
display: block;
font-size: 26rpx;
color: $asset-text-muted;
}
.bmt-shoot__item {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
line-height: 1.7;
color: $asset-text-subtle;
}
.bmt-info-bottom {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 20rpx 24rpx calc(20rpx + env(safe-area-inset-bottom));
border-top: 1px solid rgba(255, 255, 255, 0.08);
background: #191e32;
z-index: 30;
}
.bmt-info-bottom__row {
display: flex;
align-items: center;
}
.bmt-info-button {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
height: 84rpx;
border-radius: 999rpx;
font-size: 30rpx;
font-weight: 600;
}
.bmt-info-button+.bmt-info-button {
margin-left: 20rpx;
}
.bmt-info-button--ghost {
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.06);
color: $asset-text-main;
}
.bmt-info-button--primary {
background: linear-gradient(135deg, #7e6cff 0%, #6b56f6 100%);
box-shadow: 0 14rpx 24rpx rgba(93, 79, 225, 0.22);
color: #ffffff;
}
.bmt-info-button.is-disabled {
opacity: 0.6;
}
</style>