布局密度(Layout Density)功能方案

状态: ✅ 已实施完成

最后更新: 2026-01-13

一、需求分析

1.1 业务需求

用户希望能够全局调整应用的布局密度,类似于 Gmail 的密度设置:

  • 特别紧凑(Extra Tight):适合需要显示大量信息的场景(如数据表格、工作台)
  • 紧凑(Compact):适合信息密集的应用(如邮件列表、任务管理)
  • 正常(Comfortable):默认设置,平衡信息密度和可读性
  • 宽松(Spacious):适合阅读和浏览场景(如文档、博客)
  • 特别宽松(Extra Spacious):适合需要大量留白的场景(如演示、展示)

1.2 功能定位

  • 全局设置:类似主题色、字体缩放,影响整个应用
  • 用户偏好:应该可以持久化保存(localStorage)
  • 即时生效:切换后立即反映到所有组件
  • 跨平台支持:Blazor 和纯 HTML 页面都能使用

1.3 必要性分析

✅ 有必要实现

  1. 用户体验:不同用户对信息密度的偏好不同
  2. 场景适配:不同应用场景需要不同的密度(工作台 vs 阅读器)
  3. 可访问性:宽松布局对某些用户更友好
  4. 一致性:与主题色、字体缩放形成完整的个性化体系

✅ 应该在架构层面实现

  1. 全局影响:影响所有组件的间距,需要在设计系统层面统一管理
  2. 与现有机制一致:可以复用主题运行时(ThemeRuntime)的机制
  3. 向后兼容:通过 CSS 变量和语义化命名,不影响现有组件

二、技术方案

2.1 核心设计

2.1.1 密度因子(Density Scale)

引入 --sx-density-scale 变量,作为所有间距的缩放因子:

:root {
    /* Density scale factor (Dynamic - injected by theme runtime, default: 1.0) */
    --sx-density-scale: 1.0;
}

2.1.2 预设密度级别

密度级别 英文名称 缩放因子 用途描述
特别紧凑 Extra Tight 0.6 数据表格、工作台
紧凑 Compact 0.8 邮件列表、任务管理
正常 Comfortable 1.0 默认设置
宽松 Spacious 1.3 阅读、浏览
特别宽松 Extra Spacious 1.6 演示、展示

2.1.3 间距变量更新策略

所有间距变量基于密度因子动态计算:

/* 基础间距(不受密度影响,用于特殊场景) */
--sx-spacing-base-xs: 0.25rem;
--sx-spacing-base-sm: 0.5rem;
--sx-spacing-base-md: 0.75rem;
--sx-spacing-base-lg: 1rem;
--sx-spacing-base-xl: 1.5rem;

/* 应用密度缩放的间距(默认使用) */
--sx-spacing-xs: calc(var(--sx-spacing-base-xs) * var(--sx-density-scale, 1));
--sx-spacing-sm: calc(var(--sx-spacing-base-sm) * var(--sx-density-scale, 1));
--sx-spacing-md: calc(var(--sx-spacing-base-md) * var(--sx-density-scale, 1));
--sx-spacing-lg: calc(var(--sx-spacing-base-lg) * var(--sx-density-scale, 1));
--sx-spacing-xl: calc(var(--sx-spacing-base-xl) * var(--sx-density-scale, 1));

2.1.4 语义化间距变量

保持现有的语义化间距变量,它们会自动继承密度缩放:

/* 语义化间距(自动应用密度缩放) */
--sx-spacingHorizontalSmall: var(--sx-spacing-sm);
--sx-spacingHorizontalMedium: var(--sx-spacing-md);
--sx-spacingVerticalSmall: var(--sx-spacing-xs);
--sx-spacingVerticalMedium: var(--sx-spacing-sm);

2.2 实现层次

层次 1:CSS 变量层(基础)

sx-tokens.css 中:

  1. 添加 --sx-density-scale 变量
  2. 定义基础间距变量(--sx-spacing-base-*
  3. 更新所有间距变量使用 calc() 和密度因子
  4. 确保向后兼容(现有组件无需修改)

层次 2:运行时层(JS/C#)

JavaScript 运行时theme-runtime.js):

  • 添加 setDensityScale(scale) 方法
  • 添加 getDensityScale() 方法
  • 添加 setDensityPreset(preset) 方法(如 "compact", "comfortable")
  • 持久化到 localStorage(theme_density_scale

C# 运行时ThemeState.cs):

  • 添加 DensityScale 属性(double,范围 0.5-2.0)
  • 添加 SetDensityScale(double scale) 方法
  • 添加 SetDensityPreset(DensityPreset preset) 方法
  • 触发 Changed 事件

层次 3:桥接层(Blazor)

SxThemeProvider.razor 中:

  • 同步 ThemeState.DensityScale 到 CSS 变量
  • 监听 ThemeState.Changed 事件

2.3 影响范围分析

2.3.1 受影响的间距类型

✅ 应该受密度影响

  • 组件内部 padding(如 Button、Input、Card)
  • 组件之间的 gap(如 Stack、Grid)
  • 列表项之间的间距(如 List、Menu)
  • 表单字段之间的间距
  • 卡片之间的间距

⚠️ 需要谨慎处理

  • 边框宽度(通常不受密度影响)
  • 圆角大小(通常不受密度影响)
  • 图标大小(通常不受密度影响,但可以考虑)

❌ 不应该受密度影响

  • 字体大小(由 --sx-font-scale 控制)
  • 行高(通常与字体大小相关)
  • 最小触摸目标(可访问性要求)

2.3.2 组件适配策略

策略 A:自动适配(推荐)

  • 所有组件使用语义化间距变量(如 var(--sx-spacing-md)
  • 密度变化自动生效,无需修改组件代码

策略 B:显式适配(可选)

  • 某些组件可以显式声明是否受密度影响
  • 通过 CSS 类或属性控制(如 density-aware

推荐使用策略 A,因为:

  • 实现简单,无需修改现有组件
  • 一致性更好
  • 向后兼容

2.4 语义化设计变量扩展

2.4.1 新增语义化变量

为了更好的语义表达,可以添加以下变量:

/* 组件内部间距 */
--sx-spacing-component-padding: var(--sx-spacing-md);
--sx-spacing-component-gap: var(--sx-spacing-sm);

/* 布局间距 */
--sx-spacing-layout-gap: var(--sx-spacing-lg);
--sx-spacing-section-gap: var(--sx-spacing-xl);

/* 列表间距 */
--sx-spacing-list-item-gap: var(--sx-spacing-sm);
--sx-spacing-list-section-gap: var(--sx-spacing-md);

这些变量都是基于基础间距变量,会自动应用密度缩放。

2.4.2 控件尺寸考虑

控件高度是否受密度影响?建议:部分影响

/* 控件高度(受密度影响,但影响较小) */
--sx-control-height-sm: calc(1.75rem * (0.8 + 0.2 * var(--sx-density-scale, 1)));
--sx-control-height-md: calc(2.25rem * (0.8 + 0.2 * var(--sx-density-scale, 1)));
--sx-control-height-lg: calc(3rem * (0.8 + 0.2 * var(--sx-density-scale, 1)));

这样控件高度会在 80%-100% 之间变化,既保持可点击性,又体现密度差异。

三、实施计划

阶段 1:CSS 变量扩展(✅ 已完成)

  1. ✅ 在 sx-tokens.css 中添加密度相关变量
  2. ✅ 更新所有间距变量使用密度因子
  3. ✅ 添加语义化间距变量
  4. ✅ 更新 docs/DesignTokens.md
  5. ✅ 测试现有组件是否正常工作

阶段 2:运行时实现(✅ 已完成)

  1. ✅ 在 theme-runtime.js 中添加密度管理
  2. ✅ 在 ThemeState.cs 中添加密度属性
  3. ✅ 在 SxThemeProvider.razor 中添加同步逻辑
  4. ✅ 实现持久化(localStorage)

阶段 3:测试与文档(✅ 已完成)

  1. ✅ 创建测试页面(在 Workbench 中)
  2. ✅ 测试不同密度级别的视觉效果
  3. ✅ 创建完整的单元测试覆盖
    • C# 测试:45 个测试全部通过
    • JavaScript 测试:19 个测试全部通过
    • CSS 变量验证:所有密度相关变量已验证
  4. ✅ 验证所有组件在不同密度下的表现
  5. ✅ 更新文档

阶段 4:优化与完善(⏳ 可选)

  1. ⏳ 添加过渡动画(密度切换时的平滑过渡)
  2. ⏳ 优化控件高度计算
  3. ⏳ 添加更多语义化变量

三、实施完成总结

✅ 已完成的工作(2026-01-13)

CSS 变量层

  • ✅ 添加 --sx-density-scale 变量(动态注入)
  • ✅ 定义基础间距变量(--sx-spacing-base-*
  • ✅ 更新所有间距变量使用 calc() 和密度因子
  • ✅ 添加语义化间距变量(组件、布局、列表等)
  • ✅ 控件高度使用部分缩放(80%-100% 范围)
  • ✅ 更新 docs/DesignTokens.md 文档

JavaScript 运行时

  • ✅ 添加 setDensityScale(scale) 方法
  • ✅ 添加 setDensityPreset(preset) 方法(5 个预设级别)
  • ✅ 添加 getDensityScale() 方法
  • ✅ 在 init() 中加载和应用密度设置
  • ✅ 持久化到 localStorage(theme_density_scale

C# 运行时

  • ✅ 在 IThemeState 接口中添加 DensityScale 属性
  • ✅ 在 ThemeState 类中实现密度管理
  • ✅ 添加 SetDensityScale(double scale) 方法
  • ✅ 实现持久化(通过 IUserPreferences

Blazor 桥接

  • ✅ 在 SxThemeProvider 中同步密度到 CSS
  • ✅ 监听 ThemeState.Changed 事件

测试页面

  • ✅ 在 ThemeTestPage.razor 中添加密度控制
  • ✅ 创建完整的单元测试(C# 和 JavaScript)
  • ✅ 添加密度滑块和预设按钮
  • ✅ 添加实时预览(间距示例、控件高度示例)

📝 创建/修改的文件清单

修改的文件

  1. src/NextUI.Blazor/wwwroot/Styles/sx-tokens.css - 添加密度变量和语义化间距
  2. src/NextUI.Tokens/Exports/theme-runtime.js - 添加密度管理 API
  3. src/NextUI.Blazor/wwwroot/js/theme-runtime.js - 同步更新
  4. src/NextUI.Core/IThemeState.cs - 添加密度属性
  5. src/NextUI.Core/ThemeState.cs - 实现密度管理
  6. src/NextUI.Blazor/Components/SxThemeProvider.razor - 添加密度同步
  7. workbench/NextUI.Workbench/Pages/ThemeTestPage.razor - 添加密度测试
  8. src/NextUI.Core.Tests/ThemeStateTests.cs - 扩展密度相关测试
  9. src/NextUI.Core.Tests/CssTokensTests.cs - 扩展 CSS 密度变量测试
  10. tests/js/theme-runtime.test.mjs - JavaScript 密度功能测试
  11. docs/TESTING.md - 测试指南文档
  12. docs/TESTING_SUMMARY.md - 测试总结文档
  13. docs/DesignTokens.md - 更新文档

创建的文件

  1. docs/LAYOUT_DENSITY_PROPOSAL.md - 技术方案文档

四、风险评估

4.1 低风险

  • ✅ CSS 变量扩展:纯添加,不影响现有代码
  • ✅ 向后兼容:现有组件自动受益

4.2 中风险

  • ⚠️ 组件适配:某些组件可能使用了硬编码的间距值

    • 缓解:代码审查,确保所有组件使用 CSS 变量
    • 检测:编写测试检查硬编码值
  • ⚠️ 视觉一致性:不同密度下某些布局可能不协调

    • 缓解:提供预设级别,避免极端值
    • 测试:在不同密度下测试所有页面

4.3 需要确认

  • 控件高度:是否应该受密度影响?建议部分影响(80%-100%)
  • 圆角大小:是否应该受密度影响?建议不受影响
  • 图标大小:是否应该受密度影响?建议不受影响(由字体缩放控制)

五、设计决策点

5.1 密度因子范围

建议:0.5 - 2.0

  • 最小值 0.5:特别紧凑,但保持可读性
  • 最大值 2.0:特别宽松,但不过度浪费空间
  • 默认值 1.0:正常密度

5.2 预设级别

建议:5 个预设级别

  • Extra Tight: 0.6
  • Compact: 0.8
  • Comfortable: 1.0(默认)
  • Spacious: 1.3
  • Extra Spacious: 1.6

5.3 控件高度处理

建议:部分缩放(80%-100%)

  • 公式:base * (0.8 + 0.2 * density-scale)
  • 这样在密度 0.6 时,高度为 92%(0.8 + 0.2 * 0.6)
  • 在密度 1.6 时,高度为 112%(0.8 + 0.2 * 1.6)

5.4 过渡动画

建议:添加平滑过渡

* {
    transition: padding var(--sx-transition-duration, 0.2s) ease,
                margin var(--sx-transition-duration, 0.2s) ease,
                gap var(--sx-transition-duration, 0.2s) ease;
}

六、与现有功能的关系

6.1 与字体缩放的关系

  • 独立:字体缩放和密度缩放是独立的
  • 协同:可以同时使用,例如:
    • 大字体 + 宽松布局 = 适合阅读
    • 小字体 + 紧凑布局 = 适合工作台

6.2 与主题色的关系

  • 独立:主题色和密度是独立的
  • 协同:都是全局个性化设置的一部分

6.3 与响应式设计的关系

  • 叠加:密度设置在所有断点下都生效
  • 考虑:在移动设备上,可能需要限制最小密度(如不低于 0.7)

七、示例效果

7.1 紧凑模式(0.8)

[Button] [Button] [Button]    ← 按钮间距更小
┌─────────────┐
│   Content   │               ← 卡片内边距更小
└─────────────┘
┌─────────────┐
│   Content   │               ← 卡片间距更小
└─────────────┘

7.2 宽松模式(1.3)

[Button]    [Button]    [Button]    ← 按钮间距更大
┌─────────────────────┐
│                     │
│      Content        │               ← 卡片内边距更大
│                     │
└─────────────────────┘

┌─────────────────────┐
│                     │
│      Content        │               ← 卡片间距更大
│                     │
└─────────────────────┘

八、总结

8.1 核心价值

  1. 用户体验:满足不同用户对信息密度的偏好
  2. 场景适配:不同应用场景可以使用不同密度
  3. 一致性:与主题色、字体缩放形成完整的个性化体系
  4. 向后兼容:现有组件自动受益,无需修改

8.2 实施建议

  1. 分阶段实施:先实现基础功能,再优化细节
  2. 充分测试:在不同密度下测试所有组件
    • ✅ 已完成单元测试覆盖(C# 45个,JS 19个)
    • ✅ 所有测试通过
    • 参考:测试文档
  3. 文档完善:更新设计变量文档和使用指南
  4. 用户反馈:收集用户反馈,持续优化

8.3 下一步

  1. 确认方案细节(特别是控件高度、圆角、图标的处理)
  2. 开始实施阶段 1(CSS 变量扩展)
  3. 创建测试页面验证效果

请确认是否同意此方案,或提出需要调整的地方。