- Established the P0 scaffolding for the frontend, including login, layout, and routing for four roles. - Integrated the Customer Wealth Dashboard, Advisor Clients Dashboard, Analyst Market Dashboard, and Risk Alerts Dashboard. - Updated the API client to support fetching customer and product data, enhancing the overall functionality of the dashboard. - Added error handling components to improve user experience during data fetching. - Enhanced charting capabilities using Ant Design Charts for better data visualization. This update lays the groundwork for further development of the frontend application, ensuring a robust structure for future features and integrations.
21 KiB
前端 · 四角色「收益/洞察」登录首页(桌面端)
日期:2026-09-09
状态:需求已定稿 · 待实现
动机:参考移动端「我的基金 / 固收+ / 板块信号」——登录第一眼看到资产与涨跌,而非占位 Statistic。
关联:2026-09-09-frontend-p0-design.md§8 · 接口契约 v0.1 ·web/· 场景 C-01/C-04/C-05 · R-01
0. 用户痒点(为什么要做)
| 痒点 | 现在(占位工作台) | 目标(本需求) |
|---|---|---|
| 一眼看资产 | 「演示指标 A=128」 | 登录即 总市值(真持仓汇总) |
| 一眼看涨跌 | 无 | 昨日收益估算 + 累计盈亏(红涨绿跌) |
| 一眼看结构 | 无 | 饼图/柱图(哪只占得多、哪只赚/亏) |
| 马上行动 | 只有文字链接 | 表内 Chat / 详情 / 处置 按钮 |
| 角色各不同 | 四角色同一套假指标 | 客户看持仓、顾问看客户、分析看市场、风控看预警 |
参考图映射(移动端 → 桌面,非 1:1 像素复刻):
| 参考 | 桌面落地 |
|---|---|
| 「我的基金」总览卡 | DashboardHero:大数字 + 3 副指标 |
| 产品列表卡片 | Table(桌面信息密度更高,可排序) |
| 固收+ 对比柱图 | 客户:各产品 pnl_pct 柱图;分析员:类型平均涨跌 |
| 板块潜力信号列表 | 风控:待审预警表 + 类型分布 |
1. 目标与原则
| 原则 | 说明 |
|---|---|
| 痒点优先 | 登录 defaultRoute = 各角色 Dashboard,不是 generic /app/home |
| 桌面适配 | 最小设计宽 1280px(与 P0 spec §8.3 一致);lg+ 双列图表 |
| 真数据 | 平台读 API 真接;禁止 Mock 整页 REST |
| 诚实边界 | 无历史净值序列 → 不画 K 线/分时;页眉 Alert 标明静态模拟 |
| 角色分化 | 四 Demo 账号各一套叙事(用户拍板 2026-09-09) |
2. 方案(已选 B)
各角色 独立 Dashboard 页,共用 DashboardLayout(Hero + ChartsRow + DetailTable + QuickActions)。
3. 共用布局壳
┌─ PageShell ─────────────────────────────────────────────────────────┐
│ Alert:数据截至 {as_of} · Core 模拟静态净值 · 非实时交易行情 │
├─ DashboardHero(浅蓝渐变 #e6f4ff→#fff,仅此区允许渐变 · 见 P0 §8.1 例外)│
│ 主指标(Typography.Title level={2} + tabular-nums) │
│ 副指标 ×3(Statistic compact) │
│ [可选] 眼睛图标:掩码金额(localStorage,仅前端 display) │
├─ QuickActions(4 个 Button/Link,角色不同)──────────────────────────┤
├─ ChartsRow ─────────────────────────────────────────────────────────┤
│ Col lg=8 结构图(Pie/Donut) Col lg=16 对比图(Column/Bar) │
├─ DetailTable(Card 包裹 Table size=small)───────────────────────────┤
│ 默认排序 · 分页 · 行操作 │
└─────────────────────────────────────────────────────────────────────┘
| 项 | 规格 |
|---|---|
| 图表库 | @ant-design/charts(新增依赖;仅 Dashboard + 后续 market 可复用) |
| 涨跌色 | 正 Typography.Text type="danger" · 负 type="success" · 零 secondary |
| 金额格式 | Intl.NumberFormat('zh-CN', { minimumFractionDigits: 2 }) + 后缀「元」 |
| 百分比 | 带符号 +1.23% / -0.50% |
| Loading | Hero/Chart/Table 分块 Skeleton;整页不白屏 |
| 空状态 | Empty + 插画 empty-data.svg + 文案「暂无持仓/暂无预警」 |
| 错误 | ApiErrorResult + 刷新按钮;不静默降级假数据 |
| 刷新 | Toolbar「刷新」重新拉 API(不 auto-poll) |
| 金额掩码 | Hero 区眼睛图标切换 localStorage['dashboard:maskAmount'];掩码时显示 ****(仅 display,不影响 API) |
| 表格默认排序 | 客户表:market_value 降序 · 理财师表:客户持仓市值降序 · 分析员表:|daily_chg_pct| 降序 · 风控表:created_at 降序 |
| 图表空数据 | 饼图 0 项 → 隐藏饼图 Col,对比图 Col 拉满 lg=24 |
3.1 QuickActions 路由映射(定稿)
| 角色 | 按钮 1 | 按钮 2 | 按钮 3 | 按钮 4 |
|---|---|---|---|---|
| 客户 | 交易明细 → #/app/customer/trades(P0 占位页) |
产品行情 → #/app/market |
客户助手 → #/app/customer/chat |
数据分析 → #/app/analytics/query |
| 理财师 | 名下客户 → #/app/advisor/customers |
顾问助手 → #/app/advisor/chat |
产品行情 → #/app/market |
数据分析 → #/app/analytics/query |
| 分析员 | 问数工作台 → #/app/analytics/query |
分析 Chat → #/app/analytics/chat |
产品行情 → #/app/market |
—(3 按钮) |
| 风控 | 预警台账 → #/app/risk/alerts |
风控助手 → #/app/risk/chat |
产品行情 → #/app/market |
数据分析 → #/app/analytics/query |
3.2 图表配置(@ant-design/charts)
| 图 | 类型 | 数据键 | 配色 |
|---|---|---|---|
| 结构图 | Pie(内半径 0.6 甜甜圈) | label + value |
AntD 默认色板;≤5 项全显,>5 项 Top5 +「其他」 |
| 对比图 | Column(横向 Bar) |
label + value |
值 >0 #cf1322 · <0 #389e0d · =0 #8c8c8c |
| Tooltip | 金额图带「元」后缀 · 百分比图带 % |
— | — |
| 高度 | 固定 height={280} |
— | — |
4. API 与字段契约(实现必读)
4.1 通用
- 请求头:
Authorization: Bearer {accessToken} - 响应:
{ code, data, trace_id }或 risk alerts 直出{ items, total, disclaimer } - 前端封装:
web/src/api/client.ts已有apiFetch
4.2 持仓 GET /api/customers/{customer_id}/holdings
{
"data": {
"items": [{
"product_id": "PROD-110022",
"product_name": "...",
"product_type": "...",
"min_risk_code": "R3",
"market_value": 82400.0,
"pnl_pct": 3.0,
"cost_amount": 80000.0,
"qty": 80000.0,
"as_of": "2026-09-04"
}],
"total": 3,
"truncated": false
}
}
字段名是
pnl_pct(不是 profit_pct) · 种子 as_of 统一2026-09-04
4.3 净值 GET /api/products/{product_id}/nav
{
"data": {
"nav": 1.03,
"daily_chg_pct": 0.35,
"nav_date": "2026-09-04"
}
}
合并逻辑抽 mergeHoldingsWithNav(holdings, navMap)(Vitest 覆盖)。
4.4 产品列表 GET /api/products?limit=50
分析员/行情页共用;再 N×nav 或等 v0.2 nav-snapshot。
4.5 预警 GET /api/risk/alerts?status=pending_review&page_size=100
{
"items": [{ "alert_id", "alert_type", "customer_id", "risk_score", "status", "created_at", ... }],
"total": 12,
"disclaimer": "本预警由系统自动生成..."
}
Dashboard 页脚或 Alert 旁展示 disclaimer(风控域,区别于 Chat G-08)。
4.6 鉴权与 actor 映射
| 角色 | 持仓/客户 API 的 {customer_id} |
理财师 API 的 {advisor_id} |
|---|---|---|
| 客户 | authStore.actorId(CUST-9527) |
— |
| 理财师 | 名下各客户 ID(roster 返回) | authStore.actorId(STAFF-10086) |
| 分析员 | 任意(全量只读) | 任意 |
| 风控 | 任意(全量只读) | — |
403 时走
ApiErrorResult;客户 token 查他人 holdings →AUTH_403_NOT_OWNER。
4.7 理财师 roster enrichment
GET /api/advisors/{id}/customers 仅返回 customer_id + display_name。Dashboard 表需 风险等级 与 持仓市值:
1. GET /api/advisors/{advisor_id}/customers
2. Promise.all(items.map(c =>
Promise.all([
GET /api/customers/{c.customer_id} → risk_code
GET /api/customers/{c.customer_id}/holdings → Σ market_value
])
))
抽 aggregateAdvisorRoster(rows)(Vitest);Demo ≤12 客户,N+1 可接受。
4.8 风控预警 · 数据前置(重要)
Core reset 后 risk_alert 默认为空(预警由 simulate/trade 或风控引擎产生,不在 Core 种子内)。
| 场景 | 行为 |
|---|---|
仅跑 reset.ps1 + prepare_risk_demo.sql |
风控 Dashboard 展示 Empty + 文案「暂无待审预警」+ 链到 #/app/risk/alerts;通过 AC-5a |
| 走查前按《演示SOP-风控模块》§4 执行 A-1/A-3/A-5 | 产生 pending_review 预警;Dashboard 有图有表;通过 AC-5b |
前端 不得 Mock 预警条数;Empty 与有数据两种态均须 UI 就绪。
5. 聚合公式(客户 · 纯函数 · 单测)
| 指标 | 公式 | 说明 |
|---|---|---|
totalValue |
Σ market_value |
Hero 主数字 |
totalPnlAmount |
Σ market_value * pnl_pct / 100 |
累计盈亏(元) |
yesterdayPnl |
Σ market_value * daily_chg_pct / 100 |
估算;缺 nav 的项跳过 |
holdingCount |
items.length |
副指标 |
asOf |
max(holding.as_of, nav.nav_date) |
Alert 文案 |
5.1 Demo 验收锚点 · 客户 CUST-9527(reset 后 · 可写 Vitest)
逐行明细(04-seed-holdings.sql + 06-seed-nav.sql):
| product_id | market_value | pnl_pct | daily_chg_pct | 行盈亏(元) | 行昨日(元) |
|---|---|---|---|---|---|
| PROD-005827 | 47,500.00 | -5.00 | -0.80 | -2,375.00 | -380.00 |
| PROD-110022 | 82,400.00 | 3.00 | 0.15 | 2,472.00 | 123.60 |
| PROD-000001 | 20,040.00 | 0.20 | 0.01 | 40.08 | 2.00 |
| 合计 | 149,940.00 | — | — | 137.08 | -254.40 |
| Hero 指标 | 期望值 |
|---|---|
| 持仓只数 | 3 |
| 总市值 | 149,940.00 |
| 累计盈亏 | +137.08(展示 +137.08 元) |
| 昨日收益 | -254.40(展示 -254.40 元) |
| as_of | 2026-09-04 |
旧稿「约 +1,034 元」为计算笔误,以上表为准。
5.2 Demo 验收锚点 · 理财师 STAFF-10086
| Hero 指标 | 期望值(reset 后) |
|---|---|
| 名下客户数 | 12 |
| 客户持仓总市值 | 1,654,850.00 |
| 最大单客户 | CUST-DEMO-C · 615,000.00 |
| Top5 客户(市值) | DEMO-C 615k · 3001 363.5k · 9527 149.94k · 1003 152.7k · DEMO-A 71.57k |
5.3 Demo 验收锚点 · 分析员 STAFF-20001
| Hero 指标 | 期望值(14 产品有 nav) |
|---|---|
| 产品总数 | 14 |
| 上涨 / 下跌 / 平盘 | 10 / 4 / 0(按 daily_chg_pct 符号) |
|daily_chg_pct| Top1 |
PROD-XYZ999 -2.00% |
| 类型数 | bond 3 · mixed 2 · stock 4 · index 2 · money 1 · wealth_mgmt 1 · private_fund 1 |
5.4 Demo 验收锚点 · 风控 STAFF-30001
| 场景 | Hero |
|---|---|
| reset 后无预警 | 待审 0 · Empty 态 |
| SOP A-1~A-5 走查后 | 待审 ≥5(含 suitability / large_amount / aml 等);disclaimer 固定文案可见 |
6. 分角色需求
6.1 客户(CUST-9527)
路由: #/app/customer/home(登录 defaultRoute,不变)
用户故事:
- 作为客户,登录后 3 秒内 看到总市值与涨跌,不用点「持仓」菜单。
- 作为客户,我能看出 哪只产品拖后腿(柱图 + 表排序)。
- 作为客户,我能 一键进客户助手 Chat 问「为什么亏了」。
| 区块 | 内容 |
|---|---|
| Hero 主指标 | 总资产(元) |
| Hero 副指标 | 昨日收益 · 累计盈亏 · 持仓只数 |
| 结构图 | 持仓市值占比(按 product_name,Top5 + 其他) |
| 对比图 | 各产品 pnl_pct 横向柱图 |
| 表列 | 产品名 · 市值 · 昨日涨跌% · 累计盈亏% · 风险等级 · 操作 |
| 表操作 | 「详情」→ #/app/customer/holdings(或 Drawer 产品详情 P1)· 「问助手」→ Chat |
| QuickActions | 交易明细 · 产品行情 · 客户助手 · 数据分析 |
场景 ID: C-01 持仓 · C-04 阈值(P1 阈值行高亮)· C-05 净值
6.2 理财师(STAFF-10086)
路由: #/app/advisor/home(新增);侧栏「首页」指向此;「名下客户」仍 #/app/advisor/customers 全量表
用户故事:
- 作为理财师,登录后看到 名下有几位客户、谁资产最大,便于安排跟进。
- 作为理财师,我能点进 顾问 Chat 或客户详情(P1)。
| 区块 | 内容 |
|---|---|
| Hero | 名下客户数 · 客户持仓总市值(Top 客户汇总)· 待跟进占位(P1 接 L2) |
| 结构图 | 客户市值占比(Top5 + 其他) |
| 对比图 | Top5 客户市值柱图 |
| 表 | 客户 ID · 姓名 · 风险等级 · 持仓市值 · 操作(查看/Chat) |
| QuickActions | 名下客户 · 顾问助手 · 产品行情 · 数据分析 |
API: GET /api/advisors/{id}/customers + 每客户 holdings(Demo ≤10 客户,前端 Promise.all)
v0.2 候选: GET /api/advisors/{id}/portfolio-summary 避免 N+1
6.3 分析员(STAFF-20001)
路由: #/app/analyst/home 或登录 default 仍 #/app/analytics/query —— 拍板:defaultRoute 改为 #/app/analyst/home,问数仍进 analytics/query
用户故事:
- 作为分析员,登录后看到 全市场产品涨跌概况,而不是空占位。
- 作为分析员,我能一键进 问数工作台 与 分析 Chat。
| 区块 | 内容 |
|---|---|
| Hero | 产品总数 · 上涨只数 · 下跌只数 · 平盘只数 |
| 结构图 | product_type 数量分布 |
| 对比图 | 各 product_type 平均 daily_chg_pct |
| 表 | 产品行情 Top10(按 |daily_chg_pct| 排序) |
| QuickActions | 问数工作台 · 分析 Chat · 产品行情 |
API: GET /api/products + nav 合并(与 /app/market 共享 hook)
场景 ID: D-01 读数入口
6.4 风控(STAFF-30001)
路由: #/app/risk/home(新增);登录 defaultRoute 从 #/app/risk/alerts 改为 #/app/risk/home;完整台账仍 /app/risk/alerts
用户故事:
- 作为风控专员,登录后 待审预警数量 一眼可见(参考「板块信号」信息密度)。
- 作为风控专员,我能看 预警类型结构 并点进处置或 Chat。
| 区块 | 内容 |
|---|---|
| Hero | 待审总数 · 今日新增(created_at 当天)· 涉及客户数(去重) |
| 结构图 | alert_type 分布(aml/large_amount/…) |
| 对比图 | 各 alert_type 平均 risk_score |
| 表 | 预警 ID · 类型 · 客户 · 分数 · 状态 · 创建时间 · 操作 |
| 表操作 | 「处置」→ alerts 详情/抽屉(P1)· 「风控助手」→ Chat |
| Alert | 展示 API disclaimer |
| QuickActions | 预警台账 · 风控助手 · 产品行情 · 数据分析 |
API: GET /api/risk/alerts?status=pending_review&page_size=100
场景 ID: R-01 预警 · A-6 对话查预警
7. 路由与信息架构(定稿)
| 角色 | defaultRoute(改) | Dashboard 路由 | 组件 |
|---|---|---|---|
| 客户 | #/app/customer/home |
同左 | CustomerWealthDashboard |
| 理财师 | #/app/advisor/home |
同左 | AdvisorClientsDashboard |
| 分析员 | #/app/analyst/home |
同左 | AnalystMarketDashboard |
| 风控 | #/app/risk/home |
同左 | RiskAlertsDashboard |
侧栏「工作台 / 首页」 各角色指向上表;删除 generic #/app/home 或保留 redirect 到角色 home。
demoAccounts.ts: 同步更新四个 defaultRoute。
7.1 侧栏与路由文件变更清单
| 文件 | 变更 |
|---|---|
web/src/config/demoAccounts.ts |
理财师 → #/app/advisor/home · 分析员 → #/app/analyst/home · 风控 → #/app/risk/home |
web/src/routes/menus.tsx |
理财师增「首页」→ /app/advisor/home · 风控增「首页」→ /app/risk/home · 分析员增「首页」→ /app/analyst/home(问数仍保留) |
web/src/App.tsx |
新增四 Dashboard 路由;customer/home 改挂 CustomerWealthDashboard;/app/home redirect 到角色 defaultRoute |
| 删除/弃用 | RoleHomePage.tsx 假 Statistic(实现后删除或仅测试保留) |
7.2 数据加载 Hook 契约
// useHoldingsDashboard(customerId)
// 1. GET holdings → 2. 对 items 去重 product_id → Promise.all nav
// 返回 { hero, pieData, barData, tableRows, asOf, loading, error, refresh }
// useMarketSnapshot() — 与 /app/market 共用
// GET products?limit=50 → N×nav → mergeProductNavRows
// useAlertsDashboard()
// GET /api/risk/alerts?status=pending_review&page_size=100
// 注意:响应无 ok() 外壳,直读 items/total/disclaimer
// useAdvisorRosterDashboard(advisorId)
// roster + 并行 enrichment(§4.7)
并发上限:nav 请求 ≤20 并发(p-limit 或手写 batch);失败单项不影响整页,表行 daily_chg_pct 显示 -。
8. 组件与文件
web/src/
api/customers.ts | products.ts | risk.ts | advisors.ts
utils/
formatMoney.ts | formatPct.ts | pnlColor.ts
mergeHoldingsWithNav.ts | aggregateHoldingsHero.ts
mergeProductNavRows.ts # 分析员/行情共用
aggregateAdvisorRoster.ts # 理财师 Hero+表
hooks/
useHoldingsDashboard.ts | useAlertsDashboard.ts | useMarketSnapshot.ts
components/dashboard/
DashboardLayout.tsx | DashboardHero.tsx | DashboardCharts.tsx
PnlText.tsx | AmountText.tsx # 掩码 + 红涨绿跌
QuickActionBar.tsx
pages/dashboard/
CustomerWealthDashboard.tsx
AdvisorClientsDashboard.tsx
AnalystMarketDashboard.tsx
RiskAlertsDashboard.tsx
9. 与 P0 主 spec 的关系
| 文档 | 关系 |
|---|---|
2026-09-09-frontend-p0-design.md |
壳/路由/Chat/market 仍有效;本 spec 替换「RoleHomePage 假指标」与部分占位首页 |
| 实现顺序 | 本 spec §10 优先于 P0 §6 的 customer 只读页(Dashboard 已含持仓摘要) |
| 共享复用 | mergeProductNavRows、market 页、alerts 全页 共用 api/hook |
10. 非目标(P0)
- K 线 / 分时 / 自选股 / WebSocket 行情
- 「追加购买」等交易 CTA(simulate 不做)
- 理财师后端聚合 API(Demo N+1 可接受)
- 完整 ChatPanel(Dashboard 只放入口按钮;Chat 仍单独路由)
- 移动端优先布局
11. 验收标准(可测)
| # | 条件 | 通过标准 |
|---|---|---|
| AC-1 | 四 Demo 登录 | 各进 角色 Dashboard,非 Placeholder |
| AC-2 | 客户 Hero | 总市值 149940.00 · 累计盈亏 +137.08 · 昨日 -254.40(CUST-9527,reset 后) |
| AC-3 | 涨跌色 | 正红负绿;pnl_pct=0 灰色 |
| AC-4 | 图表 | 每 Dashboard ≥1 张图有数据(风控 reset 后 Empty 除外) |
| AC-5a | 风控 Empty | reset 后无预警:Empty + disclaimer 区域占位,不报错 |
| AC-5b | 风控有数据 | SOP 走查后:待审数 = API total;页眉/页脚见 disclaimer |
| AC-6 | 合规文案 | 客户页 Alert 静态净值;风控页展示 API disclaimer |
| AC-7 | 刷新 | 点刷新重新请求,trace_id 可变 |
| AC-8 | 后端挂 | ApiErrorResult,无假数字 |
| AC-9 | 构建 | npm run build + Vitest 纯函数绿 |
| AC-10 | 理财师 Hero | 客户数 12 · 总市值 1654850.00(STAFF-10086) |
| AC-11 | 分析员 Hero | 产品 14 · 涨 10 跌 4 |
12. 测试清单(Vitest)
aggregateHoldingsHero:CUST-9527 锚点(§5.1 表)mergeHoldingsWithNav:缺 nav、部分缺 navmergeProductNavRows:products + nav;14 产品涨跌统计aggregateAdvisorRoster:STAFF-10086 客户数与总市值formatPct/pnlColor边界 0、null
13. 实现顺序
- 依赖
@ant-design/charts+ api/utils 纯函数 + Vitest DashboardLayout+CustomerWealthDashboard(跑通全链)RiskAlertsDashboardAnalystMarketDashboard(与 market 共用 hook)AdvisorClientsDashboard- 路由 +
demoAccountsdefaultRoute + 删RoleHomePage假指标 - P0 其余:ChatPanel、完整 alerts/holdings 页
14. 联调前置(FLOW §0)
| 步骤 | 命令/脚本 | Dashboard 影响 |
|---|---|---|
| ① Core 灌库 | scripts/core/reset.ps1 或 FLOW 脚本化 DROP+灌 |
客户/理财师/分析员 有数据 |
| ② 风评刷新 | scripts/demo/prepare_risk_demo.sql |
不影响 Dashboard 数字 |
| ③ 后端启动 | uvicorn app.main:app --reload |
API 可达 |
| ④ 前端 | cd web && npm run dev |
Vite proxy → :8000 |
| ⑤ 风控演示(可选) | 《演示SOP-风控模块》§4 A-1~A-5 | 风控 Dashboard 有预警 |
登录: 四 Demo 卡片一键登录(web/src/config/demoAccounts.ts)· Bearer 自动写入 authStore。
15. 修订记录
| 日期 | 说明 |
|---|---|
| 2026-09-09 | 首版:四角色桌面收益/洞察首页 |
| 2026-09-09 | 需求完善 v1:痒点表 · API 字段 pnl_pct · 聚合公式与 Demo 锚点 · 交互/空错态 · 路由定稿 · AC/测试 · 与 P0 关系 |
| 2026-09-09 | 需求完善 v2:修正累计盈亏锚点 137.08 · 四角色全量锚点 · QuickActions/图表/Hook · 风控 Empty 前置 · 侧栏变更清单 · 联调步骤 |