## Context 当前前端代码约 970 行,功能完整但存在以下架构问题: 1. **hook 职责过重**:`hooks/useTargetDetail.ts`(113 行)同时承载全局查询(`useSummary`、`useTargets`)、Drawer 状态管理、条件查询和通用 `fetchJson` 封装,文件名与实际职责不匹配 2. **组件体积膨胀**:`TargetDetailDrawer.tsx`(228 行)混合了时间选择逻辑、两个 Tab 的完整渲染、列定义和统计计算 3. **类型维护重复**:`target-type-display.ts` 和 `target-table-filters.ts` 各自硬编码 checker 类型列表,新增 checker 需改两处前端文件 4. **测试覆盖不足**:仅 `constants/` 下 4 个纯函数有测试,`utils/time.ts` 和组件内统计逻辑未覆盖 5. **小问题**:`StatusDonut` 用数组索引做 key、`StatusBar` 硬编码 30 格、`TrendChart` 冗余 loading prop 后端 `CheckerRegistry` 已有 `supportedTypes` 属性,可直接暴露给前端。项目未上线,不需要向前兼容。 ## Goals / Non-Goals **Goals:** - hook 按职责拆分,文件名匹配实际内容 - `TargetDetailDrawer` 拆分为 3 个组件,每个 < 100 行 - 类型筛选器由后端 meta API 驱动,新增 checker 前端零改动 - 删除 type label 转换层,直接使用 type 原始文本 - 补齐前端纯函数测试 - 修复已知小问题 **Non-Goals:** - 不引入路由库或状态管理库 - 不重构后端 API 结构 - 不改变现有轮询策略和 QueryClient 配置 - 不新增 CSS 文件(继续使用单一 `styles.css`) ## Decisions ### Decision 1: hook 拆分策略 **选择**:按查询层级拆分为 `use-queries.ts` 和 `use-target-detail.ts` - `use-queries.ts`:`queryKeys`、`fetchJson`(不导出)、`useSummary`、`useTargets`、`useMeta` - `use-target-detail.ts`:仅保留 Drawer 状态管理(`selectedTargetId`、时间范围、分页、`openDrawer`/`closeDrawer`)和条件查询(trend/history) **理由**:全局查询(summary/targets/meta)是面板级别的,与 Drawer 详情无关。拆分后各文件职责单一,命名自解释。`fetchJson` 仅被 query 层使用,留在 `use-queries.ts` 内部不导出。 **备选方案**: - 仅重命名为 `useQueries.ts` — 解决命名问题但不解决职责混合 - 每个 query 一个文件 — 过度拆分,增加文件数量但无实质收益 ### Decision 2: TargetDetailDrawer 拆分方式 **选择**:拆为 3 个组件 + 1 个常量文件 ``` TargetDetailDrawer.tsx ← Drawer 壳 + 时间选择 + Tab 切换 OverviewTab.tsx ← 统计 + TrendChart + StatusDonut + Descriptions HistoryTab.tsx ← PrimaryTable + 分页 constants/history-table-columns.tsx ← HISTORY_COLUMNS ``` **理由**:两个 Tab 的内容完全独立,拆分后各组件 < 100 行。`HISTORY_COLUMNS` 与 `TARGET_TABLE_COLUMNS` 性质相同,应放在 `constants/` 下保持一致。 统计计算逻辑(`totalChecks`/`upChecks`/`downChecks`)提取为 `utils/stats.ts` 纯函数,便于测试和 `useMemo`。 ### Decision 3: Meta API 设计 **选择**:`GET /api/meta` 返回 `{ checkerTypes: string[] }` ```typescript // src/shared/api.ts export interface MetaResponse { checkerTypes: string[]; } ``` **理由**: - 从 `checkerRegistry.supportedTypes` 直接获取,无需额外维护 - 返回 `string[]` 而非 `{ key, label }[]`,因为决策是不做 label 转换 - 端点命名为 `/api/meta` 而非 `/api/types`,为未来扩展预留空间(如版本号、功能开关等) - `staleTime: Infinity`,应用生命周期内只请求一次 **备选方案**: - 从 targets 响应中动态提取 — 只能获取"当前有数据的类型",不能获取"系统支持的全部类型" - 在 health 端点中附带 — 语义不匹配,health 应保持最小化 ### Decision 4: 类型展示策略 **选择**:删除 `target-type-display.ts`,所有展示位置直接使用 `target.type` 原始文本 **影响位置**: - `target-table-columns.tsx` 类型列 cell:`row.type` 直接渲染 - `TargetDetailDrawer.tsx` 标题栏 Tag:`target.type` 直接渲染 - `typeFilter` 列表:从 meta API 获取,label 和 value 均为原始 type 文本 **理由**:type 文本本身已足够清晰(`http`、`command`、`tcp`),无需额外映射层。消除了前后端重复维护的问题。 ### Decision 5: 列定义动态化 **选择**:`TARGET_TABLE_COLUMNS` 从静态常量改为工厂函数 ```typescript export function createTargetTableColumns(checkerTypes: string[]): PrimaryTableCol[] { const typeFilter = { list: [ { label: "全部", value: "" }, ...checkerTypes.map(t => ({ label: t, value: t })), ], type: "single" as const, }; // ... 返回列定义数组 } ``` **数据流**:`useMeta()` → `checkerTypes` → `createTargetTableColumns(checkerTypes)` → `TargetGroup` columns prop **理由**:`typeFilter` 是唯一需要动态数据的部分,通过工厂函数注入参数,保持列定义的纯函数特性。`statusFilter` 保持静态(UP/DOWN 是固定的)。 `TargetGroup` 新增 `columns` prop 接收动态列定义,`TargetBoard` 负责调用工厂函数并传递。 ### Decision 6: StatusBar maxSlots 参数化 **选择**:新增 `maxSlots` prop(默认 30),组件根据 prop 渲染格数 ```typescript interface StatusBarProps { samples: Array<{ up: boolean }>; maxSlots?: number; } ``` **理由**:消除硬编码魔数,使组件可复用。默认值 30 保持向后兼容。 ### Decision 7: TrendChart 移除 loading prop **选择**:移除 `loading` prop,组件只接收 `data: TrendPoint[]` **理由**:调用方(`OverviewTab`)已用 `Skeleton` 处理 loading 状态,`TrendChart` 只在有数据时渲染。组件内部的 `if (loading)` 分支和 `loading={false}` 传参都是死代码。 ## Risks / Trade-offs | Risk | Mitigation | |------|-----------| | 列定义改为函数后,每次 `checkerTypes` 变化会重新创建列数组 | `useMemo` 包裹 `createTargetTableColumns` 调用;meta 数据 `staleTime: Infinity` 确保不会频繁变化 | | meta API 在 targets 之前未返回时,筛选器暂时为空 | meta 请求极轻量(无 DB 查询),通常先于 targets 返回;即使晚到,筛选器会在数据到达后自动出现 | | 拆分后组件间 props 传递增多 | 层级仅增加一层(Drawer → Tab),props 类型明确,不会造成 prop drilling 问题 | ## Open Questions 无。方案已在 explore 阶段与用户确认。