新增 docs/41-客服Agent前端开发约束_v1.md(720 行 / 68 个标题 / 13 节 + 3 附录): - §2 技术栈:原生 JS + CSS + ECharts;禁 React/Vue/TS/Tailwind(与底座现有 app/static/index.html 风格一致) - §3 目录结构:portal/<role>/<page>/ 三层命名空间 - §7 接口调用:api-client.js 封装;trace_id + JWT 自动注入;4xx 不重试 - §8 权限 RBAC:9060-9065(§T 段)映射可见性;前端不发明角色 - §11 看板专属:模块归属 portal/customer/dashboard/;4 模块按 T001 字段拆;60s 缓存 + 仅手动刷新;3 状态必齐 - §13 验收清单:通用 10 项 + 看板 9 项 + 自动化 3 项 本文件与 docs/40-前端验收清单.md 互补(约束 ≠ 验收),编号 41 不撞号
721 lines
32 KiB
Markdown
721 lines
32 KiB
Markdown
# 客服 Agent 前端开发约束
|
||
|
||
版本:v1.0
|
||
适用分支:`qyqy_develop`(含我方 §T 用户自助段已上推的 9 端点)
|
||
权威性:**团队强制规范**——所有客服 Agent 前端仓库(含本期看板、未来客户工作台、转人工会话、客户画像确认页)必须遵循本文件
|
||
关联规范:`docs/05-接口文档.md`(接口权威)、`docs/40-前端验收清单.md`(qyqy 验收清单)、`docs/客服Agent一期_合规红队与业务评测集_v1.md`(合规红线)
|
||
|
||
> 一句话:**任何"功能正确但违背本约束"的前端 PR 必须在评审阶段退回。** 本约束的检查清单见 §10。
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
1. [适用范围与术语](#1-适用范围与术语)
|
||
2. [技术栈](#2-技术栈)
|
||
3. [目录结构](#3-目录结构)
|
||
4. [命名规范](#4-命名规范)
|
||
5. [组件拆分原则](#5-组件拆分原则)
|
||
6. [状态管理](#6-状态管理)
|
||
7. [接口调用规范](#7-接口调用规范)
|
||
8. [权限与角色控制](#8-权限与角色控制)
|
||
9. [错误处理与日志埋点](#9-错误处理与日志埋点)
|
||
10. [样式与 UI 规范](#10-样式与-ui-规范)
|
||
11. [看板模块专属约束](#11-看板模块专属约束)
|
||
12. [与主客服 Agent 页面的交互跳转](#12-与主客服-agent-页面的交互跳转)
|
||
13. [验收检查清单](#13-验收检查清单)
|
||
|
||
---
|
||
|
||
## 1. 适用范围与术语
|
||
|
||
| 术语 | 含义 |
|
||
|---|---|
|
||
| **客服 Agent** | 系统中的"奶龙"智能客服;服务层为 `app/service/agent/implementations/customer_service.py` |
|
||
| **门户(Portal)** | 五端 portal:访客 / 已登录用户 / 交易系统 / 员工专属(风控)/ 员工专属(投顾) |
|
||
| **看板(Dashboard)** | 本期 §T 段新增的"我的账户"页面,端点 `T001 GET /api/v1/users/me/account/dashboard` |
|
||
| **门户身份(sub)** | JWT 的 `sub` 字段,访客为纯数字(≤19位),用户为 `customer_id`(int)字符串 |
|
||
| **envelope** | 所有接口统一响应壳:`{data, meta}`(见 `docs/05-接口文档.md` §3.3) |
|
||
| **trace_id** | 每次请求生成的 UUID v4,前后端全链路打通 |
|
||
| **Wolin OS** | 沃林学生系统的前端 v6(13 个页面),与本约束**不共享代码**——本约束仅适用于客服 Agent 前端 |
|
||
|
||
**不适用**于:
|
||
- Wolin OS(已独立维护,有自己的风格)
|
||
- 内部运营监控台(`docs/30+` 系列描述)
|
||
- 投顾/风控工作台(虽共用底座,但前端结构另起)
|
||
|
||
---
|
||
|
||
## 2. 技术栈
|
||
|
||
### 2.1 强制采用(不可替换)
|
||
|
||
| 层 | 选型 | 版本约束 | 备注 |
|
||
|---|---|---|---|
|
||
| **框架** | 原生 JavaScript(ES2022+) | 无构建依赖 | 与底座 `app/static/index.html` 现有 Chat 联调页一致,避免引入 Node/Vite/Webpack |
|
||
| **样式** | 原生 CSS + CSS 变量 | 无 Tailwind / 无 CSS-in-JS | 看板的卡片/图表使用 CSS Grid + Flexbox |
|
||
| **图表** | ECharts 5.x(按需引入) | ≥5.4 | 仅看板与允许引入,其他页面用纯 CSS 图形 |
|
||
| **HTTP** | `fetch` 封装(见 §7.3) | 不引入 axios / ky | 与底座同源,减少 bundle |
|
||
| **运行时** | 浏览器 ≥Chrome 110 / Edge 110 / Safari 16 | 不支持 IE | 公司统一桌面环境标准 |
|
||
|
||
### 2.2 禁止使用
|
||
|
||
- ❌ React / Vue / Svelte(团队暂无相关人力储备,与底座解耦成本不划算)
|
||
- ❌ TypeScript(首版不强约束;如要引入必须先经客服组评审)
|
||
- ❌ 任何 UI 组件库(Element / Ant Design / MUI 等)——会与品牌色系冲突
|
||
- ❌ jQuery / Lodash(现代 API 已覆盖)
|
||
- ❌ 任何 npm 全量安装——必须按需 <script> 引入
|
||
|
||
### 2.3 必须保留的底座兼容
|
||
|
||
- 与底座同源部署(同一域名),不引入跨域
|
||
- 一切静态资源**走 `/static/portal/<page>/...`** 路径,不绕开底座
|
||
- SSE / WebSocket 由底座托管(`docs/05 §6.4`),前端**不要自行实现长连接**
|
||
|
||
---
|
||
|
||
## 3. 目录结构
|
||
|
||
```
|
||
static/
|
||
└── portal/
|
||
├── common/ # 跨页共享:CSS 变量、envelope 解析器、错误码常量
|
||
│ ├── tokens.css # 设计令牌(颜色/字号/间距/断点)
|
||
│ ├── api-client.js # fetch 封装(含 trace_id、JWT 自动注入、envelope 解析)
|
||
│ ├── error-codes.js # 与 docs/05 §3.6 错误码一一对应
|
||
│ ├── permission-codes.js # 与 tools/seed_test_rbac.py 的权限码一一对应
|
||
│ └── layout/ # 共享壳:顶部品牌条、底部状态栏、侧边栏骨架
|
||
├── guest/ # 访客门户
|
||
│ ├── chat/index.html
|
||
│ ├── chat/chat.js
|
||
│ └── chat/chat.css
|
||
├── customer/ # 已登录用户门户(含看板!)
|
||
│ ├── chat/index.html
|
||
│ ├── chat/chat.js
|
||
│ ├── dashboard/ # ← 本期看板模块
|
||
│ │ ├── index.html
|
||
│ │ ├── dashboard.js # 入口编排
|
||
│ │ ├── modules/ # 业务模块(按卡片拆)
|
||
│ │ │ ├── account-summary.js
|
||
│ │ │ ├── portfolio-summary.js
|
||
│ │ │ ├── holdings-table.js
|
||
│ │ │ ├── quick-actions.js
|
||
│ │ │ └── refresh-indicator.js
|
||
│ │ ├── widgets/ # 通用组件(图表/数字块)
|
||
│ │ │ ├── metric-card.js
|
||
│ │ │ ├── sparkline.js
|
||
│ │ │ └── data-table.js
|
||
│ │ └── dashboard.css
|
||
│ ├── portfolio/ # 未来:组合详情
|
||
│ ├── orders/ # 未来:交易记录
|
||
│ └── profile/ # 客户画像候选确认(沿用 §8.2 二期流程)
|
||
├── employee-risk/ # 风控专员工作台(qyqy 团队负责)
|
||
├── employee-advisor/ # 投顾工作台(qyqy 团队负责)
|
||
└── README.md # 各 portal 路由约定
|
||
```
|
||
|
||
### 3.1 强制约束
|
||
|
||
| 项 | 要求 |
|
||
|---|---|
|
||
| **目录归属** | 客服 Agent 页面**只能**放在 `portal/{guest,customer,employee-*,trading}/` 下;不在 portal 目录的页面不受本规范保护 |
|
||
| **命名空间** | `portal/<角色>/<页面>/...` 三层目录;`<页面>` 用 kebab-case(多个词用 `-`) |
|
||
| **入口文件** | 每个页面**必须**有 `index.html` 作为静态入口;JS 入口与 CSS 各一 |
|
||
| **公共代码** | 跨页面复用代码**只能**放在 `portal/common/`;禁止在业务页直接拷贝 |
|
||
| **看板模块路径** | `portal/customer/dashboard/`,对应 `/api/v1/users/me/account/dashboard` 端点 |
|
||
| **README** | `portal/README.md` 列所有路由与角色映射;改路由必同步更新 README |
|
||
|
||
### 3.2 禁止
|
||
|
||
- ❌ 把页面直接放 `app/static/` 根目录(破坏 portal 命名空间)
|
||
- ❌ 在业务页里 `import` 别的业务页(跨业务复用必须抽到 `common/`)
|
||
- ❌ 在 HTML 里 `<script src="https://...">` 引入第三方资源——必须下载到本地 `static/vendor/`
|
||
|
||
---
|
||
|
||
## 4. 命名规范
|
||
|
||
### 4.1 文件命名
|
||
|
||
| 类型 | 规则 | 示例 |
|
||
|---|---|---|
|
||
| HTML 入口 | `<page>/index.html` | `dashboard/index.html` |
|
||
| JS 模块 | kebab-case | `metric-card.js`、`holdings-table.js` |
|
||
| CSS | kebab-case 或 `<page>.css` 单文件 | `dashboard.css` |
|
||
| 资源(图片)| snake_case + 后缀 | `logo_brandmark.svg`、`icon_refresh.svg` |
|
||
|
||
### 4.2 标识符命名
|
||
|
||
| 类型 | 规则 | 示例 |
|
||
|---|---|---|
|
||
| CSS 类 | BEM(block__element--modifier)| `metric-card__value--negative` |
|
||
| CSS 变量 | `--<category>-<name>` | `--ink`、`--brand-dark`、`--space-3` |
|
||
| JS 变量 | camelCase | `currentAccount`, `holdingsCache` |
|
||
| JS 类 / 构造器 | PascalCase | `HoldingsTable`, `MetricCard` |
|
||
| JS 私有(前缀约定)| 下划线前缀 | `_renderChart()`, `_lastRefreshAt` |
|
||
| DOM data-* 属性 | kebab-case + 前缀 | `data-permission-code`, `data-endpoint-id` |
|
||
| 端点 ID(用于 data-endpoint-id)| 与 `docs/05 §19` 一致 | `T001`、`A039`、`M003` |
|
||
|
||
### 4.3 错误码 / 权限码引用
|
||
|
||
**禁止**在前端硬编码错误码或权限码字面量。**所有**错误码与权限码必须从 `portal/common/error-codes.js` 与 `portal/common/permission-codes.js` 引用。
|
||
|
||
```js
|
||
// ❌ 错误:硬编码
|
||
if (error.code === 'INSUFFICIENT_FUNDS') { ... }
|
||
|
||
// ✅ 正确:从常量引用
|
||
import { ERR_CODES } from '/static/portal/common/error-codes.js';
|
||
if (error.code === ERR_CODES.INSUFFICIENT_FUNDS) { ... }
|
||
```
|
||
|
||
**理由**:避免后端改名后前端静默失败(这就是 §10 验收清单里"硬编码字面量"红线的由来)。
|
||
|
||
---
|
||
|
||
## 5. 组件拆分原则
|
||
|
||
### 5.1 三层结构
|
||
|
||
| 层 | 命名约定 | 职责 | 不允许 |
|
||
|---|---|---|---|
|
||
| **入口编排** | `dashboard.js` / `chat.js` | 拉数据 → 装配组件 → 渲染到 DOM | 写业务逻辑 |
|
||
| **业务模块** | `modules/<业务名>.js`(一个卡片一个)| 接收数据 → 渲染自身 → 暴露事件 | 调接口 |
|
||
| **通用组件** | `widgets/<组件名>.js`(可复用、无业务含义)| 纯展示 / 纯交互 | 知道任何 endpoint |
|
||
|
||
**禁止业务模块与通用组件互相调用**——必须经入口编排串接。示例:
|
||
|
||
```js
|
||
// dashboard.js(入口编排)
|
||
import { fetchAccountDashboard } from '/static/portal/customer/dashboard/api.js';
|
||
import { renderAccountSummary } from '/static/portal/customer/dashboard/modules/account-summary.js';
|
||
import { renderHoldingsTable } from '/static/portal/customer/dashboard/modules/holdings-table.js';
|
||
|
||
(async () => {
|
||
const data = await fetchAccountDashboard();
|
||
renderAccountSummary(document.getElementById('account-summary'), data.account);
|
||
renderHoldingsTable(document.getElementById('holdings'), data.holdings);
|
||
})();
|
||
```
|
||
|
||
### 5.2 组件拆分粒度
|
||
|
||
| 信号 | 应该拆 |
|
||
|---|---|
|
||
| 同一卡片有 ≥3 个独立可配置字段(标题、单位、趋势)| ✅ 抽 |
|
||
| 两个页面同时引用同一段 UI | ✅ 抽到 `widgets/` |
|
||
| 业务规则只在某卡片内使用 | ❌ 留卡片内部 |
|
||
| 单元 ≤30 行 | ❌ 不必拆 |
|
||
|
||
### 5.3 看板拆分清单
|
||
|
||
看板按 §T 端点的 4 个一级字段拆模块:
|
||
|
||
| 字段 | 模块 | 依赖 widget |
|
||
|---|---|---|
|
||
| `account` | `account-summary.js` | `metric-card` |
|
||
| `summary` | `portfolio-summary.js` | `metric-card`, `sparkline` |
|
||
| `holdings[]` | `holdings-table.js` | `data-table` |
|
||
| 通用操作 | `quick-actions.js`(按钮:去下单 / 看记录) | — |
|
||
|
||
---
|
||
|
||
## 6. 状态管理
|
||
|
||
### 6.1 全局状态
|
||
|
||
- **不引入** Redux / Vuex / MobX / Zustand 等状态库
|
||
- 跨页面状态用 `localStorage` + `sessionStorage`(按生命周期选择):
|
||
- `localStorage`:用户偏好(暗色模式、字号、刷新频率)
|
||
- `sessionStorage`:临时数据(表单草稿、未提交订单预览)—— **关闭标签页即清**
|
||
- **禁止**把 JWT / 客户敏感信息写进 localStorage
|
||
|
||
### 6.2 页面级状态
|
||
|
||
| 场景 | 方案 |
|
||
|---|---|
|
||
| 单次接口响应 | 页面内闭包变量,不导出 |
|
||
| 跨多个组件共享 | 入口 JS 维护 plain object + 显式 setter |
|
||
| 表单 | 原生 `<form>` + `FormData`,提交后清空 |
|
||
| 看板定时刷新 | `setInterval` 在 `pagehide` / `visibilitychange=hidden` 时**必须**清理(避免后台请求) |
|
||
|
||
### 6.3 关键约束
|
||
|
||
- **不引入**响应式框架(Preact / Alpine / lit-html)
|
||
- 看板 4 个模块**不能**互相修改对方的数据——必须通过入口 JS 中转
|
||
- 任何定时器(轮询、SSE 心跳)必须用单一 `setInterval` ID 维护;不嵌套
|
||
|
||
---
|
||
|
||
## 7. 接口调用规范
|
||
|
||
### 7.1 强制使用 `portal/common/api-client.js`
|
||
|
||
```js
|
||
// portal/common/api-client.js 提供的接口(伪代码)
|
||
export const apiClient = {
|
||
get(endpointId, params) { /* ... */ },
|
||
post(endpointId, body, options) { /* ... */ },
|
||
put(endpointId, body, options) { /* ... */ },
|
||
del(endpointId, params) { /* ... */ },
|
||
sse(endpointId, params, handlers) { /* ... */ }, // SSE 长连接
|
||
};
|
||
```
|
||
|
||
调用方**禁止**直接用 `fetch()`;必须走 `apiClient` 包装。
|
||
|
||
### 7.2 API 调用三大规则
|
||
|
||
1. **所有请求必传 `trace_id`**(由 `api-client.js` 自动生成 UUID v4 并写入 `X-Trace-Id` header)
|
||
2. **所有请求必传 JWT**(从 cookie `auth_token` 读取,自动注入 `Authorization: Bearer <token>`)
|
||
3. **响应必走 envelope 解析**(见 `docs/05 §3.3`:成功取 `data`、错误取 `error`)
|
||
|
||
### 7.3 端点 ID 注册
|
||
|
||
每个端点必须在 `portal/common/api-client.js` 中以**端点 ID**(如 `T001`)为 key 注册路径与 method:
|
||
|
||
```js
|
||
// 例:
|
||
const ENDPOINTS = {
|
||
T001: { method: 'GET', path: '/api/v1/users/me/account/dashboard' },
|
||
T002: { method: 'POST', path: '/api/v1/users/me/orders' },
|
||
T003: { method: 'GET', path: '/api/v1/users/me/orders' },
|
||
// ...
|
||
};
|
||
|
||
apiClient.get('T001'); // 实际请求 /api/v1/users/me/account/dashboard
|
||
```
|
||
|
||
**禁止**在前端代码里直接写字面路径。所有路径必须能从 `docs/05 §19` 查到对应端点 ID。
|
||
|
||
### 7.4 请求超时与重试
|
||
|
||
| 错误码 | 行为 |
|
||
|---|---|
|
||
| `5xx`(除 501)| 自动重试 1 次,间隔 2s |
|
||
| `503 FUND_QUOTE_UNAVAILABLE` 等可重试码 | 重试 + 显示「行情暂时不可用」 |
|
||
| `429` | 退避 5s 重试,**最多 2 次**(看板对行情类轮询尤其重要) |
|
||
| `4xx` | **不重试**——直接展示业务错误 |
|
||
| 超时(默认 8s)| 单次重试一次,仍超时则提示「网络不稳定」 |
|
||
|
||
### 7.5 看板专用:轮询与缓存
|
||
|
||
- 看板**不主动轮询**——必须由用户点击刷新按钮触发(避免无意义流量)
|
||
- 看板数据缓存策略:**短缓存**(60s),命中缓存时不发请求
|
||
- 持仓与组合的"今日盈亏"字段,**只接受**接口返回的服务端计算结果——前端禁止自行用 `latest_price × quantity` 派生
|
||
|
||
---
|
||
|
||
## 8. 权限与角色控制
|
||
|
||
### 8.1 RBAC 前端三大约束
|
||
|
||
1. **权限码引用**:所有权限码(如 `9060` `account:read:self`)从 `portal/common/permission-codes.js` 引用,**禁止硬编码**
|
||
2. **JWT 解码**:进入页面时调 `apiClient.get('M001')`(用户信息)或解析 cookie 里的 JWT 拿到 `roles[]` 与 `permissions[]`
|
||
3. **可见性控制**:UI 元素按权限显示/隐藏——`<button data-permission-code="9060">买入</button>`,由前端权限守卫脚本统一处理
|
||
|
||
### 8.2 角色与 portal 对应
|
||
|
||
| 角色 | 可访问 portal | 关键权限码 |
|
||
|---|---|---|
|
||
| 访客(无 token)| `portal/guest/` | 仅公开知识(无 RBAC 权限码)|
|
||
| 已登录用户 | `portal/customer/` | 9060 `account:read:self`、9061 `trade:order:create`、9062 `trade:order:read`、9063 `trade:order:cancel`、9064 `holding:read:self`、9065 `trade:txn:read` |
|
||
| 风控专员 | `portal/employee-risk/` | qyqy 已建(risk 段)|
|
||
| 投顾 | `portal/employee-advisor/` | qyqy 已建(promotion / investment-goal 段)|
|
||
| 平台管理员 | 全部 | 全量 |
|
||
|
||
### 8.3 看板可见性规则
|
||
|
||
| 元素 | 必需权限 | 缺权限表现 |
|
||
|---|---|---|
|
||
| 整个看板卡片(账户看板)| 9060 `account:read:self` | 卡片灰显 + tooltip「请登录后查看」 |
|
||
| 「去下单」按钮 | 9061 `trade:order:create` | 按钮禁用 + tooltip「暂未开通交易权限」 |
|
||
| 「撤单」按钮(持仓卡片内)| 9063 `trade:order:cancel` | 按钮禁用 + tooltip「暂未开通撤单权限」 |
|
||
| 「全部成交记录」链接 | 9065 `trade:txn:read` | 链接隐藏 |
|
||
|
||
### 8.4 前端强制不变量
|
||
|
||
- **不发明新角色**:前端只识别后端 JWT 里写明的 roles,禁止前端"假装是某个角色"
|
||
- **不绕过 RBAC 守卫**:即使通过开发者工具移除 `disabled` 也要让请求带正确权限码——后端会再次校验
|
||
- **权限码变更**必须**双向同步**:`tools/seed_test_rbac.py`(后端)+ `portal/common/permission-codes.js`(前端),且都通过 PR 评审
|
||
|
||
---
|
||
|
||
## 9. 错误处理与日志埋点
|
||
|
||
### 9.1 错误处理三大约束
|
||
|
||
1. **不吞错**:所有 `try/catch` 必须把错误上报到 `apiClient.reportError()`
|
||
2. **错误展示分级**:
|
||
- 业务可恢复(`4xx`):inline 提示(toast / 卡片内红条)
|
||
- 系统级(`5xx`):顶部横幅 + 自动上报
|
||
- 网络错误:底部 status bar 提示
|
||
3. **错误码文案必须来自后端**:`docs/05 §3.6` 每个错误码都有 `code` 与默认 `message`,前端**禁止**自行捏造文案
|
||
|
||
### 9.2 错误码示例(看板相关)
|
||
|
||
| 端点 | 错误码 | HTTP | 前端文案建议 |
|
||
|---|---|---|---|
|
||
| T001 / T006 / T007 / T009 | `FUND_QUOTE_UNAVAILABLE` | 503 | 「行情暂时不可用,请稍后刷新」 |
|
||
| T002 | `INSUFFICIENT_FUNDS` | 422 | 「可用余额不足,请减少买入份额」 |
|
||
| T002 | `HOLDING_RATIO_EXCEEDED` | 422 | 「超过单一投资者持有比例上限」 |
|
||
| T002 | `SUITABILITY_MISMATCH` | 422 | 「您的风险等级与产品不匹配」 |
|
||
| T005 | `ORDER_NOT_CANCELLABLE` | 409 | 「该订单无法撤单(首版市价立即成交)」 |
|
||
| 所有 | `AGENT_PERMISSION_DENIED` | 403 | 「您没有操作权限」 |
|
||
| 所有 | `ACCOUNT_NOT_FOUND` / `ORDER_NOT_FOUND` | 404 | 「记录不存在或已删除」 |
|
||
|
||
### 9.3 日志埋点(用户行为级)
|
||
|
||
- **不打印 raw fetch URL**:避免泄露 JWT
|
||
- **必埋**:
|
||
- 页面进入:`page_view { page, portal, sub_role }`
|
||
- 看板刷新:`dashboard_refresh { source: 'manual' | 'auto', duration_ms }`
|
||
- 下单提交:`order_submit_attempt { product_code, side, quantity }`
|
||
- 下单结果:`order_submit_result { order_no, status, error_code? }`
|
||
- **埋点上报**走 `apiClient.track(eventName, payload)`,禁用 `console.log`
|
||
|
||
### 9.4 Trace_id 贯穿
|
||
|
||
- 每次请求的 `trace_id` 必须写进 DOM:`document.documentElement.dataset.traceId = traceId`
|
||
- 用户报错截图时,截图工具要把 traceId 也截下来(前端做水印)
|
||
- 控制台输出**禁止**显示 traceId 明文——避免终端被复制到聊天时泄露会话关联
|
||
|
||
---
|
||
|
||
## 10. 样式与 UI 规范
|
||
|
||
### 10.1 设计令牌(必须从 `portal/common/tokens.css` 引用)
|
||
|
||
```css
|
||
:root {
|
||
/* 颜色 */
|
||
--ink: #17212b;
|
||
--muted: #607080;
|
||
--line: #d9e1e7;
|
||
--canvas: #f4f7f8;
|
||
--surface: #ffffff;
|
||
--brand: #008a7a;
|
||
--brand-dark: #00695f;
|
||
--accent: #e96f45;
|
||
--visitor: #e8f6f1;
|
||
--agent: #f2f5f7;
|
||
--danger: #b43b2c;
|
||
--success: #2e7d32;
|
||
--negative: #c62828;
|
||
|
||
/* 字号 */
|
||
--fs-display: 24px;
|
||
--fs-title: 18px;
|
||
--fs-body: 14px;
|
||
--fs-small: 12px;
|
||
|
||
/* 间距(4px 基准) */
|
||
--space-1: 4px;
|
||
--space-2: 8px;
|
||
--space-3: 12px;
|
||
--space-4: 16px;
|
||
--space-5: 24px;
|
||
--space-6: 32px;
|
||
|
||
/* 圆角 */
|
||
--radius-sm: 4px;
|
||
--radius-md: 6px;
|
||
--radius-lg: 10px;
|
||
|
||
/* 断点 */
|
||
--bp-sm: 620px; /* 移动 */
|
||
--bp-md: 900px; /* 平板 */
|
||
--bp-lg: 1280px; /* 桌面 */
|
||
}
|
||
```
|
||
|
||
### 10.2 强制约束
|
||
|
||
| 项 | 要求 |
|
||
|---|---|
|
||
| 颜色 | **只**引用 `--ink` / `--brand` 等 token;禁止 `#fff` 直接写 |
|
||
| 字号 | **只**引用 `--fs-*`;禁止 `12px`、`14px` 直接写 |
|
||
| 间距 | **只**引用 `--space-*`;禁止 `8px` 直接写 |
|
||
| 圆角 | **只**引用 `--radius-*` |
|
||
| 阴影 | 卡片阴影统一 `--shadow-card: 0 2px 6px rgba(0,0,0,0.04)` |
|
||
| 字体栈 | `"Microsoft YaHei", "PingFang SC", sans-serif`(统一) |
|
||
| 字符编码 | HTML 强制 `<meta charset="UTF-8">`、`<html lang="zh-CN">` |
|
||
| 单位 | 长度统一 `px` 或 `%`;**禁用** `rem`(与底座渲染管线冲突) |
|
||
| 嵌套 | CSS 嵌套**只允许** `& >` 一层;禁止 3 层以上 |
|
||
|
||
### 10.3 移动端适配
|
||
|
||
- 看板**必须**响应式:在 `--bp-sm (≤620px)` 时卡片单列、表格横滑
|
||
- 横滑表格**必须**有「向右滑动提示」首屏渐隐动画(≥600ms,避免被忽略)
|
||
- 触屏目标**最小 44×44 px**(苹果 HIG 标准)
|
||
- 长按选中 / 复制 / 粘贴**保留默认**——禁止 `user-select: none`
|
||
|
||
### 10.4 品牌元素(奶龙)
|
||
|
||
- 主色 `--brand` **不可**擅自换色——与底座品牌一致
|
||
- 头部品牌标识固定为 `static/portal/common/layout/brandmark.svg`(与底座 `app/static/index.html` 一致)
|
||
- 客服头像默认 `奶龙`,角色色 `--brand`;**不引入**林黛玉 / 张飞 / 唐僧等多角色头像(与 Wolin OS 风格区分)
|
||
|
||
---
|
||
|
||
## 11. 看板模块专属约束
|
||
|
||
### 11.1 模块归属
|
||
|
||
| 项 | 值 |
|
||
|---|---|
|
||
| **目录** | `portal/customer/dashboard/`(§3.1) |
|
||
| **HTML 入口** | `portal/customer/dashboard/index.html` |
|
||
| **JS 入口** | `portal/customer/dashboard/dashboard.js` |
|
||
| **CSS** | `portal/customer/dashboard/dashboard.css` |
|
||
| **后端端点** | `T001 GET /api/v1/users/me/account/dashboard` |
|
||
| **依赖权限** | 9060 `account:read:self`(顶层);子项 9061-9065 |
|
||
| **跨页面跳转源** | 主客服 Agent 对话页(`portal/customer/chat/`)、客户画像确认页(`portal/customer/profile/`) |
|
||
|
||
### 11.2 组件复用规则
|
||
|
||
#### 11.2.1 通用 widget
|
||
|
||
看板**只能**使用 `portal/customer/dashboard/widgets/` 下的组件;跨页面复用必须先抽到 `portal/common/widgets/`,**禁止**直接拷贝代码到 `customer/dashboard/widgets/`。
|
||
|
||
| widget | 复用范围 | 不允许 |
|
||
|---|---|---|
|
||
| `metric-card.js` | 看板 + 未来"组合详情"页 | 业务模块直接调用,必须经 `dashboard.js` 编排 |
|
||
| `sparkline.js` | 看板 | 内嵌业务判断 |
|
||
| `data-table.js` | 看板 + 未来"成交记录"页 | 自己实现一个排序逻辑 |
|
||
|
||
#### 11.2.2 业务模块拆 4 块
|
||
|
||
按 T001 端点的 4 个一级字段拆模块(§5.3 已列)。每个模块:
|
||
|
||
- 只接收纯数据 + DOM 容器,**不**调接口
|
||
- 暴露 `render(container, data)` 与 `update(container, data)` 两个方法
|
||
- 不依赖其他模块
|
||
- 销毁时清空 DOM 与所有事件监听(避免内存泄漏)
|
||
|
||
### 11.3 数据来源与刷新策略
|
||
|
||
#### 11.3.1 数据来源
|
||
|
||
| 模块 | 数据字段 | 来源端点 | 备注 |
|
||
|---|---|---|---|
|
||
| `account-summary` | `account.*` | T001 `dashboard.account` | 含账户号、余额、冻结金额 |
|
||
| `portfolio-summary` | `summary.*` | T001 `dashboard.summary` | 含总资产、当日盈亏 |
|
||
| `holdings-table` | `holdings[]` | T001 `dashboard.holdings[]` | 含每只基金的市值、盈亏 |
|
||
| `quick-actions` | 无数据,按权限显示 | 9061/9063/9065 | 见 §8.3 |
|
||
|
||
**禁止**看板额外调取:
|
||
- ❌ T006 `/api/v1/users/me/holdings`(T001 已包含 `holdings[]`,多余请求)
|
||
- ❌ T009 `/api/v1/users/me/cash-ledger`(资金账本,需用户主动跳转到独立页)
|
||
|
||
#### 11.3.2 刷新策略
|
||
|
||
| 触发 | 行为 |
|
||
|---|---|
|
||
| **页面加载** | 自动调一次 T001 |
|
||
| **手动刷新按钮** | 调 T001;显示 loading;60s 内不重复(防手抖) |
|
||
| **路由离开** | 取消 in-flight 请求 |
|
||
| **`visibilitychange=visible`** | **不**自动刷新(避免无意义流量) |
|
||
| **`online` 事件** | **不**自动刷新(提示用户「网络已恢复,请点击刷新」) |
|
||
| **下单/撤单成功后** | **自动**刷新一次 T001(持仓、余额会变)|
|
||
|
||
**理由**:过度主动刷新容易产生陈旧数据展示问题(与底座不变量冲突);用户主动刷新是更稳的契约。
|
||
|
||
### 11.4 筛选与时间范围控件标准
|
||
|
||
看板**首版不实现**筛选与时间范围选择器(数据已按"截至 now"由后端聚合)。**未来**若加,按以下规则:
|
||
|
||
| 控件 | 标准 |
|
||
|---|---|
|
||
| **时间范围** | 只允许下拉枚举(`今天` / `近 7 日` / `近 30 日` / `本年` / `全部`);**禁止**让用户输入起止日期(与底座行情快照策略冲突)|
|
||
| **产品筛选** | 多选下拉,最多 5 个产品;超过 5 个提示「请缩小范围」 |
|
||
| **筛选应用时机** | "应用"按钮显式触发;不实时筛选(避免每次勾选都发请求) |
|
||
| **默认值** | 时间范围=今天、产品=全部持仓 |
|
||
| **清空按钮** | 必须有;回到默认值 |
|
||
|
||
### 11.5 布局自适应
|
||
|
||
```
|
||
桌面(≥1280px):
|
||
┌──────────────────────────────────────┐
|
||
│ AccountSummary (横排 4 个 metric) │
|
||
├──────────────────┬───────────────────┤
|
||
│ PortfolioSummary │ HoldingsTable │
|
||
│ (左 1/3) │ (右 2/3) │
|
||
└──────────────────┴───────────────────┘
|
||
|
||
平板(900-1280px):
|
||
┌──────────────────────────────────────┐
|
||
│ AccountSummary (横排 4 个 metric) │
|
||
├──────────────────────────────────────┤
|
||
│ PortfolioSummary (横排 4 个 metric)│
|
||
├──────────────────────────────────────┤
|
||
│ HoldingsTable (全宽) │
|
||
└──────────────────────────────────────┘
|
||
|
||
移动(≤620px):
|
||
┌────────────────┐
|
||
│ AccountSummary │ ← 上下堆叠
|
||
├────────────────┤
|
||
│ PortfolioSummary│
|
||
├────────────────┤
|
||
│ HoldingsTable │ ← 横滑
|
||
└────────────────┘
|
||
```
|
||
|
||
### 11.6 权限可见性控制
|
||
|
||
#### 11.6.1 顶层守卫
|
||
|
||
```js
|
||
// dashboard.js 入口
|
||
import { ERR_CODES, PERM_CODES } from '/static/portal/common/index.js';
|
||
|
||
(async () => {
|
||
try {
|
||
const data = await apiClient.get('T001');
|
||
// 渲染 4 个模块
|
||
} catch (err) {
|
||
if (err.code === ERR_CODES.AGENT_PERMISSION_DENIED) {
|
||
renderForbidden(document.body); // 显示「请登录后查看」
|
||
return;
|
||
}
|
||
throw err;
|
||
}
|
||
})();
|
||
```
|
||
|
||
#### 11.6.2 元素级守卫
|
||
|
||
| 元素 | DOM 属性 | 守卫脚本处理 |
|
||
|---|---|---|
|
||
| 整个 dashboard 容器 | `data-permission-required="9060"` | 缺权限 → 显示「请登录」 |
|
||
| 「去下单」按钮 | `data-permission-required="9061"` | 缺权限 → 禁用 + tooltip |
|
||
| 「撤单」按钮(每行)| `data-permission-required="9063"` | 缺权限 → 禁用 + tooltip |
|
||
| 「成交记录」链接 | `data-permission-required="9065"` | 缺权限 → 隐藏 |
|
||
|
||
**统一脚本**:`portal/common/permission-guard.js`,在 `index.html` 的底部 `<script>` 中初始化。
|
||
|
||
### 11.7 数据展示口径
|
||
|
||
| 字段 | 展示口径 | 严禁 |
|
||
|---|---|---|
|
||
| `cash_balance` | `¥ + 千分位 + 两位小数`,红色背景 = 负数 | ❌ 自定义格式化、❌ 截断小数 |
|
||
| `available_cash` | 同上 | ❌ |
|
||
| `frozen_cash` | 灰色 + `(已冻结 ¥X)` 标注 | ❌ 隐藏冻结金额 |
|
||
| `total_asset` | 加粗 + `--fs-title` | ❌ 用 `--fs-display`(避免过度强调) |
|
||
| `total_profit_loss` | 正数 `--success`,负数 `--negative`,零 `--muted` | ❌ 用 `--danger` |
|
||
| `today_profit_loss` | 仅展示,不计算;后端返回啥就显示啥 | ❌ 自行算 `latest × qty - prev × qty` |
|
||
| 持仓产品名 | 显示中文 `product_name`;若无则回退 `product_code` | ❌ 只显示 code |
|
||
| 时间戳 `as_of` | 卡片右上角小字:`截至 2026-09-12 14:00` | ❌ 显示完整 ISO 字符串 |
|
||
|
||
### 11.8 加载与错误状态
|
||
|
||
每张卡片必须实现三种状态:
|
||
|
||
| 状态 | 表现 | 时机 |
|
||
|---|---|---|
|
||
| **loading** | 卡片骨架屏(CSS 动画 shimmer)| 接口 in-flight |
|
||
| **empty** | 居中占位符「暂无持仓」+ 空状态图 | 后端返回 `holdings: []` |
|
||
| **error** | 卡片顶部红条 + 「重试」按钮 | 接口失败 |
|
||
|
||
**禁止**整页 spinner(必须分卡片显示)。**禁止**空状态直接显示「加载失败」(混淆 loading 与 error)。
|
||
|
||
---
|
||
|
||
## 12. 与主客服 Agent 页面的交互跳转
|
||
|
||
### 12.1 三种入口
|
||
|
||
| 来源 | 入口元素 | 跳转方式 |
|
||
|---|---|---|
|
||
| 客服对话页 `portal/customer/chat/` | 客服回复中的「您的账户余额 ¥XXXXX」悬浮卡片 | 点击 → `window.location.assign('/portal/customer/dashboard/')` |
|
||
| 客服对话页 | 用户主动发问「我有多少钱」 | 客服 Agent 主动建议跳转(前端 chat.js 监听 SSE 事件 `hint_navigate`)|
|
||
| 顶部导航栏 | 「我的账户」链接 | `<a href="/portal/customer/dashboard/">` |
|
||
|
||
### 12.2 跳转不变量
|
||
|
||
1. **必带 trace_id**:跳转链接拼接 `?trace_id=<current>`,看板进入时验签(防伪跳转)
|
||
2. **保留 portal 角色上下文**:跳转后由 `dashboard.js` 重新拉用户身份(不依赖 URL 参数信任)
|
||
3. **深链支持**:看板必须支持 `?product_code=510300` 直跳到对应持仓行(高亮 1.5s 后恢复)
|
||
4. **返回路径**:从看板点「回到对话」必须回到原 chat session,URL 拼接 `?session_id=<last>`
|
||
|
||
### 12.3 反向跳转(看板 → 对话)
|
||
|
||
| 触发 | 行为 |
|
||
|---|---|
|
||
| 持仓表「分析收益」按钮 | 跳 `portal/customer/chat/?prefill="分析 510300 的近期表现"` |
|
||
| 「总收益有疑问」按钮(PortfolioSummary)| 跳 `portal/customer/chat/?prefill="我的总收益是怎么算的"` |
|
||
| 顶部「客服」链接 | 跳 `portal/customer/chat/`(不带 prefill) |
|
||
|
||
**理由**:让用户在数据视图与对话视图间平滑切换——这是「看板作为对话的延伸」的产品定位(vs 看板替代对话)。
|
||
|
||
---
|
||
|
||
## 13. 验收检查清单
|
||
|
||
任何客服 Agent 前端 PR 在合并前**必须**逐项核对:
|
||
|
||
### 13.1 通用项
|
||
|
||
- [ ] 路径在 `portal/<role>/<page>/` 下,README 已同步
|
||
- [ ] 所有接口调用走 `api-client.js`,**无直接 `fetch()`**
|
||
- [ ] 所有错误码引用自 `error-codes.js`,**无硬编码字面量**
|
||
- [ ] 所有权限码引用自 `permission-codes.js`,**无硬编码字面量**
|
||
- [ ] 所有样式 token 引用自 `tokens.css`,**无硬编码颜色/字号**
|
||
- [ ] 无 console.log(埋点用 `apiClient.track`)
|
||
- [ ] 无 raw JWT / 用户敏感信息写进 localStorage
|
||
- [ ] SSE 长连接不绕开底座托管
|
||
- [ ] 移动端(≤620px)布局测试通过
|
||
- [ ] 缺权限 UI 显隐测试通过(5 个权限码组合全过)
|
||
|
||
### 13.2 看板专属
|
||
|
||
- [ ] T001 端点 ID 在 `ENDPOINTS` 表中注册
|
||
- [ ] 4 个模块(account / portfolio / holdings / quick-actions)齐全
|
||
- [ ] 三个状态(loading / empty / error)都有
|
||
- [ ] 「去下单」「撤单」「成交记录」链接按权限显隐测试通过
|
||
- [ ] 60s 缓存策略生效(手动刷新按钮触发间隔 < 60s 提示「已缓存」)
|
||
- [ ] 下单/撤单成功后看板自动刷新一次
|
||
- [ ] trace_id 贯穿:URL 参数 + DOM `data-trace-id` + 用户报错提示模板
|
||
- [ ] 深链 `?product_code=` 高亮 1.5s 测试通过
|
||
- [ ] 反向跳转 prefill 参数正确编码
|
||
|
||
### 13.3 工具自动化(自动校验)
|
||
|
||
在 PR 流程中由脚本自动检查:
|
||
|
||
- `python tools/check_authoritative_docs.py` → 含 `docs/41-客服Agent前端开发约束_v1.md` 且编号唯一
|
||
- 后续 `tools/check_portal_frontend.py`(待建)→ 校验 `portal/` 目录命名 / 无硬编码颜色 / 引用完整性
|
||
- ESLint 自定义规则(待建)→ 拦截 `fetch(`、硬编码 `#xxx` 颜色、`localStorage.setItem('token'` 等
|
||
|
||
---
|
||
|
||
## 附录 A:与底座契约的对照
|
||
|
||
| 本约束 | 底座契约来源 |
|
||
|---|---|
|
||
| §2 技术栈 | 底座无 frontend 框架约束(与底座解耦) |
|
||
| §3 目录结构 | 底座 `app/static/index.html` 现有 Chat 页(沿用风格) |
|
||
| §7 接口调用 | `docs/05-接口文档.md` §3.3 envelope / §4 JWT / §19 端点编号 |
|
||
| §8 权限 | `tools/seed_test_rbac.py` 权限码 / `docs/05 §4.2` |
|
||
| §9 错误处理 | `docs/05-接口文档.md` §3.4 错误信封 + §3.6 错误码 |
|
||
| §11 看板 | §T 段 9 端点(已上推 qyqy_develop,含 `ebc3fe4`)|
|
||
| §12 跳转 | `docs/05 §6` Agent 运行接口 + §7 会话接口 |
|
||
|
||
## 附录 B:版本与变更记录
|
||
|
||
| 版本 | 日期 | 变更 |
|
||
|---|---|---|
|
||
| v1.0 | 2026-09-12 | 首版:覆盖 §T 段 9 端点 + 看板专属约束;与底座 `ebc3fe4` 同步 |
|
||
|
||
## 附录 C:相关文档索引
|
||
|
||
- `docs/05-接口文档.md` — 接口权威(envelope / RBAC / 错误码 / §19 端点)
|
||
- `docs/40-前端验收清单.md` — qyqy 团队的验收清单(侧重功能合规)
|
||
- `docs/客服Agent一期_合规红队与业务评测集_v1.md` — 合规红线(客服特有)
|
||
- `docs/客服Agent一期远程整合测试手册.md` — 集成测试门禁
|
||
- `docs/客服Agent二期_客户画像候选流程_v1.md` — 画像候选流程
|
||
- `tools/check_authoritative_docs.py` — 文档编号唯一性
|
||
- `tools/check_docs_endpoint_ids.py` — §19 端点编号唯一性
|
||
- `tools/check_rbac_seed_consistency.py` — 权限码一致性 |