Compare commits
2 Commits
63555f9094
...
d2030f1711
| Author | SHA1 | Date |
|---|---|---|
|
|
d2030f1711 | |
|
|
2e879255ed |
|
|
@ -1,3 +1,4 @@
|
|||
VITE_API_BASE_URL=http://localhost:8888
|
||||
npmhttp://www.baidu.com
|
||||
VITE_H5_QRCODE_BASE_URL=http://www.baidu.com
|
||||
VITE_H5_QRCODE_PATH=/mobile/personal-open
|
||||
VITE_H5_QRCODE_PATH_ENTERPRISE=/mobile/enterprise-open
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
VITE_API_BASE_URL=
|
||||
VITE_H5_QRCODE_BASE_URL=
|
||||
VITE_H5_QRCODE_PATH=/mobile/personal-open
|
||||
VITE_H5_QRCODE_PATH_ENTERPRISE=/mobile/enterprise-open
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
import request from './request'
|
||||
|
||||
// 企业开户移动端H5确认页接口契约(2026-08-07新增,详见《缺失的后端接口.md》登记):
|
||||
// - draft-detail:真实后端目前没有对应的匿名详情查询接口(与个人开户不同,企业开户后端仅提供
|
||||
// confirm 这一个"直接提交确认"接口),本次前端参照个人开户 mobile/draft-detail 的契约模式
|
||||
// 假设新增该接口,详情字段(尤其 id/channelName)均为前端假设,待后端确认。
|
||||
// - send-verify-code:企业开户没有专属的发码接口,直接复用个人开户H5确认页在用的通用发码接口
|
||||
// (该接口本身是透传给短信网关的通用参数,技术上可复用,待后端确认是否允许非个人开户场景调用)。
|
||||
// tradeNo/tradeType 固定值取自个人开户H5当前已修正的正确取值(2026-08-06 修正记录,见
|
||||
// personalOpen.js 同名字段注释):tradeNo 固定 "221506",tradeType 固定 "0"。
|
||||
// - confirm:后端已提供的真实接口,语义为"申请人扫码核实信息后点击确认,提交凡荣e链",
|
||||
// 与个人开户的 mobile/submit 不同,请求体是 {channelNo, verifyCode, confirmBto:{id,
|
||||
// applicationStatus, confirmTime}},confirmBto.id 取自本次假设的 draft-detail 接口。
|
||||
|
||||
export const fetchDraftDetailApi = (applicationNo) =>
|
||||
request.post('/api/wallet-management/enterprise-open-application/mobile/draft-detail', { applicationNo })
|
||||
|
||||
// cardName(银行卡户名):企业场景没有个人开户那种"卡户名与客户姓名一致"的天然对应关系,
|
||||
// 企业银行账号的户名通常即企业名称,故取 enterpriseName 代替(与个人开户用 customerName
|
||||
// 的处理思路一致)。
|
||||
export const sendVerifyCodeApi = ({ channelNo, mobile, cardNo, cardName, idNo }) =>
|
||||
request.post('/api/wallet-management/personal-opening/mobile/send-verify-code/general', {
|
||||
channelNo,
|
||||
tradeNo: '221506',
|
||||
tradeType: '0',
|
||||
mobile,
|
||||
cardNo,
|
||||
cardName,
|
||||
idNo
|
||||
})
|
||||
|
||||
// confirm 为真实已有接口,直接对接真实后端(不在 mock-server 实现),applicationStatus 固定传
|
||||
// 'CONFIRMED'(枚举含义"已确认",对应"申请人扫码确认"这一步),confirmTime 为提交时的当前时间。
|
||||
export const confirmApplicationApi = ({ channelNo, verifyCode, id, confirmTime }) =>
|
||||
request.post('/api/wallet-management/enterprise-open-application/confirm', {
|
||||
channelNo,
|
||||
verifyCode,
|
||||
confirmBto: {
|
||||
id,
|
||||
applicationStatus: 'CONFIRMED',
|
||||
confirmTime
|
||||
}
|
||||
})
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
// 与主项目 src/router/index.js 的定位不同:本工程无需登录鉴权/动态菜单路由,
|
||||
// 仅一个业务路由,直接静态声明即可。
|
||||
// 业务路由静态声明即可。
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import PersonalOpenMobile from '@/views/PersonalOpenMobile.vue'
|
||||
import EnterpriseOpenMobile from '@/views/EnterpriseOpenMobile.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
|
|
@ -11,7 +12,12 @@ const router = createRouter({
|
|||
name: 'PersonalOpenMobile',
|
||||
component: PersonalOpenMobile
|
||||
},
|
||||
// 兜底:任何其它路径都重定向到唯一业务页面(本工程不做多页面导航)
|
||||
{
|
||||
path: '/mobile/enterprise-open',
|
||||
name: 'EnterpriseOpenMobile',
|
||||
component: EnterpriseOpenMobile
|
||||
},
|
||||
// 兜底:未知路径重定向到个人开户页面(本工程默认业务场景,与既有行为保持一致)
|
||||
{ path: '/:pathMatch(.*)*', redirect: '/mobile/personal-open' }
|
||||
]
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,246 @@
|
|||
<!-- src/views/EnterpriseOpenMobile.vue -->
|
||||
<!-- 企业开户移动端H5确认页(2026-08-07新增),扫码入口来自管理后台企业开户表单"邀请扫码办理"
|
||||
生成的二维码。与个人开户H5(PersonalOpenMobile.vue)的"整屏替换单步"视觉不同,按产品提供的
|
||||
原型截图实现为"手风琴式竖直步骤列表":顶部蓝色横幅(业务确认+银行品牌),下方1-4步骤单页内
|
||||
纵向排列,已完成步骤折叠为"✓ 步骤名"一行,当前步骤展开显示内容,未开始步骤折叠为灰色
|
||||
"圆圈数字 步骤名"一行。截图里没有任何"上一步"按钮,本页面同样不支持步骤回退(与企业开户
|
||||
confirm 接口 DRAFT→CONFIRMED 的单向状态迁移语义一致)。
|
||||
后端契约缺口(详见《缺失的后端接口.md》与 src/api/enterpriseOpen.js 顶部注释):
|
||||
- mobile/draft-detail 是本次前端假设新增的接口,后端目前未提供
|
||||
- 发验证码复用个人开户H5在用的通用接口
|
||||
- 提交确认(confirm)是真实已有接口,本地 mock-server 未实现,只能连真实后端环境验证 -->
|
||||
<template>
|
||||
<div class="page">
|
||||
<div v-if="loading" class="center-box">
|
||||
<van-loading type="spinner" size="24px">加载中...</van-loading>
|
||||
</div>
|
||||
|
||||
<van-empty v-else-if="loadError" image="error" :description="loadError" class="center-box" />
|
||||
|
||||
<template v-else>
|
||||
<div class="banner">
|
||||
<span class="banner-title">业务确认</span>
|
||||
<div class="banner-brand">
|
||||
<span class="brand-badge">+</span>
|
||||
<span class="brand-name">浙江稠州商业银行</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step-list">
|
||||
<div class="step-row" :class="rowClass('INFO')">
|
||||
<div class="step-row-header">
|
||||
<span class="step-icon">
|
||||
<van-icon v-if="isDone('INFO')" name="success" class="icon-done" />
|
||||
<span v-else class="step-number">1</span>
|
||||
</span>
|
||||
<span class="step-label">信息核对</span>
|
||||
</div>
|
||||
<EnterpriseInfoConfirmStep
|
||||
v-if="currentStep === 'INFO'"
|
||||
:detail="draftDetail"
|
||||
@next="currentStep = 'AGREEMENT'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="step-row" :class="rowClass('AGREEMENT')">
|
||||
<div class="step-row-header">
|
||||
<span class="step-icon">
|
||||
<van-icon v-if="isDone('AGREEMENT')" name="success" class="icon-done" />
|
||||
<span v-else class="step-number">2</span>
|
||||
</span>
|
||||
<span class="step-label">相关协议</span>
|
||||
</div>
|
||||
<EnterpriseAgreementStep v-if="currentStep === 'AGREEMENT'" @next="currentStep = 'SMS'" />
|
||||
</div>
|
||||
|
||||
<div class="step-row" :class="rowClass('SMS')">
|
||||
<div class="step-row-header">
|
||||
<span class="step-icon">
|
||||
<van-icon v-if="isDone('SMS')" name="success" class="icon-done" />
|
||||
<span v-else class="step-number">3</span>
|
||||
</span>
|
||||
<span class="step-label">手机验证</span>
|
||||
</div>
|
||||
<EnterpriseSmsVerifyStep
|
||||
v-if="currentStep === 'SMS'"
|
||||
:id="draftDetail?.id"
|
||||
:channel-no="draftDetail?.channelNo"
|
||||
:mobile="draftDetail?.mobilePhone"
|
||||
:mobile-phone-mask="draftDetail?.mobilePhoneMask"
|
||||
:card-no="draftDetail?.bankCardNumber"
|
||||
:card-name="draftDetail?.enterpriseName"
|
||||
:id-no="draftDetail?.certificateNumber"
|
||||
@submitted="handleSubmitted"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="step-row" :class="rowClass('RESULT')">
|
||||
<div class="step-row-header">
|
||||
<span class="step-icon">
|
||||
<van-icon v-if="isDone('RESULT')" name="success" class="icon-done" />
|
||||
<span v-else class="step-number">4</span>
|
||||
</span>
|
||||
<span class="step-label">完成</span>
|
||||
</div>
|
||||
<ResultStep v-if="currentStep === 'RESULT'" :result="resultData" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { fetchDraftDetailApi } from '@/api/enterpriseOpen'
|
||||
import EnterpriseInfoConfirmStep from './steps/enterprise/EnterpriseInfoConfirmStep.vue'
|
||||
import EnterpriseAgreementStep from './steps/enterprise/EnterpriseAgreementStep.vue'
|
||||
import EnterpriseSmsVerifyStep from './steps/enterprise/EnterpriseSmsVerifyStep.vue'
|
||||
import ResultStep from './steps/ResultStep.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const applicationNo = route.query.applicationNo || ''
|
||||
|
||||
const loading = ref(true)
|
||||
const loadError = ref('')
|
||||
const draftDetail = ref(null)
|
||||
|
||||
// 4个步骤:INFO(信息核对) -> AGREEMENT(相关协议) -> SMS(手机验证+提交) -> RESULT(结果页,终态)
|
||||
// 截图没有任何"上一步"交互,本页面不支持步骤回退
|
||||
const STEP_ORDER = ['INFO', 'AGREEMENT', 'SMS', 'RESULT']
|
||||
const currentStep = ref('INFO')
|
||||
const resultData = ref(null)
|
||||
|
||||
function isDone(key) {
|
||||
return STEP_ORDER.indexOf(key) < STEP_ORDER.indexOf(currentStep.value)
|
||||
}
|
||||
function rowClass(key) {
|
||||
if (isDone(key)) return 'is-done'
|
||||
if (key === currentStep.value) return 'is-current'
|
||||
return 'is-pending'
|
||||
}
|
||||
|
||||
function handleSubmitted(data) {
|
||||
resultData.value = data
|
||||
currentStep.value = 'RESULT'
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!applicationNo) {
|
||||
loading.value = false
|
||||
loadError.value = '链接无效,缺少申请编号,请重新扫描二维码'
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await fetchDraftDetailApi(applicationNo)
|
||||
if (res.data?.code === 200) {
|
||||
draftDetail.value = res.data.data
|
||||
} else {
|
||||
loadError.value = res.data?.message || '获取开户信息失败,请联系柜员重新生成二维码'
|
||||
}
|
||||
} catch {
|
||||
// 网络异常已由 request.js 响应拦截器统一提示,这里仅需要把页面切到错误态
|
||||
loadError.value = '获取开户信息失败,请检查网络后重新扫描二维码'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
.center-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
}
|
||||
.banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
background: linear-gradient(90deg, #1677ff, #4096ff);
|
||||
color: #fff;
|
||||
}
|
||||
.banner-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.banner-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.brand-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
color: #ee0a24;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
}
|
||||
.brand-name {
|
||||
font-size: 12px;
|
||||
}
|
||||
.step-list {
|
||||
padding: 12px 16px 24px;
|
||||
}
|
||||
.step-row {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.step-row-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
.step-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.step-number {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: #e8e8e8;
|
||||
color: #969799;
|
||||
font-size: 13px;
|
||||
}
|
||||
.icon-done {
|
||||
color: #07c160;
|
||||
font-size: 20px;
|
||||
}
|
||||
.step-label {
|
||||
font-size: 15px;
|
||||
color: #969799;
|
||||
}
|
||||
.is-current .step-number {
|
||||
background: #1677ff;
|
||||
color: #fff;
|
||||
}
|
||||
.is-current .step-label {
|
||||
color: #323233;
|
||||
font-weight: 600;
|
||||
}
|
||||
.is-done .step-label {
|
||||
color: #323233;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
<template>
|
||||
<div class="step-body">
|
||||
<div class="agreement-row">
|
||||
<van-checkbox v-model="serviceAgreed">
|
||||
我已阅读并同意
|
||||
<span class="link" @click.stop="confirmAgreement('service')">《浙江稠州商业银行用户支付服务协议》</span>
|
||||
</van-checkbox>
|
||||
</div>
|
||||
<div class="agreement-row">
|
||||
<van-checkbox v-model="privacyAgreed">
|
||||
我已阅读并同意
|
||||
<span class="link" @click.stop="confirmAgreement('privacy')">《浙江稠州商业银行用户支付服务隐私政策》</span>
|
||||
</van-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<van-button type="primary" block round :disabled="!bothAgreed" @click="$emit('next')">下一步</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { showConfirmDialog } from 'vant'
|
||||
|
||||
defineEmits(['next'])
|
||||
|
||||
const serviceAgreed = ref(false)
|
||||
const privacyAgreed = ref(false)
|
||||
const bothAgreed = computed(() => serviceAgreed.value && privacyAgreed.value)
|
||||
|
||||
// 协议名称/占位正文与管理后台 EnterpriseOpenForm.vue/PersonalOpenForm.vue 使用的同一套占位文案
|
||||
// 保持一致(与个人开户H5 AgreementStep.vue 现在使用的"PDF填充+上传"完全不同的流程,本次企业
|
||||
// 开户H5严格按截图展示的简单勾选+确认弹窗交互,不涉及PDF生成/上传)。
|
||||
const AGREEMENT_CONTENT = {
|
||||
service: {
|
||||
title: '是否同意《浙江稠州商业银行用户支付服务协议》',
|
||||
field: 'serviceAgreed'
|
||||
},
|
||||
privacy: {
|
||||
title: '是否同意《浙江稠州商业银行用户支付服务隐私政策》',
|
||||
field: 'privacyAgreed'
|
||||
}
|
||||
}
|
||||
|
||||
// 点击协议名链接:弹出确认对话框(原型截图里是嵌入式浏览器对原生 window.confirm() 的展示效果,
|
||||
// 本次改用 Vant 组件化的 showConfirmDialog,与项目其余弹窗风格保持一致,不刻意还原原生浏览器
|
||||
// 弹窗的域名前缀展示)。确定则勾选对应复选框,取消不做任何改动。
|
||||
async function confirmAgreement(type) {
|
||||
const info = AGREEMENT_CONTENT[type]
|
||||
try {
|
||||
await showConfirmDialog({ title: info.title, confirmButtonText: '确定', cancelButtonText: '取消' })
|
||||
if (info.field === 'serviceAgreed') serviceAgreed.value = true
|
||||
else privacyAgreed.value = true
|
||||
} catch {
|
||||
// 用户点击"取消",不做任何改动
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.step-body {
|
||||
padding: 4px 16px 4px;
|
||||
}
|
||||
.agreement-row {
|
||||
padding: 8px 0;
|
||||
font-size: 14px;
|
||||
color: #646566;
|
||||
}
|
||||
.link {
|
||||
color: #1989fa;
|
||||
}
|
||||
.footer {
|
||||
padding: 12px 0 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<template>
|
||||
<div class="step-body">
|
||||
<van-cell-group inset>
|
||||
<van-cell title="业务类型" value="稠州企业开户" />
|
||||
<van-cell title="办理渠道" :value="dealChannelText" />
|
||||
<van-cell title="客户" :value="detail?.enterpriseName || '-'" />
|
||||
<van-cell title="手机号" :value="detail?.mobilePhoneMask || '-'" />
|
||||
</van-cell-group>
|
||||
|
||||
<div class="footer">
|
||||
<van-button type="primary" block round @click="$emit('next')">确认无误,下一步</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
detail: { type: Object, default: null }
|
||||
})
|
||||
defineEmits(['next'])
|
||||
|
||||
// "业务类型"后端无对应字段,前端固定文案展示(与截图一致);"办理渠道"取假设新增的
|
||||
// channelName 字段(后端目前未提供,详见 src/api/enterpriseOpen.js 顶部注释),固定拼接
|
||||
// "(线上)"后缀,因为本页面本身就是线上H5办理场景
|
||||
const dealChannelText = computed(() => `${props.detail?.channelName || '-'}(线上)`)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.step-body {
|
||||
padding: 0 0 4px;
|
||||
}
|
||||
.footer {
|
||||
padding: 16px 16px 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
<template>
|
||||
<div class="step-body">
|
||||
<van-cell-group inset>
|
||||
<van-cell title="手机号" :value="mobilePhoneMask || '-'" />
|
||||
</van-cell-group>
|
||||
|
||||
<div class="verify-row">
|
||||
<van-field v-model="verifyCode" type="digit" maxlength="6" placeholder="请输入短信验证码" class="verify-input" />
|
||||
<van-button
|
||||
size="small"
|
||||
type="primary"
|
||||
plain
|
||||
class="send-btn"
|
||||
:disabled="countdown > 0 || sending"
|
||||
:loading="sending"
|
||||
@click="handleSendCode"
|
||||
>
|
||||
{{ countdown > 0 ? `${countdown}s后重新获取` : '发送' }}
|
||||
</van-button>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<van-button
|
||||
type="primary"
|
||||
block
|
||||
round
|
||||
:disabled="!verifyCode || submitting"
|
||||
:loading="submitting"
|
||||
loading-text="提交中..."
|
||||
@click="handleSubmit"
|
||||
>
|
||||
提交
|
||||
</van-button>
|
||||
</div>
|
||||
|
||||
<!-- 耗时操作统一等待态:提交期间整页遮罩,禁止用户进行其它操作 -->
|
||||
<van-overlay :show="submitting" class="submit-overlay">
|
||||
<div class="overlay-center">
|
||||
<van-loading type="spinner" color="#fff" size="28px">提交中,请稍候...</van-loading>
|
||||
</div>
|
||||
</van-overlay>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { showToast } from 'vant'
|
||||
import { sendVerifyCodeApi, confirmApplicationApi } from '@/api/enterpriseOpen'
|
||||
|
||||
const props = defineProps({
|
||||
id: { type: [Number, String], default: undefined },
|
||||
channelNo: { type: String, default: '' },
|
||||
mobile: { type: String, default: '' },
|
||||
mobilePhoneMask: { type: String, default: '' },
|
||||
cardNo: { type: String, default: '' },
|
||||
cardName: { type: String, default: '' },
|
||||
idNo: { type: String, default: '' }
|
||||
})
|
||||
const emit = defineEmits(['submitted'])
|
||||
|
||||
const verifyCode = ref('')
|
||||
const sending = ref(false)
|
||||
const submitting = ref(false)
|
||||
const countdown = ref(0)
|
||||
let timer = null
|
||||
|
||||
function startCountdown() {
|
||||
countdown.value = 60
|
||||
timer = setInterval(() => {
|
||||
countdown.value -= 1
|
||||
if (countdown.value <= 0) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
async function handleSendCode() {
|
||||
sending.value = true
|
||||
try {
|
||||
const res = await sendVerifyCodeApi({
|
||||
channelNo: props.channelNo,
|
||||
mobile: props.mobile,
|
||||
cardNo: props.cardNo,
|
||||
cardName: props.cardName,
|
||||
idNo: props.idNo
|
||||
})
|
||||
if (res.data?.code === 200) {
|
||||
showToast('验证码已发送')
|
||||
startCountdown()
|
||||
}
|
||||
// 非200时全局响应拦截器已弹窗提示,这里无需重复处理
|
||||
} finally {
|
||||
sending.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 提交确认:直接对接真实已有的 enterprise-open-application/confirm 接口,confirmTime 取当前时间
|
||||
async function handleSubmit() {
|
||||
submitting.value = true
|
||||
try {
|
||||
const res = await confirmApplicationApi({
|
||||
channelNo: props.channelNo,
|
||||
verifyCode: verifyCode.value,
|
||||
id: props.id,
|
||||
confirmTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
|
||||
})
|
||||
if (res.data?.code === 200) {
|
||||
emit('submitted', res.data.data)
|
||||
} else {
|
||||
// 验证码错误等业务异常:全局拦截器已弹窗提示,停留本页允许重试,清空验证码便于重新输入
|
||||
verifyCode.value = ''
|
||||
}
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.step-body {
|
||||
padding: 0 0 4px;
|
||||
}
|
||||
.verify-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 16px;
|
||||
background: #f7f8fa;
|
||||
border-radius: 8px;
|
||||
padding: 4px 12px 4px 0;
|
||||
}
|
||||
.verify-input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
}
|
||||
.send-btn {
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.footer {
|
||||
padding: 4px 16px 4px;
|
||||
}
|
||||
.submit-overlay {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.overlay-center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -380,6 +380,34 @@
|
|||
<PersonalCustomerPicker ref="legalRepPickerRef" @select="handleSelectLegalRep" />
|
||||
<PersonalCustomerPicker ref="rolePickerRef" @select="handleRolePersonSelect" />
|
||||
<PersonalCustomerPicker ref="beneficiaryPickerRef" @select="handleAddBeneficiary" />
|
||||
|
||||
<!-- "确认"按钮的二次确认弹窗,仿 PersonalOpenForm.vue:confirm=文案+三按钮态,
|
||||
qrcode=邀请扫码办理成功后的二维码展示态 -->
|
||||
<a-modal
|
||||
v-model:open="confirmModalVisible"
|
||||
title="提示"
|
||||
:footer="null"
|
||||
:mask-closable="false"
|
||||
@cancel="handleModalCancel"
|
||||
>
|
||||
<template v-if="confirmModalStep === 'confirm'">
|
||||
<p>是否确认开户?</p>
|
||||
<div class="confirm-modal-actions">
|
||||
<a-space>
|
||||
<a-button type="primary" :loading="modalSaving" :disabled="modalInviting" @click="handleModalSave">保存</a-button>
|
||||
<a-button :disabled="modalSaving || modalInviting" @click="handleModalCancel">取消</a-button>
|
||||
<a-button :loading="modalInviting" :disabled="modalSaving" @click="handleModalInvite">邀请扫码办理</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="qrcode-panel">
|
||||
<qrcode-vue :value="qrCodeUrl" :size="200" level="M" />
|
||||
<p class="qrcode-application-no">申请编号:{{ qrApplicationNo }}</p>
|
||||
<a-button @click="handleModalCancel">关闭</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</a-modal>
|
||||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -389,6 +417,7 @@ import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
|
|||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
import ChannelSelect from '@/components/ChannelSelect.vue'
|
||||
import DictSelect from '@/components/DictSelect.vue'
|
||||
import LicenseUpload from '@/components/LicenseUpload.vue'
|
||||
|
|
@ -427,6 +456,15 @@ const channelNo = ref(undefined)
|
|||
const agreementChecked = ref(false)
|
||||
const submitting = ref(false)
|
||||
|
||||
// "确认"按钮触发的二次确认弹窗(仅 create 模式使用):confirm=文案+三按钮态,
|
||||
// qrcode=邀请扫码办理成功后的二维码展示态,仿 PersonalOpenForm.vue
|
||||
const confirmModalVisible = ref(false)
|
||||
const confirmModalStep = ref('confirm')
|
||||
const modalSaving = ref(false)
|
||||
const modalInviting = ref(false)
|
||||
const qrCodeUrl = ref('')
|
||||
const qrApplicationNo = ref('')
|
||||
|
||||
const businessLicenseFileNo = ref('')
|
||||
const legalPersonIdFrontFileNo = ref('')
|
||||
const legalPersonIdBackFileNo = ref('')
|
||||
|
|
@ -554,6 +592,8 @@ let smsTimer = null
|
|||
const busy = computed(
|
||||
() =>
|
||||
submitting.value ||
|
||||
modalSaving.value ||
|
||||
modalInviting.value ||
|
||||
businessLicenseUploading.value ||
|
||||
legalPersonIdFrontUploading.value ||
|
||||
legalPersonIdBackUploading.value ||
|
||||
|
|
@ -565,7 +605,7 @@ const busyTip = computed(() => {
|
|||
if (legalPersonIdFrontUploading.value || legalPersonIdBackUploading.value) return '身份证照片上传中,请稍候...'
|
||||
if (beneficiaryProofUploading.value) return '受益人材料证明上传中,请稍候...'
|
||||
if (smsSending.value) return '验证码发送中,请稍候...'
|
||||
if (submitting.value) return '提交中,请稍候...'
|
||||
if (submitting.value || modalSaving.value || modalInviting.value) return '提交中,请稍候...'
|
||||
return ''
|
||||
})
|
||||
|
||||
|
|
@ -1055,50 +1095,52 @@ function buildBeneficiaryBto(bene) {
|
|||
return bto
|
||||
}
|
||||
|
||||
// 提交给 create/update 的公共 payload,create(直接提交/邀请扫码办理)与 update(编辑保存)
|
||||
// 内容一致,仅 update 调用方需额外拼接 id 字段
|
||||
function buildSubmitPayload() {
|
||||
return {
|
||||
organizationId: form.organizationId,
|
||||
enterpriseId: form.enterpriseId,
|
||||
enterpriseName: form.enterpriseName,
|
||||
businessLicense: form.businessLicense,
|
||||
certificateType: 'BUSINESS_LICENSE',
|
||||
certificateNumber: form.certificateNumber,
|
||||
certificateEffectiveDate: form.certificateEffectiveDate,
|
||||
certificateExpiryDate: form.certificateExpiryDate,
|
||||
certificateAddress: form.certificateAddress,
|
||||
businessScope: form.businessScope,
|
||||
industry: form.industry,
|
||||
mobilePhone: smsForm.mobile,
|
||||
bankCardNumber: form.bankCardNumber,
|
||||
bankNo: form.bankNo,
|
||||
bankName: form.bankName,
|
||||
...buildLegalRepPayload(),
|
||||
...buildControllerPayload(),
|
||||
...buildLeaderPayload(),
|
||||
...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' },
|
||||
{ fileNo: legalPersonIdBackFileNo.value, fileType: '02' },
|
||||
{ fileNo: beneficiaryProofFileNo.value, fileType: '14' }
|
||||
],
|
||||
enterpriseAccountOpenApplicationBeneficiaryBtoList: beneficiaryList.value.map(buildBeneficiaryBto)
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑模式"保存"按钮:校验通过后直接调用 update 接口,不经过二次确认弹窗(与
|
||||
// PersonalOpenForm.vue 的 handleEditSubmit 行为一致)
|
||||
async function handleSubmit() {
|
||||
submitting.value = true
|
||||
try {
|
||||
const payload = {
|
||||
organizationId: form.organizationId,
|
||||
enterpriseId: form.enterpriseId,
|
||||
enterpriseName: form.enterpriseName,
|
||||
businessLicense: form.businessLicense,
|
||||
certificateType: 'BUSINESS_LICENSE',
|
||||
certificateNumber: form.certificateNumber,
|
||||
certificateEffectiveDate: form.certificateEffectiveDate,
|
||||
certificateExpiryDate: form.certificateExpiryDate,
|
||||
certificateAddress: form.certificateAddress,
|
||||
businessScope: form.businessScope,
|
||||
industry: form.industry,
|
||||
mobilePhone: smsForm.mobile,
|
||||
bankCardNumber: form.bankCardNumber,
|
||||
bankNo: form.bankNo,
|
||||
bankName: form.bankName,
|
||||
...buildLegalRepPayload(),
|
||||
...buildControllerPayload(),
|
||||
...buildLeaderPayload(),
|
||||
...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' },
|
||||
{ fileNo: legalPersonIdBackFileNo.value, fileType: '02' },
|
||||
{ fileNo: beneficiaryProofFileNo.value, fileType: '14' }
|
||||
],
|
||||
enterpriseAccountOpenApplicationBeneficiaryBtoList: beneficiaryList.value.map(buildBeneficiaryBto)
|
||||
}
|
||||
let res
|
||||
if (isCreate.value) {
|
||||
res = await createEnterpriseOpenApplicationApi(payload)
|
||||
} else {
|
||||
// 已知缺口:后端 list/detail 均不返回 id,此处仍用 applicationNo 兜底传递,以实测报错为准
|
||||
res = await updateEnterpriseOpenApplicationApi({ id: route.query.applicationNo, ...payload })
|
||||
}
|
||||
// 已知缺口:后端 list/detail 均不返回 id,此处仍用 applicationNo 兜底传递,以实测报错为准
|
||||
const res = await updateEnterpriseOpenApplicationApi({ id: route.query.applicationNo, ...buildSubmitPayload() })
|
||||
if (res.data.code === 200) {
|
||||
message.success(isCreate.value ? '开户申请已提交' : '修改成功')
|
||||
message.success('修改成功')
|
||||
goBack()
|
||||
}
|
||||
} finally {
|
||||
|
|
@ -1106,12 +1148,62 @@ async function handleSubmit() {
|
|||
}
|
||||
}
|
||||
|
||||
// "确认"按钮点击:create 模式校验通过后打开二次确认弹窗(不直接提交);edit 模式沿用现状直接保存
|
||||
async function handleConfirmButtonClick() {
|
||||
const valid = await validateBeforeSubmit()
|
||||
if (!valid) return
|
||||
if (isCreate.value) {
|
||||
confirmModalStep.value = 'confirm'
|
||||
confirmModalVisible.value = true
|
||||
return
|
||||
}
|
||||
await handleSubmit()
|
||||
}
|
||||
|
||||
// 弹窗内"保存":调用 create 接口提交草稿,成功后关闭弹窗并返回列表
|
||||
async function handleModalSave() {
|
||||
modalSaving.value = true
|
||||
try {
|
||||
const res = await createEnterpriseOpenApplicationApi(buildSubmitPayload())
|
||||
if (res.data.code === 200) {
|
||||
message.success('开户申请已提交')
|
||||
confirmModalVisible.value = false
|
||||
goBack()
|
||||
}
|
||||
} finally {
|
||||
modalSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 拼接扫码 H5 地址:域名/路径来自环境变量,域名未配置(生产环境尚未确定)时用占位域名兜底,
|
||||
// 保证二维码始终可以生成,不阻断柜员操作;域名确定后只需修改 .env.production,无需改代码。
|
||||
// 路径专用 VITE_H5_QRCODE_PATH_ENTERPRISE(与个人开户现有的 VITE_H5_QRCODE_PATH 区分,不复用)。
|
||||
function buildQrCodeUrl(applicationNo) {
|
||||
const base = import.meta.env.VITE_H5_QRCODE_BASE_URL || 'https://example.com'
|
||||
const path = import.meta.env.VITE_H5_QRCODE_PATH_ENTERPRISE || '/mobile/enterprise-open'
|
||||
return `${base}${path}?applicationNo=${applicationNo}`
|
||||
}
|
||||
|
||||
// 弹窗内"邀请扫码办理":与"保存"调用同一个提交接口,成功后不关闭弹窗,切换为二维码展示态
|
||||
async function handleModalInvite() {
|
||||
modalInviting.value = true
|
||||
try {
|
||||
const res = await createEnterpriseOpenApplicationApi(buildSubmitPayload())
|
||||
if (res.data.code === 200) {
|
||||
qrApplicationNo.value = res.data.data.applicationNo
|
||||
qrCodeUrl.value = buildQrCodeUrl(res.data.data.applicationNo)
|
||||
confirmModalStep.value = 'qrcode'
|
||||
}
|
||||
} finally {
|
||||
modalInviting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗内"取消"/关闭图标/邀请扫码办理成功后的"关闭"按钮:统一只关闭弹窗,不发请求、不跳转
|
||||
function handleModalCancel() {
|
||||
confirmModalVisible.value = false
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.push('/wallet/enterprise-open')
|
||||
}
|
||||
|
|
@ -1163,4 +1255,15 @@ function goBack() {
|
|||
height: auto;
|
||||
max-height: none;
|
||||
}
|
||||
.confirm-modal-actions {
|
||||
margin-top: 24px;
|
||||
text-align: right;
|
||||
}
|
||||
.qrcode-panel {
|
||||
text-align: center;
|
||||
}
|
||||
.qrcode-application-no {
|
||||
margin: 12px 0 8px;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in New Issue