From 490a6b1bc9a40af6b977af6f2755456bdb79e902 Mon Sep 17 00:00:00 2001 From: zichun Date: Mon, 1 Jun 2026 16:53:16 +0800 Subject: [PATCH] docs(plan:FE-02): 任务级 TDD 计划 --- docs/superpowers/plans/2026-06-01-FE-02.md | 330 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 330 insertions(+), 0 deletions(-) create mode 100644 docs/superpowers/plans/2026-06-01-FE-02.md diff --git a/docs/superpowers/plans/2026-06-01-FE-02.md b/docs/superpowers/plans/2026-06-01-FE-02.md new file mode 100644 index 0000000..51acb61 --- /dev/null +++ b/docs/superpowers/plans/2026-06-01-FE-02.md @@ -0,0 +1,330 @@ +# 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: + +- **路由壳与守卫**:`` 包裹布局路由 ``;index `/` → ``,子路由 `/usr/users`(FE-03 容器)、`/usr/users/new` 与 `/usr/users/:id`(FE-04 容器)。未登录进受保护路由 → ``(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 + + 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)复用 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` / `useMatch`或`matchPath`)/ Vite / Axios / TypeScript;`@ant-design/icons`。 +- 测试:单测 Vitest(jsdom)+ `@testing-library/react|jest-dom|user-event`;E2E Playwright。沿用 FE-01 `tests/setup.ts`、`vite.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 -- <文件名片段>`。 +- 提交格式:`(): 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) | + | 角色树分组 | `按角色` / `按流程` | + | 空数据占位 | 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 }` 或用于 `}>` 的布局守卫,内部渲染 `` 或 `children`):读 `useAppSelector(s => s.auth)`;`token` 为 null → ``(BR1);`token` 存在但 `user` 为 null → 渲染 `Spin` 占位(`authResolving`,data-testid 可选 `auth-resolving`);否则放行(`ready`)。 + - `RedirectIfAuthed`(包 `/login`,`{ children: ReactNode }`):`token` 与 `user` 均就绪(或仅 token 即视为已登录,TDD 期定,登记一处)→ ``(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` / `` / `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 }`):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):默认用真实 `store`(`configureStore`)+ `MemoryRouter`(`initialEntries` 指定路由)+ AntD `App` 上下文(经 `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(
shell-ok
, { 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` + AntD `App` + `MemoryRouter`)。复用 FE-01 `renderLogin.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` 就绪,断言放行渲染 `` 子内容(哨兵)。 +- [ ] **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'`,home `closable===false`。 + - `::openTab userlist appends userlist and activates it`——`openTab('userlist')` 后 tabs 含 home+userlist,`activeKey==='userlist'`,userlist `closable===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={[]}`,渲染 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. 子会话验证 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`——展开当前用户下拉点「退出登录」→ store `auth.token===null`、`localStorage` 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`;退出登录 `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`/``/`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`(index `HomePage` + `/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-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. 子会话验证 PASS**:`cd frontend && npm run test:unit -- request`(含 FE-01 `request` 既有用例不回归) +- [ ] **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`,捕获注册的回调(spy `registerUnauthorizedHandler`),调用之 → store `auth.token===null`、`message.warning('登录已失效,请重新登录')`、URL 到 `/login`(BR10)。 +- [ ] **2. 实现最小代码**:在 `AppLayout` `useEffect` 内 `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-01 `playwright.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) + +1. 登录后落地主页 `/`,渲染复刻原型 `#screen-main` 的 KPI 头条 / 角色流程树 / KPI 合并网格 / 常用操作 / 页脚(spec § 2 / § 6.6)。 +2. 应用外壳 `AppLayout` 渲染顶栏(Logo + 全部导航 + 标签栈 + 当前用户 + 退出)+ 导航总览 overlay + `` + 页脚(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.tsx`、`api/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/NavLeaf`、`KPI_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/` 落点。 -- libgit2 0.22.2