增加个人/企业贷款申请

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
凡荣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
npm run dev # 启动开发服务器,端口 5173
npm run build # 构建
@ -11,7 +16,15 @@ npm run preview # 预览构建产物
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`
- 绝大多数模块**没有批量删除接口**,批量操作需用 `src/utils/loopDelete.js` 循环调单条删除并汇总结果(配合 `batchDeleteResult.js` 展示成功/失败)。
- 文件(身份证/营业执照等)以 **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`)。
- 本地联调真实后端遇 CORS 时,用 `.env.development.local`(已 gitignore)配 `DEV_API_PROXY_TARGET`,走 `vite.config.js` 里的同源代理方案;别改生产构建逻辑去“解决” CORS。
- 每次请求必须附带以下 5 个 header(**字段名大小写固定为下列形式**,首字母大写、其余小写):
@ -31,8 +44,8 @@ npm run mock # 启动本地 mock 后端(express),默认端口 8888,可用 M
- `serialNo`:流水号,随机值
- `transDate`:`yyyy-MM-dd` 格式日期
- `transTradeTime`:`yyyy-MM-dd HH:mm:ss` 格式时间
- ⚠️ 现状:`src/utils/traceHeaders.js` 的 `buildTraceHeaders()` 目前只生成 `serialNo`/`transDate`/`transTradeTime` 三个小写驼峰字段,**缺少 `Appno`/`Channelno`**,尚未按最新要求改造;涉及请求 header 的改动前务必先核对/修复这里,不要照抄旧的三字段实现。
- `channelNo` 如果页面没有渠道信息,则不需要传值
- `src/utils/traceHeaders.js``buildTraceHeaders(channelNo)` 已正确实现上述 5 个字段(含固定大小写、`channelNo` 按需可选),**不需要再改造**;`fryl_h5/src/utils/traceHeaders.js` 是同一实现的独立拷贝(H5 端匿名接口,永远不传 `Channelno`)。
- 枚举值必须来自 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/components/ProTable.vue`:通用查询表单+分页表格封装,列表页优先复用它而不是重写查询/分页逻辑。
- `src/components/ProTable.vue`:通用查询表单+分页表格封装,列表页优先复用它而不是重写查询/分页逻辑。传给它的 `fetch-data` 函数必须返回 `{ list, total }`(**注意**:后端分页字段是 `records`,列表页自己的 `loadList`/`fetchData` 函数里要做 `records → list` 的映射,不要指望 `ProTable` 或 api 封装层自动转换)。
- `src/constants/*Enums.js`:各模块枚举字典(状态/类型等),来源必须是 swagger。
- `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`)。
- 遮罩内横向排列三个图标(`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`,让点击事件自然冒泡到外层 `a-upload` 容器,复用其原生文件选择流程触发重新选图(不要单独写一套"重新上传"逻辑)。
- 组件需支持 `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` 状态、阻止在文件尚未上传完成时提交表单。
- 新增页面/表单时按此模式补齐,不需要抽公共 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 必须保留界面可配置入口(已实现,见上文),不要改成硬编码。

View File

@ -45,7 +45,7 @@ router.post('/loan-application/detail', (req, res) => {
// ---------------- 个人贷款申请 ----------------
router.post('/personal-loan-application/create', (req, res) => {
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.goodsCategory) 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) => {
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.enterpriseName) 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};
// 响应 {fileNo(识别通过后可直接用于个人开户提交的人脸照片文件编号)/recode/recodeInfo(识别结果码/说明,
// 具体取值枚举后端未文档化,前端仅按"fileNo 是否返回"判断识别是否成功,recodeInfo 仅作提示展示)}
// 产品要求暂时不调用此接口(个人开户/个人贷款申请/企业贷款申请的"正面免冠照"上传项改为直接调用
// 上面的 uploadCustomerFileApi 只拿 fileNo),此导出暂无调用方,保留以便后续恢复识别校验。
export const faceRecognizeApi = (data, channelNo) =>
request.post('/api/customer-info/personal/face-recognize', data, { channelNo })

View File

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

View File

@ -166,18 +166,20 @@ export const LEGAL_PERSON_ACCOUNT_TYPE_OPTIONS = [
{ value: "NON_LOCAL_CUSTOMER", label: "异地客户" }
]
// 企业贷款申请企业信息"高新技术产业标识"下拉选项:原型截图给出"已获取/正申请"两项,
// 企业贷款申请企业信息"企业经营场地所有权"单选选项:原型截图给出"自有/租赁/其他"三项,
// CreateEnterpriseLoanDraftBto 无对应字段(已知缺口,登记于《缺失的后端接口.md》Part 25)。
export const HIGH_TECH_INDUSTRY_FLAG_OPTIONS = [
{ value: "OBTAINED", label: "已获取" },
{ value: "APPLYING", label: "正申请" }
]
// 企业贷款申请企业信息"企业股权其他情况"下拉选项:原型截图给出"自有/租用/其他"三项,
// CreateEnterpriseLoanDraftBto 无对应字段(已知缺口,登记于《缺失的后端接口.md》Part 25)。
export const ENTERPRISE_EQUITY_OTHER_TYPE_OPTIONS = [
// 2026-08-12 补充:此前同名值曾误挂在已删除的"企业股权其他情况"字段上,现更正归属到本字段。
export const BUSINESS_PREMISES_OWNERSHIP_OPTIONS = [
{ value: "OWNED", label: "自有" },
{ value: "LEASED", label: "租" },
{ value: "LEASED", 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"所属机构"利用已有的
`fetchOrgListAllApi` organizationId -> 机构名称映射(与个人/企业客户列表页同惯例)
- "申请编号"(`loanApplicationId`)非原型截图列,是后续按需补充:该字段本身已随列表接口
返回(详情页跳转/编辑跳转已在用),用于柜员定位申请比对H5扫码链接里的 loanApplicationId,
展示位置/宽度沿用 PersonalOpenList.vue"申请编号"列的既有惯例(序号后纯文本220px)
- 勾选框列按截图新增(ProTable 默认支持 rowSelection),原型页面本身也未展示任何批量操作
按钮,故本次仅还原勾选交互,未绑定批量业务操作
- "申请金额"格式化为"¥x,xxx.xx万"(原始金额单位为元),"申请利率"补充百分号,"类型"列展示
@ -122,6 +125,7 @@ const statusMap = {
const columns = [
{ title: 'No', dataIndex: 'index', width: 60 },
{ title: '申请编号', dataIndex: 'loanApplicationId', width: 220 },
{ title: '客户', dataIndex: 'customerName' },
{ title: '钱包', dataIndex: 'walletAccount' },
{ title: '经营实体', dataIndex: 'businessEntity' },

View File

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

View File

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

View File

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

View File

@ -2008,6 +2008,117 @@ INSERT INTO "SFT"."AUTH__ROLE_PERMISSION" ("ID", "ROLE_ID", "PERMISSION_ID") VAL
157. **法人代表身份证(人像面/国徽面)提交前校验为必填**:对齐截图"*法定代表人身份证"的必填星号
标记,`handleSubmit` 增加前置校验,未上传时阻断提交并提示,`loanApplicationImageBtoList`
本身对单个 `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 补充