- Added a new visitor chat API endpoint (`/api/chat/visitor`) to allow unauthenticated users to engage in conversations without requiring customer data. - Introduced a visitor context dependency to manage visitor interactions seamlessly. - Enhanced the chat API to support explicit session termination and improved response handling for customer service interactions. - Updated the database configuration to include Redis client support for caching visitor data. - Added a new customer note repository to persist user notes independently of the L1 profile slots. This update significantly improves the customer service experience by enabling visitor interactions and ensuring efficient data handling for both registered and unregistered users.
1105 lines
42 KiB
Markdown
1105 lines
42 KiB
Markdown
# JinRong 前端现代化重构实施计划
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** 在不改变现有 API、鉴权、路由、数据聚合和交互契约的前提下,将 `web/` 全面重构为 Tailwind CSS + Ant Design 混合的「静奢智能」前端,并交付可供后续 agent 使用的组件基础和交接手册。
|
||
|
||
**Architecture:** Tailwind CSS v4 负责设计令牌、布局、排版、响应式和轻量动效;Ant Design 6 继续负责 Table、Alert、Result、Empty、Skeleton、message 等复杂交互,现有 `api/`、`hooks/`、`utils/` 和路由逻辑保持不变。新增的 `components/ui` 只处理纯展示,`components/layout` 只组合应用壳层,`components/dashboard` 只接收已计算的数据,页面继续负责 hooks、路由和业务文案。
|
||
|
||
**Tech Stack:** React 19、Vite 8、TypeScript 6、Tailwind CSS 4、`@tailwindcss/vite`、Ant Design 6、`@ant-design/charts`、React Router 7、Vitest。
|
||
|
||
**Spec:** `docs/superpowers/specs/2026-09-09-frontend-modernization-design.md`
|
||
|
||
## Global Constraints
|
||
|
||
- API URL、HTTP method、参数、请求头和 `apiFetch` 行为不变。
|
||
- `AuthState` 字段、`jinrong.auth` localStorage key、路由 path 和登录默认落点不变。
|
||
- 不修改现有 Dashboard hooks、聚合纯函数、金额/百分比 formatter 或图表数据公式。
|
||
- Ant Design 继续承载 Table、Pagination、Alert、Result、Empty、Skeleton、message 和现有图表行为。
|
||
- `components/ui` 禁止导入 `api`、`hooks`、`authStore` 或角色业务类型。
|
||
- 页面层只替换 JSX 外壳、className 和组合组件,不把请求逻辑迁入视觉组件。
|
||
- 新视觉令牌统一使用 `#F6F5F1`、`#FBFAF7`、`#132B3A`、`#17212B`、`#71808A`、`#E5E2DB`、`#178A68`、`#C9564A`、`#C9A66B`、`#3F718A`;页面不得硬编码新的品牌色。
|
||
- 收益语义保持正值朱砂红、负值翡翠绿、零值次级灰;不能因视觉迁移改变业务语义。
|
||
- 不使用全局 `!important` 覆盖 Ant Design 内部行为,不添加假数据或错误兜底。
|
||
- 每个任务完成后运行该任务列出的验证命令;完整交付前必须通过 `npm run build`、`npm run test`、`npm run lint`。
|
||
- 不删除或覆盖用户已有的未提交改动;修改文件前检查当前 diff,变更集中在本计划列出的文件。
|
||
- 不在本计划中创建 git commit,除非用户另行明确要求提交。
|
||
|
||
---
|
||
|
||
## 文件地图与责任
|
||
|
||
### 将创建的文件
|
||
|
||
- `web/src/styles/tokens.css`:JinRong 颜色、字体、圆角、阴影和动效令牌。
|
||
- `web/src/lib/cn.ts`:无依赖的 className 合并工具。
|
||
- `web/src/components/ui/Button.tsx`:纯展示按钮变体和尺寸。
|
||
- `web/src/components/ui/Surface.tsx`:面板/卡片表面变体。
|
||
- `web/src/components/ui/Badge.tsx`:角色、状态、环境和风险标签。
|
||
- `web/src/components/ui/MetricCard.tsx`:通用指标卡。
|
||
- `web/src/components/ui/PageHeader.tsx`:页面 eyebrow、标题、说明和 action。
|
||
- `web/src/components/ui/SectionHeading.tsx`:区块标题和辅助操作。
|
||
- `web/src/components/ui/EmptyState.tsx`:品牌化空状态。
|
||
- `web/src/components/ui/index.ts`:基础 UI 组件统一导出。
|
||
- `web/src/components/layout/AppShell.tsx`:侧栏、顶栏、主内容区组合器。
|
||
- `web/src/components/layout/BrandSidebar.tsx`:品牌侧栏和菜单渲染。
|
||
- `web/src/components/layout/Topbar.tsx`:顶栏上下文、actor ID、退出。
|
||
- `web/src/components/layout/index.ts`:布局组件统一导出。
|
||
- `web/src/components/dashboard/ChartCard.tsx`:图表表面和标题组合。
|
||
- `web/src/components/dashboard/DataSection.tsx`:数据区标题与表格表面。
|
||
- `web/src/components/dashboard/index.ts`:Dashboard 组件统一导出。
|
||
- `web/src/components/ui/__tests__/ui.test.tsx`:基础 UI 组件可渲染、变体和可访问性 smoke tests。
|
||
- `docs/frontend/FRONTEND-HANDOFF.md`:后续 agent 的启动、组件、数据和验收手册。
|
||
|
||
### 将修改的文件
|
||
|
||
- `web/package.json`:加入 Tailwind v4 与 Vite 插件依赖。
|
||
- `web/package-lock.json`:由 npm 安装同步生成,不能手工删改无关 lock 内容。
|
||
- `web/vite.config.ts`:注册 `tailwindcss()` Vite 插件,保留 React、proxy 和 Vitest。
|
||
- `web/src/main.tsx`:改为加载统一 token/global 样式入口(如实现需要,保留现有 AntD ConfigProvider)。
|
||
- `web/src/theme.ts`:同步 JinRong 色板、圆角、Layout、Table 和状态组件 token。
|
||
- `web/src/styles/global.css`:重写 reset、AntD bridge、滚动条、focus 和通用动画。
|
||
- `web/src/index.css`:清除 Vite starter CSS,使其只保留必要的 Tailwind 入口或删除引用。
|
||
- `web/src/App.css`:清除 Vite starter `.counter/.hero/#next-steps` 等无引用规则;如果迁移后无引用则删除文件和 import。
|
||
- `web/src/layouts/AppLayout.tsx`:保留 auth/menu/location 逻辑,改为组装 `AppShell`、`BrandSidebar` 和 `Topbar`。
|
||
- `web/src/pages/login/LoginPage.tsx`:保留登录请求和跳转,重做品牌区与账号卡片。
|
||
- `web/src/components/PageShell.tsx`:改为复用 `PageHeader` 和新内容间距。
|
||
- `web/src/pages/PlaceholderPage.tsx`:改为 `PageHeader + EmptyState`。
|
||
- `web/src/components/ApiErrorResult.tsx`:品牌化错误面板,保留 error code、message、trace ID、retry。
|
||
- `web/src/components/AgentBanner.tsx`:统一 Agent 提示样式,保留 message 和 info 语义。
|
||
- `web/src/components/dashboard/DashboardHero.tsx`:改为新 Hero/MetricCard 组合,保留金额掩码和指标 props。
|
||
- `web/src/components/dashboard/QuickActionBar.tsx`:改为 ActionRail,保留每个 `to` 和 Link 行为。
|
||
- `web/src/components/dashboard/DashboardCharts.tsx`:只增加 `ChartCard` 包装和新颜色配置,保留图表数据、延迟挂载和错误边界。
|
||
- `web/src/components/dashboard/DashboardLayout.tsx`:改为统一 PageHeader、Notice、Hero、ActionRail、InsightGrid、DataSection。
|
||
- `web/src/pages/dashboard/CustomerWealthDashboard.tsx`:只迁移 JSX 组合和文案层级。
|
||
- `web/src/pages/dashboard/AdvisorClientsDashboard.tsx`:只迁移 JSX 组合和文案层级。
|
||
- `web/src/pages/dashboard/AnalystMarketDashboard.tsx`:只迁移 JSX 组合和文案层级。
|
||
- `web/src/pages/dashboard/RiskAlertsDashboard.tsx`:只迁移 JSX 组合和文案层级。
|
||
|
||
### 明确不应修改的文件
|
||
|
||
- `web/src/api/**`
|
||
- `web/src/hooks/**`
|
||
- `web/src/utils/**`
|
||
- `web/src/stores/authStore.ts`
|
||
- `web/src/routes/menus.tsx`
|
||
- `web/src/App.tsx`
|
||
- `web/src/config/demoAccounts.ts`
|
||
- 现有纯函数测试,除非新样式迁移暴露真实类型错误。
|
||
|
||
---
|
||
|
||
### Task 1: Tailwind 基础设施与设计令牌
|
||
|
||
**Files:**
|
||
- Modify: `web/package.json`
|
||
- Modify: `web/package-lock.json`
|
||
- Modify: `web/vite.config.ts`
|
||
- Create: `web/src/styles/tokens.css`
|
||
- Modify: `web/src/styles/global.css`
|
||
- Modify: `web/src/index.css`
|
||
- Modify: `web/src/main.tsx`
|
||
|
||
**Interfaces:**
|
||
- Produces the global Tailwind utility layer and CSS variables consumed by every later UI component.
|
||
- Preserves Vite `/api` proxy, React plugin, Vitest `jsdom` environment, and existing AntD `ConfigProvider`.
|
||
|
||
- [ ] **Step 1: Record the current baseline before dependency changes**
|
||
|
||
Run from `D:\项目\JinRong\web`:
|
||
|
||
```bash
|
||
npm run build
|
||
npm run test
|
||
npm run lint
|
||
```
|
||
|
||
Expected: record the current result; if a command already fails, note the pre-existing failure and do not silently attribute it to this task.
|
||
|
||
- [ ] **Step 2: Install the Tailwind v4 Vite integration**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
npm install -D tailwindcss@^4.1.0 @tailwindcss/vite@^4.1.0
|
||
```
|
||
|
||
Expected: `package.json` and `package-lock.json` gain only the Tailwind packages and their transitive dependencies; existing runtime dependencies remain unchanged.
|
||
|
||
- [ ] **Step 3: Register the Vite plugin without changing the dev proxy**
|
||
|
||
Update `web/vite.config.ts` to preserve the current React plugin and server settings while adding the Tailwind plugin:
|
||
|
||
```ts
|
||
import react from '@vitejs/plugin-react'
|
||
import tailwindcss from '@tailwindcss/vite'
|
||
import { defineConfig } from 'vitest/config'
|
||
|
||
export default defineConfig({
|
||
plugins: [react(), tailwindcss()],
|
||
server: {
|
||
port: 5173,
|
||
proxy: {
|
||
'/api': {
|
||
target: 'http://127.0.0.1:8000',
|
||
changeOrigin: true,
|
||
},
|
||
},
|
||
},
|
||
test: {
|
||
environment: 'jsdom',
|
||
},
|
||
})
|
||
```
|
||
|
||
- [ ] **Step 4: Add the theme source and exact JinRong tokens**
|
||
|
||
Create `web/src/styles/tokens.css`:
|
||
|
||
```css
|
||
@import "tailwindcss";
|
||
|
||
@theme {
|
||
--color-jr-bg: #f6f5f1;
|
||
--color-jr-surface: #fbfaf7;
|
||
--color-jr-surface-strong: #ffffff;
|
||
--color-jr-ink: #132b3a;
|
||
--color-jr-text: #17212b;
|
||
--color-jr-muted: #71808a;
|
||
--color-jr-border: #e5e2db;
|
||
--color-jr-positive: #178a68;
|
||
--color-jr-negative: #c9564a;
|
||
--color-jr-gold: #c9a66b;
|
||
--color-jr-info: #3f718a;
|
||
--radius-jr-sm: 8px;
|
||
--radius-jr-md: 12px;
|
||
--radius-jr-lg: 16px;
|
||
--shadow-jr-surface: 0 12px 32px rgb(32 42 48 / 6%);
|
||
--shadow-jr-float: 0 18px 44px rgb(32 42 48 / 10%);
|
||
--font-sans: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||
--font-mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||
}
|
||
|
||
:root {
|
||
--jr-bg: #f6f5f1;
|
||
--jr-surface: #fbfaf7;
|
||
--jr-surface-strong: #ffffff;
|
||
--jr-ink: #132b3a;
|
||
--jr-text: #17212b;
|
||
--jr-muted: #71808a;
|
||
--jr-border: #e5e2db;
|
||
--jr-positive: #178a68;
|
||
--jr-negative: #c9564a;
|
||
--jr-gold: #c9a66b;
|
||
--jr-info: #3f718a;
|
||
--jr-radius-sm: 8px;
|
||
--jr-radius-md: 12px;
|
||
--jr-radius-lg: 16px;
|
||
--jr-shadow-surface: 0 12px 32px rgb(32 42 48 / 6%);
|
||
--jr-shadow-float: 0 18px 44px rgb(32 42 48 / 10%);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 5: Replace starter global CSS with the shared reset and AntD bridge**
|
||
|
||
Update `web/src/styles/global.css` to import tokens and define only global concerns:
|
||
|
||
```css
|
||
@import './tokens.css';
|
||
|
||
:root {
|
||
color: var(--jr-text);
|
||
background: var(--jr-bg);
|
||
font-family: var(--font-sans);
|
||
font-synthesis: none;
|
||
text-rendering: optimizeLegibility;
|
||
-webkit-font-smoothing: antialiased;
|
||
-moz-osx-font-smoothing: grayscale;
|
||
}
|
||
|
||
html,
|
||
body,
|
||
#root {
|
||
min-height: 100%;
|
||
margin: 0;
|
||
}
|
||
|
||
body {
|
||
min-width: 320px;
|
||
background: var(--jr-bg);
|
||
}
|
||
|
||
button,
|
||
a,
|
||
input,
|
||
textarea,
|
||
select {
|
||
font: inherit;
|
||
}
|
||
|
||
button:focus-visible,
|
||
a:focus-visible,
|
||
[role='button']:focus-visible {
|
||
outline: 2px solid var(--jr-gold);
|
||
outline-offset: 3px;
|
||
}
|
||
|
||
.tabular-nums {
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.ant-layout,
|
||
.ant-layout-content {
|
||
background: transparent;
|
||
}
|
||
|
||
.ant-table-wrapper .ant-table {
|
||
background: transparent;
|
||
}
|
||
|
||
.ant-table-wrapper .ant-table-thead > tr > th {
|
||
background: color-mix(in srgb, var(--jr-bg) 72%, white);
|
||
color: var(--jr-muted);
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.08em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
@keyframes jr-fade-up {
|
||
from { opacity: 0; transform: translateY(6px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
.jr-page-enter {
|
||
animation: jr-fade-up 320ms ease-out both;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 6: Make stylesheet imports single-source and remove starter declarations**
|
||
|
||
Update `web/src/main.tsx` to keep one global style import:
|
||
|
||
```ts
|
||
import './styles/global.css'
|
||
```
|
||
|
||
Remove the old `import './index.css'` if it exists. Empty `web/src/index.css` and `web/src/App.css` may remain temporarily for this task, but remove their imports and all Vite starter rules in Task 5 only after a reference search confirms they are unused.
|
||
|
||
- [ ] **Step 7: Verify infrastructure**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
npm run build
|
||
npm run test
|
||
npm run lint
|
||
```
|
||
|
||
Expected: all commands pass, with no API or test behavior changes. If Tailwind’s generated CSS reports unsupported `color-mix`, replace only the bridge declaration with a static fallback while retaining the same token value and visual intent.
|
||
|
||
---
|
||
|
||
### Task 2: Pure UI primitives and component tests
|
||
|
||
**Files:**
|
||
- Create: `web/src/lib/cn.ts`
|
||
- Create: `web/src/components/ui/Button.tsx`
|
||
- Create: `web/src/components/ui/Surface.tsx`
|
||
- Create: `web/src/components/ui/Badge.tsx`
|
||
- Create: `web/src/components/ui/MetricCard.tsx`
|
||
- Create: `web/src/components/ui/PageHeader.tsx`
|
||
- Create: `web/src/components/ui/SectionHeading.tsx`
|
||
- Create: `web/src/components/ui/EmptyState.tsx`
|
||
- Create: `web/src/components/ui/index.ts`
|
||
- Create: `web/src/components/ui/__tests__/ui.test.tsx`
|
||
|
||
**Interfaces:**
|
||
- Consumes: CSS classes and tokens from Task 1.
|
||
- Produces: pure components with no imports from `api`, `hooks`, `stores`, `routes`, or dashboard-specific types.
|
||
- `Button`: `{ variant?: 'primary' | 'secondary' | 'ghost' | 'quiet'; size?: 'sm' | 'md' | 'lg'; loading?: boolean } & React.ButtonHTMLAttributes<HTMLButtonElement>`.
|
||
- `Surface`: `{ variant?: 'default' | 'subtle' | 'dark'; padding?: 'none' | 'sm' | 'md' | 'lg'; as?: ElementType }` plus intrinsic props.
|
||
- `Badge`: `{ tone?: 'neutral' | 'info' | 'positive' | 'negative' | 'gold' | 'ink'; children: ReactNode }`.
|
||
- `MetricCard`: `{ label: string; value: ReactNode; detail?: ReactNode; trend?: ReactNode; emphasis?: 'default' | 'hero' }`.
|
||
- `PageHeader`: `{ eyebrow?: string; title: string; description?: ReactNode; action?: ReactNode }`.
|
||
- `SectionHeading`: `{ eyebrow?: string; title: string; description?: ReactNode; action?: ReactNode }`.
|
||
- `EmptyState`: `{ image?: string; title: string; description?: ReactNode; action?: ReactNode }`.
|
||
|
||
- [ ] **Step 1: Write the class merge helper**
|
||
|
||
Create `web/src/lib/cn.ts` with a deliberately dependency-free implementation:
|
||
|
||
```ts
|
||
export function cn(...values: Array<string | false | null | undefined>) {
|
||
return values.filter(Boolean).join(' ')
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Add the failing primitive tests**
|
||
|
||
Create `web/src/components/ui/__tests__/ui.test.tsx`:
|
||
|
||
```tsx
|
||
import { describe, expect, it } from 'vitest'
|
||
import { render, screen } from '@testing-library/react'
|
||
import { Badge, Button, EmptyState, MetricCard, PageHeader, Surface } from '../index'
|
||
|
||
describe('JinRong UI primitives', () => {
|
||
it('merges only truthy class names', async () => {
|
||
const { cn } = await import('../../../lib/cn')
|
||
expect(cn('a', false, undefined, 'b')).toBe('a b')
|
||
})
|
||
|
||
it('renders a primary button with an accessible label', () => {
|
||
render(<Button variant="primary">刷新数据</Button>)
|
||
expect(screen.getByRole('button', { name: '刷新数据' })).toHaveClass('bg-jr-ink')
|
||
})
|
||
|
||
it('renders a metric card with value and detail', () => {
|
||
render(<MetricCard label="总市值" value="149,940.00" detail="持仓市值合计" />)
|
||
expect(screen.getByText('总市值')).toBeInTheDocument()
|
||
expect(screen.getByText('149,940.00')).toBeInTheDocument()
|
||
expect(screen.getByText('持仓市值合计')).toBeInTheDocument()
|
||
})
|
||
|
||
it('renders page and section semantics without business dependencies', () => {
|
||
render(
|
||
<>
|
||
<PageHeader eyebrow="WEALTH DESK" title="我的资产" description="数据概览" />
|
||
<Surface aria-label="资产面板">内容</Surface>
|
||
<Badge tone="positive">运行正常</Badge>
|
||
<EmptyState title="暂无持仓" description="稍后再试" />
|
||
</>,
|
||
)
|
||
expect(screen.getByRole('heading', { name: '我的资产' })).toBeInTheDocument()
|
||
expect(screen.getByLabelText('资产面板')).toBeInTheDocument()
|
||
expect(screen.getByText('运行正常')).toBeInTheDocument()
|
||
expect(screen.getByText('暂无持仓')).toBeInTheDocument()
|
||
})
|
||
})
|
||
```
|
||
|
||
Run:
|
||
|
||
```bash
|
||
npm run test -- src/components/ui/__tests__/ui.test.tsx
|
||
```
|
||
|
||
Expected: FAIL because the new components do not exist yet.
|
||
|
||
- [ ] **Step 3: Implement `Button`, `Surface`, and `Badge` minimally**
|
||
|
||
Use `forwardRef`, native button semantics, and class variants. The core class choices must include:
|
||
|
||
```ts
|
||
const buttonVariants = {
|
||
primary: 'bg-jr-ink text-white shadow-sm hover:bg-[#1d3b4d]',
|
||
secondary: 'border border-jr-border bg-jr-surface-strong text-jr-ink hover:border-jr-ink',
|
||
ghost: 'text-jr-ink hover:bg-jr-bg',
|
||
quiet: 'text-jr-muted hover:text-jr-ink',
|
||
} as const
|
||
```
|
||
|
||
Avoid adding a new arbitrary brand token for the hover color; if the inline literal is rejected by project policy, use `hover:brightness-95` instead. `loading` renders a text-neutral `<span aria-hidden="true">` indicator and sets `aria-busy` without changing the click contract.
|
||
|
||
`Surface` renders the requested `as` element with `rounded-jr-md border border-jr-border bg-jr-surface-strong shadow-jr-surface` by default. `Badge` renders a `<span>` with `role="status"` only when a caller supplies that semantic, otherwise remains an inline element.
|
||
|
||
- [ ] **Step 4: Implement `MetricCard`, `PageHeader`, `SectionHeading`, and `EmptyState`**
|
||
|
||
Required markup:
|
||
|
||
```tsx
|
||
<article className={...}>
|
||
<div className="...">{label}</div>
|
||
<div className="tabular-nums ...">{value}</div>
|
||
{detail && <div className="...">{detail}</div>}
|
||
{trend && <div className="...">{trend}</div>}
|
||
</article>
|
||
```
|
||
|
||
`PageHeader` and `SectionHeading` must render an `h1` and `h2` respectively, expose action content without hard-coding buttons, and keep descriptions optional. `EmptyState` must preserve the supplied image path and render a meaningful heading even when no description is present.
|
||
|
||
- [ ] **Step 5: Export the primitives**
|
||
|
||
Create `web/src/components/ui/index.ts`:
|
||
|
||
```ts
|
||
export { Badge } from './Badge'
|
||
export { Button } from './Button'
|
||
export { EmptyState } from './EmptyState'
|
||
export { MetricCard } from './MetricCard'
|
||
export { PageHeader } from './PageHeader'
|
||
export { SectionHeading } from './SectionHeading'
|
||
export { Surface } from './Surface'
|
||
```
|
||
|
||
- [ ] **Step 6: Run the tests and build**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
npm run test -- src/components/ui/__tests__/ui.test.tsx
|
||
npm run build
|
||
```
|
||
|
||
Expected: the primitive tests pass and the existing application build remains green. Do not import these primitives into production pages until the next task if doing so would require unrelated edits.
|
||
|
||
---
|
||
|
||
### Task 3: Layout shell, navigation, and shared state components
|
||
|
||
**Files:**
|
||
- Create: `web/src/components/layout/AppShell.tsx`
|
||
- Create: `web/src/components/layout/BrandSidebar.tsx`
|
||
- Create: `web/src/components/layout/Topbar.tsx`
|
||
- Create: `web/src/components/layout/index.ts`
|
||
- Modify: `web/src/layouts/AppLayout.tsx`
|
||
- Modify: `web/src/components/PageShell.tsx`
|
||
- Modify: `web/src/pages/PlaceholderPage.tsx`
|
||
- Modify: `web/src/components/ApiErrorResult.tsx`
|
||
- Modify: `web/src/components/AgentBanner.tsx`
|
||
|
||
**Interfaces:**
|
||
- Consumes: existing `AuthState`, AntD menu group items, `useLocation`, `useNavigate`, and UI primitives from Task 2.
|
||
- Produces: `AppShell` with `{ sidebar: ReactNode; topbar: ReactNode; children: ReactNode; collapsed: boolean }`, `BrandSidebar` with role/menu/path/collapse props, and `Topbar` with role/actor/callback props.
|
||
- Preserves `buildMenuGroups`, `flattenMenuPaths`, selected route calculation, `clearAuth`, actor copy, and logout navigation exactly.
|
||
|
||
- [ ] **Step 1: Capture current navigation behavior**
|
||
|
||
Before editing, manually inspect the four menu groups from `web/src/routes/menus.tsx` and run the current build. The new components must receive menu data rather than reimplementing role-specific route logic.
|
||
|
||
- [ ] **Step 2: Implement `BrandSidebar` with menu behavior isolated from styling**
|
||
|
||
The component should accept:
|
||
|
||
```ts
|
||
type BrandSidebarProps = {
|
||
roleLabel: string
|
||
collapsed: boolean
|
||
selectedKey: string
|
||
menuItems: MenuProps['items']
|
||
onNavigate: (key: string) => void
|
||
onToggle: () => void
|
||
}
|
||
```
|
||
|
||
Render the existing `Menu` with `items`, `selectedKeys`, and `onClick`, but place it in the new brand surface. The expanded header contains the existing `/assets/brand/logo-mark.svg`, `JinRong`, and the role label; collapsed mode retains the image with `alt="JinRong"` and hides text using CSS, not conditional route logic. Keep the menu item keys untouched.
|
||
|
||
- [ ] **Step 3: Implement `Topbar` with actor copy and logout affordance**
|
||
|
||
The component should accept:
|
||
|
||
```ts
|
||
type TopbarProps = {
|
||
roleLabel: string
|
||
actorId: string
|
||
collapsed: boolean
|
||
onToggle: () => void
|
||
onLogout: () => void
|
||
}
|
||
```
|
||
|
||
Use existing AntD `Button`, `Tag` or the new `Badge` as needed, but keep `aria-label="折叠导航"` / `aria-label="展开导航"`, copyable actor ID, role text, and `onLogout` callback. Do not call `clearAuth` from this component.
|
||
|
||
- [ ] **Step 4: Implement `AppShell` and responsive content structure**
|
||
|
||
`AppShell` renders a fixed desktop sidebar and a content column:
|
||
|
||
```tsx
|
||
<div className="min-h-screen bg-jr-bg text-jr-text lg:flex">
|
||
{sidebar}
|
||
<div className="min-w-0 flex-1">
|
||
{topbar}
|
||
<main className="jr-page-enter px-4 py-5 sm:px-6 lg:px-8">{children}</main>
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
Use CSS media classes or an AntD Drawer only if needed for narrow screens; do not create a second menu data source. The content column must have `min-w-0` so tables can use their existing horizontal behavior without forcing page overflow.
|
||
|
||
- [ ] **Step 5: Adapt `AppLayout` without changing its data flow**
|
||
|
||
Keep these lines conceptually unchanged:
|
||
|
||
```ts
|
||
const menuGroups = useMemo(() => buildMenuGroups(auth.roleLabel), [auth.roleLabel])
|
||
const menuItems = useMemo(() => toAntdMenuItems(menuGroups), [menuGroups])
|
||
const allowedPaths = useMemo(() => flattenMenuPaths(menuGroups), [menuGroups])
|
||
const selectedKey = allowedPaths.find((p) => location.pathname.startsWith(p)) ?? location.pathname
|
||
const onMenuClick = ({ key }: { key: string }) => navigate(key)
|
||
const logout = () => { clearAuth(); navigate('/login') }
|
||
```
|
||
|
||
Replace only the current AntD `Layout` JSX with `AppShell`, `BrandSidebar`, and `Topbar`, and pass `<Outlet context={auth} />` as the children.
|
||
|
||
- [ ] **Step 6: Migrate `PageShell` and shared state components**
|
||
|
||
`PageShell` continues to accept its current `{ title, crumbs, extra, alert, children }` props. Use `PageHeader` for the title/action region, render existing breadcrumbs above it when present, and wrap children with the shared page spacing.
|
||
|
||
`PlaceholderPage` must preserve `title`, `illustration`, and `description`; render `EmptyState` with image `/assets/illustrations/empty-data.svg` by default and do not introduce fake actions.
|
||
|
||
`ApiErrorResult` must preserve:
|
||
|
||
- status/error code title;
|
||
- original `error.message`;
|
||
- copyable `trace_id` when available;
|
||
- retry callback.
|
||
|
||
`AgentBanner` must preserve its default message and optional override; only replace its visual wrapper.
|
||
|
||
- [ ] **Step 7: Verify auth and navigation behavior**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
npm run build
|
||
npm run test
|
||
npm run lint
|
||
```
|
||
|
||
Then with the dev server running, verify each existing `defaultRoute` manually:
|
||
|
||
```text
|
||
CUST-9527 → /app/customer/home
|
||
STAFF-10086 → /app/advisor/home
|
||
STAFF-20001 → /app/analyst/home
|
||
STAFF-30001 → /app/risk/home
|
||
```
|
||
|
||
Verify menu selection, collapse/expand, actor ID copy, logout, placeholder empty state, and error retry. No API request should have changed.
|
||
|
||
---
|
||
|
||
### Task 4: Dashboard visual primitives and shared composition
|
||
|
||
**Files:**
|
||
- Create: `web/src/components/dashboard/ChartCard.tsx`
|
||
- Create: `web/src/components/dashboard/DataSection.tsx`
|
||
- Create: `web/src/components/dashboard/index.ts`
|
||
- Modify: `web/src/components/dashboard/DashboardHero.tsx`
|
||
- Modify: `web/src/components/dashboard/QuickActionBar.tsx`
|
||
- Modify: `web/src/components/dashboard/DashboardCharts.tsx`
|
||
- Modify: `web/src/components/dashboard/DashboardLayout.tsx`
|
||
|
||
**Interfaces:**
|
||
- Consumes: existing `DashboardLayoutProps`, `DashboardHeroProps`, `QuickAction`, `ChartDatum`, AntD chart/table props, and UI primitives from Task 2.
|
||
- Produces: shared Dashboard composition with unchanged public page inputs and unchanged chart/table callbacks.
|
||
- `ChartCard`: `{ eyebrow?: string; title: string; description?: ReactNode; children: ReactNode }`.
|
||
- `DataSection<T>`: `{ title: string; description?: ReactNode; action?: ReactNode; children: ReactNode }`.
|
||
|
||
- [ ] **Step 1: Add failing tests for data-independent composition**
|
||
|
||
Extend `web/src/components/ui/__tests__/ui.test.tsx` or create `web/src/components/dashboard/__tests__/composition.test.tsx` with:
|
||
|
||
```tsx
|
||
it('renders a chart card title around chart content', () => {
|
||
render(<ChartCard title="资产结构"><div data-testid="chart">图表</div></ChartCard>)
|
||
expect(screen.getByRole('heading', { name: '资产结构' })).toBeInTheDocument()
|
||
expect(screen.getByTestId('chart')).toBeInTheDocument()
|
||
})
|
||
|
||
it('renders quick actions as links without changing destinations', () => {
|
||
render(<QuickActionBar actions={[{ label: '产品行情', to: '/app/market' }]} />)
|
||
expect(screen.getByRole('link', { name: '产品行情' })).toHaveAttribute('href', '#/app/market')
|
||
})
|
||
```
|
||
|
||
Run the targeted test and expect failure until the composition is migrated.
|
||
|
||
- [ ] **Step 2: Implement `ChartCard` and `DataSection`**
|
||
|
||
`ChartCard` should render a `Surface` with an optional eyebrow, an `h2`, optional description, and a content region with no assumptions about chart library internals. `DataSection` should render an `h2`, optional description/action, and a table content region with `overflow-x-auto` only around the table area.
|
||
|
||
- [ ] **Step 3: Migrate `DashboardHero` while preserving its props**
|
||
|
||
Keep the existing `DashboardHeroProps` and the following behavior exactly:
|
||
|
||
```tsx
|
||
mainDisplay ?? <AmountText value={mainValue} masked={masked} />
|
||
```
|
||
|
||
Keep `onToggleMask`, `EyeOutlined`, `EyeInvisibleOutlined`, `aria-label="切换金额显示"`, all metric labels, and `Typography` children. Replace the blue gradient div with a layered warm surface: ink accent, subtle border, hero main value, and three responsive metric cells. Use `MetricCard` only if it can render `ReactNode` values without coercing numbers or changing mask output.
|
||
|
||
- [ ] **Step 4: Migrate `QuickActionBar` without changing links**
|
||
|
||
Keep the exact `QuickAction` type and map:
|
||
|
||
```tsx
|
||
{actions.map((a) => (
|
||
<Link key={a.to} to={a.to}>...</Link>
|
||
))}
|
||
```
|
||
|
||
Present actions as a horizontally wrapping action rail. Do not use buttons with `onClick` in place of links, because route behavior and browser semantics must remain intact.
|
||
|
||
- [ ] **Step 5: Wrap existing charts in `ChartCard`**
|
||
|
||
In `DashboardCharts.tsx`, preserve:
|
||
|
||
- `barColor` sign mapping;
|
||
- `formatValue` output;
|
||
- `ChartBoundary` behavior;
|
||
- `DeferredDashboardCharts` requestAnimationFrame behavior;
|
||
- Pie/Column data keys, tooltip formatters, heights and responsive columns.
|
||
|
||
Only add `ChartCard` wrappers and replace chart colors with `var(--jr-negative)`, `var(--jr-positive)`, and `var(--jr-muted)` values passed through a supported chart config mechanism. If `@ant-design/charts` requires hex strings, use the exact token hex values in this one shared adapter rather than changing page files.
|
||
|
||
- [ ] **Step 6: Migrate `DashboardLayout` composition**
|
||
|
||
Preserve the current generic props and error branch:
|
||
|
||
```tsx
|
||
if (error) {
|
||
return (
|
||
<PageShell title={title}>
|
||
<ApiErrorResult error={error} onRetry={onRefresh} />
|
||
</PageShell>
|
||
)
|
||
}
|
||
```
|
||
|
||
Preserve loading branch, `extraAlert`, `hideCharts`, `tableData.length`, `tableColumns`, `tableRowKey`, and `tableEmptyText`. Change only the structure so success becomes:
|
||
|
||
```text
|
||
PageShell/PageHeader
|
||
Notice + extraAlert
|
||
DashboardHero
|
||
QuickActionBar
|
||
ChartCards (unless hidden or empty)
|
||
DataSection + AntD Table/Empty
|
||
```
|
||
|
||
The existing table remains the source of pagination, sorting and row action behavior.
|
||
|
||
- [ ] **Step 7: Run composition tests and full static checks**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
npm run test -- src/components/dashboard/__tests__/composition.test.tsx
|
||
npm run build
|
||
npm run test
|
||
npm run lint
|
||
```
|
||
|
||
Expected: composition tests and all existing pure-function tests pass; no data hook files are modified.
|
||
|
||
---
|
||
|
||
### Task 5: Login page and four Dashboard page migration
|
||
|
||
**Files:**
|
||
- Modify: `web/src/pages/login/LoginPage.tsx`
|
||
- Modify: `web/src/pages/dashboard/CustomerWealthDashboard.tsx`
|
||
- Modify: `web/src/pages/dashboard/AdvisorClientsDashboard.tsx`
|
||
- Modify: `web/src/pages/dashboard/AnalystMarketDashboard.tsx`
|
||
- Modify: `web/src/pages/dashboard/RiskAlertsDashboard.tsx`
|
||
|
||
**Interfaces:**
|
||
- Consumes: unchanged login API/auth functions, unchanged four dashboard hooks, Task 4 shared composition.
|
||
- Produces: visually migrated pages with identical route links, API inputs, table columns, chart data and state branches.
|
||
|
||
- [ ] **Step 1: Migrate the login page without touching login behavior**
|
||
|
||
Keep the current `handleLogin` body’s account lookup, loading key, `login(account.actorId, account.tokenType)`, `matchDemoAccount`, `saveAuth`, `message.success`, navigation, `ApiError` message, and finally cleanup. Replace only the returned markup with:
|
||
|
||
```text
|
||
full viewport warm background
|
||
├── brand panel: logo, product name, environment badge, positioning copy, existing illustration
|
||
└── login panel: eyebrow, heading, four role cards, development disclaimer
|
||
```
|
||
|
||
Each account card must retain `key={acc.key}`, `loading={loadingKey === acc.key}`, `onClick={() => handleLogin(acc.key)}`, `acc.label`, and `acc.description`. The card may add a visible role badge and actor ID only from existing `acc` fields; do not make new login requests.
|
||
|
||
- [ ] **Step 2: Capture customer Dashboard numeric baseline**
|
||
|
||
With the existing backend seed available, verify the current customer values before changing page JSX:
|
||
|
||
```text
|
||
总市值 149940.00
|
||
累计盈亏 +137.08
|
||
昨日收益 -254.40
|
||
持仓只数 3
|
||
as_of 2026-09-04
|
||
```
|
||
|
||
If the backend is unavailable, rely on the existing hook/unit tests and record that browser numeric validation is blocked; do not add fixtures to production code.
|
||
|
||
- [ ] **Step 3: Migrate `CustomerWealthDashboard`**
|
||
|
||
Keep all imports and table column definitions that provide business behavior. Change only the Dashboard composition props and visible class/layout wrappers. Preserve:
|
||
|
||
- `useAppAuth()` and `useHoldingsDashboard(auth.accessToken, auth.actorId)`;
|
||
- `masked`, `toggle` and both eye states;
|
||
- exact `Link` destinations `/app/customer/holdings`, `/app/customer/chat`, `/app/customer/trades`, `/app/market`, `/app/analytics/query`;
|
||
- exact `alertText` semantics;
|
||
- `barValueIsPercent`, row key `product_id`, and empty text `暂无持仓`.
|
||
|
||
- [ ] **Step 4: Migrate `AdvisorClientsDashboard`**
|
||
|
||
Preserve:
|
||
|
||
- `useAdvisorRosterDashboard(auth.accessToken, auth.actorId)`;
|
||
- customer count, total AUM and top customer values;
|
||
- `AmountText` usage and table sort by `totalValue` descending;
|
||
- exact links to advisor customers/chat, market and analytics;
|
||
- row key `customer_id`, bar suffix ` 元`, and empty text `暂无名下客户`.
|
||
|
||
Use the new visual hierarchy to distinguish “服务客户” from the aggregate metrics, but do not change any field access or fallback (`display_name || customer_id || '—'`).
|
||
|
||
- [ ] **Step 5: Migrate `AnalystMarketDashboard`**
|
||
|
||
Preserve:
|
||
|
||
- `useMarketSnapshot(auth.accessToken)`;
|
||
- top ten absolute daily change sort;
|
||
- `PnlText` sign rendering;
|
||
- exact links to analytics query/chat and market;
|
||
- `barValueIsPercent`, row key `product_id`, and `暂无产品净值`.
|
||
|
||
Use the new neutral/positive/negative visual tokens in the market summary, but leave the existing red-up/green-down semantic component unchanged.
|
||
|
||
- [ ] **Step 6: Migrate `RiskAlertsDashboard`**
|
||
|
||
Preserve:
|
||
|
||
- `useAlertsDashboard(auth.accessToken)`;
|
||
- `disclaimer` conditional rendering;
|
||
- `hideCharts={items.length === 0}`;
|
||
- exact links to risk alerts/chat, market and analytics;
|
||
- row key `alert_id`, alert column sort behavior and `暂无待审预警`.
|
||
|
||
Ensure the reset/no-alert state still renders an Empty state instead of a zero-filled fake chart; the API’s disclaimer remains visible when provided.
|
||
|
||
- [ ] **Step 7: Run page-level static checks**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
npm run build
|
||
npm run test
|
||
npm run lint
|
||
```
|
||
|
||
Expected: all tests pass, no changes under `api/`, `hooks/`, `utils/`, `authStore.ts`, or route configuration, and TypeScript reports no unused imports after visual-only JSX changes.
|
||
|
||
---
|
||
|
||
### Task 6: CSS cleanup, responsive polish, and AntD visual bridge
|
||
|
||
**Files:**
|
||
- Modify: `web/src/theme.ts`
|
||
- Modify: `web/src/styles/global.css`
|
||
- Modify: `web/src/index.css`
|
||
- Modify: `web/src/App.css`
|
||
- Modify: `web/src/main.tsx` if an obsolete import remains
|
||
|
||
**Interfaces:**
|
||
- Consumes: all migrated components and pages from Tasks 1–5.
|
||
- Produces: one coherent global styling entry, AntD tokens aligned to JinRong, no starter CSS, and no page-wide overflow at 320px or 1280px.
|
||
|
||
- [ ] **Step 1: Update `appTheme` to the exact brand values**
|
||
|
||
Update `web/src/theme.ts` while preserving `ThemeConfig` export and ConfigProvider usage:
|
||
|
||
```ts
|
||
export const appTheme: ThemeConfig = {
|
||
token: {
|
||
colorPrimary: '#132B3A',
|
||
colorText: '#17212B',
|
||
colorTextSecondary: '#71808A',
|
||
colorBgLayout: '#F6F5F1',
|
||
colorBgContainer: '#FFFFFF',
|
||
colorBorder: '#E5E2DB',
|
||
borderRadius: 12,
|
||
fontSize: 14,
|
||
fontFamily: '-apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif',
|
||
},
|
||
components: {
|
||
Layout: { headerBg: '#FFFFFF', siderBg: '#FBFAF7', bodyBg: '#F6F5F1' },
|
||
Table: { headerBg: '#F6F5F1', rowHoverBg: '#FBFAF7' },
|
||
Card: { borderRadiusLG: 16 },
|
||
Button: { borderRadius: 10 },
|
||
},
|
||
}
|
||
```
|
||
|
||
Keep the app’s existing AntD locale and ConfigProvider arrangement.
|
||
|
||
- [ ] **Step 2: Remove Vite starter CSS only after reference search**
|
||
|
||
Search the source tree for each starter selector before deleting:
|
||
|
||
```bash
|
||
# Use the repository search tool or an equivalent source search.
|
||
# Confirm .counter, .hero, #center, #next-steps, #docs, #spacer, .ticks have no references.
|
||
```
|
||
|
||
After confirmation, remove the selectors from `web/src/App.css`, remove any Vite variables from `web/src/index.css`, and remove imports that no longer have consumers. Do not remove `tabular-nums` or login/dashboard class rules until their replacements are present.
|
||
|
||
- [ ] **Step 3: Add narrow-screen safeguards**
|
||
|
||
Verify that shared shell and Dashboard wrappers include:
|
||
|
||
```text
|
||
min-w-0 on the content column
|
||
overflow-x-auto only around wide tables
|
||
flex-wrap on action rails
|
||
responsive metric grid
|
||
no fixed 1126px root width
|
||
```
|
||
|
||
At 320px, login panels stack vertically, the sidebar can be collapsed/drawn away, and no body-level horizontal scrollbar appears. At 1280px, chart cards and tables retain their intended hierarchy.
|
||
|
||
- [ ] **Step 4: Run visual static checks**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
npm run build
|
||
npm run test
|
||
npm run lint
|
||
```
|
||
|
||
Expected: no unused CSS import, no TypeScript error, and no lint regressions.
|
||
|
||
---
|
||
|
||
### Task 7: Browser verification and regression pass
|
||
|
||
**Files:**
|
||
- No production file changes unless a verified regression is found.
|
||
- Read: `docs/superpowers/specs/2026-09-09-frontend-modernization-design.md`
|
||
- Read: `docs/superpowers/specs/2026-09-09-frontend-wealth-dashboard-design.md`
|
||
|
||
**Interfaces:**
|
||
- Consumes: completed implementation from Tasks 1–6 and the real FastAPI API when available.
|
||
- Produces: a verified browser checklist and a short list of any fixes required before handoff.
|
||
|
||
- [ ] **Step 1: Start the backend and frontend using the documented commands**
|
||
|
||
Backend terminal:
|
||
|
||
```bash
|
||
uvicorn app.main:app --reload
|
||
```
|
||
|
||
Frontend terminal:
|
||
|
||
```bash
|
||
cd web
|
||
npm run dev
|
||
```
|
||
|
||
If the backend cannot be started, continue with unauthenticated/login and static states, then explicitly record API-dependent checks as blocked rather than claiming they passed.
|
||
|
||
- [ ] **Step 2: Verify login and shell for all roles**
|
||
|
||
In the browser, exercise all four demo cards and confirm:
|
||
|
||
```text
|
||
客户 → /app/customer/home
|
||
理财师 → /app/advisor/home
|
||
分析员 → /app/analyst/home
|
||
风控 → /app/risk/home
|
||
```
|
||
|
||
For each role, verify brand logo, role context, active menu state, collapse/expand, actor ID copy, logout, and a shared platform route.
|
||
|
||
- [ ] **Step 3: Verify each Dashboard’s success and interaction paths**
|
||
|
||
Customer:
|
||
|
||
- total value and P&L render from API values;
|
||
- eye button toggles `****` without changing row data;
|
||
- charts appear when holdings exist;
|
||
- table sorting, pagination and Details/Assistant links work.
|
||
|
||
Advisor:
|
||
|
||
- customer count and AUM render;
|
||
- top customer fallback text is correct;
|
||
- table sorting and customer/chat links work.
|
||
|
||
Analyst:
|
||
|
||
- product count and up/down/flat metrics render;
|
||
- absolute daily-change ordering is preserved;
|
||
- market/query/chat links work.
|
||
|
||
Risk:
|
||
|
||
- pending count and disclaimer render from API;
|
||
- empty reset state has no fake chart/table data;
|
||
- populated alerts show chart/table and disposition/chat links.
|
||
|
||
- [ ] **Step 4: Verify error and loading states**
|
||
|
||
Temporarily stop or point the API away only through a reversible local dev configuration; do not commit proxy changes. Confirm the branded error panel still shows error code, message, trace ID and retry. Confirm loading skeletons retain page structure and the app shell does not become a blank white page.
|
||
|
||
- [ ] **Step 5: Verify responsive behavior**
|
||
|
||
Use browser viewport widths 1280px, 1024px, 768px and 320px. Confirm:
|
||
|
||
- no body horizontal overflow;
|
||
- table overflow is local to table region;
|
||
- login columns stack;
|
||
- action rail wraps;
|
||
- metric cards collapse to readable columns;
|
||
- focus indicators remain visible.
|
||
|
||
- [ ] **Step 6: Run the final automated suite**
|
||
|
||
```bash
|
||
cd web
|
||
npm run build
|
||
npm run test
|
||
npm run lint
|
||
```
|
||
|
||
Expected: all three commands pass after browser verification. Any fix found here must be made in the smallest relevant component and the affected command rerun.
|
||
|
||
---
|
||
|
||
### Task 8: Write the frontend handoff manual
|
||
|
||
**Files:**
|
||
- Create: `docs/frontend/FRONTEND-HANDOFF.md`
|
||
|
||
**Interfaces:**
|
||
- Consumes: final directory structure, public component props, API/hooks boundaries, and verified commands from Tasks 1–7.
|
||
- Produces: a self-contained guide for another agent to add a page or component without rediscovering the architecture.
|
||
|
||
- [ ] **Step 1: Document startup and verification commands**
|
||
|
||
Include the exact commands:
|
||
|
||
```bash
|
||
cd web
|
||
npm install
|
||
npm run dev
|
||
npm run build
|
||
npm run test
|
||
npm run lint
|
||
```
|
||
|
||
Document the Vite `/api` proxy target `http://127.0.0.1:8000`, the HashRouter URL shape, and the requirement to start FastAPI separately.
|
||
|
||
- [ ] **Step 2: Document the dependency direction**
|
||
|
||
Include this diagram and explain each boundary:
|
||
|
||
```text
|
||
pages → hooks/api/utils
|
||
pages → components/dashboard
|
||
components/dashboard → components/ui
|
||
layouts/AppLayout → components/layout
|
||
components/ui → styles/tokens.css
|
||
```
|
||
|
||
Explicitly state that `components/ui` must not import business modules and that hooks remain the owners of API loading/error state.
|
||
|
||
- [ ] **Step 3: Document the design tokens and semantic colors**
|
||
|
||
List the exact tokens from `web/src/styles/tokens.css`, including:
|
||
|
||
```text
|
||
jr-bg #F6F5F1
|
||
jr-surface #FBFAF7
|
||
jr-ink #132B3A
|
||
jr-text #17212B
|
||
jr-muted #71808A
|
||
jr-border #E5E2DB
|
||
jr-positive #178A68
|
||
jr-negative #C9564A
|
||
jr-gold #C9A66B
|
||
jr-info #3F718A
|
||
```
|
||
|
||
State that positive P&L is red, negative P&L is green, and zero is muted gray because this is the product’s existing financial convention.
|
||
|
||
- [ ] **Step 4: Document component props and examples**
|
||
|
||
Provide concise examples for:
|
||
|
||
```tsx
|
||
<Surface><MetricCard label="总市值" value={<AmountText value={value} />} /></Surface>
|
||
<PageHeader eyebrow="WEALTH DESK" title="我的资产" action={<Button>刷新</Button>} />
|
||
<Badge tone="positive">运行正常</Badge>
|
||
<ChartCard title="资产结构">...</ChartCard>
|
||
<DataSection title="持仓明细"> <Table ... /> </DataSection>
|
||
<EmptyState title="暂无持仓" image="/assets/illustrations/empty-data.svg" />
|
||
```
|
||
|
||
Record supported button, surface and badge variants and explain that components accept ReactNode slots rather than owning data fetching.
|
||
|
||
- [ ] **Step 5: Document how to add a role Dashboard and API hook**
|
||
|
||
Describe the required sequence:
|
||
|
||
1. Add or reuse an API function under `src/api`.
|
||
2. Keep loading/error/refresh logic in a hook under `src/hooks`.
|
||
3. Keep aggregation and formatting in `src/utils` where applicable.
|
||
4. Compose `PageHeader`, `DashboardHero`, `ChartCard`, `DataSection` and `EmptyState` in the page.
|
||
5. Preserve `ApiErrorResult`, no-fake-data behavior, and route links.
|
||
6. Add the menu and route only in their existing owners (`routes/menus.tsx`, `App.tsx`) rather than duplicating navigation.
|
||
|
||
- [ ] **Step 6: Document state and AntD boundaries**
|
||
|
||
Include the state matrix:
|
||
|
||
```text
|
||
Loading → Skeleton while shell remains visible
|
||
Success → shared surfaces + existing Table/Charts
|
||
Empty → EmptyState/AntD Empty, never fake numbers
|
||
Error → ApiErrorResult with error code and trace ID
|
||
```
|
||
|
||
State that AntD owns Table sorting/pagination, Alert/Result/Empty/Skeleton, message, and chart internals; Tailwind owns surrounding layout and visual hierarchy. Prohibit global `!important` overrides and business imports in `components/ui`.
|
||
|
||
- [ ] **Step 7: Add the browser acceptance checklist and common pitfalls**
|
||
|
||
Include checks for all four demo roles, default routes, local table overflow, amount masking, red-up/green-down semantics, risk disclaimer, error trace ID, static-nav disclaimer, and the three npm commands. List pitfalls:
|
||
|
||
- do not move API calls into visual components;
|
||
- do not add fallback demo numbers on API errors;
|
||
- do not hard-code a new color or spacing value in a page;
|
||
- do not replace a route Link with a click-only button;
|
||
- do not change `AuthState` or `jinrong.auth` while restyling;
|
||
- do not delete a shared component without checking downstream agent work.
|
||
|
||
- [ ] **Step 8: Verify documentation references real files**
|
||
|
||
Run a source search or manually check every referenced path and command. Then run:
|
||
|
||
```bash
|
||
cd web
|
||
npm run build
|
||
npm run test
|
||
npm run lint
|
||
```
|
||
|
||
Expected: handoff paths match the final tree and all automated checks pass.
|
||
|
||
---
|
||
|
||
## Final self-review checklist
|
||
|
||
- [ ] Every spec section has an implementation task: tokens/architecture (Tasks 1–2), shell and shared states (Task 3), Dashboard composition (Task 4), login and four pages (Task 5), responsive/AntD bridge (Task 6), browser acceptance (Task 7), and handoff documentation (Task 8).
|
||
- [ ] No task changes `api/`, `hooks/`, `utils/`, auth storage, or route contracts.
|
||
- [ ] All component interfaces used by later tasks are defined in the earlier task that produces them.
|
||
- [ ] All new tests include concrete assertions and a targeted command.
|
||
- [ ] No `TODO`, `TBD`, “appropriate handling”, or unspecified implementation placeholder remains.
|
||
- [ ] The plan does not require a git commit unless the user explicitly requests one.
|
||
- [ ] The plan includes the required 149940.00 / 137.08 / -254.40, 12 / 1654850.00, 14 / 10 / 4 and risk-empty regression checks.
|
||
- [ ] The handoff manual is created only after the final component APIs and paths are stable.
|
||
|
||
## Execution handoff
|
||
|
||
Plan complete and saved to `docs/superpowers/plans/2026-09-09-frontend-modernization.md`. Two execution options:
|
||
|
||
1. **Subagent-Driven (recommended)** — dispatch a fresh subagent per task with review between tasks; useful for the multi-file visual migration while keeping each boundary independently checked.
|
||
2. **Inline Execution** — execute the tasks in this session using the executing-plans skill with checkpoints.
|
||
|
||
Choose one approach before implementation begins.
|