refactor: 拆分个人开户提交逻辑并新增确认弹窗(保存/取消)

main
halo 2026-07-23 17:15:38 +08:00
parent e49e75e7ed
commit b5cd02452b
1 changed files with 548 additions and 95 deletions

View File

@ -1,50 +1,167 @@
<!-- src/views/wallet/personal-open/PersonalOpenForm.vue --> <!-- src/views/wallet/personal-open/PersonalOpenForm.vue -->
<!-- 2.6.2 个人账户开立:选择个人客户回填资料 -> 补充手机号/职业/银行卡 -> 上传身份证 -> 提交草稿 -> <!-- 2.6.2 个人账户开立(2026-07-22 按最新原型截图改造,2026-07-23 补充 create/edit/detail 三态):
展示申请编号,提供"刷新状态"手动轮询按钮(不做自动定时轮询) --> 选择个人客户回填基础资料(只读,作为身份核验基准) -> 补充职业/银行卡/手机号 ->
上传身份证正反面(触发OCR,识别结果与所选客户信息比对,不一致弹窗阻断提交)+正面免冠照
(调用后端人脸识别接口 face-recognize,与所选客户的身份证号/姓名做匹配,识别通过才拿到可提交
的文件编号) -> 勾选同意协议 -> 提交草稿 -> 展示申请编号,提供"刷新状态"手动轮询按钮(不做
自动定时轮询)
选择客户改用与"客户管理 > 个人客户管理"页面一致的处理方式:先分页查询 /api/customer-info/personal/list
(PersonalCustomerPicker.vue),选中后再调用 /api/customer-info/personal/detail 取完整字段回填,
不使用本模块专属但字段未必对齐的 personal-opening/query-customer 接口
按产品要求(2026-07-22)本页面不需要短信验证码环节
mode(create/edit/detail) route.path 后缀推导,照抄 EnterpriseOpenForm.vue 的写法,避免
复刻阶段3 PersonalForm/EnterpriseForm 的Bugedit/detail 模式下的 list/detail/update 接口
后端尚未提供,前端已按 enterprise-open-application 四件套的假设契约实现,detail/edit 模式
下无法真正联调验证,详见缺失的后端接口.mdPart 1 §1.10 Part 2 新增条目
已知缺口(详见缺失的后端接口.md):人脸识别接口 recode/recodeInfo 取值枚举未文档化
(前端仅按"是否返回 fileNo"判断识别是否通过)协议正文/URL无真实内容职业码表为前端占位值
编辑/查看态因缺少"按 fileNo 回显图片"的接口,无法重新展示已上传的证件/人脸照片缩略图(
企业开户表单 LicenseUpload/IdCardUpload 在编辑态的现状一致,非本次新增的独立缺口) -->
<template> <template>
<div class="personal-open-form-page"> <div class="personal-open-form-page">
<a-page-header title="个人账户开立" @back="goBack" /> <a-page-header :title="pageTitle" @back="goBack">
<template v-if="isCreate && !submittedApplicationNo" #extra>
<a-space wrap>
<a-checkbox v-model:checked="agreementChecked">本人已阅读并同意</a-checkbox>
<a @click="showAgreement('service')"></a>
<a @click="showAgreement('privacy')"></a>
<a-button type="primary" :disabled="!agreementChecked" @click="handleConfirmButtonClick"></a-button>
<a-button @click="goBack"></a-button>
</a-space>
</template>
<template v-else-if="isEdit" #extra>
<a-space>
<a-button type="primary" :loading="submitting" @click="handleEditSubmit"></a-button>
<a-button @click="goBack"></a-button>
</a-space>
</template>
</a-page-header>
<a-card v-if="!applicationNo"> <a-card v-if="isDetail && (detailInfo.applicationStatus || detailInfo.failReason)" style="margin-bottom: 16px">
<a-form ref="formRef" layout="vertical" :model="form" :rules="rules"> <a-descriptions :column="2">
<a-form-item label="所属渠道" required> <a-descriptions-item label="申请状态">{{ statusText }}</a-descriptions-item>
<ChannelSelect v-model:model-value="channelNo" /> <a-descriptions-item label="失败原因">{{ detailInfo.failReason || '-' }}</a-descriptions-item>
<a-descriptions-item label="主账户账号">{{ detailInfo.mainAccountNo || '-' }}</a-descriptions-item>
<a-descriptions-item label="客户编号">{{ detailInfo.customerNo || '-' }}</a-descriptions-item>
</a-descriptions>
<a-button style="margin-top: 12px" :loading="polling" @click="handleQueryProgress"></a-button>
</a-card>
<a-card v-if="!(isCreate && submittedApplicationNo)">
<div class="section-title">基础信息<span v-if="!isDetail" class="required-mark">*</span></div>
<a-form
ref="formRef"
layout="horizontal"
:label-col="{ style: { width: '130px' } }"
:wrapper-col="{ style: { flex: '1 1 auto', maxWidth: '420px' } }"
:model="form"
:rules="rules"
:disabled="isDetail"
class="open-form"
>
<a-form-item label="渠道归属" required>
<ChannelSelect v-model:model-value="channelNo" placeholder="请选择核心企业" style="width: 300px" />
</a-form-item> </a-form-item>
<a-form-item label="账户名称" required> <a-form-item label="客户" required>
<a-space> <a-space>
<span v-if="form.customerName">{{ form.customerName }}</span> <a-input :value="form.customerName" placeholder="请选择客户" disabled style="width: 200px" />
<a-button @click="openPicker"></a-button> <a-button v-if="!isDetail" @click="openPicker">{{ form.customerId ? '' : '' }}</a-button>
</a-space> </a-space>
</a-form-item> </a-form-item>
<a-form-item label="证件号码"><a-input :value="form.certificateNumber" disabled /></a-form-item> <a-form-item label="身份证号" required>
<a-form-item label="签发机关"><a-input :value="form.issuingAuthority" disabled /></a-form-item> <a-input :value="form.certificateNumber" disabled placeholder="请输入身份证号" />
<a-form-item label="证件地址"><a-input :value="form.certificateAddress" disabled /></a-form-item>
<a-form-item label="手机号码" name="mobilePhone">
<a-input v-model:value="form.mobilePhone" placeholder="请输入11位手机号" />
</a-form-item> </a-form-item>
<a-form-item label="职业" name="occupation"> <a-form-item label="证件生效日期" required>
<a-input v-model:value="form.occupation" placeholder="请输入职业" /> <a-date-picker :value="effectiveDateValue" disabled style="width: 100%" placeholder="请选择日期" />
</a-form-item> </a-form-item>
<a-form-item label="绑定银行账号" name="bankCardNumber"> <a-form-item label="证件失效日期" required>
<a-input v-model:value="form.bankCardNumber" placeholder="请输入银行卡号" /> <a-space>
</a-form-item> <a-date-picker :value="expiryDateValue" disabled style="width: 200px" placeholder="请选择日期" />
<a-form-item label="开户行号"><a-input v-model:value="form.bankNo" placeholder="请输入开户行号(选填)" /></a-form-item> <a-checkbox :checked="isLongTerm" disabled>长期</a-checkbox>
<a-form-item label="开户行名称"><a-input v-model:value="form.bankName" placeholder="请输入开户行全称(选填)" /></a-form-item>
<a-form-item label="身份证照片" required>
<a-space size="large">
<IdCardUpload side="front" :channel-no="channelNo" v-model:file-no="idCardFrontFileNo" />
<IdCardUpload side="back" :channel-no="channelNo" v-model:file-no="idCardBackFileNo" />
</a-space> </a-space>
</a-form-item> </a-form-item>
<a-form-item label="签发机关名称" required>
<a-input :value="form.issuingAuthority" disabled placeholder="请输入签发机关名称" />
</a-form-item>
<a-form-item label="证件地址" required>
<a-input :value="form.certificateAddress" disabled placeholder="请输入证件地址或常住地址" />
</a-form-item>
<a-form-item label="性别">
<a-radio-group :value="form.gender" disabled>
<a-radio value="MALE">男性</a-radio>
<a-radio value="FEMALE">女性</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="民族">
<a-select :value="form.ethnic" disabled :options="ethnicOptions" style="width: 200px" placeholder="-" />
</a-form-item>
<a-form-item label="职业" name="occupationType" required>
<a-select v-model:value="form.occupationType" placeholder="请选择职业" style="width: 300px" allow-clear>
<a-select-option v-for="opt in OCCUPATION_TYPE_OPTIONS" :key="opt.value" :value="opt.value">
{{ opt.label }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-if="form.occupationType === 'OTHER'" label="职业备注" name="jobNote" required>
<a-input v-model:value="form.jobNote" placeholder="职业为“其他”时请填写备注" style="width: 300px" />
</a-form-item>
<a-form-item label="证件照片" :required="isCreate">
<a-space size="large" align="start">
<div>
<div class="upload-label">身份证人像面</div>
<IdCardUpload
side="front"
:channel-no="channelNo"
:readonly="isDetail"
v-model:file-no="idCardFrontFileNo"
v-model:uploading="idCardFrontUploading"
@ocr-result="handleFrontOcrResult"
@ocr-status="(s) => (frontOcrStatus = s)"
/>
</div>
<div>
<div class="upload-label">身份证国徽面</div>
<IdCardUpload
side="back"
:channel-no="channelNo"
:readonly="isDetail"
v-model:file-no="idCardBackFileNo"
v-model:uploading="idCardBackUploading"
@ocr-result="handleBackOcrResult"
@ocr-status="(s) => (backOcrStatus = s)"
/>
</div>
<div>
<div class="upload-label">正面免冠照</div>
<FacePhotoUpload
:key="form.customerId"
:channel-no="channelNo"
:id-no="form.certificateNumber"
:name="form.customerName"
:readonly="isDetail"
v-model:file-no="facePhotoFileNo"
v-model:uploading="facePhotoUploading"
/>
</div>
</a-space>
<div v-if="!isDetail" class="id-card-hint">*, 线</div>
<div v-if="identityMismatch" class="id-card-hint">
身份证信息与所选客户信息不一致,请核实后重新上传身份证或重新选择客户,当前无法提交
</div>
</a-form-item>
<a-form-item label="银行卡号" name="bankCardNumber" required>
<a-input v-model:value="form.bankCardNumber" placeholder="请输入银行卡号" style="width: 300px" />
</a-form-item>
<a-form-item label="手机号" name="mobilePhone" required>
<a-input v-model:value="form.mobilePhone" placeholder="请输入手机号" style="width: 300px" />
</a-form-item>
</a-form> </a-form>
<a-space>
<a-button type="primary" :loading="submitting" @click="handleSubmit"></a-button>
<a-button @click="goBack"></a-button>
</a-space>
</a-card> </a-card>
<a-card v-else> <a-card v-else>
<a-result status="success" title="开户申请已提交" :sub-title="`申请编号:${applicationNo}`"> <a-result status="success" title="开户申请已提交" :sub-title="`申请编号:${submittedApplicationNo}`">
<template #extra> <template #extra>
<a-space direction="vertical" style="width: 100%"> <a-space direction="vertical" style="width: 100%">
<div v-if="progress.applicationStatus">:{{ progressStatusText }}</div> <div v-if="progress.applicationStatus">:{{ progressStatusText }}</div>
@ -58,30 +175,103 @@
</a-result> </a-result>
</a-card> </a-card>
<WalletCustomerPicker ref="pickerRef" mode="personal" @select="handleSelectCustomer" /> <a-modal
v-model:open="confirmModalVisible"
title="提示"
:footer="null"
:mask-closable="false"
@cancel="handleModalCancel"
>
<template v-if="confirmModalStep === 'confirm'">
<p>是否确认开户?</p>
<div class="confirm-modal-actions">
<a-space>
<a-button type="primary" :loading="modalSaving" :disabled="modalInviting" @click="handleModalSave"></a-button>
<a-button :disabled="modalSaving || modalInviting" @click="handleModalCancel"></a-button>
<a-button :loading="modalInviting" :disabled="modalSaving" @click="handleModalInvite"></a-button>
</a-space>
</div>
</template>
<template v-else>
<div class="qrcode-panel">
<qrcode-vue :value="qrCodeUrl" :size="200" level="M" />
<p class="qrcode-application-no">申请编号:{{ qrApplicationNo }}</p>
<a-button @click="handleModalCancel"></a-button>
</div>
</template>
</a-modal>
<PersonalCustomerPicker ref="pickerRef" @select="handleSelectCustomer" />
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, reactive, computed } from 'vue' import { ref, reactive, computed, watch, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { message } from 'ant-design-vue' import { message, Modal } from 'ant-design-vue'
import dayjs from 'dayjs'
import ChannelSelect from '@/components/ChannelSelect.vue' import ChannelSelect from '@/components/ChannelSelect.vue'
import IdCardUpload from '@/components/IdCardUpload.vue' import IdCardUpload from '@/components/IdCardUpload.vue'
import WalletCustomerPicker from '@/components/WalletCustomerPicker.vue' import FacePhotoUpload from '@/components/FacePhotoUpload.vue'
import { createPersonalOpenDraftApi, queryPersonalOpenProgressApi } from '@/api/wallet' import PersonalCustomerPicker from '@/components/PersonalCustomerPicker.vue'
import {
createPersonalOpenDraftApi,
queryPersonalOpenProgressApi,
fetchPersonalOpenDetailApi,
updatePersonalOpenApplicationApi
} from '@/api/wallet'
import { fetchPersonalCustomerDetailApi } from '@/api/customer'
import { phoneValidatorRule } from '@/utils/validators' import { phoneValidatorRule } from '@/utils/validators'
import { showErrorModal } from '@/utils/errorModal'
import { mapOcrGender } from '@/utils/ocrMapping'
import { OCCUPATION_TYPE_OPTIONS, CERTIFICATE_LONG_TERM_DATE } from '@/constants/customerEnums'
const route = useRoute()
const router = useRouter() const router = useRouter()
const formRef = ref(null) const formRef = ref(null)
const pickerRef = ref(null) const pickerRef = ref(null)
const channelNo = ref(undefined) const channelNo = ref(undefined)
const agreementChecked = ref(false)
const mode = computed(() => {
if (route.path.endsWith('/edit')) return 'edit'
if (route.path.endsWith('/detail')) return 'detail'
return 'create'
})
const isCreate = computed(() => mode.value === 'create')
const isEdit = computed(() => mode.value === 'edit')
const isDetail = computed(() => mode.value === 'detail')
const pageTitle = computed(
() => ({ create: '新增个人开户申请', edit: '编辑个人开户申请', detail: '查看个人开户申请' }[mode.value])
)
const idCardFrontFileNo = ref('') const idCardFrontFileNo = ref('')
const idCardBackFileNo = ref('') const idCardBackFileNo = ref('')
const facePhotoFileNo = ref('')
const idCardFrontUploading = ref(false)
const idCardBackUploading = ref(false)
const facePhotoUploading = ref(false)
const frontOcrStatus = ref('PENDING')
const backOcrStatus = ref('PENDING')
// / OCR ,,
const frontOcrData = ref(null)
const backOcrData = ref(null)
const identityMismatch = ref(false)
const submitting = ref(false) const submitting = ref(false)
const applicationNo = ref('') // create ""使, edit/detail route.query.applicationNo
const submittedApplicationNo = ref('')
// "":confirm=+,qrcode=
const confirmModalVisible = ref(false)
const confirmModalStep = ref('confirm')
const modalSaving = ref(false)
const modalInviting = ref(false)
const qrCodeUrl = ref('')
const qrApplicationNo = ref('')
const polling = ref(false) const polling = ref(false)
const progress = reactive({ applicationStatus: '', failReason: '' }) const progress = reactive({ applicationStatus: '', failReason: '' })
const detailInfo = reactive({ applicationStatus: '', failReason: '', mainAccountNo: '', customerNo: '' })
const form = reactive({ const form = reactive({
customerId: undefined, customerId: undefined,
@ -94,17 +284,36 @@ const form = reactive({
certificateAddress: '', certificateAddress: '',
gender: undefined, gender: undefined,
ethnic: '', ethnic: '',
occupation: '', occupationType: undefined,
jobNote: '',
mobilePhone: '', mobilePhone: '',
bankCardNumber: '', bankCardNumber: ''
bankNo: '',
bankName: ''
}) })
const effectiveDateValue = computed(() => (form.certificateEffectiveDate ? dayjs(form.certificateEffectiveDate) : undefined))
const isLongTerm = computed(
() => !!form.certificateExpiryDate && String(form.certificateExpiryDate).startsWith(CERTIFICATE_LONG_TERM_DATE.slice(0, 10))
)
const expiryDateValue = computed(() =>
!isLongTerm.value && form.certificateExpiryDate ? dayjs(form.certificateExpiryDate) : undefined
)
// ,(56),
const ethnicOptions = computed(() => (form.ethnic ? [{ value: form.ethnic, label: form.ethnic }] : []))
const rules = { const rules = {
mobilePhone: [phoneValidatorRule()], occupationType: [{ required: true, message: '请选择职业' }],
occupation: [{ required: true, message: '请输入职业' }], jobNote: [
bankCardNumber: [{ required: true, message: '请输入银行卡号' }] {
validator: (_rule, value) => {
if (form.occupationType === 'OTHER' && !value) {
return Promise.reject(new Error('职业为“其他”时请填写职业备注'))
}
return Promise.resolve()
}
}
],
bankCardNumber: [{ required: true, message: '请输入银行卡号' }],
mobilePhone: [phoneValidatorRule()]
} }
const progressStatusMap = { const progressStatusMap = {
@ -115,76 +324,248 @@ const progressStatusMap = {
APPLY_FAILED: '申请失败' APPLY_FAILED: '申请失败'
} }
const progressStatusText = computed(() => progressStatusMap[progress.applicationStatus] || progress.applicationStatus) const progressStatusText = computed(() => progressStatusMap[progress.applicationStatus] || progress.applicationStatus)
const statusText = computed(() => progressStatusMap[detailInfo.applicationStatus] || detailInfo.applicationStatus)
// :/URL,,.md
const AGREEMENT_CONTENT = {
service: {
title: '浙江稠州商业银行用户支付服务协议',
content: '协议正文内容待产品/法务提供,当前为占位说明,不代表最终协议条款。'
},
privacy: {
title: '浙江稠州商业银行用户支付服务隐私政策',
content: '隐私政策正文内容待产品/法务提供,当前为占位说明,不代表最终政策条款。'
}
}
function showAgreement(type) {
const info = AGREEMENT_CONTENT[type]
Modal.info({ title: info.title, content: info.content, okText: '我知道了' })
}
function openPicker() { function openPicker() {
if (!channelNo.value) { if (!channelNo.value) {
message.warning('请先选择所属渠道') showErrorModal('请先选择渠道归属')
return return
} }
pickerRef.value.show() pickerRef.value.show()
} }
function handleSelectCustomer(record) { // OCR (///),
form.customerId = record.id function runIdentityComparison() {
form.customerName = record.customerName if (!form.customerId) return
form.certificateType = record.certificateType || 'ID_CARD' const mismatches = []
form.certificateNumber = record.certificateNumber const front = frontOcrData.value
form.certificateEffectiveDate = record.certificateEffectiveDate if (front) {
form.certificateExpiryDate = record.certificateExpiryDate if (front.idcard && form.certificateNumber && front.idcard !== form.certificateNumber) mismatches.push('身份证号')
form.issuingAuthority = record.issuingAuthority if (front.name && form.customerName && front.name !== form.customerName) mismatches.push('姓名')
form.certificateAddress = record.certificateAddress const ocrGender = mapOcrGender(front.sex)
form.gender = record.gender if (ocrGender && form.gender && ocrGender !== form.gender) mismatches.push('性别')
form.ethnic = record.ethnicity if (front.nation && form.ethnic && front.nation !== form.ethnic) mismatches.push('民族')
form.occupation = record.occupation || '' }
form.mobilePhone = record.mobilePhone || '' if (mismatches.length > 0) {
identityMismatch.value = true
showErrorModal(
`身份证识别信息与所选客户档案不一致:${mismatches.join('、')},请核实后重新上传身份证或重新选择客户`,
'身份信息核验失败'
)
} else {
identityMismatch.value = false
}
} }
async function handleSubmit() { function handleFrontOcrResult(data) {
if (!channelNo.value) { frontOcrData.value = data
message.warning('请先选择所属渠道') runIdentityComparison()
}
function handleBackOcrResult(data) {
backOcrData.value = data
}
watch(idCardFrontFileNo, (val) => {
if (!val) {
frontOcrData.value = null
runIdentityComparison()
}
})
watch(idCardBackFileNo, (val) => {
if (!val) backOcrData.value = null
})
async function handleSelectCustomer(record) {
// " > ":,
// /////
const res = await fetchPersonalCustomerDetailApi({ id: record.id })
if (res.data.code !== 200) {
showErrorModal(res.data.message || '获取客户详情失败')
return return
} }
const detail = res.data.data
form.customerId = detail.id
form.customerName = detail.customerName || ''
form.certificateType = detail.certificateType || 'ID_CARD'
form.certificateNumber = detail.certificateNumber
form.certificateEffectiveDate = detail.certificateEffectiveDate
form.certificateExpiryDate = detail.certificateExpiryDate
form.issuingAuthority = detail.issuingAuthority
form.certificateAddress = detail.certificateAddress
form.gender = detail.gender
form.ethnic = detail.ethnicity
form.mobilePhone = detail.mobilePhone || form.mobilePhone
// occupation ,,
// (/,.md)
form.occupationType = OCCUPATION_TYPE_OPTIONS.some((opt) => opt.value === detail.occupation)
? detail.occupation
: undefined
form.jobNote = ''
// /,, OCR
runIdentityComparison()
// (fileNo)"" idNo/name ,,
// /(FacePhotoUpload :key="form.customerId" )
facePhotoFileNo.value = ''
}
// fileType :01/02/04, create-draft
function applyFileList(fileList) {
const files = fileList || []
idCardFrontFileNo.value = files.find((f) => f.fileType === '01')?.fileNo || ''
idCardBackFileNo.value = files.find((f) => f.fileType === '02')?.fileNo || ''
facePhotoFileNo.value = files.find((f) => f.fileType === '04')?.fileNo || ''
}
// edit/detail :personal-opening/detail ,
// create-draft ,.mdPart 2
async function loadDetail() {
const res = await fetchPersonalOpenDetailApi({ applicationNo: route.query.applicationNo })
if (res.data.code !== 200) {
showErrorModal(res.data.message || '获取申请详情失败')
return
}
const data = res.data.data || {}
channelNo.value = data.channelNo
Object.assign(form, {
customerId: data.customerId,
customerName: data.customerName,
certificateType: data.certificateType || 'ID_CARD',
certificateNumber: data.certificateNumber,
certificateEffectiveDate: data.certificateEffectiveDate,
certificateExpiryDate: data.certificateExpiryDate,
issuingAuthority: data.issuingAuthority,
certificateAddress: data.certificateAddress,
gender: data.gender,
ethnic: data.ethnic,
occupationType: data.occupation,
jobNote: data.jobNote || '',
mobilePhone: data.mobilePhone,
bankCardNumber: data.bankCardNumber
})
applyFileList(data.fileList)
detailInfo.applicationStatus = data.applicationStatus
detailInfo.failReason = data.failReason
detailInfo.mainAccountNo = data.mainAccountNo
detailInfo.customerNo = data.customerNo
}
onMounted(() => {
if (!isCreate.value && route.query.applicationNo) {
loadDetail()
}
})
// ://///
// ""(create ,)""
async function validateBeforeSubmit() {
if (isCreate.value && !agreementChecked.value) {
showErrorModal('请先阅读并勾选同意用户支付服务协议与隐私政策')
return false
}
if (!channelNo.value) {
showErrorModal('请先选择渠道归属')
return false
}
if (!form.customerId) { if (!form.customerId) {
message.warning('请先选择账户客户') showErrorModal('请先选择客户')
return return false
} }
if (!idCardFrontFileNo.value || !idCardBackFileNo.value) { if (idCardFrontUploading.value || idCardBackUploading.value || facePhotoUploading.value) {
message.warning('请上传身份证正反面照片') showErrorModal('证件照片正在上传中,请稍候')
return return false
}
// , loadDetail fileNo ,
if (isCreate.value && (!idCardFrontFileNo.value || !idCardBackFileNo.value || !facePhotoFileNo.value)) {
showErrorModal('请上传身份证人像面、国徽面和正面免冠照')
return false
}
if (identityMismatch.value) {
showErrorModal('身份证信息与所选客户信息不一致,请核实后重新上传身份证或重新选择客户')
return false
} }
try { try {
await formRef.value.validate() await formRef.value.validate()
return true
} catch { } catch {
return return false
} }
}
// create-draft/update payload ,create/edit ,
// (update accountOpenApplicationFileBtoList,)
function buildSubmitPayload() {
const fileList = [
{ fileNo: idCardFrontFileNo.value, fileType: '01' },
{ fileNo: idCardBackFileNo.value, fileType: '02' },
{ fileNo: facePhotoFileNo.value, fileType: '04' }
]
const payload = {
accountProperty: '1',
channelNo: channelNo.value,
customerId: form.customerId,
customerName: form.customerName,
certificateType: form.certificateType,
certificateNumber: form.certificateNumber,
certificateEffectiveDate: form.certificateEffectiveDate,
certificateExpiryDate: form.certificateExpiryDate,
issuingAuthority: form.issuingAuthority,
certificateAddress: form.certificateAddress,
gender: form.gender,
ethnic: form.ethnic,
occupation: form.occupationType,
jobNote: form.occupationType === 'OTHER' ? form.jobNote : undefined,
mobilePhone: form.mobilePhone,
bankCardNumber: form.bankCardNumber
}
return { payload, fileList }
}
// 稿(create-draft), applicationNo , null
// src/api/request.js ,
async function submitCreateDraft() {
const { payload, fileList } = buildSubmitPayload()
const res = await createPersonalOpenDraftApi({ ...payload, fileList })
if (res.data.code === 200) {
submittedApplicationNo.value = res.data.data.applicationNo
return res.data.data.applicationNo
}
return null
}
// "": update , handleSubmit
async function handleEditSubmit() {
const valid = await validateBeforeSubmit()
if (!valid) return
submitting.value = true submitting.value = true
try { try {
const res = await createPersonalOpenDraftApi({ const { payload, fileList } = buildSubmitPayload()
accountProperty: '1', // :personal-opening/update , applicationNo ,
channelNo: channelNo.value, // .mdPart 2 ; create-draft ,
customerId: form.customerId, // accountOpenApplicationFileBtoList
customerName: form.customerName, const res = await updatePersonalOpenApplicationApi({
certificateType: form.certificateType, applicationNo: route.query.applicationNo,
certificateNumber: form.certificateNumber, ...payload,
certificateEffectiveDate: form.certificateEffectiveDate, accountOpenApplicationFileBtoList: fileList
certificateExpiryDate: form.certificateExpiryDate,
issuingAuthority: form.issuingAuthority,
certificateAddress: form.certificateAddress,
gender: form.gender,
ethnic: form.ethnic,
occupation: form.occupation,
mobilePhone: form.mobilePhone,
bankCardNumber: form.bankCardNumber,
bankNo: form.bankNo,
bankName: form.bankName,
fileList: [
{ fileNo: idCardFrontFileNo.value, fileType: '01' },
{ fileNo: idCardBackFileNo.value, fileType: '02' }
]
}) })
if (res.data.code === 200) { if (res.data.code === 200) {
applicationNo.value = res.data.data.applicationNo message.success('修改成功')
message.success('申请已提交') goBack()
} }
} finally { } finally {
submitting.value = false submitting.value = false
@ -194,16 +575,88 @@ async function handleSubmit() {
async function handleQueryProgress() { async function handleQueryProgress() {
polling.value = true polling.value = true
try { try {
const res = await queryPersonalOpenProgressApi({ applicationNo: applicationNo.value }) const res = await queryPersonalOpenProgressApi({ applicationNo: submittedApplicationNo.value || route.query.applicationNo })
if (res.data.code === 200) { if (res.data.code === 200) {
Object.assign(progress, res.data.data) if (isDetail.value) {
Object.assign(detailInfo, {
applicationStatus: res.data.data.applicationStatus,
failReason: res.data.data.failReason,
mainAccountNo: res.data.data.mainAccountNo,
customerNo: res.data.data.customerNo
})
} else {
Object.assign(progress, res.data.data)
}
} }
} finally { } finally {
polling.value = false polling.value = false
} }
} }
// "":,
async function handleConfirmButtonClick() {
const valid = await validateBeforeSubmit()
if (!valid) return
confirmModalStep.value = 'confirm'
confirmModalVisible.value = true
}
// "":,""
async function handleModalSave() {
modalSaving.value = true
try {
const applicationNo = await submitCreateDraft()
if (applicationNo) {
message.success('申请已提交')
confirmModalVisible.value = false
}
} finally {
modalSaving.value = false
}
}
// ""//"":,
function handleModalCancel() {
confirmModalVisible.value = false
}
function goBack() { function goBack() {
router.push('/wallet/account') router.push('/wallet/personal-open')
} }
</script> </script>
<style scoped>
.section-title {
font-size: 15px;
font-weight: 600;
margin-bottom: 16px;
}
.required-mark {
color: #ff4d4f;
margin-left: 2px;
}
.open-form {
max-width: 680px;
}
.upload-label {
margin-bottom: 8px;
font-size: 12px;
color: #666;
}
.id-card-hint {
color: #ff4d4f;
font-size: 12px;
margin-top: 8px;
}
.confirm-modal-actions {
margin-top: 24px;
text-align: right;
}
.qrcode-panel {
text-align: center;
}
.qrcode-application-no {
margin: 12px 0 8px;
color: #333;
}
</style>