- 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.
42 KiB
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.authlocalStorage 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.tsweb/src/routes/menus.tsxweb/src/App.tsxweb/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
/apiproxy, React plugin, Vitestjsdomenvironment, and existing AntDConfigProvider. -
Step 1: Record the current baseline before dependency changes
Run from D:\项目\JinRong\web:
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:
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:
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:
@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:
@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:
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:
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:
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:
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:
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, andBadgeminimally
Use forwardRef, native button semantics, and class variants. The core class choices must include:
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, andEmptyState
Required markup:
<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:
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:
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:
AppShellwith{ sidebar: ReactNode; topbar: ReactNode; children: ReactNode; collapsed: boolean },BrandSidebarwith role/menu/path/collapse props, andTopbarwith 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
BrandSidebarwith menu behavior isolated from styling
The component should accept:
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
Topbarwith actor copy and logout affordance
The component should accept:
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
AppShelland responsive content structure
AppShell renders a fixed desktop sidebar and a content column:
<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
AppLayoutwithout changing its data flow
Keep these lines conceptually unchanged:
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
PageShelland 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_idwhen 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:
npm run build
npm run test
npm run lint
Then with the dev server running, verify each existing defaultRoute manually:
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:
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
ChartCardandDataSection
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
DashboardHerowhile preserving its props
Keep the existing DashboardHeroProps and the following behavior exactly:
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
QuickActionBarwithout changing links
Keep the exact QuickAction type and map:
{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:
barColorsign mapping;formatValueoutput;ChartBoundarybehavior;DeferredDashboardChartsrequestAnimationFrame 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
DashboardLayoutcomposition
Preserve the current generic props and error branch:
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:
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:
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:
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:
总市值 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()anduseHoldingsDashboard(auth.accessToken, auth.actorId); -
masked,toggleand both eye states; -
exact
Linkdestinations/app/customer/holdings,/app/customer/chat,/app/customer/trades,/app/market,/app/analytics/query; -
exact
alertTextsemantics; -
barValueIsPercent, row keyproduct_id, and empty text暂无持仓. -
Step 4: Migrate
AdvisorClientsDashboard
Preserve:
useAdvisorRosterDashboard(auth.accessToken, auth.actorId);- customer count, total AUM and top customer values;
AmountTextusage and table sort bytotalValuedescending;- 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;
PnlTextsign rendering;- exact links to analytics query/chat and market;
barValueIsPercent, row keyproduct_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);disclaimerconditional 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:
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.tsxif 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
appThemeto the exact brand values
Update web/src/theme.ts while preserving ThemeConfig export and ConfigProvider usage:
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:
# 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:
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:
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:
uvicorn app.main:app --reload
Frontend terminal:
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:
客户 → /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
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:
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:
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:
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:
<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:
- Add or reuse an API function under
src/api. - Keep loading/error/refresh logic in a hook under
src/hooks. - Keep aggregation and formatting in
src/utilswhere applicable. - Compose
PageHeader,DashboardHero,ChartCard,DataSectionandEmptyStatein the page. - Preserve
ApiErrorResult, no-fake-data behavior, and route links. - 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:
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
AuthStateorjinrong.authwhile 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:
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:
- 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.
- Inline Execution — execute the tasks in this session using the executing-plans skill with checkpoints.
Choose one approach before implementation begins.