增加个人/企业贷款申请

main
halo 2026-08-12 21:22:13 +08:00
parent c09f43b1fd
commit 5a1d88618f
11 changed files with 1090 additions and 1151 deletions

View File

@ -1,9 +1,14 @@
# AGENTS.md # AGENTS.md
凡荣e链管理后台前端。Vue 3 + Vite,**纯 JavaScript(无 TypeScript)**,Ant Design Vue + Pinia + Vue Router + Axios。无 Node 服务端(mock-server 仅本地演示用),构建产物是纯静态 SPA。 **本仓库包含两个独立的 npm 工程,各自 `package.json`/`node_modules`/`dist`,不是 monorepo/workspaces:**
- **根目录**:凡荣e链**管理后台**前端(PC 端,银行内部人员用)。Vue 3 + Vite,**纯 JavaScript(无 TypeScript)**,Ant Design Vue + Pinia + Vue Router + Axios。无 Node 服务端(mock-server 仅本地演示用),构建产物是纯静态 SPA。
- **`fryl_h5/`**:凡荣e链**移动端 H5**(手机端,未登录终端客户扫码后访问,用于开户/贷款申请信息核对)。Vue 3 + Vite + **Vant**(不是 Ant Design Vue)+ Vue Router + Axios,**无 Pinia/任何状态管理库**,纯静态路由(无动态菜单)。它没有自己的 README/AGENTS.md,本文件下方"H5 子工程"一节即是它的说明来源;不是 git submodule,是根仓库直接纳管的目录,有自己独立的 commit 历史(`git log -- fryl_h5`)。
- 两者**共享同一个真实后端**,接口契约统一记录在根目录《缺失的后端接口.md》和 `docs/superpowers/specs/`(`fryl_h5` 自己没有这些文档)。改动前先确认改的是哪一个工程,不要把两者的依赖/组件/约定搞混。
## 常用命令 ## 常用命令
根目录(管理后台):
```bash ```bash
npm run dev # 启动开发服务器,端口 5173 npm run dev # 启动开发服务器,端口 5173
npm run build # 构建 npm run build # 构建
@ -11,7 +16,15 @@ npm run preview # 预览构建产物
npm run mock # 启动本地 mock 后端(express),默认端口 8888,可用 MOCK_PORT 覆盖 npm run mock # 启动本地 mock 后端(express),默认端口 8888,可用 MOCK_PORT 覆盖
``` ```
没有配置 lint / prettier / 单元测试脚本,不要假设它们存在,也不要臆造 `npm run lint`/`npm test`。 `fryl_h5/`(H5,命令要在该目录下跑):
```bash
npm run dev # 端口 5174(特意与根项目 5173 区分,可同时开两个工程调试)
npm run build
npm run preview
```
`fryl_h5` **没有自己的 mock 命令**,本地联调依赖根目录的 `npm run mock`(默认端口 8888,`fryl_h5/.env.development` 的 `VITE_API_BASE_URL` 也指向它)。
两个工程都没有配置 lint / prettier / 单元测试脚本,不要假设它们存在,也不要臆造 `npm run lint`/`npm test`。
## 后端接口对接(最重要,容易出错) ## 后端接口对接(最重要,容易出错)
@ -22,7 +35,7 @@ npm run mock # 启动本地 mock 后端(express),默认端口 8888,可用 M
- 分页接口 Body 含 `page`(从1开始)/`pageSize`,响应 `data``{ total, page, pageSize, records }`——字段是 **records**,不是 `list` - 分页接口 Body 含 `page`(从1开始)/`pageSize`,响应 `data``{ total, page, pageSize, records }`——字段是 **records**,不是 `list`
- 绝大多数模块**没有批量删除接口**,批量操作需用 `src/utils/loopDelete.js` 循环调单条删除并汇总结果(配合 `batchDeleteResult.js` 展示成功/失败)。 - 绝大多数模块**没有批量删除接口**,批量操作需用 `src/utils/loopDelete.js` 循环调单条删除并汇总结果(配合 `batchDeleteResult.js` 展示成功/失败)。
- 文件(身份证/营业执照等)以 **Base64 字符串**放在 JSON body 传输,不用 `multipart/form-data` - 文件(身份证/营业执照等)以 **Base64 字符串**放在 JSON body 传输,不用 `multipart/form-data`
- `.toco/CONFIG` 目前只有 `ORG_ID`/`ORG_NAME`,**没有 `PROJECT_ID`**。若需要用 MCP 工具(`getApiList`/`getApiSwaggerByUris`)读取最新 swagger,先确认该文件是否已补上 `PROJECT_ID`;否则以仓库根目录`swagger_project_scfs_2026-07-09_14-15-23.json` 和《缺失的后端接口.md》为准 - `.toco/CONFIG` 目前只有 `ORG_ID`/`ORG_NAME`,**没有 `PROJECT_ID`**。若需要用 MCP 工具(`getApiList`/`getApiSwaggerByUris`)读取最新 swagger,先确认该文件是否已补上 `PROJECT_ID`;否则以仓库根目录**最新日期**的 `swagger_project_scfs_*.json`(目前有 `2026-07-09`/`2026-07-31`/`2026-08-05` 三份快照,取文件名日期最大的)和《缺失的后端接口.md》为准,不要默认用最早那份
- `mock-server/`(`npm run mock`,端口 8888)是真实后端不可用时的应急本地演示后端,**契约已知与真实接口不完全一致**(见 `mock-server/README.md` “已知简化点”),不要把它当作接口设计依据。真实后端已接入,`.env.development` 默认 `VITE_API_BASE_URL=http://localhost:8888`,登录页齿轮图标可在运行时切换到真实后端地址(存 `localStorage.base_url_override`,见 `src/api/request.js``getBaseUrl`/`setBaseUrlOverride`)。 - `mock-server/`(`npm run mock`,端口 8888)是真实后端不可用时的应急本地演示后端,**契约已知与真实接口不完全一致**(见 `mock-server/README.md` “已知简化点”),不要把它当作接口设计依据。真实后端已接入,`.env.development` 默认 `VITE_API_BASE_URL=http://localhost:8888`,登录页齿轮图标可在运行时切换到真实后端地址(存 `localStorage.base_url_override`,见 `src/api/request.js``getBaseUrl`/`setBaseUrlOverride`)。
- 本地联调真实后端遇 CORS 时,用 `.env.development.local`(已 gitignore)配 `DEV_API_PROXY_TARGET`,走 `vite.config.js` 里的同源代理方案;别改生产构建逻辑去“解决” CORS。 - 本地联调真实后端遇 CORS 时,用 `.env.development.local`(已 gitignore)配 `DEV_API_PROXY_TARGET`,走 `vite.config.js` 里的同源代理方案;别改生产构建逻辑去“解决” CORS。
- 每次请求必须附带以下 5 个 header(**字段名大小写固定为下列形式**,首字母大写、其余小写): - 每次请求必须附带以下 5 个 header(**字段名大小写固定为下列形式**,首字母大写、其余小写):
@ -31,8 +44,8 @@ npm run mock # 启动本地 mock 后端(express),默认端口 8888,可用 M
- `serialNo`:流水号,随机值 - `serialNo`:流水号,随机值
- `transDate`:`yyyy-MM-dd` 格式日期 - `transDate`:`yyyy-MM-dd` 格式日期
- `transTradeTime`:`yyyy-MM-dd HH:mm:ss` 格式时间 - `transTradeTime`:`yyyy-MM-dd HH:mm:ss` 格式时间
- ⚠️ 现状:`src/utils/traceHeaders.js` 的 `buildTraceHeaders()` 目前只生成 `serialNo`/`transDate`/`transTradeTime` 三个小写驼峰字段,**缺少 `Appno`/`Channelno`**,尚未按最新要求改造;涉及请求 header 的改动前务必先核对/修复这里,不要照抄旧的三字段实现。
- `channelNo` 如果页面没有渠道信息,则不需要传值 - `channelNo` 如果页面没有渠道信息,则不需要传值
- `src/utils/traceHeaders.js``buildTraceHeaders(channelNo)` 已正确实现上述 5 个字段(含固定大小写、`channelNo` 按需可选),**不需要再改造**;`fryl_h5/src/utils/traceHeaders.js` 是同一实现的独立拷贝(H5 端匿名接口,永远不传 `Channelno`)。
- 枚举值必须来自 swagger/《缺失的后端接口.md》,已整理在 `src/constants/*Enums.js`,不要编造新枚举。 - 枚举值必须来自 swagger/《缺失的后端接口.md》,已整理在 `src/constants/*Enums.js`,不要编造新枚举。
## 路由架构(新增页面容易漏配置) ## 路由架构(新增页面容易漏配置)
@ -56,13 +69,13 @@ npm run mock # 启动本地 mock 后端(express),默认端口 8888,可用 M
## 目录速览 ## 目录速览
- `src/api/*.js`:按后端模块拆分的请求封装,`src/api/request.js` 是唯一的 axios 实例(含鉴权/刷新/trace header/错误提示拦截器)。 - `src/api/*.js`:按后端模块拆分的请求封装,`src/api/request.js` 是唯一的 axios 实例(含鉴权/刷新/trace header/错误提示拦截器)。
- `src/components/ProTable.vue`:通用查询表单+分页表格封装,列表页优先复用它而不是重写查询/分页逻辑。 - `src/components/ProTable.vue`:通用查询表单+分页表格封装,列表页优先复用它而不是重写查询/分页逻辑。传给它的 `fetch-data` 函数必须返回 `{ list, total }`(**注意**:后端分页字段是 `records`,列表页自己的 `loadList`/`fetchData` 函数里要做 `records → list` 的映射,不要指望 `ProTable` 或 api 封装层自动转换)。
- `src/constants/*Enums.js`:各模块枚举字典(状态/类型等),来源必须是 swagger。 - `src/constants/*Enums.js`:各模块枚举字典(状态/类型等),来源必须是 swagger。
- `src/views/<module>/<sub-module>/`:页面按模块/子模块分文件夹,列表页与新增/编辑/查看表单通常拆成 `XxxList.vue` + 共用的 `XxxForm.vue`(查看态字段只读,不是单独组件)。 - `src/views/<module>/<sub-module>/`:页面按模块/子模块分文件夹,列表页与新增/编辑/查看表单通常拆成 `XxxList.vue` + 共用的 `XxxForm.vue`(查看态字段只读,不是单独组件)。
## 图片/证件照片上传交互规范 ## 图片/证件照片上传交互规范
所有"图片上传卡片"(身份证、营业执照、人脸照片等 `list-type="picture-card"``a-upload` 封装组件)一旦已选中图片,**必须**支持鼠标 hover 缩略图时显示半透明遮罩 + 预览/重新上传/删除三个操作按钮,不允许只放一张裸 `<img>`。以 `src/components/IdCardUpload.vue` 为标准实现参照,新增或修改任何图片上传组件都要照此模式实现(`src/components/LicenseUpload.vue` 已按此模式改造): 所有"图片上传卡片"(身份证、营业执照、人脸照片等 `list-type="picture-card"``a-upload` 封装组件)一旦已选中图片,**必须**支持鼠标 hover 缩略图时显示半透明遮罩 + 预览/重新上传/删除三个操作按钮,不允许只放一张裸 `<img>`。以 `src/components/IdCardUpload.vue` 为标准实现参照,新增或修改任何图片上传组件都要照此模式实现(`LicenseUpload.vue`/`FacePhotoUpload.vue` 已按此模式改造):
- 缩略图外层包 `.thumb-wrapper`(`position: relative`),内部 `<img>` + 一个 `position:absolute; inset:0``.hover-mask`(`rgba(0,0,0,0.5)` 背景,默认 `opacity:0`,`:hover` 时 `opacity:1`,`transition:opacity 0.2s`)。 - 缩略图外层包 `.thumb-wrapper`(`position: relative`),内部 `<img>` + 一个 `position:absolute; inset:0``.hover-mask`(`rgba(0,0,0,0.5)` 背景,默认 `opacity:0`,`:hover` 时 `opacity:1`,`transition:opacity 0.2s`)。
- 遮罩内横向排列三个图标(`EyeOutlined` 预览 / `EditOutlined` 重新上传 / `DeleteOutlined` 删除),之间用 1px 竖线 `.mask-divider` 分隔,图标颜色 `#fff`,hover 变 `#1677ff` - 遮罩内横向排列三个图标(`EyeOutlined` 预览 / `EditOutlined` 重新上传 / `DeleteOutlined` 删除),之间用 1px 竖线 `.mask-divider` 分隔,图标颜色 `#fff`,hover 变 `#1677ff`
@ -70,7 +83,7 @@ npm run mock # 启动本地 mock 后端(express),默认端口 8888,可用 M
- **删除**:图标加 `@click.stop`,清空本地预览态并 `emit('update:fileNo', '')`,让父组件感知已清空。 - **删除**:图标加 `@click.stop`,清空本地预览态并 `emit('update:fileNo', '')`,让父组件感知已清空。
- **重新上传**:图标**不加** `@click.stop`,让点击事件自然冒泡到外层 `a-upload` 容器,复用其原生文件选择流程触发重新选图(不要单独写一套"重新上传"逻辑)。 - **重新上传**:图标**不加** `@click.stop`,让点击事件自然冒泡到外层 `a-upload` 容器,复用其原生文件选择流程触发重新选图(不要单独写一套"重新上传"逻辑)。
- 组件需支持 `readonly` prop(默认 `false`):为 `true` 时只保留预览图标,隐藏"重新上传/删除"——用于查看详情等只读表单场景。调用处如果页面本身有 `isDetail` 之类的只读态标记,必须把它传给上传组件的 `readonly`,不要漏传(否则只读页面会出现可用的删除按钮)。 - 组件需支持 `readonly` prop(默认 `false`):为 `true` 时只保留预览图标,隐藏"重新上传/删除"——用于查看详情等只读表单场景。调用处如果页面本身有 `isDetail` 之类的只读态标记,必须把它传给上传组件的 `readonly`,不要漏传(否则只读页面会出现可用的删除按钮)。
- 当前项目**没有抽出通用的图片上传基础组件**,`IdCardUpload.vue`/`LicenseUpload.vue`/`FacePhotoUpload.vue` 是各自独立实现(结构相似但不复用)。`FacePhotoUpload.vue` 目前还没有按此规范改造,后续涉及到它时要一并补上 - 当前项目**没有抽出通用的图片上传基础组件**,`IdCardUpload.vue`/`LicenseUpload.vue`/`FacePhotoUpload.vue` 是各自独立实现(结构相似但不复用)。新增同类组件时照抄这三个的模式,不要指望后续会抽公共组件
## 耗时操作统一等待态规范 ## 耗时操作统一等待态规范
@ -83,6 +96,17 @@ npm run mock # 启动本地 mock 后端(express),默认端口 8888,可用 M
- 图片上传组件(见上一节规范)通过 `update:uploading` 事件把自身上传中状态回传给父表单,父表单纳入 `busy` 统一遮罩,并在提交前校验各 `xxxUploading` 状态、阻止在文件尚未上传完成时提交表单。 - 图片上传组件(见上一节规范)通过 `update:uploading` 事件把自身上传中状态回传给父表单,父表单纳入 `busy` 统一遮罩,并在提交前校验各 `xxxUploading` 状态、阻止在文件尚未上传完成时提交表单。
- 新增页面/表单时按此模式补齐,不需要抽公共 hook/组件,保持和现有页面一致的写法即可。 - 新增页面/表单时按此模式补齐,不需要抽公共 hook/组件,保持和现有页面一致的写法即可。
## H5 子工程(`fryl_h5/`)特殊约定
- 路由**是纯静态声明**(`fryl_h5/src/router/index.js`),没有根项目那套动态菜单机制;向导式流程(信息核对→协议/人脸→短验→完成)的步骤切换靠页面内部 `currentStep` ref 维护,**不为每个步骤单独设路由**,避免用户分享/刷新中间步骤 URL 导致状态错乱。新增步骤直接加 `views/steps/**` 组件即可,不用碰路由文件。
- `fryl_h5/src/api/request.js` 是独立的精简版 axios 实例,**不注入 `Authorization`、不做 401 刷新**(面向未登录终端客户的匿名接口),不要照抄根项目 `src/api/request.js` 的鉴权逻辑。
- **无 Pinia**,所有状态用组件内 `ref` 管理,不要为这个子工程引入状态管理库。
- Trace header 同样用 `buildTraceHeaders()`,但**永远不传 `channelNo`**(二维码链接不带渠道上下文)。
- 与根项目**复用同一批后端接口**:例如协议模板接口 `GET /api/customer-info/bank-agreement/base64` 根项目在 `src/api/customer.js` 封装,`fryl_h5` 在 `src/api/personalOpen.js` 里对同一接口重新封装了一份(两处各自维护,改接口字段要两边一起改)。
- 生成二维码指向 `fryl_h5` 哪个页面的环境变量(`VITE_H5_QRCODE_BASE_URL`/`VITE_H5_QRCODE_PATH*`)配置在**根项目**的 `.env.development`/`.env.production` 里,由根项目 `src/utils/qrCode.js` 拼出完整链接,不在 `fryl_h5` 自己的 env 文件中。
- 相关设计文档在根目录 `docs/superpowers/specs/`,文件名含 `mobile-h5`/`loan-application-mobile` 关键字(如 `2026-07-27-personal-open-mobile-h5-design.md`、`2026-08-11-loan-application-mobile-confirm-design.md`),改动前先查。
- 企业开户 H5 的提交接口本地 `mock-server` 未实现,只能连真实后端联调验证;涉及企业开户 H5 时先看《缺失的后端接口.md》里标注"前端假设"的段落,不要假定 mock 能跑通。
## 其他约定 ## 其他约定
- BASE_URL 必须保留界面可配置入口(已实现,见上文),不要改成硬编码。 - BASE_URL 必须保留界面可配置入口(已实现,见上文),不要改成硬编码。

View File

@ -45,7 +45,7 @@ router.post('/loan-application/detail', (req, res) => {
// ---------------- 个人贷款申请 ---------------- // ---------------- 个人贷款申请 ----------------
router.post('/personal-loan-application/create', (req, res) => { router.post('/personal-loan-application/create', (req, res) => {
const body = req.body || {} const body = req.body || {}
if (!body.channel) return sendFail(res, '所属渠道不能为空', 400) if (!body.channelNo) return sendFail(res, '所属渠道不能为空', 400)
if (!body.loanProductCode) return sendFail(res, '贷款产品编号不能为空', 400) if (!body.loanProductCode) return sendFail(res, '贷款产品编号不能为空', 400)
if (!body.goodsCategory) return sendFail(res, '商品类目不能为空', 400) if (!body.goodsCategory) return sendFail(res, '商品类目不能为空', 400)
if (!body.paymentCycle) return sendFail(res, '回款周期不能为空', 400) if (!body.paymentCycle) return sendFail(res, '回款周期不能为空', 400)
@ -85,7 +85,7 @@ router.post('/personal-loan-application/update', (req, res) => {
// ---------------- 企业贷款申请 ---------------- // ---------------- 企业贷款申请 ----------------
router.post('/enterprise-loan-application/create', (req, res) => { router.post('/enterprise-loan-application/create', (req, res) => {
const body = req.body || {} const body = req.body || {}
if (!body.channel) return sendFail(res, '所属渠道不能为空', 400) if (!body.channelNo) return sendFail(res, '所属渠道不能为空', 400)
if (!body.loanProductCode) return sendFail(res, '贷款产品编号不能为空', 400) if (!body.loanProductCode) return sendFail(res, '贷款产品编号不能为空', 400)
if (!body.enterpriseName) return sendFail(res, '企业客户不能为空', 400) if (!body.enterpriseName) return sendFail(res, '企业客户不能为空', 400)
if (!body.goodsCategory) return sendFail(res, '商品类目不能为空', 400) if (!body.goodsCategory) return sendFail(res, '商品类目不能为空', 400)

View File

@ -18,6 +18,8 @@ export const uploadCustomerFileApi = (data, channelNo) =>
// 入参 {fileData(人脸照片base64,字段名含"zip"但实际直传图片base64即可)/idNo/name/channelNo}; // 入参 {fileData(人脸照片base64,字段名含"zip"但实际直传图片base64即可)/idNo/name/channelNo};
// 响应 {fileNo(识别通过后可直接用于个人开户提交的人脸照片文件编号)/recode/recodeInfo(识别结果码/说明, // 响应 {fileNo(识别通过后可直接用于个人开户提交的人脸照片文件编号)/recode/recodeInfo(识别结果码/说明,
// 具体取值枚举后端未文档化,前端仅按"fileNo 是否返回"判断识别是否成功,recodeInfo 仅作提示展示)} // 具体取值枚举后端未文档化,前端仅按"fileNo 是否返回"判断识别是否成功,recodeInfo 仅作提示展示)}
// 产品要求暂时不调用此接口(个人开户/个人贷款申请/企业贷款申请的"正面免冠照"上传项改为直接调用
// 上面的 uploadCustomerFileApi 只拿 fileNo),此导出暂无调用方,保留以便后续恢复识别校验。
export const faceRecognizeApi = (data, channelNo) => export const faceRecognizeApi = (data, channelNo) =>
request.post('/api/customer-info/personal/face-recognize', data, { channelNo }) request.post('/api/customer-info/personal/face-recognize', data, { channelNo })

View File

@ -1,10 +1,12 @@
<!-- src/components/FacePhotoUpload.vue --> <!-- src/components/FacePhotoUpload.vue -->
<!-- 个人开户"证件照片"区块的"正面免冠照"(人脸照片,fileType='04')上传 <!-- 个人开户/个人贷款申请/企业贷款申请"证件照片"区块的"正面免冠照"(人脸照片,fileType='04')上传
后端已提供人脸识别接口(/api/customer-info/personal/face-recognize,e222005), 后端已提供人脸识别接口(/api/customer-info/personal/face-recognize,e222005),
选好照片后直接调用该接口做"人脸与身份证号/姓名匹配"识别,识别通过后接口直接返回可用于 选好照片后调用该接口做"人脸与身份证号/姓名匹配"识别,识别通过后接口直接返回可用于提交的
提交的文件编号(fileNo),不再走通用的 upload-file 普通上传接口 文件编号(fileNo)产品要求暂时不调用该识别接口,选好照片后改为直接调用通用文件上传接口
uploadCustomerFileApi( IdCardUpload.vue/LicenseUpload.vue 一致)只拿 fileNo,不做任何
人脸比对/校验;若后续需要恢复识别校验,可参考 src/api/customer.js 保留的 faceRecognizeApi
"去采集"按钮项目暂无真实的摄像头采集/活体检测能力(缺失的后端接口.md),仍是与图片框 "去采集"按钮项目暂无真实的摄像头采集/活体检测能力(缺失的后端接口.md),仍是与图片框
共享同一个文件选择弹窗的入口,选中的照片一样会送去做人脸识别 共享同一个文件选择弹窗的入口
readonly(2026-07-23 补充,对齐 IdCardUpload.vue hover 遮罩+预览/删除 标准实现): readonly(2026-07-23 补充,对齐 IdCardUpload.vue hover 遮罩+预览/删除 标准实现):
查看态(个人开户申请详情)只保留预览,隐藏"去采集"按钮与重新上传/删除操作 --> 查看态(个人开户申请详情)只保留预览,隐藏"去采集"按钮与重新上传/删除操作 -->
<template> <template>
@ -14,7 +16,7 @@
list-type="picture-card" list-type="picture-card"
:show-upload-list="false" :show-upload-list="false"
:before-upload="handleBeforeUpload" :before-upload="handleBeforeUpload"
:disabled="readonly || !canRecognize || uploading" :disabled="readonly || !canUpload || uploading"
> >
<div v-if="previewUrl" class="thumb-wrapper"> <div v-if="previewUrl" class="thumb-wrapper">
<img :src="previewUrl" class="preview-img" alt="预览" /> <img :src="previewUrl" class="preview-img" alt="预览" />
@ -34,7 +36,7 @@
<div class="upload-text">正面免冠照</div> <div class="upload-text">正面免冠照</div>
</div> </div>
</a-upload> </a-upload>
<a-button v-if="!readonly" :disabled="!canRecognize || uploading" @click="handleCollectClick"></a-button> <a-button v-if="!readonly" :disabled="!canUpload || uploading" @click="handleCollectClick"></a-button>
</a-space> </a-space>
<div v-if="!readonly && !channelNo" class="hint"></div> <div v-if="!readonly && !channelNo" class="hint"></div>
<div v-else-if="!readonly && (!idNo || !name)" class="hint">请先选择客户</div> <div v-else-if="!readonly && (!idNo || !name)" class="hint">请先选择客户</div>
@ -49,12 +51,12 @@
import { ref, computed, watch } from 'vue' import { ref, computed, watch } from 'vue'
import { showErrorModal } from '@/utils/errorModal' import { showErrorModal } from '@/utils/errorModal'
import { PlusOutlined, LoadingOutlined, EyeOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue' import { PlusOutlined, LoadingOutlined, EyeOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue'
import { faceRecognizeApi } from '@/api/customer' import { uploadCustomerFileApi } from '@/api/customer'
const props = defineProps({ const props = defineProps({
channelNo: { type: String, default: undefined }, channelNo: { type: String, default: undefined },
idNo: { type: String, default: '' }, // , idNo: { type: String, default: '' }, // (,"")
name: { type: String, default: '' }, // , name: { type: String, default: '' }, // (,"")
// ():,""/ // ():,""/
readonly: { type: Boolean, default: false } readonly: { type: Boolean, default: false }
}) })
@ -64,7 +66,7 @@ const uploading = ref(false)
const previewUrl = ref('') const previewUrl = ref('')
const previewVisible = ref(false) const previewVisible = ref(false)
const fileInputRef = ref(null) const fileInputRef = ref(null)
const canRecognize = computed(() => !!props.channelNo && !!props.idNo && !!props.name) const canUpload = computed(() => !!props.channelNo && !!props.idNo && !!props.name)
watch(uploading, (val) => emit('update:uploading', val)) watch(uploading, (val) => emit('update:uploading', val))
function readAsBase64(file) { function readAsBase64(file) {
@ -87,24 +89,20 @@ async function handleBeforeUpload(file) {
} }
uploading.value = true uploading.value = true
try { try {
const fileData = await readAsBase64(file) const fileBase64 = await readAsBase64(file)
previewUrl.value = fileData previewUrl.value = fileBase64
const res = await faceRecognizeApi( const res = await uploadCustomerFileApi({ fileBase64, channelNo: props.channelNo }, props.channelNo)
{ fileData, idNo: props.idNo, name: props.name, channelNo: props.channelNo }, if (res.data.code === 200 && res.data.data && res.data.data.fileNo) {
props.channelNo emit('update:fileNo', res.data.data.fileNo)
)
const result = res.data.code === 200 ? res.data.data : null
if (result && result.fileNo) {
emit('update:fileNo', result.fileNo)
} else { } else {
previewUrl.value = '' previewUrl.value = ''
emit('update:fileNo', '') emit('update:fileNo', '')
showErrorModal((result && result.recodeInfo) || res.data.message || '人脸识别失败,请重新拍摄/上传', '人脸识别未通过') showErrorModal(res.data.message || '上传失败,请重新拍摄/上传')
} }
} catch (e) { } catch (e) {
previewUrl.value = '' previewUrl.value = ''
emit('update:fileNo', '') emit('update:fileNo', '')
showErrorModal('人脸识别失败,请重新拍摄/上传', '人脸识别未通过') showErrorModal('上传失败,请重新拍摄/上传')
} finally { } finally {
uploading.value = false uploading.value = false
} }
@ -112,7 +110,7 @@ async function handleBeforeUpload(file) {
} }
// "":/, // "":/,
// <input type="file"> ,"" // <input type="file"> ,""
function handleCollectClick() { function handleCollectClick() {
if (!props.channelNo) { if (!props.channelNo) {
showErrorModal('请先选择所属渠道') showErrorModal('请先选择所属渠道')

View File

@ -166,18 +166,20 @@ export const LEGAL_PERSON_ACCOUNT_TYPE_OPTIONS = [
{ value: "NON_LOCAL_CUSTOMER", label: "异地客户" } { value: "NON_LOCAL_CUSTOMER", label: "异地客户" }
] ]
// 企业贷款申请企业信息"高新技术产业标识"下拉选项:原型截图给出"已获取/正申请"两项, // 企业贷款申请企业信息"企业经营场地所有权"单选选项:原型截图给出"自有/租赁/其他"三项,
// CreateEnterpriseLoanDraftBto 无对应字段(已知缺口,登记于《缺失的后端接口.md》Part 25)。 // CreateEnterpriseLoanDraftBto 无对应字段(已知缺口,登记于《缺失的后端接口.md》Part 25)。
export const HIGH_TECH_INDUSTRY_FLAG_OPTIONS = [ // 2026-08-12 补充:此前同名值曾误挂在已删除的"企业股权其他情况"字段上,现更正归属到本字段。
{ value: "OBTAINED", label: "已获取" }, export const BUSINESS_PREMISES_OWNERSHIP_OPTIONS = [
{ value: "APPLYING", label: "正申请" }
]
// 企业贷款申请企业信息"企业股权其他情况"下拉选项:原型截图给出"自有/租用/其他"三项,
// CreateEnterpriseLoanDraftBto 无对应字段(已知缺口,登记于《缺失的后端接口.md》Part 25)。
export const ENTERPRISE_EQUITY_OTHER_TYPE_OPTIONS = [
{ value: "OWNED", label: "自有" }, { value: "OWNED", label: "自有" },
{ value: "LEASED", label: "租" }, { value: "LEASED", label: "租赁" },
{ value: "OTHER", label: "其他" } { value: "OTHER", label: "其他" }
] ]
// 企业贷款申请企业信息"政府融资平台分类(银监)"单选选项:原型截图给出"检测/监管"两项(文案疑似
// 截图 OCR 识别所限,经用户确认按原文保留),CreateEnterpriseLoanDraftBto 无对应字段(已知缺口,
// 登记于《缺失的后端接口.md》Part 25)。
export const GOVERNMENT_PLATFORM_CATEGORY_OPTIONS = [
{ value: "INSPECTION", label: "检测" },
{ value: "SUPERVISION", label: "监管" }
]

File diff suppressed because it is too large Load Diff

View File

@ -13,6 +13,9 @@
(钱包账号账户类型经营实体机构名称申请/创建时间均缺失),前端按截图展示列结构, (钱包账号账户类型经营实体机构名称申请/创建时间均缺失),前端按截图展示列结构,
内容展示为"-",不编造数据,已登记于缺失的后端接口.mdPart 16"所属机构"利用已有的 内容展示为"-",不编造数据,已登记于缺失的后端接口.mdPart 16"所属机构"利用已有的
`fetchOrgListAllApi` organizationId -> 机构名称映射(与个人/企业客户列表页同惯例) `fetchOrgListAllApi` organizationId -> 机构名称映射(与个人/企业客户列表页同惯例)
- "申请编号"(`loanApplicationId`)非原型截图列,是后续按需补充:该字段本身已随列表接口
返回(详情页跳转/编辑跳转已在用),用于柜员定位申请比对H5扫码链接里的 loanApplicationId,
展示位置/宽度沿用 PersonalOpenList.vue"申请编号"列的既有惯例(序号后纯文本220px)
- 勾选框列按截图新增(ProTable 默认支持 rowSelection),原型页面本身也未展示任何批量操作 - 勾选框列按截图新增(ProTable 默认支持 rowSelection),原型页面本身也未展示任何批量操作
按钮,故本次仅还原勾选交互,未绑定批量业务操作 按钮,故本次仅还原勾选交互,未绑定批量业务操作
- "申请金额"格式化为"¥x,xxx.xx万"(原始金额单位为元),"申请利率"补充百分号,"类型"列展示 - "申请金额"格式化为"¥x,xxx.xx万"(原始金额单位为元),"申请利率"补充百分号,"类型"列展示
@ -122,6 +125,7 @@ const statusMap = {
const columns = [ const columns = [
{ title: 'No', dataIndex: 'index', width: 60 }, { title: 'No', dataIndex: 'index', width: 60 },
{ title: '申请编号', dataIndex: 'loanApplicationId', width: 220 },
{ title: '客户', dataIndex: 'customerName' }, { title: '客户', dataIndex: 'customerName' },
{ title: '钱包', dataIndex: 'walletAccount' }, { title: '钱包', dataIndex: 'walletAccount' },
{ title: '经营实体', dataIndex: 'businessEntity' }, { title: '经营实体', dataIndex: 'businessEntity' },

View File

@ -38,97 +38,72 @@
</a-card> </a-card>
<a-spin :spinning="busy" :tip="busyTip"> <a-spin :spinning="busy" :tip="busyTip">
<a-form layout="vertical" :model="form" :disabled="isDetail"> <a-form
layout="horizontal"
:label-col="{ style: { width: '130px' } }"
:wrapper-col="{ style: { flex: '1 1 auto', maxWidth: '420px' } }"
:model="form"
:disabled="isDetail"
class="single-column-form"
>
<a-card title="基础信息" style="margin-bottom: 16px"> <a-card title="基础信息" style="margin-bottom: 16px">
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="所属渠道" required> <a-form-item label="所属渠道" required>
<ChannelSelect v-model:model-value="form.channel" /> <ChannelSelect v-model:model-value="form.channelNo" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="所属机构"> <a-form-item label="所属机构">
<OrgTreeSelect v-model="form.organizationId" placeholder="请选择所属机构" /> <OrgTreeSelect v-model="form.organizationId" placeholder="请选择所属机构" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="贷款产品编号" required> <a-form-item label="贷款产品编号" required>
<a-space style="width: 100%"> <a-space style="width: 100%">
<a-input v-model:value="form.loanProductCode" placeholder="请输入或选择贷款产品编号" /> <a-input v-model:value="form.loanProductCode" placeholder="请输入或选择贷款产品编号" />
<a-button v-if="!isDetail" @click="openProductPicker"></a-button> <a-button v-if="!isDetail" @click="openProductPicker"></a-button>
</a-space> </a-space>
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="贷款申请人钱包" required> <a-form-item label="贷款申请人钱包" required>
<a-space> <a-space>
<span v-if="walletAccountNo">{{ walletAccountNo }}<template v-if="walletMainBalance"> (:{{ walletMainBalance }})</template></span> <span v-if="walletAccountNo">{{ walletAccountNo }}<template v-if="walletMainBalance"> (:{{ walletMainBalance }})</template></span>
<a-button v-if="!isDetail" @click="openWalletPicker">{{ walletAccountNo ? '' : '' }}</a-button> <a-button v-if="!isDetail" @click="openWalletPicker">{{ walletAccountNo ? '' : '' }}</a-button>
</a-space> </a-space>
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="申请金额(元)" required> <a-form-item label="申请金额(元)" required>
<a-input v-model:value="form.loanAmount" placeholder="请输入申请金额" /> <a-input v-model:value="form.loanAmount" placeholder="请输入申请金额" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="申请利率(%)" required> <a-form-item label="申请利率(%)" required>
<a-input v-model:value="form.applicationInterestRate" placeholder="请输入申请利率" /> <a-input v-model:value="form.applicationInterestRate" placeholder="请输入申请利率" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="证件号码"> <a-form-item label="证件号码">
<a-input v-model:value="certificateNumber" placeholder="选择钱包后自动回显,可手动修改" /> <a-input v-model:value="certificateNumber" placeholder="选择钱包后自动回显,可手动修改" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="手机号码"> <a-form-item label="手机号码">
<a-input v-model:value="form.phoneNo" placeholder="请输入11位手机号" /> <a-input v-model:value="form.phoneNo" placeholder="请输入11位手机号" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="户籍/常住地市" required> <a-form-item label="户籍/常住地市" required>
<AdministrativeDivisionCascader v-model:value="form.registeredCity" /> <AdministrativeDivisionCascader v-model:value="form.registeredCity" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="16">
<a-form-item label="户籍/常住地址" required> <a-form-item label="户籍/常住地址" required>
<a-input v-model:value="form.permanentAddress" placeholder="请输入户籍/常住地址" /> <a-input v-model:value="form.permanentAddress" placeholder="请输入户籍/常住地址" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="民族" required> <a-form-item label="民族" required>
<a-select v-model:value="form.ethnicity" placeholder="请选择民族" show-search option-filter-prop="label"> <a-select v-model:value="form.ethnicity" placeholder="请选择民族" show-search option-filter-prop="label">
<a-select-option v-for="opt in ETHNICITY_OPTIONS" :key="opt.value" :value="opt.value">{{ opt.label }}</a-select-option> <a-select-option v-for="opt in ETHNICITY_OPTIONS" :key="opt.value" :value="opt.value">{{ opt.label }}</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="学历" required> <a-form-item label="学历" required>
<a-select v-model:value="form.educationLevel" placeholder="请选择学历"> <a-select v-model:value="form.educationLevel" placeholder="请选择学历">
<a-select-option v-for="opt in EDUCATION_LEVEL_OPTIONS" :key="opt.value" :value="opt.value">{{ opt.label }}</a-select-option> <a-select-option v-for="opt in EDUCATION_LEVEL_OPTIONS" :key="opt.value" :value="opt.value">{{ opt.label }}</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="婚姻状况" required> <a-form-item label="婚姻状况" required>
<a-select v-model:value="form.maritalStatus" placeholder="请选择婚姻状况"> <a-select v-model:value="form.maritalStatus" placeholder="请选择婚姻状况">
<a-select-option v-for="opt in MARITAL_STATUS_OPTIONS" :key="opt.value" :value="opt.value">{{ opt.label }}</a-select-option> <a-select-option v-for="opt in MARITAL_STATUS_OPTIONS" :key="opt.value" :value="opt.value">{{ opt.label }}</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="个人年收入(元)" required> <a-form-item label="个人年收入(元)" required>
<a-input v-model:value="form.annualPersonalIncome" placeholder="请输入个人年收入" /> <a-input v-model:value="form.annualPersonalIncome" placeholder="请输入个人年收入" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="家庭年收入(元)" required> <a-form-item label="家庭年收入(元)" required>
<a-input v-model:value="form.annualFamilyIncome" placeholder="请输入家庭年收入" /> <a-input v-model:value="form.annualFamilyIncome" placeholder="请输入家庭年收入" />
</a-form-item> </a-form-item>
</a-col>
</a-row>
</a-card> </a-card>
<a-card title="经营实体信息" style="margin-bottom: 16px"> <a-card title="经营实体信息" style="margin-bottom: 16px">
@ -138,105 +113,71 @@
<a-switch v-model:checked="businessEntityEnabled" :disabled="isDetail" /> <a-switch v-model:checked="businessEntityEnabled" :disabled="isDetail" />
</a-space> </a-space>
</template> </template>
<a-row v-if="businessEntityEnabled" :gutter="16"> <template v-if="businessEntityEnabled">
<a-col :span="8">
<a-form-item label="证件类型"> <a-form-item label="证件类型">
<a-input value="统一社会信用代码" disabled /> <a-input value="统一社会信用代码" disabled />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="经营企业" required> <a-form-item label="经营企业" required>
<a-space> <a-space>
<span v-if="form.enterpriseName">{{ form.enterpriseName }}</span> <span v-if="form.enterpriseName">{{ form.enterpriseName }}</span>
<a-button v-if="!isDetail" @click="openEnterpriseWalletPicker">{{ form.enterpriseName ? '' : '' }}</a-button> <a-button v-if="!isDetail" @click="openEnterpriseWalletPicker">{{ form.enterpriseName ? '' : '' }}</a-button>
</a-space> </a-space>
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="证件号码"> <a-form-item label="证件号码">
<a-input v-model:value="enterpriseCertificateNumber" placeholder="选择经营企业后自动回显,可手动修改" /> <a-input v-model:value="enterpriseCertificateNumber" placeholder="选择经营企业后自动回显,可手动修改" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="与经营企业关系" required> <a-form-item label="与经营企业关系" required>
<a-select v-model:value="enterpriseRelation" placeholder="请选择与经营企业关系"> <a-select v-model:value="enterpriseRelation" placeholder="请选择与经营企业关系">
<a-select-option v-for="opt in ENTERPRISE_RELATION_OPTIONS" :key="opt.value" :value="opt.value">{{ opt.label }}</a-select-option> <a-select-option v-for="opt in ENTERPRISE_RELATION_OPTIONS" :key="opt.value" :value="opt.value">{{ opt.label }}</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="企业类型"> <a-form-item label="企业类型">
<DictSelect v-model:model-value="enterpriseType" dict-type-code="qiyeleixing" placeholder="请选择企业类型" /> <DictSelect v-model:model-value="enterpriseType" dict-type-code="qiyeleixing" placeholder="请选择企业类型" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="经营起始日" required> <a-form-item label="经营起始日" required>
<a-date-picker v-model:value="businessStartDateModel" style="width: 100%" /> <a-date-picker v-model:value="businessStartDateModel" style="width: 100%" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="经营范围" required> <a-form-item label="经营范围" required>
<a-input v-model:value="form.businessScope" placeholder="经营执照、挂靠说明描述等" /> <a-input v-model:value="form.businessScope" placeholder="经营执照、挂靠说明描述等" />
<div class="business-scope-hint">请输入经营范围,至少10个汉字</div> <div class="business-scope-hint">请输入经营范围,至少10个汉字</div>
</a-form-item> </a-form-item>
</a-col> </template>
</a-row>
</a-card> </a-card>
<a-card title="紧急联系人信息" style="margin-bottom: 16px"> <a-card title="紧急联系人信息" style="margin-bottom: 16px">
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="联系人名称" required> <a-form-item label="联系人名称" required>
<a-input v-model:value="form.emergencyContactName" placeholder="请输入紧急联系人名称" /> <a-input v-model:value="form.emergencyContactName" placeholder="请输入紧急联系人名称" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="联系人电话"> <a-form-item label="联系人电话">
<a-input v-model:value="form.emergencyContactPhone" placeholder="请输入紧急联系人电话" /> <a-input v-model:value="form.emergencyContactPhone" placeholder="请输入紧急联系人电话" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="与紧急联系人关系"> <a-form-item label="与紧急联系人关系">
<a-select v-model:value="form.emergencyContactRelation" placeholder="请选择与紧急联系人关系"> <a-select v-model:value="form.emergencyContactRelation" placeholder="请选择与紧急联系人关系">
<a-select-option v-for="opt in EMERGENCY_CONTACT_RELATION_OPTIONS" :key="opt.value" :value="opt.value">{{ opt.label }}</a-select-option> <a-select-option v-for="opt in EMERGENCY_CONTACT_RELATION_OPTIONS" :key="opt.value" :value="opt.value">{{ opt.label }}</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</a-col>
</a-row>
</a-card> </a-card>
<a-card v-if="form.maritalStatus === 'MARRIED'" title="配偶信息" style="margin-bottom: 16px"> <a-card v-if="form.maritalStatus === 'MARRIED'" title="配偶信息" style="margin-bottom: 16px">
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="配偶姓名"> <a-form-item label="配偶姓名">
<a-input v-model:value="form.spouseName" placeholder="请输入配偶姓名" /> <a-input v-model:value="form.spouseName" placeholder="请输入配偶姓名" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="配偶电话"> <a-form-item label="配偶电话">
<a-input v-model:value="form.spousePhone" placeholder="请输入配偶电话" /> <a-input v-model:value="form.spousePhone" placeholder="请输入配偶电话" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="配偶性别"> <a-form-item label="配偶性别">
<a-select v-model:value="form.spouseGender" placeholder="请选择性别"> <a-select v-model:value="form.spouseGender" placeholder="请选择性别">
<a-select-option value="MALE"></a-select-option> <a-select-option value="MALE"></a-select-option>
<a-select-option value="FEMALE"></a-select-option> <a-select-option value="FEMALE"></a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="配偶证件类型"> <a-form-item label="配偶证件类型">
<a-input v-model:value="form.spouseCertType" placeholder="如:身份证" /> <a-input v-model:value="form.spouseCertType" placeholder="如:身份证" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="配偶证件号码"> <a-form-item label="配偶证件号码">
<a-input v-model:value="form.spouseCertId" placeholder="请输入配偶证件号码" /> <a-input v-model:value="form.spouseCertId" placeholder="请输入配偶证件号码" />
</a-form-item> </a-form-item>
</a-col>
</a-row>
</a-card> </a-card>
<a-card title="影像资料" style="margin-bottom: 16px"> <a-card title="影像资料" style="margin-bottom: 16px">
@ -245,7 +186,7 @@
<div class="upload-label">身份证人像面</div> <div class="upload-label">身份证人像面</div>
<IdCardUpload <IdCardUpload
side="front" side="front"
:channel-no="form.channel" :channel-no="form.channelNo"
:readonly="isDetail" :readonly="isDetail"
v-model:file-no="idCardFrontFileNo" v-model:file-no="idCardFrontFileNo"
v-model:uploading="idCardFrontUploading" v-model:uploading="idCardFrontUploading"
@ -255,7 +196,7 @@
<div class="upload-label">身份证国徽面</div> <div class="upload-label">身份证国徽面</div>
<IdCardUpload <IdCardUpload
side="back" side="back"
:channel-no="form.channel" :channel-no="form.channelNo"
:readonly="isDetail" :readonly="isDetail"
v-model:file-no="idCardBackFileNo" v-model:file-no="idCardBackFileNo"
v-model:uploading="idCardBackUploading" v-model:uploading="idCardBackUploading"
@ -265,7 +206,7 @@
<div class="upload-label">代付情况说明</div> <div class="upload-label">代付情况说明</div>
<a-space align="start"> <a-space align="start">
<LicenseUpload <LicenseUpload
:channel-no="form.channel" :channel-no="form.channelNo"
:readonly="isDetail" :readonly="isDetail"
upload-text="上传代付情况说明" upload-text="上传代付情况说明"
v-model:file-no="businessLicenseFileNo" v-model:file-no="businessLicenseFileNo"
@ -277,7 +218,7 @@
<div> <div>
<div class="upload-label">人脸识别信息</div> <div class="upload-label">人脸识别信息</div>
<FacePhotoUpload <FacePhotoUpload
:channel-no="form.channel" :channel-no="form.channelNo"
:id-no="certificateNumber" :id-no="certificateNumber"
:name="form.customerName" :name="form.customerName"
:readonly="isDetail" :readonly="isDetail"
@ -460,7 +401,7 @@ const statusText = computed(
const form = reactive({ const form = reactive({
loanApplicationId: undefined, loanApplicationId: undefined,
organizationId: getCurrentOrganizationId(), organizationId: getCurrentOrganizationId(),
channel: undefined, channelNo: undefined,
loanProductCode: '', loanProductCode: '',
customerName: '', customerName: '',
goodsCategory: undefined, goodsCategory: undefined,
@ -513,9 +454,9 @@ const walletMainBalance = ref('')
async function handleSelectWallet(record) { async function handleSelectWallet(record) {
walletAccountNo.value = record.accountNo || '' walletAccountNo.value = record.accountNo || ''
walletMainBalance.value = '' walletMainBalance.value = ''
if (form.channel) { if (form.channelNo) {
try { try {
const detailRes = await fetchWalletAccountDetailApi({ id: record.id, channelNo: form.channel }) const detailRes = await fetchWalletAccountDetailApi({ id: record.id, channelNo: form.channelNo })
if (detailRes.data.code === 200) { if (detailRes.data.code === 200) {
walletMainBalance.value = detailRes.data.data?.mainBalance || '' walletMainBalance.value = detailRes.data.data?.mainBalance || ''
} }
@ -625,7 +566,7 @@ async function loadDetail() {
Object.assign(form, { Object.assign(form, {
loanApplicationId: data.loanApplicationId, loanApplicationId: data.loanApplicationId,
organizationId: data.organizationId, organizationId: data.organizationId,
channel: data.channel, channelNo: data.channel,
loanProductCode: data.loanProductCode, loanProductCode: data.loanProductCode,
customerName: data.customerName, customerName: data.customerName,
goodsCategory: data.goodsCategory, goodsCategory: data.goodsCategory,
@ -681,7 +622,7 @@ function buildImageList() {
} }
async function handleConfirmButtonClick() { async function handleConfirmButtonClick() {
if (!form.channel) { if (!form.channelNo) {
showErrorModal('请先选择所属渠道') showErrorModal('请先选择所属渠道')
return return
} }
@ -711,7 +652,7 @@ async function submitApplication() {
try { try {
const payload = { const payload = {
organizationId: form.organizationId, organizationId: form.organizationId,
channel: form.channel, channelNo: form.channelNo,
loanProductCode: form.loanProductCode, loanProductCode: form.loanProductCode,
customerName: form.customerName, customerName: form.customerName,
// 2026-08-10 "", 7 ;create // 2026-08-10 "", 7 ;create
@ -808,6 +749,9 @@ function goBack() {
</script> </script>
<style scoped> <style scoped>
.single-column-form {
max-width: 680px;
}
.upload-label { .upload-label {
margin-bottom: 8px; margin-bottom: 8px;
font-size: 12px; font-size: 12px;

View File

@ -45,9 +45,14 @@
<a-card style="margin-bottom: 16px"> <a-card style="margin-bottom: 16px">
<div class="section-title">基础信息<span class="required-mark">*</span></div> <div class="section-title">基础信息<span class="required-mark">*</span></div>
<a-form layout="vertical" :model="form" :disabled="fieldsDisabled"> <a-form
<a-row :gutter="16"> layout="horizontal"
<a-col :span="8"> :label-col="{ style: { width: '180px' } }"
:wrapper-col="{ style: { flex: '1 1 auto', maxWidth: '420px' } }"
:model="form"
:disabled="fieldsDisabled"
class="single-column-form"
>
<a-form-item label="渠道归属" required> <a-form-item label="渠道归属" required>
<ChannelSelect <ChannelSelect
v-model:model-value="channelNo" v-model:model-value="channelNo"
@ -56,8 +61,6 @@
:disabled="isDetail" :disabled="isDetail"
/> />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="客户" required> <a-form-item label="客户" required>
<a-space> <a-space>
<span v-if="form.enterpriseName">{{ form.enterpriseName }}</span> <span v-if="form.enterpriseName">{{ form.enterpriseName }}</span>
@ -67,29 +70,21 @@
</a-button> </a-button>
</a-space> </a-space>
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="证件类型" required> <a-form-item label="证件类型" required>
<a-radio-group v-model:value="form.certificateKind"> <a-radio-group v-model:value="form.certificateKind">
<a-radio value="BUSINESS_LICENSE">营业执照</a-radio> <a-radio value="BUSINESS_LICENSE">营业执照</a-radio>
<a-radio value="INDIVIDUAL_BUSINESS">个体工商户</a-radio> <a-radio value="INDIVIDUAL_BUSINESS">个体工商户</a-radio>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="营业执照/统一社会信用代码" required> <a-form-item label="营业执照/统一社会信用代码" required>
<a-space> <a-space>
<a-input :value="form.businessLicense" disabled placeholder="请输入18位营业执照编号" style="width: 220px" /> <a-input :value="form.businessLicense" disabled placeholder="请输入18位营业执照编号" style="width: 220px" />
<a-button :disabled="!channelNo || !form.enterpriseName" @click="handleQueryBeneficiary"></a-button> <a-button :disabled="!channelNo || !form.enterpriseName" @click="handleQueryBeneficiary"></a-button>
</a-space> </a-space>
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="营业执照发放日" required> <a-form-item label="营业执照发放日" required>
<a-input :value="form.certificateEffectiveDate" disabled placeholder="请选择日期" /> <a-input :value="form.certificateEffectiveDate" disabled placeholder="请选择日期" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="营业执照到期日" required> <a-form-item label="营业执照到期日" required>
<a-space> <a-space>
<a-input <a-input
@ -101,23 +96,15 @@
<a-checkbox :checked="isCertExpiryLongTerm" disabled>长期</a-checkbox> <a-checkbox :checked="isCertExpiryLongTerm" disabled>长期</a-checkbox>
</a-space> </a-space>
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="居住地省市" required> <a-form-item label="居住地省市" required>
<RegionCascader v-model:value="form.regionCode" :disabled="fieldsDisabled" @update:text="(val) => (form.regionText = val)" /> <RegionCascader v-model:value="form.regionCode" :disabled="fieldsDisabled" @update:text="(val) => (form.regionText = val)" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="经营地址" required> <a-form-item label="经营地址" required>
<a-input :value="form.certificateAddress" disabled placeholder="请输入经营地址" /> <a-input :value="form.certificateAddress" disabled placeholder="请输入经营地址" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="企业银行账号" required> <a-form-item label="企业银行账号" required>
<a-input v-model:value="form.bankCardNumber" placeholder="请输入企业银行卡号" /> <a-input v-model:value="form.bankCardNumber" placeholder="请输入企业银行卡号" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="16">
<a-form-item label="开户银行/网点" required> <a-form-item label="开户银行/网点" required>
<BankSelector <BankSelector
v-model:bank-no="form.bankNo" v-model:bank-no="form.bankNo"
@ -126,13 +113,9 @@
:default-tab="isCreate ? 'quick' : 'manual'" :default-tab="isCreate ? 'quick' : 'manual'"
/> />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="经营范围" required> <a-form-item label="经营范围" required>
<a-textarea :value="form.businessScope" disabled :rows="2" placeholder="请输入经营范围" /> <a-textarea :value="form.businessScope" disabled :rows="2" placeholder="请输入经营范围" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="行业类别" required> <a-form-item label="行业类别" required>
<DictSelect <DictSelect
v-model:model-value="form.industry" v-model:model-value="form.industry"
@ -141,14 +124,16 @@
:disabled="fieldsDisabled" :disabled="fieldsDisabled"
/> />
</a-form-item> </a-form-item>
</a-col>
</a-row>
</a-form> </a-form>
</a-card> </a-card>
<a-card title="法定代表人" style="margin-bottom: 16px"> <a-card title="法定代表人" style="margin-bottom: 16px">
<a-row :gutter="16"> <a-form
<a-col :span="8"> layout="horizontal"
:label-col="{ style: { width: '180px' } }"
:wrapper-col="{ style: { flex: '1 1 auto', maxWidth: '420px' } }"
class="single-column-form"
>
<a-form-item label="姓名" required> <a-form-item label="姓名" required>
<a-space direction="vertical" style="width: 100%"> <a-space direction="vertical" style="width: 100%">
<a-space> <a-space>
@ -161,21 +146,16 @@
<div class="hint-text">*请先在个人客户管理中维护</div> <div class="hint-text">*请先在个人客户管理中维护</div>
</a-space> </a-space>
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="身份证号" required> <a-form-item label="身份证号" required>
<a-input :value="legalRep.idNo" disabled placeholder="请输入证件号码" /> <a-input :value="legalRep.idNo" disabled placeholder="请输入证件号码" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="身份证到期日" required> <a-form-item label="身份证到期日" required>
<a-space> <a-space>
<a-input :value="isLegalRepLongTerm ? '' : legalRep.opto" disabled style="width: 200px" placeholder="请选择日期" /> <a-input :value="isLegalRepLongTerm ? '' : legalRep.opto" disabled style="width: 200px" placeholder="请选择日期" />
<a-checkbox :checked="isLegalRepLongTerm" disabled>长期</a-checkbox> <a-checkbox :checked="isLegalRepLongTerm" disabled>长期</a-checkbox>
</a-space> </a-space>
</a-form-item> </a-form-item>
</a-col> </a-form>
</a-row>
</a-card> </a-card>
<a-card style="margin-bottom: 16px"> <a-card style="margin-bottom: 16px">
@ -191,23 +171,21 @@
<span class="role-card-title">实际控制人</span> <span class="role-card-title">实际控制人</span>
<a-button v-if="!isDetail" :disabled="!channelNo" @click="openPersonPicker('controller')"></a-button> <a-button v-if="!isDetail" :disabled="!channelNo" @click="openPersonPicker('controller')"></a-button>
</div> </div>
<a-row :gutter="16"> <a-form
<a-col :span="8"> layout="horizontal"
:label-col="{ style: { width: '180px' } }"
:wrapper-col="{ style: { flex: '1 1 auto', maxWidth: '420px' } }"
class="single-column-form"
>
<a-form-item label="证件类型"> <a-form-item label="证件类型">
<span>{{ CERTIFICATE_TYPE_LABEL_MAP[controller.idType] || controller.idType || '-' }}</span> <span>{{ CERTIFICATE_TYPE_LABEL_MAP[controller.idType] || controller.idType || '-' }}</span>
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="证件号码"> <a-form-item label="证件号码">
<a-input :value="controller.idNo" disabled placeholder="请输入证件号码" /> <a-input :value="controller.idNo" disabled placeholder="请输入证件号码" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="姓名"> <a-form-item label="姓名">
<a-input :value="controller.customerName" disabled placeholder="请输入姓名" /> <a-input :value="controller.customerName" disabled placeholder="请输入姓名" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="证件失效日期"> <a-form-item label="证件失效日期">
<a-space> <a-space>
<a-input <a-input
@ -219,80 +197,69 @@
<a-checkbox :checked="isControllerLongTerm" disabled>长期</a-checkbox> <a-checkbox :checked="isControllerLongTerm" disabled>长期</a-checkbox>
</a-space> </a-space>
</a-form-item> </a-form-item>
</a-col> </a-form>
</a-row>
</a-card> </a-card>
<a-card style="margin-bottom: 16px"> <a-card style="margin-bottom: 16px">
<div class="role-card-header"> <div class="role-card-header">
<span class="role-card-title">负责人</span> <span class="role-card-title">负责人</span>
<a-button v-if="!isDetail" :disabled="!channelNo" @click="openPersonPicker('leader')"></a-button> <a-button v-if="!isDetail" :disabled="!channelNo" @click="openPersonPicker('leader')"></a-button>
</div> </div>
<a-row :gutter="16"> <a-form
<a-col :span="8"> layout="horizontal"
:label-col="{ style: { width: '180px' } }"
:wrapper-col="{ style: { flex: '1 1 auto', maxWidth: '420px' } }"
class="single-column-form"
>
<a-form-item label="证件类型"> <a-form-item label="证件类型">
<span>{{ CERTIFICATE_TYPE_LABEL_MAP[leader.idType] || leader.idType || '-' }}</span> <span>{{ CERTIFICATE_TYPE_LABEL_MAP[leader.idType] || leader.idType || '-' }}</span>
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="证件号码"> <a-form-item label="证件号码">
<a-input :value="leader.idNo" disabled placeholder="请输入证件号码" /> <a-input :value="leader.idNo" disabled placeholder="请输入证件号码" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="姓名"> <a-form-item label="姓名">
<a-input :value="leader.customerName" disabled placeholder="请输入姓名" /> <a-input :value="leader.customerName" disabled placeholder="请输入姓名" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="手机号"> <a-form-item label="手机号">
<a-input v-model:value="leader.mobile" :disabled="fieldsDisabled" placeholder="请输入手机号码" /> <a-input v-model:value="leader.mobile" :disabled="fieldsDisabled" placeholder="请输入手机号码" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="证件失效日期"> <a-form-item label="证件失效日期">
<a-space> <a-space>
<a-input :value="isLeaderLongTerm ? '' : leader.opto" disabled style="width: 160px" placeholder="请选择日期" /> <a-input :value="isLeaderLongTerm ? '' : leader.opto" disabled style="width: 160px" placeholder="请选择日期" />
<a-checkbox :checked="isLeaderLongTerm" disabled>长期</a-checkbox> <a-checkbox :checked="isLeaderLongTerm" disabled>长期</a-checkbox>
</a-space> </a-space>
</a-form-item> </a-form-item>
</a-col> </a-form>
</a-row>
</a-card> </a-card>
<a-card style="margin-bottom: 16px"> <a-card style="margin-bottom: 16px">
<div class="role-card-header"> <div class="role-card-header">
<span class="role-card-title">经办人</span> <span class="role-card-title">经办人</span>
<a-button v-if="!isDetail" :disabled="!channelNo" @click="openPersonPicker('operator')"></a-button> <a-button v-if="!isDetail" :disabled="!channelNo" @click="openPersonPicker('operator')"></a-button>
</div> </div>
<a-row :gutter="16"> <a-form
<a-col :span="8"> layout="horizontal"
:label-col="{ style: { width: '180px' } }"
:wrapper-col="{ style: { flex: '1 1 auto', maxWidth: '420px' } }"
class="single-column-form"
>
<a-form-item label="证件类型"> <a-form-item label="证件类型">
<span>{{ CERTIFICATE_TYPE_LABEL_MAP[operator.idType] || operator.idType || '-' }}</span> <span>{{ CERTIFICATE_TYPE_LABEL_MAP[operator.idType] || operator.idType || '-' }}</span>
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="证件号码"> <a-form-item label="证件号码">
<a-input :value="operator.idNo" disabled placeholder="请输入证件号码" /> <a-input :value="operator.idNo" disabled placeholder="请输入证件号码" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="姓名"> <a-form-item label="姓名">
<a-input :value="operator.customerName" disabled placeholder="请输入姓名" /> <a-input :value="operator.customerName" disabled placeholder="请输入姓名" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="手机号"> <a-form-item label="手机号">
<a-input v-model:value="operator.mobile" :disabled="fieldsDisabled" placeholder="请输入手机号码" /> <a-input v-model:value="operator.mobile" :disabled="fieldsDisabled" placeholder="请输入手机号码" />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="证件失效日期"> <a-form-item label="证件失效日期">
<a-space> <a-space>
<a-input :value="isOperatorLongTerm ? '' : operator.opto" disabled style="width: 160px" placeholder="请选择日期" /> <a-input :value="isOperatorLongTerm ? '' : operator.opto" disabled style="width: 160px" placeholder="请选择日期" />
<a-checkbox :checked="isOperatorLongTerm" disabled>长期</a-checkbox> <a-checkbox :checked="isOperatorLongTerm" disabled>长期</a-checkbox>
</a-space> </a-space>
</a-form-item> </a-form-item>
</a-col> </a-form>
</a-row>
</a-card> </a-card>
</template> </template>
@ -369,13 +336,16 @@
</a-card> </a-card>
<a-card title="手机号" style="margin-bottom: 16px"> <a-card title="手机号" style="margin-bottom: 16px">
<a-row :gutter="16"> <a-form
<a-col :span="8"> layout="horizontal"
:label-col="{ style: { width: '180px' } }"
:wrapper-col="{ style: { flex: '1 1 auto', maxWidth: '420px' } }"
class="single-column-form"
>
<a-form-item label="手机号" required> <a-form-item label="手机号" required>
<a-input v-model:value="mobileForm.mobile" :disabled="fieldsDisabled" placeholder="请输入手机号" /> <a-input v-model:value="mobileForm.mobile" :disabled="fieldsDisabled" placeholder="请输入手机号" />
</a-form-item> </a-form-item>
</a-col> </a-form>
</a-row>
</a-card> </a-card>
<WalletCustomerPicker ref="enterprisePickerRef" mode="enterprise" @select="handleSelectEnterprise" /> <WalletCustomerPicker ref="enterprisePickerRef" mode="enterprise" @select="handleSelectEnterprise" />
@ -1187,6 +1157,9 @@ function goBack() {
</script> </script>
<style scoped> <style scoped>
.single-column-form {
max-width: 680px;
}
.section-title { .section-title {
font-size: 15px; font-size: 15px;
font-weight: 600; font-weight: 600;

View File

@ -2,9 +2,9 @@
<!-- 2.6.2 个人账户开立(2026-07-22 按最新原型截图改造,2026-07-23 补充 create/edit/detail 三态): <!-- 2.6.2 个人账户开立(2026-07-22 按最新原型截图改造,2026-07-23 补充 create/edit/detail 三态):
选择个人客户回填基础资料(只读,作为身份核验基准) -> 补充职业/银行卡/手机号 -> 选择个人客户回填基础资料(只读,作为身份核验基准) -> 补充职业/银行卡/手机号 ->
上传身份证正反面(触发OCR,识别结果与所选客户信息比对,不一致弹窗阻断提交)+正面免冠照 上传身份证正反面(触发OCR,识别结果与所选客户信息比对,不一致弹窗阻断提交)+正面免冠照
(调用后端人脸识别接口 face-recognize,与所选客户的身份证号/姓名做匹配,识别通过才拿到可提交 (产品要求暂时不调用后端人脸识别接口 face-recognize 做身份匹配,改为直接调用通用文件
的文件编号) -> 勾选同意协议 -> 提交草稿 -> 展示申请编号,提供"刷新状态"手动轮询按钮(不做 上传接口 uploadCustomerFileApi,仅拿文件编号,不做任何比对/校验) -> 勾选同意协议 ->
自动定时轮询) 提交草稿 -> 展示申请编号,提供"刷新状态"手动轮询按钮(不做自动定时轮询)
选择客户改用与"客户管理 > 个人客户管理"页面一致的处理方式:先分页查询 /api/customer-info/personal/list 选择客户改用与"客户管理 > 个人客户管理"页面一致的处理方式:先分页查询 /api/customer-info/personal/list
(PersonalCustomerPicker.vue),选中后再调用 /api/customer-info/personal/detail 取完整字段回填, (PersonalCustomerPicker.vue),选中后再调用 /api/customer-info/personal/detail 取完整字段回填,
不使用本模块专属但字段未必对齐的 personal-opening/query-customer 接口 不使用本模块专属但字段未必对齐的 personal-opening/query-customer 接口
@ -13,8 +13,7 @@
复刻阶段3 PersonalForm/EnterpriseForm 的Bugedit/detail 模式下的 list/detail/update 接口 复刻阶段3 PersonalForm/EnterpriseForm 的Bugedit/detail 模式下的 list/detail/update 接口
后端尚未提供,前端已按 enterprise-open-application 四件套的假设契约实现,detail/edit 模式 后端尚未提供,前端已按 enterprise-open-application 四件套的假设契约实现,detail/edit 模式
下无法真正联调验证,详见缺失的后端接口.mdPart 1 §1.10 Part 2 新增条目 下无法真正联调验证,详见缺失的后端接口.mdPart 1 §1.10 Part 2 新增条目
已知缺口(详见缺失的后端接口.md):人脸识别接口 recode/recodeInfo 取值枚举未文档化 已知缺口(详见缺失的后端接口.md):协议正文/URL无真实内容编辑/查看态因缺少
(前端仅按"是否返回 fileNo"判断识别是否通过)协议正文/URL无真实内容编辑/查看态因缺少
"按 fileNo 回显图片"的接口,无法重新展示已上传的证件/人脸照片缩略图(与企业开户表单 "按 fileNo 回显图片"的接口,无法重新展示已上传的证件/人脸照片缩略图(与企业开户表单
LicenseUpload/IdCardUpload 在编辑态的现状一致,非本次新增的独立缺口) LicenseUpload/IdCardUpload 在编辑态的现状一致,非本次新增的独立缺口)
2026-08-06 更新:"职业"字段已改为从后端字典接口(dictTypeCode='job',三级级联)取数据, 2026-08-06 更新:"职业"字段已改为从后端字典接口(dictTypeCode='job',三级级联)取数据,
@ -570,7 +569,7 @@ async function handleSelectCustomer(record) {
form.jobNote = '' form.jobNote = ''
// /,, OCR // /,, OCR
runIdentityComparison() runIdentityComparison()
// (fileNo)"" idNo/name ,, // (fileNo)"",,
// /(FacePhotoUpload :key="form.customerId" ) // /(FacePhotoUpload :key="form.customerId" )
facePhotoFileNo.value = '' facePhotoFileNo.value = ''
} }

View File

@ -2008,6 +2008,117 @@ INSERT INTO "SFT"."AUTH__ROLE_PERMISSION" ("ID", "ROLE_ID", "PERMISSION_ID") VAL
157. **法人代表身份证(人像面/国徽面)提交前校验为必填**:对齐截图"*法定代表人身份证"的必填星号 157. **法人代表身份证(人像面/国徽面)提交前校验为必填**:对齐截图"*法定代表人身份证"的必填星号
标记,`handleSubmit` 增加前置校验,未上传时阻断提交并提示,`loanApplicationImageBtoList` 标记,`handleSubmit` 增加前置校验,未上传时阻断提交并提示,`loanApplicationImageBtoList`
本身对单个 `fileType` 是否必填无 swagger 声明,该校验规则纯前端本地实现。 本身对单个 `fileType` 是否必填无 swagger 声明,该校验规则纯前端本地实现。
158. **2026-08-12 补充:"授信基础信息"卡片去掉"机构"/"手机号码"/"授信种类"三个字段,"授信品种"
改回"贷款投向"并变为动态字典驱动**:产品要求精简该卡片,具体变更:
- 去掉"机构"字段(`organizationId`,`OrgTreeSelect` 选择器),`CreateEnterpriseLoanDraftBto`/
`UpdateEnterpriseLoanApplicationBto` 均有该属性,只是本页不再提供录入入口,历史数据不受
影响,与第146条同类处理。
- 去掉"手机号码"字段(`phoneNo`),即撤销第147条本次新增的输入框(不涉及验证码交互,单纯移除
该字段的 UI 与提交)。
- 去掉"授信种类"字段(`creditCategory`),第148条登记的"无对应后端属性"缺口随之撤销,不再
提交该字段。
- 原"授信品种"字段(第149条 ⚠️映射)改回展示为"贷款投向",控件由 `a-input` 改为
`DictSelect`(`dict-type-code="industry_type"`,与第24条"所属行业"用法一致),提交值仍是
`form.loanPurpose`(叶子字典项的 `itemValue` 字符串),不新增后端字段,`industry_type`
字典类型已在第24条登记过,沿用即可。
159. **2026-08-12 补充(二):"法定代表人信息"卡片按更精确的截图重新核对,第151条对该卡片"更多信息"
区约20个字段的识别整体有误(非仅第149条待review提到的"住处邮编"一处),现以本条为最终依据**,
详见 `docs/superpowers/specs/2026-08-10-enterprise-loan-application-form-redesign.md`
"2026-08-12 更新"小节完整字段映射表。要点:
- 主区新增 🆕 `legalPersonResidenceCity`(居住城市,复用 `AdministrativeDivisionCascader.vue`
级联组件),`legalPersonResidence` 改为仅承载"详细居住地址"(不再含城市),
`legalPersonMobile`/`legalPersonGender`/`legalPersonBirthday`/`legalPersonCertExpiry`/
`legalPersonEducation`/`legalPersonMarital`/`legalPersonResidenceCity`/`legalPersonResidence`
补充为必填(对齐截图星号标记)。
- 更多信息区新增 🆕 字段:`legalPersonCustomerType`(个人客户分类,`DictSelect`
`dict-type-code="gerenkehufenlei"`,⚠️ 第151条误读为"个人户籍分类",已按用户更正为动态字典
驱动的"个人客户分类")、`legalPersonMarketOperator`(个人是否市场经营户,Y/N;⚠️ 此前误写为
"个人是否本地经营户",已按用户更正)、
`legalPersonBusinessLicenseNo`(经营企业营业执照号码)、`legalPersonBusinessType`(经营种类)、
`legalPersonBusinessPhone`(住宅/经营地址电话)、`legalPersonResidenceStatus`(居住状况,复用
已有枚举 `RESIDENCE_STATUS_OPTIONS`)、`legalPersonFamilyMonthlyIncome`(家庭月收入)、
`legalPersonEmploymentDate`(从业时间)、`legalPersonFirstLoanDate`(首贷时间;⚠️ 用户明确更正
为"首贷时间"而非"启户时间")。
- 更多信息区复用现有字段但改 label:`legalPersonBusinessCompanyName`("经营公司名称"→"经营企业
名称")、`legalPersonBankType`("开户行别"→"个人基本账户行")、`legalPersonBasicAccountNo`
("个人基本户账号"→"基本账户号");`legalPersonMailAddress`(通讯地址)从主区移到更多信息区;
`legalPersonAnnualIncome`(个人年收入)此前已存在于 `form`/提交 payload 但模板从未渲染,本次
补上表单项。
- 第151条登记的以下字段截图未出现,本次连同 UI/`form`状态/提交 payload 一并删除(不再提交,
第151条对应文字仅作历史记录,实际以本条为准):`legalPersonAccountType`(个人开户户名,
`LEGAL_PERSON_ACCOUNT_TYPE_OPTIONS` 枚举常量本身保留不删)、`legalPersonHasSocialInsurance`
(个人是否有社保医保)、`legalPersonBusinessNature`(经营公司营业性质用途)、
`legalPersonBusinessZipCode`(经营邮编)、`legalPersonResidenceZipCode`(住处邮编,即原
"待review第2项",现确认为已删除字段而非"读取不准确")、`legalPersonCreditBusiness`(资信业务)、
`emergencyContactName`/`emergencyContactPhone`(紧急联系人姓名/电话)、
`legalPersonBasicAccountOpened`(个人基本户开户)、`legalPersonBasicAccountName`(个人基本户
户名)、`legalPersonStockCode`(股票代码)。
- 以上全部 🆕 字段仍是 `CreateEnterpriseLoanDraftBto`/`UpdateEnterpriseLoanApplicationBto`
完全没有对应属性,随表单一起提交,风险与第151条一致,需后端评估。
160. **2026-08-12 补充(三):"企业信息"卡片按更精确的截图重新核对,第151条对该卡片"更多信息"区
的识别整体有误,现以本条为最终依据**,详见
`docs/superpowers/specs/2026-08-10-enterprise-loan-application-form-redesign.md`
"2026-08-12 更新(二)"小节完整字段映射表。要点:
- **两个此前误读、实为 swagger 已有真实字段**(非新缺口):`isMarketOperator`(标题"是否市场
经营户",此前误读为"是否本地股票参与经营" `isLocalStockParticipated`,旧字段已删除)、
`enterpriseEconomyType`(标题"国民经济部门",此前完全未识别到该字段,现已启用)。
- **7个下拉框改为动态字典驱动**(用户直接提供字典编码,非前端猜测):`enterpriseOrgType`
(经济类型,`DictSelect dict-type-code="jinjileixing"`)、`enterpriseEconomyType`(国民
经济部门,`guominjingjibumen`)、`enterpriseType`(企业类型,`qiyeleixing`,与
`PersonalLoanForm.vue` 同名字段同一字典)、`enterpriseAffiliation`(隶属,`lishu`)、
`listedStatus`(上市情况,`shangshiqingkuang`)、`financialStatementType`(财务报表类型,
`caiwubaobiaoleixing`)、`enterpriseRiskType`(企业环境、安全等重大风险企业,
`anquanfengxian`)。若对应 `dict-type-code` 尚未在"基础配置 > 动态字典管理"创建,下拉会
加载为空,需业务侧提前配置字典数据。
- **注册资本/实收资本币种改为下拉**:`registeredCapitalCurrency`/`paidInCapitalCurrency`
由自由文本输入框改为 `a-select`,复用项目已有 `CURRENCY_OPTIONS`(`src/constants/
enterpriseEnums.js`:CNY/USD/EUR/HKD/OTHER),与最新 swagger `CurrencyEnum` 一致,非新缺口。
- **恢复"是否政府融资平台"字段**:`isGovernmentPlatform`(Y/N,必填),2026-08-10 改造第146条
曾因旧截图未出现而移除录入入口,本次新截图重新出现该字段(必填),经用户确认恢复。
- **新增 🆕 字段(`CreateEnterpriseLoanDraftBto`/`UpdateEnterpriseLoanApplicationBto` 均无
对应属性,随表单一起提交)**:主区 `businessLicenseEffectiveDate`(证件生效日期)、
`enterpriseEstablishDate`(企业成立日期)、`enterpriseRegisteredRegion`(所属地区,
`AdministrativeDivisionCascader.vue`)、`enterpriseCreditReferenceCode`(企业中征码,原
贷款卡卡号)、`loanCardPassword`(贷款卡密码)、`isLoanNumberValid`(企业贷款号是否有效,
Y/N)、`paidInCapitalCurrency`(实收资本币种)、`enterpriseContactPhone`(企业联系电话)、
`enterpriseFinanceContactPhone`(企业财务部联系电话);更多信息区
`enterpriseOfficeRegion`(省直辖市/企业国内办公行政区划,级联)、
`enterpriseOfficeAddressUpdateDate`(办公地址更新日期)、`enterpriseAffiliation`(隶属)、
`listedStatus`(上市情况)、`enterpriseZipCode`(企业邮政编码)、`enterpriseOtherBankName`/
`enterpriseOtherBankAccountNo`(企业他行开户行/开户账号)、`firstAccountOpenDate`(企业在
我行首次开立账户时间)、`enterpriseFirstLoanDate`(企业首贷时间)、`creditRelationshipDate`
(企业与我行建立信贷关系时间)、`financialStatementType`(财务报表类型)、
`taxRefundRegistrationCert`(企业退税登记证)、`hasImportExportRight`(有无进出口经营权,
Y/N)、`isKeySmeEnterprise`(是否培养重点中小企业,Y/N,必填)、`isGroupCustomer`(是否集团
客户,Y/N)、`governmentPlatformCategory`(政府融资平台分类(银监),枚举见
`GOVERNMENT_PLATFORM_CATEGORY_OPTIONS`:`INSPECTION`检测/`SUPERVISION`监管,⚠️ 文案疑似
截图 OCR 限制,经用户确认按原文处理)、`enterpriseRiskType`(风险类型)、
`enterpriseFaxPhone`(企业传真电话)、`enterpriseHistoryManagementIntro`(企业客户历史
沿革、管理水平简介)、`businessPremisesArea`(企业经营场地面积,单位 m²,⚠️ 截图字面显示
"m3",经用户确认为上标渲染问题按 m² 处理)、`businessPremisesOwnership`(企业经营场地
所有权,新增独立枚举 `BUSINESS_PREMISES_OWNERSHIP_OPTIONS`:自有/租赁/其他)、
`parentCompanyOrgCode`(企业上级公司组织机构代码,选填)、`parentCompanyLoanCardNo`
(企业上级公司贷款卡编号,选填)。
- **删除约30个截图未出现的旧字段**(不再展示/提交,不是仅隐藏):主区
`enterpriseEconomicOwnership`(隶属经济产权)、`enterpriseEconomicZone`(企业经济区域)、
`enterpriseMailAddress`(通讯地址)、`enterpriseCode`(企业代码)、`withdrawalTypeFlag`
(提款种类标识)、`isLocalStockParticipated`(是否本地股票参与经营,已被 `isMarketOperator`
承接)、`enterpriseLocationAddress`(企业所在地地址)、`enterpriseLocationZipCode`(企业
所在地邮编);更多信息区 `enterprisePrimaryShareholder`(首要股东占公司比例)、
`hasStorefront`(是否有商铺)、`enterprisePerformance`(业绩)、`enterpriseMarketingRegion`
(企业营销所在区域)、`hasExternalGuarantee`(有无对外担保情况)、`isSmeEnterprise`(是否
属于中小企业)、`isLocalEcological`(是否属地生态化)、`isEquitySystem`(是否属于股权制)、
`highTechIndustryFlag`(高新技术产业标识,连带删除枚举 `HIGH_TECH_INDUSTRY_FLAG_OPTIONS`)、
`companyCodeAndAffiliates`(公司代号及关系企业)、`enterpriseAccountOpeningMethod`(企业
开户方式及运营开户)、`enterpriseEquityOtherType`(企业股权其他情况,连带删除枚举
`ENTERPRISE_EQUITY_OTHER_TYPE_OPTIONS`)、`enterpriseBusinessAddressScope`(企业经营地址
范围)、`parentCompanyAddress`(企业上级公司所在地址)、`parentCompanyShareRatio`(企业
上级公司持股比例)、`enterpriseBusinessWebsite`(企业经营网址)、
`enterpriseRemitAccountName`/`enterpriseRemitAccountNo`/`enterpriseRemitBankBranch`
(企业银行汇账户名/账号/开户行网点,用户确认不复用,新截图对应字段"企业他行开户行/账号"
已作为新字段单独新增)、`enterpriseOtherBankCreditRelation`(企业与他行建立信贷关系情况,
用户确认不复用)、`financialStatementNo`(财务报表编号,用户确认不复用)、`listedStockCode`
(上市代码)、`enterpriseOfficeZipCode`(办公地址邮编,语义由新字段"企业邮政编码"承接)。
## Part 26:贷款申请移动端H5信息核对页(个人/企业) —— 发码/人脸接口字段缺口 —— 2026-08-11 补充 ## Part 26:贷款申请移动端H5信息核对页(个人/企业) —— 发码/人脸接口字段缺口 —— 2026-08-11 补充