企业开户流程调试

main
halo 2026-08-08 10:31:10 +08:00
parent d2030f1711
commit 982b8723a4
6 changed files with 382 additions and 82 deletions

View File

@ -8,9 +8,15 @@ import request from './request'
// (该接口本身是透传给短信网关的通用参数,技术上可复用,待后端确认是否允许非个人开户场景调用)。
// tradeNo/tradeType 固定值取自个人开户H5当前已修正的正确取值(2026-08-06 修正记录,见
// personalOpen.js 同名字段注释):tradeNo 固定 "221506",tradeType 固定 "0"。
// - fetchBankAgreementApi/uploadFileApi:与个人开户H5共用同一对通用接口(协议模板下载、通用
// 文件上传均不区分业务类型),原样从 personalOpen.js 复制一份到本文件,保持"各开户流程独立
// api文件"的项目既有风格(不跨文件 import),详见 2026-08-06-agreement-pdf-fill-and-upload-design.md。
// - confirm:后端已提供的真实接口,语义为"申请人扫码核实信息后点击确认,提交凡荣e链",
// 与个人开户的 mobile/submit 不同,请求体是 {channelNo, verifyCode, confirmBto:{id,
// applicationStatus, confirmTime}},confirmBto.id 取自本次假设的 draft-detail 接口。
// 2026-08-07 新增 payAgreementFileNo(顶层字段,与 confirmBto 同级):真实接口当前请求体没有
// 这个字段,是前端参照个人开户 mobile/submit 新增 payAgreementFileNo 字段的先例假设新增,
// 已登记《缺失的后端接口.md》,待后端确认字段名/位置。
export const fetchDraftDetailApi = (applicationNo) =>
request.post('/api/wallet-management/enterprise-open-application/mobile/draft-detail', { applicationNo })
@ -29,15 +35,32 @@ export const sendVerifyCodeApi = ({ channelNo, mobile, cardNo, cardName, idNo })
idNo
})
// 获取银行协议模板文件(base64),与个人开户H5共用同一个通用接口,详见
// docs/superpowers/specs/2026-08-06-agreement-pdf-fill-and-upload-design.md。
// 响应 data:{ fileType, fileName, fileContent(base64) }。
export const fetchBankAgreementApi = (fileType) =>
request.get('/api/customer-info/bank-agreement/base64', { params: { fileType } })
// 通用文件上传(与主项目 src/api/customer.js 的 uploadCustomerFileApi 对接同一个后端接口),
// 用于把"填好企业信息后的《用户支付服务协议》PDF"(base64)上传换取 fileNo。channelNo 后端
// 标记必填,取自 draft-detail 响应字段(同 send-verify-code/general 的取值来源)。
export const uploadFileApi = ({ fileBase64, channelNo }) =>
request.post('/api/customer-info/personal/upload-file', { fileBase64, channelNo })
// confirm 为真实已有接口,直接对接真实后端(不在 mock-server 实现),applicationStatus 固定传
// 'CONFIRMED'(枚举含义"已确认",对应"申请人扫码确认"这一步),confirmTime 为提交时的当前时间。
export const confirmApplicationApi = ({ channelNo, verifyCode, id, confirmTime }) =>
// payAgreementFileNo 为本次新增的假设字段(见文件头注释),放在请求体顶层,与 channelNo/
// verifyCode 同级,不放进 confirmBto(该 Bto 对应后端已定义的 ConfirmEnterpriseAccountOpenBto,
// 只有 id/applicationStatus/confirmTime 三个字段,不属于这个 Bto 的既有结构)。
export const confirmApplicationApi = ({ channelNo, verifyCode, id, confirmTime, payAgreementFileNo }) =>
request.post('/api/wallet-management/enterprise-open-application/confirm', {
channelNo,
verifyCode,
payAgreementFileNo,
confirmBto: {
id,
applicationStatus: 'CONFIRMED',
confirmTime
}
})

View File

@ -8,7 +8,11 @@
后端契约缺口(详见缺失的后端接口.md src/api/enterpriseOpen.js 顶部注释):
- mobile/draft-detail 是本次前端假设新增的接口,后端目前未提供
- 发验证码复用个人开户H5在用的通用接口
- 提交确认(confirm)是真实已有接口,本地 mock-server 未实现,只能连真实后端环境验证 -->
- 提交确认(confirm)是真实已有接口,本地 mock-server 未实现,只能连真实后端环境验证
- 2026-08-07 新增:相关协议步骤按产品要求对齐个人开户H5的"协议模板下载+自动填充+上传"
机制(详见 EnterpriseAgreementStep.vue),填入PDF签名区块的是企业名称+统一社会信用
代码(不是法人姓名/身份证号),生成的 payAgreementFileNo confirm 请求体顶层字段
一并提交,该字段是前端假设新增,详见 enterpriseOpen.js 顶部注释 -->
<template>
<div class="page">
<div v-if="loading" class="center-box">
@ -50,7 +54,13 @@
</span>
<span class="step-label">相关协议</span>
</div>
<EnterpriseAgreementStep v-if="currentStep === 'AGREEMENT'" @next="currentStep = 'SMS'" />
<EnterpriseAgreementStep
v-if="currentStep === 'AGREEMENT'"
:enterprise-name="draftDetail?.enterpriseName"
:certificate-number="draftDetail?.certificateNumber"
:channel-no="draftDetail?.channelNo"
@next="handleAgreementNext"
/>
</div>
<div class="step-row" :class="rowClass('SMS')">
@ -61,15 +71,21 @@
</span>
<span class="step-label">手机验证</span>
</div>
<!-- 2026-08-07 修复:真实后端 draft-detail 响应没有独立的明文 mobilePhone 字段
(与个人开户 MobileDraftDetailVo 不同),只有 mobilePhoneMask 一个字段
(且实测该字段返回的是完整11位手机号,并非真正打了掩码),故发验证码用的
mobile 参数与展示用的 mobile-phone-mask 均取同一个字段,详见
缺失的后端接口.mdPart 20 第124/125条更新记录 -->
<EnterpriseSmsVerifyStep
v-if="currentStep === 'SMS'"
:id="draftDetail?.id"
:channel-no="draftDetail?.channelNo"
:mobile="draftDetail?.mobilePhone"
:mobile="draftDetail?.mobilePhoneMask"
:mobile-phone-mask="draftDetail?.mobilePhoneMask"
:card-no="draftDetail?.bankCardNumber"
:card-name="draftDetail?.enterpriseName"
:id-no="draftDetail?.certificateNumber"
:pay-agreement-file-no="payAgreementFileNo"
@submitted="handleSubmitted"
/>
</div>
@ -110,6 +126,9 @@ const draftDetail = ref(null)
const STEP_ORDER = ['INFO', 'AGREEMENT', 'SMS', 'RESULT']
const currentStep = ref('INFO')
const resultData = ref(null)
// EnterpriseAgreementStep +PDF,,
// EnterpriseSmsVerifyStep (confirm payAgreementFileNo )
const payAgreementFileNo = ref('')
function isDone(key) {
return STEP_ORDER.indexOf(key) < STEP_ORDER.indexOf(currentStep.value)
@ -120,6 +139,11 @@ function rowClass(key) {
return 'is-pending'
}
function handleAgreementNext(fileNo) {
payAgreementFileNo.value = fileNo
currentStep.value = 'SMS'
}
function handleSubmitted(data) {
resultData.value = data
currentStep.value = 'RESULT'

View File

@ -1,76 +1,290 @@
<template>
<div class="step-body">
<div class="agreement-row">
<van-checkbox v-model="serviceAgreed">
<div class="step-page">
<div class="content">
<div class="tip">请仔细阅读以下协议内容,勾选即代表您已阅读并同意</div>
<div class="agreement-list">
<van-checkbox v-model="agreedPayService" shape="square" class="agreement-item">
我已阅读并同意
<span class="link" @click.stop="confirmAgreement('service')">浙江稠州商业银行用户支付服务协议</span>
<span class="link" @click.stop="openAgreement('PAY_SERVICE_AGREEMENT')">{{ PAY_SERVICE_AGREEMENT_TITLE }}</span>
</van-checkbox>
</div>
<div class="agreement-row">
<van-checkbox v-model="privacyAgreed">
<van-checkbox v-model="agreedPrivacy" shape="square" class="agreement-item">
我已阅读并同意
<span class="link" @click.stop="confirmAgreement('privacy')">浙江稠州商业银行用户支付服务隐私政策</span>
<span class="link" @click.stop="openAgreement('PAY_SERVICE_PRIVACY')">{{ PAY_SERVICE_PRIVACY_TITLE }}</span>
</van-checkbox>
</div>
<div class="footer">
<van-button type="primary" block round :disabled="!bothAgreed" @click="$emit('next')"></van-button>
<!-- 两个协议都勾选后自动在用户支付服务协议PDF末页填入企业名称/统一社会信用代码/
当前日期并上传,失败时展示重试入口;"下一步"在文件生成成功前保持禁用,避免用户带着
空的 payAgreementFileNo 进入下一步与个人开户 AgreementStep.vue 的机制完全一致,
唯一差异是填入PDF的是企业名称+统一社会信用代码,不是个人姓名+身份证号 -->
<div v-if="generateError" class="generate-error">
{{ generateError }}
<span class="retry-link" @click="generateAndUpload"></span>
</div>
</div>
<div class="footer">
<van-button
type="primary"
block
round
:disabled="!canProceed"
:loading="generating"
loading-text="生成协议中..."
@click="handleNext"
>
下一步
</van-button>
</div>
<!-- 耗时操作统一等待态:生成+上传协议PDF期间整页遮罩,禁止用户进行其它操作 -->
<van-overlay :show="generating" class="generate-overlay">
<div class="overlay-center">
<van-loading type="spinner" color="#fff" size="28px">正在生成协议文件,请稍候...</van-loading>
</div>
</van-overlay>
<!-- 协议原文预览:调用 /api/customer-info/bank-agreement/base64 拿到真实模板文件(base64),
转成 Blob 生成 blob: URL 后用 iframe 内嵌展示 PDF,机制与个人开户 AgreementStep.vue
完全一致(包含 Safari/WebKit iframe data: URI 的导航限制的规避方式) -->
<van-popup v-model:show="showPreview" position="bottom" round :style="{ height: '85%' }">
<div class="preview-modal">
<div class="preview-title">{{ previewTitle }}</div>
<div class="preview-body">
<van-loading v-if="previewLoading" type="spinner" class="preview-loading">...</van-loading>
<van-empty v-else-if="previewError" image="error" :description="previewError" />
<iframe v-else-if="previewSrc" :src="previewSrc" class="preview-iframe" title="协议预览"></iframe>
</div>
</div>
</van-popup>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
import { showConfirmDialog } from 'vant'
import { computed, onBeforeUnmount, ref, watch } from 'vue'
import { fetchBankAgreementApi, uploadFileApi } from '@/api/enterpriseOpen'
import { BANK_AGREEMENT_FILE_TYPE_LABELS } from '@/constants/enums'
defineEmits(['next'])
// enterpriseName/certificateNumber/channelNo EnterpriseOpenMobile.vue
// draftDetail(mobile/draft-detail ),**+
// **(/,)PDF,
// channelNo upload-file ( send-verify-code/general )
// "",,
// /
const props = defineProps({
enterpriseName: { type: String, default: '' },
certificateNumber: { type: String, default: '' },
channelNo: { type: String, default: '' }
})
// next payAgreementFileNo, EnterpriseSmsVerifyStep
const emit = defineEmits(['next'])
const serviceAgreed = ref(false)
const privacyAgreed = ref(false)
const bothAgreed = computed(() => serviceAgreed.value && privacyAgreed.value)
const PAY_SERVICE_AGREEMENT_TITLE = BANK_AGREEMENT_FILE_TYPE_LABELS.PAY_SERVICE_AGREEMENT
const PAY_SERVICE_PRIVACY_TITLE = BANK_AGREEMENT_FILE_TYPE_LABELS.PAY_SERVICE_PRIVACY
// / EnterpriseOpenForm.vue/PersonalOpenForm.vue 使
// (H5 AgreementStep.vue 使"PDF+",
// H5+,PDF/)
const AGREEMENT_CONTENT = {
service: {
title: '是否同意《浙江稠州商业银行用户支付服务协议》',
field: 'serviceAgreed'
},
privacy: {
title: '是否同意《浙江稠州商业银行用户支付服务隐私政策》',
field: 'privacyAgreed'
const agreedPayService = ref(false)
const agreedPrivacy = ref(false)
const bothAgreed = computed(() => agreedPayService.value && agreedPrivacy.value)
// ,PDF//
// fileNo(,)
const generating = ref(false)
const generateError = ref('')
const payAgreementFileNo = ref('')
// "", payAgreementFileNo
const canProceed = computed(() => bothAgreed.value && !!payAgreementFileNo.value && !generating.value)
watch(bothAgreed, (val) => {
if (val && !payAgreementFileNo.value && !generating.value) {
generateAndUpload()
}
}
})
// :( window.confirm() ,
// Vant showConfirmDialog,,
// ),
async function confirmAgreement(type) {
const info = AGREEMENT_CONTENT[type]
async function generateAndUpload() {
if (generating.value) return
if (!props.enterpriseName || !props.certificateNumber || !props.channelNo) {
generateError.value = '该开户申请缺少企业名称/统一社会信用代码/渠道信息,无法生成协议文件,请联系柜员核实后重新生成二维码'
return
}
generating.value = true
generateError.value = ''
try {
await showConfirmDialog({ title: info.title, confirmButtonText: '确定', cancelButtonText: '取消' })
if (info.field === 'serviceAgreed') serviceAgreed.value = true
else privacyAgreed.value = true
const templateRes = await fetchBankAgreementApi('PAY_SERVICE_AGREEMENT')
const templateBase64 = templateRes.data?.code === 200 ? templateRes.data.data?.fileContent : ''
if (!templateBase64) {
generateError.value = templateRes.data?.message || '协议模板加载失败,请重试'
return
}
// pdf-lib , import Vite , AgreementStep.vue ,
// PDF,H5
const { fillPayAgreementPdf } = await import('@/utils/agreementPdfFill')
// fillPayAgreementPdf customerName/certificateNumber(,),
// "",/,
// agreementPdfFill.js
const filledBase64 = await fillPayAgreementPdf(templateBase64, {
customerName: props.enterpriseName,
certificateNumber: props.certificateNumber
})
// upload-file fileBase64 data:<mime>;base64, ,
// AgreementStep.vue ,
// docs/superpowers/specs/2026-08-06-agreement-pdf-fill-and-upload-design.md""
const uploadRes = await uploadFileApi({
fileBase64: `data:application/pdf;base64,${filledBase64}`,
channelNo: props.channelNo
})
if (uploadRes.data?.code === 200 && uploadRes.data.data?.fileNo) {
payAgreementFileNo.value = uploadRes.data.data.fileNo
} else {
generateError.value = uploadRes.data?.message || '协议文件上传失败,请重试'
}
} catch {
// "",
generateError.value = '协议文件生成失败,请检查网络后重试'
} finally {
generating.value = false
}
}
function handleNext() {
if (!canProceed.value) return
emit('next', payAgreementFileNo.value)
}
const showPreview = ref(false)
const previewLoading = ref(false)
const previewError = ref('')
const previewTitle = ref('')
const previewSrc = ref('')
// fileType blob: URL, + ;
// revokeObjectURL ( onBeforeUnmount)
const previewCache = {}
// base64 -> Blob: data:application/pdf;base64,... iframe src
// Safari/WebKit iframe data: URI , Blob
// URL.createObjectURL blob: URL, AgreementStep.vue
function base64ToBlobUrl(base64, mime = 'application/pdf') {
const byteChars = atob(base64)
const byteNumbers = new Uint8Array(byteChars.length)
for (let i = 0; i < byteChars.length; i++) {
byteNumbers[i] = byteChars.charCodeAt(i)
}
const blob = new Blob([byteNumbers], { type: mime })
return URL.createObjectURL(blob)
}
async function openAgreement(fileType) {
previewTitle.value = BANK_AGREEMENT_FILE_TYPE_LABELS[fileType]
showPreview.value = true
previewError.value = ''
if (previewCache[fileType]) {
previewSrc.value = previewCache[fileType]
previewLoading.value = false
return
}
previewSrc.value = ''
previewLoading.value = true
try {
const res = await fetchBankAgreementApi(fileType)
const fileContent = res.data?.code === 200 ? res.data.data?.fileContent : ''
if (fileContent) {
const url = base64ToBlobUrl(fileContent)
previewCache[fileType] = url
previewSrc.value = url
} else {
previewError.value = res.data?.message || '协议内容加载失败,请稍后重试'
}
} catch {
previewError.value = '协议内容加载失败,请检查网络后重试'
} finally {
previewLoading.value = false
}
}
onBeforeUnmount(() => {
Object.values(previewCache).forEach((url) => URL.revokeObjectURL(url))
})
</script>
<style scoped>
.step-body {
.step-page {
display: flex;
flex-direction: column;
min-height: calc(100vh - 46px);
}
.content {
flex: 1;
padding: 4px 16px 4px;
}
.agreement-row {
padding: 8px 0;
.tip {
font-size: 14px;
color: #646566;
margin-bottom: 20px;
line-height: 1.6;
}
.agreement-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.agreement-item {
font-size: 14px;
color: #646566;
}
.link {
color: #1989fa;
}
.generate-error {
margin-top: 16px;
padding: 10px 12px;
background: #fff2f0;
color: #ee0a24;
font-size: 13px;
line-height: 1.6;
border-radius: 6px;
}
.retry-link {
margin-left: 8px;
color: #1989fa;
text-decoration: underline;
cursor: pointer;
}
.generate-overlay {
display: flex;
align-items: center;
justify-content: center;
}
.overlay-center {
display: flex;
flex-direction: column;
align-items: center;
}
.footer {
padding: 12px 0 4px;
padding: 12px 16px 4px;
}
.preview-modal {
height: 100%;
display: flex;
flex-direction: column;
}
.preview-title {
padding: 14px 16px;
font-size: 15px;
font-weight: 600;
text-align: center;
border-bottom: 1px solid #ebedf0;
}
.preview-body {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.preview-loading {
color: #969799;
}
.preview-iframe {
width: 100%;
height: 100%;
border: none;
}
</style>

View File

@ -55,7 +55,8 @@ const props = defineProps({
mobilePhoneMask: { type: String, default: '' },
cardNo: { type: String, default: '' },
cardName: { type: String, default: '' },
idNo: { type: String, default: '' }
idNo: { type: String, default: '' },
payAgreementFileNo: { type: String, default: '' }
})
const emit = defineEmits(['submitted'])
@ -96,7 +97,9 @@ async function handleSendCode() {
}
}
// : enterprise-open-application/confirm ,confirmTime
// : enterprise-open-application/confirm ,confirmTime ,
// payAgreementFileNo PDF(2026-08-07,
// src/api/enterpriseOpen.js )
async function handleSubmit() {
submitting.value = true
try {
@ -104,7 +107,8 @@ async function handleSubmit() {
channelNo: props.channelNo,
verifyCode: verifyCode.value,
id: props.id,
confirmTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
confirmTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
payAgreementFileNo: props.payAgreementFileNo
})
if (res.data?.code === 200) {
emit('submitted', res.data.data)

View File

@ -101,9 +101,20 @@ export const dictTypes = [
'前端只提交最终选中叶子字典项的 itemValue 字符串;叶子文案为"其他"的项用于触发前端' +
'"职业备注"必填校验,详见《缺失的后端接口.md》',
createdAt: '2026-08-06 10:00:00'
},
{
id: 5,
dictTypeCode: 'kaihuhang',
dictTypeName: '开户行',
dictCategory: 'NORMAL',
status: 'ENABLED',
remark: '企业开户申请"开户银行/网点"字段"快速"录入方式使用,普通字典(单层)。前端提交给' +
'后端的 bankName 是选中项的 itemValue(银行编码),不是 itemLabel(中文银行名称展示文案),' +
'详见《缺失的后端接口.md》',
createdAt: '2026-08-07 10:00:00'
}
]
initCounter('dictType', 5)
initCounter('dictType', 6)
export const dictItems = [
// CUST_TYPE:普通字典,仅一层
@ -181,6 +192,23 @@ export const dictItems = [
{ id: 59, dictTypeCode: 'job', parentId: 44, itemValue: 'JOB_08_02_02', itemLabel: '自由职业者', sortOrder: 2, status: 'ENABLED', remark: '' },
{ id: 45, dictTypeCode: 'job', parentId: 32, itemValue: 'JOB_08_03', itemLabel: '其他', sortOrder: 3, status: 'ENABLED', remark: '' },
// 叶子文案精确等于"其他"两个字,前端 PersonalOpenForm.vue 靠这个约定判断是否要求填写职业备注
{ id: 60, dictTypeCode: 'job', parentId: 45, itemValue: 'JOB_08_03_01', itemLabel: '其他', sortOrder: 1, status: 'ENABLED', remark: '' }
{ id: 60, dictTypeCode: 'job', parentId: 45, itemValue: 'JOB_08_03_01', itemLabel: '其他', sortOrder: 1, status: 'ENABLED', remark: '' },
// kaihuhang:普通字典,仅一层。itemValue 是联行号/银行编码(12位数字风格,除中国工商银行的
// '102100099996' 系用户确认的真实示例外,其余9条均为仅供本地演示的占位编码,不代表真实
// 联行号,后端/产品配置真实数据时不需要照抄),itemLabel 是中文银行名称展示文案。
// 2026-08-07 二次修正:提交给 enterprise-open-application/create 的 bankName 取 itemLabel,
// bankNo 取 itemValue(与第一版"bankName=itemValue"相反),详见 BankSelector.vue 顶部注释。
{ id: 61, dictTypeCode: 'kaihuhang', parentId: null, itemValue: '102100099996', itemLabel: '中国工商银行', sortOrder: 1, status: 'ENABLED', remark: '' },
{ id: 62, dictTypeCode: 'kaihuhang', parentId: null, itemValue: '103100000026', itemLabel: '中国农业银行', sortOrder: 2, status: 'ENABLED', remark: '' },
{ id: 63, dictTypeCode: 'kaihuhang', parentId: null, itemValue: '104100000004', itemLabel: '中国银行', sortOrder: 3, status: 'ENABLED', remark: '' },
{ id: 64, dictTypeCode: 'kaihuhang', parentId: null, itemValue: '105100000017', itemLabel: '中国建设银行', sortOrder: 4, status: 'ENABLED', remark: '' },
{ id: 65, dictTypeCode: 'kaihuhang', parentId: null, itemValue: '301100000013', itemLabel: '交通银行', sortOrder: 5, status: 'ENABLED', remark: '' },
{ id: 66, dictTypeCode: 'kaihuhang', parentId: null, itemValue: '308100000006', itemLabel: '招商银行', sortOrder: 6, status: 'ENABLED', remark: '' },
{ id: 67, dictTypeCode: 'kaihuhang', parentId: null, itemValue: '403100000009', itemLabel: '中国邮政储蓄银行', sortOrder: 7, status: 'ENABLED', remark: '' },
{ id: 68, dictTypeCode: 'kaihuhang', parentId: null, itemValue: '313331000011', itemLabel: '浙商银行', sortOrder: 8, status: 'ENABLED', remark: '' },
// 浙江稠州商业银行的联行号复用本文件顶部 coreEnterprises 种子数据里已经在用的同一个值
// ('313584000018',义乌诚信支行),避免同一个仓库里出现两个不同的"浙江稠州商业银行"编码
{ id: 69, dictTypeCode: 'kaihuhang', parentId: null, itemValue: '313584000018', itemLabel: '浙江稠州商业银行', sortOrder: 9, status: 'ENABLED', remark: '' },
{ id: 70, dictTypeCode: 'kaihuhang', parentId: null, itemValue: '305100000015', itemLabel: '中国民生银行', sortOrder: 10, status: 'ENABLED', remark: '' }
]
initCounter('dictItem', 61)
initCounter('dictItem', 71)

View File

@ -2,10 +2,14 @@
<!--
企业开户"开户银行/网点"录入组件,支持"快速/详细/手动"三种录入方式,详见
docs/superpowers/specs/2026-07-28-enterprise-open-application-redesign.md 第一节
项目里没有联行号字典查询接口(已登记缺失的后端接口.md),三种方式最终都只产出
bankNo/bankName 两个已有字段,不新增后端字段:
- 快速:前端硬编码常见银行名称下拉,仅产出 bankName,bankNo 留空
- 详细:省市(RegionCascader)+支行名称拼接产出 bankName,bankNo 留空
三种方式最终都只产出 bankNo/bankName 两个已有字段,不新增后端字段:
- 快速:2026-08-07 改为动态字典选择(dictTypeCode='kaihuhang'),按产品明确要求(2026-08-07
二次修正):提交给后端的 bankName 是选中字典项的 itemLabel(中文银行名称,"中国工商
银行"),bankNo 是该字典项的 itemValue(联行号/银行编码,如"102100099996")即字典项的
"标签"对应 bankName,"值"对应 bankNo,与第一版"bankName=itemValue"的理解不同,已按此
修正字典类型 'kaihuhang' 真实后端目前不存在,需要后端/管理员在"动态字典管理"模块新建,
详见缺失的后端接口.md登记
- 详细:省市(RegionCascader)+支行名称拼接产出 bankName,bankNo 留空(无联行号来源)
- 手动:bankNo/bankName 两个文本框直接输入(现状写法)
切换 Tab 不会清空已填值,只是切换录入方式,只有用户在新 Tab 内实际操作控件时才会覆盖值
@ -24,18 +28,14 @@
</a-radio-group>
<div class="bank-selector-panel">
<a-select
<DictSelect
v-if="activeTab === 'quick'"
:value="bankName || undefined"
:model-value="bankNo || undefined"
dict-type-code="kaihuhang"
placeholder="请选择开户行"
allow-clear
show-search
:disabled="disabled"
style="width: 100%"
@update:value="handleQuickSelect"
>
<a-select-option v-for="name in COMMON_BANK_NAMES" :key="name" :value="name">{{ name }}</a-select-option>
</a-select>
@update:model-value="handleQuickSelect"
/>
<a-space v-else-if="activeTab === 'detail'" direction="vertical" style="width: 100%">
<RegionCascader v-model:value="detailRegion" :disabled="disabled" @update:text="handleDetailRegionText" />
@ -68,8 +68,10 @@
</template>
<script setup>
import { ref } from 'vue'
import { onMounted, ref } from 'vue'
import RegionCascader from '@/components/RegionCascader.vue'
import DictSelect from '@/components/DictSelect.vue'
import { useDictStore } from '@/stores/dict'
const props = defineProps({
bankNo: { type: String, default: '' },
@ -84,22 +86,27 @@ const detailRegion = ref([])
const detailRegionText = ref('')
const detailBranchName = ref('')
const COMMON_BANK_NAMES = [
'中国工商银行',
'中国农业银行',
'中国银行',
'中国建设银行',
'交通银行',
'招商银行',
'中国邮政储蓄银行',
'浙商银行',
'浙江稠州商业银行',
'中国民生银行'
]
// DictSelect v-model itemValue( itemLabel),""
// itemValue( bankNo) itemLabel( bankName), store
// dictTypeCode ( store , DictSelect ),
// itemValue itemLabel
const dictStore = useDictStore()
const kaihuhangItems = ref([])
onMounted(async () => {
kaihuhangItems.value = await dictStore.getDictItems('kaihuhang')
})
// 2026-08-07 :bankName itemLabel(),bankNo itemValue
// (/),"bankName=itemValue",
function handleQuickSelect(value) {
emit('update:bankName', value || '')
if (!value) {
emit('update:bankName', '')
emit('update:bankNo', '')
return
}
const item = kaihuhangItems.value.find((i) => i.itemValue === value)
emit('update:bankName', item?.itemLabel || '')
emit('update:bankNo', value)
}
function handleDetailRegionText(text) {