SFT/AGENTS.md

13 KiB

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 自己没有这些文档)。改动前先确认改的是哪一个工程,不要把两者的依赖/组件/约定搞混。

常用命令

根目录(管理后台):

npm run dev       # 启动开发服务器,端口 5173
npm run build     # 构建
npm run preview   # 预览构建产物
npm run mock      # 启动本地 mock 后端(express),默认端口 8888,可用 MOCK_PORT 覆盖

fryl_h5/(H5,命令要在该目录下跑):

npm run dev       # 端口 5174(特意与根项目 5173 区分,可同时开两个工程调试)
npm run build
npm run preview

fryl_h5 没有自己的 mock 命令,本地联调依赖根目录的 npm run mock(默认端口 8888,fryl_h5/.env.developmentVITE_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.jsgetBaseUrl/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.jsbuildTraceHeaders(channelNo) 已正确实现上述 5 个字段(含固定大小写、channelNo 按需可选),不需要再改造;fryl_h5/src/utils/traceHeaders.js 是同一实现的独立拷贝(H5 端匿名接口,永远不传 Channelno)。
  • 枚举值必须来自 swagger/《缺失的后端接口.md》,已整理在 src/constants/*Enums.js,不要编造新枚举。

路由架构(新增页面容易漏配置)

路由不是静态声明的,而是登录后按后端返回的菜单权限树动态生成(src/router/dynamic.jsinstallDynamicRoutes)。新增一个菜单页面必须同时做两件事,少一件都会报错或 404:

  1. src/views/** 下新建组件。
  2. src/router/componentRegistry.jsroutePathComponentMap 里把后端菜单 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,:hoveropacity: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_h5src/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.md2026-08-11-loan-application-mobile-confirm-design.md),改动前先查。
  • 企业开户 H5 的提交接口本地 mock-server 未实现,只能连真实后端联调验证;涉及企业开户 H5 时先看《缺失的后端接口.md》里标注"前端假设"的段落,不要假定 mock 能跑通。

其他约定

  • BASE_URL 必须保留界面可配置入口(已实现,见上文),不要改成硬编码。
  • 不写测试用例、不写 CHANGELOG,不新建项目子目录当根目录。