Files
group_fqcd_jr/docs/41-客服Agent前端开发约束_v1.md
ZSY_docs 554d125190 docs(41): 客服 Agent 前端开发约束 v1.0(团队强制规范)
新增 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 不撞号
2026-09-12 17:04:37 +08:00

721 lines
32 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 客服 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` — 权限码一致性