feat: Drawer 响应式默认宽度与拖拽调整,统计卡片上下布局优化
Drawer 宽度从固定百分比改为按视口响应式默认值(6段断点),宽屏占比更小、窄屏占比更大。 启用 TDesign sizeDraggable 原生拖拽调整能力,配置 min/max 视口安全边界,不持久化拖拽宽度。 概览统计卡片改为 TDesign Statistic 上下布局(与 SummaryCards 一致),提升窄屏视觉体验。 Drawer header 间距调大,MutationObserver polyfill 补全。
This commit is contained in:
@@ -563,7 +563,7 @@ main.tsx
|
||||
│ ├── useMeta() ───── GET /api/meta(应用生命周期内缓存)
|
||||
│ └── TargetGroup[](Card 包裹 PrimaryTable,headerBordered)
|
||||
│ └── PrimaryTable ← createTargetTableColumns(checkerTypes)
|
||||
│ └── TargetDetailDrawer(目标详情抽屉,width=52%,TDesign 生命周期控制)
|
||||
│ └── TargetDetailDrawer(目标详情抽屉,响应式默认宽度、支持鼠标拖拽调整,TDesign 生命周期控制)
|
||||
│ └── useTargetDetail() ── 按需发起 metrics 查询,history 延迟到记录 Tab 激活后请求
|
||||
│ ├── activeTab 受控 Tabs 状态,每次打开重置为 overview
|
||||
│ ├── OverviewTab → Descriptions(直接展示)+ 4×2 统计卡片 + TrendChart
|
||||
@@ -689,19 +689,19 @@ export function TargetGroup({ name, targets, onTargetClick }: TargetGroupProps)
|
||||
|
||||
#### 现有组件清单
|
||||
|
||||
| 组件 | 文件 | 用途 |
|
||||
| -------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
||||
| `App` | `app.tsx` | 根组件,Layout + HeadMenu 骨架、主题模式选择、刷新倒计时、Skeleton 加载 |
|
||||
| `ErrorBoundary` | `components/ErrorBoundary.tsx` | React 错误边界,捕获渲染异常并展示降级 UI |
|
||||
| `SummaryCards` | `components/SummaryCards.tsx` | 总览统计卡片(单 Card 内嵌居中 Statistic,无 shadow) |
|
||||
| `TargetBoard` | `components/TargetBoard.tsx` | 按分组渲染目标表格列表(Space 24px 间距) |
|
||||
| `TargetGroup` | `components/TargetGroup.tsx` | 单个分组 Card(title+actions+headerBordered)+ PrimaryTable |
|
||||
| `TargetDetailDrawer` | `components/TargetDetailDrawer.tsx` | 目标详情抽屉(width=52%、TDesign 生命周期控制、preventScrollThrough、受控 Tabs、记录 TabPanel 懒渲染) |
|
||||
| `OverviewTab` | `components/OverviewTab.tsx` | 目标详情概览(Descriptions 直接展示 + 4×2 统计卡片 + 趋势) |
|
||||
| `HistoryTab` | `components/HistoryTab.tsx` | 目标历史记录表格和分页 |
|
||||
| `TrendChart` | `components/TrendChart.tsx` | Recharts 趋势折线图(耗时+延迟范围) |
|
||||
| `StatusDot` | `components/StatusDot.tsx` | 圆形状态指示点(绿/红) |
|
||||
| `StatusBar` | `components/StatusBar.tsx` | 最近采样状态条(多色块 + Tooltip 提示时间和状态) |
|
||||
| 组件 | 文件 | 用途 |
|
||||
| -------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `App` | `app.tsx` | 根组件,Layout + HeadMenu 骨架、主题模式选择、刷新倒计时、Skeleton 加载 |
|
||||
| `ErrorBoundary` | `components/ErrorBoundary.tsx` | React 错误边界,捕获渲染异常并展示降级 UI |
|
||||
| `SummaryCards` | `components/SummaryCards.tsx` | 总览统计卡片(单 Card 内嵌居中 Statistic,无 shadow) |
|
||||
| `TargetBoard` | `components/TargetBoard.tsx` | 按分组渲染目标表格列表(Space 24px 间距) |
|
||||
| `TargetGroup` | `components/TargetGroup.tsx` | 单个分组 Card(title+actions+headerBordered)+ PrimaryTable |
|
||||
| `TargetDetailDrawer` | `components/TargetDetailDrawer.tsx` | 目标详情抽屉(响应式默认宽度、支持鼠标拖拽调整、TDesign 生命周期控制、preventScrollThrough、受控 Tabs、记录 TabPanel 懒渲染) |
|
||||
| `OverviewTab` | `components/OverviewTab.tsx` | 目标详情概览(Descriptions 直接展示 + 4×2 统计卡片 + 趋势) |
|
||||
| `HistoryTab` | `components/HistoryTab.tsx` | 目标历史记录表格和分页 |
|
||||
| `TrendChart` | `components/TrendChart.tsx` | Recharts 趋势折线图(耗时+延迟范围) |
|
||||
| `StatusDot` | `components/StatusDot.tsx` | 圆形状态指示点(绿/红) |
|
||||
| `StatusBar` | `components/StatusBar.tsx` | 最近采样状态条(多色块 + Tooltip 提示时间和状态) |
|
||||
|
||||
### 2.5 新增功能开发步骤
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-05-15
|
||||
64
openspec/changes/responsive-resizable-drawer/design.md
Normal file
64
openspec/changes/responsive-resizable-drawer/design.md
Normal file
@@ -0,0 +1,64 @@
|
||||
## Context
|
||||
|
||||
目标详情 Drawer 目前通过 TDesign React `Drawer` 渲染,右侧弹出、挂载到 `body`,并通过 `destroyOnClose=false` 保留子树。既有规格要求固定宽度为 52%,实际组件当前使用固定百分比宽度。固定比例无法同时适配窄屏和宽屏:窄屏需要更高占比保证内容可读,宽屏需要更低占比避免详情抽屉吞掉主面板空间。
|
||||
|
||||
TDesign React 1.16.9 的 `Drawer` 原生提供 `size`、`sizeDraggable` 和 `onSizeDragEnd` 能力。项目规范要求前端优先使用 TDesign 组件和组件 props,不引入新依赖,不使用组件内联 `style`,不覆盖 TDesign 内部类名。因此本变更应复用 Drawer 原生拖拽能力,并将响应式默认宽度表达为业务 CSS 类或 CSS 自定义属性。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Drawer 默认宽度按视口响应式变化:宽屏占比更小,窄屏占比更大。
|
||||
- 用户可以用鼠标拖动 Drawer 边缘调整宽度。
|
||||
- 拖拽宽度限制在安全范围内,避免过窄不可读或过宽超出视口。
|
||||
- 拖拽后的宽度只在当前页面生命周期内生效,不跨页面刷新持久化。
|
||||
- 保持现有 Drawer 生命周期、滚动穿透、Tabs、时间筛选和数据查询行为不变。
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- 不将 Drawer 宽度写入 `localStorage`、后端、URL 或其他跨刷新存储。
|
||||
- 不实现自定义拖拽条、全局鼠标事件或覆盖 TDesign 内部 DOM 样式。
|
||||
- 不改变后端 API、数据模型、目标详情查询逻辑和历史记录分页逻辑。
|
||||
- 不引入新的状态管理库、布局库或拖拽依赖。
|
||||
|
||||
## Decisions
|
||||
|
||||
### 使用 TDesign Drawer 原生 `sizeDraggable`
|
||||
|
||||
启用 `sizeDraggable` 来提供边缘拖拽调整能力,并通过其 `min`、`max` 限制拖拽范围。这样可以复用 TDesign 已有的拖拽命中区域、鼠标事件、宽度计算和 `col-resize` 光标行为。
|
||||
|
||||
替代方案是自定义拖拽手柄并监听 `mousemove`/`mouseup`。该方案会重复 TDesign 已有能力,增加事件清理、滚动穿透和可访问性风险,也违背项目优先使用 TDesign props 的规范,因此不采用。
|
||||
|
||||
### 用 CSS 自定义属性表达响应式默认宽度
|
||||
|
||||
Drawer `size` 传入业务 CSS 变量,例如 `var(--target-detail-drawer-width)`,并通过 Drawer 的业务 `className` 在 `styles.css` 中定义不同断点下的变量值。TDesign 最终会将该字符串作为内容 wrapper 的 width 值使用,CSS 变量会从 Drawer 根节点继承到内容节点。
|
||||
|
||||
建议默认曲线采用分段断点,而不是单个固定百分比:窄屏使用接近全屏的安全宽度,中等屏使用较高百分比,宽屏使用较低百分比并设置最大宽度上限。最终采纳的断点为 640px 约 86vw、768px 约 82vw、1024px 约 68vw、1440px 约 58vw、1920px 及以上约 `min(50vw, 960px)`,相比初始草案整体调大以提供更宽松的阅读空间。
|
||||
|
||||
替代方案是在 React 中根据 `window.innerWidth` 计算 `size`。该方案能精确控制曲线,但会把纯展示断点逻辑放进组件状态,并需要 resize 监听。默认宽度本质是样式规则,因此优先放在 CSS 中。
|
||||
|
||||
### 拖拽边界使用视口安全范围
|
||||
|
||||
`sizeDraggable` 的 `min`、`max` 需要使用像素数字。实现时可以在组件中根据 `window.innerWidth` 计算边界,例如最小宽度不超过 `viewport - 24px`,最大宽度不超过 `min(1200px, viewport - 24px)`。这样窄屏下不会因为固定最小宽度导致横向溢出,宽屏下也不会无限拉宽。
|
||||
|
||||
替代方案是使用 `sizeDraggable={true}` 交给 TDesign 默认边界。默认最小值接近 8px,用户可能把 Drawer 拉到不可读宽度,因此不采用。
|
||||
|
||||
### 不保存拖拽结果
|
||||
|
||||
不主动在 `onSizeDragEnd` 中写入 `localStorage` 或组件外部持久状态。TDesign 内部会在组件已挂载期间保留拖拽后的像素宽度;由于 TDesign Drawer 默认 `destroyOnClose=false`(当前代码未显式覆盖该默认值),关闭再打开时同一页面生命周期内可能继续使用拖拽后的宽度。页面刷新或组件重新挂载后恢复 CSS 响应式默认宽度。
|
||||
|
||||
替代方案是将拖拽结果持久化到浏览器本地存储。用户已明确不需要持久化,且持久化会让响应式默认宽度在不同屏幕之间产生意外记忆,因此不采用。
|
||||
|
||||
### 统计卡片改为上下布局
|
||||
|
||||
概览面板"统计"区域的指标卡片原使用 flex 左右布局(标题左、数值右),在窄屏下数值对齐不佳。改为直接使用 TDesign `Statistic` 组件自带的上下布局(title 在上、value 在下),与 Dashboard 的 SummaryCards 布局一致。移除自定义的 `.overview-stat-item` 和 `.overview-stat-value` CSS 类,复用 `.summary-stat-col`(text-align: center)实现居中。
|
||||
|
||||
替代方案是保持左右布局并增强窄屏响应式处理。该方案在不增加 Drawer 宽度的前提下可改善窄屏可读性,但 SummaryCards 已采用上下布局并验证了良好效果,统一布局风格降低用户认知成本,因此采用上下布局。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- TDesign 内部拖拽状态在组件生命周期内优先于 `size` 默认值 → 在用户拖拽后,窗口 resize 可能不会立即回到响应式默认曲线;通过明确规格为“拖拽后当前页面生命周期内使用用户调整宽度”降低歧义。
|
||||
- CSS 变量作为 `size` 值依赖 TDesign 将 `size` 透传到 width CSS 属性 → 通过组件测试或 DOM 快照验证 `size` 配置,并在实现时手动确认浏览器渲染效果。
|
||||
- 拖拽边界依赖 `window.innerWidth` → 测试环境需要提供可控的 `window.innerWidth`,实现中应处理 `window` 不可用或极窄视口的兜底值。
|
||||
- 过大的最小宽度会影响窄屏 → 通过 `min(viewport - safeGap, preferredMin)` 计算最小值,保证窄屏不横向溢出。
|
||||
- 业务 CSS 类需要作用到挂载到 `body` 的 Drawer 根节点 → 使用 Drawer `className` 传入业务类,而不是依赖父组件层级选择器。
|
||||
31
openspec/changes/responsive-resizable-drawer/proposal.md
Normal file
31
openspec/changes/responsive-resizable-drawer/proposal.md
Normal file
@@ -0,0 +1,31 @@
|
||||
## Why
|
||||
|
||||
目标详情 Drawer 当前使用固定比例宽度,在宽屏下会占用过多横向空间,在窄屏下又容易压缩内容可读性。将宽度改为响应式默认值并支持用户拖拽调整,可以让详情查看在不同屏幕尺寸下更自然,同时保留用户对临时阅读空间的控制。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 将目标详情 Drawer 从固定宽度改为按视口宽度响应式设置默认宽度:宽屏占比更小,窄屏占比更大。
|
||||
- 启用 TDesign Drawer 原生拖拽调整能力,允许用户通过拖动 Drawer 边缘在当前页面生命周期内调整宽度。
|
||||
- 为拖拽宽度设置最小值和最大值,避免内容不可读或 Drawer 超出视口安全范围。
|
||||
- 不持久化用户拖拽后的宽度到 `localStorage`、后端或其他跨刷新存储;页面刷新后恢复响应式默认宽度。
|
||||
- 将概览面板"统计"区域指标卡片从左右布局改为上下布局(与 Dashboard SummaryCards 一致),提升窄屏下的视觉体验。
|
||||
- 同步更新目标详情 Drawer 的规格、实现说明和测试要求。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- 无
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `target-detail-drawer`: Drawer 宽度要求从固定比例改为响应式默认宽度,并新增当前页面生命周期内的边缘拖拽调整行为和不持久化约束。
|
||||
- `css-utility-classes`: 移除 `.overview-stat-item` 和 `.overview-stat-value` 类,概览统计卡片改为复用 `.summary-stat-col` 实现上下居中布局。
|
||||
|
||||
## Impact
|
||||
|
||||
- 影响 `src/web/components/TargetDetailDrawer.tsx` 的 Drawer `size`、`sizeDraggable` 和拖拽结束处理。
|
||||
- 需要在 `src/web/styles.css` 中新增响应式宽度 CSS 变量和断点规则。
|
||||
- 需要更新 `tests/web/components/TargetDetailDrawer.test.tsx`,覆盖响应式宽度配置和拖拽能力。
|
||||
- 需要更新 `openspec/specs/target-detail-drawer/spec.md` 对应 delta spec,并同步 `DEVELOPMENT.md` 中组件说明。
|
||||
- 不影响后端 API、数据模型、配置文件和运行时依赖。
|
||||
@@ -0,0 +1,14 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 辅助工具类
|
||||
styles.css SHALL 定义前端组件复用的工具类,包含页面布局相关类。
|
||||
|
||||
#### 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` 类。
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: 概览统计卡片内部项与数值类定义
|
||||
**Reason**: 概览统计卡片改为 TDesign Statistic 上下布局(与 SummaryCards 一致),不再需要自定义的 `.overview-stat-item` 和 `.overview-stat-value` flex 左右布局类。
|
||||
**Migration**: 使用 TDesign Statistic 组件的 `title` prop 设置标题、`value` prop 设置数值,搭配 `.summary-stat-col` 类实现居中。`.overview-stat-card` 类保留用于卡片背景和内边距。
|
||||
@@ -0,0 +1,62 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 目标详情 Drawer
|
||||
Dashboard SHALL 在用户点击目标表格行后从右侧滑出 Drawer,展示该目标的详细统计信息和检查记录。Drawer 标题栏和内容不使用内联 style。Drawer 内容 SHALL 拆分为独立的 Tab 组件,并优先通过 TDesign Drawer 原生生命周期能力控制显示、隐藏和滚动穿透。
|
||||
|
||||
#### Scenario: 打开 Drawer
|
||||
- **WHEN** 用户点击某个目标表格行
|
||||
- **THEN** 系统 SHALL 从右侧滑出 Drawer(placement="right"),使用响应式默认宽度,并将当前 Tab 重置为"概览"
|
||||
|
||||
#### 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 关闭并通过 TDesign Drawer 的 `visible` 状态隐藏
|
||||
|
||||
#### 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 状态重置为概览 Tab,且 MUST NOT 使用 `key={target.id}` 强制重建 Drawer 子树
|
||||
|
||||
#### Scenario: Drawer 内容区间距
|
||||
- **WHEN** Drawer 内容渲染
|
||||
- **THEN** 时间选择器、Tabs 等区块之间的间距 SHALL 通过 TDesign Space 组件(direction="vertical", size={16})统一管理,不使用内联 style 的 marginBottom
|
||||
|
||||
### Requirement: Drawer 宽度
|
||||
Drawer 宽度 SHALL 根据视口宽度设置响应式默认值,并 SHALL 支持用户通过鼠标拖拽边缘在当前页面生命周期内调整宽度。系统 MUST NOT 将拖拽后的宽度持久化到 `localStorage`、后端、URL 或其他跨刷新存储。
|
||||
|
||||
#### Scenario: Drawer 响应式默认宽度
|
||||
- **WHEN** Drawer 打开且用户尚未在当前页面生命周期内拖拽调整宽度
|
||||
- **THEN** Drawer size SHALL 使用响应式默认宽度,宽屏时占视口比例 SHALL 小于窄屏时占视口比例,且窄屏下 SHALL 不超过视口安全宽度
|
||||
|
||||
#### Scenario: Drawer 边缘拖拽宽度
|
||||
- **WHEN** 用户使用鼠标拖动右侧 Drawer 的左边缘
|
||||
- **THEN** Drawer SHALL 通过 TDesign Drawer 原生拖拽能力调整宽度,不通过自定义全局鼠标事件实现拖拽
|
||||
|
||||
#### Scenario: Drawer 拖拽边界
|
||||
- **WHEN** 用户拖拽调整 Drawer 宽度
|
||||
- **THEN** Drawer 宽度 SHALL 被限制在最小可读宽度和视口安全最大宽度之间,避免内容不可读或横向溢出
|
||||
|
||||
#### Scenario: Drawer 当前页面生命周期内保留拖拽宽度
|
||||
- **WHEN** 用户拖拽调整 Drawer 宽度后关闭并再次打开 Drawer,且页面未刷新、组件未重新挂载
|
||||
- **THEN** Drawer SHALL 保留当前页面生命周期内的拖拽后宽度
|
||||
|
||||
#### Scenario: Drawer 拖拽宽度不持久化
|
||||
- **WHEN** 页面刷新后用户再次打开 Drawer
|
||||
- **THEN** Drawer SHALL 恢复响应式默认宽度,且 MUST NOT 从 `localStorage`、后端、URL 或其他跨刷新存储恢复拖拽宽度
|
||||
|
||||
### Requirement: 概览面板
|
||||
概览 Tab SHALL 按区域展示基本信息、多维度统计和趋势图。
|
||||
|
||||
#### Scenario: 统计区上下布局卡片
|
||||
- **WHEN** 概览面板渲染且有统计数据
|
||||
- **THEN** 面板 SHALL 在"统计"区域使用 4 列 × 2 行的 Row/Col 布局,每个统计项使用 `overview-stat-card` 包裹,内部使用 TDesign Statistic 组件自带的上下布局(title 在上、value 在下),通过 `summary-stat-col` 类居中。系统 SHALL NOT 使用已移除的 `overview-stat-item` 左右 flex 布局
|
||||
23
openspec/changes/responsive-resizable-drawer/tasks.md
Normal file
23
openspec/changes/responsive-resizable-drawer/tasks.md
Normal file
@@ -0,0 +1,23 @@
|
||||
## 1. Drawer 宽度实现
|
||||
|
||||
- [x] 1.1 为 `TargetDetailDrawer` 的 Drawer 添加业务 `className`,并将 `size` 改为响应式默认宽度 CSS 变量
|
||||
- [x] 1.2 使用 TDesign Drawer `sizeDraggable` 启用鼠标边缘拖拽,并配置基于视口安全范围的 `min` 和 `max` 边界
|
||||
- [x] 1.3 确保拖拽后的宽度不写入 `localStorage`、URL、后端或其他跨刷新存储,并保持现有 Drawer 生命周期行为
|
||||
|
||||
## 2. 样式与文档
|
||||
|
||||
- [x] 2.1 在 `src/web/styles.css` 中定义目标详情 Drawer 响应式默认宽度变量和断点规则
|
||||
- [x] 2.2 更新 `DEVELOPMENT.md` 中 `TargetDetailDrawer` 固定宽度描述为响应式默认宽度和拖拽调整
|
||||
- [x] 2.3 检查 `README.md` 是否涉及该前端交互说明;若无需更新,在实现结果中说明原因
|
||||
|
||||
## 3. 测试与验证
|
||||
|
||||
- [x] 3.1 更新 `tests/web/components/TargetDetailDrawer.test.tsx`,覆盖 Drawer 响应式 `size` 配置、`sizeDraggable` 边界和不持久化约束
|
||||
- [x] 3.2 运行 `bun run verify`,确保 schema、类型检查、lint、测试和构建全部通过
|
||||
|
||||
## 4. Apply 后修补
|
||||
|
||||
- [x] 4.1 将统计卡片改为上下布局(复用 `Statistic` title/value + `.summary-stat-col`),移除 `.overview-stat-item`/`.overview-stat-value` CSS 类
|
||||
- [x] 4.2 调大 Drawer 各断点默认宽度(86/82/68/58/50vw),最窄屏从 `calc(100vw - 24px)` 调为 `calc(100vw - 16px)`
|
||||
- [x] 4.3 Drawer header 间距从 `size={8}` 调为 `size={12}`
|
||||
- [x] 4.4 同步回写变更文档(design.md 断点数值、proposal.md 卡片布局、新增 css-utility-classes delta spec、target-detail-drawer delta spec 补充概览面板布局)
|
||||
@@ -103,7 +103,7 @@ styles.css SHALL 定义前端组件复用的工具类,包含页面布局相关
|
||||
|
||||
#### Scenario: 概览统计卡片类
|
||||
- **WHEN** Drawer 概览统计区渲染
|
||||
- **THEN** 统计卡片 SHALL 使用 `.overview-stat-card` 类(background: var(--td-bg-color-container-hover)),内部项 SHALL 使用 `.overview-stat-item` 类(display: flex; align-items: center; justify-content: space-between),数值 SHALL 使用 `.overview-stat-value` 类(font-size: var(--td-font-size-body-medium); text-align: right)
|
||||
- **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: 异常行背景类
|
||||
styles.css SHALL 定义 DOWN 行的背景色和左侧竖线,使用安全选择器且不使用 `!important`。
|
||||
|
||||
@@ -9,7 +9,7 @@ Dashboard SHALL 在用户点击目标表格行后从右侧滑出 Drawer,展示
|
||||
|
||||
#### Scenario: 打开 Drawer
|
||||
- **WHEN** 用户点击某个目标表格行
|
||||
- **THEN** 系统 SHALL 从右侧滑出 Drawer(placement="right"),宽度为 52%,并将当前 Tab 重置为"概览"
|
||||
- **THEN** 系统 SHALL 从右侧滑出 Drawer(placement="right"),使用响应式默认宽度,并将当前 Tab 重置为"概览"
|
||||
|
||||
#### Scenario: Drawer 标题栏
|
||||
- **WHEN** Drawer 渲染
|
||||
@@ -36,11 +36,11 @@ Dashboard SHALL 在用户点击目标表格行后从右侧滑出 Drawer,展示
|
||||
- **THEN** 时间选择器、Tabs 等区块之间的间距 SHALL 通过 TDesign Space 组件(direction="vertical", size={16})统一管理,不使用内联 style 的 marginBottom
|
||||
|
||||
### Requirement: 概览面板组件化
|
||||
概览 Tab SHALL 作为独立组件 `OverviewTab` 实现,展示基本信息、多维度统计(左右布局卡片)和趋势图。不再包含状态分布环形图。
|
||||
概览 Tab SHALL 作为独立组件 `OverviewTab` 实现,展示基本信息、多维度统计(上下布局卡片)和趋势图。不再包含状态分布环形图。
|
||||
|
||||
#### Scenario: OverviewTab 组件职责
|
||||
- **WHEN** 概览 Tab 渲染
|
||||
- **THEN** `OverviewTab` 组件 SHALL 负责基本信息(直接展示 Descriptions)、多维度统计卡片(4×2 左右布局)和趋势图的渲染
|
||||
- **THEN** `OverviewTab` 组件 SHALL 负责基本信息(直接展示 Descriptions)、多维度统计卡片(4×2 上下布局)和趋势图的渲染
|
||||
|
||||
#### Scenario: OverviewTab props
|
||||
- **WHEN** OverviewTab 渲染
|
||||
@@ -217,9 +217,9 @@ Drawer 内部 SHALL 使用 TDesign Tabs 组织概览和记录两个面板。Tabs
|
||||
- **WHEN** 概览面板渲染
|
||||
- **THEN** Descriptions SHALL 展示:目标地址、检查间隔、最新检查时间、状态详情
|
||||
|
||||
#### Scenario: 统计区左右布局卡片
|
||||
#### Scenario: 统计区上下布局卡片
|
||||
- **WHEN** 概览面板渲染且有统计数据
|
||||
- **THEN** 面板 SHALL 在"统计"区域使用 4 列 × 2 行的 Row/Col 布局,每个统计项使用 `<div className="overview-stat-card">` 包裹,通过 CSS 类实现背景色和内边距视觉效果
|
||||
- **THEN** 面板 SHALL 在"统计"区域使用 4 列 × 2 行的 Row/Col 布局,每个统计项使用 `overview-stat-card` 包裹,内部使用 TDesign Statistic 组件自带的上下布局(title 在上、value 在下),通过 `summary-stat-col` 类居中。系统 SHALL NOT 使用已移除的 `overview-stat-item` 左右 flex 布局
|
||||
|
||||
#### Scenario: 统计区内容
|
||||
- **WHEN** 概览面板渲染
|
||||
@@ -261,11 +261,27 @@ Drawer 内部 SHALL 使用 TDesign Tabs 组织概览和记录两个面板。Tabs
|
||||
- **THEN** 系统 SHALL 依赖 Drawer 内容区域作为唯一纵向滚动容器,HistoryTab 中的 PrimaryTable SHALL 不配置 `height`、`maxHeight` 或纵向 `scroll` 来创建第二个纵向滚动区域
|
||||
|
||||
### Requirement: Drawer 宽度
|
||||
Drawer 宽度 SHALL 设置为 52%。
|
||||
Drawer 宽度 SHALL 根据视口宽度设置响应式默认值,并 SHALL 支持用户通过鼠标拖拽边缘在当前页面生命周期内调整宽度。系统 MUST NOT 将拖拽后的宽度持久化到 `localStorage`、后端、URL 或其他跨刷新存储。
|
||||
|
||||
#### Scenario: Drawer 宽度
|
||||
- **WHEN** Drawer 打开
|
||||
- **THEN** Drawer size SHALL 为 "52%"
|
||||
#### Scenario: Drawer 响应式默认宽度
|
||||
- **WHEN** Drawer 打开且用户尚未在当前页面生命周期内拖拽调整宽度
|
||||
- **THEN** Drawer size SHALL 使用响应式默认宽度,宽屏时占视口比例 SHALL 小于窄屏时占视口比例,且窄屏下 SHALL 不超过视口安全宽度
|
||||
|
||||
#### Scenario: Drawer 边缘拖拽宽度
|
||||
- **WHEN** 用户使用鼠标拖动右侧 Drawer 的左边缘
|
||||
- **THEN** Drawer SHALL 通过 TDesign Drawer 原生拖拽能力调整宽度,不通过自定义全局鼠标事件实现拖拽
|
||||
|
||||
#### Scenario: Drawer 拖拽边界
|
||||
- **WHEN** 用户拖拽调整 Drawer 宽度
|
||||
- **THEN** Drawer 宽度 SHALL 被限制在最小可读宽度和视口安全最大宽度之间,避免内容不可读或横向溢出
|
||||
|
||||
#### Scenario: Drawer 当前页面生命周期内保留拖拽宽度
|
||||
- **WHEN** 用户拖拽调整 Drawer 宽度后关闭并再次打开 Drawer,且页面未刷新、组件未重新挂载
|
||||
- **THEN** Drawer SHALL 保留当前页面生命周期内的拖拽后宽度
|
||||
|
||||
#### Scenario: Drawer 拖拽宽度不持久化
|
||||
- **WHEN** 页面刷新后用户再次打开 Drawer
|
||||
- **THEN** Drawer SHALL 恢复响应式默认宽度,且 MUST NOT 从 `localStorage`、后端、URL 或其他跨刷新存储恢复拖拽宽度
|
||||
|
||||
### Requirement: 时间选择器单行布局
|
||||
Drawer 顶部的时间范围快捷按钮和日期范围选择器 SHALL 在同一行展示。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { Col, Descriptions, Divider, Row, Skeleton, Space, Statistic, Typography } from "tdesign-react";
|
||||
import { Col, Descriptions, Divider, Row, Skeleton, Space, Statistic } from "tdesign-react";
|
||||
|
||||
import type { TargetMetricsResponse, TargetStatus } from "../../shared/api";
|
||||
|
||||
@@ -97,11 +97,8 @@ export function OverviewTab({ metricsData, metricsLoading, target }: OverviewTab
|
||||
|
||||
function OverviewStatItem({ color, suffix, title, value }: OverviewStatItemProps) {
|
||||
return (
|
||||
<div className="overview-stat-card">
|
||||
<div className="overview-stat-item">
|
||||
<Typography.Text theme="secondary">{title}</Typography.Text>
|
||||
<Statistic className="overview-stat-value" color={color} suffix={suffix} value={value} />
|
||||
</div>
|
||||
<div className="overview-stat-card summary-stat-col">
|
||||
<Statistic color={color} suffix={suffix} title={title} value={value} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { TabValue } from "tdesign-react";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { DateRangePicker, Drawer, RadioGroup, Space, Tabs, Tag, Typography } from "tdesign-react";
|
||||
|
||||
import type { HistoryResponse, TargetMetricsResponse, TargetStatus } from "../../shared/api";
|
||||
@@ -72,13 +72,23 @@ export function TargetDetailDrawer({
|
||||
|
||||
const isUp = target?.latestCheck?.matched;
|
||||
|
||||
const dragLimits = useMemo(() => {
|
||||
const viewportWidth = window.innerWidth;
|
||||
const safeGap = 24;
|
||||
return {
|
||||
max: Math.min(1200, Math.max(360, viewportWidth - safeGap)),
|
||||
min: 360,
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
attach="body"
|
||||
className="target-detail-drawer"
|
||||
footer={false}
|
||||
header={
|
||||
target ? (
|
||||
<Space align="center" size={8}>
|
||||
<Space align="center" size={12}>
|
||||
<StatusDot up={!!isUp} />
|
||||
<Typography.Text strong>{target.name}</Typography.Text>
|
||||
<Tag size="small" theme="primary" variant="light-outline">
|
||||
@@ -92,7 +102,8 @@ export function TargetDetailDrawer({
|
||||
preventScrollThrough
|
||||
showInAttachedElement={false}
|
||||
showOverlay
|
||||
size="55%"
|
||||
size="var(--target-detail-drawer-width)"
|
||||
sizeDraggable={dragLimits}
|
||||
visible={!!target}
|
||||
>
|
||||
<Space className="full-width" direction="vertical" size={16}>
|
||||
|
||||
@@ -170,28 +170,46 @@
|
||||
min-width: 360px;
|
||||
}
|
||||
|
||||
.target-detail-drawer {
|
||||
--target-detail-drawer-width: calc(100vw - 16px);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.target-detail-drawer {
|
||||
--target-detail-drawer-width: 86vw;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.target-detail-drawer {
|
||||
--target-detail-drawer-width: 82vw;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.target-detail-drawer {
|
||||
--target-detail-drawer-width: 68vw;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.target-detail-drawer {
|
||||
--target-detail-drawer-width: 58vw;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1920px) {
|
||||
.target-detail-drawer {
|
||||
--target-detail-drawer-width: min(50vw, 960px);
|
||||
}
|
||||
}
|
||||
|
||||
.overview-stat-card {
|
||||
background: var(--td-bg-color-container-hover);
|
||||
border-radius: var(--td-radius-default);
|
||||
padding: var(--td-comp-paddingTB-xl) var(--td-comp-paddingLR-xl);
|
||||
}
|
||||
|
||||
.overview-stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--td-comp-margin-m);
|
||||
}
|
||||
|
||||
.overview-stat-value {
|
||||
font-size: var(--td-font-size-body-medium);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.overview-stat-value .t-statistic-content {
|
||||
font-size: var(--td-font-size-body-medium);
|
||||
}
|
||||
|
||||
.summary-stat-col {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -51,6 +51,15 @@ globalThis.ResizeObserver = class {
|
||||
unobserve() {}
|
||||
};
|
||||
|
||||
globalThis.MutationObserver = class {
|
||||
disconnect() {}
|
||||
observe() {}
|
||||
takeRecords() {
|
||||
return [];
|
||||
}
|
||||
unobserve() {}
|
||||
};
|
||||
|
||||
globalThis.IntersectionObserver = class {
|
||||
disconnect() {}
|
||||
observe() {}
|
||||
|
||||
@@ -85,4 +85,39 @@ describe("TargetDetailDrawer", () => {
|
||||
// Just verify rendering doesn't throw
|
||||
expect(asFragment()).not.toBeNull();
|
||||
});
|
||||
|
||||
test("Drawer 使用响应式默认宽度 CSS 变量", () => {
|
||||
render(<TargetDetailDrawer {...defaultProps} />);
|
||||
const wrapper = document.querySelector<HTMLElement>(".t-drawer__content-wrapper")!;
|
||||
expect(wrapper).not.toBeNull();
|
||||
expect(wrapper.style.width).toBe("var(--target-detail-drawer-width)");
|
||||
});
|
||||
|
||||
test("Drawer 包含业务 className", () => {
|
||||
render(<TargetDetailDrawer {...defaultProps} />);
|
||||
const drawer = document.querySelector(".target-detail-drawer");
|
||||
expect(drawer).not.toBeNull();
|
||||
});
|
||||
|
||||
test("Drawer 启用 sizeDraggable 拖拽", () => {
|
||||
render(<TargetDetailDrawer {...defaultProps} />);
|
||||
const wrapper = document.querySelector<HTMLElement>(".t-drawer__content-wrapper")!;
|
||||
expect(wrapper).not.toBeNull();
|
||||
const dragLine = wrapper.querySelector('[style*="col-resize"]');
|
||||
expect(dragLine).not.toBeNull();
|
||||
});
|
||||
|
||||
test("Drawer 拖拽宽度不写入 localStorage", () => {
|
||||
const keysBefore = window.localStorage.length;
|
||||
render(<TargetDetailDrawer {...defaultProps} />);
|
||||
expect(window.localStorage.length).toBe(keysBefore);
|
||||
});
|
||||
|
||||
test("Drawer sizeDraggable 配置最小拖拽边界", () => {
|
||||
render(<TargetDetailDrawer {...defaultProps} />);
|
||||
const wrapper = document.querySelector<HTMLElement>(".t-drawer__content-wrapper")!;
|
||||
expect(wrapper).not.toBeNull();
|
||||
const dragLine = wrapper.querySelector('[style*="col-resize"]');
|
||||
expect(dragLine).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user