# 个人开户移动端H5确认页 设计文档 ## 背景 `docs/superpowers/specs/2026-07-23-personal-open-confirm-modal-design.md` 已实现管理后台侧"个人开户"新增表单的二次确认弹窗 + "邀请扫码办理",柜员点击后生成二维码,二维码内容为: ``` ${VITE_H5_QRCODE_BASE_URL}${VITE_H5_QRCODE_PATH}?applicationNo=${applicationNo} ``` 当前 `VITE_H5_QRCODE_PATH=/mobile/personal-open`。该设计文档明确将"扫码后跳转的移动端 H5 页面"排除在范围外。后端已提供对应的 `personal-opening/mobile/draft-detail`、`personal-opening/mobile/submit` 接口(见《缺失的后端接口.md》第19条),但前端从未实现这个 H5 页面。本次设计即补齐该页面。 ## 范围 - 新建一个**完全独立的子工程** `./fryl_h5`(独立 `package.json`/构建产物/部署单元,不与管理后台共用依赖或构建配置)。 - 仅实现"个人开户"扫码核实场景一个页面流程,不涉及企业开户 confirm、贷款申请确认扫码等其他移动端确认场景。 - 不涉及管理后台侧代码改动(二维码生成逻辑已完成,本次不改)。 ## 技术方案 - **技术栈**:Vue3 + Vite + Vant(移动端 UI 组件库),纯 JavaScript(与主项目一致,不引入 TypeScript)。 - **工程定位**:面向终端客户、无需登录鉴权的匿名 H5 页面(不同于管理后台的员工登录体系),因此不引入 Pinia/auth store/refresh-token 逻辑。 - **路由**:`vue-router`,单一业务路由 `/mobile/personal-open`(与二维码 `VITE_H5_QRCODE_PATH` 保持一致),通过 query 参数 `applicationNo` 定位申请单。页面内部用组件本地状态(`currentStep` ref,值为 `INFO`/`FACE`/`SMS`)维护向导步骤,不为每个步骤单独设路由(避免用户直接分享/刷新中间步骤 URL 导致状态错乱)。 - **BASE_URL**:不提供页面内可配置入口(用户明确决定,区别于主项目登录页齿轮图标切换的默认惯例),仅通过 `.env.development`/`.env.production` 的 `VITE_API_BASE_URL` 区分环境。 ## 页面流程 **2026-08-05 更新**:原"步骤1 信息核对(含协议展示)"拆分为两个独立步骤,协议勾选不再耦合在 信息核对页面里,改为单独的"相关协议"步骤,且该步骤调用真实后端接口获取协议模板原文(不再是 占位文案)。整体变为 **5 步向导**: ``` 进入页面 → 调用 draft-detail 加载草稿 ├─ 失败/无效 applicationNo → 展示错误提示页(终态,无法继续) └─ 成功 → 步骤1 信息核对 → 步骤2 相关协议 → 步骤3 人脸识别 → 步骤4 手机验证+提交 → 步骤5 结果页(终态) ``` **2026-08-05 二次更新**:按产品要求,客户在步骤2"相关协议"点击"下一步"后**跳过步骤3人脸识别, 直接进入步骤4手机验证+提交**: ``` 进入页面 → 调用 draft-detail 加载草稿 ├─ 失败/无效 applicationNo → 展示错误提示页(终态,无法继续) └─ 成功 → 步骤1 信息核对 → 步骤2 相关协议 → 步骤4 手机验证+提交 → 步骤5 结果页(终态) (步骤3 人脸识别:当前从向导流程跳过,代码未删除,见下方"步骤3"章节说明) ``` `PersonalOpenMobile.vue` 里 `AgreementStep` 的 `@next` 直接指向 `currentStep = 'SMS'`, `SmsVerifyStep` 的 `@prev` 直接指向 `currentStep = 'AGREEMENT'`;`FaceCompareStep.vue` 组件、 `faceRecognizeApi` 接口封装、`facePhoto`/`faceVerified` 状态均**未删除**,只是不再被引用/声明, 如需恢复该环节,把上述两个事件改回指向 `'FACE'` 并重新引入组件即可(`PersonalOpenMobile.vue` 里保留了具体恢复步骤的代码注释)。 步骤1/2/4之间支持"上一步"返回(步骤3当前不在流程内,故不涉及);步骤5(结果页)为终态, 不支持返回上一步。页面整体视觉仍为 "每步骤单独全屏展示,点击下一步整页切换"(`van-nav-bar` + 组件内 `currentStep` 切换),**不是** 产品截图里"顶部业务确认banner + 左侧1-5编号竖向步骤条、未激活步骤仅显示标题"的单页累积式布局 ——本次改动经与产品确认,只对齐内容/交互逻辑,不做整体视觉布局改版。 ### 步骤0:加载态 / 错误态 - 页面挂载时读取 query 中的 `applicationNo`,为空则直接展示"链接无效"错误态(不发请求)。 - 调用 `mobile/draft-detail` 接口: - `code === 200`:进入步骤1。 - `code !== 200`(如申请单不存在/已过期/已提交过):展示错误提示页,用 `message` 原文展示,提供"关闭页面"提示文案(无可操作按钮,H5 场景下没有其它页面可跳转)。 ### 步骤1:信息核对(**2026-08-05 更新**:不再包含协议展示/勾选,协议已独立为步骤2) 展示字段(与管理后台"个人开户申请详情"现有展示字段保持一致,全部只读): | 字段 | 来源 | | --- | --- | | 客户姓名 | customerName | | 证件类型 | certificateType | | 证件号码 | certificateNumber | | 证件有效期起 | certificateEffectiveDate | | 证件失效日期 | certificateExpiryDate | | 签发机关 | issuingAuthority | | 证件地址 | certificateAddress | | 性别 | gender | | 民族 | ethnic | | 职业 | occupation(+ `occupationNote`,当 occupation 为"其他"时展示备注) | | 银行卡号 | bankCardNumber | | 手机号 | mobilePhoneMask(**2026-07-31 更新**:后端直接提供掩码字段,前端不再自行计算,见接口契约章节) | 不展示证件照片本身(客户本人无需再核对自己的证件照片图像,且原始图片以文件形式存放不适合在此场景重复暴露)、不展示 `applicationStatus`/`failReason`/`mainAccountNo`/`customerNo`(这些是提交后的结果字段,核对阶段不应出现)。 点击"确认无误,下一步"进入步骤2(**2026-08-05 更新**:本步骤不再包含协议勾选/占位文案, 按钮不再有任何禁用条件,点击直接进入步骤2)。 ### 步骤2:相关协议(**2026-08-05 新增,独立步骤,接入真实协议模板接口**) - 展示2份协议的"我已阅读并同意"复选框:《浙江稠州商业银行用户支付服务协议》 (`PAY_SERVICE_AGREEMENT`)、《浙江稠州商业银行用户支付服务隐私政策》(`PAY_SERVICE_PRIVACY`)。 取值来源于新增的 `BankAgreementFileTypeEnum`(该枚举共4项,另外2项 `CREDIT_REPORT_AUTH`个人信用信息报送查询使用授权书/`THIRD_PARTY_DATA_AUTH`第三方数据信息 查询和使用授权书**不在本页面展示范围内**——与产品确认,个人开户场景只需前2项)。 - 点击协议标题链接时调用 `GET /api/customer-info/bank-agreement/base64?fileType=xxx` 拉取协议模板文件(base64 编码的 PDF),**不能直接拼成 `data:application/pdf;base64,...` 赋给 iframe 的 `src`**——真实后端联调时实测 Safari/WebKit 会拦截 iframe 对 `data:` URI 的 导航(表现为接口正常返回 `fileContent`,但预览区域空白,Chrome/安卓不受影响),必须把 base64 转成 `Blob` 再用 `URL.createObjectURL()` 生成 `blob:` URL 赋给 iframe(**2026-08-05 二次更新**,见 `AgreementStep.vue` 的 `base64ToBlobUrl` 函数),组件卸载时统一 `revokeObjectURL` 释放内存。用 `van-popup` 内嵌该 iframe 展示原文;同一份协议重复点击时 使用本地缓存(缓存的是 blob: URL),不重复请求/解码。 - 两份协议均勾选后"下一步"按钮才可点击,未勾选禁用;底部提供"上一步"文字链接返回步骤1 (协议勾选状态本次不做跨步骤保留,退回再进入需重新勾选,与产品截图行为一致——截图未展示 该步骤支持"上一步",此处出于与其它步骤一致的返回体验保留了该入口,纯前端交互决策, 不影响提交参数)。 - 点击"下一步"进入步骤3(人脸对比,**2026-08-05 二次更新:当前从流程中跳过,实际直接进入 步骤4**,见下方说明)。 - **2026-08-06 更新**:两份协议都勾选后,自动在《用户支付服务协议》PDF末页填入客户姓名/证件 号码/当前日期并调用通用文件上传接口拿到 `fileNo`,"下一步"在文件生成并上传成功前保持禁用; 该 `fileNo` 会随最终提交一起传给后端(`mobile/submit` 新增 `payAgreementFileNo` 字段)。 详细方案(PDF坐标测量方法、中文字体渲染的两个已知 bug 及规避方式、mock-server 配套改造)见 独立设计文档 `docs/superpowers/specs/2026-08-06-agreement-pdf-fill-and-upload-design.md`, 接口契约缺口见《缺失的后端接口.md》Part 19。 ### 步骤3:人脸对比(**2026-08-05 二次更新:按产品要求,当前从向导流程中跳过,代码保留未删除**) > `AgreementStep`(步骤2)点击"下一步"后直接进入步骤4(手机验证),不再经过本步骤; > `FaceCompareStep.vue`、`faceRecognizeApi`、下方描述的识别逻辑均**未删除**,只是暂未被 > `PersonalOpenMobile.vue` 引用/挂载。以下为该步骤原有设计描述,供后续恢复时参考: - 提供"拍照"入口(调用 Vant 的图片选择/拍照能力,`accept="image/*" capture="camera"`),拍照或选择后 立即调用后端真实提供的人脸识别接口 `POST /api/customer-info/personal/face-recognize`(与主项目 管理后台 `src/components/FacePhotoUpload.vue` 调用同一接口),详见 `docs/superpowers/specs/2026-07-31-personal-open-mobile-h5-face-recognize-and-field-sync-design.md`。 - 识别通过(响应 `data.fileNo` 非空)才能点击"下一步";识别未通过则清空已选照片,强制重新拍摄。 - 识别中整页遮罩禁止操作,识别结果不会透传给 `mobile/submit`(该接口描述"影像资料由后端从本地 查询"),本步骤仅作为"识别通过才能继续"的前置校验关卡。 - 支持"上一步"返回步骤2(相关协议);从步骤4(手机验证)退回本步骤时,若照片与识别通过状态仍保留, 不重新触发识别。 ### 步骤4:手机验证 + 提交 - 展示掩码手机号(直接使用 `mobile/draft-detail` 返回的 `mobilePhoneMask`,不再前端自行计算,见2026-07-31更新)。 - "获取验证码"按钮:点击调用**通用发码接口 `send-verify-code/general`**(见下),成功后按钮进入 60 秒倒计时禁用态,`message.success('验证码已发送')`。**2026-08-06 更新**:请求体 `channelNo` 取自 `draft-detail` 返回的 `MobileDraftDetailVo.channelNo`(不再固定传空字符串)、 `cardName` 取自 `customerName`(个人账户卡户名与客户姓名一致,`MobileDraftDetailVo` 无独立的 "银行卡户名"字段)、`tradeNo` 固定 `"221506"`、`tradeType` 固定 `"0"`(此前两者取值写反且 `tradeType` 超长,导致真实后端报错`短信类型字段长度超出`,先临时改成 `"1"`,后端确认正确 取值应为 `"0"`,已二次修正)。真实后端联调还发现该短信类型要求 `cardNo`/`cardName`/`idNo` 三者必须同时非空(报错`该短信类型银行卡号、户名、证件号码或者电子账户、金额必须填写`), 故点击"获取验证码"前新增前置校验:`mobile`/`channelNo`/`cardNo`/`cardName`/`idNo` 任一为空 都直接弹窗提示"请联系柜员核实"并**不发起请求**,避免带着必然失败的参数反复重试(见 `SmsVerifyStep.vue` 的 `handleSendCode` 函数)。 - 验证码输入框(6位数字)。 - "确认提交"按钮:仅当验证码已输入时可点击,点击后: - 按钮 `loading` 态防重复点击,期间整页 `van-overlay` 遮罩禁止操作(耗时操作统一等待态,与主项目 `busy`/`a-spin` 规范同一诉求,Vant 场景下用 `van-overlay` + `van-loading` 实现)。 - 调用 `mobile/submit`(`{applicationNo, verifyCode}`)。**2026-08-06 更新**:新增 `payAgreementFileNo` 字段,取自步骤2生成并上传的《用户支付服务协议》PDF文件编号(通过 `PersonalOpenMobile.vue` 提升到父组件状态后作为 prop 传入本组件),详见 `docs/superpowers/specs/2026-08-06-agreement-pdf-fill-and-upload-design.md`。 - `code === 200`:跳转步骤5,展示 `data.success` 对应的成功/失败内容。 - `code !== 200`(验证码错误等业务异常):**停留在本页**,用 `message` 原文以 Vant `showFailToast`/`showNotify` 提示错误,允许用户重新输入验证码或重新获取验证码后重试;不跳转步骤5。 - 支持"上一步"返回步骤2(相关协议,**2026-08-05 二次更新**:步骤3人脸对比已跳过,"上一步" 不再经过它)。 ### 步骤5:结果页(终态) - 成功(`data.success === true`):展示成功图标 + "开户成功" + 主账户号(`mainAccountNo`)+ 客户号(`customerNo`)。 - 失败(`data.success === false`):展示失败图标 + "开户失败" + 失败原因(`failReason`)。 - 无任何可点击的跳转/重试按钮(H5 场景下没有其它页面可去,且提交已产生后端最终结果,不支持重复提交)。 ## 接口契约 ### 已有明确契约(来自 swagger,无需假设,**2026-07-31 更新为完整真实字段**) **`POST /api/wallet-management/personal-opening/mobile/draft-detail`** 请求:`{ applicationNo }` 响应 `data`(`MobileDraftDetailVo`):`customerName`/`channelNo`(**2026-08-06 新增字段**, send-verify-code/general 依赖它)/`certificateType`/`certificateNumber`/ `certificateEffectiveDate`/`certificateExpiryDate`/`issuingAuthority`/`gender`/`certificateAddress`/ `occupation`/`occupationNote`/`mobilePhone`(明文)/`mobilePhoneMask`(后端直接提供的掩码值)/ `bankCardNumber`/`ethnic`。 **`POST /api/wallet-management/personal-opening/mobile/send-verify-code/general`** (**2026-08-06 补全契约**,`SendVerifyCodeGeneralParamVo`) 请求:`{ channelNo, tradeNo(固定"221506"), tradeType(固定"0"), mobile, cardNo, cardName, idNo }` (`cardNo`/`cardName`/`idNo` 三者必须同时非空,否则后端报错`该短信类型银行卡号、户名、证件 号码或者电子账户、金额必须填写`) 响应:`{ code, message, data: null }` **`POST /api/customer-info/personal/face-recognize`**(2026-07-31 新增,真实接口) 请求:`{ fileData(人脸照片base64), idNo, name, channelNo }` 响应 `data`(`FaceRecognizeResultVo`):`fileNo`/`recode`/`recodeInfo`/`sysSerialNo`/`sysDate` **`POST /api/wallet-management/personal-opening/mobile/submit`** 请求:`{ applicationNo, verifyCode }` 响应 `data`(`SubmitApplicationResultVo`):`success`/`mainAccountNo`/`customerNo`/`failReason` **`GET /api/customer-info/bank-agreement/base64`**(2026-08-05 新增,真实接口) 请求:query 参数 `fileType`(枚举 `BankAgreementFileTypeEnum`,本页面只用到 `PAY_SERVICE_AGREEMENT`/`PAY_SERVICE_PRIVACY` 两项,见步骤2说明) 响应 `data`(`BankAgreementFileVo`):`fileType`/`fileName`/`fileContent`(base64 编码的 PDF) ### 仍未解决的契约缺口(详见《缺失的后端接口.md》Part 12 第97条、Part 15 第97/99条) 1. ~~**`channelNo`/`bankCardName` 两个字段仍不存在**~~——**2026-08-06 更新**: `MobileDraftDetailVo` 已补充 `channelNo` 字段,`send-verify-code/general` 的 `channelNo` 已从 draft-detail 响应取值,不再是契约缺口;`cardName`(银行卡户名)仍无独立字段,用 `customerName` 代替(个人账户场景卡户名与客户姓名一致)。`face-recognize` 的 `channelNo` 因该步骤当前已从流程跳过(见步骤3说明),暂不受影响,固定传空字符串的写法保留在 `faceRecognizeApi` 里未改动。 2. **匿名访问 `face-recognize` 的可行性未经真实后端环境验证**:该接口此前只被已登录的管理后台 调用,H5 侧本次调用不带 `Authorization`,本地无法验证真实后端是否已将其纳入匿名白名单。 详见 `docs/superpowers/specs/2026-07-31-personal-open-mobile-h5-face-recognize-and-field-sync-design.md` 的"已知风险/遗留缺口"章节。 ### 请求头规则 - **不携带 `Authorization`**:这些接口面向未登录的终端客户,H5 页面无法获得管理后台的登录态 token,属于匿名接口(`face-recognize` 是否真的允许匿名访问尚待后端确认,见上方契约缺口第2点)。 - **仍携带通用 trace header**(`Appno`/`Serialno`/`Transdate`/`Transtradetime`):沿用主项目 `src/utils/traceHeaders.js` 的实现逻辑(原样迁移一份到 `fryl_h5`,无需改动)。 - **不携带 `Channelno` 请求头**:二维码 URL 中不包含渠道信息(`?applicationNo=xxx`),页面无渠道上下文,按现有规则("channelNo 如果页面没有渠道信息,则不需要传值")不传该 header。**注意**:这里指的是 trace header 里的 `Channelno`,与请求体中的 `channelNo` 参数是两个独立概念,互不影响。 - 响应体统一 `{code, message, data}` 判定成功用 `code === 200`,与主项目一致。 ## 工程结构(`fryl_h5`) ``` fryl_h5/ ├── index.html ├── package.json # 独立依赖:vue、vue-router、vant、axios、dayjs、vite ├── vite.config.js # 参照主项目结构,@ 指向 ./src,server.port 另设(如 5174,避免与主项目 5173 冲突) ├── .env.development # VITE_API_BASE_URL=http://localhost:8888(与主项目 mock-server 共用端口) ├── .env.production # VITE_API_BASE_URL=(留空,构建时按实际环境注入或走同源部署) └── src/ ├── main.js ├── App.vue ├── router/index.js # 单路由 /mobile/personal-open ├── api/ │ ├── request.js # 精简版 axios 实例:baseURL + trace header注入 + 统一错误提示,无 auth/refresh 逻辑 │ └── personalOpen.js # fetchDraftDetailApi / sendVerifyCodeApi / faceRecognizeApi / submitApplicationApi / fetchBankAgreementApi(2026-08-05新增) / uploadFileApi(2026-08-06新增) ├── constants/ │ └── enums.js # 含 BANK_AGREEMENT_FILE_TYPE_LABELS(2026-08-05新增) ├── utils/ │ └── traceHeaders.js # 从主项目原样迁移 └── views/ └── PersonalOpenMobile.vue # 页面主组件,内部拆 5 个子组件(2026-08-05 由4个拆为5个): ├── steps/InfoConfirmStep.vue ├── steps/AgreementStep.vue # 2026-08-05 新增:相关协议,调用 fetchBankAgreementApi ├── steps/FaceCompareStep.vue ├── steps/SmsVerifyStep.vue └── steps/ResultStep.vue ``` **2026-08-06 更新**:新增 `src/utils/agreementPdfFill.js`(用 `pdf-lib` 在《用户支付服务协议》 PDF末页填入客户姓名/证件号码/当前日期;姓名用 Canvas 渲染成小图片再嵌入,不嵌入自定义中文 字体——最初的字体嵌入方案已推翻,原因及推翻过程见 `docs/superpowers/specs/2026-08-06-agreement-pdf-fill-and-upload-design.md`),新增依赖 `pdf-lib`。 ## mock-server 配套改造(供本地联调,**2026-07-31 更新**:新增人脸识别路由 + 路由挂载顺序修复,详见 `docs/superpowers/specs/2026-07-31-personal-open-mobile-h5-face-recognize-and-field-sync-design.md`) 在 `mock-server/routes/personalOpenMobile.js` 实现三个路由,新增 `mock-server/routes/faceRecognize.js` 实现人脸识别路由(不带 `requireAuth`,管理后台与H5共用),`mock-server/state.js` 复用现有 `genSmsCode`/`verifySmsCode`(**2026-07-28 更新**:发码接口改为通用 `/general` 路径且不再携带 `applicationNo`,验证码 key 统一改用手机号): - `POST /wallet-management/personal-opening/mobile/draft-detail`:按 `applicationNo` 查找 `personalOpenApplications`,不存在返回 `sendFail('申请单不存在', 404)`;存在则返回真实 swagger 字段,`mobilePhone` 直接返回 `application.mobilePhone` 明文,`mobilePhoneMask` 由 mock 自行按"前3后4"算法计算返回(后端真实接口已确认直接提供该字段,前端不再自行计算),`occupationNote` 取 `application.jobNote`(种子数据字段名未变,仅响应体字段名对齐真实swagger)。**2026-08-06 更新**:新增返回 `channelNo`(取 `application.channelNo`,种子数据已有此字段),真实 swagger 已补充该字段,不再是契约缺口;`bankCardName` 仍无对应字段,前端改用 `customerName` 代替。 - `POST /customer-info/personal/face-recognize`:校验 `fileData`/`idNo`/`name` 非空,缺失返回 `sendFail`;校验通过返回 `{fileNo, recode:'0000', recodeInfo:'识别通过', sysSerialNo, sysDate}`,不模拟"人脸不符"等业务失败场景(无可参照的真实失败样例,与主项目现状一致)。 - `POST /wallet-management/personal-opening/mobile/send-verify-code/general`:不再依赖 `applicationNo`(新接口本身与申请单解耦),直接以请求体 `mobile` 校验非空后调用 `genSmsCode('WALLET_PERSONAL_OPEN_MOBILE', mobile)`(以手机号作为 key),固定码 `123456` 同时生效(与现有短信验证码 mock 规则一致)。 - `POST /wallet-management/personal-opening/mobile/submit`:申请单不存在返回 `sendFail('申请单不存在', 404)`(与其余 mock 路由一致,`code!==200`,前端停留本页提示,不进入结果页——现实中该场景仅发生于极端并发下的过期链接,重试大概率仍失败,但由前端统一按"停留本页"规则处理,不单独特殊化);校验 `verifySmsCode('WALLET_PERSONAL_OPEN_MOBILE', application.mobilePhone, verifyCode)` 失败返回 `sendFail('验证码错误或已过期', 400)`(key 与发码环节的 `mobile` 保持一致);两者均为 `code!==200`,触发前端"停留本页"分支。验证码校验通过后执行与现有 `query-progress` 相同的"生成主账户"逻辑(若尚未开立),返回 `code===200` + `{success: true, mainAccountNo, customerNo}`,前端据此跳转结果页展示成功。`data.success === false` 的业务失败分支(如核心银行侧开户失败)mock 暂不模拟(无对应真实失败场景可参照),仅保留前端结果页对该字段的展示逻辑,供后端真实接口返回失败时兜底展示。 - `GET /customer-info/bank-agreement/base64`(2026-08-05 新增):按 `fileType` 查 mock 内置的 文件名->本地占位 PDF 映射表(`mock-server/assets/bank-agreements/*.pdf`,占位内容非正式条款 文本,仅用于验证前端 iframe 预览 PDF 的渲染效果),读取文件并转 base64 返回 `{fileType, fileName, fileContent}`;`fileType` 不在枚举内返回 `sendFail('不支持的协议文件类型', 400)`。 同样**不带** `requireAuth`,且路由定义在本文件内(`personalOpenMobileRouter` 本身已在 `customerRouter` 等之前挂载,见下方"路由挂载顺序缺陷"说明),无需额外调整挂载顺序。 **路由挂载顺序缺陷(2026-07-31 修复)**:`mock-server/index.js` 里 `customerRouter`/`walletRouter`/ `userRouter`/`roleRouter`/`baseConfigRouter` 等均以 `app.use('/api', xxxRouter)` 形式挂载且内部 用 `router.use(requireAuth)`(不带路径)拦截鉴权,会拦截"该 router 挂载前缀下的任意路径"而不仅是 自身定义的路由。此前 `personalOpenMobileRouter` 挂载顺序在它们之后,导致本地 mock 环境下 H5 匿名 接口全部被误拦截返回401(已实测复现)。已将 `personalOpenMobileRouter`/`faceRecognizeRouter` 调整 到这些 router 之前挂载,详见《缺失的后端接口.md》Part 15 第100条。 此改动**仅为本地离线联调用**,真实后端接口到位后无需删除(管理后台侧 mock 路由同样长期保留,供后续演示环境使用)。 ## 环境变量 `fryl_h5/.env.development`: ``` VITE_API_BASE_URL=http://localhost:8888 ``` `fryl_h5/.env.production`: ``` VITE_API_BASE_URL= ``` (留空表示构建产物部署时与后端网关同源,或由运维在部署时通过其它机制配置;不提供页面内配置入口,符合本次用户决策。) **2026-08-01 更新:本地联调真实后端遇 CORS 的临时规避方案**——与根目录管理后台完全一致的做法: 在 `fryl_h5/.env.development.local`(已被 `.gitignore` 的 `*.local` 规则忽略,不会提交)中配置 `DEV_API_PROXY_TARGET=真实后端地址`,同时把该文件里的 `VITE_API_BASE_URL` 设为空字符串, `fryl_h5/vite.config.js` 会把浏览器发往同源 `/api` 的请求在 Node 端转发给真实后端(`changeOrigin` + 请求/响应体打印日志,与主项目 `vite.config.js` 的代理配置逐条对应,仅端口号随本工程固定为 5174)。未配置 `DEV_API_PROXY_TARGET` 时行为不变(直接请求 `VITE_API_BASE_URL` 指向的地址, 默认走本地 mock server)。仅用于本地 `npm run dev` 调试,不影响生产构建逻辑。 ## 依赖清单(新增) `vue`、`vue-router`、`vant`、`axios`、`dayjs`(`traceHeaders.js` 依赖)、`@vitejs/plugin-vue`(devDependencies)、`vite`(devDependencies)。均为独立 `fryl_h5/package.json`,与根目录 `package.json` 完全隔离。 ## 范围边界(本次不做) - ~~不实现真实人脸活体检测/比对逻辑(后端无对应接口,仅做拍照UI占位)~~——**2026-07-31 更新**: 后端已提供真实人脸识别接口,已改为真实调用,见 `docs/superpowers/specs/2026-07-31-personal-open-mobile-h5-face-recognize-and-field-sync-design.md`。 - ~~不实现协议正式文本/链接(占位文案,后续产品补充后再替换,不涉及提交参数变更)。~~—— **2026-08-05 更新**:后端已提供 `bank-agreement/base64` 真实接口,"相关协议"步骤已改为调用 真实接口展示协议原文(PDF),不再是占位文案。 - 不修改管理后台侧任何代码(二维码生成逻辑已完成)。 - 不做 H5 页面内 BASE_URL 可配置入口。 - 不支持结果页之后的"重新提交"/跳转其它页面。 - 不引入 TypeScript、不引入状态管理库(Pinia 等,本页面状态足够简单,用组件内 `ref` 管理即可)。