HarmonyOS 6.0 极简 UI 设计系统实战 - 基于「今天空白」当前 UiTokens 拆颜色、间距与样式约束
本文介绍了HarmonyOS 6.0开发中如何通过UiTokens.ets文件构建极简设计系统。该文件集中管理了颜色、间距、圆角等视觉常量,确保TodayPage、EditSheet和Settings页面间的视觉统一。文章分析了UiTokens的语义化命名特点、颜色系统层次结构,以及如何通过约束间距和圆角建立页面节奏。当前实现注重最小化设计约束而非构建完整主题系统,符合MVP阶段的开发原则,为后续
系列文章:HarmonyOS 6.0 实战开发 - 「今天空白」应用 第7篇 / 共30篇 发布时间:2026-04-06 阅读时长:17分钟 难度:(进阶)
本文导读
前面几篇我们已经把 V2 状态管理、Store 分层和 ArkUI 页面骨架拆开了。这一篇继续沿着当前仓库往下看,但重点不再是“状态怎么流动”,而是样式为什么没有散成一地魔法数字。
如果你直接打开当前项目,会发现它并没有一个庞大的组件库,也没有复杂的主题引擎。真正承担“设计系统”角色的,是一个非常短的文件:UiTokens.ets。
这一篇只讲当前仓库已经落下来的东西,重点看 4 件事:
-
UiTokens里到底收了哪些视觉常量 -
这些常量如何在
TodayPage、EditSheet、Settings之间复用 -
当前实现为什么更像“最小设计约束”,而不是“大而全组件库”
-
这种写法在 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. 内容表面色
ColorSurface 和 ColorSurfaceSoft 分别承担两种轻量层级:
-
ColorSurface:卡片主体、弹层主体 -
ColorSurfaceSoft:次级按钮、弱操作背景
比如首页内容卡片:
.backgroundColor(UiTokens.ColorSurface)
比如“设置”按钮、“取消”按钮、“清空”按钮:
.backgroundColor(UiTokens.ColorSurfaceSoft)
这说明当前仓库里的“主次关系”不是靠很多颜色堆出来,而是靠主表面 / 次表面 / 文字 / 主按钮这几个角色区分出来。
3. 文字与动作色
ColorPrimary 和 ColorText 在当前实现里用了同一个深色值:
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 写的是布局,不是数值清单
页面里出现的是 GapL、PagePadding、ColorSurface 这些词,不是到处 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. 组件级抽象
仓库里还没有抽出统一的 PrimaryButton、SurfaceCard、ConfirmDialog 之类组件。当前主要还是在页面里直接使用 token。
3. 更细的语义层拆分
比如 ColorPrimary 和 ColorText 当前还是同值,说明视觉语义还保持在够用即可的层级,没有继续往品牌系统方向扩。
但这并不是问题。
因为只按这个项目当前规模看,先做 token 收口,再视复杂度决定是否抽组件,才是合理顺序。上来就把一页应用写成 UI 框架,纯属给自己加戏。
从当前仓库里可以直接总结的 5 个实现事实
1. UiTokens 解决的首先是“不要重复写视觉常量”
它并不是为了显得高级,而是为了减少散落的颜色、圆角、间距数字。
2. 当前 token 已经覆盖了最常重复的 UI 基础元素
-
页面底色
-
卡片表面色
-
主次按钮样式
-
字体
-
间距
-
圆角
3. 首页、弹层、设置页主体样式大体共用同一套 token 节奏
当前主要页面的大多数颜色、间距、圆角和按钮尺寸都走 UiTokens,但设置页里仍然能看到少量硬编码值,比如背景预览卡片的 borderRadius(12) 和输入框局部 margin。这说明仓库已经有统一约束,但还没有做到完全收口。
4. 当前仓库故意不提前抽复杂组件
这符合项目一贯的 KISS / YAGNI 边界,不会为了“设计系统”而制造额外复杂度。
5. 设计 token 在这个项目里首先服务的是页面一致性,而不是追求大而全抽象
「今天空白」的产品文案是冷静、直白、只陈述事实。当前这套颜色和样式约束也在服务同样的气质:克制、统一、不夸张。
小结
这一篇如果只按当前仓库来总结,最重要的不是“如何搭建一个宏大的 HarmonyOS 设计系统”,而是看清一件更实际的事:
在页面还不多、需求还很收敛的时候,先把颜色、间距、圆角、按钮尺寸这些高频视觉常量收成 UiTokens,就已经能显著提升一致性。
放到「今天空白」当前实现里,它已经带来了这些结果:
-
TodayPage、EditSheet、Settings共享同一套视觉语言 -
页面代码更像在描述布局和层级,而不是堆数字
-
后续如果真要抽按钮组件、卡片组件,也已经有统一 token 作为基础
下一篇我们把视角从 UI 切到数据层,继续看当前仓库为什么会把“每天一条记录”的本地存储落在 Preferences 上,而不是一开始就引数据库或更复杂的存储方案。( ̄▽ ̄)/
更多推荐


所有评论(0)