<!-- src/views/wallet/personal-open/PersonalOpenForm.vue -->
<!-- 2.6.2 个人账户开立(2026-07-22 按最新原型截图改造,2026-07-23 补充 create/edit/detail 三态):
     选择个人客户回填基础资料(只读,作为身份核验基准) -> 补充职业/银行卡/手机号 ->
     上传身份证正反面(触发OCR,识别结果与所选客户信息比对,不一致弹窗阻断提交)+正面免冠照
     (调用后端人脸识别接口 face-recognize,与所选客户的身份证号/姓名做匹配,识别通过才拿到可提交
     的文件编号) -> 勾选同意协议 -> 提交草稿 -> 展示申请编号,提供"刷新状态"手动轮询按钮(不做
     自动定时轮询)。
     选择客户改用与"客户管理 > 个人客户管理"页面一致的处理方式:先分页查询 /api/customer-info/personal/list
     (PersonalCustomerPicker.vue),选中后再调用 /api/customer-info/personal/detail 取完整字段回填,
     不使用本模块专属但字段未必对齐的 personal-opening/query-customer 接口。
     按产品要求(2026-07-22)本页面不需要短信验证码环节。
     mode(create/edit/detail)由 route.path 后缀推导,照抄 EnterpriseOpenForm.vue 的写法,避免
     复刻阶段3 PersonalForm/EnterpriseForm 的Bug。edit/detail 模式下的 list/detail/update 接口
     后端尚未提供,前端已按 enterprise-open-application 四件套的假设契约实现,detail/edit 模式
     下无法真正联调验证,详见《缺失的后端接口.md》Part 1 §1.10 与 Part 2 新增条目。
     已知缺口(详见《缺失的后端接口.md》):人脸识别接口 recode/recodeInfo 取值枚举未文档化
     (前端仅按"是否返回 fileNo"判断识别是否通过)、协议正文/URL无真实内容、职业码表为前端占位值、
     编辑/查看态因缺少"按 fileNo 回显图片"的接口,无法重新展示已上传的证件/人脸照片缩略图(与
     企业开户表单 LicenseUpload/IdCardUpload 在编辑态的现状一致,非本次新增的独立缺口)。 -->
<template>
  <div class="personal-open-form-page">
    <a-page-header :title="pageTitle" @back="goBack">
      <template v-if="isCreate && !submittedApplicationNo" #extra>
        <a-space wrap>
          <a-checkbox v-model:checked="agreementChecked">本人已阅读并同意</a-checkbox>
          <a @click="showAgreement('service')">《浙江稠州商业银行用户支付服务协议》</a>
          <a @click="showAgreement('privacy')">《浙江稠州商业银行用户支付服务隐私政策》</a>
          <a-button type="primary" :disabled="!agreementChecked" @click="handleConfirmButtonClick">确认</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>
    </a-page-header>

    <a-card v-if="isDetail && (detailInfo.applicationStatus || detailInfo.failReason)" style="margin-bottom: 16px">
      <a-descriptions :column="2">
        <a-descriptions-item label="申请状态">{{ statusText }}</a-descriptions-item>
        <a-descriptions-item label="失败原因">{{ detailInfo.failReason || '-' }}</a-descriptions-item>
        <a-descriptions-item label="主账户账号">{{ detailInfo.mainAccountNo || '-' }}</a-descriptions-item>
        <a-descriptions-item label="客户编号">{{ detailInfo.customerNo || '-' }}</a-descriptions-item>
      </a-descriptions>
      <a-button style="margin-top: 12px" :loading="polling" @click="handleQueryProgress">刷新状态</a-button>
    </a-card>

    <a-card v-if="!(isCreate && submittedApplicationNo)">
      <div class="section-title">基础信息<span v-if="!isDetail" class="required-mark">*</span></div>
      <a-form
        ref="formRef"
        layout="horizontal"
        :label-col="{ style: { width: '130px' } }"
        :wrapper-col="{ style: { flex: '1 1 auto', maxWidth: '420px' } }"
        :model="form"
        :rules="rules"
        :disabled="isDetail"
        class="open-form"
      >
        <a-form-item label="渠道归属" required>
          <ChannelSelect v-model:model-value="channelNo" placeholder="请选择核心企业" style="width: 300px" />
        </a-form-item>
        <a-form-item label="客户" required>
          <a-space>
            <a-input :value="form.customerName" placeholder="请选择客户" disabled style="width: 200px" />
            <a-button v-if="!isDetail" @click="openPicker">{{ form.customerId ? '重新选择' : '选择' }}</a-button>
          </a-space>
        </a-form-item>
        <a-form-item label="身份证号" required>
          <a-input :value="form.certificateNumber" disabled placeholder="请输入身份证号" />
        </a-form-item>
        <a-form-item label="证件生效日期" required>
          <a-date-picker :value="effectiveDateValue" disabled style="width: 100%" placeholder="请选择日期" />
        </a-form-item>
        <a-form-item label="证件失效日期" required>
          <a-space>
            <a-date-picker :value="expiryDateValue" disabled style="width: 200px" placeholder="请选择日期" />
            <a-checkbox :checked="isLongTerm" disabled>长期</a-checkbox>
          </a-space>
        </a-form-item>
        <a-form-item label="签发机关名称" required>
          <a-input :value="form.issuingAuthority" disabled placeholder="请输入签发机关名称" />
        </a-form-item>
        <a-form-item label="证件地址" required>
          <a-input :value="form.certificateAddress" disabled placeholder="请输入证件地址或常住地址" />
        </a-form-item>
        <a-form-item label="性别">
          <a-radio-group :value="form.gender" disabled>
            <a-radio value="MALE">男性</a-radio>
            <a-radio value="FEMALE">女性</a-radio>
          </a-radio-group>
        </a-form-item>
        <a-form-item label="民族">
          <a-select :value="form.ethnic" disabled :options="ethnicOptions" style="width: 200px" placeholder="-" />
        </a-form-item>
        <a-form-item label="职业" name="occupationType" required>
          <a-select v-model:value="form.occupationType" placeholder="请选择职业" style="width: 300px" allow-clear>
            <a-select-option v-for="opt in OCCUPATION_TYPE_OPTIONS" :key="opt.value" :value="opt.value">
              {{ opt.label }}
            </a-select-option>
          </a-select>
        </a-form-item>
        <a-form-item v-if="form.occupationType === 'OTHER'" label="职业备注" name="jobNote" required>
          <a-input v-model:value="form.jobNote" placeholder="职业为“其他”时请填写备注" style="width: 300px" />
        </a-form-item>

        <a-form-item label="证件照片" :required="isCreate">
          <a-space size="large" align="start">
            <div>
              <div class="upload-label">身份证人像面</div>
              <IdCardUpload
                side="front"
                :channel-no="channelNo"
                :readonly="isDetail"
                v-model:file-no="idCardFrontFileNo"
                v-model:uploading="idCardFrontUploading"
                @ocr-result="handleFrontOcrResult"
                @ocr-status="(s) => (frontOcrStatus = s)"
              />
            </div>
            <div>
              <div class="upload-label">身份证国徽面</div>
              <IdCardUpload
                side="back"
                :channel-no="channelNo"
                :readonly="isDetail"
                v-model:file-no="idCardBackFileNo"
                v-model:uploading="idCardBackUploading"
                @ocr-result="handleBackOcrResult"
                @ocr-status="(s) => (backOcrStatus = s)"
              />
            </div>
            <div>
              <div class="upload-label">正面免冠照</div>
              <FacePhotoUpload
                :key="form.customerId"
                :channel-no="channelNo"
                :id-no="form.certificateNumber"
                :name="form.customerName"
                :readonly="isDetail"
                v-model:file-no="facePhotoFileNo"
                v-model:uploading="facePhotoUploading"
              />
            </div>
          </a-space>
          <div v-if="!isDetail" class="id-card-hint">*请确保身份证信息在有效期范围内,若证件信息过期 需要去线下银行网点更新</div>
          <div v-if="identityMismatch" class="id-card-hint">
            身份证信息与所选客户信息不一致,请核实后重新上传身份证或重新选择客户,当前无法提交
          </div>
        </a-form-item>

        <a-form-item label="银行卡号" name="bankCardNumber" required>
          <a-input v-model:value="form.bankCardNumber" placeholder="请输入银行卡号" style="width: 300px" />
        </a-form-item>
        <a-form-item label="手机号" name="mobilePhone" required>
          <a-input v-model:value="form.mobilePhone" placeholder="请输入手机号" style="width: 300px" />
        </a-form-item>
      </a-form>
    </a-card>

    <a-card v-else>
      <a-result status="success" title="开户申请已提交" :sub-title="`申请编号:${submittedApplicationNo}`">
        <template #extra>
          <a-space direction="vertical" style="width: 100%">
            <div v-if="progress.applicationStatus">当前状态:{{ progressStatusText }}</div>
            <div v-if="progress.failReason" style="color: #ff4d4f">失败原因:{{ progress.failReason }}</div>
            <a-space>
              <a-button :loading="polling" @click="handleQueryProgress">刷新状态</a-button>
              <a-button @click="goBack">返回列表</a-button>
            </a-space>
          </a-space>
        </template>
      </a-result>
    </a-card>

    <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" />
  </div>
</template>

<script setup>
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'
import IdCardUpload from '@/components/IdCardUpload.vue'
import FacePhotoUpload from '@/components/FacePhotoUpload.vue'
import PersonalCustomerPicker from '@/components/PersonalCustomerPicker.vue'
import {
  createPersonalOpenDraftApi,
  queryPersonalOpenProgressApi,
  fetchPersonalOpenDetailApi,
  updatePersonalOpenApplicationApi
} from '@/api/wallet'
import { fetchPersonalCustomerDetailApi } from '@/api/customer'
import { phoneValidatorRule } from '@/utils/validators'
import { showErrorModal } from '@/utils/errorModal'
import { mapOcrGender } from '@/utils/ocrMapping'
import { OCCUPATION_TYPE_OPTIONS, CERTIFICATE_LONG_TERM_DATE } from '@/constants/customerEnums'

const route = useRoute()
const router = useRouter()
const formRef = ref(null)
const pickerRef = ref(null)
const channelNo = ref(undefined)
const agreementChecked = ref(false)

const mode = computed(() => {
  if (route.path.endsWith('/edit')) return 'edit'
  if (route.path.endsWith('/detail')) return 'detail'
  return 'create'
})
const isCreate = computed(() => mode.value === 'create')
const isEdit = computed(() => mode.value === 'edit')
const isDetail = computed(() => mode.value === 'detail')
const pageTitle = computed(
  () => ({ create: '新增个人开户申请', edit: '编辑个人开户申请', detail: '查看个人开户申请' }[mode.value])
)

const idCardFrontFileNo = ref('')
const idCardBackFileNo = ref('')
const facePhotoFileNo = ref('')
const idCardFrontUploading = ref(false)
const idCardBackUploading = ref(false)
const facePhotoUploading = ref(false)
const frontOcrStatus = ref('PENDING')
const backOcrStatus = ref('PENDING')
// 身份证正/反面 OCR 原始结果,选择客户后作为比对基准发生变化时需要重新核验,故保留原始数据而非只保留结论
const frontOcrData = ref(null)
const backOcrData = ref(null)
const identityMismatch = ref(false)

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('')
const polling = ref(false)
const progress = reactive({ applicationStatus: '', failReason: '' })
const detailInfo = reactive({ applicationStatus: '', failReason: '', mainAccountNo: '', customerNo: '' })

const form = reactive({
  customerId: undefined,
  customerName: '',
  certificateType: 'ID_CARD',
  certificateNumber: '',
  certificateEffectiveDate: '',
  certificateExpiryDate: '',
  issuingAuthority: '',
  certificateAddress: '',
  gender: undefined,
  ethnic: '',
  occupationType: undefined,
  jobNote: '',
  mobilePhone: '',
  bankCardNumber: ''
})

const effectiveDateValue = computed(() => (form.certificateEffectiveDate ? dayjs(form.certificateEffectiveDate) : undefined))
const isLongTerm = computed(
  () => !!form.certificateExpiryDate && String(form.certificateExpiryDate).startsWith(CERTIFICATE_LONG_TERM_DATE.slice(0, 10))
)
const expiryDateValue = computed(() =>
  !isLongTerm.value && form.certificateExpiryDate ? dayjs(form.certificateExpiryDate) : undefined
)
// 民族字段选择客户后只读展示,不提供可选枚举(避免拟定不准确的56民族码表),仅用当前值构造唯一选项以正常显示
const ethnicOptions = computed(() => (form.ethnic ? [{ value: form.ethnic, label: form.ethnic }] : []))

const rules = {
  occupationType: [{ required: true, message: '请选择职业' }],
  jobNote: [
    {
      validator: (_rule, value) => {
        if (form.occupationType === 'OTHER' && !value) {
          return Promise.reject(new Error('职业为“其他”时请填写职业备注'))
        }
        return Promise.resolve()
      }
    }
  ],
  bankCardNumber: [{ required: true, message: '请输入银行卡号' }],
  mobilePhone: [phoneValidatorRule()]
}

const progressStatusMap = {
  DRAFT: '待确认',
  PENDING_ACCOUNT_OPEN: '待调用开户接口',
  CONFIRMED: '已确认',
  OPENED: '已开立',
  APPLY_FAILED: '申请失败'
}
const progressStatusText = computed(() => progressStatusMap[progress.applicationStatus] || progress.applicationStatus)
const statusText = computed(() => progressStatusMap[detailInfo.applicationStatus] || detailInfo.applicationStatus)

// 顶部协议链接:项目暂无真实协议正文/URL,以弹窗占位说明代替,详见《缺失的后端接口.md》
const AGREEMENT_CONTENT = {
  service: {
    title: '浙江稠州商业银行用户支付服务协议',
    content: '协议正文内容待产品/法务提供,当前为占位说明,不代表最终协议条款。'
  },
  privacy: {
    title: '浙江稠州商业银行用户支付服务隐私政策',
    content: '隐私政策正文内容待产品/法务提供,当前为占位说明,不代表最终政策条款。'
  }
}
function showAgreement(type) {
  const info = AGREEMENT_CONTENT[type]
  Modal.info({ title: info.title, content: info.content, okText: '我知道了' })
}

function openPicker() {
  if (!channelNo.value) {
    showErrorModal('请先选择渠道归属')
    return
  }
  pickerRef.value.show()
}

// 比对身份证 OCR 结果与所选客户档案信息(身份证号/姓名/性别/民族),不一致则弹窗报错并阻断提交
function runIdentityComparison() {
  if (!form.customerId) return
  const mismatches = []
  const front = frontOcrData.value
  if (front) {
    if (front.idcard && form.certificateNumber && front.idcard !== form.certificateNumber) mismatches.push('身份证号')
    if (front.name && form.customerName && front.name !== form.customerName) mismatches.push('姓名')
    const ocrGender = mapOcrGender(front.sex)
    if (ocrGender && form.gender && ocrGender !== form.gender) mismatches.push('性别')
    if (front.nation && form.ethnic && front.nation !== form.ethnic) mismatches.push('民族')
  }
  if (mismatches.length > 0) {
    identityMismatch.value = true
    showErrorModal(
      `身份证识别信息与所选客户档案不一致:${mismatches.join('、')},请核实后重新上传身份证或重新选择客户`,
      '身份信息核验失败'
    )
  } else {
    identityMismatch.value = false
  }
}

function handleFrontOcrResult(data) {
  frontOcrData.value = data
  runIdentityComparison()
}
function handleBackOcrResult(data) {
  backOcrData.value = data
}

watch(idCardFrontFileNo, (val) => {
  if (!val) {
    frontOcrData.value = null
    runIdentityComparison()
  }
})
watch(idCardBackFileNo, (val) => {
  if (!val) backOcrData.value = null
})

async function handleSelectCustomer(record) {
  // 与"客户管理 > 个人客户管理"页面一致的处理方式:列表接口字段有限,选中后需再查一次详情接口
  // 取得性别/民族/证件生效失效日期/签发机关/证件地址/职业等完整字段
  const res = await fetchPersonalCustomerDetailApi({ id: record.id })
  if (res.data.code !== 200) {
    showErrorModal(res.data.message || '获取客户详情失败')
    return
  }
  const detail = res.data.data
  form.customerId = detail.id
  form.customerName = detail.customerName || ''
  form.certificateType = detail.certificateType || 'ID_CARD'
  form.certificateNumber = detail.certificateNumber
  form.certificateEffectiveDate = detail.certificateEffectiveDate
  form.certificateExpiryDate = detail.certificateExpiryDate
  form.issuingAuthority = detail.issuingAuthority
  form.certificateAddress = detail.certificateAddress
  form.gender = detail.gender
  form.ethnic = detail.ethnicity
  form.mobilePhone = detail.mobilePhone || form.mobilePhone
  // occupation 目前是后端自由文本字段,仅当命中前端职业码表占位选项时才预选,否则留空由人工选择
  // (真实标准职业码表待后端/产品确认,详见《缺失的后端接口.md》)
  form.occupationType = OCCUPATION_TYPE_OPTIONS.some((opt) => opt.value === detail.occupation)
    ? detail.occupation
    : undefined
  form.jobNote = ''
  // 选择/重新选择客户后,身份核验基准变化,需要用已上传身份证的 OCR 结果重新核验
  runIdentityComparison()
  // 人脸识别结果(fileNo)是针对"上一次选中的客户"的 idNo/name 匹配出来的,重新选择客户后不再有效,
  // 需要清空并要求重新采集/上传(FacePhotoUpload 上绑定了 :key="form.customerId" 会同步重置组件内部预览态)
  facePhotoFileNo.value = ''
}

// fileType 编码:01身份证正面/02身份证反面/04正面免冠照,与 create-draft 提交时保持一致
function applyFileList(fileList) {
  const files = fileList || []
  idCardFrontFileNo.value = files.find((f) => f.fileType === '01')?.fileNo || ''
  idCardBackFileNo.value = files.find((f) => f.fileType === '02')?.fileNo || ''
  facePhotoFileNo.value = files.find((f) => f.fileType === '04')?.fileNo || ''
}

// edit/detail 模式加载申请单详情:personal-opening/detail 接口后端尚未提供,前端按
// create-draft 请求体反推的假设契约实现,详见《缺失的后端接口.md》Part 2 新增条目
async function loadDetail() {
  const res = await fetchPersonalOpenDetailApi({ applicationNo: route.query.applicationNo })
  if (res.data.code !== 200) {
    showErrorModal(res.data.message || '获取申请详情失败')
    return
  }
  const data = res.data.data || {}
  channelNo.value = data.channelNo
  Object.assign(form, {
    customerId: data.customerId,
    customerName: data.customerName,
    certificateType: data.certificateType || 'ID_CARD',
    certificateNumber: data.certificateNumber,
    certificateEffectiveDate: data.certificateEffectiveDate,
    certificateExpiryDate: data.certificateExpiryDate,
    issuingAuthority: data.issuingAuthority,
    certificateAddress: data.certificateAddress,
    gender: data.gender,
    ethnic: data.ethnic,
    occupationType: data.occupation,
    jobNote: data.jobNote || '',
    mobilePhone: data.mobilePhone,
    bankCardNumber: data.bankCardNumber
  })
  applyFileList(data.fileList)
  detailInfo.applicationStatus = data.applicationStatus
  detailInfo.failReason = data.failReason
  detailInfo.mainAccountNo = data.mainAccountNo
  detailInfo.customerNo = data.customerNo
}

onMounted(() => {
  if (!isCreate.value && route.query.applicationNo) {
    loadDetail()
  }
})

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

async function handleQueryProgress() {
  polling.value = true
  try {
    const res = await queryPersonalOpenProgressApi({ applicationNo: submittedApplicationNo.value || route.query.applicationNo })
    if (res.data.code === 200) {
      if (isDetail.value) {
        Object.assign(detailInfo, {
          applicationStatus: res.data.data.applicationStatus,
          failReason: res.data.data.failReason,
          mainAccountNo: res.data.data.mainAccountNo,
          customerNo: res.data.data.customerNo
        })
      } else {
        Object.assign(progress, res.data.data)
      }
    }
  } finally {
    polling.value = false
  }
}

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

// 拼接扫码 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
  }
}

// 弹窗内"取消"/关闭图标/邀请扫码办理成功后的"关闭"按钮:统一只关闭弹窗,不发请求、不跳转
function handleModalCancel() {
  confirmModalVisible.value = false
}

function goBack() {
  router.push('/wallet/personal-open')
}
</script>

<style scoped>
.section-title {
  font-size: 15px;
  font-weight: 600;
  margin-bottom: 16px;
}
.required-mark {
  color: #ff4d4f;
  margin-left: 2px;
}
.open-form {
  max-width: 680px;
}
.upload-label {
  margin-bottom: 8px;
  font-size: 12px;
  color: #666;
}
.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;
}
</style>
