|
|
@@ -0,0 +1,1150 @@
|
|
|
+<template>
|
|
|
+ <div class="subject-info-page">
|
|
|
+ <h1 class="page-title">主体信息</h1>
|
|
|
+
|
|
|
+ <el-form
|
|
|
+ ref="formRef"
|
|
|
+ class="subject-form"
|
|
|
+ :model="form"
|
|
|
+ :rules="rules"
|
|
|
+ label-width="140px"
|
|
|
+ require-asterisk-position="right"
|
|
|
+ label-position="right"
|
|
|
+ >
|
|
|
+ <!-- 主体身份 -->
|
|
|
+ <section class="form-block">
|
|
|
+ <h2 class="section-head">
|
|
|
+ <span class="section-bar" aria-hidden="true" />
|
|
|
+ 主体身份 <span class="section-intro">请上传「营业执照」,需年检章齐全,当年注册除外</span>
|
|
|
+ </h2>
|
|
|
+
|
|
|
+ <!-- <el-form-item label="是否金融机构" prop="isFinancialInstitution">
|
|
|
+ <div class="field-stack">
|
|
|
+ <el-select v-model="form.isFinancialInstitution" placeholder="请选择" style="width: 260px">
|
|
|
+ <el-option label="否" :value="0" />
|
|
|
+ <el-option label="是" :value="1" />
|
|
|
+ </el-select>
|
|
|
+ <p class="field-tip">
|
|
|
+ 金融机构是指从事金融类业务的机构,如信贷、融资、理财、担保、信托、货币兑换等。
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </el-form-item> -->
|
|
|
+
|
|
|
+ <el-form-item prop="businessLicenseUrl">
|
|
|
+ <template #label>
|
|
|
+ <span class="label-with-icon">
|
|
|
+ 营业执照照片
|
|
|
+ <el-tooltip placement="top" content="请上传清晰、完整的营业执照照片,需与后续填写信息一致。">
|
|
|
+ <el-icon class="info-icon" :size="16"><InfoFilled /></el-icon>
|
|
|
+ </el-tooltip>
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ <div class="license-upload-wrap">
|
|
|
+ <el-upload
|
|
|
+ v-model:file-list="form.businessLicenseFileList"
|
|
|
+ list-type="picture-card"
|
|
|
+ :limit="1"
|
|
|
+ accept=".jpg,.jpeg,.png,.bmp,image/jpeg,image/png,image/bmp"
|
|
|
+ :before-upload="beforeLicenseUpload"
|
|
|
+ :http-request="handleLicenseUpload"
|
|
|
+ :on-remove="onLicenseRemove"
|
|
|
+ >
|
|
|
+ <el-icon><Plus /></el-icon>
|
|
|
+ </el-upload>
|
|
|
+ <!-- <div class="license-example" aria-hidden="true" style="border:1px solid red;">
|
|
|
+ <div class="license-example-thumb">
|
|
|
+ <span class="license-example-placeholder">营业执照</span>
|
|
|
+ </div>
|
|
|
+ </div> -->
|
|
|
+ </div>
|
|
|
+ <div class="upload-rules">
|
|
|
+ <p>
|
|
|
+ 1.
|
|
|
+ 请上传彩色照片或彩色扫描件或加盖公章鲜章的复印件,要求正面拍摄,露出证件四角且清晰、完整,所有字符清晰可识别,不得反光或遮挡。不得翻拍、截图、镜像、PS。
|
|
|
+ </p>
|
|
|
+ <p>2. 图片只支持 JPG、BMP、PNG 格式,文件大小不能超过 5M。</p>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ <div v-if="hasBusinessLicenseOcr" class="license-ocr-result">
|
|
|
+ <div v-if="businessLicenseOcr.creditCode" class="license-ocr-row">
|
|
|
+ <span class="license-ocr-label">统一社会信用代码</span>
|
|
|
+ <span class="license-ocr-value">{{ businessLicenseOcr.creditCode }}</span>
|
|
|
+ </div>
|
|
|
+ <div v-if="businessLicenseOcr.companyName" class="license-ocr-row">
|
|
|
+ <span class="license-ocr-label">企业名称</span>
|
|
|
+ <span class="license-ocr-value">{{ businessLicenseOcr.companyName }}</span>
|
|
|
+ </div>
|
|
|
+ <div v-if="businessLicenseOcr.legalPerson" class="license-ocr-row">
|
|
|
+ <span class="license-ocr-label">法定代表人</span>
|
|
|
+ <span class="license-ocr-value">{{ businessLicenseOcr.legalPerson }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <!-- 法定代表人证件 -->
|
|
|
+ <section class="form-block">
|
|
|
+ <h2 class="section-head">
|
|
|
+ <span class="section-bar" aria-hidden="true" />
|
|
|
+ 法定代表人证件
|
|
|
+ <span class="section-intro">请上传法定代表人的证件信息</span>
|
|
|
+ </h2>
|
|
|
+
|
|
|
+ <el-form-item label="证件类型" prop="legalIdType">
|
|
|
+ <div class="field-stack">
|
|
|
+ <el-select v-model="form.legalIdType" placeholder="请选择" clearable style="width: 260px">
|
|
|
+ <el-option label="居民身份证" value="id_card" />
|
|
|
+ <el-option label="护照" value="passport" />
|
|
|
+ <el-option label="港澳居民来往内地通行证" value="hk_macau_pass" />
|
|
|
+ <el-option label="台湾居民来往大陆通行证" value="taiwan_pass" />
|
|
|
+ </el-select>
|
|
|
+ <p class="field-tip">若需变更受益人信息请</p>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <!-- 居民身份证:人像面 / 国徽面 -->
|
|
|
+ <template v-if="form.legalIdType === 'id_card'">
|
|
|
+ <el-form-item prop="idPortraitUrl">
|
|
|
+ <template #label>
|
|
|
+ <span class="label-with-icon">
|
|
|
+ 身份证人像面照片
|
|
|
+ <el-tooltip placement="top" content="请上传身份证人像面(带头像一面)清晰彩色照片。">
|
|
|
+ <el-icon class="info-icon" :size="16"><InfoFilled /></el-icon>
|
|
|
+ </el-tooltip>
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ <div class="license-upload-wrap">
|
|
|
+ <el-upload
|
|
|
+ v-model:file-list="form.idPortraitFileList"
|
|
|
+ list-type="picture-card"
|
|
|
+ :limit="1"
|
|
|
+ accept=".jpg,.jpeg,.png,.bmp,image/jpeg,image/png,image/bmp"
|
|
|
+ :before-upload="beforeLicenseUpload"
|
|
|
+ :http-request="opt => handleIdCardUpload(opt, 'portrait')"
|
|
|
+ :on-remove="(_f, fl) => onIdCardRemove(fl, 'portrait')"
|
|
|
+ >
|
|
|
+ <el-icon><Plus /></el-icon>
|
|
|
+ </el-upload>
|
|
|
+ <!-- <div class="license-example" aria-hidden="true">
|
|
|
+ <div class="license-example-thumb id-example-thumb">
|
|
|
+ <span class="license-example-badge">示例</span>
|
|
|
+ <span class="license-example-placeholder">人像面</span>
|
|
|
+ </div>
|
|
|
+ </div> -->
|
|
|
+ </div>
|
|
|
+ <div class="upload-rules">
|
|
|
+ <p>
|
|
|
+ 1. 请上传彩色照片 or 彩色扫描件 or
|
|
|
+ 加盖公章鲜章的复印件,要求正面拍摄,露出证件四角且清晰、完整,所有字符清晰可识别,不得反光或遮挡。不得翻拍、截图、镜像、PS。
|
|
|
+ </p>
|
|
|
+ <p>2. 图片只支持 JPG、BMP、PNG 格式,文件大小不能超过 5M。</p>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item prop="idEmblemUrl">
|
|
|
+ <template #label>
|
|
|
+ <span class="label-with-icon">
|
|
|
+ 身份证国徽面照片
|
|
|
+ <el-tooltip placement="top" content="请上传身份证国徽面清晰彩色照片。">
|
|
|
+ <el-icon class="info-icon" :size="16"><InfoFilled /></el-icon>
|
|
|
+ </el-tooltip>
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ <div class="license-upload-wrap">
|
|
|
+ <el-upload
|
|
|
+ v-model:file-list="form.idEmblemFileList"
|
|
|
+ list-type="picture-card"
|
|
|
+ :limit="1"
|
|
|
+ accept=".jpg,.jpeg,.png,.bmp,image/jpeg,image/png,image/bmp"
|
|
|
+ :before-upload="beforeLicenseUpload"
|
|
|
+ :http-request="opt => handleIdCardUpload(opt, 'emblem')"
|
|
|
+ :on-remove="(_f, fl) => onIdCardRemove(fl, 'emblem')"
|
|
|
+ >
|
|
|
+ <el-icon><Plus /></el-icon>
|
|
|
+ </el-upload>
|
|
|
+ <!-- <div class="license-example" aria-hidden="true">
|
|
|
+ <div class="license-example-thumb id-example-thumb">
|
|
|
+ <span class="license-example-badge">示例</span>
|
|
|
+ <span class="license-example-placeholder">国徽面</span>
|
|
|
+ </div>
|
|
|
+ </div> -->
|
|
|
+ </div>
|
|
|
+ <div class="upload-rules">
|
|
|
+ <p>
|
|
|
+ 1. 请上传彩色照片 or 彩色扫描件 or
|
|
|
+ 加盖公章鲜章的复印件,要求正面拍摄,露出证件四角且清晰、完整,所有字符清晰可识别,不得反光或遮挡。不得翻拍、截图、镜像、PS。
|
|
|
+ </p>
|
|
|
+ <p>2. 图片只支持 JPG、BMP、PNG 格式,文件大小不能超过 5M。</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="showIdPortraitOcrBlock" class="ocr-result-container">
|
|
|
+ <div v-if="isIdPortraitOcrProcessing" class="ocr-result-item">
|
|
|
+ <span class="label">识别中:</span>
|
|
|
+ <span class="value">正在识别身份证信息,请稍候...</span>
|
|
|
+ </div>
|
|
|
+ <template v-else>
|
|
|
+ <div v-if="idPortraitOcr.name" class="ocr-result-item">
|
|
|
+ <span class="label">姓名:</span>
|
|
|
+ <span class="value">{{ idPortraitOcr.name }}</span>
|
|
|
+ </div>
|
|
|
+ <div v-if="idPortraitOcr.idNumber" class="ocr-result-item">
|
|
|
+ <span class="label">身份证号:</span>
|
|
|
+ <span class="value">{{ idPortraitOcr.idNumber }}</span>
|
|
|
+ </div>
|
|
|
+ <div v-if="idCardValidityDisplay" class="ocr-result-item">
|
|
|
+ <span class="label">身份证有效期:</span>
|
|
|
+ <span class="value">{{ idCardValidityDisplay }}</span>
|
|
|
+ </div>
|
|
|
+ <div v-if="!idPortraitOcr.name && !idPortraitOcr.idNumber && !idCardValidityDisplay" class="ocr-result-tip">
|
|
|
+ 请等待身份证识别完成
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <p class="ubo-footer-tip">
|
|
|
+ 平台将通过外部渠道核验该法人是否为受益人,若通过,将使用法人信息作为该主体的受益人(UBO)信息
|
|
|
+ </p>
|
|
|
+ </template>
|
|
|
+ </section>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <div class="footer-actions">
|
|
|
+ <el-button @click="onBack"> 返回 </el-button>
|
|
|
+ <el-button type="primary" class="btn-next" :loading="nextLoading" @click="onNext"> 下一步 </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { computed, nextTick, onMounted, reactive, ref, watch } from "vue";
|
|
|
+import { useRoute, useRouter } from "vue-router";
|
|
|
+import { ElMessage } from "element-plus";
|
|
|
+import type { FormInstance, FormRules } from "element-plus";
|
|
|
+import type { UploadRequestOptions, UploadUserFile } from "element-plus";
|
|
|
+import { Plus, InfoFilled } from "@element-plus/icons-vue";
|
|
|
+import { getOcrRequestByBase64, getUpload } from "@/api/modules/businessInfo";
|
|
|
+import { localGet, localSet } from "@/utils/index";
|
|
|
+
|
|
|
+const BUSINESS_DATA_CACHE_KEY = "businessData";
|
|
|
+
|
|
|
+const formRef = ref<FormInstance>();
|
|
|
+const nextLoading = ref(false);
|
|
|
+const route = useRoute();
|
|
|
+const router = useRouter();
|
|
|
+
|
|
|
+const businessLicenseOcr = reactive({
|
|
|
+ creditCode: "",
|
|
|
+ companyName: "",
|
|
|
+ legalPerson: ""
|
|
|
+});
|
|
|
+
|
|
|
+const hasBusinessLicenseOcr = computed(
|
|
|
+ () => !!(businessLicenseOcr.creditCode || businessLicenseOcr.companyName || businessLicenseOcr.legalPerson)
|
|
|
+);
|
|
|
+
|
|
|
+function clearBusinessLicenseOcr() {
|
|
|
+ businessLicenseOcr.creditCode = "";
|
|
|
+ businessLicenseOcr.companyName = "";
|
|
|
+ businessLicenseOcr.legalPerson = "";
|
|
|
+}
|
|
|
+
|
|
|
+/** 从 ocrRequestByBase64 成功响应中取出 creditCode / companyName / legalPerson */
|
|
|
+function pickBusinessLicenseOcrFields(res: any) {
|
|
|
+ const raw = res?.data ?? res;
|
|
|
+ let node: any = raw;
|
|
|
+ if (node && typeof node === "object" && "data" in node && node.data !== undefined) {
|
|
|
+ node = node.data;
|
|
|
+ }
|
|
|
+ const item = Array.isArray(node) ? node[0] : node;
|
|
|
+ if (!item || typeof item !== "object") {
|
|
|
+ return { creditCode: "", companyName: "", legalPerson: "" };
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ creditCode: String(item.creditCode ?? item.credit_code ?? "").trim(),
|
|
|
+ companyName: String(item.companyName ?? item.company_name ?? "").trim(),
|
|
|
+ legalPerson: String(item.legalPerson ?? item.legal_person ?? "").trim()
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+/** 身份证有效期文案 → YYYY-MM-DD */
|
|
|
+function splitIdCardValidPeriod(raw: string): { begin: string; end: string } {
|
|
|
+ const s = (raw || "").trim();
|
|
|
+ if (!s) return { begin: "", end: "" };
|
|
|
+ if (/长期|长期有效/.test(s)) return { begin: "", end: "长期" };
|
|
|
+ const norm = s
|
|
|
+ .replace(/年|月|日/g, ".")
|
|
|
+ .replace(/\.+/g, ".")
|
|
|
+ .trim();
|
|
|
+ const parts = norm.split(/[-至~~]/);
|
|
|
+ const normOne = (p: string) => {
|
|
|
+ const m = p.trim().match(/(\d{4})\D*(\d{1,2})\D*(\d{1,2})/);
|
|
|
+ if (m) return `${m[1]}-${m[2].padStart(2, "0")}-${m[3].padStart(2, "0")}`;
|
|
|
+ return p.trim();
|
|
|
+ };
|
|
|
+ if (parts.length >= 2) {
|
|
|
+ return { begin: normOne(parts[0]), end: normOne(parts[1]) };
|
|
|
+ }
|
|
|
+ return { begin: "", end: "" };
|
|
|
+}
|
|
|
+
|
|
|
+/** 人像面 getOcrRequestByBase64(ID_CARD) 成功响应 */
|
|
|
+function pickIdCardPortraitOcrFields(res: any): {
|
|
|
+ name: string;
|
|
|
+ idNumber: string;
|
|
|
+ idCardAddress: string;
|
|
|
+ cardPeriodBegin: string;
|
|
|
+ cardPeriodEnd: string;
|
|
|
+} {
|
|
|
+ const raw = res?.data ?? res;
|
|
|
+ let node: any = raw;
|
|
|
+ if (node && typeof node === "object" && "data" in node && node.data !== undefined) {
|
|
|
+ node = node.data;
|
|
|
+ }
|
|
|
+ const arr = Array.isArray(node) ? node : [node];
|
|
|
+ const item = arr[0];
|
|
|
+ const item2 = arr[1];
|
|
|
+ if (!item || typeof item !== "object") {
|
|
|
+ return { name: "", idNumber: "", idCardAddress: "", cardPeriodBegin: "", cardPeriodEnd: "" };
|
|
|
+ }
|
|
|
+ let name = String(item.name ?? item.userName ?? "").trim();
|
|
|
+ let idNumber = String(item.idNumber ?? item.id_number ?? item.idCard ?? item.id_card ?? "").trim();
|
|
|
+ const faceData = item.face?.data || item2?.face?.data;
|
|
|
+ const backData = item.back?.data || item2?.back?.data;
|
|
|
+ if (faceData && typeof faceData === "object") {
|
|
|
+ if (!name) name = String(faceData.name ?? "").trim();
|
|
|
+ if (!idNumber) idNumber = String(faceData.idNumber ?? faceData.id_number ?? "").trim();
|
|
|
+ }
|
|
|
+ let idCardAddress = "";
|
|
|
+ if (backData && typeof backData === "object") {
|
|
|
+ idCardAddress = String(backData.address ?? backData.registeredAddress ?? backData.issueAuthority ?? "").trim();
|
|
|
+ }
|
|
|
+ if (faceData && typeof faceData === "object" && !idCardAddress) {
|
|
|
+ idCardAddress = String(faceData.address ?? "").trim();
|
|
|
+ }
|
|
|
+ const faceKv = item.face?.prism_keyValueInfo || item2?.face?.prism_keyValueInfo;
|
|
|
+ if (Array.isArray(faceKv)) {
|
|
|
+ for (const row of faceKv) {
|
|
|
+ if (row?.key === "name" && row?.value && !name) name = String(row.value).trim();
|
|
|
+ if (row?.key === "idNumber" && row?.value && !idNumber) idNumber = String(row.value).trim();
|
|
|
+ if (row?.key === "address" && row?.value && !idCardAddress) {
|
|
|
+ idCardAddress = String(row.value).trim();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const backKv = item.back?.prism_keyValueInfo || item2?.back?.prism_keyValueInfo;
|
|
|
+ let validPeriodFromKv = "";
|
|
|
+ if (Array.isArray(backKv)) {
|
|
|
+ for (const row of backKv) {
|
|
|
+ if (row?.key === "address" && row?.value && !idCardAddress) {
|
|
|
+ idCardAddress = String(row.value).trim();
|
|
|
+ }
|
|
|
+ if (row?.value) {
|
|
|
+ const keyRaw = String(row.key || "");
|
|
|
+ if (/有效期限|有效期|失效日期|签发日期|validPeriod|validDate|valid_period/i.test(keyRaw)) {
|
|
|
+ const v = String(row.value).trim();
|
|
|
+ if (v && !validPeriodFromKv) validPeriodFromKv = v;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ let vp = String(
|
|
|
+ (backData && (backData.validPeriod ?? backData.validDate)) ||
|
|
|
+ (faceData && (faceData.validPeriod ?? faceData.validDate)) ||
|
|
|
+ (item && (item.validPeriod ?? item.validDate)) ||
|
|
|
+ (item2 && (item2.validPeriod ?? item2.validDate)) ||
|
|
|
+ ""
|
|
|
+ ).trim();
|
|
|
+ if (!vp && validPeriodFromKv) vp = validPeriodFromKv;
|
|
|
+ const { begin: cardPeriodBegin, end: cardPeriodEnd } = splitIdCardValidPeriod(vp);
|
|
|
+ return { name, idNumber, idCardAddress, cardPeriodBegin, cardPeriodEnd };
|
|
|
+}
|
|
|
+
|
|
|
+const form = reactive<{
|
|
|
+ isFinancialInstitution: number;
|
|
|
+ businessLicenseFileList: UploadUserFile[];
|
|
|
+ businessLicenseUrl: string;
|
|
|
+ legalIdType: string;
|
|
|
+ idPortraitFileList: UploadUserFile[];
|
|
|
+ idEmblemFileList: UploadUserFile[];
|
|
|
+ idPortraitUrl: string;
|
|
|
+ idEmblemUrl: string;
|
|
|
+}>({
|
|
|
+ isFinancialInstitution: 0,
|
|
|
+ businessLicenseFileList: [],
|
|
|
+ businessLicenseUrl: "",
|
|
|
+ legalIdType: "id_card",
|
|
|
+ idPortraitFileList: [],
|
|
|
+ idEmblemFileList: [],
|
|
|
+ idPortraitUrl: "",
|
|
|
+ idEmblemUrl: ""
|
|
|
+});
|
|
|
+
|
|
|
+const idPortraitOcr = reactive({
|
|
|
+ name: "",
|
|
|
+ idNumber: "",
|
|
|
+ idCardAddress: "",
|
|
|
+ cardPeriodBegin: "",
|
|
|
+ cardPeriodEnd: ""
|
|
|
+});
|
|
|
+
|
|
|
+const isIdPortraitOcrProcessing = ref(false);
|
|
|
+
|
|
|
+const hasPortraitUploadSuccess = computed(() => {
|
|
|
+ const f = form.idPortraitFileList[0];
|
|
|
+ return f?.status === "success";
|
|
|
+});
|
|
|
+
|
|
|
+const hasEmblemUploadSuccess = computed(() => {
|
|
|
+ const f = form.idEmblemFileList[0];
|
|
|
+ return f?.status === "success";
|
|
|
+});
|
|
|
+
|
|
|
+/** 身份证有效期展示(国徽面 OCR 主要提供) */
|
|
|
+const idCardValidityDisplay = computed(() => {
|
|
|
+ const b = (idPortraitOcr.cardPeriodBegin || "").trim();
|
|
|
+ const e = (idPortraitOcr.cardPeriodEnd || "").trim();
|
|
|
+ if (b && e) return e === "长期" ? `${b} 至 长期` : `${b} 至 ${e}`;
|
|
|
+ if (e === "长期" && !b) return "长期";
|
|
|
+ if (b && !e) return b;
|
|
|
+ if (!b && e) return e;
|
|
|
+ return "";
|
|
|
+});
|
|
|
+
|
|
|
+const hasIdPortraitOcrDisplay = computed(() => !!(idPortraitOcr.name || idPortraitOcr.idNumber || idCardValidityDisplay.value));
|
|
|
+
|
|
|
+const showIdPortraitOcrBlock = computed(
|
|
|
+ () =>
|
|
|
+ form.legalIdType === "id_card" &&
|
|
|
+ (hasPortraitUploadSuccess.value ||
|
|
|
+ hasEmblemUploadSuccess.value ||
|
|
|
+ isIdPortraitOcrProcessing.value ||
|
|
|
+ hasIdPortraitOcrDisplay.value)
|
|
|
+);
|
|
|
+
|
|
|
+function clearIdPortraitOcr() {
|
|
|
+ idPortraitOcr.name = "";
|
|
|
+ idPortraitOcr.idNumber = "";
|
|
|
+ idPortraitOcr.idCardAddress = "";
|
|
|
+ idPortraitOcr.cardPeriodBegin = "";
|
|
|
+ idPortraitOcr.cardPeriodEnd = "";
|
|
|
+}
|
|
|
+
|
|
|
+function buildSubjectInfoForBusinessData(): {
|
|
|
+ contact_type: "LEGAL";
|
|
|
+ subject_type: "SUBJECT_TYPE_ENTERPRISE" | "SUBJECT_TYPE_INDIVIDUAL";
|
|
|
+ business_license_info: {
|
|
|
+ license_copy: string;
|
|
|
+ license_number: string;
|
|
|
+ merchant_name: string;
|
|
|
+ legal_person: string;
|
|
|
+ };
|
|
|
+ identity_info: {
|
|
|
+ id_doc_type: string;
|
|
|
+ id_holder_type: "LEGAL";
|
|
|
+ id_card_info: {
|
|
|
+ id_card_copy: string;
|
|
|
+ id_card_national: string;
|
|
|
+ id_card_name: string;
|
|
|
+ id_card_number: string;
|
|
|
+ id_card_address: string;
|
|
|
+ card_period_begin: string;
|
|
|
+ card_period_end: string;
|
|
|
+ };
|
|
|
+ };
|
|
|
+} {
|
|
|
+ const cached = localGet(BUSINESS_DATA_CACHE_KEY) as { entityType?: string } | null | undefined;
|
|
|
+ const entityType = String(
|
|
|
+ (cached && typeof cached === "object" && cached.entityType) ||
|
|
|
+ (typeof route.query.entityType === "string" ? route.query.entityType : "") ||
|
|
|
+ ""
|
|
|
+ ).trim();
|
|
|
+ /** 企业 → SUBJECT_TYPE_ENTERPRISE;个体工商户(entityType=individual)及其他默认 → SUBJECT_TYPE_INDIVIDUAL */
|
|
|
+ const subject_type = entityType === "enterprise" ? ("SUBJECT_TYPE_ENTERPRISE" as const) : ("SUBJECT_TYPE_INDIVIDUAL" as const);
|
|
|
+
|
|
|
+ return {
|
|
|
+ contact_type: "LEGAL" as const,
|
|
|
+ subject_type,
|
|
|
+ business_license_info: {
|
|
|
+ license_copy: (form.businessLicenseUrl || "").trim(),
|
|
|
+ license_number: (businessLicenseOcr.creditCode || "").trim(),
|
|
|
+ merchant_name: (businessLicenseOcr.companyName || "").trim(),
|
|
|
+ legal_person: (businessLicenseOcr.legalPerson || "").trim()
|
|
|
+ },
|
|
|
+ identity_info: {
|
|
|
+ id_doc_type: "IDENTIFICATION_TYPE_IDCARD",
|
|
|
+ id_holder_type: "LEGAL" as const,
|
|
|
+ id_card_info: {
|
|
|
+ id_card_copy: (form.idPortraitUrl || "").trim(),
|
|
|
+ id_card_national: (form.idEmblemUrl || "").trim(),
|
|
|
+ id_card_name: (idPortraitOcr.name || "").trim(),
|
|
|
+ id_card_number: (idPortraitOcr.idNumber || "").trim(),
|
|
|
+ id_card_address: (idPortraitOcr.idCardAddress || "").trim(),
|
|
|
+ card_period_begin: (idPortraitOcr.cardPeriodBegin || "").trim(),
|
|
|
+ card_period_end: (idPortraitOcr.cardPeriodEnd || "").trim()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function mergeSubjectInfoIntoBusinessDataCache() {
|
|
|
+ const prev = localGet(BUSINESS_DATA_CACHE_KEY);
|
|
|
+ const base = prev && typeof prev === "object" && !Array.isArray(prev) ? { ...(prev as Record<string, unknown>) } : {};
|
|
|
+ base.subject_info = buildSubjectInfoForBusinessData();
|
|
|
+ localSet(BUSINESS_DATA_CACHE_KEY, base);
|
|
|
+}
|
|
|
+
|
|
|
+function mapApiIdDocTypeToLegalId(apiType: unknown): string {
|
|
|
+ const s = String(apiType || "").trim();
|
|
|
+ switch (s) {
|
|
|
+ case "IDENTIFICATION_TYPE_IDCARD":
|
|
|
+ return "id_card";
|
|
|
+ case "IDENTIFICATION_TYPE_OVERSEA_PASSPORT":
|
|
|
+ return "passport";
|
|
|
+ case "IDENTIFICATION_TYPE_HONGKONG_PASSPORT":
|
|
|
+ case "IDENTIFICATION_TYPE_MACAO_PASSPORT":
|
|
|
+ return "hk_macau_pass";
|
|
|
+ case "IDENTIFICATION_TYPE_TAIWAN_PASSPORT":
|
|
|
+ return "taiwan_pass";
|
|
|
+ default:
|
|
|
+ if (/IDCARD|ID_CARD/i.test(s)) return "id_card";
|
|
|
+ return "id_card";
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 从 localStorage businessData.subject_info 回显本页表单与 OCR 展示 */
|
|
|
+function hydrateSubjectFormFromBusinessDataCache() {
|
|
|
+ const raw = localGet(BUSINESS_DATA_CACHE_KEY) as Record<string, unknown> | null | undefined;
|
|
|
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
|
|
|
+ const si = raw.subject_info as Record<string, unknown> | undefined;
|
|
|
+ if (!si || typeof si !== "object" || Array.isArray(si)) return;
|
|
|
+
|
|
|
+ const bli = si.business_license_info as Record<string, unknown> | undefined;
|
|
|
+ if (bli && typeof bli === "object" && !Array.isArray(bli)) {
|
|
|
+ const lic = String(bli.license_copy ?? "").trim();
|
|
|
+ if (lic) {
|
|
|
+ form.businessLicenseUrl = lic;
|
|
|
+ form.businessLicenseFileList = [{ name: "license.jpg", url: lic, status: "success" } as UploadUserFile];
|
|
|
+ } else {
|
|
|
+ form.businessLicenseUrl = "";
|
|
|
+ form.businessLicenseFileList = [];
|
|
|
+ }
|
|
|
+ businessLicenseOcr.creditCode = String(bli.license_number ?? "").trim();
|
|
|
+ businessLicenseOcr.companyName = String(bli.merchant_name ?? "").trim();
|
|
|
+ businessLicenseOcr.legalPerson = String(bli.legal_person ?? "").trim();
|
|
|
+ }
|
|
|
+
|
|
|
+ const ii = si.identity_info as Record<string, unknown> | undefined;
|
|
|
+ if (ii && typeof ii === "object" && !Array.isArray(ii)) {
|
|
|
+ if (String(ii.id_doc_type ?? "").trim()) {
|
|
|
+ form.legalIdType = mapApiIdDocTypeToLegalId(ii.id_doc_type);
|
|
|
+ }
|
|
|
+ const ici = ii.id_card_info as Record<string, unknown> | undefined;
|
|
|
+ if (ici && typeof ici === "object" && !Array.isArray(ici)) {
|
|
|
+ const copy = String(ici.id_card_copy ?? "").trim();
|
|
|
+ const nat = String(ici.id_card_national ?? "").trim();
|
|
|
+ if (copy) {
|
|
|
+ form.idPortraitUrl = copy;
|
|
|
+ form.idPortraitFileList = [{ name: "id-portrait.jpg", url: copy, status: "success" } as UploadUserFile];
|
|
|
+ } else {
|
|
|
+ form.idPortraitUrl = "";
|
|
|
+ form.idPortraitFileList = [];
|
|
|
+ }
|
|
|
+ if (nat) {
|
|
|
+ form.idEmblemUrl = nat;
|
|
|
+ form.idEmblemFileList = [{ name: "id-emblem.jpg", url: nat, status: "success" } as UploadUserFile];
|
|
|
+ } else {
|
|
|
+ form.idEmblemUrl = "";
|
|
|
+ form.idEmblemFileList = [];
|
|
|
+ }
|
|
|
+ idPortraitOcr.name = String(ici.id_card_name ?? "").trim();
|
|
|
+ idPortraitOcr.idNumber = String(ici.id_card_number ?? "").trim();
|
|
|
+ idPortraitOcr.idCardAddress = String(ici.id_card_address ?? "").trim();
|
|
|
+ idPortraitOcr.cardPeriodBegin = String(ici.card_period_begin ?? "").trim();
|
|
|
+ idPortraitOcr.cardPeriodEnd = String(ici.card_period_end ?? "").trim();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ nextTick(() => {
|
|
|
+ formRef.value?.clearValidate();
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+const rules: FormRules = {
|
|
|
+ businessLicenseUrl: [
|
|
|
+ {
|
|
|
+ required: true,
|
|
|
+ validator: (_r, _v, cb) => {
|
|
|
+ if (!form.businessLicenseUrl?.trim()) {
|
|
|
+ cb(new Error("请上传营业执照照片"));
|
|
|
+ } else {
|
|
|
+ cb();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ trigger: "change"
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ legalIdType: [{ required: true, message: "请选择证件类型", trigger: "change" }],
|
|
|
+ idPortraitUrl: [
|
|
|
+ {
|
|
|
+ validator: (_r, _v, cb) => {
|
|
|
+ if (form.legalIdType === "id_card" && !form.idPortraitUrl?.trim()) {
|
|
|
+ cb(new Error("请上传身份证人像面照片"));
|
|
|
+ } else {
|
|
|
+ cb();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ trigger: "change"
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ idEmblemUrl: [
|
|
|
+ {
|
|
|
+ validator: (_r, _v, cb) => {
|
|
|
+ if (form.legalIdType === "id_card" && !form.idEmblemUrl?.trim()) {
|
|
|
+ cb(new Error("请上传身份证国徽面照片"));
|
|
|
+ } else {
|
|
|
+ cb();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ trigger: "change"
|
|
|
+ }
|
|
|
+ ]
|
|
|
+};
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => form.legalIdType,
|
|
|
+ v => {
|
|
|
+ if (v !== "id_card") {
|
|
|
+ form.idPortraitUrl = "";
|
|
|
+ form.idEmblemUrl = "";
|
|
|
+ form.idPortraitFileList = [];
|
|
|
+ form.idEmblemFileList = [];
|
|
|
+ clearIdPortraitOcr();
|
|
|
+ }
|
|
|
+ }
|
|
|
+);
|
|
|
+
|
|
|
+const MAX_LICENSE_MB = 5;
|
|
|
+
|
|
|
+function beforeLicenseUpload(file: File) {
|
|
|
+ const name = file.name?.toLowerCase() || "";
|
|
|
+ const okExt = /\.(jpe?g|png|bmp)$/i.test(name);
|
|
|
+ const okMime = ["image/jpeg", "image/png", "image/bmp"].includes(file.type);
|
|
|
+ if (!okExt && !okMime) {
|
|
|
+ ElMessage.error("图片只支持 JPG、BMP、PNG 格式");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ const sizeMb = file.size / 1024 / 1024;
|
|
|
+ if (sizeMb > MAX_LICENSE_MB) {
|
|
|
+ ElMessage.error(`文件大小不能超过 ${MAX_LICENSE_MB}M`);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+/** 与 getUpload(微信 media/upload)返回结构对齐 */
|
|
|
+function parseMediaUploadResult(res: any): { fileUrl: string; mediaId: string; errMsg: string } {
|
|
|
+ const envelope = res?.data ?? res;
|
|
|
+ const body = envelope?.data !== undefined ? envelope.data : envelope;
|
|
|
+ const fileUrl =
|
|
|
+ body?.url ??
|
|
|
+ body?.fileUrl ??
|
|
|
+ envelope?.url ??
|
|
|
+ envelope?.data?.url ??
|
|
|
+ envelope?.data?.fileUrl ??
|
|
|
+ body?.mediaUrl ??
|
|
|
+ body?.data?.mediaUrl ??
|
|
|
+ "";
|
|
|
+ const mediaId =
|
|
|
+ body?.media_id ?? body?.mediaId ?? envelope?.media_id ?? envelope?.data?.media_id ?? envelope?.data?.mediaId ?? "";
|
|
|
+ const errMsg = envelope?.msg ?? body?.msg ?? res?.msg ?? "";
|
|
|
+ return { fileUrl, mediaId, errMsg };
|
|
|
+}
|
|
|
+
|
|
|
+function mergeIdCardOcrFromFields(fields: ReturnType<typeof pickIdCardPortraitOcrFields>, mode: "portrait" | "emblem") {
|
|
|
+ if (mode === "portrait") {
|
|
|
+ if (fields.name) idPortraitOcr.name = fields.name;
|
|
|
+ if (fields.idNumber) idPortraitOcr.idNumber = fields.idNumber;
|
|
|
+ if (fields.idCardAddress) idPortraitOcr.idCardAddress = fields.idCardAddress;
|
|
|
+ if (fields.cardPeriodBegin) idPortraitOcr.cardPeriodBegin = fields.cardPeriodBegin;
|
|
|
+ if (fields.cardPeriodEnd) idPortraitOcr.cardPeriodEnd = fields.cardPeriodEnd;
|
|
|
+ } else {
|
|
|
+ if (fields.cardPeriodBegin) idPortraitOcr.cardPeriodBegin = fields.cardPeriodBegin;
|
|
|
+ if (fields.cardPeriodEnd) idPortraitOcr.cardPeriodEnd = fields.cardPeriodEnd;
|
|
|
+ if (fields.idCardAddress) idPortraitOcr.idCardAddress = fields.idCardAddress;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function requestIdCardPortraitOcr(file: File) {
|
|
|
+ const formData = new FormData();
|
|
|
+ formData.append("imageFile", file, file.name || "id-portrait.jpg");
|
|
|
+ formData.append("ocrType", "ID_CARD");
|
|
|
+ isIdPortraitOcrProcessing.value = true;
|
|
|
+ try {
|
|
|
+ const res: any = await getOcrRequestByBase64(formData);
|
|
|
+ if (res?.code === 200 || res?.code === "200") {
|
|
|
+ const fields = pickIdCardPortraitOcrFields(res);
|
|
|
+ mergeIdCardOcrFromFields(fields, "portrait");
|
|
|
+ ElMessage.success("身份证人像面识别成功");
|
|
|
+ } else {
|
|
|
+ clearIdPortraitOcr();
|
|
|
+ ElMessage.warning(res?.msg || "身份证人像面识别未通过,请核对照片清晰度");
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ isIdPortraitOcrProcessing.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 国徽面单独 OCR,合并有效期限(及背面地址等),不清空人像面已识别字段 */
|
|
|
+async function requestIdCardEmblemOcr(file: File) {
|
|
|
+ const formData = new FormData();
|
|
|
+ formData.append("imageFile", file, file.name || "id-emblem.jpg");
|
|
|
+ formData.append("ocrType", "ID_CARD");
|
|
|
+ isIdPortraitOcrProcessing.value = true;
|
|
|
+ try {
|
|
|
+ const res: any = await getOcrRequestByBase64(formData);
|
|
|
+ if (res?.code === 200 || res?.code === "200") {
|
|
|
+ const fields = pickIdCardPortraitOcrFields(res);
|
|
|
+ mergeIdCardOcrFromFields(fields, "emblem");
|
|
|
+ if (fields.cardPeriodBegin || fields.cardPeriodEnd) {
|
|
|
+ ElMessage.success("身份证国徽面识别成功");
|
|
|
+ } else {
|
|
|
+ ElMessage.warning("未识别到有效期限,请确认国徽面照片清晰完整");
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ ElMessage.warning(res?.msg || "身份证国徽面识别未通过,请核对照片清晰度");
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ isIdPortraitOcrProcessing.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function requestBusinessLicenseOcr(file: File) {
|
|
|
+ const formData = new FormData();
|
|
|
+ formData.append("imageFile", file, file.name || "license.jpg");
|
|
|
+ formData.append("ocrType", "BUSINESS_LICENSE");
|
|
|
+ const res: any = await getOcrRequestByBase64(formData);
|
|
|
+ if (res?.code === 200 || res?.code === "200") {
|
|
|
+ const fields = pickBusinessLicenseOcrFields(res);
|
|
|
+ businessLicenseOcr.creditCode = fields.creditCode;
|
|
|
+ businessLicenseOcr.companyName = fields.companyName;
|
|
|
+ businessLicenseOcr.legalPerson = fields.legalPerson;
|
|
|
+ ElMessage.success("营业执照识别成功");
|
|
|
+ } else {
|
|
|
+ clearBusinessLicenseOcr();
|
|
|
+ ElMessage.warning(res?.msg || "营业执照识别未通过,请核对照片清晰度");
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function handleLicenseUpload(options: UploadRequestOptions) {
|
|
|
+ const uploadFileItem = options.file as UploadUserFile;
|
|
|
+ const raw = uploadFileItem.raw || uploadFileItem;
|
|
|
+ const file = raw instanceof File ? raw : null;
|
|
|
+ if (!file) return;
|
|
|
+
|
|
|
+ uploadFileItem.status = "uploading";
|
|
|
+ try {
|
|
|
+ const formData = new FormData();
|
|
|
+ formData.append("file", file);
|
|
|
+ const res: any = await getUpload(formData);
|
|
|
+ const { fileUrl, mediaId, errMsg } = parseMediaUploadResult(res);
|
|
|
+
|
|
|
+ if (fileUrl || mediaId) {
|
|
|
+ uploadFileItem.status = "success";
|
|
|
+ if (fileUrl) {
|
|
|
+ uploadFileItem.url = fileUrl;
|
|
|
+ }
|
|
|
+ uploadFileItem.response = { media_id: mediaId, url: fileUrl };
|
|
|
+ form.businessLicenseUrl = fileUrl || mediaId;
|
|
|
+ try {
|
|
|
+ await requestBusinessLicenseOcr(file);
|
|
|
+ } catch {
|
|
|
+ clearBusinessLicenseOcr();
|
|
|
+ ElMessage.warning("营业执照识别服务暂时不可用,请稍后重试");
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ uploadFileItem.status = "fail";
|
|
|
+ ElMessage.error(errMsg || "上传失败,未返回可用结果");
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ uploadFileItem.status = "fail";
|
|
|
+ }
|
|
|
+ formRef.value?.validateField("businessLicenseUrl").catch(() => {});
|
|
|
+}
|
|
|
+
|
|
|
+function onLicenseRemove(_file: UploadUserFile, fileList: UploadUserFile[]) {
|
|
|
+ if (!fileList.length) {
|
|
|
+ form.businessLicenseUrl = "";
|
|
|
+ clearBusinessLicenseOcr();
|
|
|
+ } else {
|
|
|
+ const first = fileList[0] as UploadUserFile & { url?: string; response?: { url?: string } };
|
|
|
+ form.businessLicenseUrl = first?.url || first?.response?.url || "";
|
|
|
+ }
|
|
|
+ formRef.value?.validateField("businessLicenseUrl").catch(() => {});
|
|
|
+}
|
|
|
+
|
|
|
+type IdCardSide = "portrait" | "emblem";
|
|
|
+
|
|
|
+async function handleIdCardUpload(options: UploadRequestOptions, side: IdCardSide) {
|
|
|
+ const uploadFileItem = options.file as UploadUserFile;
|
|
|
+ const raw = uploadFileItem.raw || uploadFileItem;
|
|
|
+ const file = raw instanceof File ? raw : null;
|
|
|
+ if (!file) return;
|
|
|
+
|
|
|
+ uploadFileItem.status = "uploading";
|
|
|
+ try {
|
|
|
+ const formData = new FormData();
|
|
|
+ formData.append("file", file);
|
|
|
+ const res: any = await getUpload(formData);
|
|
|
+ const { fileUrl, mediaId, errMsg } = parseMediaUploadResult(res);
|
|
|
+
|
|
|
+ if (fileUrl || mediaId) {
|
|
|
+ uploadFileItem.status = "success";
|
|
|
+ if (fileUrl) {
|
|
|
+ uploadFileItem.url = fileUrl;
|
|
|
+ }
|
|
|
+ uploadFileItem.response = { media_id: mediaId, url: fileUrl };
|
|
|
+ const stored = fileUrl || mediaId;
|
|
|
+ if (side === "portrait") {
|
|
|
+ form.idPortraitUrl = stored;
|
|
|
+ try {
|
|
|
+ await requestIdCardPortraitOcr(file);
|
|
|
+ } catch {
|
|
|
+ clearIdPortraitOcr();
|
|
|
+ ElMessage.warning("身份证识别服务暂时不可用,请稍后重试");
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ form.idEmblemUrl = stored;
|
|
|
+ try {
|
|
|
+ await requestIdCardEmblemOcr(file);
|
|
|
+ } catch {
|
|
|
+ ElMessage.warning("身份证国徽面识别服务暂时不可用,请稍后重试");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ uploadFileItem.status = "fail";
|
|
|
+ ElMessage.error(errMsg || "上传失败,未返回可用结果");
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ uploadFileItem.status = "fail";
|
|
|
+ }
|
|
|
+ const field = side === "portrait" ? "idPortraitUrl" : "idEmblemUrl";
|
|
|
+ formRef.value?.validateField(field).catch(() => {});
|
|
|
+}
|
|
|
+
|
|
|
+function onIdCardRemove(fileList: UploadUserFile[], side: IdCardSide) {
|
|
|
+ if (!fileList.length) {
|
|
|
+ if (side === "portrait") {
|
|
|
+ form.idPortraitUrl = "";
|
|
|
+ clearIdPortraitOcr();
|
|
|
+ } else {
|
|
|
+ form.idEmblemUrl = "";
|
|
|
+ idPortraitOcr.cardPeriodBegin = "";
|
|
|
+ idPortraitOcr.cardPeriodEnd = "";
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ const first = fileList[0] as UploadUserFile & {
|
|
|
+ url?: string;
|
|
|
+ response?: { url?: string; media_id?: string; mediaId?: string };
|
|
|
+ };
|
|
|
+ const r = first?.response;
|
|
|
+ const stored = first?.url || r?.url || r?.media_id || r?.mediaId || "";
|
|
|
+ if (side === "portrait") form.idPortraitUrl = stored;
|
|
|
+ else form.idEmblemUrl = stored;
|
|
|
+ }
|
|
|
+ const field = side === "portrait" ? "idPortraitUrl" : "idEmblemUrl";
|
|
|
+ formRef.value?.validateField(field).catch(() => {});
|
|
|
+}
|
|
|
+
|
|
|
+/** 从草稿/详情接口 URL 还原身份证上传展示(可在外部拉取数据后调用) */
|
|
|
+function restoreIdCardFromUrls(portraitUrl?: string | null, emblemUrl?: string | null) {
|
|
|
+ if (portraitUrl) {
|
|
|
+ form.idPortraitUrl = portraitUrl;
|
|
|
+ form.idPortraitFileList = [{ name: "id-portrait.jpg", url: portraitUrl, status: "success" } as UploadUserFile];
|
|
|
+ }
|
|
|
+ if (emblemUrl) {
|
|
|
+ form.idEmblemUrl = emblemUrl;
|
|
|
+ form.idEmblemFileList = [{ name: "id-emblem.jpg", url: emblemUrl, status: "success" } as UploadUserFile];
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ hydrateSubjectFormFromBusinessDataCache();
|
|
|
+ const q = route.query as Record<string, string | string[] | undefined>;
|
|
|
+ const pu = typeof q.idPortraitUrl === "string" ? q.idPortraitUrl : undefined;
|
|
|
+ const eu = typeof q.idEmblemUrl === "string" ? q.idEmblemUrl : undefined;
|
|
|
+ if (pu && !form.idPortraitUrl?.trim()) {
|
|
|
+ form.legalIdType = "id_card";
|
|
|
+ restoreIdCardFromUrls(pu, undefined);
|
|
|
+ }
|
|
|
+ if (eu && !form.idEmblemUrl?.trim()) {
|
|
|
+ form.legalIdType = "id_card";
|
|
|
+ restoreIdCardFromUrls(undefined, eu);
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+defineExpose({ restoreIdCardFromUrls, hydrateSubjectFormFromBusinessDataCache });
|
|
|
+
|
|
|
+function onModifyUbo() {
|
|
|
+ ElMessage.info("受益人信息修改入口可在此对接");
|
|
|
+}
|
|
|
+
|
|
|
+function onBack() {
|
|
|
+ router.push({
|
|
|
+ path: "/businessInfo/dataEntry",
|
|
|
+ query: { ...route.query }
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function onSaveDraft() {
|
|
|
+ ElMessage.success("草稿已保存(可对接接口)");
|
|
|
+}
|
|
|
+
|
|
|
+async function onNext() {
|
|
|
+ if (!formRef.value) return;
|
|
|
+ nextLoading.value = true;
|
|
|
+ try {
|
|
|
+ await formRef.value.validate();
|
|
|
+ mergeSubjectInfoIntoBusinessDataCache();
|
|
|
+ await router.push({
|
|
|
+ path: "/businessInfo/manageInfo",
|
|
|
+ query: { ...route.query }
|
|
|
+ });
|
|
|
+ } catch {
|
|
|
+ ElMessage.warning("请完善必填信息");
|
|
|
+ } finally {
|
|
|
+ nextLoading.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+$wechat-green: #07c160;
|
|
|
+.subject-info-page {
|
|
|
+ box-sizing: border-box;
|
|
|
+ min-height: 100%;
|
|
|
+ padding: 24px 32px 40px;
|
|
|
+ margin: 0 auto;
|
|
|
+ background: #ffffff;
|
|
|
+}
|
|
|
+.page-title {
|
|
|
+ margin: 0 0 24px;
|
|
|
+ font-size: 20px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+.subject-form {
|
|
|
+ :deep(.el-form-item__label) {
|
|
|
+ font-weight: 500;
|
|
|
+ color: #606266;
|
|
|
+ }
|
|
|
+}
|
|
|
+.form-block {
|
|
|
+ padding-bottom: 8px;
|
|
|
+ margin-bottom: 28px;
|
|
|
+ border-bottom: 1px solid #ebeef5;
|
|
|
+ &:last-of-type {
|
|
|
+ border-bottom: none;
|
|
|
+ }
|
|
|
+}
|
|
|
+.section-head {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin: 0 0 16px;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+.section-bar {
|
|
|
+ display: inline-block;
|
|
|
+ flex-shrink: 0;
|
|
|
+ width: 4px;
|
|
|
+ height: 16px;
|
|
|
+ margin-right: 8px;
|
|
|
+ background: $wechat-green;
|
|
|
+ border-radius: 2px;
|
|
|
+}
|
|
|
+.section-intro {
|
|
|
+ padding-left: 20px;
|
|
|
+ font-size: 13px;
|
|
|
+ line-height: 1.65;
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+.field-stack {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+.field-tip {
|
|
|
+ margin: 8px 0 0;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1.65;
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+.label-with-icon {
|
|
|
+ display: inline-flex;
|
|
|
+ gap: 4px;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+.info-icon {
|
|
|
+ color: #909399;
|
|
|
+ cursor: help;
|
|
|
+}
|
|
|
+.license-upload-wrap {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 16px;
|
|
|
+ align-items: flex-start;
|
|
|
+}
|
|
|
+.license-example {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 6px;
|
|
|
+}
|
|
|
+.license-example-label {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+.license-example-thumb {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ width: 148px;
|
|
|
+ height: 148px;
|
|
|
+ background: #f5f7fa;
|
|
|
+ border: 1px dashed #dcdfe6;
|
|
|
+ border-radius: 6px;
|
|
|
+}
|
|
|
+.license-example-placeholder {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #c0c4cc;
|
|
|
+}
|
|
|
+.id-example-thumb {
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+.license-example-badge {
|
|
|
+ position: absolute;
|
|
|
+ top: 6px;
|
|
|
+ right: 6px;
|
|
|
+ padding: 2px 6px;
|
|
|
+ font-size: 11px;
|
|
|
+ color: #ffffff;
|
|
|
+ background: rgb(0 0 0 / 45%);
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+.ubo-footer-tip {
|
|
|
+ padding: 12px 0 0;
|
|
|
+ margin: 8px 0 0;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1.65;
|
|
|
+ color: #909399;
|
|
|
+ border-top: 1px dashed #ebeef5;
|
|
|
+}
|
|
|
+:deep(.el-form-item__content) {
|
|
|
+ display: block;
|
|
|
+}
|
|
|
+.upload-rules {
|
|
|
+ margin-top: 12px;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1.65;
|
|
|
+ color: #909399;
|
|
|
+ p {
|
|
|
+ margin: 0 0 6px;
|
|
|
+ &:last-child {
|
|
|
+ margin-bottom: 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+.ocr-result-container {
|
|
|
+ max-width: 640px;
|
|
|
+ padding: 14px 16px;
|
|
|
+ margin-top: 12px;
|
|
|
+ background: #f5f7fa;
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
+ border-radius: 6px;
|
|
|
+}
|
|
|
+.ocr-result-item {
|
|
|
+ display: flex;
|
|
|
+ gap: 12px;
|
|
|
+ align-items: flex-start;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ font-size: 13px;
|
|
|
+ line-height: 1.5;
|
|
|
+ &:last-child {
|
|
|
+ margin-bottom: 0;
|
|
|
+ }
|
|
|
+ .label {
|
|
|
+ flex-shrink: 0;
|
|
|
+ min-width: 88px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #606266;
|
|
|
+ }
|
|
|
+ .value {
|
|
|
+ flex: 1;
|
|
|
+ color: #303133;
|
|
|
+ word-break: break-all;
|
|
|
+ }
|
|
|
+}
|
|
|
+.ocr-result-tip {
|
|
|
+ font-size: 13px;
|
|
|
+ line-height: 1.5;
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+.license-ocr-result {
|
|
|
+ max-width: 640px;
|
|
|
+ padding: 14px 16px;
|
|
|
+ margin-top: 12px;
|
|
|
+ margin-left: 140px;
|
|
|
+ background: #f5f7fa;
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
+ border-radius: 6px;
|
|
|
+}
|
|
|
+.license-ocr-row {
|
|
|
+ display: flex;
|
|
|
+ gap: 12px;
|
|
|
+ align-items: flex-start;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ font-size: 13px;
|
|
|
+ line-height: 1.5;
|
|
|
+ &:last-child {
|
|
|
+ margin-bottom: 0;
|
|
|
+ }
|
|
|
+}
|
|
|
+.license-ocr-label {
|
|
|
+ flex-shrink: 0;
|
|
|
+ width: 132px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #606266;
|
|
|
+}
|
|
|
+.license-ocr-value {
|
|
|
+ flex: 1;
|
|
|
+ color: #303133;
|
|
|
+ word-break: break-all;
|
|
|
+}
|
|
|
+:deep(.el-upload--picture-card) {
|
|
|
+ --el-upload-picture-card-size: 148px;
|
|
|
+}
|
|
|
+:deep(.el-upload-list--picture-card .el-upload-list__item) {
|
|
|
+ width: 148px;
|
|
|
+ height: 148px;
|
|
|
+}
|
|
|
+.footer-actions {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding-top: 24px;
|
|
|
+ margin-top: 8px;
|
|
|
+ border-top: 1px solid #ebeef5;
|
|
|
+}
|
|
|
+.footer-center {
|
|
|
+ display: flex;
|
|
|
+ flex: 1;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+.btn-next {
|
|
|
+ min-width: 100px;
|
|
|
+ background: $wechat-green;
|
|
|
+ border-color: $wechat-green;
|
|
|
+ &:hover {
|
|
|
+ background: #06ad56;
|
|
|
+ border-color: #06ad56;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|