企业开户流程调试
parent
d2030f1711
commit
982b8723a4
|
|
@ -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
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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 均取同一个字段,详见
|
||||
《缺失的后端接口.md》Part 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'
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue