FE-02 主页与导航框架 — 任务级 TDD 计划(前端)
阶段:前端(frontend)。作用域:
frontend/**(页面 / 布局 / 路由 / store / api / 样式 / 测试)。禁止写backend/**/sql/**/scripts/**。 上游 SSoT:specdocs/superpowers/specs/2026-06-01-FE-02.md;原型prototype/erp.html(#topbar/#nav-overlay/#screen-main布局/交互权威,含内联kpiRows/navSide/navColsdemo 数据);技术规范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.tsx与src/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 态,用
AppLayout内useState/useTabStack本地管理,不进 Redux;登录态(token/user)复用 ReduxauthSlice。 -
请求封装 / 错误处理(docs/04 § 2.3/2.4):本壳不新增业务取数(D1);仅扩展
request.ts401 统一处理(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/useMatch或matchPath)/ Vite / Axios / TypeScript;@ant-design/icons。 - 测试:单测 Vitest(jsdom)+
@testing-library/react|jest-dom|user-event;E2E Playwright。沿用 FE-01tests/setup.ts、vite.config.ts配置。 - 命令(docs/04 § 零):build
npm run build;lintnpm run lint;unitnpm run test:unit;e2enpm 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.success,var(--color-success)) | | 被动 401 提示 |登录已失效,请重新登录(message.warning,BR10) | | 导航占位项点击提示 |功能开发中(message.info,BR7/D4) | | 角色树分组 |按角色/按流程| | 空数据占位 | AntDEmpty默认「暂无数据」(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 }):token与user均就绪(或仅 token 即视为已登录,TDD 期定,登记一处)→<Navigate to={from ?? '/'} replace />(BR2,from取location.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.ts的onUnauthorized(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右网格;叶子项有routePath→onNavigate,否则 →onPlaceholder;点遮罩/Esc →onClose。 -
CurrentUserMenu({ user: AuthUser | null; onLogout(): void }):AntDDropdown,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):默认用真实
store(configureStore)+MemoryRouter(initialEntries指定路由)+ AntDApp上下文(经renderShell.tsx)。守卫/标签栈/overlay/主页交互均在组件层断言;不依赖真实后端(本壳无取数)。useTabStack纯逻辑可用renderHook直测。 -
Playwright E2E:
page.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?)(configureStore挂auth,可注入token/user)与renderShell(ui, { initialEntries?, preloadedAuth?, store? })(Provider + 真实 store +ConfigProvider+ AntDApp+MemoryRouter)。复用 FE-01renderLogin.tsx模式。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit -- renderShell - 4. commit:
test(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(签名见关键签名;用useLocation取from)。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit -- RequireAuth - 4. commit:
feat(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. 子会话验证 PASS:
cd frontend && npm run test:unit -- RedirectIfAuthed - 4. commit:
feat(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)——初始tabs仅home,activeKey==='home',homeclosable===false。 -
::openTab userlist appends userlist and activates it——openTab('userlist')后 tabs 含 home+userlist,activeKey==='userlist',userlistclosable===true、routePath==='/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. 子会话验证 PASS:
cd frontend && npm run test:unit -- useTabStack - 4. commit:
feat(fe-shell): 顶栏标签栈联动逻辑 useTabStack REQ-USR-003
T4 — navConfig 与 dashboardData 静态配置(D1/D2/D4)(jsdom 单测)
- 测试先行类型:jsdom 组件测试(纯数据断言)
- 1. 写失败测试:
-
frontend/tests/unit/navConfig.test.ts:NAV_SIDE长度为 20 且含「系统设置」active:true;NAV_COLS含 7 组、组标题为期初设置/用户管理/系统参数/计算方案/日志/开发平台/API对接管理;「用户管理」组内「用户列表」routePath==='/usr/users'且star===true;「开发平台」组内「系统功能模块设置」star===true且无routePath(占位,BR7/D4)。 -
frontend/tests/unit/dashboardData.test.ts:KPI_STATS.todayPending===37428、openTotal===56433(D2);KPI_ROWS首行role==='核价人员'、navTypeFirst===true、roleSpan===4、sub==='估价管理流程'、subSpan===5(复刻原型 kpiRows 字段,D2);ROLE_GROUPS含「所有部门」计数 37428、「客服部」计数 30127。
-
- 2. 实现最小代码:
frontend/src/layouts/AppLayout/navConfig.ts(NAV_SIDE/NAV_COLS)+frontend/src/pages/home/HomePage/dashboardData.ts(KPI_STATS/ROLE_GROUPS/PROCESS_GROUPS/KPI_ROWS),全量复刻原型内联数据(D1/D2)。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit -- navConfig dashboardData - 4. commit:
feat(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={[]},渲染 AntDEmpty(empty 态,BR11)。 -
::KPI item/desc rendered as link-styled text without navigation——「KPI待处理事项/内容描述」为蓝色链接样式但点击不发生路由跳转(纯展示,BR11)。
-
- 2. 实现最小代码:
frontend/src/pages/home/HomePage/KpiBoard.tsx(AntDTablerowSpan 合并「导航类型/角色/子流程」列,或 CSS GridgridRow span复刻原型——TDD 期二选一,登记 D5;空数组 →Empty)+HomePage.module.css网格样式(线/底色用var(--color-border)/var(--color-table-header-bg)等 token)。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit -- KpiBoard - 4. commit:
feat(fe-shell): 主页 KPI 合并网格与空数据态 REQ-USR-003
T6 — HomePage 落地页区域组合(KPI 头条 / 角色树 / 常用操作 / 页脚,BR8/BR11)(jsdom 组件测)
- 测试先行类型:jsdom 组件测试
- 1. 写失败测试:
frontend/tests/unit/HomePage.test.tsx(renderShell,initialEntries:['/']):-
::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(页脚归外壳;主页内.foot由AppLayout渲染——若主页自带页脚则置于 HomePage,TDD 期定一处,登记);HomePage.module.css。语义色用 token,统计红/蓝用var(--color-error)/var(--color-primary)。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit -- HomePage - 4. commit:
feat(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. 子会话验证 PASS:
cd frontend && npm run test:unit -- NavOverlay - 4. commit:
feat(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——展开当前用户下拉点「退出登录」→ storeauth.token===null、localStoragetoken 被移除、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(签名见关键签名;标签条用 AntDTabseditable-card 或自定义受控 div,D3——主页 tabclosable:false;退出登录onLogout内dispatch(clearCredentials())+message.success+navigate('/login',{replace:true}),由AppLayout提供回调)。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit -- AppLayout.topbar - 4. commit:
feat(fe-shell): 顶栏与当前用户/退出登录 REQ-USR-004
T9 — AppLayout 外壳装配 + 标签↔路由同步(ready / navOverlayOpen / tabOpen 态)(jsdom 组件测)
- 测试先行类型:jsdom 组件测试
- 1. 写失败测试:
frontend/tests/unit/AppLayout.shell.test.tsx(renderShell已登录,渲染真实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/>/AppFooter;useTabStack+navOverlayOpen本地态;标签点击 →navigate(routePath);据useLocation反向同步 activeKey 与已打开标签;overlay 路由项 → 关 overlay + openTab + navigate)+AppLayout.module.css(顶栏深色底 scoped,D9)。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit -- AppLayout.shell - 4. commit:
feat(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(indexHomePage+/usr/users//usr/users/new//usr/users/:id暂用占位/懒加载位,FE-03/FE-04 落地时替换);/login包RedirectIfAuthed;外壳内套AppErrorBoundary;未匹配Navigate to="/"(受保护)/经守卫到/login。新增frontend/src/router/AppErrorBoundary.tsx。子路由目标内容(FE-03/FE-04)不在本 FE 实现,仅留可挂载的占位元素。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit -- router AppErrorBoundary - 4. commit:
feat(fe-shell): 受保护嵌套路由表与错误边界 REQ-USR-003
T11 — request.ts 401 统一登出回调(BR10 / D11)(jsdom 单测)
- 测试先行类型:jsdom 组件测试
- 1. 写失败测试:
frontend/tests/unit/request.unauthorized.test.ts(沿用 FE-01axios-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 仍 rejectApiError,不抛额外异常。 - 不破坏 FE-01 既有断言(401 业务体仍映射
ApiError)。
-
- 2. 实现最小代码:改
frontend/src/api/request.ts——加HTTP_UNAUTHORIZED=401常量、模块级registerUnauthorizedHandler(fn)与onUnauthorized单例;响应错误拦截器中error.response?.status===401时调用回调(若已注册)再走原ApiError映射逻辑。不改变 FE-01 既有拆包/网络兜底语义。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit -- request(含 FE-01request既有用例不回归) - 4. commit:
feat(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,捕获注册的回调(spyregisterUnauthorizedHandler),调用之 → storeauth.token===null、message.warning('登录已失效,请重新登录')、URL 到/login(BR10)。
-
- 2. 实现最小代码:在
AppLayoutuseEffect内registerUnauthorizedHandler(() => { dispatch(clearCredentials()); message.warning('登录已失效,请重新登录'); navigate('/login',{replace:true}); })(卸载时可清理)。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit -- AppLayout.unauthorized - 4. commit:
feat(fe-shell): 外壳注册被动401统一登出 REQ-USR-004
T13 — E2E 外壳关键旅程(Playwright)
- 测试先行类型:Playwright E2E
- 1. 写失败测试:
frontend/tests/e2e/shell.spec.ts(page.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-01playwright.config.ts。 - 3. 子会话验证 PASS:
cd frontend && npm run test:e2e -- shell - 4. commit:
test(fe-shell): 应用外壳 E2E 关键旅程 REQ-USR-003
T14 — 全量门禁回归 + 收尾(chore)
- 测试先行类型:无新增测试(全量验证)
- 1. 写失败测试:无。
- 2. 实现最小代码:修 lint / build / 类型问题;确认语义色全部
var(--color-*)、无硬编码 hex/rgba(顶栏/overlay 深色装饰 scoped 例外,D9);确认无TBD/TODO/【人工填写】。 - 3. 子会话验证 PASS:
cd frontend && npm run lint && npm run build && npm run test:unit && npm run test:e2e全绿。 - 4. commit:
chore(fe-shell): FE-02 门禁回归通过 REQ-USR-003
完成判据(Definition of Done)
- 登录后落地主页
/,渲染复刻原型#screen-main的 KPI 头条 / 角色流程树 / KPI 合并网格 / 常用操作 / 页脚(spec § 2 / § 6.6)。 - 应用外壳
AppLayout渲染顶栏(Logo + 全部导航 + 标签栈 + 当前用户 + 退出)+ 导航总览 overlay +<Outlet/>+ 页脚(spec § 2)。 - 状态机覆盖并测试固化:
authResolving/unauthenticated/ready(T1/T10)、navOverlayOpen(T7/T9)、tabOpen(T3/T9)、empty(T5)、error(T10 ErrorBoundary)(spec § 3)。 - 业务/交互规则 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)。
-
不新增任何后端取数;KPI/导航/角色树为前端静态配置(
dashboardData.ts/navConfig.ts),复刻原型 demo(spec § 4 / D1/D2)。 - 退出登录纯前端清登录态 + 删 token + 跳
/login(spec § 6.7 / D6);被动 401 经request.ts统一登出回调跳/login(BR10 / D11)。 - 标签栈复刻原型
tabsOpen/openTab/.close联动(主页固定不可关、userlist↔userdetail 父子联动)(spec § 6.4 / BR4-6)。 - 语义色只用
var(--color-*),AntDcolorPrimary沿用 FE-01ConfigProvider对齐;顶栏/overlay 深色底 scoped 装饰不新增全局 token(spec § 7 / D9)。 - 全部落点在
frontend/**,无backend//sql//scripts/改动;改router/index.tsx、api/request.ts属共享骨架,已在《模块完成报告》留痕。 - 门禁全绿:
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/NavLeaf、KPI_STATS/KPI_ROWS/KpiRow(字段名对齐原型kpiRows)、AuthUser(复用 FE-01api/types.ts)、TOKEN_STORAGE_KEY/HTTP_UNAUTHORIZED、各文案常量跨 T0-T14 一致;当前用户文案规则`${sUserName}(${sUserType})`与 BR3/D10 一致。 -
作用域自审:所有
impl_file/test_file均以frontend/开头;无backend//sql//scripts/落点。