企业开户流程调试
parent
d2030f1711
commit
982b8723a4
|
|
@ -8,9 +8,15 @@ import request from './request'
|
||||||
// (该接口本身是透传给短信网关的通用参数,技术上可复用,待后端确认是否允许非个人开户场景调用)。
|
// (该接口本身是透传给短信网关的通用参数,技术上可复用,待后端确认是否允许非个人开户场景调用)。
|
||||||
// tradeNo/tradeType 固定值取自个人开户H5当前已修正的正确取值(2026-08-06 修正记录,见
|
// tradeNo/tradeType 固定值取自个人开户H5当前已修正的正确取值(2026-08-06 修正记录,见
|
||||||
// personalOpen.js 同名字段注释):tradeNo 固定 "221506",tradeType 固定 "0"。
|
// 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链",
|
// - confirm:后端已提供的真实接口,语义为"申请人扫码核实信息后点击确认,提交凡荣e链",
|
||||||
// 与个人开户的 mobile/submit 不同,请求体是 {channelNo, verifyCode, confirmBto:{id,
|
// 与个人开户的 mobile/submit 不同,请求体是 {channelNo, verifyCode, confirmBto:{id,
|
||||||
// applicationStatus, confirmTime}},confirmBto.id 取自本次假设的 draft-detail 接口。
|
// applicationStatus, confirmTime}},confirmBto.id 取自本次假设的 draft-detail 接口。
|
||||||
|
// 2026-08-07 新增 payAgreementFileNo(顶层字段,与 confirmBto 同级):真实接口当前请求体没有
|
||||||
|
// 这个字段,是前端参照个人开户 mobile/submit 新增 payAgreementFileNo 字段的先例假设新增,
|
||||||
|
// 已登记《缺失的后端接口.md》,待后端确认字段名/位置。
|
||||||
|
|
||||||
export const fetchDraftDetailApi = (applicationNo) =>
|
export const fetchDraftDetailApi = (applicationNo) =>
|
||||||
request.post('/api/wallet-management/enterprise-open-application/mobile/draft-detail', { 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
|
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 固定传
|
// confirm 为真实已有接口,直接对接真实后端(不在 mock-server 实现),applicationStatus 固定传
|
||||||
// 'CONFIRMED'(枚举含义"已确认",对应"申请人扫码确认"这一步),confirmTime 为提交时的当前时间。
|
// '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', {
|
request.post('/api/wallet-management/enterprise-open-application/confirm', {
|
||||||
channelNo,
|
channelNo,
|
||||||
verifyCode,
|
verifyCode,
|
||||||
|
payAgreementFileNo,
|
||||||
confirmBto: {
|
confirmBto: {
|
||||||
id,
|
id,
|
||||||
applicationStatus: 'CONFIRMED',
|
applicationStatus: 'CONFIRMED',
|
||||||
confirmTime
|
confirmTime
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,11 @@
|
||||||
后端契约缺口(详见《缺失的后端接口.md》与 src/api/enterpriseOpen.js 顶部注释):
|
后端契约缺口(详见《缺失的后端接口.md》与 src/api/enterpriseOpen.js 顶部注释):
|
||||||
- mobile/draft-detail 是本次前端假设新增的接口,后端目前未提供
|
- mobile/draft-detail 是本次前端假设新增的接口,后端目前未提供
|
||||||
- 发验证码复用个人开户H5在用的通用接口
|
- 发验证码复用个人开户H5在用的通用接口
|
||||||
- 提交确认(confirm)是真实已有接口,本地 mock-server 未实现,只能连真实后端环境验证 -->
|
- 提交确认(confirm)是真实已有接口,本地 mock-server 未实现,只能连真实后端环境验证
|
||||||
|
- 2026-08-07 新增:相关协议步骤按产品要求对齐个人开户H5的"协议模板下载+自动填充+上传"
|
||||||
|
机制(详见 EnterpriseAgreementStep.vue),填入PDF签名区块的是企业名称+统一社会信用
|
||||||
|
代码(不是法人姓名/身份证号),生成的 payAgreementFileNo 随 confirm 请求体顶层字段
|
||||||
|
一并提交,该字段是前端假设新增,详见 enterpriseOpen.js 顶部注释 -->
|
||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<div v-if="loading" class="center-box">
|
<div v-if="loading" class="center-box">
|
||||||
|
|
@ -50,7 +54,13 @@
|
||||||
</span>
|
</span>
|
||||||
<span class="step-label">相关协议</span>
|
<span class="step-label">相关协议</span>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="step-row" :class="rowClass('SMS')">
|
<div class="step-row" :class="rowClass('SMS')">
|
||||||
|
|
@ -61,15 +71,21 @@
|
||||||
</span>
|
</span>
|
||||||
<span class="step-label">手机验证</span>
|
<span class="step-label">手机验证</span>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 2026-08-07 修复:真实后端 draft-detail 响应没有独立的明文 mobilePhone 字段
|
||||||
|
(与个人开户 MobileDraftDetailVo 不同),只有 mobilePhoneMask 一个字段
|
||||||
|
(且实测该字段返回的是完整11位手机号,并非真正打了掩码),故发验证码用的
|
||||||
|
mobile 参数与展示用的 mobile-phone-mask 均取同一个字段,详见
|
||||||
|
《缺失的后端接口.md》Part 20 第124/125条更新记录 -->
|
||||||
<EnterpriseSmsVerifyStep
|
<EnterpriseSmsVerifyStep
|
||||||
v-if="currentStep === 'SMS'"
|
v-if="currentStep === 'SMS'"
|
||||||
:id="draftDetail?.id"
|
:id="draftDetail?.id"
|
||||||
:channel-no="draftDetail?.channelNo"
|
:channel-no="draftDetail?.channelNo"
|
||||||
:mobile="draftDetail?.mobilePhone"
|
:mobile="draftDetail?.mobilePhoneMask"
|
||||||
:mobile-phone-mask="draftDetail?.mobilePhoneMask"
|
:mobile-phone-mask="draftDetail?.mobilePhoneMask"
|
||||||
:card-no="draftDetail?.bankCardNumber"
|
:card-no="draftDetail?.bankCardNumber"
|
||||||
:card-name="draftDetail?.enterpriseName"
|
:card-name="draftDetail?.enterpriseName"
|
||||||
:id-no="draftDetail?.certificateNumber"
|
:id-no="draftDetail?.certificateNumber"
|
||||||
|
:pay-agreement-file-no="payAgreementFileNo"
|
||||||
@submitted="handleSubmitted"
|
@submitted="handleSubmitted"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -110,6 +126,9 @@ const draftDetail = ref(null)
|
||||||
const STEP_ORDER = ['INFO', 'AGREEMENT', 'SMS', 'RESULT']
|
const STEP_ORDER = ['INFO', 'AGREEMENT', 'SMS', 'RESULT']
|
||||||
const currentStep = ref('INFO')
|
const currentStep = ref('INFO')
|
||||||
const resultData = ref(null)
|
const resultData = ref(null)
|
||||||
|
// EnterpriseAgreementStep 生成+上传《用户支付服务协议》PDF后拿到的文件编号,提升到父组件状态,
|
||||||
|
// 转发给 EnterpriseSmsVerifyStep 用于最终提交(confirm 接口的 payAgreementFileNo 字段)
|
||||||
|
const payAgreementFileNo = ref('')
|
||||||
|
|
||||||
function isDone(key) {
|
function isDone(key) {
|
||||||
return STEP_ORDER.indexOf(key) < STEP_ORDER.indexOf(currentStep.value)
|
return STEP_ORDER.indexOf(key) < STEP_ORDER.indexOf(currentStep.value)
|
||||||
|
|
@ -120,6 +139,11 @@ function rowClass(key) {
|
||||||
return 'is-pending'
|
return 'is-pending'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleAgreementNext(fileNo) {
|
||||||
|
payAgreementFileNo.value = fileNo
|
||||||
|
currentStep.value = 'SMS'
|
||||||
|
}
|
||||||
|
|
||||||
function handleSubmitted(data) {
|
function handleSubmitted(data) {
|
||||||
resultData.value = data
|
resultData.value = data
|
||||||
currentStep.value = 'RESULT'
|
currentStep.value = 'RESULT'
|
||||||
|
|
|
||||||
|
|
@ -1,76 +1,290 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="step-body">
|
<div class="step-page">
|
||||||
<div class="agreement-row">
|
<div class="content">
|
||||||
<van-checkbox v-model="serviceAgreed">
|
<div class="tip">请仔细阅读以下协议内容,勾选即代表您已阅读并同意</div>
|
||||||
我已阅读并同意
|
<div class="agreement-list">
|
||||||
<span class="link" @click.stop="confirmAgreement('service')">《浙江稠州商业银行用户支付服务协议》</span>
|
<van-checkbox v-model="agreedPayService" shape="square" class="agreement-item">
|
||||||
</van-checkbox>
|
我已阅读并同意
|
||||||
</div>
|
<span class="link" @click.stop="openAgreement('PAY_SERVICE_AGREEMENT')">《{{ PAY_SERVICE_AGREEMENT_TITLE }}》</span>
|
||||||
<div class="agreement-row">
|
</van-checkbox>
|
||||||
<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>
|
</van-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 两个协议都勾选后自动在《用户支付服务协议》PDF末页填入企业名称/统一社会信用代码/
|
||||||
|
当前日期并上传,失败时展示重试入口;"下一步"在文件生成成功前保持禁用,避免用户带着
|
||||||
|
空的 payAgreementFileNo 进入下一步。与个人开户 AgreementStep.vue 的机制完全一致,
|
||||||
|
唯一差异是填入PDF的是企业名称+统一社会信用代码,不是个人姓名+身份证号 -->
|
||||||
|
<div v-if="generateError" class="generate-error">
|
||||||
|
{{ generateError }}
|
||||||
|
<span class="retry-link" @click="generateAndUpload">点击重试</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<van-button type="primary" block round :disabled="!bothAgreed" @click="$emit('next')">下一步</van-button>
|
<van-button
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
|
round
|
||||||
|
:disabled="!canProceed"
|
||||||
|
:loading="generating"
|
||||||
|
loading-text="生成协议中..."
|
||||||
|
@click="handleNext"
|
||||||
|
>
|
||||||
|
下一步
|
||||||
|
</van-button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
import { showConfirmDialog } from 'vant'
|
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 PAY_SERVICE_AGREEMENT_TITLE = BANK_AGREEMENT_FILE_TYPE_LABELS.PAY_SERVICE_AGREEMENT
|
||||||
const privacyAgreed = ref(false)
|
const PAY_SERVICE_PRIVACY_TITLE = BANK_AGREEMENT_FILE_TYPE_LABELS.PAY_SERVICE_PRIVACY
|
||||||
const bothAgreed = computed(() => serviceAgreed.value && privacyAgreed.value)
|
|
||||||
|
|
||||||
// 协议名称/占位正文与管理后台 EnterpriseOpenForm.vue/PersonalOpenForm.vue 使用的同一套占位文案
|
const agreedPayService = ref(false)
|
||||||
// 保持一致(与个人开户H5 AgreementStep.vue 现在使用的"PDF填充+上传"完全不同的流程,本次企业
|
const agreedPrivacy = ref(false)
|
||||||
// 开户H5严格按截图展示的简单勾选+确认弹窗交互,不涉及PDF生成/上传)。
|
const bothAgreed = computed(() => agreedPayService.value && agreedPrivacy.value)
|
||||||
const AGREEMENT_CONTENT = {
|
|
||||||
service: {
|
// 两个协议都勾选后,自动在《用户支付服务协议》PDF末页填入企业名称/统一社会信用代码/当前日期
|
||||||
title: '是否同意《浙江稠州商业银行用户支付服务协议》',
|
// 并上传拿 fileNo(隐私政策不需要走这套流程,与个人开户一致)
|
||||||
field: 'serviceAgreed'
|
const generating = ref(false)
|
||||||
},
|
const generateError = ref('')
|
||||||
privacy: {
|
const payAgreementFileNo = ref('')
|
||||||
title: '是否同意《浙江稠州商业银行用户支付服务隐私政策》',
|
// "下一步"必须在协议文件真正生成并上传成功后才允许点击,避免带着空的 payAgreementFileNo 提交
|
||||||
field: 'privacyAgreed'
|
const canProceed = computed(() => bothAgreed.value && !!payAgreementFileNo.value && !generating.value)
|
||||||
|
|
||||||
|
watch(bothAgreed, (val) => {
|
||||||
|
if (val && !payAgreementFileNo.value && !generating.value) {
|
||||||
|
generateAndUpload()
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
// 点击协议名链接:弹出确认对话框(原型截图里是嵌入式浏览器对原生 window.confirm() 的展示效果,
|
async function generateAndUpload() {
|
||||||
// 本次改用 Vant 组件化的 showConfirmDialog,与项目其余弹窗风格保持一致,不刻意还原原生浏览器
|
if (generating.value) return
|
||||||
// 弹窗的域名前缀展示)。确定则勾选对应复选框,取消不做任何改动。
|
if (!props.enterpriseName || !props.certificateNumber || !props.channelNo) {
|
||||||
async function confirmAgreement(type) {
|
generateError.value = '该开户申请缺少企业名称/统一社会信用代码/渠道信息,无法生成协议文件,请联系柜员核实后重新生成二维码'
|
||||||
const info = AGREEMENT_CONTENT[type]
|
return
|
||||||
|
}
|
||||||
|
generating.value = true
|
||||||
|
generateError.value = ''
|
||||||
try {
|
try {
|
||||||
await showConfirmDialog({ title: info.title, confirmButtonText: '确定', cancelButtonText: '取消' })
|
const templateRes = await fetchBankAgreementApi('PAY_SERVICE_AGREEMENT')
|
||||||
if (info.field === 'serviceAgreed') serviceAgreed.value = true
|
const templateBase64 = templateRes.data?.code === 200 ? templateRes.data.data?.fileContent : ''
|
||||||
else privacyAgreed.value = true
|
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 {
|
} 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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.step-body {
|
.step-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: calc(100vh - 46px);
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
padding: 4px 16px 4px;
|
padding: 4px 16px 4px;
|
||||||
}
|
}
|
||||||
.agreement-row {
|
.tip {
|
||||||
padding: 8px 0;
|
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;
|
font-size: 14px;
|
||||||
color: #646566;
|
color: #646566;
|
||||||
}
|
}
|
||||||
.link {
|
.link {
|
||||||
color: #1989fa;
|
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 {
|
.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>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,8 @@ const props = defineProps({
|
||||||
mobilePhoneMask: { type: String, default: '' },
|
mobilePhoneMask: { type: String, default: '' },
|
||||||
cardNo: { type: String, default: '' },
|
cardNo: { type: String, default: '' },
|
||||||
cardName: { type: String, default: '' },
|
cardName: { type: String, default: '' },
|
||||||
idNo: { type: String, default: '' }
|
idNo: { type: String, default: '' },
|
||||||
|
payAgreementFileNo: { type: String, default: '' }
|
||||||
})
|
})
|
||||||
const emit = defineEmits(['submitted'])
|
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() {
|
async function handleSubmit() {
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
|
|
@ -104,7 +107,8 @@ async function handleSubmit() {
|
||||||
channelNo: props.channelNo,
|
channelNo: props.channelNo,
|
||||||
verifyCode: verifyCode.value,
|
verifyCode: verifyCode.value,
|
||||||
id: props.id,
|
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) {
|
if (res.data?.code === 200) {
|
||||||
emit('submitted', res.data.data)
|
emit('submitted', res.data.data)
|
||||||
|
|
|
||||||
|
|
@ -101,9 +101,20 @@ export const dictTypes = [
|
||||||
'前端只提交最终选中叶子字典项的 itemValue 字符串;叶子文案为"其他"的项用于触发前端' +
|
'前端只提交最终选中叶子字典项的 itemValue 字符串;叶子文案为"其他"的项用于触发前端' +
|
||||||
'"职业备注"必填校验,详见《缺失的后端接口.md》',
|
'"职业备注"必填校验,详见《缺失的后端接口.md》',
|
||||||
createdAt: '2026-08-06 10:00:00'
|
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 = [
|
export const dictItems = [
|
||||||
// CUST_TYPE:普通字典,仅一层
|
// 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: 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: '' },
|
{ id: 45, dictTypeCode: 'job', parentId: 32, itemValue: 'JOB_08_03', itemLabel: '其他', sortOrder: 3, status: 'ENABLED', remark: '' },
|
||||||
// 叶子文案精确等于"其他"两个字,前端 PersonalOpenForm.vue 靠这个约定判断是否要求填写职业备注
|
// 叶子文案精确等于"其他"两个字,前端 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 第一节。
|
docs/superpowers/specs/2026-07-28-enterprise-open-application-redesign.md 第一节。
|
||||||
项目里没有联行号字典查询接口(已登记《缺失的后端接口.md》),三种方式最终都只产出
|
三种方式最终都只产出 bankNo/bankName 两个已有字段,不新增后端字段:
|
||||||
bankNo/bankName 两个已有字段,不新增后端字段:
|
- 快速:2026-08-07 改为动态字典选择(dictTypeCode='kaihuhang'),按产品明确要求(2026-08-07
|
||||||
- 快速:前端硬编码常见银行名称下拉,仅产出 bankName,bankNo 留空
|
二次修正):提交给后端的 bankName 是选中字典项的 itemLabel(中文银行名称,如"中国工商
|
||||||
- 详细:省市(RegionCascader)+支行名称拼接产出 bankName,bankNo 留空
|
银行"),bankNo 是该字典项的 itemValue(联行号/银行编码,如"102100099996")——即字典项的
|
||||||
|
"标签"对应 bankName,"值"对应 bankNo,与第一版"bankName=itemValue"的理解不同,已按此
|
||||||
|
修正。字典类型 'kaihuhang' 真实后端目前不存在,需要后端/管理员在"动态字典管理"模块新建,
|
||||||
|
详见《缺失的后端接口.md》登记。
|
||||||
|
- 详细:省市(RegionCascader)+支行名称拼接产出 bankName,bankNo 留空(无联行号来源)
|
||||||
- 手动:bankNo/bankName 两个文本框直接输入(现状写法)
|
- 手动:bankNo/bankName 两个文本框直接输入(现状写法)
|
||||||
切换 Tab 不会清空已填值,只是切换录入方式,只有用户在新 Tab 内实际操作控件时才会覆盖值。
|
切换 Tab 不会清空已填值,只是切换录入方式,只有用户在新 Tab 内实际操作控件时才会覆盖值。
|
||||||
|
|
||||||
|
|
@ -24,18 +28,14 @@
|
||||||
</a-radio-group>
|
</a-radio-group>
|
||||||
|
|
||||||
<div class="bank-selector-panel">
|
<div class="bank-selector-panel">
|
||||||
<a-select
|
<DictSelect
|
||||||
v-if="activeTab === 'quick'"
|
v-if="activeTab === 'quick'"
|
||||||
:value="bankName || undefined"
|
:model-value="bankNo || undefined"
|
||||||
|
dict-type-code="kaihuhang"
|
||||||
placeholder="请选择开户行"
|
placeholder="请选择开户行"
|
||||||
allow-clear
|
|
||||||
show-search
|
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
style="width: 100%"
|
@update:model-value="handleQuickSelect"
|
||||||
@update:value="handleQuickSelect"
|
/>
|
||||||
>
|
|
||||||
<a-select-option v-for="name in COMMON_BANK_NAMES" :key="name" :value="name">{{ name }}</a-select-option>
|
|
||||||
</a-select>
|
|
||||||
|
|
||||||
<a-space v-else-if="activeTab === 'detail'" direction="vertical" style="width: 100%">
|
<a-space v-else-if="activeTab === 'detail'" direction="vertical" style="width: 100%">
|
||||||
<RegionCascader v-model:value="detailRegion" :disabled="disabled" @update:text="handleDetailRegionText" />
|
<RegionCascader v-model:value="detailRegion" :disabled="disabled" @update:text="handleDetailRegionText" />
|
||||||
|
|
@ -68,8 +68,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import RegionCascader from '@/components/RegionCascader.vue'
|
import RegionCascader from '@/components/RegionCascader.vue'
|
||||||
|
import DictSelect from '@/components/DictSelect.vue'
|
||||||
|
import { useDictStore } from '@/stores/dict'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
bankNo: { type: String, default: '' },
|
bankNo: { type: String, default: '' },
|
||||||
|
|
@ -84,22 +86,27 @@ const detailRegion = ref([])
|
||||||
const detailRegionText = ref('')
|
const detailRegionText = ref('')
|
||||||
const detailBranchName = 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) {
|
function handleQuickSelect(value) {
|
||||||
emit('update:bankName', value || '')
|
if (!value) {
|
||||||
emit('update:bankNo', '')
|
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) {
|
function handleDetailRegionText(text) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue