2026-06-01-FE-02.md 44.3 KB

FE-02 主页与导航框架 — 任务级 TDD 计划(前端)

阶段:前端(frontend)。作用域:frontend/**(页面 / 布局 / 路由 / store / api / 样式 / 测试)。禁止backend/** / sql/** / scripts/**。 上游 SSoT:spec docs/superpowers/specs/2026-06-01-FE-02.md;原型 prototype/erp.html#topbar / #nav-overlay / #screen-main 布局/交互权威,含内联 kpiRows / navSide / navCols demo 数据);技术规范 docs/04-技术规范.md § 零 / § 二;Design Tokens 仓库根 src/styles/tokens.css;登录态来源 FE-01(authSlice + request.ts + token 持久化键 xly_erp_token)。 本计划告诉 TDD 执行者做什么 / 文件边界 / 测试意图 / props 与配置形状 / 完成判据;具体代码由红-绿-提交循环产出,不在此 dump 整组件 / 配置文件内容。 本 FE 是登录后落地页 + 应用外壳(壳层),复用 FE-01 已搭好的工程骨架(package.json / vite / 测试栈 / Redux / Router / api/request.ts / authSlice / tokens 引入)。它不新增任何后端取数(spec § 4 / D1):主页 KPI 看板、角色/流程树、导航分组均为前端静态配置(复刻原型 demo);当前用户身份复用 authSlice.user


Goal(目标)

把 FE-01 留下的 / 占位(HomePlaceholder)替换为真实应用外壳与主页落地页,复刻原型 #topbar + #nav-overlay + #screen-main 的布局与交互语义,登录态守卫与导航编排真实生效,KPI/导航数据为前端静态 demo:

  • 路由壳与守卫<RequireAuth> 包裹布局路由 <AppLayout>;index /<HomePage>,子路由 /usr/users(FE-03 容器)、/usr/users/new/usr/users/:id(FE-04 容器)。未登录进受保护路由 → <Navigate to="/login" replace state={{from}}/>(BR1);token 已存在但 user 未就绪 → Spin 占位(authResolving)。已登录访问 /login → 回主页(BR2,FE-01 § 6.7 已指明此守卫归 FE-02)。
  • 顶栏 TopBar:品牌 Logo(鹿角 SVG,点击回 /)、「全部导航」汉堡按钮(切 overlay,navOverlayOpen 时高亮)、固定「主页」标签(不可关)+ 动态业务标签栈、右侧搜索/通知图标(占位)、当前用户区 sUserName(sUserType) + 下拉「退出登录」、更多「⋯」占位。
  • 标签栈(BR4/BR5/BR6,复刻 tabsOpen/openTab/.close):本地受控态。「主页」恒在最左不可关;打开 FE-03 → 追加「用户列表」;打开 FE-04 → 先确保「用户列表」存在再追加「用户信息单据」;关「用户列表」联动关「用户信息单据」并回主页;关「用户信息单据」回「用户列表」。标签激活与当前路由同步(点标签 = navigate)。
  • 全部导航总览 NavOverlay(BR7,复刻 #nav-overlay):覆盖内容区深色浮层,左列 20 个一级模块(navSide,「系统设置」默认 active)、右侧 7 列分组(navCols)。仅「用户列表」有真实路由(点击 → 关 overlay + navigate('/usr/users'));其余占位项点击关 overlay + message.info('功能开发中');「用户列表」「系统功能模块设置」带 ★。点遮罩 / Esc 关闭。
  • 主页 HomePage(BR11,复刻 #screen-main):KpiHeadBar(标题 + 今日未处理 37428 红 / 未清总数 56433 蓝 + AI 助手占位按钮)+ DashboardThreeCol(左 280px 角色/流程树 + 右 KPI 合并网格)+ CommonOps(常用操作:用户列表 → /usr/users;系统功能模块设置 → 占位)+ AppFooter 页脚。数据全部来自 dashboardData.ts 静态 demo(D1/D2)。
  • 退出登录(BR9):下拉「退出登录」→ dispatch(clearCredentials())(自动清 localStorage token)→ message.success('已退出登录')navigate('/login', { replace:true })
  • 被动 401(BR10):request.ts 响应拦截器对 401 触发统一登出回调(清登录态 + message.warning('登录已失效,请重新登录') + 跳 /login),由外壳在挂载时注册回调(拦截器内无法用 React hooks,见 D11)。
  • 状态机 ≥5 态(spec § 3):authResolving / unauthenticated / ready / navOverlayOpen / tabOpen / empty / error 均有测试固化。
  • 语义色只用 var(--color-*);顶栏 / 导航 overlay 深色底为外壳局部装饰,scoped 保留,不新增全局 token、不挪用语义 token(spec § 7 / D9)。

Architecture(架构 / 分层)

遵循 docs/04 § 2.1,落点全在 frontend/**新增/改动文件:

frontend/
├── src/
│   ├── router/index.tsx                                  # 【改】替换 HomePlaceholder:嵌套路由 RequireAuth>AppLayout>{index HomePage, /usr/users, /usr/users/new, /usr/users/:id} + /login 包 RedirectIfAuthed + ErrorBoundary + 未匹配重定向 /
│   ├── router/RequireAuth.tsx                            # 【新增】受保护区守卫:读 authSlice.token/user → ready / authResolving(Spin) / unauthenticated(Navigate to /login,state.from)
│   ├── router/RedirectIfAuthed.tsx                       # 【新增】/login 守卫:已有有效登录态 → Navigate 回 from 或 /(BR2)
│   ├── router/AppErrorBoundary.tsx                       # 【新增】路由级 ErrorBoundary:子路由渲染抛错兜底「页面出错,请刷新或返回主页」+ 返回主页入口(spec § 3 error / D7)
│   ├── layouts/AppLayout/AppLayout.tsx                   # 【新增】应用外壳:TopBar + NavOverlay + <Outlet/> + AppFooter;持有标签栈 / overlay 开关本地态(D3)
│   ├── layouts/AppLayout/TopBar.tsx                      # 【新增】顶栏:Logo + 导航按钮 + 标签条 + 右侧用户区(接收标签栈/overlay/用户 props)
│   ├── layouts/AppLayout/NavOverlay.tsx                  # 【新增】全部导航总览浮层(受控 open,左 navSide 列 + 右 navCols 网格;onNavigate / onClose)
│   ├── layouts/AppLayout/CurrentUserMenu.tsx             # 【新增】当前用户 Dropdown(展示 sUserName(sUserType) + 退出登录)
│   ├── layouts/AppLayout/AppFooter.tsx                   # 【新增】页脚版权/经营范围/备案号(复刻原型 footer.foot)
│   ├── layouts/AppLayout/useTabStack.ts                  # 【新增】标签栈 hook(openTab/closeTab/activeKey 逻辑,BR4/5/6,复刻 tabsOpen/openTab/.close)
│   ├── layouts/AppLayout/navConfig.ts                    # 【新增】静态导航配置:NAV_SIDE(20 项)+ NAV_COLS(7 列,复刻 navSide/navCols;标注 routePath / star)
│   ├── layouts/AppLayout/AppLayout.module.css            # 【新增】外壳 scoped 样式:语义色用 var(--color-*);顶栏/overlay 深色底为局部装饰(D9)
│   ├── pages/home/HomePage/HomePage.tsx                  # 【新增】主页落地页根(组合 KpiHeadBar + DashboardThreeCol + CommonOps)
│   ├── pages/home/HomePage/KpiHeadBar.tsx                # 【新增】KPI 头条(标题 + 今日未处理/未清总数统计 + AI 占位按钮)
│   ├── pages/home/HomePage/RoleProcessTree.tsx          # 【新增】左侧角色/流程树(按角色/按流程分组 + 计数;点击高亮,不取数)
│   ├── pages/home/HomePage/KpiBoard.tsx                  # 【新增】KPI 合并网格(导航类型/角色/子流程列跨行合并;空数据 Empty,BR11/D5)
│   ├── pages/home/HomePage/CommonOps.tsx                 # 【新增】常用操作卡(用户列表 → 路由;系统功能模块设置 → 占位)
│   ├── pages/home/HomePage/dashboardData.ts             # 【新增】静态 demo 数据:KPI_STATS / ROLE_GROUPS / PROCESS_GROUPS / KPI_ROWS(复刻原型 kpiRows,D1/D2)
│   └── pages/home/HomePage/HomePage.module.css          # 【新增】主页 scoped 样式(语义色用 var(--color-*);网格线/底色经 token)
├── src/api/request.ts                                    # 【改】响应拦截器加 401 处理:触发已注册的 onUnauthorized 回调(D11,集中常量 HTTP_UNAUTHORIZED=401)
└── tests/
    ├── unit/RequireAuth.test.tsx                         # 【新增】守卫态:ready / authResolving / unauthenticated
    ├── unit/RedirectIfAuthed.test.tsx                    # 【新增】已登录访问 /login 回主页(BR2)
    ├── unit/useTabStack.test.ts(x)                       # 【新增】标签栈 BR4/5/6 联动
    ├── unit/AppLayout.topbar.test.tsx                    # 【新增】顶栏结构 + 当前用户文案 + 退出登录(BR3/BR9)
    ├── unit/NavOverlay.test.tsx                          # 【新增】overlay 开关 + 分组渲染 + 路由项/占位项点击(BR7/BR8)
    ├── unit/AppErrorBoundary.test.tsx                    # 【新增】子组件抛错兜底
    ├── unit/HomePage.test.tsx                            # 【新增】主页区域结构 + 统计文案 + 常用操作跳转(BR8/BR11)
    ├── unit/KpiBoard.test.tsx                            # 【新增】KPI 网格表头/行渲染 + 空数据 Empty(BR11/empty 态)
    ├── unit/request.unauthorized.test.ts                # 【新增】401 触发 onUnauthorized 回调(BR10)
    ├── unit/renderShell.tsx                              # 【新增】外壳/路由测试共享渲染工具(Provider + 真实 store + MemoryRouter + AntD App)
    └── e2e/shell.spec.ts                                 # 【新增】E2E 关键旅程:登录后落地主页 / 导航 overlay / 打开关闭用户列表标签 / 退出登录
  • 跨阶段/跨模块:本 FE 落点全在 frontend/**,不触 backend/ / sql/ / scripts/。改动 src/router/index.tsxsrc/api/request.ts 属 FE-01 搭建的全前端共享骨架(非 FE-02 私有),在《模块完成报告》留痕「FE-02 将 / 占位替换为应用外壳 + 受保护路由守卫;为 BR10 给 request.ts 增 401 统一登出回调;改动属共享骨架,FE-03/FE-04 复用」。
  • 状态管理(docs/04 § 2.2 / D3):标签栈(已打开集合 + activeKey)与 overlay 开关为外壳局部 UI 态,用 AppLayoutuseState / useTabStack 本地管理,不进 Redux;登录态(token/user)复用 Redux authSlice
  • 请求封装 / 错误处理(docs/04 § 2.3/2.4):本壳不新增业务取数(D1);仅扩展 request.ts 401 统一处理(D11),与守卫 unauthenticated 态协同。
  • Design Tokens(docs/04 § 2.1 / spec § 7):语义色(主操作/文字/边框/错误/成功/表头/行)只用 var(--color-*);顶栏 #1f1f23、overlay #2b3137 等品牌深色底为外壳局部装饰,scoped 在 *.module.css,不新增全局 token、不挪用语义 token(D9,与 FE-01 § 7 D7 一致)。

Tech Stack(技术栈,源自 docs/04 § 零 + FE-01 骨架)

  • React 18 / Ant Design 5(Tabs / Dropdown / Drawer或受控 div / Tree或列表 / Table或 CSS Grid / Empty / Spin / message)/ Redux Toolkit / React Router v6(Outlet / Navigate / useNavigate / useLocation / useMatchmatchPath)/ Vite / Axios / TypeScript;@ant-design/icons
  • 测试:单测 Vitest(jsdom)+ @testing-library/react|jest-dom|user-event;E2E Playwright。沿用 FE-01 tests/setup.tsvite.config.ts 配置。
  • 命令(docs/04 § 零):build npm run build;lint npm run lint;unit npm run test:unit;e2e npm run test:e2e。子会话验证用 cd frontend && npm run test:unit -- <文件名片段>
  • 提交格式:<type>(<scope>): <subject> REQ-USR-XXX。本 FE 无单一 CRUD REQ,是 USR 子功能的导航/承载容器;scope 统一用 fe-shell;subject REQ 后缀按导向的子功能标注(导航壳/路由/守卫/标签栈相关用 REQ-USR-003=其承载的首要入口「用户列表」;用户身份展示/退出相关用 REQ-USR-004=登录态来源)。每个任务在其 commit 行注明所用 REQ tag。

合同级常量(跨 task 必须一致)

  • 路由 path(React Router v6):
    • /login(FE-01 登录页,放行;包 RedirectIfAuthed,不包 AppLayout)。
    • /(index,受保护,→ HomePage)。
    • /usr/users(受保护,FE-03 容器;本 FE 仅提供导航入口与标签挂载位,目标内容属 FE-03)。
    • /usr/users/new(受保护,FE-04 新增容器)。
    • /usr/users/:id(受保护,FE-04 修改容器)。
    • 未匹配:受保护区内 → Navigate to="/";整体未匹配兜底 → 经守卫落到 /login(D7)。
  • 标签栈 key(业务标签标识,跨组件一致)'userlist'(标题「用户列表」,路由 /usr/users,可关闭)、'userdetail'(标题「用户信息单据」,路由 /usr/users/new/usr/users/:id,可关闭);固定标签 'home'(标题「主页」,路由 /不可关闭,恒在最左)。
  • localStorage token 键TOKEN_STORAGE_KEY = 'xly_erp_token'(复用 api/request.ts 已导出常量,不写字面量)。
  • HTTP 状态码常量api/request.ts,新增):HTTP_UNAUTHORIZED = 401
  • 静态文案(逐字一致,复刻原型 / spec): | 用途 | 文案 | |---|---| | 全部导航按钮 | 全部导航 | | 主页标签 | 主页 | | 用户列表标签/入口 | 用户列表 | | 用户单据标签 | 用户信息单据 | | KPI 标题 | KPI监控 | | 今日未处理统计前缀 | 今日未处理:(值 37428,红,var(--color-error)) | | 未清总数统计前缀 | 未清总数:(值 56433,蓝,var(--color-primary)) | | AI 助手按钮 | 小ai同学,请帮我安排今日工作 | | 常用操作卡标题 | 常用操作 | | 常用操作项 | 用户列表 / 系统功能模块设置(后者占位) | | KPI 网格表头 7 列 | 导航类型 / 角色 / KPI待处理事项(当前行双击进入) / KPI内容描述及处理结果(点击蓝色查看明细) / 今日未处理 / 未清总数 / 子流程 | | 退出登录菜单项 | 退出登录 | | 退出登录成功提示 | 已退出登录message.successvar(--color-success)) | | 被动 401 提示 | 登录已失效,请重新登录message.warning,BR10) | | 导航占位项点击提示 | 功能开发中message.info,BR7/D4) | | 角色树分组 | 按角色 / 按流程 | | 空数据占位 | AntD Empty 默认「暂无数据」(KPI 网格/角色树空时,empty 态) | | 路由级错误兜底 | 页面出错,请刷新或返回主页 + 「返回主页」入口(D7) | | 页脚正文 | 复刻原型 footer.foot:©Copyright Antler Software | 印刷智慧工厂 | 印刷MES | 印刷ERP | 印刷电商平台 | 文件智能处理 | 印前自动化 | 400-880-6237 + 备案号 沪ICP备14034791号-1 | | 当前用户区文案规则 | `${sUserName}(${sUserType})`(D10;user 缺失时退化为占位用户名,见 BR3) |
  • 当前用户显示规则(BR3 / D10):拼 `${user.sUserName}(${user.sUserType})`sUserType 已是中文「超级管理员/普通用户」,不再映射);user 为 null 时退化展示占位(如 未登录用户用户,TDD 期定,登记于本计划占位常量并保持一处定义)。

关键签名(首次出现处给出,跨 task 一致)

  • 路由守卫router/):
    • RequireAuth{ children?: ReactNode } 或用于 <Route element={<RequireAuth/>}> 的布局守卫,内部渲染 <Outlet/>children):读 useAppSelector(s => s.auth)token 为 null → <Navigate to="/login" replace state={{ from: location.pathname }} />(BR1);token 存在但 user 为 null → 渲染 Spin 占位(authResolving,data-testid 可选 auth-resolving);否则放行(ready)。
    • RedirectIfAuthed(包 /login{ children: ReactNode }):tokenuser 均就绪(或仅 token 即视为已登录,TDD 期定,登记一处)→ <Navigate to={from ?? '/'} replace />(BR2,fromlocation.state.from);否则渲染 children(LoginPage)。
    • AppErrorBoundary(React class ErrorBoundary,{ children: ReactNode }):componentDidCatch 后渲染兜底 UI + 「返回主页」按钮(window.location 或 navigate /)。
  • 应用外壳layouts/AppLayout/):
    • AppLayout(default export,无 props,布局路由组件):内部 useTabStack() + useState(navOverlayOpen) + useAppSelector(authSlice.user) + useNavigate + useLocation;渲染 TopBar / NavOverlay / <Outlet/> / AppFooter;挂载时注册 request.tsonUnauthorized(D11)。
    • useTabStack(){ tabs: TabItem[]; activeKey: string; openTab(key: 'userlist'|'userdetail'): void; closeTab(key: string): void; setActive(key: string): void },其中 TabItem = { key: string; title: string; closable: boolean; routePath: string }。逻辑(BR4/5/6):恒含 home(不可关、最左);openTab('userlist') 确保 userlist 在;openTab('userdetail') 确保 userlist+userdetail 在;closeTab('userlist') 同时移除 userdetail 并激活 home;closeTab('userdetail') 激活 userlist。该 hook 只管标签集合与 activeKey,路由跳转由调用方据 routePath 执行(保持 hook 纯净可测)。
    • TopBar{ user: AuthUser | null; tabs: TabItem[]; activeKey: string; navOverlayOpen: boolean; onToggleNav(): void; onSelectTab(key): void; onCloseTab(key): void; onLogout(): void; onLogoHome(): void })。
    • NavOverlay{ open: boolean; onClose(): void; onNavigate(routePath: string): void; onPlaceholder(): void }):渲染 NAV_SIDE 左列 + NAV_COLS 右网格;叶子项有 routePathonNavigate,否则 → onPlaceholder;点遮罩/Esc → onClose
    • CurrentUserMenu{ user: AuthUser | null; onLogout(): void }):AntD Dropdown,menu 项「退出登录」→ onLogout
    • AppFooter(无 props):静态文案条。
  • 导航静态配置layouts/AppLayout/navConfig.ts):
    • NAV_SIDE: { key: string; label: string; active?: boolean }[](20 项,复刻 navSide 的 label 与「系统设置」active;图标可省或用占位)。
    • NAV_COLS: { title: string; items: NavLeaf[] }[]NavLeaf = { label: string; routePath?: string; star?: boolean }(复刻 navCols;仅「用户列表」routePath:'/usr/users';「用户列表」「系统功能模块设置」star:true;其余 routePath 省略=占位)。
  • 主页静态数据pages/home/HomePage/dashboardData.ts):
    • KPI_STATS = { todayPending: 37428; openTotal: 56433 }(复刻原型 .kpi-head,D2)。
    • ROLE_GROUPS: { label: string; count: number }[]PROCESS_GROUPS: { label: string; count: number }[](复刻原型「按角色」「按流程」条目与计数,D2)。
    • KPI_ROWS: KpiRow[]KpiRow = { role: string | null; item: string; desc: string; today: string; total: string; sub?: string; red?: boolean; navTypeFirst?: boolean; roleSpan?: number; subSpan?: number }字段名与原型 kpiRows 一致,复刻全量行,D2)。
  • 主页组件 props
    • KpiHeadBar{ stats: typeof KPI_STATS });RoleProcessTree{ roleGroups; processGroups; onSelect?(label) },本地高亮态);KpiBoard{ rows: KpiRow[] },空数组 → Empty);CommonOps{ onOpenUserList(): void })。
  • request.ts 401 钩子api/request.ts,新增导出):
    • HTTP_UNAUTHORIZED = 401 常量。
    • registerUnauthorizedHandler(fn: () => void): void(模块级单例,外壳挂载时注册;响应拦截器捕获 HTTP 401 时调用该回调再抛 ApiError)。onUnauthorized 回调内容由外壳提供:clearCredentials + message.warning('登录已失效,请重新登录') + 跳 /login(BR10)。

测试栈说明

  • jsdom 组件 / hook / store 单测(Vitest + RTL):默认用真实 storeconfigureStore)+ MemoryRouterinitialEntries 指定路由)+ AntD App 上下文(经 renderShell.tsx)。守卫/标签栈/overlay/主页交互均在组件层断言;不依赖真实后端(本壳无取数)。useTabStack 纯逻辑可用 renderHook 直测。
  • Playwright E2Epage.route**/api/usr/login**/api/usr/users(FE-03 取数桩,仅为标签可挂载,不验列表内容),覆盖:登录→落地主页(顶栏可见、KPI 标题可见);点「全部导航」→ overlay 显隐;从常用操作/导航打开「用户列表」标签并关闭(联动回主页);退出登录回 /login。不依赖真实后端起服。
  • 可测性:优先用语义查询(role/text/label);仅当 RTL/Playwright 无法稳定定位时添加最小 data-testid(如 nav-overlay / auth-resolving / tab-userlist)。

任务列表(每个 task = red → green → 子会话验证 → commit)

硬护栏:以下每个 impl_file / test_file 均以 frontend/ 开头;无任何 backend/ / sql/ / scripts/ 落点。 提交 scope 统一 fe-shell;REQ tag 按任务承载的子功能标注(见上「合同级常量」末段规则)。

T0 — 外壳/路由测试共享渲染工具(chore,先建测试地基)

  • 测试先行类型:jsdom 组件测试(自身即一个最小冒烟用例)
  • 1. 写失败测试frontend/tests/unit/renderShell.tsx 自带最小冒烟 frontend/tests/unit/renderShell.smoke.test.tsx::renderShell mounts a route element——用 renderShell(<div>shell-ok</div>, { initialEntries:['/'], preloadedAuth:{ token:'t', user:{...} } }) 渲染并断言 shell-ok 在文档中;初始因工具未实现而失败。
  • 2. 实现最小代码frontend/tests/unit/renderShell.tsx——导出 makeShellStore(preloadedAuth?)configureStoreauth,可注入 token/user)与 renderShell(ui, { initialEntries?, preloadedAuth?, store? })(Provider + 真实 store + ConfigProvider + AntD App + MemoryRouter)。复用 FE-01 renderLogin.tsx 模式。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- renderShell
  • 4. committest(fe-shell): 外壳/路由测试共享渲染工具 REQ-USR-003

T1 — RequireAuth 守卫三态(authResolving / unauthenticated / ready,BR1)(jsdom 组件测)

  • 测试先行类型:jsdom 组件测试
  • 1. 写失败测试frontend/tests/unit/RequireAuth.test.tsx
    • ::redirects to /login when no token——无 token 时进 /,断言落到 /login 渲染(用一条 /login 哨兵路由 + 断言哨兵文本/URL),且携带 state.from(可用哨兵读 useLocation().state)。
    • ::renders Spin placeholder when token present but user not resolved——preloadedAuth:{ token:'t', user:null },断言出现加载占位(auth-resolving / Spin),不渲染受保护内容。
    • ::renders protected content when token and user ready——token + user 就绪,断言放行渲染 <Outlet/> 子内容(哨兵)。
  • 2. 实现最小代码frontend/src/router/RequireAuth.tsx(签名见关键签名;用 useLocationfrom)。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- RequireAuth
  • 4. commitfeat(fe-shell): 受保护路由守卫 RequireAuth 三态 REQ-USR-004

T2 — RedirectIfAuthed:已登录访问 /login 回主页(BR2)(jsdom 组件测)

  • 测试先行类型:jsdom 组件测试
  • 1. 写失败测试frontend/tests/unit/RedirectIfAuthed.test.tsx
    • ::renders children when unauthenticated——无登录态,进 /login,渲染 children(哨兵「login-screen」)。
    • ::redirects to / when already authenticated——token+user 就绪进 /login,断言重定向到 /(哨兵)。
    • ::redirects to from when present——state.from='/usr/users' 且已登录 → 重定向到 /usr/users
  • 2. 实现最小代码frontend/src/router/RedirectIfAuthed.tsx(签名见关键签名)。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- RedirectIfAuthed
  • 4. commitfeat(fe-shell): 已登录访问登录页重定向守卫 REQ-USR-004

T3 — useTabStack 标签栈逻辑(BR4/BR5/BR6)(jsdom hook 测)

  • 测试先行类型:jsdom 组件测试(renderHook
  • 1. 写失败测试frontend/tests/unit/useTabStack.test.tsx
    • ::starts with fixed home tab only (closable false, leftmost)——初始 tabshomeactiveKey==='home',home closable===false
    • ::openTab userlist appends userlist and activates it——openTab('userlist') 后 tabs 含 home+userlist,activeKey==='userlist',userlist closable===trueroutePath==='/usr/users'(BR4)。
    • ::openTab userdetail ensures userlist exists then appends userdetail——直接 openTab('userdetail'),tabs 含 home+userlist+userdetail,activeKey==='userdetail'(BR6)。
    • ::closeTab userlist also removes userdetail and activates home——先开 userlist+userdetail,closeTab('userlist') → tabs 仅 home,activeKey==='home'(BR5)。
    • ::closeTab userdetail activates userlist——开 userlist+userdetail,closeTab('userdetail') → tabs 含 home+userlist,activeKey==='userlist'
    • ::open existing tab does not duplicate——重复 openTab('userlist') 不产生重复项。
  • 2. 实现最小代码frontend/src/layouts/AppLayout/useTabStack.ts(签名 / TabItem 见关键签名;标签 key/title/routePath 用合同级常量)。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- useTabStack
  • 4. commitfeat(fe-shell): 顶栏标签栈联动逻辑 useTabStack REQ-USR-003

T4 — navConfig 与 dashboardData 静态配置(D1/D2/D4)(jsdom 单测)

  • 测试先行类型:jsdom 组件测试(纯数据断言)
  • 1. 写失败测试
    • frontend/tests/unit/navConfig.test.tsNAV_SIDE 长度为 20 且含「系统设置」active:trueNAV_COLS 含 7 组、组标题为 期初设置/用户管理/系统参数/计算方案/日志/开发平台/API对接管理;「用户管理」组内「用户列表」routePath==='/usr/users'star===true;「开发平台」组内「系统功能模块设置」star===true 且无 routePath(占位,BR7/D4)。
    • frontend/tests/unit/dashboardData.test.tsKPI_STATS.todayPending===37428openTotal===56433(D2);KPI_ROWS 首行 role==='核价人员'navTypeFirst===trueroleSpan===4sub==='估价管理流程'subSpan===5(复刻原型 kpiRows 字段,D2);ROLE_GROUPS 含「所有部门」计数 37428、「客服部」计数 30127。
  • 2. 实现最小代码frontend/src/layouts/AppLayout/navConfig.tsNAV_SIDE / NAV_COLS)+ frontend/src/pages/home/HomePage/dashboardData.tsKPI_STATS / ROLE_GROUPS / PROCESS_GROUPS / KPI_ROWS),全量复刻原型内联数据(D1/D2)。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- navConfig dashboardData
  • 4. commitfeat(fe-shell): 导航与主页 KPI 静态配置数据 REQ-USR-003

T5 — KpiBoard KPI 合并网格 + 空数据(BR11 / empty 态 / D5)(jsdom 组件测)

  • 测试先行类型:jsdom 组件测试
  • 1. 写失败测试frontend/tests/unit/KpiBoard.test.tsx
    • ::renders 7 column headers——渲染 7 个表头文案(合同级常量逐字一致)。
    • ::renders all kpi rows with item/desc/today/total——传 KPI_ROWS,断言可见某几行 item(如「01/04【新增】新报价单」)与红色统计数(red 行数字用 var(--color-error))。
    • ::renders Empty when rows is empty——传 rows={[]},渲染 AntD Empty(empty 态,BR11)。
    • ::KPI item/desc rendered as link-styled text without navigation——「KPI待处理事项/内容描述」为蓝色链接样式但点击不发生路由跳转(纯展示,BR11)。
  • 2. 实现最小代码frontend/src/pages/home/HomePage/KpiBoard.tsx(AntD Table rowSpan 合并「导航类型/角色/子流程」列,或 CSS Grid gridRow span 复刻原型——TDD 期二选一,登记 D5;空数组 → Empty)+ HomePage.module.css 网格样式(线/底色用 var(--color-border) / var(--color-table-header-bg) 等 token)。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- KpiBoard
  • 4. commitfeat(fe-shell): 主页 KPI 合并网格与空数据态 REQ-USR-003

T6 — HomePage 落地页区域组合(KPI 头条 / 角色树 / 常用操作 / 页脚,BR8/BR11)(jsdom 组件测)

  • 测试先行类型:jsdom 组件测试
  • 1. 写失败测试frontend/tests/unit/HomePage.test.tsxrenderShellinitialEntries:['/']):
    • ::renders KPI head with title and stats——可见「KPI监控」、「今日未处理:」+ 37428、「未清总数:」+ 56433、AI 按钮文案「小ai同学,请帮我安排今日工作」。
    • ::renders role/process tree groups——可见「按角色」「按流程」分组及条目(如「所有部门 (37428)」「客服部 (30127)」)。
    • ::tree item click highlights without navigation——点击角色条目后该项高亮(active),不触发路由跳转/取数(BR11)。
    • ::common ops user-list click navigates to /usr/users and opens tab——点「常用操作 > 用户列表」触发 navigate('/usr/users')(断言哨兵/URL 改变或 onOpenUserList 被调)(BR8)。
    • ::renders footer copyright text——可见页脚版权与备案号文本。
  • 2. 实现最小代码frontend/src/pages/home/HomePage/HomePage.tsx + KpiHeadBar.tsx + RoleProcessTree.tsx(本地高亮 useState)+ CommonOps.tsx(用户列表 → 调用 onOpenUserList / navigate)+ pages/home/HomePage/AppFooter 复用外壳 AppFooter(页脚归外壳;主页内 .footAppLayout 渲染——若主页自带页脚则置于 HomePage,TDD 期定一处,登记);HomePage.module.css。语义色用 token,统计红/蓝用 var(--color-error)/var(--color-primary)
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- HomePage
  • 4. commitfeat(fe-shell): 主页落地页区域组合 REQ-USR-003

T7 — NavOverlay 全部导航总览(开关 / 分组渲染 / 路由项与占位项,BR7/BR8/D4)(jsdom 组件测)

  • 测试先行类型:jsdom 组件测试
  • 1. 写失败测试frontend/tests/unit/NavOverlay.test.tsx
    • ::hidden when open is false / visible when true——open=false 不渲染网格内容;open=true 渲染左列 20 项 + 7 组标题。
    • ::side has 系统设置 active——左列「系统设置」处激活态。
    • ::clicking 用户列表 calls onNavigate('/usr/users')——点右网格「用户列表」叶子 → onNavigate 收到 /usr/users(BR8)。
    • ::clicking placeholder leaf calls onPlaceholder (no navigate)——点占位叶子(如「系统权限」)→ onPlaceholder 被调、onNavigate 未调(BR7/D4)。
    • ::Esc / mask click calls onClose——按 Esc 或点遮罩 → onClose 被调。
  • 2. 实现最小代码frontend/src/layouts/AppLayout/NavOverlay.tsx(受控 open;左列渲染 NAV_SIDE、右网格渲染 NAV_COLS;叶子据 routePath 分流 onNavigate/onPlaceholder;Esc/遮罩 onClose)+ overlay 深色底 scoped 样式(D9)。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- NavOverlay
  • 4. commitfeat(fe-shell): 全部导航总览浮层 NavOverlay REQ-USR-003

T8 — TopBar 顶栏结构 + 当前用户 + 退出登录(BR3/BR9)(jsdom 组件测)

  • 测试先行类型:jsdom 组件测试
  • 1. 写失败测试frontend/tests/unit/AppLayout.topbar.test.tsx
    • ::renders brand logo / 全部导航 button / 主页 tab——可见 Logo、「全部导航」按钮、固定「主页」标签(无关闭按钮)。
    • ::renders current user as sUserName(sUserType)——user:{ sUserName:'朱子纯', sUserType:'超级管理员', ... } → 可见「朱子纯(超级管理员)」(BR3/D10)。
    • ::user fallback when user is null——user:null → 退化占位用户名(合同级占位常量),不崩溃。
    • ::logout menu dispatches clearCredentials, shows success, navigates /login——展开当前用户下拉点「退出登录」→ store auth.token===nulllocalStorage token 被移除、message.success('已退出登录')、URL/哨兵到 /login(BR9)。
    • ::nav toggle button highlights when navOverlayOpen——navOverlayOpen=true 时「全部导航」按钮带激活态。
    • ::clicking business tab close calls onCloseTab——点 userlist 标签「✕」→ onCloseTab('userlist') 被调。
  • 2. 实现最小代码frontend/src/layouts/AppLayout/TopBar.tsx + CurrentUserMenu.tsx(签名见关键签名;标签条用 AntD Tabs editable-card 或自定义受控 div,D3——主页 tab closable:false;退出登录 onLogoutdispatch(clearCredentials()) + message.success + navigate('/login',{replace:true}),由 AppLayout 提供回调)。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- AppLayout.topbar
  • 4. commitfeat(fe-shell): 顶栏与当前用户/退出登录 REQ-USR-004

T9 — AppLayout 外壳装配 + 标签↔路由同步(ready / navOverlayOpen / tabOpen 态)(jsdom 组件测)

  • 测试先行类型:jsdom 组件测试
  • 1. 写失败测试frontend/tests/unit/AppLayout.shell.test.tsxrenderShell 已登录,渲染真实 AppLayout + 子哨兵路由):
    • ::renders TopBar + Outlet + Footer when ready——已登录进 /,顶栏 + 主页 Outlet + 页脚同时在(ready 态)。
    • ::toggle 全部导航 opens/closes overlay——点「全部导航」→ overlay 显(navOverlayOpen);再点/遮罩 → 隐。
    • ::nav overlay 用户列表 navigates and opens tab——overlay 点「用户列表」→ overlay 关、URL 到 /usr/users、顶栏出现「用户列表」标签并激活(tabOpen 态,BR8)。
    • ::clicking home tab navigates back to /——多标签下点「主页」标签 → URL 回 /、主页激活。
    • ::active tab syncs with current route——直接进 /usr/users 时「用户列表」标签为激活态(路由→标签同步)。
  • 2. 实现最小代码frontend/src/layouts/AppLayout/AppLayout.tsx(组合 TopBar/NavOverlay/<Outlet/>/AppFooteruseTabStack + navOverlayOpen 本地态;标签点击 → navigate(routePath);据 useLocation 反向同步 activeKey 与已打开标签;overlay 路由项 → 关 overlay + openTab + navigate)+ AppLayout.module.css(顶栏深色底 scoped,D9)。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- AppLayout.shell
  • 4. commitfeat(fe-shell): 应用外壳装配与标签路由同步 REQ-USR-003

T10 — 路由表接线 + ErrorBoundary(替换 / 占位,含 401 协同入口,BR1/BR2/D7)(jsdom 组件测)

  • 测试先行类型:jsdom 组件测试
  • 1. 写失败测试
    • frontend/tests/unit/router.test.tsx(用真实 AppRouter + MemoryRouter,mock 子页面/FE-03/FE-04 为哨兵占位以隔离未实现页):
    • ::unauthenticated / redirects to /login(BR1);::authenticated / renders HomePage shell(落地主页);::authenticated /usr/users renders under AppLayout(受保护子路由在外壳内);::authenticated /login redirects to /(BR2);::unknown protected path redirects to /(D7)。
    • frontend/tests/unit/AppErrorBoundary.test.tsx::renders fallback with 返回主页 when child throws——子组件抛错 → 兜底文案「页面出错,请刷新或返回主页」+「返回主页」入口。
  • 2. 实现最小代码:改 frontend/src/router/index.tsx——嵌套结构 RequireAuth > AppLayout(index HomePage + /usr/users / /usr/users/new / /usr/users/:id 暂用占位/懒加载位,FE-03/FE-04 落地时替换);/loginRedirectIfAuthed;外壳内套 AppErrorBoundary;未匹配 Navigate to="/"(受保护)/经守卫到 /login。新增 frontend/src/router/AppErrorBoundary.tsx子路由目标内容(FE-03/FE-04)不在本 FE 实现,仅留可挂载的占位元素。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- router AppErrorBoundary
  • 4. commitfeat(fe-shell): 受保护嵌套路由表与错误边界 REQ-USR-003

T11 — request.ts 401 统一登出回调(BR10 / D11)(jsdom 单测)

  • 测试先行类型:jsdom 组件测试
  • 1. 写失败测试frontend/tests/unit/request.unauthorized.test.ts(沿用 FE-01 axios-mock-adapter 模式):
    • ::HTTP 401 triggers registered onUnauthorized then rejects ApiError——registerUnauthorizedHandler(spy),mock 适配器对某请求返回 HTTP 401,断言 spy 被调用一次且请求最终 reject 为 ApiError
    • ::no handler registered does not throw——未注册回调时 401 仍 reject ApiError,不抛额外异常。
    • 不破坏 FE-01 既有断言(401 业务体仍映射 ApiError)。
  • 2. 实现最小代码:改 frontend/src/api/request.ts——加 HTTP_UNAUTHORIZED=401 常量、模块级 registerUnauthorizedHandler(fn)onUnauthorized 单例;响应错误拦截器中 error.response?.status===401 时调用回调(若已注册)再走原 ApiError 映射逻辑。不改变 FE-01 既有拆包/网络兜底语义。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- request(含 FE-01 request 既有用例不回归)
  • 4. commitfeat(fe-shell): 401 统一登出回调接入 request 拦截器 REQ-USR-004

T12 — AppLayout 注册 401 登出处理(壳层接线,BR10)(jsdom 组件测)

  • 测试先行类型:jsdom 组件测试
  • 1. 写失败测试frontend/tests/unit/AppLayout.unauthorized.test.tsx
    • ::registers onUnauthorized on mount; invoking it clears auth + warns + navigates /login——渲染已登录 AppLayout,捕获注册的回调(spy registerUnauthorizedHandler),调用之 → store auth.token===nullmessage.warning('登录已失效,请重新登录')、URL 到 /login(BR10)。
  • 2. 实现最小代码:在 AppLayout useEffectregisterUnauthorizedHandler(() => { dispatch(clearCredentials()); message.warning('登录已失效,请重新登录'); navigate('/login',{replace:true}); })(卸载时可清理)。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- AppLayout.unauthorized
  • 4. commitfeat(fe-shell): 外壳注册被动401统一登出 REQ-USR-004

T13 — E2E 外壳关键旅程(Playwright)

  • 测试先行类型:Playwright E2E
  • 1. 写失败测试frontend/tests/e2e/shell.spec.tspage.route**/api/usr/login 成功响应;桩 **/api/usr/users 返回 {code:0,...,data:{records:[],total:0,...}} 仅为标签挂载;不验 FE-03 列表内容):
    • ::login then lands on home with topbar and KPI title——登录成功后 URL 到 /,顶栏「全部导航」与「KPI监控」可见。
    • ::open and close 全部导航 overlay——点「全部导航」overlay 显示 7 组;Esc/遮罩关闭。
    • ::open 用户列表 tab from common ops then close back to home——常用操作点「用户列表」→ 出现「用户列表」标签、URL /usr/users;点标签「✕」→ 回主页、标签移除(BR5/BR8)。
    • ::logout returns to /login——当前用户下拉「退出登录」→ URL 回 /login、可见「已退出登录」提示。
    • ::visiting / unauthenticated redirects to /login——清除 token 后直接访问 / → 跳 /login(BR1)。
  • 2. 实现最小代码:补任何为可测性需要的最小 data-testid(仅 RTL/Playwright 无法稳定定位时);E2E 桩中 FE-03/FE-04 子路由用占位即可(本 FE 不实现其内容)。沿用 FE-01 playwright.config.ts
  • 3. 子会话验证 PASScd frontend && npm run test:e2e -- shell
  • 4. committest(fe-shell): 应用外壳 E2E 关键旅程 REQ-USR-003

T14 — 全量门禁回归 + 收尾(chore)

  • 测试先行类型:无新增测试(全量验证)
  • 1. 写失败测试:无。
  • 2. 实现最小代码:修 lint / build / 类型问题;确认语义色全部 var(--color-*)、无硬编码 hex/rgba(顶栏/overlay 深色装饰 scoped 例外,D9);确认无 TBD/TODO/【人工填写】
  • 3. 子会话验证 PASScd frontend && npm run lint && npm run build && npm run test:unit && npm run test:e2e 全绿。
  • 4. commitchore(fe-shell): FE-02 门禁回归通过 REQ-USR-003

完成判据(Definition of Done)

  1. 登录后落地主页 /,渲染复刻原型 #screen-main 的 KPI 头条 / 角色流程树 / KPI 合并网格 / 常用操作 / 页脚(spec § 2 / § 6.6)。
  2. 应用外壳 AppLayout 渲染顶栏(Logo + 全部导航 + 标签栈 + 当前用户 + 退出)+ 导航总览 overlay + <Outlet/> + 页脚(spec § 2)。
  3. 状态机覆盖并测试固化:authResolving / unauthenticated / ready(T1/T10)、navOverlayOpen(T7/T9)、tabOpen(T3/T9)、empty(T5)、error(T10 ErrorBoundary)(spec § 3)。
  4. 业务/交互规则 BR1~BR11 在组件层 / E2E 有断言:BR1/BR2(T1/T2/T10)、BR3/BR9(T8)、BR4/BR5/BR6(T3/T9)、BR7(T7)、BR8(T6/T7/T9)、BR10(T11/T12)、BR11(T5/T6)(spec § 5)。
  5. 不新增任何后端取数;KPI/导航/角色树为前端静态配置(dashboardData.ts / navConfig.ts),复刻原型 demo(spec § 4 / D1/D2)。
  6. 退出登录纯前端清登录态 + 删 token + 跳 /login(spec § 6.7 / D6);被动 401 经 request.ts 统一登出回调跳 /login(BR10 / D11)。
  7. 标签栈复刻原型 tabsOpen/openTab/.close 联动(主页固定不可关、userlist↔userdetail 父子联动)(spec § 6.4 / BR4-6)。
  8. 语义色只用 var(--color-*),AntD colorPrimary 沿用 FE-01 ConfigProvider 对齐;顶栏/overlay 深色底 scoped 装饰不新增全局 token(spec § 7 / D9)。
  9. 全部落点在 frontend/**,无 backend/ / sql/ / scripts/ 改动;改 router/index.tsxapi/request.ts 属共享骨架,已在《模块完成报告》留痕。
  10. 门禁全绿:npm run lint / npm run build / npm run test:unit / npm run test:e2e(docs/04 § 零)。

自审记录

  • 占位符扫描:本计划无 【人工填写:】 / TBD / TODO 占位(正文中 TBD/TODO 仅作为「禁止出现的字样」被引用,非真实占位)。
  • spec coverage:spec § 1 关联 REQ/原型→架构 + 全任务;§ 2 组件树→T5(KpiBoard)/T6(HomePage)/T7(NavOverlay)/T8(TopBar)/T9(AppLayout)/T10(路由);§ 3 状态机→T1/T10(auth 三态+error)/T7/T9(navOverlayOpen)/T3/T9(tabOpen)/T5(empty);§ 4 端点关系(仅承接守卫/跳转,不取数)→T1/T2/T10/T11/T12;§ 5 BR1-BR11→见 DoD 第 4 条逐条映射;§ 6 路由与交互→T1/T2/T3/T7/T8/T9/T10;§ 7 tokens→T5/T7/T8/T9/T14;§ 8 decisions D1-D10 已落实于架构/合同级常量/任务,本计划新增 D11(401 钩子机制)见下。
  • 本计划新增决策 D11(401 统一登出机制)request.ts 拦截器内无法使用 React hooks(useNavigate/message),故采用「模块级 registerUnauthorizedHandler 注册回调」模式:AppLayout 挂载时注册含 clearCredentials + message.warning + navigate('/login') 的回调,拦截器捕获 HTTP 401 时调用之(spec BR10 / docs/04 § 2.4)。这是对 FE-01 共享 request.ts 的最小非破坏性扩展(不改既有拆包/网络兜底语义),并在《模块完成报告》留痕。置信度 high,登记进 decisions[]。
  • 类型一致性:标签 key(home/userlist/userdetail)、TabItem、路由 path(/ /login /usr/users /usr/users/new /usr/users/:id)、NAV_SIDE/NAV_COLS/NavLeafKPI_STATS/KPI_ROWS/KpiRow(字段名对齐原型 kpiRows)、AuthUser(复用 FE-01 api/types.ts)、TOKEN_STORAGE_KEY/HTTP_UNAUTHORIZED、各文案常量跨 T0-T14 一致;当前用户文案规则 `${sUserName}(${sUserType})` 与 BR3/D10 一致。
  • 作用域自审:所有 impl_file / test_file 均以 frontend/ 开头;无 backend/ / sql/ / scripts/ 落点。