# Dashboard 布局规则（骨架/栅格/滚动/响应式）

## 0. 快查索引

| 你的问题 | 跳到 |
| --- | --- |
| 看板是整页滚还是面板内滚？ | §1 滚动模型 |
| 栅格、卡片内边距、间距怎么定？ | §2 栅格与间距 |
| 同行卡片要不要等高对齐？ | §2.0 / §2.0.2 分组对齐 |
| 卡内"图表+表格"怎么排？ | §2.0.1 主辅水平分区 |
| 多图属于同一主题，要不要拆白卡？ | §2.0.3 聚合卡（一卡多图） |
| 顶部 Tabs 用 line / pill / segment 哪个？ | `PAGE_ARCHETYPES.md` §0.1 / §0.2 |
| 图表高度/自适应怎么约束？ | §3 图表高度策略 |
| 图表被截断怎么办？ | §3.2 截断降级 |
| 图表 Y 轴没左对齐？ | §3.3 Y 轴左对齐 |
| legend/tooltip 抖动怎么防？ | §4 抖动防线（legend/tooltip） |
| 响应式怎么做？ | §5 响应式策略 |
| 嵌入 b 端页面（如策略列表）？ | §6 embedded 模式 |
| 嵌入 b 端 TagBar 怎么用？ | §6.1 TagBar 标准用法 |
| 含 TagBar 的页面顶栏怎么放？ | §6.2 顶栏跨整宽对齐 |
| 文本溢出/挤压/超长列表崩版？ | §7 防御性布局 |
| 关联 recipe id 与滚动决策树？ | §1.2 滚动模型决策树 |

## 1. 滚动模型

> Dashboard 属于多面板对照工作流，默认 **面板内滚**：Header/筛选固定，模块区内部滚动。

- 面板容器：`flex-1 min-h-0 overflow-y-auto`
- 禁止：Header 跟随滚动消失；同时出现 body 滚动 + 面板滚动

### 1.1 滚动 owner 的唯一性（硬门禁）

只允许一个“主滚动容器 owner”。其它列表/表格若需要滚动，必须是“同一滚动 owner 内的子滚动”，且不能与 owner 竞争滚轮焦点。

### 1.2 滚动模型决策树（含 recipe id）

> 输出时必须明确：**你选的是哪一条分支**，以及对应的 `recipeId`。

- **Q1：用户是否需要“筛选与结果强对照（筛选固定）”？**
  - **是** → `recipeId = filter-sticky-modules-scroll`
    - **scrollOwner**：模块区（内容面板）滚动；筛选区固定（不滚）
  - **否** → 进入 Q2
- **Q2：是否为“监控大屏，尽量一屏全展示”？**
  - **是** → `recipeId = monitor-no-scroll`（首选）或 `monitor-wall-split-columns`
    - **scrollOwner**：默认不滚；若必须滚，只允许“右列列表/评论流容器”内滚
  - **否** → 进入 Q3
- **Q3：是否为“钻取对比（左 TopN/聚类 + 右明细表）”？**
  - **是** → `recipeId = drilldown-compare`
    - **scrollOwner**：右侧明细面板（或模块区）滚动；左右不得同时滚
  - **否** → `recipeId = dashboard-overview`
    - **scrollOwner**：模块区滚动（默认）

## 2. 栅格与间距

- **模块/卡片之间默认（dashboard 主区）**：`gap-3`（`--spacing-3` = 12px）。dashboard 主区的各行之间、grid 列之间统一用 12px，这是**默认值**而非紧凑特例。
  - 仅在明确"信息密度低、需要呼吸感"的特殊页面才放宽到 `gap-4`（16px）。
- **卡片内边距（dashboard 白卡铁律）**：`pt-4 px-[20px] pb-[20px]`，即 **上 16px / 左右 20px / 下 20px**。
  - 统一引用常量 `DASHBOARD_CARD_PADDING_CLASS`（= `'px-[20px] pb-[20px] pt-4'`，见 `src/data/systems/dashboard/typography.js`），不要在各处手写。
  - **内容严格不允许超出该内边距**：任何图表 / 文字 / SVG 都必须在内边距内完整展示，溢出即为缺陷（见 §3.2 截断降级）。
  - 说明：dashboard 卡片**不再使用** 24px(p-6) 作为默认内边距（p-6 是 b 端通用卡片的默认值，勿混用）。
- **关于 20px 内边距与 token**：`theme.css` 当前 spacing token 为 `--spacing-2/3/4/6`（8/12/16/24），**不含 20px**。因此卡片左右下内边距 20px 用任意值 `px-[20px] pb-[20px]` 表达，这是**唯一被豁免的"非 token 间距"**，已收敛在 `DASHBOARD_CARD_PADDING_CLASS` 常量内（上内边距 16px 仍走 token `pt-4`）。
- **卡片圆角铁律**：dashboard 白卡统一用 `rounded-xl`（**12px**，对应 `--radius-lg`）。`theme.css` 圆角 token 最大就是 `--radius-lg: 12px`，**没有 16px / `--radius-xl` / `rounded-2xl`**——不要写 16px 圆角。卡片背景用 `bg-white`（`--color-surface`），边框/阴影二选一不混用（`--color-border` 细边框 或 `--shadow-sm` 浅阴影）。
- **禁止**：
  - 其它 Tailwind 任意值（如 `p-[18px]`、`gap-[10px]`）——卡片内边距 20px 之外，不得再引入散落的任意值。
  - 为了"底部对齐"拉高兄弟卡片高度后留出硬空白（应让卡内图表自适应吃掉高度，见 §2.0 与 §3）。

### 2.0 同一行卡片的"分组对齐"硬规则（P0）

> 当一行 grid 内的多张卡片属于**同一语义分组**（例：`核心指标 / 雷达 / 评分` 这种并列展示），**必须等高、底部对齐**。

- 一行三/二列同语义分组：使用 `items-stretch`（grid 默认即 stretch）。**不得使用 `items-start`** 让矮卡贴顶。
- 内部元素必须能"吃掉"剩余高度，避免出现底部硬空白：
  - 图表 / SVG：用 `aspect-ratio + svg w-full h-auto + preserveAspectRatio` 跟随容器缩放（仪表盘、雷达、环图均已支持）。
  - KPI 网格：父容器 `flex-1`，内部 grid 加 `flex-1` + `gridAutoRows: 1fr`，让 KPI 单元等高、撑满。
  - "图表 + 辅助说明"组合：图表区 `flex-1 flex items-center justify-center`，下方说明用自然 margin（如 `mt-3`）；不再用 `mt-auto / pt-N` 强行贴底（会塞空白）。
- 例外：当一行卡片**不属于同一分组**（如 banner + 操作面板），允许 `items-start`；这种情况必须显式注释 "非同一分组，不要求底部对齐"。

> 决策口诀：**有分组 → 等高 + 内部撑满；高度语义差异极大 → 顶部对齐**。

#### 2.0.2 相邻"图表卡"的等高补充（P0，来自实战）

即使两张卡**不是严格同一语义分组**（例：`会话趋势卡` + `反馈情绪分布卡`），只要它们是同一行并列的"图表型卡片"，**优先以内容较高的一张为基准等高对齐**，另一张卡内的图表用自适应缩放（`fillContainer` / viewBox）填满剩余高度。

- ✅ 正确：`items-stretch` 等高 + 矮卡内图表 `fillContainer` 自适应放大/缩小填满。
- ❌ 错误：用 `items-start` 让两张图表卡顶部对齐，导致较矮的卡片底部出现大段硬空白；或较高卡片内图表用固定 height 撑不满而留白。
- 仅当一张是 banner/纯说明、与图表卡高度语义差异极大时，才回退到 `items-start`。

> 口诀升级：**并列图表卡 → 等高 + 内图表自适应填满**；**只有 banner/操作类才顶部对齐**。
> 任何 PR / Skill 输出，三列以上 grid 没写明这条选择就视为缺省 = 等高 + 内部撑满。

### 2.0.1 卡片**内部**水平分区的"撑满"硬规则（P0）

> 当一张卡片内部分为"主视图 + 辅助面板"两个明确分区（例：`图表 + 指标明细表`、`图表 + 图例列表`），**主视图必须撑满剩余空间，与辅助面板之间保持固定间距**。绝不能让主视图被锁在 50%。

- **结构**：使用 `flex flex-col sm:flex-row gap-2 lg:gap-3`
  - 主视图（图表）：`flex-1 min-w-0 w-full`，吃掉所有剩余水平空间。
  - 辅助面板（表格 / 图例）：`sm:w-auto sm:shrink-0` + `style={{ maxWidth: panelMaxWidth }}`，按自身最大宽度占位。
- **错误示范**：`grid grid-cols-2`（强制 1fr 1fr）会让图表被锁在 50%，导致水平方向出现大段空白。
- **间距**：默认 8px（`gap-2`），≥1024px 起 12px（`lg:gap-3`）；不允许写自定义 px 值。
- **窄屏降级**：`flex-col` 单列堆叠，图表与辅助面板都 `w-full`，不会溢出。

> 决策口诀：**主+辅分区 → flex-row + 主 flex-1 + 辅按内容宽**。**禁用 grid-cols-2 来做"主+辅"**。

### 2.0.3 聚合卡：同主题多图共享一张大白卡（P0，反碎片化）

> **核心原则**：**白卡 = 语义分组单位，不是图表容器单位**。

**触发场景**（满足任一即应聚合）：
- 多张图表/KPI **属于同一业务主题**（如"反馈特征" = 反馈情绪 + 会员情况 + 设备版本，三图都是在描述同一群人/同一批反馈的不同特征切片）
- 多张 KPI 卡是**同一指标体系下的并列维度**（如用户标签的 7 个分类标签）
- 这一组内容**有统一的副标题/口径/AI 摘要**适用

**做法**：
- **一张大白卡** `WHITE_CARD = 'rounded-xl bg-white pt-4 px-[20px] pb-[20px]'` 包整组
- 卡顶 `<DashboardModuleTitle>` 主标题 + 副标题（说明聚合主题）
- 可选：紧跟一条 `<AISummaryPreview variant="page-summary">` AI 摘要条
- 子图分区用 `border border-[#F2F4F7]` + `rounded-xl` + `p-3` 做"内框分隔"，**不要再用白底叠白底**（视觉无层级）
- 子图布局走 `grid grid-cols-1 items-stretch gap-3 min-[1024px]:grid-cols-3`（等高分组，参考 §2.0）
- 每张子图内必须 `<DashboardModuleTitle as="h4">` 写小标题 + 副标题

**反例（不要做）**：
- ❌ 把 7 张相同的 KPI 标签拆成 7 张独立白卡 → 视觉碎片，主题感全无
- ❌ 把"反馈情绪 / 会员情况 / 设备版本"三图各自一张白卡 → 用户读不出"它们在讲同一件事"
- ❌ 在大白卡内子图又用白底 (`bg-white`) → 白底叠白底，没层级感

**正确范式参考**：[`UserPersonaDashboardPattern`](file:///Users/bytedance/Desktop/TFDS/src/data/systems/dashboard/page-patterns/UserPersonaDashboardPattern.jsx) — 用户画像看板的「用户标签数据」「核心反馈特征」两张大卡。

**与"每图独立白卡"的取舍判断**：

| 情况 | 选 |
| --- | --- |
| N 个图共享同一主题/口径/AI 摘要 | **聚合卡（一卡 N 图）** |
| N 个图属于不同模块、各自独立分析 | 独立白卡（一图一卡） |
| N 个 KPI 是**并列指标维度**（同体系） | **聚合卡** |
| N 个 KPI 跨模块（如核心指标 vs 健康度 vs 告警量） | 独立白卡 |
| 单图 + 表格（主辅分区） | 单白卡内走 §2.0.1 主辅水平分区 |

### 2.1 栅格（Dashboard 常用列策略）

- **总览/驾驶舱**：优先 “KPI 网格（auto-fit）+ 模块纵向分段”
- **监控双列墙**：优先 “左列趋势/分布 + 右列榜单/评论流”，列宽固定比例（例如 2:1），避免内容撑爆
- **钻取对比**：左 1/3（TopN/聚类）+ 右 2/3（明细表/详情），右侧必须给稳定高度（见 §3）

## 3. 图表高度策略

- 图表**容器**应有稳定高度（min-height 或由卡片等高决定），避免随 tooltip/legend 抖动。
- 单模块图表区建议 `min-height: 240px`，大图表 `min-height: 320px`。

### 3.1 "高度先行" vs "内部自适应"的正确关系（P0，重要更新）

"高度先行"针对的是**容器层防抖动**，**不等于图表内部写死像素**。二者分工如下：

- **容器层**：高度由卡片（等高分组 / `flex-1`）或 `min-height` 决定，保证布局稳定、不随数据抖动。
- **图表内部**：必须用 **viewBox + `aspect-ratio` + `preserveAspectRatio`** 自适应填满容器，**反对用固定像素 height 硬撑**导致底部留白或内容被截断。
  - `SmartGaugeChart`：viewBox + aspect-ratio 自适应；viewBox 左右各预留 16，防止两端刻度文字（如 `3`、`5`）被截断。
  - `SmartRadarChart`：半径很小（紧凑）时隐藏文字轴标签，仅 hover tooltip 展示具体指标。
  - `SmartPieChart`：支持 `config.fillContainer`，跟随父容器高度自适应缩放，设最小半径下限，**缩小而不被裁切**。
- 因此：**等高 / 自适应场景下不强制写 `spec.config.height`**；只有在"独立、无等高约束、需固定防抖"的场景才显式写高度。

### 3.2 内容不可被截断 + 降级策略（P0，来自实战）

卡片内边距固定为「上 16 / 左右下 20」，**任何图表内容都不得超出该内边距被截断**。出现截断时按以下顺序排查与降级：

1. **先查内边距是否异常偏大**（是否误用了不该有的 padding / margin 叠加）。
2. 内边距正常仍放不下时，按优先级降级：
   - 隐藏坐标轴文字 / 数据标签（如雷达紧凑时只留图形，hover 再显示）。
   - 图表整体**等比缩小**（viewBox 缩放 / fillContainer），保留最小可读尺寸下限。
   - 给 SVG 两端预留安全边（如仪表盘 viewBox 左右 +16）防止端点文字被裁。
- ❌ 禁止：直接让内容溢出卡片、或用 `overflow:hidden` 把超出部分"切掉"当作解决。

### 3.3 图表 Y 轴左对齐 / 去除多余内边距（P1，来自实战）

- 折线 / 组合图（`SmartComposedChart`）的 y 轴留白要收紧：`yAxisGutter` 最小 28、`margin.left` 最小 0，避免 y 轴与卡片左内边距之间多出空白。
- 折线 / 面积 / 组合图中的趋势线默认使用 `curveType: "linear"`；除非需求明确写“平滑曲线”，不要使用 `monotone`，避免视觉上把折点抹平。
- Mock 趋势数据不能写成等差递增/递减或近似直线；7 个点以内至少出现 2 次方向变化，且相邻点差值要有大有小，确保折线有清晰折点。示例：`[72, 76, 74, 81, 78, 84, 82]` 优于 `[72, 74, 76, 78, 80, 82, 84]`。
- 折线图 / 面积图数据标签是可选增强能力：需要外显点位数值时使用 `config.showLabel: true`（或 `showValueLabel`），默认保持关闭；开启前确认系列数量和点位密度足够轻，避免数值文字互相压住折线或面积边线；开关标签不得改变坐标轴、Y 轴比例和网格间距。
- 数据标签开启时，折线 / 面积边线折点自动补同色 6px 圆点，并让线从圆点中心穿过；如果业务显式开启 `showDots`，不要再重复叠加标签圆点。
- 组合图数据标签必须按系列类型分别配置：柱形数值用 `config.bar.showLabel: true`；折线数值用 `config.line.showLabel: true`（或 `showValueLabel`）；二者不要混用成一个全局开关。
- native `YAxis` 支持 `labelInset`：让 y 轴刻度文字从卡片左内边距起排（左对齐），而非缩在轴线左侧。
- 图表底部不应叠加多余 `margin.bottom`，否则会和卡片下内边距（20px）叠加，视觉上"下边距过大"。X 轴正常时 `margin.bottom` 给 0，由卡片内边距统一兜底。

---

## 4. 抖动防线（legend/tooltip）（P0）

### 4.1 Legend 抖动（高度变化）的 4 条防线

1. **legend 区域预留高度**：legend 置底时，容器高度包含 legend 区；避免数据变化时换行挤压图表
2. **legend 文案长度控制**：系列名过长必须在业务层裁剪（ellipsis），避免换行
3. **图表区与 legend 区分层**：图表容器与 legend 容器分离，避免 reflow 影响绘图区
4. **同屏多图的 legend 对齐**：同一行的图表尽量采用一致 legend 位置（上/下），减少视觉跳动

### 4.2 Tooltip 抖动（宽度变化）的 3 条防线

1. **Tooltip 数值格式固定**：统一小数位/百分号/单位（万、%、pp），避免宽度随数据波动
2. **Tooltip 布局固定**：标题 1 行（semibold 600），内容多行；不要把系列名与数值拼成可变长字符串
3. **hover 指示线稳定**：cursor/activeShape 的 stroke 与 opacity 走 token，不要随 hover 状态切换多套尺寸

---

## 5. 响应式策略（P1）

> 目标：保证“可读性优先”，而不是强行把所有模块塞进窄屏。

- **断点原则**：在模块级做响应式（KPI 卡列数、图表堆叠、左右分栏改上下堆叠）。图表**内部**只做"等比缩放 + 紧凑时隐藏文字标签"这类有限自适应（见 §3.1/§3.2），不做复杂的逐元素重排。
- **KPI 区**：按 2/4/8 列（或 1/2/3/4）响应式；保持最小卡宽，宁可换行不挤压
- **双列布局**：宽度不足时，优先从“左右”切到“上下”，并保持模块标题与筛选区在上方
- **表格/明细**：给出固定高度与滚动容器；窄屏可改为“卡片列表”展示（仍使用 B 端基础组件语义）

## 6. 嵌入 b 端页面（embedded 模式）（P0，来自实战）

当 dashboard 直接复用 b 端 page-pattern（如 `StrategyListPage`）作为一个区块时，**必须使用 `embedded` 模式**，否则会出现"间距叠加 + 没撑满灰底容器"两类问题。

- **问题根因**：
  1. b 端基础页面框架自带 `margin: 16px 16px 16px 0`（`PAGE_FRAME_CONTENT_MARGIN`），嵌入 dashboard 后与 dashboard 自身的 `gap-3` 叠加 → 顶部空隙过大、右侧不对齐。
  2. b 端页面用 `flex-1` 控制宽度，但 dashboard 外层是普通 block 容器（非 flex 父级），`flex-1` 不生效 → 白卡按内容收缩、左右露出灰底，没撑满。
- **正确做法**：组件提供 `embedded` 入口，开启后：
  - 去掉 `PAGE_FRAME_CONTENT_MARGIN`（嵌入时由 dashboard 外层 `gap-3` 统一控制与上方卡片的间距）。
  - 显式加 `w-full` / `width: '100%'`，主动撑满 dashboard 灰底容器（红线区域），与同列其它卡片同宽、右对齐。
- ❌ 禁止：直接把 b 端页面塞进 dashboard 而不处理 frame margin / 宽度，导致间距叠加或白卡未撑满。

### 6.1. 嵌入 b 端 TagBar 标准用法（P0）

dashboard 三个含 TagBar 的模板（驾驶舱 / 全功能链路 / 服务数据大屏）统一遵循以下用法。**禁止 diy 改 b-end TagBar 源码**。

```jsx
const [tagBarCollapsed, setTagBarCollapsed] = useState(false);

<aside
  className="dashboard-scrollbar-hide flex h-full shrink-0 flex-col overflow-hidden rounded-xl bg-white transition-[width] duration-200 ease-out"
  style={{ width: tagBarCollapsed ? 72 : 250 }}
>
  <TagBar
    tone="transparent"
    resizable={false}
    collapsed={tagBarCollapsed}
    onCollapsedChange={setTagBarCollapsed}
    className="!h-full min-h-0 flex-1 !border-r-0"
    style={{ width: '100%', background: 'transparent' }}
  />
</aside>
```

要点：
- **保留 b-end TagBar 自带的展开/收起按钮**（`collapsible` 默认 true，不要传 `false` 把它去掉）。
- **白卡宽度联动 collapsed**：展开 250 / 收起 72（与 b 端整页一致），通过外层 aside 的 `style.width` 受控；TagBar 自身 `style.width: '100%'` 跟随父级。
- **滚动条美化只走 dashboard 层**：在外层 aside 加 `dashboard-scrollbar-hide`（dashboard 自有工具类，递归隐藏后代滚动条），**禁止**改 b-end TagBar 内部的 `overflow-y-auto`，也禁止给 TagBar 自身 className 附加该类（避免污染组件 className 链路）。
- ❌ 禁止：`collapsible={false}` / 自己写 `width={250}` 不受控 / 直接改 b-end 组件源码。

### 6.2. 含 TagBar 页面的顶栏（标题 + 筛选）必须跨整宽（P0）

**问题**：若顶栏 header（标题 + 渠道/时间筛选）写在 main（右侧主区）内部，TagBar 会从 NavBar 顶部开始、Banner 被压在 header 下方 → **TagBar 顶部高于 Banner 顶部**，整体视觉错位（实战截图 1）。

**正确做法**：顶栏 header 必须**提到 TagBar + main 同级的外层容器**，跨 TagBar 与右侧主区整宽显示，让 **TagBar 顶部 与 Banner / 主内容顶部完全平齐**（实战截图 2）。

骨架：

```jsx
<div className="flex h-full min-h-0 min-w-0 flex-1 flex-col overflow-hidden px-3 pb-3 pt-2">
  {/* 顶栏：跨 TagBar + main 整宽 */}
  <header className="flex h-14 shrink-0 items-center justify-between gap-3 px-1">
    <h2 className={DASHBOARD_PAGE_TITLE}>页面标题</h2>
    <FilterAndTabs />
  </header>

  {/* 下方：TagBar | main 并排 */}
  <div className="flex min-h-0 min-w-0 flex-1 gap-3 overflow-hidden">
    <aside>...TagBar...</aside>
    <main className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
      ...
    </main>
  </div>
</div>
```

- ❌ 禁止：把 header 写在 `<main>` 内部第一行——这样 TagBar 顶部会与 Banner 错位。
- ❌ 禁止：用 `pb-3` 或标题 `text-[20px]` 撑高 header；有页面标题时统一使用固定 `h-14` + `DASHBOARD_PAGE_TITLE`，header 下方间距交给父级 `gap-3`。
- ✅ 顶部画布 padding 可按页面基准微调：常规页面 24px；监控类大屏顶部可收敛到 16px（如 `pt-4`），但不要吞掉 header 与内容之间的 12px 间距。
- ✅ 这条规则适用于**所有左 TagBar + 右主区**的 dashboard 页面。

## 7. 防御性布局（健壮性，P1）

> 目标：内容长度/数据量不可控时也不崩版、不溢出、不撑爆外层。

- **文本防溢出**：模块标题、数值、单行列表项必须可截断（`truncate` 或 `overflow-hidden whitespace-nowrap text-ellipsis`），避免长文本撑破布局。
- **Flex 挤压防御**：任何**包含可截断文本**或需要收缩的 flex 子项，必须加 `min-w-0`，否则 `truncate` 失效、还会把兄弟项挤变形。这也是图表/明细并排时图表侧必带 `min-w-0` 的原因（见 §2.0.1）。
- **局部滚动域**：排行榜、长列表、长表格等超长内容，必须在**内部容器**滚动（`overflow-y-auto` + `flex-1` 或 `max-h-*` 限高），不得让内容撑爆外层页面、破坏 §1 的滚动 owner。
- **动态网格换行**：KPI/指标卡用 grid 多列时（如 `grid-cols-1 sm:grid-cols-2 lg:grid-cols-4` + `gap-3`），容器过窄必须自动换行，**禁止挤压重叠**（与 §5「宁可换行不挤压」一致）。
- **弹性+并排等高**：「自适应图表 + 侧栏列表/榜单」并排时，父容器 `flex`、图表侧 `flex-1 min-w-0`、侧栏可定宽 `shrink-0`（宽度走列策略，勿散落任意值）；两侧等高，窄屏（`<xl`）转 `flex-col` 上下堆叠。注意：dashboard 内**明细表优先自适应**而非固定宽度（见近期实战与 §2.0.1）。
