HarmonyOS 6.0 极简 UI 设计系统实战 - 基于「今天空白」当前 UiTokens 拆颜色、间距与样式约束

系列文章 :HarmonyOS 6.0 实战开发 - 「今天空白」应用 第7篇 / 共30篇 发布时间 :2026-04-06 阅读时长 :17分钟 难度:(进阶)


本文导读

前面几篇我们已经把 V2 状态管理、Store 分层和 ArkUI 页面骨架拆开了。这一篇继续沿着当前仓库往下看,但重点不再是"状态怎么流动",而是样式为什么没有散成一地魔法数字

如果你直接打开当前项目,会发现它并没有一个庞大的组件库,也没有复杂的主题引擎。真正承担"设计系统"角色的,是一个非常短的文件:UiTokens.ets

这一篇只讲当前仓库已经落下来的东西,重点看 4 件事:

  • UiTokens 里到底收了哪些视觉常量

  • 这些常量如何在 TodayPageEditSheetSettings 之间复用

  • 当前实现为什么更像"最小设计约束",而不是"大而全组件库"

  • 这种写法在 MVP 阶段解决了什么,又刻意没做什么

对应的真实代码主要来自这些文件:

  • frontend/entry/src/main/ets/features/ui/UiTokens.ets

  • frontend/entry/src/main/ets/features/today/TodayPage.ets

  • frontend/entry/src/main/ets/features/today/EditSheet.ets

  • frontend/entry/src/main/ets/pages/Settings.ets


为什么当前项目值得在这里谈"设计系统"

很多人一听"设计系统",脑子里先冒出来的是:

  • Figma 组件库

  • 深浅色主题切换

  • 一整套 Button / Card / Dialog 抽象层

  • 好几百个 token

但如果只看「今天空白」当前仓库,事情没有那么夸张。

这个项目目前页面并不多,主要还是:

  • 首页 TodayPage

  • 编辑弹层 EditSheet

  • 设置页 Settings

页面数量不多,不代表不需要设计约束。恰恰相反,越是这种功能收敛、视觉气质明确的项目,越应该先把颜色、间距、圆角、按钮高度这些基础常量收起来。

否则很快就会出现这些问题:

  • 首页卡片圆角是 18,设置页卡片圆角又写成 16

  • 主按钮高度一会儿 44,一会儿 48

  • 相同的背景色在不同页面复制多次

  • 二级文字颜色到处手写十六进制值

当前仓库选择的做法不是先造组件库,而是先把会重复的视觉常量收口。这比一开始就堆抽象靠谱得多,也更符合这个项目的 KISS / YAGNI 约束。


先看当前仓库的 UiTokens

当前 UiTokens.ets 很短,代码如下:

复制代码
export class UiTokens {
  static readonly FontFamily: string = 'HarmonyOS Sans';
​
  static readonly ColorPageBg: string = '#F7F4F0';
  static readonly ColorSurface: string = '#F6F1EB';
  static readonly ColorSurfaceSoft: string = '#ECE6DF';
​
  static readonly ColorText: string = '#1E1B17';
  static readonly ColorTextMuted: string = '#5A524B';
  static readonly ColorDanger: string = '#B3382C';
​
  static readonly ColorPrimary: string = '#1E1B17';
  static readonly ColorPrimaryText: string = '#F5F2EE';
​
  static readonly PagePadding: number = 24;
  static readonly CardPadding: number = 16;
  static readonly ModalCardPadding: number = 20;
​
  static readonly RadiusM: number = 16;
  static readonly RadiusL: number = 18;
​
  static readonly GapS: number = 8;
  static readonly GapM: number = 12;
  static readonly GapL: number = 16;
​
  static readonly ButtonHeight: number = 44;
  static readonly ButtonRadius: number = 14;
}

这份定义有几个很鲜明的特点:

1. 范围非常克制

当前只收了 4 类最常用的视觉常量:

  • 字体

  • 颜色

  • 间距

  • 圆角 / 按钮尺寸

没有阴影、没有字号梯度表、没有多套主题、没有动效 token。

这不是"做得不完整",而是因为当前仓库真正用得到的就是这些。对一个首页 + 设置页 + 弹层为主的 MVP 来说,先把高频重复项抽出来已经足够。

2. 命名偏语义,而不是偏数值

比如:

  • ColorSurface

  • ColorSurfaceSoft

  • ColorTextMuted

  • PagePadding

  • ModalCardPadding

这样的命名传达的是用途,而不是单纯数字。

对比一下:

复制代码
.backgroundColor(UiTokens.ColorSurface)
.borderRadius(UiTokens.RadiusL)
.padding(UiTokens.ModalCardPadding)

和:

复制代码
.backgroundColor('#F6F1EB')
.borderRadius(18)
.padding(20)

前者更容易读,因为它表达的是"这是卡片表面色""这是大圆角""这是弹层内边距",而不是在猜一串数字有什么含义。


颜色系统:这套项目视觉不是靠"艳",而是靠层次

先看颜色部分:

复制代码
static readonly ColorPageBg: string = '#F7F4F0';
static readonly ColorSurface: string = '#F6F1EB';
static readonly ColorSurfaceSoft: string = '#ECE6DF';
​
static readonly ColorText: string = '#1E1B17';
static readonly ColorTextMuted: string = '#5A524B';
static readonly ColorDanger: string = '#B3382C';
​
static readonly ColorPrimary: string = '#1E1B17';
static readonly ColorPrimaryText: string = '#F5F2EE';

如果只按当前页面效果和代码结构来理解,这组颜色主要在做三层区分:

1. 页面底色

ColorPageBg 用于最底层页面背景。

首页和设置页都先铺一层这个颜色,再叠背景图:

复制代码
Column() {}
  .width('100%')
  .height('100%')
  .backgroundColor(UiTokens.ColorPageBg);

也就是说,当前项目不是纯靠背景图片撑气氛,底色本身就先把页面整体基调定住了。

2. 内容表面色

ColorSurfaceColorSurfaceSoft 分别承担两种轻量层级:

  • ColorSurface:卡片主体、弹层主体

  • ColorSurfaceSoft:次级按钮、弱操作背景

比如首页内容卡片:

复制代码
.backgroundColor(UiTokens.ColorSurface)

比如"设置"按钮、"取消"按钮、"清空"按钮:

复制代码
.backgroundColor(UiTokens.ColorSurfaceSoft)

这说明当前仓库里的"主次关系"不是靠很多颜色堆出来,而是靠主表面 / 次表面 / 文字 / 主按钮这几个角色区分出来。

3. 文字与动作色

ColorPrimaryColorText 在当前实现里用了同一个深色值:

复制代码
static readonly ColorText: string = '#1E1B17';
static readonly ColorPrimary: string = '#1E1B17';

这很有意思。

它说明这个项目当前并没有追求"品牌主色"式的视觉策略,而是让主按钮继续保持和文本同一套深色基调。这样带来的结果是:

  • 整体视觉更克制

  • 主按钮虽然突出,但不会跳出页面气质

  • 文本和动作按钮处在同一语言体系里

再看错误色 ColorDanger,只在校验或失败提示里出现,例如:

复制代码
Text(this.store.errorMessage)
  .fontSize(12)
  .fontColor(UiTokens.ColorDanger)

用途也很明确:只在确实需要用户注意时出场,不承担普通强调任务。


间距、圆角和按钮尺寸:真正决定"统一感"的往往不是颜色

当前 UiTokens 里还有一组很关键的常量:

复制代码
static readonly PagePadding: number = 24;
static readonly CardPadding: number = 16;
static readonly ModalCardPadding: number = 20;

static readonly RadiusM: number = 16;
static readonly RadiusL: number = 18;

static readonly GapS: number = 8;
static readonly GapM: number = 12;
static readonly GapL: number = 16;

static readonly ButtonHeight: number = 44;
static readonly ButtonRadius: number = 14;

这组值决定的不是"颜色好不好看",而是页面有没有统一节奏。

页面级内边距和卡片级内边距被分开

当前项目至少区分了 3 种空间尺度:

  • PagePadding:整页内容边距

  • CardPadding:普通卡片内边距

  • ModalCardPadding:弹层卡片内边距

这意味着仓库并没有把所有 padding 都偷懒写成一个数字,而是按场景做了最小拆分。

圆角也区分了普通卡片和更强调的容器

  • RadiusM = 16

  • RadiusL = 18

看似只差一点,但在当前实现里已经足够表达:

  • 设置页卡片用普通圆角

  • 首页主内容卡片用更大的圆角

按钮高度和按钮圆角固定下来

按钮几乎都统一使用:

复制代码
.height(UiTokens.ButtonHeight)
.borderRadius(UiTokens.ButtonRadius)

这在当前仓库里非常重要,因为首页、弹层、设置页都有大量按钮。如果这里不统一,视觉会立刻散掉。


TodayPage:当前 token 使用最密集的地方

首页 TodayPage 基本把这套 token 全部串起来了。

先看顶部和主内容结构:

复制代码
Column() {
  Row() {
    Column() {
      Text('今天')
        .fontSize(18)
        .fontWeight(FontWeight.Medium)
        .fontColor(UiTokens.ColorText)
        .fontFamily(UiTokens.FontFamily);
    }

    Button('设置')
      .height(UiTokens.ButtonHeight)
      .borderRadius(UiTokens.ButtonRadius)
      .backgroundColor(UiTokens.ColorSurfaceSoft)
      .fontColor(UiTokens.ColorText);
  }
  .margin({ bottom: UiTokens.GapL });
}
.padding(UiTokens.PagePadding)

这里至少能直接看出 3 个事实:

1. 版面节奏由 token 控制

整页留白、顶部区域下边距、按钮高度,全都统一走 token,而不是页面局部临时决定。

2. 文本和操作共享同一套视觉语义

  • 普通文本走 ColorText

  • 次级按钮走 ColorSurfaceSoft + ColorText

  • 主按钮走 ColorPrimary + ColorPrimaryText

这意味着"页面信息"和"页面动作"并不是两套互相打架的系统。

3. TodayPage 写的是布局,不是数值清单

页面里出现的是 GapLPagePaddingColorSurface 这些词,不是到处 scattered 的 16/18/#F6F1EB。维护时更容易理解设计意图。


EditSheet:弹层没有单独造一套视觉体系

再看编辑弹层:

复制代码
Column() {
  Text('记录')
    .margin({ bottom: UiTokens.GapM })
    .fontFamily(UiTokens.FontFamily)
    .fontColor(UiTokens.ColorText);

  if (this.store.errorMessage) {
    Text(this.store.errorMessage)
      .fontColor(UiTokens.ColorDanger)
      .margin({ top: UiTokens.GapS });
  }

  Row() {
    Button('取消')
      .height(UiTokens.ButtonHeight)
      .borderRadius(UiTokens.ButtonRadius)
      .backgroundColor(UiTokens.ColorSurfaceSoft)
      .fontColor(UiTokens.ColorText);

    Button('保存')
      .height(UiTokens.ButtonHeight)
      .borderRadius(UiTokens.ButtonRadius)
      .backgroundColor(UiTokens.ColorPrimary)
      .fontColor(UiTokens.ColorPrimaryText)
      .margin({ left: UiTokens.GapM });
  }
}
.padding(UiTokens.ModalCardPadding)
.backgroundColor(UiTokens.ColorSurface)
.borderRadius(UiTokens.RadiusM)

这里最值得注意的,不是 token 用了几个,而是弹层继续沿用页面同一套视觉规则

  • 文字颜色还是 ColorText

  • 错误提示还是 ColorDanger

  • 取消按钮还是 ColorSurfaceSoft

  • 保存按钮还是 ColorPrimary

  • 外层容器还是 ColorSurface + RadiusM

这让 EditSheet 看起来像首页的一部分,而不是临时插进来的另一个界面。


Settings:说明这套 token 已经跨页面工作了

如果只有首页使用 UiTokens,那它还只能算"首页样式常量"。

但当前仓库的 Settings.ets 也沿用了同一套值:

复制代码
Column() {
  Text('账号')
    .fontColor(UiTokens.ColorText)
    .fontFamily(UiTokens.FontFamily)
    .margin({ bottom: UiTokens.GapS });
}
.padding(UiTokens.CardPadding)
.backgroundColor(UiTokens.ColorSurface)
.borderRadius(UiTokens.RadiusM)
.margin({ bottom: UiTokens.GapL });

再比如按钮:

复制代码
Button('登录')
  .height(UiTokens.ButtonHeight)
  .borderRadius(UiTokens.ButtonRadius)
  .backgroundColor(UiTokens.ColorPrimary)
  .fontColor(UiTokens.ColorPrimaryText)

这说明 UiTokens 在当前仓库里已经具备了一个很实际的能力:

  • 跨页面统一背景

  • 跨页面统一卡片节奏

  • 跨页面统一按钮层级

  • 跨页面统一字体和文字颜色

对于只有少量页面的项目,这样的收益已经非常直接。


当前实现为什么还不能叫"完整主题系统"

虽然第 7 篇的计划主题叫"极简 UI 设计系统",但如果只按当前代码说实话,本项目现在更准确的状态是:

它已经有最小设计 token 约束,但还没有演进成完整主题系统。

1. 基于 UiTokens 的运行时主题切换

当前 ets 代码里没有基于 UiTokens 的"浅色 / 深色"运行时切换逻辑,也没有多套 token 映射。仓库里确实存在启动页的明暗资源,但它们还没有进入这套页面 token 体系。

2. 组件级抽象

仓库里还没有抽出统一的 PrimaryButtonSurfaceCardConfirmDialog 之类组件。当前主要还是在页面里直接使用 token。

3. 更细的语义层拆分

比如 ColorPrimaryColorText 当前还是同值,说明视觉语义还保持在够用即可的层级,没有继续往品牌系统方向扩。

但这并不是问题。

因为只按这个项目当前规模看,先做 token 收口,再视复杂度决定是否抽组件,才是合理顺序。上来就把一页应用写成 UI 框架,纯属给自己加戏。


从当前仓库里可以直接总结的 5 个实现事实

1. UiTokens 解决的首先是"不要重复写视觉常量"

它并不是为了显得高级,而是为了减少散落的颜色、圆角、间距数字。

2. 当前 token 已经覆盖了最常重复的 UI 基础元素

  • 页面底色

  • 卡片表面色

  • 主次按钮样式

  • 字体

  • 间距

  • 圆角

3. 首页、弹层、设置页主体样式大体共用同一套 token 节奏

当前主要页面的大多数颜色、间距、圆角和按钮尺寸都走 UiTokens,但设置页里仍然能看到少量硬编码值,比如背景预览卡片的 borderRadius(12) 和输入框局部 margin。这说明仓库已经有统一约束,但还没有做到完全收口。

4. 当前仓库故意不提前抽复杂组件

这符合项目一贯的 KISS / YAGNI 边界,不会为了"设计系统"而制造额外复杂度。

5. 设计 token 在这个项目里首先服务的是页面一致性,而不是追求大而全抽象

「今天空白」的产品文案是冷静、直白、只陈述事实。当前这套颜色和样式约束也在服务同样的气质:克制、统一、不夸张。


小结

这一篇如果只按当前仓库来总结,最重要的不是"如何搭建一个宏大的 HarmonyOS 设计系统",而是看清一件更实际的事:

在页面还不多、需求还很收敛的时候,先把颜色、间距、圆角、按钮尺寸这些高频视觉常量收成 UiTokens,就已经能显著提升一致性。

放到「今天空白」当前实现里,它已经带来了这些结果:

  • TodayPageEditSheetSettings 共享同一套视觉语言

  • 页面代码更像在描述布局和层级,而不是堆数字

  • 后续如果真要抽按钮组件、卡片组件,也已经有统一 token 作为基础

下一篇我们把视角从 UI 切到数据层,继续看当前仓库为什么会把"每天一条记录"的本地存储落在 Preferences 上,而不是一开始就引数据库或更复杂的存储方案。( ̄▽ ̄)/

相关推荐
提子拌饭1332 小时前
开源鸿蒙跨平台Flutter开发:国寿险收益速算表系统:基于 Flutter 的金融精算模型与 IRR 收益率动态测绘架构
flutter·华为·金融·开源·harmonyos·鸿蒙
AI_零食2 小时前
开源鸿蒙跨平台Flutter开发:极简暗黑风与五行雷达测绘架构
学习·flutter·游戏·华为·开源·交互·harmonyos
提子拌饭1332 小时前
开源鸿蒙跨平台Flutter开发:中小学跳绳遥测记录表:基于 Flutter 的体能监测与分钟级频域测绘架构
flutter·华为·架构·开源·harmonyos
浮芷.2 小时前
Flutter 框架跨平台鸿蒙开发 - 沉默挑战应用
flutter·华为·harmonyos
stevenzqzq2 小时前
架构设计深度解析:策略模式 + 抽象工厂在UI适配中的高级应用
ui·策略模式
小雨青年2 小时前
鸿蒙 HarmonyOS 6 | TextPickerDialog 迁移实战
华为·harmonyos
sycmancia2 小时前
Qt——计算器示例(用户界面与业务逻辑的分离)
开发语言·qt·ui
独特的螺狮粉2 小时前
开源鸿蒙跨平台Flutter开发:DNA测序波峰色谱可视化分离平台:基于 Flutter 的信号解耦与基因组流体渲染架构
flutter·华为·架构·开源·harmonyos·鸿蒙
Swift社区2 小时前
ArkUI 项目结构设计:小项目 vs 大项目
harmonyos·arkui