SFT/docs/superpowers/plans/2026-07-23-personal-open-co...

24 KiB

个人开户确认二次确认弹窗 + 邀请扫码办理 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.productionpackage.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.jsondependencies 中出现 "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: 提交
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> —— 编辑模式"保存"按钮绑定的函数,行为等价于原 handleSubmitelse 分支。
  • Step 1: 用 Edit 工具删除原 handleSubmit 整个函数(第 440-519 行),替换为下列 4 个函数

原内容(440-519 行,将被完整替换):

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
  }
}

新内容(替换为):

// 提交前的全部前置校验:协议勾选/渠道/客户/证件照片上传状态/身份证信息一致性/表单必填项。
// 供"确认"按钮预检(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 行原内容:

      <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>

改为:

      <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 合并为一次提交)
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 行"确认"按钮绑定

原内容:

          <a-button type="primary" :disabled="!agreementChecked" :loading="submitting" @click="handleSubmit">确认</a-button>

改为:

          <a-button type="primary" :disabled="!agreementChecked" @click="handleConfirmButtonClick">确认</a-button>
  • Step 2: 在 <PersonalCustomerPicker ref="pickerRef" @select="handleSelectCustomer" /> 之前新增确认弹窗

第 178 行原内容:

    <PersonalCustomerPicker ref="pickerRef" @select="handleSelectCustomer" />

改为(新增 a-modal,置于其之前):

    <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 行)插入以下新状态:

原内容:

const submitting = ref(false)
// 仅 create 模式下"提交成功后原地展示结果面板"使用,与 edit/detail 模式读取的 route.query.applicationNo 区分
const submittedApplicationNo = ref('')

改为:

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 函数之后)新增以下函数:

// "确认"按钮点击:校验通过后打开二次确认弹窗,不直接提交
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> 末尾新增弹窗样式

原文件末尾:

.id-card-hint {
  color: #ff4d4f;
  font-size: 12px;
  margin-top: 8px;
}

改为:

.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: 提交
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 引入、buildQrCodeUrlhandleModalInvite)

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): stringasync function handleModalInvite(): Promise<void>(模板中"邀请扫码办理"按钮已在 Task 3 绑定好 @click="handleModalInvite",本 Task 只需把函数定义补上)。

  • Step 1: 在 <script setup> 顶部 import 区新增 qrcode.vue 引入

原第 182-186 行:

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'

改为:

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 函数之前插入:

// 拼接扫码 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.developmentVITE_H5_QRCODE_BASE_URL 留空,应回退到占位域名 https://example.com)。
  6. 点二维码态的"关闭"按钮 —— 弹窗关闭,页面已处于"开户申请已提交"结果面板(与第4步效果一致)。

确认以上 6 点全部符合预期后,按 Ctrl+C 停止 npm run dev 进程。

  • Step 5: 提交
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 沿用同名变量,未出现命名漂移。