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:
@@ -1,2 +0,0 @@
|
||||
schema: spec-driven
|
||||
created: 2026-05-13
|
||||
@@ -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 → Tab),props 类型明确,不会造成 prop drilling 问题 |
|
||||
|
||||
## Open Questions
|
||||
|
||||
无。方案已在 explore 阶段与用户确认。
|
||||
@@ -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 中前端目录结构和组件清单
|
||||
@@ -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 header,body 为空
|
||||
|
||||
### Requirement: MetaResponse 共享类型
|
||||
系统 SHALL 在 `src/shared/api.ts` 中定义 `MetaResponse` 类型。
|
||||
|
||||
#### Scenario: MetaResponse 类型定义
|
||||
- **WHEN** 前后端引用 `MetaResponse` 类型
|
||||
- **THEN** 该类型 SHALL 包含 `checkerTypes: string[]` 字段
|
||||
@@ -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 不被包含在产物中
|
||||
@@ -1,91 +0,0 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 目标详情 Drawer
|
||||
Dashboard SHALL 在用户点击目标表格行后从右侧滑出 Drawer,展示该目标的详细统计信息和检查记录。Drawer 标题栏和内容不使用内联 style。Drawer 内容 SHALL 拆分为独立的 Tab 组件。
|
||||
|
||||
#### Scenario: 打开 Drawer
|
||||
- **WHEN** 用户点击某个目标表格行
|
||||
- **THEN** 系统 SHALL 从右侧滑出 Drawer(placement="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 状态
|
||||
@@ -1,69 +0,0 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 表格列定义
|
||||
每个分组的 PrimaryTable SHALL 包含状态、名称、类型、可用率、最近状态条、延迟、间隔 7 列,不含分组列(同组内冗余)。列渲染不使用内联 style。列定义 SHALL 通过工厂函数动态生成。
|
||||
|
||||
#### Scenario: 状态列
|
||||
- **WHEN** 表格渲染
|
||||
- **THEN** 状态列 SHALL 使用 StatusDot 组件渲染,标题显示"#",宽度 60px,fixed="left",居中对齐,支持筛选(UP/DOWN/全部)。StatusDot SHALL 通过 CSS 类(`.status-dot--up` / `.status-dot--down`)控制颜色,不使用内联 style
|
||||
|
||||
#### Scenario: 名称列
|
||||
- **WHEN** 表格渲染
|
||||
- **THEN** 名称列 SHALL 显示目标名称,支持字母排序(zh-CN),ellipsis 超长名称自动省略并 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 导入,不在组件中重复定义
|
||||
@@ -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**: 无需迁移,删除即可。
|
||||
@@ -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 说明
|
||||
Reference in New Issue
Block a user