From 9f2b906063fb5ee95112d483f7712555f4cdd6fa Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Tue, 12 May 2026 12:42:11 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=85=A8=E9=9D=A2=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E5=89=8D=E7=AB=AF=E6=A0=B7=E5=BC=8F=EF=BC=8C=E6=B6=88?= =?UTF-8?q?=E9=99=A4=E5=86=85=E8=81=94=20style=20=E5=92=8C=E7=A1=AC?= =?UTF-8?q?=E7=BC=96=E7=A0=81=E8=89=B2=E5=80=BC=EF=BC=8C=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=20TDesign=20=E8=A7=84=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重写 styles.css:CSS 变量化可用率色阶、状态色类、工具类、安全选择器 - 组件改造:StatusDot/StatusBar/TargetDetailDrawer/GroupHeader 等改用 CSS 类和 Typography - color-threshold 移除 getLatencyColor 死代码,保留 getAvailabilityProgressColor 返回 CSS 变量 - target-table-columns 状态列和延迟列切换为 CSS 类 - 新增 css-utility-classes spec,更新 4 个 main specs(probe/card/table/drawer) - README 和 config.yaml 写入前端样式开发规范 --- README.md | 17 +++ openspec/config.yaml | 2 + openspec/specs/card-dashboard/spec.md | 14 ++- openspec/specs/css-utility-classes/spec.md | 69 +++++++++++++ openspec/specs/probe-dashboard/spec.md | 6 +- openspec/specs/target-detail-drawer/spec.md | 26 +++-- openspec/specs/target-table/spec.md | 18 ++-- src/web/app.tsx | 6 +- src/web/components/GroupHeader.tsx | 8 +- src/web/components/StatusBar.tsx | 7 +- src/web/components/StatusDot.tsx | 9 +- src/web/components/SummaryCards.tsx | 2 +- src/web/components/TargetBoard.tsx | 2 +- src/web/components/TargetDetailDrawer.tsx | 94 ++++++++--------- src/web/components/TargetGroup.tsx | 2 +- src/web/constants/color-threshold.ts | 22 +--- src/web/constants/target-table-columns.tsx | 12 +-- src/web/styles.css | 106 ++++++++++++++++--- tests/web/constants/color-threshold.test.ts | 108 ++++++++------------ 19 files changed, 332 insertions(+), 198 deletions(-) create mode 100644 openspec/specs/css-utility-classes/spec.md diff --git a/README.md b/README.md index 944d11e..ed57133 100644 --- a/README.md +++ b/README.md @@ -263,6 +263,23 @@ bun run verify 前端只通过 HTTP 调用后端,API 路径为 `/api/*`。共享类型放在 `src/shared`,前端不得 import `src/server` 的运行时实现。 +## 前端样式规范 + +前端基于 TDesign React 构建UI,样式开发遵循以下优先级(从高到低): + +1. **使用 TDesign 组件**:布局、间距、排版优先使用 TDesign 组件(如 Space、Divider、Typography) +2. **使用 TDesign 组件 props**:通过组件的 props 参数控制外观(如 `theme`、`variant`、`size`) +3. **使用 TDesign CSS tokens**:颜色、间距、字体等使用 `--td-*` CSS 变量(如 `--td-success-color`、`--td-comp-margin-xxl`) +4. **在 styles.css 中定义 CSS 类**:无法通过上述方式满足的样式需求,集中定义在 `styles.css` 中 +5. **自行开发组件**:仅在 TDesign 无法满足需求时自行开发 + +**红线**: + +- **严禁在组件中使用 `style` 属性内联调整样式** +- **严禁通过 CSS 覆盖 TDesign 组件内部类名**(如 `.t-tab-panel`),如需定制使用组件的 `className` prop +- **严禁使用 `!important`** +- 颜色统一使用 TDesign CSS tokens(`--td-success-color`、`--td-error-color`、`--td-warning-color` 等),不使用硬编码色值 + ## 目标状态判定 单层判定模型,适用于 HTTP 和 Command 两种类型: diff --git a/openspec/config.yaml b/openspec/config.yaml index 791790d..9fbac0d 100644 --- a/openspec/config.yaml +++ b/openspec/config.yaml @@ -11,6 +11,8 @@ context: | - src/server目录下是基于bun实现的后端代码 - src/web目录下是基于vite、react、TDesign实现的前端代码 - 代码开发优先使用公共组件实现功能逻辑(优先级:官方库>主流三方库>项目公共工具>自行实现) + - 前端样式开发优先级:TDesign组件 > 组件props > TDesign CSS tokens(--td-*) > styles.css CSS类 > 自行开发组件 + - 前端严禁:组件内联style属性、CSS覆盖TD内部类名、使用!important、硬编码色值 - Git提交: 仅中文; 格式"类型: 简短描述", 类型: feat/fix/refactor/docs/style/test/chore; 多行描述空行后写详细说明 - 禁止创建git操作task - 积极使用subagents精心设计并行任务,节省上下文空间,加速任务执行 diff --git a/openspec/specs/card-dashboard/spec.md b/openspec/specs/card-dashboard/spec.md index 5e8c3e3..450b2d9 100644 --- a/openspec/specs/card-dashboard/spec.md +++ b/openspec/specs/card-dashboard/spec.md @@ -5,7 +5,7 @@ ## Requirements ### Requirement: 分组卡片布局 -Dashboard SHALL 按 group 字段将目标分组,每个分组包含带统计的分组标题和独立 TDesign PrimaryTable 表格。 +Dashboard SHALL 按 group 字段将目标分组,每个分组包含带统计的分组标题和独立 TDesign PrimaryTable 表格。分组标题使用 TDesign Typography 组件渲染。 #### Scenario: 按分组展示目标 - **WHEN** 用户打开 Dashboard 页面 @@ -13,7 +13,7 @@ Dashboard SHALL 按 group 字段将目标分组,每个分组包含带统计的 #### Scenario: 分组标题带统计标签 - **WHEN** 页面渲染某个分组 -- **THEN** 分组标题 SHALL 使用 TDesign Space + Tag 组件显示分组名称和三个标签:总数(theme=primary, variant=light)、正常数(theme=success, variant=light)、异常数(theme=danger, variant=light),标签仅显示数字 +- **THEN** 分组标题 SHALL 使用 CSS flex 布局(`display:flex; align-items:center`)显示分组名称和三个标签:总数(theme=primary, variant=light)、正常数(theme=success, variant=light)、异常数(theme=danger, variant=light),标签仅显示数字。分组名称 SHALL 使用 TDesign Typography.Title 组件(level="h4")渲染,不使用原生 h2 标签和内联 style。Typography.Title 默认 margin SHALL 通过 CSS 覆盖归零 #### Scenario: 分组统计标签提示 - **WHEN** 鼠标悬停在分组统计标签上 @@ -23,6 +23,10 @@ Dashboard SHALL 按 group 字段将目标分组,每个分组包含带统计的 - **WHEN** 分组名称为 "default" - **THEN** 分组标题 SHALL 显示 "默认分组" +#### Scenario: 分组标题间距 +- **WHEN** 分组标题渲染 +- **THEN** 标题与表格之间的间距 SHALL 通过 CSS 类控制,不使用内联 style 的 marginBottom + ### Requirement: 响应式卡片网格 Dashboard SHALL 使用 TDesign PrimaryTable 展示每个分组的目标,表格宽度自适应容器。 @@ -70,7 +74,7 @@ Dashboard SHALL 使用 TDesign PrimaryTable 展示每个分组的目标,表格 - **THEN** 间隔列 SHALL 显示检查间隔(如 "5s"、"30s"),居中对齐,宽度 72px ### Requirement: 卡片交互 -表格行 SHALL 支持 hover 效果和点击打开 Drawer。 +表格行 SHALL 支持 hover 效果和点击打开 Drawer。cursor 样式通过 CSS 类实现。 #### Scenario: 行 hover 效果 - **WHEN** 鼠标悬停在表格行上 @@ -79,3 +83,7 @@ Dashboard SHALL 使用 TDesign PrimaryTable 展示每个分组的目标,表格 #### Scenario: 行点击打开详情 - **WHEN** 用户点击某个目标表格行 - **THEN** 系统 SHALL 打开该目标的详情 Drawer + +#### Scenario: 行 cursor 样式 +- **WHEN** 表格渲染 +- **THEN** PrimaryTable SHALL 通过 CSS 类 `.clickable-table` 设置 cursor: pointer,不使用内联 style diff --git a/openspec/specs/css-utility-classes/spec.md b/openspec/specs/css-utility-classes/spec.md new file mode 100644 index 0000000..851f86d --- /dev/null +++ b/openspec/specs/css-utility-classes/spec.md @@ -0,0 +1,69 @@ +## Purpose + +定义 styles.css 中集中管理的前端样式工具类和 CSS 自定义属性,供 TDesign 组件之外的自定义组件(StatusDot、StatusBar 等)引用。 + +## Requirements + +### 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`),不使用内联 style + +#### Scenario: 等宽数字类 +- **WHEN** 数值需要等宽显示 +- **THEN** 组件 SHALL 使用 `.tabular-nums` 类(font-variant-numeric: tabular-nums) + +#### Scenario: 延迟色值类 +- **WHEN** 延迟数值渲染 +- **THEN** 组件 SHALL 使用 `.latency-ok`(color: `--td-success-color`)、`.latency-warn`(color: `--td-warning-color`)或 `.latency-error`(color: `--td-error-color`)类,不使用内联 style + +#### Scenario: 全宽布局类 +- **WHEN** 组件需要占满父容器宽度 +- **THEN** 组件 SHALL 使用 `.full-width` 类(width: 100%),不使用内联 style + +#### Scenario: 可点击表格类 +- **WHEN** PrimaryTable 行支持点击交互 +- **THEN** 表格 SHALL 使用 `.clickable-table` 类(cursor: pointer),不使用内联 style + +#### Scenario: Tab 面板内边距类 +- **WHEN** Drawer 内 Tabs 面板需要内边距 +- **THEN** TabPanel SHALL 使用 `className="tab-panel-padded"` prop 传入类名,不通过入侵 TDesign 内部类名覆盖 + +### Requirement: 异常行背景类 +styles.css SHALL 定义 DOWN 行的背景色,使用安全选择器且不使用 `!important`。 + +#### Scenario: DOWN 行背景色 +- **WHEN** 表格行标记为 DOWN 状态 +- **THEN** 行 SHALL 通过 `.t-table tr.row-down` 选择器获得浅红色背景(引用 `--td-error-color-light` token),不使用 `!important` + +#### Scenario: DOWN 行 hover 状态 +- **WHEN** 鼠标悬停在 DOWN 行上 +- **THEN** 行背景 SHALL 通过 `.t-table--hoverable tbody tr.row-down:hover` 选择器显示 hover 状态色 diff --git a/openspec/specs/probe-dashboard/spec.md b/openspec/specs/probe-dashboard/spec.md index d954e27..0982382 100644 --- a/openspec/specs/probe-dashboard/spec.md +++ b/openspec/specs/probe-dashboard/spec.md @@ -42,7 +42,7 @@ Dashboard SHALL 使用 TDesign Drawer 展示目标详情,包含时间范围筛 - **THEN** Drawer SHALL 关闭 ### Requirement: 页面加载与错误状态 -Dashboard SHALL 使用 TDesign 组件正确处理加载状态和 API 错误。 +Dashboard SHALL 使用 TDesign 组件正确处理加载状态和 API 错误。页面标题使用 TDesign Typography 组件渲染。 #### Scenario: 首次加载 - **WHEN** 页面首次加载且数据尚未返回 @@ -55,3 +55,7 @@ Dashboard SHALL 使用 TDesign 组件正确处理加载状态和 API 错误。 #### Scenario: Drawer 内部加载状态 - **WHEN** Drawer 内趋势数据或历史记录正在加载 - **THEN** 概览面板的"趋势"区域 SHALL 显示 TDesign Skeleton 加载占位,记录表格 SHALL 显示 loading 状态 + +#### Scenario: 页面标题 +- **WHEN** Dashboard 页面渲染 +- **THEN** 页面标题 SHALL 使用 TDesign Typography.Title 组件(level="h1")渲染"DiAL",副标题 SHALL 使用 Typography.Text 组件(theme="secondary")渲染"统一拨测平台",不使用原生 h1/p 标签和内联 style diff --git a/openspec/specs/target-detail-drawer/spec.md b/openspec/specs/target-detail-drawer/spec.md index 7d32b8e..675fad8 100644 --- a/openspec/specs/target-detail-drawer/spec.md +++ b/openspec/specs/target-detail-drawer/spec.md @@ -5,7 +5,7 @@ ## Requirements ### Requirement: 目标详情 Drawer -Dashboard SHALL 在用户点击目标表格行后从右侧滑出 Drawer,展示该目标的详细统计信息和检查记录。 +Dashboard SHALL 在用户点击目标表格行后从右侧滑出 Drawer,展示该目标的详细统计信息和检查记录。Drawer 标题栏和内容不使用内联 style。 #### Scenario: 打开 Drawer - **WHEN** 用户点击某个目标表格行 @@ -13,7 +13,7 @@ Dashboard SHALL 在用户点击目标表格行后从右侧滑出 Drawer,展示 #### Scenario: Drawer 标题栏 - **WHEN** Drawer 渲染 -- **THEN** 标题栏 SHALL 显示 StatusDot、目标名称和类型标签(TDesign Tag),以及内建关闭按钮 +- **THEN** 标题栏 SHALL 使用 TDesign Space 组件(align="center")布局,包含 StatusDot、目标名称(TDesign Typography.Text strong)和类型标签(TDesign Tag),以及内建关闭按钮。不使用内联 style 的 flex 布局 #### Scenario: 关闭 Drawer - **WHEN** 用户点击关闭按钮、ESC 键或遮罩层 @@ -31,6 +31,10 @@ Dashboard SHALL 在用户点击目标表格行后从右侧滑出 Drawer,展示 - **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: 时间范围选择器 Drawer SHALL 在 Tabs 外层提供时间范围选择器,影响概览和记录两个面板的数据。时间选择器 SHALL 分两行显示:第一行为快捷按钮,第二行为日期时间范围选择器。 @@ -52,7 +56,7 @@ Drawer SHALL 在 Tabs 外层提供时间范围选择器,影响概览和记录 #### Scenario: DateRangePicker 全宽显示 - **WHEN** Drawer 渲染 -- **THEN** DateRangePicker SHALL 占满时间选择区第二行的宽度(width: 100%) +- **THEN** DateRangePicker SHALL 通过 CSS 类 `.full-width` 占满时间选择区第二行的宽度,不使用内联 style 的 width: 100% #### Scenario: 默认时间范围 - **WHEN** Drawer 打开 @@ -63,22 +67,30 @@ Drawer SHALL 在 Tabs 外层提供时间范围选择器,影响概览和记录 - **THEN** 系统 SHALL 重新请求趋势数据和历史记录 ### Requirement: Tabs 内容组织 -Drawer 内部 SHALL 使用 TDesign Tabs 组织概览和记录两个面板。 +Drawer 内部 SHALL 使用 TDesign Tabs 组织概览和记录两个面板。TabPanel 内边距通过 className prop 控制。 #### Scenario: Tab 标签 - **WHEN** Drawer 渲染 - **THEN** Tabs SHALL 显示两个标签:概览、记录 +#### Scenario: Tab 面板内边距 +- **WHEN** TabPanel 渲染 +- **THEN** TabPanel SHALL 通过 `className` prop 传入自定义类名(`tab-panel-padded`)控制内边距,不通过入侵 TDesign 内部类名(`.t-tab-panel`)覆盖 + ### Requirement: 概览面板 -概览 Tab SHALL 按区域展示目标统计摘要、趋势图、状态分布和基本信息,每个区域使用小标题分隔。 +概览 Tab SHALL 按区域展示目标统计摘要、趋势图、状态分布和基本信息,每个区域使用 TDesign Divider 组件作为小标题分隔。 #### Scenario: 区域排列顺序 - **WHEN** 概览面板渲染 -- **THEN** 面板 SHALL 按以下顺序展示区域:统计 → 趋势 → 状态分布 → 基本信息,每个区域前 SHALL 显示小标题 +- **THEN** 面板 SHALL 按以下顺序展示区域:统计 → 趋势 → 状态分布 → 基本信息,每个区域前 SHALL 显示 TDesign Divider(align="left")作为小标题,不使用内联 style 的 h4 标签 + +#### Scenario: 区域间距 +- **WHEN** 概览面板渲染 +- **THEN** 各区域之间的间距 SHALL 通过 TDesign Space 组件(direction="vertical")统一管理,不使用内联 style 的 margin #### Scenario: 统计数值卡片 - **WHEN** 概览面板渲染 -- **THEN** 面板 SHALL 在"统计"区域使用 TDesign Statistic 组件展示 4 个统计值:总检查(color=blue)、正常(color=green)、异常(color=red)、可用率(color=green, suffix="%"),使用 TDesign Row/Col 横向排列 +- **THEN** 面板 SHALL 在"统计"区域使用 TDesign Statistic 组件展示 4 个统计值:总检查(color=blue)、正常(color=green)、异常(color=red)、可用率(color=green, suffix="%"),使用 TDesign Row/Col 横向排列。Row 的外层间距 SHALL 通过 TDesign Space 或 CSS 类控制,不使用内联 style #### Scenario: 趋势折线图 - **WHEN** 概览面板渲染且趋势数据可用 diff --git a/openspec/specs/target-table/spec.md b/openspec/specs/target-table/spec.md index 92baf57..3090212 100644 --- a/openspec/specs/target-table/spec.md +++ b/openspec/specs/target-table/spec.md @@ -24,11 +24,11 @@ Dashboard SHALL 按 group 字段将目标分组,每个分组渲染一个独立 - **THEN** 分组标题 SHALL 显示 "默认分组" ### Requirement: 表格列定义 -每个分组的 PrimaryTable SHALL 包含状态、名称、类型、可用率、最近状态条、延迟、间隔 7 列,不含分组列(同组内冗余)。 +每个分组的 PrimaryTable SHALL 包含状态、名称、类型、可用率、最近状态条、延迟、间隔 7 列,不含分组列(同组内冗余)。列渲染不使用内联 style。 #### Scenario: 状态列 - **WHEN** 表格渲染 -- **THEN** 状态列 SHALL 使用 StatusDot 组件渲染,fixed="left",宽度 80px,居中对齐,支持筛选(UP/DOWN/全部) +- **THEN** 状态列 SHALL 使用 StatusDot 组件渲染,标题显示"#",宽度 60px,fixed="left",居中对齐,支持筛选(UP/DOWN/全部)。StatusDot SHALL 通过 CSS 类(`.status-dot--up` / `.status-dot--down`)控制颜色,不使用内联 style #### Scenario: 名称列 - **WHEN** 表格渲染 @@ -40,15 +40,15 @@ Dashboard SHALL 按 group 字段将目标分组,每个分组渲染一个独立 #### Scenario: 可用率列 - **WHEN** 表格渲染 -- **THEN** 可用率列 SHALL 使用 TDesign Progress 组件(theme=line, size=small)渲染,颜色按可用率数值每 10% 一档:0-10% 最红(#d54941),每升高 10% 色阶偏移一档,经过橙色区间,90-100% 最绿(#3dba60),label 显示百分比数值,支持排序(升序优先,最差排最前) +- **THEN** 可用率列 SHALL 使用 TDesign Progress 组件(theme=line, size=small)渲染,颜色通过 CSS 自定义属性 `--avail-N`(基于项目自定义色值)控制,每 10% 一档,label 显示百分比数值,支持排序(升序优先,最差排最前)。color-threshold 函数 SHALL 返回 CSS 自定义属性引用而非硬编码色值 #### Scenario: 最近状态列 - **WHEN** 表格渲染 -- **THEN** 最近状态列 SHALL 使用 StatusBar 组件渲染 30 格采样色块,宽度 220px,色块使用 flex:1 自适应列宽 +- **THEN** 最近状态列 SHALL 使用 StatusBar 组件渲染 30 格采样色块,宽度 220px。StatusBar SHALL 通过 CSS 类(`.status-bar-block--up` / `.status-bar-block--down` / `.status-bar-block--empty`)控制色块颜色,不使用内联 style #### Scenario: 延迟列 - **WHEN** 表格渲染 -- **THEN** 延迟列 SHALL 显示最近一次检查的延迟毫秒数,右对齐,颜色根据阈值变化:≤100ms 使用 --td-success-color、100-500ms 使用 --td-warning-color、>500ms 使用 --td-error-color,无数据显示"-",支持数值排序 +- **THEN** 延迟列 SHALL 显示最近一次检查的延迟毫秒数,右对齐。颜色 SHALL 通过 CSS 类实现:≤100ms 使用 `.latency-ok`、100-500ms 使用 `.latency-warn`、>500ms 使用 `.latency-error`。无数据 SHALL 使用 `.text-disabled` 类显示 "-",数值 SHALL 使用 `.tabular-nums` 类等宽显示。不使用内联 style #### Scenario: 间隔列 - **WHEN** 表格渲染 @@ -62,11 +62,15 @@ Dashboard SHALL 按 group 字段将目标分组,每个分组渲染一个独立 - **THEN** 每个分组表格 SHALL 默认按状态降序排列,DOWN 目标排在同组最前面 ### Requirement: DOWN 行视觉强化 -表格中状态为 DOWN 的行 SHALL 具有视觉区分。 +表格中状态为 DOWN 的行 SHALL 具有视觉区分,使用安全 CSS 选择器实现。 #### Scenario: DOWN 行背景色 - **WHEN** 目标最近一次检查 matched=false -- **THEN** 该行 SHALL 使用浅红色背景(--td-error-color-light),与正常行形成视觉区分 +- **THEN** 该行 SHALL 通过 `.t-table tr.row-down` CSS 选择器获得浅红色背景(`--td-error-color-light`),不使用 `!important` + +#### Scenario: DOWN 行 hover 状态 +- **WHEN** 鼠标悬停在 DOWN 行上 +- **THEN** 行背景 SHALL 通过 `.t-table--hoverable tbody tr.row-down:hover` 选择器显示 hover 状态色,与正常行 hover 效果协调 ### Requirement: 行点击交互 表格行 SHALL 支持点击打开目标详情 Drawer。 diff --git a/src/web/app.tsx b/src/web/app.tsx index dbebf61..bd2b212 100644 --- a/src/web/app.tsx +++ b/src/web/app.tsx @@ -1,4 +1,4 @@ -import { Alert, Loading } from "tdesign-react"; +import { Alert, Loading, Typography } from "tdesign-react"; import { useSummary, useTargets, useTargetDetail } from "./hooks/useTargetDetail"; import { SummaryCards } from "./components/SummaryCards"; import { TargetBoard } from "./components/TargetBoard"; @@ -26,8 +26,8 @@ export function App() { return (
-

DiAL

-

统一拨测平台

+ DiAL + 统一拨测平台
{error && } diff --git a/src/web/components/GroupHeader.tsx b/src/web/components/GroupHeader.tsx index aeb3f2d..6096638 100644 --- a/src/web/components/GroupHeader.tsx +++ b/src/web/components/GroupHeader.tsx @@ -1,4 +1,4 @@ -import { Space, Tag } from "tdesign-react"; +import { Tag, Typography } from "tdesign-react"; interface GroupHeaderProps { name: string; @@ -11,8 +11,8 @@ export function GroupHeader({ name, total, up, down }: GroupHeaderProps) { const displayName = name === "default" ? "默认分组" : name; return ( - -

{displayName}

+
+ {displayName} {total} @@ -22,6 +22,6 @@ export function GroupHeader({ name, total, up, down }: GroupHeaderProps) { {down} - +
); } diff --git a/src/web/components/StatusBar.tsx b/src/web/components/StatusBar.tsx index 630eec3..7d705bc 100644 --- a/src/web/components/StatusBar.tsx +++ b/src/web/components/StatusBar.tsx @@ -10,14 +10,11 @@ export function StatusBar({ samples }: StatusBarProps) { blocks.push( , ); } else { - blocks.push( - , - ); + blocks.push(); } } diff --git a/src/web/components/StatusDot.tsx b/src/web/components/StatusDot.tsx index 11f2de5..61793f4 100644 --- a/src/web/components/StatusDot.tsx +++ b/src/web/components/StatusDot.tsx @@ -3,12 +3,5 @@ interface StatusDotProps { } export function StatusDot({ up }: StatusDotProps) { - const color = up ? "var(--td-success-color)" : "var(--td-error-color)"; - const shadow = up ? "var(--td-success-color)" : "var(--td-error-color)"; - return ( - - ); + return ; } diff --git a/src/web/components/SummaryCards.tsx b/src/web/components/SummaryCards.tsx index 551a9b6..e413cd8 100644 --- a/src/web/components/SummaryCards.tsx +++ b/src/web/components/SummaryCards.tsx @@ -15,7 +15,7 @@ export function SummaryCards({ summary }: SummaryCardsProps) { ]; return ( - + {cards.map((card) => ( diff --git a/src/web/components/TargetBoard.tsx b/src/web/components/TargetBoard.tsx index fd9cd7d..eb85c0c 100644 --- a/src/web/components/TargetBoard.tsx +++ b/src/web/components/TargetBoard.tsx @@ -25,7 +25,7 @@ export function TargetBoard({ targets, onTargetClick }: TargetBoardProps) { }); return ( - + {sortedGroups.map(([name, groupTargets]) => ( ))} diff --git a/src/web/components/TargetDetailDrawer.tsx b/src/web/components/TargetDetailDrawer.tsx index deed717..dc8c485 100644 --- a/src/web/components/TargetDetailDrawer.tsx +++ b/src/web/components/TargetDetailDrawer.tsx @@ -11,6 +11,9 @@ import { Descriptions, Skeleton, PrimaryTable, + Divider, + Space, + Typography, } from "tdesign-react"; import type { TabValue } from "tdesign-react"; import type { CheckResult, TargetStatus, TrendPoint, HistoryResponse } from "../../shared/api"; @@ -40,13 +43,6 @@ const TIME_SHORTCUTS = [ { label: "7天", hours: 168, value: "7d" }, ] as const; -const SECTION_TITLE_STYLE: React.CSSProperties = { - fontSize: 14, - fontWeight: 600, - color: "var(--td-text-color-primary)", - margin: "16px 0 8px", -}; - const HISTORY_COLUMNS = [ { colKey: "matched", @@ -136,16 +132,16 @@ export function TargetDetailDrawer({ footer={false} onClose={onClose} header={ -
+ - {target.name} + {target.name} {getTargetTypeDisplay(target.type)} -
+
} > -
+ ({ label: s.label, value: s.value }))} onChange={handleShortcut} /> -
-
-
+
- setActiveTab(val)}> - -

统计

- - - - - - - - - - - - - - + setActiveTab(val)}> + + + 统计 + + + + + + + + + + + + + + -

趋势

- {trendLoading ? : } + 趋势 + {trendLoading ? : } -

状态分布

- + 状态分布 + -

基本信息

- + 基本信息 + +
- + ); diff --git a/src/web/constants/color-threshold.ts b/src/web/constants/color-threshold.ts index c351e98..1d8275a 100644 --- a/src/web/constants/color-threshold.ts +++ b/src/web/constants/color-threshold.ts @@ -1,24 +1,4 @@ -const AVAILABILITY_COLORS = [ - "#d54941", // 0-10% - "#d96241", // 10-20% - "#e37318", // 20-30% - "#e89318", // 30-40% - "#d9a818", // 40-50% - "#b8b020", // 50-60% - "#8dba30", // 60-70% - "#6dba3f", // 70-80% - "#4dba50", // 80-90% - "#3dba60", // 90-100% -]; - export function getAvailabilityProgressColor(availability: number): string { const index = Math.min(Math.floor(availability / 10), 9); - return AVAILABILITY_COLORS[index]!; -} - - -export function getLatencyColor(ms: number): string { - if (ms <= 100) return "var(--td-success-color)"; - if (ms <= 500) return "var(--td-warning-color)"; - return "var(--td-error-color)"; + return `var(--avail-${index})`; } diff --git a/src/web/constants/target-table-columns.tsx b/src/web/constants/target-table-columns.tsx index a99c61e..deed4b1 100644 --- a/src/web/constants/target-table-columns.tsx +++ b/src/web/constants/target-table-columns.tsx @@ -4,15 +4,15 @@ import type { TargetStatus } from "../../shared/api"; import { StatusDot } from "../components/StatusDot"; import { StatusBar } from "../components/StatusBar"; import { getTargetTypeDisplay } from "./target-type-display"; -import { getAvailabilityProgressColor, getLatencyColor } from "./color-threshold"; +import { getAvailabilityProgressColor } from "./color-threshold"; import { availabilitySorter, latencySorter, nameSorter } from "./target-table-sorters"; import { statusFilter, typeFilter } from "./target-table-filters"; export const TARGET_TABLE_COLUMNS: PrimaryTableCol[] = [ { colKey: "latestCheck.matched", - title: "状态", - width: 80, + title: "#", + width: 60, fixed: "left", align: "center", filter: statusFilter, @@ -72,9 +72,9 @@ export const TARGET_TABLE_COLUMNS: PrimaryTableCol[] = [ sortType: "all", cell: ({ row }: PrimaryTableCellParams) => { const ms = row.latestCheck?.durationMs; - if (ms === null || ms === undefined) return -; - const color = getLatencyColor(ms); - return {Math.round(ms)}ms; + if (ms === null || ms === undefined) return -; + const colorClass = ms <= 100 ? "latency-ok" : ms <= 500 ? "latency-warn" : "latency-error"; + return {Math.round(ms)}ms; }, }, { diff --git a/src/web/styles.css b/src/web/styles.css index d9917f7..5e02321 100644 --- a/src/web/styles.css +++ b/src/web/styles.css @@ -1,22 +1,28 @@ +:root { + --avail-0: #d54941; + --avail-1: #d96241; + --avail-2: #e37318; + --avail-3: #e89318; + --avail-4: #d9a818; + --avail-5: #b8b020; + --avail-6: #8dba30; + --avail-7: #6dba3f; + --avail-8: #4dba50; + --avail-9: #3dba60; +} + .dashboard { - padding: 32px 24px; + padding: var(--td-comp-paddingTB-xl) var(--td-comp-paddingLR-xl); width: 100%; } .dashboard-header { - margin-bottom: 32px; + margin-bottom: var(--td-comp-margin-l); } -.dashboard-header h1 { - margin: 0 0 4px; - font-size: 1.75rem; - letter-spacing: -0.03em; -} - -.dashboard-subtitle { +.dashboard-header .t-typography { margin: 0; - color: var(--td-text-color-secondary); - font-size: 0.9rem; + line-height: 1.3; } .status-dot { @@ -27,6 +33,16 @@ flex-shrink: 0; } +.status-dot--up { + background: var(--td-success-color); + box-shadow: 0 0 0 6px color-mix(in srgb, var(--td-success-color) 14%, transparent); +} + +.status-dot--down { + background: var(--td-error-color); + box-shadow: 0 0 0 6px color-mix(in srgb, var(--td-error-color) 14%, transparent); +} + .status-bar { display: flex; gap: 2px; @@ -41,6 +57,18 @@ border-radius: 2px; } +.status-bar-block--up { + background: var(--td-success-color); +} + +.status-bar-block--down { + background: var(--td-error-color); +} + +.status-bar-block--empty { + background: var(--td-bg-color-component-disabled); +} + .status-donut { position: relative; display: flex; @@ -71,10 +99,60 @@ font-size: 0.85rem; } -.drawer-tabs .t-tab-panel { +.tab-panel-padded { padding: 15px; } -.row-down { - background: color-mix(in srgb, var(--td-error-color) 6%, transparent) !important; +.t-table tr.row-down { + background: color-mix(in srgb, var(--td-error-color) 6%, transparent); +} + +.t-table--hoverable tbody tr.row-down:hover { + background: color-mix(in srgb, var(--td-error-color) 10%, transparent); +} + +.text-disabled { + color: var(--td-text-color-disabled); +} + +.tabular-nums { + font-variant-numeric: tabular-nums; +} + +.latency-ok { + color: var(--td-success-color); +} + +.latency-warn { + color: var(--td-warning-color); +} + +.latency-error { + color: var(--td-error-color); +} + +.full-width { + width: 100%; +} + +.clickable-table { + cursor: pointer; +} + +.group-header { + margin-bottom: var(--td-comp-margin-m); + display: flex; + align-items: center; + gap: 8px; +} + +.group-header .t-typography { + margin: 0; + font-size: var(--td-font-size-title-medium); + font-weight: 600; + line-height: 1.5; +} + +.summary-cards-row { + margin-bottom: var(--td-comp-margin-xl); } diff --git a/tests/web/constants/color-threshold.test.ts b/tests/web/constants/color-threshold.test.ts index e67de68..e4d3d37 100644 --- a/tests/web/constants/color-threshold.test.ts +++ b/tests/web/constants/color-threshold.test.ts @@ -1,95 +1,69 @@ import { describe, test, expect } from "bun:test"; -import { getAvailabilityProgressColor, getLatencyColor } from "../../../src/web/constants/color-threshold"; +import { getAvailabilityProgressColor } from "../../../src/web/constants/color-threshold"; describe("color-threshold", () => { describe("getAvailabilityProgressColor", () => { - test("0-10% 返回第一档颜色", () => { - expect(getAvailabilityProgressColor(0)).toBe("#d54941"); - expect(getAvailabilityProgressColor(5)).toBe("#d54941"); - expect(getAvailabilityProgressColor(9.99)).toBe("#d54941"); + test("0-10% 返回第一档 CSS 变量", () => { + expect(getAvailabilityProgressColor(0)).toBe("var(--avail-0)"); + expect(getAvailabilityProgressColor(5)).toBe("var(--avail-0)"); + expect(getAvailabilityProgressColor(9.99)).toBe("var(--avail-0)"); }); - test("10-20% 返回第二档颜色", () => { - expect(getAvailabilityProgressColor(10)).toBe("#d96241"); - expect(getAvailabilityProgressColor(15)).toBe("#d96241"); - expect(getAvailabilityProgressColor(19.99)).toBe("#d96241"); + test("10-20% 返回第二档 CSS 变量", () => { + expect(getAvailabilityProgressColor(10)).toBe("var(--avail-1)"); + expect(getAvailabilityProgressColor(15)).toBe("var(--avail-1)"); + expect(getAvailabilityProgressColor(19.99)).toBe("var(--avail-1)"); }); - test("20-30% 返回第三档颜色", () => { - expect(getAvailabilityProgressColor(20)).toBe("#e37318"); - expect(getAvailabilityProgressColor(25)).toBe("#e37318"); + test("20-30% 返回第三档 CSS 变量", () => { + expect(getAvailabilityProgressColor(20)).toBe("var(--avail-2)"); + expect(getAvailabilityProgressColor(25)).toBe("var(--avail-2)"); }); - test("30-40% 返回第四档颜色", () => { - expect(getAvailabilityProgressColor(30)).toBe("#e89318"); - expect(getAvailabilityProgressColor(35)).toBe("#e89318"); + test("30-40% 返回第四档 CSS 变量", () => { + expect(getAvailabilityProgressColor(30)).toBe("var(--avail-3)"); + expect(getAvailabilityProgressColor(35)).toBe("var(--avail-3)"); }); - test("40-50% 返回第五档颜色", () => { - expect(getAvailabilityProgressColor(40)).toBe("#d9a818"); - expect(getAvailabilityProgressColor(45)).toBe("#d9a818"); + test("40-50% 返回第五档 CSS 变量", () => { + expect(getAvailabilityProgressColor(40)).toBe("var(--avail-4)"); + expect(getAvailabilityProgressColor(45)).toBe("var(--avail-4)"); }); - test("50-60% 返回第六档颜色", () => { - expect(getAvailabilityProgressColor(50)).toBe("#b8b020"); - expect(getAvailabilityProgressColor(55)).toBe("#b8b020"); + test("50-60% 返回第六档 CSS 变量", () => { + expect(getAvailabilityProgressColor(50)).toBe("var(--avail-5)"); + expect(getAvailabilityProgressColor(55)).toBe("var(--avail-5)"); }); - test("60-70% 返回第七档颜色", () => { - expect(getAvailabilityProgressColor(60)).toBe("#8dba30"); - expect(getAvailabilityProgressColor(65)).toBe("#8dba30"); + test("60-70% 返回第七档 CSS 变量", () => { + expect(getAvailabilityProgressColor(60)).toBe("var(--avail-6)"); + expect(getAvailabilityProgressColor(65)).toBe("var(--avail-6)"); }); - test("70-80% 返回第八档颜色", () => { - expect(getAvailabilityProgressColor(70)).toBe("#6dba3f"); - expect(getAvailabilityProgressColor(75)).toBe("#6dba3f"); + test("70-80% 返回第八档 CSS 变量", () => { + expect(getAvailabilityProgressColor(70)).toBe("var(--avail-7)"); + expect(getAvailabilityProgressColor(75)).toBe("var(--avail-7)"); }); - test("80-90% 返回第九档颜色", () => { - expect(getAvailabilityProgressColor(80)).toBe("#4dba50"); - expect(getAvailabilityProgressColor(85)).toBe("#4dba50"); + test("80-90% 返回第九档 CSS 变量", () => { + expect(getAvailabilityProgressColor(80)).toBe("var(--avail-8)"); + expect(getAvailabilityProgressColor(85)).toBe("var(--avail-8)"); }); - test("90-100% 返回第十档颜色", () => { - expect(getAvailabilityProgressColor(90)).toBe("#3dba60"); - expect(getAvailabilityProgressColor(95)).toBe("#3dba60"); - expect(getAvailabilityProgressColor(99.9)).toBe("#3dba60"); - expect(getAvailabilityProgressColor(100)).toBe("#3dba60"); + test("90-100% 返回第十档 CSS 变量", () => { + expect(getAvailabilityProgressColor(90)).toBe("var(--avail-9)"); + expect(getAvailabilityProgressColor(95)).toBe("var(--avail-9)"); + expect(getAvailabilityProgressColor(99.9)).toBe("var(--avail-9)"); + expect(getAvailabilityProgressColor(100)).toBe("var(--avail-9)"); }); test("边界值", () => { - expect(getAvailabilityProgressColor(9.999)).toBe("#d54941"); - expect(getAvailabilityProgressColor(10)).toBe("#d96241"); - expect(getAvailabilityProgressColor(19.999)).toBe("#d96241"); - expect(getAvailabilityProgressColor(20)).toBe("#e37318"); - expect(getAvailabilityProgressColor(89.999)).toBe("#4dba50"); - expect(getAvailabilityProgressColor(90)).toBe("#3dba60"); - }); - }); - - describe("getLatencyColor", () => { - test("<=100ms 返回 success 色", () => { - expect(getLatencyColor(0)).toBe("var(--td-success-color)"); - expect(getLatencyColor(50)).toBe("var(--td-success-color)"); - expect(getLatencyColor(100)).toBe("var(--td-success-color)"); - }); - - test("100-500ms 返回 warning 色", () => { - expect(getLatencyColor(101)).toBe("var(--td-warning-color)"); - expect(getLatencyColor(250)).toBe("var(--td-warning-color)"); - expect(getLatencyColor(500)).toBe("var(--td-warning-color)"); - }); - - test(">500ms 返回 error 色", () => { - expect(getLatencyColor(501)).toBe("var(--td-error-color)"); - expect(getLatencyColor(1000)).toBe("var(--td-error-color)"); - }); - - test("边界值", () => { - expect(getLatencyColor(100)).toBe("var(--td-success-color)"); - expect(getLatencyColor(100.01)).toBe("var(--td-warning-color)"); - expect(getLatencyColor(500)).toBe("var(--td-warning-color)"); - expect(getLatencyColor(500.01)).toBe("var(--td-error-color)"); + expect(getAvailabilityProgressColor(9.999)).toBe("var(--avail-0)"); + expect(getAvailabilityProgressColor(10)).toBe("var(--avail-1)"); + expect(getAvailabilityProgressColor(19.999)).toBe("var(--avail-1)"); + expect(getAvailabilityProgressColor(20)).toBe("var(--avail-2)"); + expect(getAvailabilityProgressColor(89.999)).toBe("var(--avail-8)"); + expect(getAvailabilityProgressColor(90)).toBe("var(--avail-9)"); }); }); });