SFT/AGENTS.md

114 lines
13 KiB
Markdown

# AGENTS.md
**本仓库包含两个独立的 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 # 构建
npm run preview # 预览构建产物
npm run mock # 启动本地 mock 后端(express),默认端口 8888,可用 MOCK_PORT 覆盖
```
`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`。
## 后端接口对接(最重要,容易出错)
- **权威接口契约在《缺失的后端接口.md》,不是 mock-server**。改/加接口调用前必须先读这个文件的 Part 1(已对接契约速查)。核心通用约定:
- 响应体统一 `{ code, message, data }`,业务成功判定为 `code === 200`;非 200 时用 `message` 提示,不做具体错误码分支。
- 除登录/刷新令牌/发短信外,所有请求必须带 `Authorization: Bearer {token}`
- 日期时间字段统一 `yyyy-MM-dd HH:mm:ss`
- 分页接口 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_*.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(**字段名大小写固定为下列形式**,首字母大写、其余小写):
- `appNo`:随机值
- `channelNo`:渠道 ID,来自页面上用户选择的渠道(非固定值,不能硬编码/写死)
- `serialNo`:流水号,随机值
- `transDate`:`yyyy-MM-dd` 格式日期
- `transTradeTime`:`yyyy-MM-dd HH:mm:ss` 格式时间
- `channelNo` 如果页面没有渠道信息,则不需要传值
- `src/utils/traceHeaders.js``buildTraceHeaders(channelNo)` 已正确实现上述 5 个字段(含固定大小写、`channelNo` 按需可选),**不需要再改造**;`fryl_h5/src/utils/traceHeaders.js` 是同一实现的独立拷贝(H5 端匿名接口,永远不传 `Channelno`)。
- 枚举值必须来自 swagger/《缺失的后端接口.md》,已整理在 `src/constants/*Enums.js`,不要编造新枚举。
## 路由架构(新增页面容易漏配置)
路由**不是**静态声明的,而是登录后按后端返回的菜单权限树动态生成(`src/router/dynamic.js` 的 `installDynamicRoutes`)。新增一个菜单页面必须同时做两件事,少一件都会报错或 404:
1.`src/views/**` 下新建组件。
2.`src/router/componentRegistry.js``routePathComponentMap` 里把后端菜单 `path` 手工映射到组件相对路径。
- 新增/编辑/详情等**不在侧边栏菜单**里的表单页,注册到同文件的 `extraChildRoutes`(而不是 `routePathComponentMap`)。
3. 权限字典由权限管理页面动态维护,但组件映射永远是前端手工登记,后端加菜单不会自动出现页面。
鉴权流程(`src/router/index.js` + `src/stores/auth.js`):`accessToken` 存 localStorage;刷新页面后靠 `fetchCurrentUser`/`rebuildPermissions` 重建菜单树再挂路由;401 由 `src/api/request.js` 响应拦截器用 httpOnly cookie 里的 `refresh_token` 静默刷新(请求本身不带 Authorization 头),刷新失败清空登录态跳 `/login`
## 项目范围(不要擅自扩展)
- `docs/superpowers/specs/2026-07-08-fryl-frontend-overall-design.md` 是范围与阶段划分的权威文档,明确列出了 9 个一级模块的实施范围,以及**明确排除**的模块(非银专区/分账专区/税筹专区/农都专区/驾驶舱/首页仪表盘等,及各模块下需求文档未提及的原型子页面)。
- `website_detail/` 是从已上线的稠州银行商业平台**完整抓取的原型参考**(84 个页面,每页含截图 png + html + summary.md),覆盖面远大于本项目实际需要实现的功能,**不能当作“待实现清单”**,只用于查阅已排除模块之外的页面交互细节。
- `docs/superpowers/specs/``docs/superpowers/plans/` 保存了各阶段已确认的设计文档与实施计划(按 `YYYY-MM-DD-主题.md` 命名),开工前先查有没有相关阶段的既有设计,避免重新决策已定的架构/字段方案。
- 机构数据隔离(下拉框可选范围裁剪等)由后端按登录用户身份限定,前端不做二次裁剪,详见 `src/utils/orgScope.js` 注释与 `docs/superpowers/specs/2026-07-16-org-data-isolation-design.md`
## 目录速览
- `src/api/*.js`:按后端模块拆分的请求封装,`src/api/request.js` 是唯一的 axios 实例(含鉴权/刷新/trace header/错误提示拦截器)。
- `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` 为标准实现参照,新增或修改任何图片上传组件都要照此模式实现(`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`
- **预览**:图标加 `@click.stop`,点击打开一个 `a-modal`(`:footer="null"`)放大展示图片,不要用浏览器默认行为。
- **删除**:图标加 `@click.stop`,清空本地预览态并 `emit('update:fileNo', '')`,让父组件感知已清空。
- **重新上传**:图标**不加** `@click.stop`,让点击事件自然冒泡到外层 `a-upload` 容器,复用其原生文件选择流程触发重新选图(不要单独写一套"重新上传"逻辑)。
- 组件需支持 `readonly` prop(默认 `false`):为 `true` 时只保留预览图标,隐藏"重新上传/删除"——用于查看详情等只读表单场景。调用处如果页面本身有 `isDetail` 之类的只读态标记,必须把它传给上传组件的 `readonly`,不要漏传(否则只读页面会出现可用的删除按钮)。
- 当前项目**没有抽出通用的图片上传基础组件**,`IdCardUpload.vue`/`LicenseUpload.vue`/`FacePhotoUpload.vue` 是各自独立实现(结构相似但不复用)。新增同类组件时照抄这三个的模式,不要指望后续会抽公共组件。
## 耗时操作统一等待态规范
图片/文件上传、点击"提交"/"创建"/"保存"等触发接口调用的按钮,以及其他明显耗时的异步操作,**必须**统一展示等待态,期间禁止用户对页面做其他操作,不允许无反馈地"卡住"或允许重复点击。以 `src/views/customer/enterprise/EnterpriseForm.vue`(及 `src/views/customer/personal/PersonalForm.vue`)已有实现为标准参照:
- 页面级遮罩:最外层 `a-card`/表单内容用 `<a-spin :spinning="busy" :tip="busyTip">` 包裹,转菊花+可选提示文案,遮罩期间用户无法操作表单其他元素。
- `busy` 用一个 `computed` 统一收敛所有"进行中"状态(如 `submitting.value || xxxUploading.value`,多个耗时状态用 `||` 合并),不要每个异步操作各自维护一套互不关联的 loading 变量却不接入统一遮罩。
- `busyTip` 按当前处于哪个耗时状态给出对应提示文案(如"营业执照上传中,请稍候..."/"提交中,请稍候..."),没有耗时状态时留空。
- 触发耗时操作的按钮本身也要加 `:loading="submitting"`(或对应状态),双重防止重复点击。
- 图片上传组件(见上一节规范)通过 `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 必须保留界面可配置入口(已实现,见上文),不要改成硬编码。
- 不写测试用例、不写 CHANGELOG,不新建项目子目录当根目录。