1
0

refactor: 前端架构重构 — hook拆分、组件拆分、类型筛选器动态化、Meta API

- 后端新增 GET /api/meta 端点(checkerRegistry.supportedTypes)及 MetaResponse 类型
- 前端 hook 拆分为 use-queries.ts(全局查询+useMeta)和 use-target-detail.ts(Drawer状态)
- TargetDetailDrawer 拆分为 OverviewTab + HistoryTab + history-table-columns + stats.ts
- 类型筛选器由 meta API 动态驱动,删除 target-type-display 静态映射
- 列定义改为工厂函数 createTargetTableColumns(checkerTypes),TargetGroup 新增 columns prop
- 修复 StatusDonut key、StatusBar maxSlots prop、TrendChart 移除 loading prop
- 补充 utils/time、utils/stats、动态列工厂测试,删除旧 mapping 测试
- 同步 delta specs 到主 specs,归档 frontend-architecture-refactor change
This commit is contained in:
2026-05-13 20:55:42 +08:00
parent a62007083d
commit 31aeee6d60
41 changed files with 713 additions and 902 deletions

View File

@@ -1,2 +0,0 @@
schema: spec-driven
created: 2026-05-13

View File

@@ -1,145 +0,0 @@
## 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<TargetStatus>[] {
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 → Tabprops 类型明确,不会造成 prop drilling 问题 |
## Open Questions
无。方案已在 explore 阶段与用户确认。

View File

@@ -1,34 +0,0 @@
## Why
前端代码经过多轮功能迭代后,出现了 hook 职责过重(`useTargetDetail.ts` 承载全部数据层)、组件体积膨胀(`TargetDetailDrawer` 228 行混合多种逻辑)、类型筛选器与后端硬编码重复维护等架构问题。需要通过拆分、动态化和测试补齐来提升可维护性,使新增 checker 类型时前端零改动。
## What Changes
- 拆分 `hooks/useTargetDetail.ts``use-queries.ts`(全局查询)和 `use-target-detail.ts`Drawer 状态管理)
- 拆分 `TargetDetailDrawer.tsx` 为 Drawer 壳、`OverviewTab.tsx``HistoryTab.tsx` 三个组件
-`HISTORY_COLUMNS` 移至 `constants/history-table-columns.tsx`
- 提取统计计算逻辑为 `utils/stats.ts` 纯函数
- 后端新增 `GET /api/meta` 端点,返回 `checkerTypes` 列表
- 前端新增 `useMeta()` hook 消费 meta API动态生成类型筛选器
- **BREAKING** 删除 `constants/target-type-display.ts`,前端直接使用 type 原始文本,不再做 label 转换
- 列定义从静态常量改为工厂函数 `createTargetTableColumns(checkerTypes)`
- 修复小问题:`StatusDonut` key、`StatusBar` 硬编码、`TrendChart` 冗余 prop、统计计算 useMemo
- 补充前端测试:`utils/time.ts``utils/stats.ts`、动态列生成
## Capabilities
### New Capabilities
- `meta-api`: 后端 meta 信息 API提供 checker 类型列表等运行时元数据
### Modified Capabilities
- `target-type-display`: 移除前端静态映射,改为直接使用后端返回的 type 原始文本,筛选器列表由 meta API 动态驱动
- `tanstack-query-data-layer`: hook 文件拆分为 `use-queries.ts``use-target-detail.ts`,新增 `useMeta()` 查询
- `target-detail-drawer`: 组件拆分为 Drawer 壳 + OverviewTab + HistoryTab统计计算提取为纯函数
- `target-table`: 列定义从静态常量改为工厂函数,接收动态 checkerTypes 参数;类型列直接显示 type 原始文本
## Impact
- 后端:新增 `src/server/routes/meta.ts``src/shared/api.ts` 增加 `MetaResponse` 类型、`app.ts` 注册路由
- 前端hooks/components/constants 目录结构调整,删除 `target-type-display.ts`
- 测试:删除 `target-type-display.test.ts`,新增 `time.test.ts``stats.test.ts`,更新 `target-table-filters.test.ts`
- 文档:更新 DEVELOPMENT.md 中前端目录结构和组件清单

View File

@@ -1,27 +0,0 @@
## ADDED Requirements
### Requirement: Meta 信息 API
系统 SHALL 提供 `GET /api/meta` 端点,返回系统运行时元数据。
#### Scenario: 获取 checker 类型列表
- **WHEN** 客户端请求 `GET /api/meta`
- **THEN** 系统 SHALL 返回 JSON `{ checkerTypes: string[] }`,包含所有已注册的 checker 类型标识符(如 `["http", "command"]`
#### Scenario: 类型列表来源
- **WHEN** 系统启动并注册了 checker
- **THEN** `/api/meta` 返回的 `checkerTypes` SHALL 与 `CheckerRegistry.supportedTypes` 完全一致
#### Scenario: 仅允许 GET/HEAD 方法
- **WHEN** 客户端使用 POST/PUT/DELETE 等方法请求 `/api/meta`
- **THEN** 系统 SHALL 返回 405 状态码
#### Scenario: HEAD 请求返回空体
- **WHEN** 客户端使用 HEAD 方法请求 `/api/meta`
- **THEN** 系统 SHALL 返回 200 状态码和正确的 Content-Type headerbody 为空
### Requirement: MetaResponse 共享类型
系统 SHALL 在 `src/shared/api.ts` 中定义 `MetaResponse` 类型。
#### Scenario: MetaResponse 类型定义
- **WHEN** 前后端引用 `MetaResponse` 类型
- **THEN** 该类型 SHALL 包含 `checkerTypes: string[]` 字段

View File

@@ -1,109 +0,0 @@
## MODIFIED Requirements
### Requirement: TanStack Query 数据层
前端 SHALL 使用 TanStack Query@tanstack/react-query管理所有 API 请求,数据层代码 SHALL 按职责拆分为独立 hook 文件。
#### Scenario: QueryClient 配置
- **WHEN** 应用启动
- **THEN** 系统 SHALL 创建 QueryClient默认配置 retry=1、refetchOnWindowFocus=true、staleTime=5000
#### Scenario: QueryClientProvider 挂载
- **WHEN** 应用渲染
- **THEN** 根组件 SHALL 包裹在 QueryClientProvider 中,提供 QueryClient 实例
### Requirement: queryKey 工厂
系统 SHALL 提供统一的 queryKey 工厂函数,确保 queryKey 的唯一性和一致性。
#### Scenario: summary queryKey
- **WHEN** 查询 summary 数据
- **THEN** queryKey SHALL 为 ["summary"]
#### Scenario: targets queryKey
- **WHEN** 查询 targets 数据
- **THEN** queryKey SHALL 为 ["targets"]
#### Scenario: meta queryKey
- **WHEN** 查询 meta 数据
- **THEN** queryKey SHALL 为 ["meta"]
#### Scenario: trend queryKey
- **WHEN** 查询某目标的趋势数据
- **THEN** queryKey SHALL 为 ["trend", targetId, from, to]
#### Scenario: history queryKey
- **WHEN** 查询某目标的历史记录
- **THEN** queryKey SHALL 为 ["history", targetId, from, to, page]
### Requirement: Hook 文件拆分
数据层 hook SHALL 按职责拆分为独立文件。
#### Scenario: 全局查询 hook 文件
- **WHEN** 开发者需要使用全局面板级查询
- **THEN** `useSummary``useTargets``useMeta` SHALL 从 `hooks/use-queries.ts` 导出
#### Scenario: Drawer 状态 hook 文件
- **WHEN** 开发者需要使用 Drawer 状态管理
- **THEN** `useTargetDetail` SHALL 从 `hooks/use-target-detail.ts` 导出
#### Scenario: fetchJson 不导出
- **WHEN** 数据层内部需要 fetch 封装
- **THEN** `fetchJson` SHALL 定义在 `use-queries.ts` 内部,不作为公共 API 导出
#### Scenario: queryKeys 不导出
- **WHEN** 数据层内部需要 query key
- **THEN** `queryKeys` 对象 SHALL 定义在 `use-queries.ts` 内部,不作为公共 API 导出
### Requirement: Meta 查询
系统 SHALL 提供 `useMeta` hook 查询系统元数据。
#### Scenario: meta 查询配置
- **WHEN** 应用启动
- **THEN** `useMeta` SHALL 请求 `/api/meta`,配置 `staleTime: Infinity`(应用生命周期内只请求一次)
#### Scenario: meta 数据返回
- **WHEN** meta 查询成功
- **THEN** hook SHALL 返回 `MetaResponse` 类型数据,包含 `checkerTypes` 字段
### Requirement: Summary 轮询查询
系统 SHALL 使用 useQuery 实现总览统计的自动轮询。
#### Scenario: summary 自动轮询
- **WHEN** Dashboard 页面处于打开状态
- **THEN** 系统 SHALL 每 8 秒自动请求 /api/summary使用 refetchInterval=8000
#### Scenario: summary 后台刷新
- **WHEN** 页面处于后台标签页
- **THEN** 系统 SHALL 暂停轮询refetchIntervalInBackground=false
### Requirement: Targets 轮询查询
系统 SHALL 使用 useQuery 实现目标列表的自动轮询。
#### Scenario: targets 自动轮询
- **WHEN** Dashboard 页面处于打开状态
- **THEN** 系统 SHALL 每 8 秒自动请求 /api/targets使用 refetchInterval=8000
### Requirement: 条件查询
趋势和历史记录查询 SHALL 使用 enabled 条件控制,仅在目标被选中时触发。
#### Scenario: 未选中目标时不请求
- **WHEN** 用户未点击任何目标表格行
- **THEN** trend 和 history 的 useQuery SHALL enabled=false不发起请求
#### Scenario: 选中目标时自动请求
- **WHEN** 用户点击目标表格行
- **THEN** trend 和 history 的 useQuery SHALL enabled=true自动发起请求
#### Scenario: 时间范围变化时重新请求
- **WHEN** 用户更改时间范围
- **THEN** trend 和 history 的 useQuery SHALL 因 queryKey 变化自动重新请求
### Requirement: 开发调试面板
开发环境下 SHALL 挂载 TanStack Query Devtools。
#### Scenario: 开发环境显示 Devtools
- **WHEN** 应用在开发模式下运行
- **THEN** 页面 SHALL 显示 ReactQueryDevtools 浮动面板
#### Scenario: 生产环境排除 Devtools
- **WHEN** 应用在生产模式下构建
- **THEN** ReactQueryDevtools SHALL 不被包含在产物中

View File

@@ -1,91 +0,0 @@
## MODIFIED Requirements
### Requirement: 目标详情 Drawer
Dashboard SHALL 在用户点击目标表格行后从右侧滑出 Drawer展示该目标的详细统计信息和检查记录。Drawer 标题栏和内容不使用内联 style。Drawer 内容 SHALL 拆分为独立的 Tab 组件。
#### Scenario: 打开 Drawer
- **WHEN** 用户点击某个目标表格行
- **THEN** 系统 SHALL 从右侧滑出 Drawerplacement="right"),宽度为视口 60%
#### Scenario: Drawer 标题栏
- **WHEN** Drawer 渲染
- **THEN** 标题栏 SHALL 使用 TDesign Space 组件align="center")布局,包含 StatusDot、目标名称TDesign Typography.Text strong和类型标签TDesign Tag直接显示 target.type 原始文本),以及内建关闭按钮。不使用内联 style 的 flex 布局
#### Scenario: 关闭 Drawer
- **WHEN** 用户点击关闭按钮、ESC 键或遮罩层
- **THEN** Drawer SHALL 关闭
#### Scenario: Drawer 无底部按钮
- **WHEN** Drawer 渲染
- **THEN** Drawer SHALL 不显示底部操作栏footer={false}
#### Scenario: Drawer 数据同步
- **WHEN** Drawer 打开期间后台轮询刷新了 targets 数据
- **THEN** Drawer 中 selectedTarget 的状态 SHALL 随之同步更新
#### Scenario: 切换目标重置 Tab
- **WHEN** 用户从目标 A 切换到目标 B点击不同的表格行
- **THEN** Drawer SHALL 重置为概览 Tab使用 key={target.id} 确保组件状态不残留
#### Scenario: Drawer 内容区间距
- **WHEN** Drawer 内容渲染
- **THEN** 时间选择器、Tabs 等区块之间的间距 SHALL 通过 TDesign Space 组件direction="vertical", size={16})统一管理,不使用内联 style 的 marginBottom
### Requirement: 概览面板组件化
概览 Tab SHALL 作为独立组件 `OverviewTab` 实现,接收数据 props 进行渲染。
#### Scenario: OverviewTab 组件职责
- **WHEN** 概览 Tab 渲染
- **THEN** `OverviewTab` 组件 SHALL 负责统计卡片、趋势图、状态分布环形图和基本信息的渲染
#### Scenario: 统计计算使用纯函数
- **WHEN** OverviewTab 需要计算 totalChecks、upChecks、downChecks
- **THEN** 计算逻辑 SHALL 通过 `utils/stats.ts` 中的纯函数实现,并使用 `useMemo` 缓存结果
#### Scenario: OverviewTab props
- **WHEN** OverviewTab 渲染
- **THEN** 组件 SHALL 接收 `target: TargetStatus``trendData: TrendPoint[]``trendLoading: boolean` 作为 props
### Requirement: 记录面板组件化
记录 Tab SHALL 作为独立组件 `HistoryTab` 实现。
#### Scenario: HistoryTab 组件职责
- **WHEN** 记录 Tab 渲染
- **THEN** `HistoryTab` 组件 SHALL 负责检查结果表格和分页的渲染
#### Scenario: HistoryTab props
- **WHEN** HistoryTab 渲染
- **THEN** 组件 SHALL 接收 `historyData: HistoryResponse``historyLoading: boolean``onPageChange: (page: number) => void` 作为 props
#### Scenario: 历史记录列定义外置
- **WHEN** HistoryTab 渲染表格
- **THEN** 列定义 SHALL 从 `constants/history-table-columns.tsx` 导入,不在组件内部定义
### Requirement: TrendChart 简化
TrendChart 组件 SHALL 仅接收数据 props不处理 loading 状态。
#### Scenario: TrendChart 无 loading prop
- **WHEN** TrendChart 渲染
- **THEN** 组件 SHALL 仅接收 `data: TrendPoint[]` prop不接收 `loading` prop
#### Scenario: TrendChart 空数据
- **WHEN** TrendChart 接收空数组
- **THEN** 组件 SHALL 显示"暂无趋势数据"占位文本
### Requirement: StatusDonut key 修复
StatusDonut 组件 SHALL 使用语义化的 key。
#### Scenario: Pie Cell key
- **WHEN** StatusDonut 渲染 Pie Cell 列表
- **THEN** 每个 Cell 的 key SHALL 使用 data item 的 `name` 字段,不使用数组索引
### Requirement: StatusBar 参数化
StatusBar 组件 SHALL 支持可配置的格数。
#### Scenario: maxSlots prop
- **WHEN** StatusBar 渲染
- **THEN** 组件 SHALL 接收可选的 `maxSlots` prop默认 30根据该值渲染对应数量的格子
#### Scenario: 格子渲染逻辑
- **WHEN** StatusBar 渲染且 samples 数量少于 maxSlots
- **THEN** 多余的格子 SHALL 显示为 empty 状态

View File

@@ -1,69 +0,0 @@
## MODIFIED Requirements
### Requirement: 表格列定义
每个分组的 PrimaryTable SHALL 包含状态、名称、类型、可用率、最近状态条、延迟、间隔 7 列,不含分组列(同组内冗余)。列渲染不使用内联 style。列定义 SHALL 通过工厂函数动态生成。
#### Scenario: 状态列
- **WHEN** 表格渲染
- **THEN** 状态列 SHALL 使用 StatusDot 组件渲染,标题显示"#",宽度 60pxfixed="left"居中对齐支持筛选UP/DOWN/全部。StatusDot SHALL 通过 CSS 类(`.status-dot--up` / `.status-dot--down`)控制颜色,不使用内联 style
#### Scenario: 名称列
- **WHEN** 表格渲染
- **THEN** 名称列 SHALL 显示目标名称支持字母排序zh-CNellipsis 超长名称自动省略并 Tooltip 显示全名
#### Scenario: 类型列
- **WHEN** 表格渲染
- **THEN** 类型列 SHALL 使用 TDesign Tag 组件size=small, theme=primary, variant=light-outline直接显示 target.type 原始文本,支持单选筛选
#### Scenario: 类型筛选器动态生成
- **WHEN** 表格渲染
- **THEN** 类型列的筛选器列表 SHALL 从 meta API 返回的 `checkerTypes` 动态生成,包含"全部"选项和每个 checker 类型选项label 和 value 均为 type 原始文本)
#### Scenario: 可用率列
- **WHEN** 表格渲染
- **THEN** 可用率列 SHALL 使用 TDesign Progress 组件theme=line, size=small渲染颜色通过 CSS 自定义属性 `--avail-N`(基于项目自定义色值)控制,每 10% 一档label 显示百分比数值支持排序升序优先最差排最前。color-threshold 函数 SHALL 返回 CSS 自定义属性引用而非硬编码色值
#### Scenario: 最近状态列
- **WHEN** 表格渲染
- **THEN** 最近状态列 SHALL 使用 StatusBar 组件渲染采样色块,宽度 220px。StatusBar SHALL 通过 CSS 类(`.status-bar-block--up` / `.status-bar-block--down` / `.status-bar-block--empty`)控制色块颜色,不使用内联 style
#### Scenario: 延迟列
- **WHEN** 表格渲染
- **THEN** 延迟列 SHALL 显示最近一次检查的延迟毫秒数,右对齐。颜色 SHALL 通过 CSS 类实现≤100ms 使用 `.latency-ok`、100-500ms 使用 `.latency-warn`、>500ms 使用 `.latency-error`。无数据 SHALL 使用 `.text-disabled` 类显示 "-",数值 SHALL 使用 `.tabular-nums` 类等宽显示。不使用内联 style
#### Scenario: 间隔列
- **WHEN** 表格渲染
- **THEN** 间隔列 SHALL 显示检查间隔(如 "5s"、"30s"),居中对齐,宽度 72px
### Requirement: 列定义工厂函数
列定义 SHALL 通过工厂函数生成,接收动态参数。
#### Scenario: createTargetTableColumns 函数
- **WHEN** 需要生成表格列定义
- **THEN** 系统 SHALL 调用 `createTargetTableColumns(checkerTypes: string[])` 函数,返回 `PrimaryTableCol<TargetStatus>[]`
#### Scenario: checkerTypes 为空数组
- **WHEN** meta API 尚未返回或返回空数组
- **THEN** 类型列的筛选器 SHALL 仅包含"全部"选项
#### Scenario: 列定义缓存
- **WHEN** TargetBoard 组件渲染
- **THEN** 列定义 SHALL 通过 `useMemo` 缓存,仅在 `checkerTypes` 变化时重新生成
### Requirement: TargetGroup 接收 columns prop
TargetGroup 组件 SHALL 通过 prop 接收列定义,不再直接导入静态常量。
#### Scenario: columns prop
- **WHEN** TargetGroup 渲染
- **THEN** 组件 SHALL 接收 `columns: PrimaryTableCol<TargetStatus>[]` prop 并传递给 PrimaryTable
#### Scenario: TargetBoard 传递 columns
- **WHEN** TargetBoard 渲染子组件
- **THEN** TargetBoard SHALL 调用 `createTargetTableColumns` 生成列定义并传递给每个 TargetGroup
### Requirement: 列定义复用
所有分组的表格 SHALL 共享同一套列定义常量。
#### Scenario: 列定义提取为常量
- **WHEN** 多个分组表格渲染
- **THEN** 列定义 SHALL 从独立的 constants/target-table-columns.tsx 导入,不在组件中重复定义

View File

@@ -1,13 +0,0 @@
## REMOVED Requirements
### Requirement: 类型显示名称映射
**Reason**: 前端不再维护 type → label 的静态映射,直接使用后端返回的 type 原始文本展示。类型筛选器列表改由 meta API 动态驱动。
**Migration**: 所有使用 `getTargetTypeDisplay(type)` 的位置改为直接使用 `type` 字符串。`TARGET_TYPE_DISPLAY` 常量和 `target-type-display.ts` 文件删除。
### Requirement: 映射可扩展性
**Reason**: 不再需要前端映射扩展机制,新增 checker 类型时后端注册即自动通过 meta API 暴露给前端。
**Migration**: 无需迁移,删除即可。
### Requirement: 类型安全
**Reason**: 不再有映射常量,无需 TypeScript 类型推导和 fallback 逻辑。
**Migration**: 无需迁移,删除即可。

View File

@@ -1,48 +0,0 @@
## 1. 后端 Meta API
- [ ] 1.1 在 `src/shared/api.ts` 中新增 `MetaResponse` 类型定义
- [ ] 1.2 创建 `src/server/routes/meta.ts`,实现 `handleMeta``checkerRegistry.supportedTypes` 返回数据
- [ ] 1.3 在 `src/server/app.ts` 中注册 `/api/meta` 路由
- [ ] 1.4 在 `tests/server/app.test.ts` 中添加 `/api/meta` 端点测试GET/HEAD/405
## 2. 前端 Hook 拆分
- [ ] 2.1 创建 `src/web/hooks/use-queries.ts`,迁入 `queryKeys``fetchJson``useSummary``useTargets`,新增 `useMeta`
- [ ] 2.2 重写 `src/web/hooks/use-target-detail.ts`,仅保留 Drawer 状态管理和条件查询trend/history
- [ ] 2.3 更新 `src/web/app.tsx` 的 import 路径适配新 hook 文件
## 3. 前端组件拆分
- [ ] 3.1 创建 `src/web/utils/stats.ts`,提取统计计算纯函数(`computeTrendStats`
- [ ] 3.2 创建 `src/web/constants/history-table-columns.tsx`,将 `HISTORY_COLUMNS` 从 Drawer 中移出
- [ ] 3.3 创建 `src/web/components/OverviewTab.tsx`,从 TargetDetailDrawer 中提取概览面板逻辑
- [ ] 3.4 创建 `src/web/components/HistoryTab.tsx`,从 TargetDetailDrawer 中提取记录面板逻辑
- [ ] 3.5 精简 `src/web/components/TargetDetailDrawer.tsx`,仅保留 Drawer 壳 + 时间选择 + Tab 切换
## 4. 类型筛选器动态化
- [ ] 4.1 将 `src/web/constants/target-table-columns.tsx` 中的 `TARGET_TABLE_COLUMNS` 改为工厂函数 `createTargetTableColumns(checkerTypes)`
- [ ] 4.2 从 `src/web/constants/target-table-filters.ts` 中移除 `typeFilter``statusFilter` 保留)
- [ ] 4.3 更新 `src/web/components/TargetBoard.tsx`,调用 `useMeta` + `useMemo` 生成列定义并传递给 TargetGroup
- [ ] 4.4 更新 `src/web/components/TargetGroup.tsx`,新增 `columns` prop 替代静态导入
- [ ] 4.5 删除 `src/web/constants/target-type-display.ts`
- [ ] 4.6 更新 `src/web/components/TargetDetailDrawer.tsx` 标题栏,直接使用 `target.type` 替代 `getTargetTypeDisplay`
## 5. 小问题修复
- [ ] 5.1 修复 `StatusDonut.tsx`Cell key 从 `index` 改为 `data[index].name`
- [ ] 5.2 修复 `StatusBar.tsx`:新增 `maxSlots` prop默认 30用 prop 驱动格数渲染
- [ ] 5.3 修复 `TrendChart.tsx`:移除 `loading` prop仅保留 `data` prop
## 6. 测试补充与更新
- [ ] 6.1 创建 `tests/web/utils/time.test.ts`,测试 `subtractHours`正常、跨天、跨月、0 小时)
- [ ] 6.2 创建 `tests/web/utils/stats.test.ts`,测试 `computeTrendStats` 纯函数
- [ ] 6.3 更新 `tests/web/constants/target-table-filters.test.ts`,移除 `typeFilter` 相关测试
- [ ] 6.4 删除 `tests/web/constants/target-type-display.test.ts`
- [ ] 6.5 创建 `tests/web/constants/target-table-columns.test.ts`,测试 `createTargetTableColumns` 工厂函数
## 7. 质量保障与文档
- [ ] 7.1 执行 `bun run check` 确保类型检查、lint、测试全部通过
- [ ] 7.2 更新 DEVELOPMENT.md 中前端目录结构、组件清单和 hook 说明