## Purpose

定义拨测系统前端 Dashboard 页面：页面骨架布局、刷新频率控制与倒计时、CSS 工具类基础设施、总览统计卡片、Dashboard 数据查询、加载和错误状态处理。分组表格布局见 `target-table`，目标详情 Drawer 见 `target-detail-drawer`，数据轮询和缓存见 `tanstack-query-data-layer`。

## Requirements

### Requirement: 页面骨架布局
Dashboard SHALL 使用 TDesign Layout 组件体系构建页面骨架，包含顶部导航栏和内容区域。

#### Scenario: Layout 结构
- **WHEN** Dashboard 页面渲染
- **THEN** 页面 SHALL 使用 TDesign `Layout` 组件包裹 `Layout.Header` 和 `Layout.Content`

#### Scenario: 顶部导航栏
- **WHEN** Dashboard 页面渲染
- **THEN** `Layout.Header` SHALL 内嵌 TDesign `HeadMenu` 组件，`logo` prop 渲染品牌名 "DiAL" 和副标题 "统一拨测平台"（水平排列），`operations` prop 渲染主题模式选择器、刷新频率选择器和倒计时/刷新按钮组合控件

#### Scenario: Header 右侧操作区
- **WHEN** Dashboard 页面渲染
- **THEN** HeadMenu operations 区域 SHALL 包含主题模式 RadioGroup、刷新频率 RadioGroup 和倒计时文本（或手动刷新按钮），三者水平排列并垂直居中

#### Scenario: 主题选择器位置
- **WHEN** HeadMenu operations 区域渲染
- **THEN** 主题模式 RadioGroup SHALL 位于刷新频率 RadioGroup 前面

#### Scenario: Header 右侧操作区位置
- **WHEN** HeadMenu 渲染
- **THEN** operations 区域 SHALL 使用右侧 margin 向内收缩，避免紧贴浏览器右边缘

#### Scenario: 内容区域居中
- **WHEN** Dashboard 内容区渲染
- **THEN** `Layout.Content` 内部 SHALL 使用 CSS 类限制最大宽度（max-width: 1400px）并水平居中

#### Scenario: 页面背景色
- **WHEN** Dashboard 页面渲染
- **THEN** 页面背景色 SHALL 使用 `var(--td-bg-color-page)`，内容卡片浮于当前 TDesign 主题背景之上

### Requirement: Header 版本号展示
Dashboard SHALL 在顶部导航栏品牌区域展示当前运行实例的应用版本号，版本号 SHALL 使用 `/api/meta` 返回的 `version` 字段，并以 `v` 前缀显示。

#### Scenario: Meta 数据已加载
- **WHEN** Dashboard 成功获取 `/api/meta` 且返回 `version: "0.1.0"`
- **THEN** Header 品牌区域 SHALL 展示 `v0.1.0`

#### Scenario: Meta 数据尚未加载或请求失败
- **WHEN** Dashboard 尚未获取到有效 `version`
- **THEN** Header SHALL 保持可用并省略版本号占位，不影响品牌名、主题模式选择器、刷新频率选择器和倒计时/刷新按钮渲染

#### Scenario: 版本号视觉层级
- **WHEN** Header 展示版本号
- **THEN** 版本号 SHALL 使用次级文本样式弱展示，不得使用内联 style、硬编码色值、`!important` 或覆盖 TDesign 内部类名

### Requirement: 刷新频率选择器
HeadMenu operations 区域 SHALL 提供 RadioGroup 组件供用户选择刷新频率。

#### Scenario: RadioGroup 渲染
- **WHEN** Dashboard 页面渲染
- **THEN** HeadMenu operations 区域 SHALL 显示 RadioGroup（theme="button", variant="default-filled"），选项为：手动、10秒、30秒、1分钟、5分钟

#### Scenario: 默认选中
- **WHEN** 页面首次加载
- **THEN** RadioGroup SHALL 默认选中"30秒"

#### Scenario: 切换频率立即刷新
- **WHEN** 用户切换刷新频率选项
- **THEN** 系统 SHALL 立即触发一次数据刷新，然后应用新的刷新间隔

### Requirement: 倒计时显示
RadioGroup 右侧 SHALL 显示距下次自动刷新的倒计时。倒计时逻辑 SHALL 封装在独立的 `RefreshCountdown` 组件中，App 组件 SHALL NOT 持有每秒更新的 `now` state。自动倒计时数字 SHALL 使用 `@number-flow/react` 提供滚动过渡，非倒计时状态 SHALL 保持普通文本或按钮语义。

#### Scenario: RefreshCountdown 组件封装
- **WHEN** Dashboard 页面渲染
- **THEN** 倒计时显示 SHALL 由独立的 `RefreshCountdown` 组件负责，该组件内部持有 `now` state 和每秒 `setInterval`，渲染边界限制在该组件内部

#### Scenario: RefreshCountdown props
- **WHEN** RefreshCountdown 组件渲染
- **THEN** 组件 SHALL 接收 `dashboardUpdatedAt: number`、`refreshInterval: number`、`isFetching: boolean`、`isManualRefresh: boolean`、`onRefresh: () => void` 作为 props

#### Scenario: NumberFlow 数字滚动
- **WHEN** 自动刷新模式下已完成首次刷新且当前未处于刷新中状态
- **THEN** 倒计时数字 SHALL 使用 `@number-flow/react` 的 `NumberFlow` 渲染，并使用向下滚动趋势表达倒计时递减

#### Scenario: 秒级间隔格式
- **WHEN** 自动刷新间隔小于 60 秒
- **THEN** 倒计时 SHALL 显示为"xx秒"格式（如"26秒"）

#### Scenario: 分钟级稳定格式
- **WHEN** 自动刷新间隔大于等于 60 秒
- **THEN** 倒计时 SHALL 显示为"x分xx秒"格式，秒数 SHALL 固定为两位（如"4分30秒"、"0分09秒"）

#### Scenario: 时间数字边界
- **WHEN** 分钟级倒计时中的秒数在 59 到 00 边界变化
- **THEN** 秒数十位 SHALL 按时间显示规则限制在 0 到 5 之间滚动

#### Scenario: 无前缀
- **WHEN** 倒计时显示
- **THEN** 可见倒计时文本 SHALL 不包含任何前缀（如"下一次刷新："），直接显示时间

#### Scenario: 可访问文本
- **WHEN** NumberFlow 倒计时渲染
- **THEN** 倒计时容器 SHALL 暴露与当前倒计时等价的可访问文本，供测试和辅助技术读取

#### Scenario: 刷新中状态
- **WHEN** 数据正在刷新（isFetching=true 且 isLoading=false）
- **THEN** 倒计时文本 SHALL 显示为"刷新中..."

#### Scenario: 等待首次刷新状态
- **WHEN** 自动刷新模式下尚未完成首次刷新
- **THEN** 倒计时文本 SHALL 显示为"等待首次刷新"

### Requirement: App 组件渲染隔离
App 组件 SHALL NOT 持有任何高频更新的 state（如每秒更新的时钟），确保 App 的重渲染频率与数据刷新频率一致（默认 30 秒一次）。

#### Scenario: App 无 now state
- **WHEN** App 组件渲染
- **THEN** App SHALL NOT 包含 `useState` 管理的时钟 state，也 SHALL NOT 包含每秒触发的 `setInterval`

#### Scenario: App 重渲染频率
- **WHEN** Dashboard 处于自动刷新模式
- **THEN** App 组件的重渲染 SHALL 仅由 TanStack Query 的 refetch 触发（频率等于用户选择的刷新间隔），而非每秒触发

### Requirement: 手动刷新按钮
选择"手动"模式时，倒计时区域 SHALL 替换为刷新按钮。

#### Scenario: 手动模式显示按钮
- **WHEN** 用户选择"手动"刷新频率
- **THEN** 倒计时区域 SHALL 替换为刷新图标按钮

#### Scenario: 点击刷新
- **WHEN** 用户点击刷新按钮
- **THEN** 系统 SHALL 触发一次数据刷新

#### Scenario: 刷新中禁用
- **WHEN** 数据正在刷新
- **THEN** 刷新按钮 SHALL 显示 loading 状态且 disabled，防止连续点击

### Requirement: 布局稳定性
倒计时/按钮容器 SHALL 保持布局稳定，避免内容变化导致的抖动。NumberFlow 倒计时 SHALL 通过分组同步和等宽数字样式降低位数、单位和动画变化带来的布局偏移。

#### Scenario: 数字等宽
- **WHEN** 倒计时数字变化
- **THEN** 容器和 NumberFlow 倒计时 SHALL 使用 tabular-nums 字体特性，确保数字等宽不抖动

#### Scenario: NumberFlow 分组同步
- **WHEN** 分钟级倒计时同时渲染分钟和秒数
- **THEN** 分钟和秒数 SHALL 使用 `NumberFlowGroup` 同步布局变化

#### Scenario: 格式切换不抖动
- **WHEN** 倒计时在按钮、秒级文本和分钟级文本之间切换
- **THEN** 容器 SHALL 使用 min-width 确保最小宽度，避免 RadioGroup 位移

### Requirement: 状态色 CSS 类
styles.css SHALL 定义状态指示相关的 CSS 类，颜色使用 TDesign tokens。

#### Scenario: StatusDot 颜色类
- **WHEN** StatusDot 组件渲染
- **THEN** 组件 SHALL 使用 `.status-dot` 基础类 + `.status-dot--up`（background: `--td-success-color`）或 `.status-dot--down`（background: `--td-error-color`）修饰类，不使用内联 style

#### Scenario: StatusDot 发光阴影
- **WHEN** StatusDot 组件渲染
- **THEN** `.status-dot--up` SHALL 定义 `box-shadow` 使用 `--td-success-color`，`.status-dot--down` SHALL 定义 `box-shadow` 使用 `--td-error-color`

#### Scenario: StatusBar 色块类
- **WHEN** StatusBar 组件渲染色块
- **THEN** 组件 SHALL 使用 `.status-bar-block` 基础类 + `.status-bar-block--up`（background: `--td-success-color`）、`.status-bar-block--down`（background: `--td-error-color`）或 `.status-bar-block--empty`（background: `--td-bg-color-component-disabled`）修饰类，不使用内联 style

### Requirement: 可用率色阶 CSS 变量
styles.css SHALL 定义 10 级可用率色阶 CSS 自定义属性，使用项目自定义色值。

#### Scenario: 色阶变量定义
- **WHEN** 可用率进度条渲染
- **THEN** 色阶 SHALL 通过 CSS 自定义属性 `--avail-0` 到 `--avail-9` 定义，值为项目自定义色值（`#d54941` 到 `#3dba60`）

#### Scenario: 色阶渐变方向
- **WHEN** 色阶变量被引用
- **THEN** 色阶 SHALL 从红色（0-30%）经橙色（30-60%）过渡到绿色（60-100%）

### Requirement: 辅助工具类
styles.css SHALL 定义前端组件复用的工具类，包含页面布局相关类。

#### Scenario: 文本禁用色类
- **WHEN** 延迟列无数据需要显示占位符
- **THEN** 组件 SHALL 使用 `.text-disabled` 类（color: `--td-text-color-disabled`）

#### Scenario: 等宽数字类
- **WHEN** 数值需要等宽显示
- **THEN** 组件 SHALL 使用 `.tabular-nums` 类（font-variant-numeric: tabular-nums）

#### Scenario: 延迟色值类
- **WHEN** 延迟数值渲染
- **THEN** 组件 SHALL 使用 `.latency-ok`、`.latency-warn` 或 `.latency-error` 类

#### Scenario: 延迟值容器类
- **WHEN** 延迟数值需要固定宽度对齐
- **THEN** 组件 SHALL 使用 `.latency-value` 类（display: inline-block; min-width: 7ch; white-space: nowrap）

#### Scenario: 全宽布局类
- **WHEN** 组件需要占满父容器宽度
- **THEN** 组件 SHALL 使用 `.full-width` 类（width: 100%）

#### Scenario: 可点击表格类
- **WHEN** PrimaryTable 行支持点击交互
- **THEN** 表格 SHALL 使用 `.clickable-table` 类（cursor: pointer）

#### Scenario: Tab 面板内边距类
- **WHEN** Drawer 内 Tabs 面板需要内边距
- **THEN** TabPanel SHALL 使用 `className="tab-panel-padded"` prop 传入类名

#### Scenario: 内容区居中类
- **WHEN** Dashboard 内容区需要居中且限制最大宽度
- **THEN** 内容区 SHALL 使用 `.dashboard-content` 类（max-width: 1400px; margin: 0 auto; padding: var(--td-comp-paddingTB-xl) var(--td-comp-paddingLR-xl)）

#### Scenario: 页面背景色
- **WHEN** Dashboard 页面渲染
- **THEN** `.dashboard` 类 SHALL 设置 background: var(--td-bg-color-page)，min-height: 100vh，width: 100%

#### Scenario: 品牌标识类
- **WHEN** HeadMenu logo 区域渲染品牌名和副标题
- **THEN** 品牌 SHALL 使用 `.dashboard-brand` 类（display: inline-flex; align-items: baseline; gap: var(--td-comp-margin-s)），品牌名 SHALL 使用 `.dashboard-logo` 类（font-size: calc(var(--td-font-size-title-large) + 6px); font-weight: 700），副标题 SHALL 使用 `.dashboard-subtitle` 类（font-size: var(--td-font-size-body-medium); color: var(--td-text-color-secondary)）

#### Scenario: Header 右侧操作区类
- **WHEN** HeadMenu operations 区域渲染主题模式选择器、刷新频率选择器和倒计时/按钮
- **THEN** 容器 SHALL 使用 `.dashboard-header-controls` 类（display: inline-flex; align-items: center; gap: var(--td-comp-margin-s); margin-right: var(--td-comp-margin-xxl)）

#### Scenario: Header 右侧操作区单行布局
- **WHEN** Header 右侧操作区渲染
- **THEN** `.dashboard-header-controls` SHALL 保持桌面单行水平布局，不为该区域新增窄屏换行或收纳规则

#### Scenario: 倒计时文本类
- **WHEN** 倒计时文本或刷新按钮渲染
- **THEN** 容器 SHALL 使用 `.dashboard-countdown` 类（display: inline-flex; align-items: center; font-variant-numeric: tabular-nums; min-width: 5ch），确保数字等宽且格式切换不抖动

#### Scenario: SummaryCard 居中类
- **WHEN** SummaryCards 内 Statistic 需要居中
- **THEN** Statistic 所在的 Col SHALL 使用 `.summary-stat-col` 类（text-align: center）

#### Scenario: SummaryCards 行间距类
- **WHEN** SummaryCards 容器需要与下方内容保持间距
- **THEN** 容器 SHALL 使用 `.summary-cards-row` 类（margin-bottom: var(--td-comp-margin-xl)）

#### Scenario: Drawer 时间控件单行类
- **WHEN** Drawer 时间选择器需要单行布局
- **THEN** 控件容器 SHALL 使用 `.drawer-time-controls` 类（display: flex; align-items: center; gap: var(--td-comp-margin-m); width: 100%），日期选择器 SHALL 使用 `.drawer-date-range` 类（flex: 1; min-width: 360px）

#### Scenario: Drawer 时间控件响应式
- **WHEN** 视口宽度 ≤ 768px
- **THEN** `.drawer-time-controls` SHALL 启用 flex-wrap，`.drawer-date-range` min-width 改为 100%

#### Scenario: 概览统计卡片类
- **WHEN** Drawer 概览统计区渲染
- **THEN** 统计卡片 SHALL 使用 `.overview-stat-card` 类（background: var(--td-bg-color-container-hover)），并使用 TDesign Statistic 组件自带的上下布局（title 在上、value 在下），通过 `.summary-stat-col` 类（text-align: center）实现内容居中。系统 SHALL NOT 使用已移除的 `.overview-stat-item` 和 `.overview-stat-value` 类。

### Requirement: NumberFlow 倒计时样式类
styles.css SHALL 定义 NumberFlow 倒计时相关样式类，供 Header 倒计时组件使用。样式 SHALL 继承 TDesign 文本颜色或使用 TDesign CSS tokens，不得使用组件内联 `style`、硬编码色值、`!important` 或覆盖 TDesign 内部类名。

#### Scenario: 倒计时滚动容器类
- **WHEN** Header 自动刷新倒计时以 NumberFlow 形式渲染
- **THEN** 倒计时 SHALL 使用集中定义的滚动容器类，保持 inline-flex、baseline 对齐、nowrap 和 tabular-nums

#### Scenario: 倒计时数字类
- **WHEN** NumberFlow 数字渲染
- **THEN** 数字 SHALL 使用集中定义的数字类配置 line-height 和 NumberFlow mask CSS 变量，减少滚动边缘突兀感

#### Scenario: 倒计时单位类
- **WHEN** 分钟或秒单位文本渲染
- **THEN** 单位 SHALL 使用集中定义的单位类与数字保持基线对齐，并继承当前 TDesign 文本色

#### Scenario: 不使用内联样式
- **WHEN** RefreshCountdown 组件渲染 NumberFlow 倒计时
- **THEN** 组件 SHALL 通过 `className` 引用 styles.css 中的样式类，不得通过 React `style` prop 设置 NumberFlow 展示样式

### Requirement: 异常行背景类
styles.css SHALL 定义 DOWN 行的背景色和左侧竖线，使用安全选择器且不使用 `!important`。

#### Scenario: DOWN 行背景色
- **WHEN** 表格行标记为 DOWN 状态
- **THEN** 行 SHALL 通过 `.t-table tr.row-down` 选择器获得浅红色背景

#### Scenario: DOWN 行左侧竖线
- **WHEN** 表格行标记为 DOWN 状态
- **THEN** 行 SHALL 通过 `.t-table tr.row-down` 选择器获得 `border-left: 3px solid var(--td-error-color)`

#### Scenario: DOWN 行 hover 状态
- **WHEN** 鼠标悬停在 DOWN 行上
- **THEN** 行背景 SHALL 通过 `.t-table--hoverable tbody tr.row-down:hover` 选择器显示 hover 状态色

### Requirement: Dashboard 数据查询
Dashboard SHALL 通过 `GET /api/dashboard` 获取首屏总览统计和目标列表数据。

#### Scenario: 查询 Dashboard 数据
- **WHEN** 页面处于打开状态
- **THEN** 前端 SHALL 使用 TanStack Query 请求 `GET /api/dashboard?window=24h&recentLimit=30`

#### Scenario: 统计数据自动刷新
- **WHEN** 页面处于打开状态
- **THEN** Dashboard 数据 SHALL 通过 TanStack Query 的 refetchInterval=8000 自动刷新

#### Scenario: 元信息独立查询
- **WHEN** 页面需要 checker 类型列表
- **THEN** 前端 SHALL 继续通过 `GET /api/meta` 独立查询 checkerTypes

### Requirement: 总览统计卡片
Dashboard SHALL 在页面顶部使用单个 TDesign Card 组件内嵌一行居中的 Statistic 展示总览统计，包含总目标数、正常数、异常数和窗口异常事件数。

#### Scenario: 展示统计卡片
- **WHEN** 用户打开 Dashboard 页面
- **THEN** 页面顶部 SHALL 使用单个 TDesign Card（无 shadow、无 bordered）内嵌 TDesign Row/Col 布局展示 4 个居中的 Statistic：全部目标数（color=blue）、正常目标数（color=green）、异常目标数（color=red）、24h 异常事件数（color=orange）

#### Scenario: 指标居中显示
- **WHEN** SummaryCards 渲染
- **THEN** 每个 Statistic 所在的 Col SHALL 使用 `.summary-stat-col` 类实现标题和数字居中对齐

#### Scenario: 异常事件数据来源
- **WHEN** SummaryCards 渲染 24h 异常事件数
- **THEN** 该数值 SHALL 使用 DashboardResponse.summary.incidents 字段，标题 SHALL 基于当前 window 展示为"24h 异常事件数"

### Requirement: 页面加载与错误状态
Dashboard SHALL 使用 TDesign Skeleton 组件处理首次加载状态，使用 Alert 处理错误。

#### Scenario: 首次加载
- **WHEN** 页面首次加载且数据尚未返回
- **THEN** 页面 SHALL 使用 TDesign Skeleton 组件（animation="gradient"）展示页面骨架，模拟 Summary 区域和 Table 区域的大致结构

#### Scenario: API 请求失败
- **WHEN** 前端 API 请求失败
- **THEN** 页面 SHALL 使用 TDesign Alert 组件（theme=error）显示错误提示

