1
0
Files
DiAL/openspec/changes/responsive-resizable-drawer/design.md
lanyuanxiaoyao 88f4119a4e feat: Drawer 响应式默认宽度与拖拽调整,统计卡片上下布局优化
Drawer 宽度从固定百分比改为按视口响应式默认值(6段断点),宽屏占比更小、窄屏占比更大。

启用 TDesign sizeDraggable 原生拖拽调整能力,配置 min/max 视口安全边界,不持久化拖拽宽度。

概览统计卡片改为 TDesign Statistic 上下布局(与 SummaryCards 一致),提升窄屏视觉体验。

Drawer header 间距调大,MutationObserver polyfill 补全。
2026-05-15 23:10:08 +08:00

5.9 KiB
Raw Blame History

Context

目标详情 Drawer 目前通过 TDesign React Drawer 渲染,右侧弹出、挂载到 body,并通过 destroyOnClose=false 保留子树。既有规格要求固定宽度为 52%,实际组件当前使用固定百分比宽度。固定比例无法同时适配窄屏和宽屏:窄屏需要更高占比保证内容可读,宽屏需要更低占比避免详情抽屉吞掉主面板空间。

TDesign React 1.16.9 的 Drawer 原生提供 sizesizeDraggableonSizeDragEnd 能力。项目规范要求前端优先使用 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 来提供边缘拖拽调整能力,并通过其 minmax 限制拖拽范围。这样可以复用 TDesign 已有的拖拽命中区域、鼠标事件、宽度计算和 col-resize 光标行为。

替代方案是自定义拖拽手柄并监听 mousemove/mouseup。该方案会重复 TDesign 已有能力,增加事件清理、滚动穿透和可访问性风险,也违背项目优先使用 TDesign props 的规范,因此不采用。

用 CSS 自定义属性表达响应式默认宽度

Drawer size 传入业务 CSS 变量,例如 var(--target-detail-drawer-width),并通过 Drawer 的业务 classNamestyles.css 中定义不同断点下的变量值。TDesign 最终会将该字符串作为内容 wrapper 的 width 值使用CSS 变量会从 Drawer 根节点继承到内容节点。

建议默认曲线采用分段断点,而不是单个固定百分比:窄屏使用接近全屏的安全宽度,中等屏使用较高百分比,宽屏使用较低百分比并设置最大宽度上限。最终采纳的断点为 640px 约 86vw、768px 约 82vw、1024px 约 68vw、1440px 约 58vw、1920px 及以上约 min(50vw, 960px),相比初始草案整体调大以提供更宽松的阅读空间。

替代方案是在 React 中根据 window.innerWidth 计算 size。该方案能精确控制曲线,但会把纯展示断点逻辑放进组件状态,并需要 resize 监听。默认宽度本质是样式规则,因此优先放在 CSS 中。

拖拽边界使用视口安全范围

sizeDraggableminmax 需要使用像素数字。实现时可以在组件中根据 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-coltext-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 传入业务类,而不是依赖父组件层级选择器。