docs: 个人开户确认二次确认弹窗实施计划
parent
dfbf16e81c
commit
2ee041c674
|
|
@ -0,0 +1,585 @@
|
|||
# 个人开户确认二次确认弹窗 + 邀请扫码办理 Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 在"新增个人开户申请"页面(`PersonalOpenForm.vue`)的"确认"按钮点击后新增二次确认弹窗,弹窗提供"保存"(等同现有提交逻辑)/"取消"(仅关闭弹窗)/"邀请扫码办理"(提交后在弹窗内展示携带 `applicationNo` 的二维码)三个选项。
|
||||
|
||||
**Architecture:** 不改动接口契约,仍复用现有 `createPersonalOpenDraftApi`(`/api/wallet-management/personal-opening/create-draft`)。把原 `handleSubmit` 拆成 `validateBeforeSubmit`(前置校验)/`buildSubmitPayload`(公共 payload 构造)/`submitCreateDraft`(纯提交,返回 applicationNo)三个可复用函数,新增确认弹窗的两态 UI(`confirm`/`qrcode`)与对应状态、事件处理函数,二维码用新增依赖 `qrcode.vue` 渲染,二维码 URL 域名/路径通过新增环境变量配置。
|
||||
|
||||
**Tech Stack:** Vue 3 `<script setup>` + Ant Design Vue 4 (`a-modal`/`a-space`/`a-button`) + `qrcode.vue`(新增依赖) + Vite 环境变量。
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- 项目为纯 JavaScript(无 TypeScript),不引入任何 `.ts` 文件。
|
||||
- 不写测试用例、不写 CHANGELOG(项目 AGENTS.md 明确要求),因此**本计划中"验证"步骤统一使用 `npm run build` 做语法/引用检查,以及手动通过 `npm run dev` 走一遍页面交互流程**,不是传统单测 TDD 循环——这是对 writing-plans 默认 TDD 步骤模板的必要调整,原因是本项目未配置任何测试框架/脚本。
|
||||
- 日期时间字段格式、trace header 等约定与本次改动无关,不涉及。
|
||||
- 仅修改 `src/views/wallet/personal-open/PersonalOpenForm.vue`、`.env.development`、`.env.production`、`package.json`/`package-lock.json`(新增依赖),不改动其他文件。
|
||||
- `isEdit`(编辑)与 `isDetail`(查看)模式的现有行为必须保持不变,只有 `isCreate`(新增)模式下"确认"按钮的行为发生变化。
|
||||
|
||||
---
|
||||
|
||||
## Task 1: 新增 qrcode.vue 依赖与二维码 H5 地址环境变量
|
||||
|
||||
**Files:**
|
||||
- Modify: `package.json`(新增 dependency,通过 `npm install` 自动写入)
|
||||
- Modify: `package-lock.json`(`npm install` 自动更新)
|
||||
- Modify: `.env.development`
|
||||
- Modify: `.env.production`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: 无
|
||||
- Produces: `qrcode.vue` 包在 `<script setup>` 中可通过 `import QrcodeVue from 'qrcode.vue'` 引入,模板内用 `<qrcode-vue :value="..." :size="200" />` 渲染;`import.meta.env.VITE_H5_QRCODE_BASE_URL` / `import.meta.env.VITE_H5_QRCODE_PATH` 可在任意前端代码中读取。
|
||||
|
||||
- [ ] **Step 1: 安装 qrcode.vue 依赖**
|
||||
|
||||
Run: `npm install qrcode.vue@3.10.0`
|
||||
Expected: 命令成功退出(exit code 0),`package.json` 的 `dependencies` 中出现 `"qrcode.vue": "^3.10.0"`,`package-lock.json` 有对应更新。
|
||||
|
||||
- [ ] **Step 2: 校验依赖已正确写入 package.json**
|
||||
|
||||
Run: `Select-String -Path package.json -Pattern "qrcode.vue"`
|
||||
Expected: 输出包含一行 `"qrcode.vue": "^3.10.0",`(或 npm 实际写入的版本范围)。
|
||||
|
||||
- [ ] **Step 3: 在 `.env.development` 末尾新增两行环境变量**
|
||||
|
||||
现有 `.env.development` 内容:
|
||||
```
|
||||
VITE_API_BASE_URL=http://localhost:8888
|
||||
```
|
||||
|
||||
改为:
|
||||
```
|
||||
VITE_API_BASE_URL=http://localhost:8888
|
||||
VITE_H5_QRCODE_BASE_URL=
|
||||
VITE_H5_QRCODE_PATH=/mobile/personal-open
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 在 `.env.production` 末尾新增两行环境变量**
|
||||
|
||||
现有 `.env.production` 内容:
|
||||
```
|
||||
VITE_API_BASE_URL=
|
||||
```
|
||||
|
||||
改为:
|
||||
```
|
||||
VITE_API_BASE_URL=
|
||||
VITE_H5_QRCODE_BASE_URL=
|
||||
VITE_H5_QRCODE_PATH=/mobile/personal-open
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 提交**
|
||||
|
||||
```bash
|
||||
git add package.json package-lock.json .env.development .env.production
|
||||
git commit -m "chore: 新增qrcode.vue依赖及个人开户扫码H5地址环境变量"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: 拆分 PersonalOpenForm.vue 的提交逻辑为可复用函数
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/views/wallet/personal-open/PersonalOpenForm.vue:440-519`(删除原 `handleSubmit`,替换为下列新函数)
|
||||
- Modify: `src/views/wallet/personal-open/PersonalOpenForm.vue:32-37`(编辑模式"保存"按钮改绑 `handleEditSubmit`)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: 组件内已有的 `form`(reactive)、`channelNo`/`agreementChecked`/`idCardFrontFileNo`/`idCardBackFileNo`/`facePhotoFileNo`/`idCardFrontUploading`/`idCardBackUploading`/`facePhotoUploading`/`identityMismatch`/`formRef`/`submitting`/`submittedApplicationNo`(均为既有 ref/reactive,不改变其定义)、`createPersonalOpenDraftApi`/`updatePersonalOpenApplicationApi`(既有 import)。
|
||||
- Produces: 供 Task 3/Task 4 使用的函数签名:
|
||||
- `async function validateBeforeSubmit(): Promise<boolean>` —— 校验全部通过返回 `true`,否则弹出对应错误提示并返回 `false`。
|
||||
- `function buildSubmitPayload(): { payload: object, fileList: Array<{fileNo: string, fileType: string}> }`
|
||||
- `async function submitCreateDraft(): Promise<string|null>` —— 调用 `create-draft`,成功时把 `res.data.data.applicationNo` 写入 `submittedApplicationNo.value` 并返回该值,失败(`code !== 200`)返回 `null`。
|
||||
- `async function handleEditSubmit(): Promise<void>` —— 编辑模式"保存"按钮绑定的函数,行为等价于原 `handleSubmit` 的 `else` 分支。
|
||||
|
||||
- [ ] **Step 1: 用 Edit 工具删除原 `handleSubmit` 整个函数(第 440-519 行),替换为下列 4 个函数**
|
||||
|
||||
原内容(440-519 行,将被完整替换):
|
||||
```js
|
||||
async function handleSubmit() {
|
||||
if (isCreate.value && !agreementChecked.value) {
|
||||
showErrorModal('请先阅读并勾选同意用户支付服务协议与隐私政策')
|
||||
return
|
||||
}
|
||||
if (!channelNo.value) {
|
||||
showErrorModal('请先选择渠道归属')
|
||||
return
|
||||
}
|
||||
if (!form.customerId) {
|
||||
showErrorModal('请先选择客户')
|
||||
return
|
||||
}
|
||||
if (idCardFrontUploading.value || idCardBackUploading.value || facePhotoUploading.value) {
|
||||
showErrorModal('证件照片正在上传中,请稍候')
|
||||
return
|
||||
}
|
||||
// 编辑模式下若未重新上传,保留 loadDetail 回填的原 fileNo 即可提交,故只在创建时强制校验必须已上传
|
||||
if (isCreate.value && (!idCardFrontFileNo.value || !idCardBackFileNo.value || !facePhotoFileNo.value)) {
|
||||
showErrorModal('请上传身份证人像面、国徽面和正面免冠照')
|
||||
return
|
||||
}
|
||||
if (identityMismatch.value) {
|
||||
showErrorModal('身份证信息与所选客户信息不一致,请核实后重新上传身份证或重新选择客户')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
const fileList = [
|
||||
{ fileNo: idCardFrontFileNo.value, fileType: '01' },
|
||||
{ fileNo: idCardBackFileNo.value, fileType: '02' },
|
||||
{ fileNo: facePhotoFileNo.value, fileType: '04' }
|
||||
]
|
||||
const payload = {
|
||||
accountProperty: '1',
|
||||
channelNo: channelNo.value,
|
||||
customerId: form.customerId,
|
||||
customerName: form.customerName,
|
||||
certificateType: form.certificateType,
|
||||
certificateNumber: form.certificateNumber,
|
||||
certificateEffectiveDate: form.certificateEffectiveDate,
|
||||
certificateExpiryDate: form.certificateExpiryDate,
|
||||
issuingAuthority: form.issuingAuthority,
|
||||
certificateAddress: form.certificateAddress,
|
||||
gender: form.gender,
|
||||
ethnic: form.ethnic,
|
||||
occupation: form.occupationType,
|
||||
jobNote: form.occupationType === 'OTHER' ? form.jobNote : undefined,
|
||||
mobilePhone: form.mobilePhone,
|
||||
bankCardNumber: form.bankCardNumber
|
||||
}
|
||||
if (isCreate.value) {
|
||||
const res = await createPersonalOpenDraftApi({ ...payload, fileList })
|
||||
if (res.data.code === 200) {
|
||||
submittedApplicationNo.value = res.data.data.applicationNo
|
||||
message.success('申请已提交')
|
||||
}
|
||||
} else {
|
||||
// 已知缺口:personal-opening/update 接口尚未提供,前端按 applicationNo 作为定位字段假设实现,
|
||||
// 详见《缺失的后端接口.md》Part 2 新增条目;证件照片参数名与 create-draft 不同,后端要求为
|
||||
// accountOpenApplicationFileBtoList
|
||||
const res = await updatePersonalOpenApplicationApi({
|
||||
applicationNo: route.query.applicationNo,
|
||||
...payload,
|
||||
accountOpenApplicationFileBtoList: fileList
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
message.success('修改成功')
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
新内容(替换为):
|
||||
```js
|
||||
// 提交前的全部前置校验:协议勾选/渠道/客户/证件照片上传状态/身份证信息一致性/表单必填项。
|
||||
// 供"确认"按钮预检(create 模式,通过后弹二次确认弹窗)与编辑模式"保存"按钮共用。
|
||||
async function validateBeforeSubmit() {
|
||||
if (isCreate.value && !agreementChecked.value) {
|
||||
showErrorModal('请先阅读并勾选同意用户支付服务协议与隐私政策')
|
||||
return false
|
||||
}
|
||||
if (!channelNo.value) {
|
||||
showErrorModal('请先选择渠道归属')
|
||||
return false
|
||||
}
|
||||
if (!form.customerId) {
|
||||
showErrorModal('请先选择客户')
|
||||
return false
|
||||
}
|
||||
if (idCardFrontUploading.value || idCardBackUploading.value || facePhotoUploading.value) {
|
||||
showErrorModal('证件照片正在上传中,请稍候')
|
||||
return false
|
||||
}
|
||||
// 编辑模式下若未重新上传,保留 loadDetail 回填的原 fileNo 即可提交,故只在创建时强制校验必须已上传
|
||||
if (isCreate.value && (!idCardFrontFileNo.value || !idCardBackFileNo.value || !facePhotoFileNo.value)) {
|
||||
showErrorModal('请上传身份证人像面、国徽面和正面免冠照')
|
||||
return false
|
||||
}
|
||||
if (identityMismatch.value) {
|
||||
showErrorModal('身份证信息与所选客户信息不一致,请核实后重新上传身份证或重新选择客户')
|
||||
return false
|
||||
}
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 构造提交给 create-draft/update 的公共 payload 与证件文件列表,create/edit 提交内容一致,仅
|
||||
// 文件字段名不同(update 接口要求 accountOpenApplicationFileBtoList,由调用方自行拼接)。
|
||||
function buildSubmitPayload() {
|
||||
const fileList = [
|
||||
{ fileNo: idCardFrontFileNo.value, fileType: '01' },
|
||||
{ fileNo: idCardBackFileNo.value, fileType: '02' },
|
||||
{ fileNo: facePhotoFileNo.value, fileType: '04' }
|
||||
]
|
||||
const payload = {
|
||||
accountProperty: '1',
|
||||
channelNo: channelNo.value,
|
||||
customerId: form.customerId,
|
||||
customerName: form.customerName,
|
||||
certificateType: form.certificateType,
|
||||
certificateNumber: form.certificateNumber,
|
||||
certificateEffectiveDate: form.certificateEffectiveDate,
|
||||
certificateExpiryDate: form.certificateExpiryDate,
|
||||
issuingAuthority: form.issuingAuthority,
|
||||
certificateAddress: form.certificateAddress,
|
||||
gender: form.gender,
|
||||
ethnic: form.ethnic,
|
||||
occupation: form.occupationType,
|
||||
jobNote: form.occupationType === 'OTHER' ? form.jobNote : undefined,
|
||||
mobilePhone: form.mobilePhone,
|
||||
bankCardNumber: form.bankCardNumber
|
||||
}
|
||||
return { payload, fileList }
|
||||
}
|
||||
|
||||
// 提交个人开户草稿(create-draft),成功时记录 applicationNo 并返回,失败返回 null。
|
||||
// 失败时的错误提示由 src/api/request.js 的响应拦截器统一弹出,这里不重复提示。
|
||||
async function submitCreateDraft() {
|
||||
const { payload, fileList } = buildSubmitPayload()
|
||||
const res = await createPersonalOpenDraftApi({ ...payload, fileList })
|
||||
if (res.data.code === 200) {
|
||||
submittedApplicationNo.value = res.data.data.applicationNo
|
||||
return res.data.data.applicationNo
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 编辑模式"保存"按钮:校验通过后调用 update 接口,行为与改造前的 handleSubmit 编辑分支完全一致。
|
||||
async function handleEditSubmit() {
|
||||
const valid = await validateBeforeSubmit()
|
||||
if (!valid) return
|
||||
submitting.value = true
|
||||
try {
|
||||
const { payload, fileList } = buildSubmitPayload()
|
||||
// 已知缺口:personal-opening/update 接口尚未提供,前端按 applicationNo 作为定位字段假设实现,
|
||||
// 详见《缺失的后端接口.md》Part 2 新增条目;证件照片参数名与 create-draft 不同,后端要求为
|
||||
// accountOpenApplicationFileBtoList
|
||||
const res = await updatePersonalOpenApplicationApi({
|
||||
applicationNo: route.query.applicationNo,
|
||||
...payload,
|
||||
accountOpenApplicationFileBtoList: fileList
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
message.success('修改成功')
|
||||
goBack()
|
||||
}
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 更新编辑模式"保存"按钮绑定**
|
||||
|
||||
第 32-37 行原内容:
|
||||
```vue
|
||||
<template v-else-if="isEdit" #extra>
|
||||
<a-space>
|
||||
<a-button type="primary" :loading="submitting" @click="handleSubmit">保存</a-button>
|
||||
<a-button @click="goBack">取消</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
```
|
||||
|
||||
改为:
|
||||
```vue
|
||||
<template v-else-if="isEdit" #extra>
|
||||
<a-space>
|
||||
<a-button type="primary" :loading="submitting" @click="handleEditSubmit">保存</a-button>
|
||||
<a-button @click="goBack">取消</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 运行 build 校验无语法/引用错误**
|
||||
|
||||
Run: `npm run build`
|
||||
Expected: 命令成功退出(exit code 0),输出中不包含 `handleSubmit is not defined` 或其他 Rollup/Vite 报错。(此时"确认"按钮仍绑定旧的 `handleSubmit`,尚未替换,预期此步骤会因为第 28 行仍引用已删除的 `handleSubmit` 而报错——这是预期的失败,用于确认 Task 3 必须紧接着修复该引用)
|
||||
|
||||
- [ ] **Step 4: 提交(允许 build 暂时失败,因为"确认"按钮引用将在 Task 3 修复;若不希望有中间失败态,可将本 Task 与 Task 3 合并为一次提交)**
|
||||
|
||||
```bash
|
||||
git add src/views/wallet/personal-open/PersonalOpenForm.vue
|
||||
git commit -m "refactor: 拆分个人开户提交逻辑为validateBeforeSubmit/buildSubmitPayload/submitCreateDraft"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: 新增二次确认弹窗(确认态:保存/取消/邀请扫码办理)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/views/wallet/personal-open/PersonalOpenForm.vue`(template 新增 `a-modal`、第 28 行按钮改绑、script 新增状态与函数、style 新增样式)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 2 产出的 `validateBeforeSubmit()`、`submitCreateDraft()`。
|
||||
- Produces:
|
||||
- `const confirmModalVisible = ref(false)`
|
||||
- `const confirmModalStep = ref('confirm')`(`'confirm' | 'qrcode'`,Task 4 会用到)
|
||||
- `const modalSaving = ref(false)`
|
||||
- `const modalInviting = ref(false)`(Task 4 会用到)
|
||||
- `async function handleConfirmButtonClick(): Promise<void>`
|
||||
- `async function handleModalSave(): Promise<void>`
|
||||
- `function handleModalCancel(): void`
|
||||
|
||||
- [ ] **Step 1: 修改第 28 行"确认"按钮绑定**
|
||||
|
||||
原内容:
|
||||
```vue
|
||||
<a-button type="primary" :disabled="!agreementChecked" :loading="submitting" @click="handleSubmit">确认</a-button>
|
||||
```
|
||||
|
||||
改为:
|
||||
```vue
|
||||
<a-button type="primary" :disabled="!agreementChecked" @click="handleConfirmButtonClick">确认</a-button>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 在 `<PersonalCustomerPicker ref="pickerRef" @select="handleSelectCustomer" />` 之前新增确认弹窗**
|
||||
|
||||
第 178 行原内容:
|
||||
```vue
|
||||
<PersonalCustomerPicker ref="pickerRef" @select="handleSelectCustomer" />
|
||||
```
|
||||
|
||||
改为(新增 `a-modal`,置于其之前):
|
||||
```vue
|
||||
<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>
|
||||
|
||||
<PersonalCustomerPicker ref="pickerRef" @select="handleSelectCustomer" />
|
||||
```
|
||||
|
||||
(注:`qrcode-vue` 组件、`qrCodeUrl`/`qrApplicationNo`/`handleModalInvite` 在 Task 4 中定义,本 Task 先把模板结构和"确认态"三按钮跑通即可;Task 3 完成时 `confirmModalStep` 永远是 `'confirm'`,不会走到 `v-else` 分支,不影响本 Task 的可测试性。)
|
||||
|
||||
- [ ] **Step 3: 在 script setup 中新增弹窗相关状态与函数**
|
||||
|
||||
在 `const submitting = ref(false)` 与 `const submittedApplicationNo = ref('')` 之间(约第 235-237 行)插入以下新状态:
|
||||
|
||||
原内容:
|
||||
```js
|
||||
const submitting = ref(false)
|
||||
// 仅 create 模式下"提交成功后原地展示结果面板"使用,与 edit/detail 模式读取的 route.query.applicationNo 区分
|
||||
const submittedApplicationNo = ref('')
|
||||
```
|
||||
|
||||
改为:
|
||||
```js
|
||||
const submitting = ref(false)
|
||||
// 仅 create 模式下"提交成功后原地展示结果面板"使用,与 edit/detail 模式读取的 route.query.applicationNo 区分
|
||||
const submittedApplicationNo = ref('')
|
||||
|
||||
// "确认"按钮触发的二次确认弹窗:confirm=文案+三按钮态,qrcode=邀请扫码办理成功后的二维码展示态
|
||||
const confirmModalVisible = ref(false)
|
||||
const confirmModalStep = ref('confirm')
|
||||
const modalSaving = ref(false)
|
||||
const modalInviting = ref(false)
|
||||
const qrCodeUrl = ref('')
|
||||
const qrApplicationNo = ref('')
|
||||
```
|
||||
|
||||
在文件末尾 `function goBack() { ... }` 之前(即原 `handleQueryProgress` 函数之后)新增以下函数:
|
||||
|
||||
```js
|
||||
// "确认"按钮点击:校验通过后打开二次确认弹窗,不直接提交
|
||||
async function handleConfirmButtonClick() {
|
||||
const valid = await validateBeforeSubmit()
|
||||
if (!valid) return
|
||||
confirmModalStep.value = 'confirm'
|
||||
confirmModalVisible.value = true
|
||||
}
|
||||
|
||||
// 弹窗内"保存":与原有提交逻辑一致,成功后关闭弹窗并展示"已提交"结果面板
|
||||
async function handleModalSave() {
|
||||
modalSaving.value = true
|
||||
try {
|
||||
const applicationNo = await submitCreateDraft()
|
||||
if (applicationNo) {
|
||||
message.success('申请已提交')
|
||||
confirmModalVisible.value = false
|
||||
}
|
||||
} finally {
|
||||
modalSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗内"取消"/关闭图标/邀请扫码办理成功后的"关闭"按钮:统一只关闭弹窗,不发请求、不跳转
|
||||
function handleModalCancel() {
|
||||
confirmModalVisible.value = false
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 在 `<style scoped>` 末尾新增弹窗样式**
|
||||
|
||||
原文件末尾:
|
||||
```css
|
||||
.id-card-hint {
|
||||
color: #ff4d4f;
|
||||
font-size: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
```
|
||||
|
||||
改为:
|
||||
```css
|
||||
.id-card-hint {
|
||||
color: #ff4d4f;
|
||||
font-size: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.confirm-modal-actions {
|
||||
margin-top: 24px;
|
||||
text-align: right;
|
||||
}
|
||||
.qrcode-panel {
|
||||
text-align: center;
|
||||
}
|
||||
.qrcode-application-no {
|
||||
margin: 12px 0 8px;
|
||||
color: #333;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 运行 build 校验**
|
||||
|
||||
Run: `npm run build`
|
||||
Expected: 命令成功退出(exit code 0)。若报错提示 `qrCodeUrl`/`qrApplicationNo`/`handleModalInvite`/`qrcode-vue` 未定义,属预期(将在 Task 4 补齐),若报错内容是其他(如 `handleConfirmButtonClick`/`handleModalSave`/`handleModalCancel` 未定义),需回头检查 Step 3 是否插入到了 `<script setup>` 块内正确位置。
|
||||
|
||||
- [ ] **Step 6: 提交**
|
||||
|
||||
```bash
|
||||
git add src/views/wallet/personal-open/PersonalOpenForm.vue
|
||||
git commit -m "feat: 个人开户确认按钮新增二次确认弹窗(保存/取消)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: 邀请扫码办理 —— 生成并展示二维码
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/views/wallet/personal-open/PersonalOpenForm.vue`(新增 `qrcode.vue` 引入、`buildQrCodeUrl`、`handleModalInvite`)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1 的 `qrcode.vue` 依赖与 `VITE_H5_QRCODE_BASE_URL`/`VITE_H5_QRCODE_PATH` 环境变量;Task 3 的 `confirmModalStep`/`modalInviting`/`qrCodeUrl`/`qrApplicationNo`/`submitCreateDraft`。
|
||||
- Produces: `function buildQrCodeUrl(applicationNo: string): string`、`async function handleModalInvite(): Promise<void>`(模板中"邀请扫码办理"按钮已在 Task 3 绑定好 `@click="handleModalInvite"`,本 Task 只需把函数定义补上)。
|
||||
|
||||
- [ ] **Step 1: 在 `<script setup>` 顶部 import 区新增 `qrcode.vue` 引入**
|
||||
|
||||
原第 182-186 行:
|
||||
```js
|
||||
import { ref, reactive, computed, watch, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import ChannelSelect from '@/components/ChannelSelect.vue'
|
||||
```
|
||||
|
||||
改为:
|
||||
```js
|
||||
import { ref, reactive, computed, watch, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
import ChannelSelect from '@/components/ChannelSelect.vue'
|
||||
```
|
||||
|
||||
(`<script setup>` 下,导入的组件变量 `QrcodeVue` 会被编译器自动注册,模板里用 kebab-case `<qrcode-vue>` 即可解析到它,无需额外 `components: {}` 声明。)
|
||||
|
||||
- [ ] **Step 2: 在 `handleConfirmButtonClick` 之前新增 `buildQrCodeUrl` 函数,并补齐 `handleModalInvite`**
|
||||
|
||||
在 Task 3 新增的 `handleModalSave` 函数之后、`handleModalCancel` 函数之前插入:
|
||||
|
||||
```js
|
||||
// 拼接扫码 H5 地址:域名/路径来自环境变量,域名未配置(生产环境尚未确定)时用占位域名兜底,
|
||||
// 保证二维码始终可以生成,不阻断柜员操作;域名确定后只需修改 .env.production,无需改代码。
|
||||
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 || '/mobile/personal-open'
|
||||
return `${base}${path}?applicationNo=${applicationNo}`
|
||||
}
|
||||
|
||||
// 弹窗内"邀请扫码办理":与"保存"调用同一个提交接口,成功后不关闭弹窗,切换为二维码展示态
|
||||
async function handleModalInvite() {
|
||||
modalInviting.value = true
|
||||
try {
|
||||
const applicationNo = await submitCreateDraft()
|
||||
if (applicationNo) {
|
||||
qrApplicationNo.value = applicationNo
|
||||
qrCodeUrl.value = buildQrCodeUrl(applicationNo)
|
||||
confirmModalStep.value = 'qrcode'
|
||||
}
|
||||
} finally {
|
||||
modalInviting.value = false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
完成后,`handleModalSave`/`buildQrCodeUrl`/`handleModalInvite`/`handleModalCancel` 四个函数应按此顺序相邻排列在 `handleConfirmButtonClick` 之后。
|
||||
|
||||
- [ ] **Step 3: 运行 build 校验**
|
||||
|
||||
Run: `npm run build`
|
||||
Expected: 命令成功退出(exit code 0),无未定义变量/组件报错。
|
||||
|
||||
- [ ] **Step 4: 启动开发服务器做端到端人工验证**
|
||||
|
||||
Run: `npm run dev`
|
||||
|
||||
打开浏览器访问个人开户新增页(`/wallet/personal-open` 列表页点"新增" 或直接访问对应路由),依次验证:
|
||||
1. 不勾选协议/不选渠道/不选客户时点"确认" —— 应维持原有报错提示,不弹出二次确认弹窗。
|
||||
2. 补全信息、上传身份证正反面+免冠照并勾选协议后点"确认" —— 应弹出"提示"弹窗,文案"是否确认开户?",三个按钮"保存"/"取消"/"邀请扫码办理"。
|
||||
3. 点"取消" —— 弹窗关闭,表单数据保留,可再次点"确认"重新弹出弹窗。
|
||||
4. 再次点"确认"后点"保存" —— 弹窗关闭,页面切换为"开户申请已提交"结果面板,展示真实申请编号。
|
||||
5. (需要另一条全新数据,重新填一遍表单)点"确认"后点"邀请扫码办理" —— 弹窗不关闭,切换为二维码图片 + "申请编号:xxx" 文字 + "关闭"按钮;二维码图片用手机扫码 App 或任意在线二维码解码工具验证其内容为 `https://example.com/mobile/personal-open?applicationNo=<刚生成的申请编号>`(因 `.env.development` 中 `VITE_H5_QRCODE_BASE_URL` 留空,应回退到占位域名 `https://example.com`)。
|
||||
6. 点二维码态的"关闭"按钮 —— 弹窗关闭,页面已处于"开户申请已提交"结果面板(与第4步效果一致)。
|
||||
|
||||
确认以上 6 点全部符合预期后,按 `Ctrl+C` 停止 `npm run dev` 进程。
|
||||
|
||||
- [ ] **Step 5: 提交**
|
||||
|
||||
```bash
|
||||
git add src/views/wallet/personal-open/PersonalOpenForm.vue
|
||||
git commit -m "feat: 个人开户邀请扫码办理生成并展示二维码"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review Notes
|
||||
|
||||
- **Spec coverage**:设计文档中"触发时机与校验"→Task 2/3;"弹窗结构""三个按钮行为"→Task 3/4;"技术实现"(依赖、环境变量、代码拆分)→Task 1/2;端到端验证覆盖设计文档全部 6 步交互流程→Task 4 Step 4。
|
||||
- **Placeholder scan**:所有代码块均为完整可运行代码,无 TODO/TBD。
|
||||
- **Type/命名一致性**:`validateBeforeSubmit`/`buildSubmitPayload`/`submitCreateDraft` 在 Task 2 定义、Task 3/4 中的调用方与签名一致;`confirmModalStep`/`modalSaving`/`modalInviting`/`qrCodeUrl`/`qrApplicationNo` 在 Task 3 声明,Task 4 沿用同名变量,未出现命名漂移。
|
||||
Loading…
Reference in New Issue