企业开户申请修改

main
halo 2026-08-08 12:33:13 +08:00
parent 982b8723a4
commit 1ce3a6bc8c
3 changed files with 106 additions and 95 deletions

View File

@ -4,6 +4,7 @@
:value="modelValue"
:options="options"
:loading="loading"
:disabled="disabled"
show-search
option-filter-prop="label"
:placeholder="placeholder"
@ -20,7 +21,10 @@ import { fetchCoreEnterprisePageApi } from '@/api/baseConfig'
defineProps({
modelValue: { type: String, default: undefined },
// ""(""),
placeholder: { type: String, default: '请选择所属渠道' }
placeholder: { type: String, default: '请选择所属渠道' },
// disabled, a-form (,
// "")
disabled: { type: Boolean, default: false }
})
defineEmits(['update:modelValue'])

View File

@ -139,6 +139,22 @@ function decorate(node, parentId) {
const displayData = computed(() => rawData.value.map((node) => decorate(node, null)))
// (dictTypeCode),(itemLabel),
// (/),,.mdPart 23
function flattenTree(nodes) {
const result = []
const walk = (list) => {
list.forEach((node) => {
result.push(node)
if (node.children && node.children.length) walk(node.children)
})
}
walk(nodes)
return result
}
const allItems = computed(() => flattenTree(displayData.value))
async function loadData() {
if (!dictCode.value) return
loading.value = true
@ -174,9 +190,23 @@ const form = reactive({
remark: ''
})
// ,;(),
// .mdPart 23 133
async function validateItemLabelUnique(_rule, value) {
if (!value) return Promise.resolve()
const duplicated = allItems.value.some((item) => item.itemLabel === value && item.id !== form.id)
if (duplicated) {
return Promise.reject(new Error('同一动态字典下已存在相同的字典项标签,请修改'))
}
return Promise.resolve()
}
const rules = {
itemValue: [{ required: true, message: '请输入字典项值' }],
itemLabel: [{ required: true, message: '请输入字典项标签' }]
itemLabel: [
{ required: true, message: '请输入字典项标签' },
{ validator: validateItemLabelUnique, trigger: 'change' }
]
}
const modalTitle = computed(() => {

View File

@ -1,13 +1,16 @@
<!-- src/views/wallet/enterprise-open/EnterpriseOpenForm.vue -->
<!-- 2.6.3 企业账户开立申请:基础信息+法定代表人+(负责人/经办人/实际控制人)+受益人信息(多条)+
影像资料+短信认证,create/edit/detail 三态2026-07-28 按最新原型截图改版,详见
影像资料+联系手机号,create/edit/detail 三态2026-07-28 按最新原型截图改版,详见
docs/superpowers/specs/2026-07-28-enterprise-open-application-redesign.md
mode route.path 后缀推导(不用 route.query.mode),避免复刻阶段3 PersonalForm/EnterpriseForm 的Bug
已知缺口(仍完整实现,不在本次修复范围):EnterpriseAccountOpenApplicationDetailVo id 字段,
update 接口却要求 id;编辑模式下提交时 id route.query.applicationNo 兜底,以实测报错为准
本次改版新增的缺口已登记缺失的后端接口.md:证件类型无"个体工商户"枚举值居住地省市无法
自动回填"查询受益人"无接口开户行联行号字典缺失受益人认定类型"实际控制" actCtrlCpny
枚举语义映射待确认受益人材料证明影像 fileType 编码待确认create/update verifyCode 字段 -->
枚举语义映射待确认受益人材料证明影像 fileType 编码待确认
2026-08-08 按需求调整:①受益人信息中"是/否"语义字段(actCtrl/priBenfit/seniorMgr/seniorMgrCn/
actConUpperMarket)提交时改用 'Y'/'N' 而非 '是'/'否';②去除短信验证码校验与发送功能,仅保留
手机号采集字段;③未选择"渠道归属"时冻结页面其余输入项,防止在渠道未确定前录入业务数据 -->
<template>
<div class="enterprise-open-form-page">
<a-spin :spinning="busy" :tip="busyTip">
@ -42,11 +45,16 @@
<a-card style="margin-bottom: 16px">
<div class="section-title">基础信息<span class="required-mark">*</span></div>
<a-form layout="vertical" :model="form" :disabled="isDetail">
<a-form layout="vertical" :model="form" :disabled="fieldsDisabled">
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="渠道归属" required>
<ChannelSelect v-model:model-value="channelNo" placeholder="请选择核心企业" style="width: 100%" />
<ChannelSelect
v-model:model-value="channelNo"
placeholder="请选择核心企业"
style="width: 100%"
:disabled="isDetail"
/>
</a-form-item>
</a-col>
<a-col :span="8">
@ -54,7 +62,9 @@
<a-space>
<span v-if="form.enterpriseName">{{ form.enterpriseName }}</span>
<span v-else class="placeholder-text">请选择客户</span>
<a-button v-if="!isDetail" @click="openEnterprisePicker">{{ form.enterpriseName ? '' : '' }}</a-button>
<a-button v-if="!isDetail" :disabled="!channelNo" @click="openEnterprisePicker">
{{ form.enterpriseName ? '重新选择' : '选择' }}
</a-button>
</a-space>
</a-form-item>
</a-col>
@ -70,7 +80,7 @@
<a-form-item label="营业执照/统一社会信用代码" required>
<a-space>
<a-input :value="form.businessLicense" disabled placeholder="请输入18位营业执照编号" style="width: 220px" />
<a-button :disabled="!form.enterpriseName" @click="handleQueryBeneficiary"></a-button>
<a-button :disabled="!channelNo || !form.enterpriseName" @click="handleQueryBeneficiary"></a-button>
</a-space>
</a-form-item>
</a-col>
@ -94,7 +104,7 @@
</a-col>
<a-col :span="8">
<a-form-item label="居住地省市" required>
<RegionCascader v-model:value="form.regionCode" :disabled="isDetail" @update:text="(val) => (form.regionText = val)" />
<RegionCascader v-model:value="form.regionCode" :disabled="fieldsDisabled" @update:text="(val) => (form.regionText = val)" />
</a-form-item>
</a-col>
<a-col :span="8">
@ -112,7 +122,7 @@
<BankSelector
v-model:bank-no="form.bankNo"
v-model:bank-name="form.bankName"
:disabled="isDetail"
:disabled="fieldsDisabled"
:default-tab="isCreate ? 'quick' : 'manual'"
/>
</a-form-item>
@ -128,7 +138,7 @@
v-model:model-value="form.industry"
dict-type-code="industry_type"
placeholder="请选择所属行业"
:disabled="isDetail"
:disabled="fieldsDisabled"
/>
</a-form-item>
</a-col>
@ -144,7 +154,9 @@
<a-space>
<span v-if="legalRep.customerName">{{ legalRep.customerName }}</span>
<span v-else class="placeholder-text">请选择客户</span>
<a-button v-if="!isDetail" @click="openLegalRepPicker">{{ legalRep.customerName ? '' : '' }}</a-button>
<a-button v-if="!isDetail" :disabled="!channelNo" @click="openLegalRepPicker">
{{ legalRep.customerName ? '重新选择' : '选择' }}
</a-button>
</a-space>
<div class="hint-text">*请先在个人客户管理中维护</div>
</a-space>
@ -168,7 +180,7 @@
<a-card style="margin-bottom: 16px">
<a-space>
<a-switch v-model:checked="sameParty" :disabled="isDetail" />
<a-switch v-model:checked="sameParty" :disabled="fieldsDisabled" />
<span>法定代表人负责人经办人均为同一人</span>
</a-space>
</a-card>
@ -177,7 +189,7 @@
<a-card style="margin-bottom: 16px">
<div class="role-card-header">
<span class="role-card-title">实际控制人</span>
<a-button v-if="!isDetail" @click="openPersonPicker('controller')"></a-button>
<a-button v-if="!isDetail" :disabled="!channelNo" @click="openPersonPicker('controller')"></a-button>
</div>
<a-row :gutter="16">
<a-col :span="8">
@ -213,7 +225,7 @@
<a-card style="margin-bottom: 16px">
<div class="role-card-header">
<span class="role-card-title">负责人</span>
<a-button v-if="!isDetail" @click="openPersonPicker('leader')"></a-button>
<a-button v-if="!isDetail" :disabled="!channelNo" @click="openPersonPicker('leader')"></a-button>
</div>
<a-row :gutter="16">
<a-col :span="8">
@ -233,7 +245,7 @@
</a-col>
<a-col :span="8">
<a-form-item label="手机号">
<a-input v-model:value="leader.mobile" :disabled="isDetail" placeholder="请输入手机号码" />
<a-input v-model:value="leader.mobile" :disabled="fieldsDisabled" placeholder="请输入手机号码" />
</a-form-item>
</a-col>
<a-col :span="8">
@ -249,7 +261,7 @@
<a-card style="margin-bottom: 16px">
<div class="role-card-header">
<span class="role-card-title">经办人</span>
<a-button v-if="!isDetail" @click="openPersonPicker('operator')"></a-button>
<a-button v-if="!isDetail" :disabled="!channelNo" @click="openPersonPicker('operator')"></a-button>
</div>
<a-row :gutter="16">
<a-col :span="8">
@ -269,7 +281,7 @@
</a-col>
<a-col :span="8">
<a-form-item label="手机号">
<a-input v-model:value="operator.mobile" :disabled="isDetail" placeholder="请输入手机号码" />
<a-input v-model:value="operator.mobile" :disabled="fieldsDisabled" placeholder="请输入手机号码" />
</a-form-item>
</a-col>
<a-col :span="8">
@ -347,30 +359,20 @@
:record="bene"
:index="index"
:removable="index !== 0"
:readonly="isDetail"
:readonly="fieldsDisabled"
@remove="removeBeneficiary"
/>
<a-button v-if="!isDetail" type="dashed" block @click="openAddBeneficiary">
<a-button v-if="!isDetail" type="dashed" block :disabled="!channelNo" @click="openAddBeneficiary">
<PlusOutlined /> 添加受益人
</a-button>
</a-card>
<a-card title="短信认证" style="margin-bottom: 16px">
<a-card title="手机号" style="margin-bottom: 16px">
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="手机号" required>
<a-input v-model:value="smsForm.mobile" :disabled="isDetail" placeholder="请输入手机号" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="验证码" required>
<a-space>
<a-input v-model:value="smsForm.verifyCode" :disabled="isDetail" placeholder="请输入验证码" style="width: 180px" />
<a-button v-if="!isDetail" :disabled="smsCountdown > 0" :loading="smsSending" @click="handleSendSms">
{{ smsCountdown > 0 ? `${smsCountdown}秒后重新发送` : '发送' }}
</a-button>
</a-space>
<a-input v-model:value="mobileForm.mobile" :disabled="fieldsDisabled" placeholder="请输入手机号" />
</a-form-item>
</a-col>
</a-row>
@ -413,7 +415,7 @@
</template>
<script setup>
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import { ref, reactive, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { message, Modal } from 'ant-design-vue'
import { PlusOutlined } from '@ant-design/icons-vue'
@ -434,7 +436,6 @@ import {
fetchEnterpriseOpenDetailApi,
updateEnterpriseOpenApplicationApi
} from '@/api/wallet'
import { sendSmsCodeApi } from '@/api/auth'
import { showErrorModal } from '@/utils/errorModal'
import { isValidPhone } from '@/utils/validators'
import { CERTIFICATE_LONG_TERM_DATE } from '@/constants/customerEnums'
@ -453,6 +454,9 @@ const isDetail = computed(() => mode.value === 'detail')
const pageTitle = computed(() => ({ create: '新增企业开户', edit: '编辑企业开户', detail: '查看企业开户' }[mode.value]))
const channelNo = ref(undefined)
// ,(, ChannelSelect.vue
// disabled prop ),
const fieldsDisabled = computed(() => isDetail.value || !channelNo.value)
const agreementChecked = ref(false)
const submitting = ref(false)
@ -584,10 +588,9 @@ function buildBeneficiaryFromDetail(detail, record) {
}
const beneficiaryList = ref([])
const smsForm = reactive({ mobile: '', verifyCode: '' })
const smsSending = ref(false)
const smsCountdown = ref(0)
let smsTimer = null
// :2026-08-08 /,( Bto
// mobilePhone ),
const mobileForm = reactive({ mobile: '' })
const busy = computed(
() =>
@ -597,14 +600,12 @@ const busy = computed(
businessLicenseUploading.value ||
legalPersonIdFrontUploading.value ||
legalPersonIdBackUploading.value ||
beneficiaryProofUploading.value ||
smsSending.value
beneficiaryProofUploading.value
)
const busyTip = computed(() => {
if (businessLicenseUploading.value) return '营业执照上传中,请稍候...'
if (legalPersonIdFrontUploading.value || legalPersonIdBackUploading.value) return '身份证照片上传中,请稍候...'
if (beneficiaryProofUploading.value) return '受益人材料证明上传中,请稍候...'
if (smsSending.value) return '验证码发送中,请稍候...'
if (submitting.value || modalSaving.value || modalInviting.value) return '提交中,请稍候...'
return ''
})
@ -653,6 +654,10 @@ function handleQueryBeneficiary() {
}
function openLegalRepPicker() {
if (!channelNo.value) {
showErrorModal('请先选择渠道归属')
return
}
legalRepPickerRef.value.show()
}
@ -677,13 +682,17 @@ async function handleSelectLegalRep(record) {
} else {
Object.assign(beneficiaryList.value[0], bene, { types: beneficiaryList.value[0].types })
}
// :,便,
if (!smsForm.mobile && detail.mobilePhone) {
smsForm.mobile = detail.mobilePhone
// :,便,
if (!mobileForm.mobile && detail.mobilePhone) {
mobileForm.mobile = detail.mobilePhone
}
}
function openPersonPicker(role) {
if (!channelNo.value) {
showErrorModal('请先选择渠道归属')
return
}
activePersonRole.value = role
rolePickerRef.value.show()
}
@ -695,6 +704,10 @@ async function handleRolePersonSelect(record) {
}
function openAddBeneficiary() {
if (!channelNo.value) {
showErrorModal('请先选择渠道归属')
return
}
beneficiaryPickerRef.value.show()
}
@ -709,36 +722,6 @@ function removeBeneficiary(index) {
beneficiaryList.value.splice(index, 1)
}
async function handleSendSms() {
if (!smsForm.mobile) {
showErrorModal('请先输入手机号')
return
}
if (!isValidPhone(smsForm.mobile)) {
showErrorModal('请输入正确的11位手机号')
return
}
smsSending.value = true
try {
// create/update verifyCode ,,.md
const res = await sendSmsCodeApi({ phone: smsForm.mobile, businessType: 'WALLET_ENTERPRISE_OPEN' })
if (res.data.code === 200) {
message.success('验证码已发送')
smsCountdown.value = 60
smsTimer = setInterval(() => {
smsCountdown.value -= 1
if (smsCountdown.value <= 0) clearInterval(smsTimer)
}, 1000)
}
} finally {
smsSending.value = false
}
}
onUnmounted(() => {
if (smsTimer) clearInterval(smsTimer)
})
async function loadDetail() {
const res = await fetchEnterpriseOpenDetailApi({ applicationNo: route.query.applicationNo })
if (res.data.code !== 200) return
@ -759,7 +742,7 @@ async function loadDetail() {
bankNo: data.bankNo,
bankName: data.bankName
})
smsForm.mobile = data.mobilePhone || ''
mobileForm.mobile = data.mobilePhone || ''
legalRep.customerName = data.legalPersonName
legalRep.idType = data.legalPersonIdType
legalRep.idNo = data.legalPersonIdNo
@ -805,11 +788,11 @@ async function loadDetail() {
beneMobile: bene.beneMobile,
beneBirthday: bene.beneBirthday,
types: {
equity25: bene.actCtrl === '',
profit25: bene.priBenfit === '',
equity25: bene.actCtrl === 'Y',
profit25: bene.priBenfit === 'Y',
control: !!bene.actCtrlCpny,
seniorMgr: bene.seniorMgr === '',
seniorMgrCn: bene.seniorMgrCn === ''
seniorMgr: bene.seniorMgr === 'Y',
seniorMgrCn: bene.seniorMgrCn === 'Y'
},
equityInfo: {
ratio: bene.actHdRat,
@ -828,7 +811,7 @@ async function loadDetail() {
controlType: bene.actCtrlCpny || undefined,
controlContent: bene.actCtrlType || '',
otherForms: bene.actOtherForms || '',
upperExists: bene.actConUpperMarket === '',
upperExists: bene.actConUpperMarket === 'Y',
upperEntityName: bene.actConUpperEntName || '',
upperEntityCode: bene.actConUniScid || '',
startDate: bene.obtainActDate || '',
@ -953,18 +936,14 @@ async function validateBeforeSubmit() {
return false
}
}
if (!smsForm.mobile) {
showErrorModal('请输入短信认证手机号')
if (!mobileForm.mobile) {
showErrorModal('请输入手机号')
return false
}
if (!isValidPhone(smsForm.mobile)) {
if (!isValidPhone(mobileForm.mobile)) {
showErrorModal('请输入正确的11位手机号')
return false
}
if (!smsForm.verifyCode) {
showErrorModal('请输入短信验证码')
return false
}
return true
}
@ -1050,10 +1029,10 @@ function buildBeneficiaryBto(bene) {
beneBirthday: bene.beneBirthday,
beneAddress: bene.beneAddress,
beneMobile: bene.beneMobile,
actCtrl: bene.types.equity25 ? '是' : '否',
priBenfit: bene.types.profit25 ? '是' : '否',
seniorMgr: bene.types.seniorMgr ? '是' : '否',
seniorMgrCn: bene.types.seniorMgrCn ? '是' : '否'
actCtrl: bene.types.equity25 ? 'Y' : 'N',
priBenfit: bene.types.profit25 ? 'Y' : 'N',
seniorMgr: bene.types.seniorMgr ? 'Y' : 'N',
seniorMgrCn: bene.types.seniorMgrCn ? 'Y' : 'N'
}
if (bene.types.equity25) {
bto.actHdRat = bene.equityInfo.ratio
@ -1072,7 +1051,7 @@ function buildBeneficiaryBto(bene) {
bto.actCtrlCpny = bene.controlInfo.controlType
bto.actCtrlType = bene.controlInfo.controlContent
bto.actOtherForms = bene.controlInfo.otherForms
bto.actConUpperMarket = bene.controlInfo.upperExists ? '是' : '否'
bto.actConUpperMarket = bene.controlInfo.upperExists ? 'Y' : 'N'
bto.obtainActDate = bene.controlInfo.startDate
bto.terminationActDate = bene.controlInfo.endDate
if (bene.controlInfo.upperExists) {
@ -1110,7 +1089,7 @@ function buildSubmitPayload() {
certificateAddress: form.certificateAddress,
businessScope: form.businessScope,
industry: form.industry,
mobilePhone: smsForm.mobile,
mobilePhone: mobileForm.mobile,
bankCardNumber: form.bankCardNumber,
bankNo: form.bankNo,
bankName: form.bankName,
@ -1120,8 +1099,6 @@ function buildSubmitPayload() {
...buildOperatorPayload(),
channelNo: channelNo.value,
businessLicenseFileNo: businessLicenseFileNo.value,
// create/update verifyCode ,.md
verifyCode: smsForm.verifyCode,
enterpriseAccountOpenApplicationFileBtoList: [
{ fileNo: businessLicenseFileNo.value, fileType: '12' },
{ fileNo: legalPersonIdFrontFileNo.value, fileType: '01' },