【三国志 App 实战系列 02】ArkUI 主题 Token 与深浅色模式实践:让历史类 App 有统一气质
结合 HarmonyOS 三国志 App,讲解 ArkUI 中如何通过颜色、字号、间距、圆角 Token 统一页面风格,并支持深浅色模式切换。
一、为什么不要在页面里写死样式
三国历史类 App 页面很多:人物卡片、事件详情、地图、听书、收藏、我的页面。若每个页面都写:
.fontSize(14)
.fontColor('#333333')
.padding(12)
.borderRadius(16)后期调整主题会非常痛苦。更好的方式是把视觉规范抽象成 Token:
AppColorsAppFontSizeAppSpacingAppRadiusAppTheme
二、页面使用 Token 的方式
在页面中只关心语义,不直接关心具体数值:
Text(this.text)
.fontSize(AppFontSize.F12)
.fontColor(this.selectedCategory === this.text
? this.palette().textInverse
: this.palette().textSecondary)
.padding({
left: AppSpacing.S12,
right: AppSpacing.S12,
top: AppSpacing.S8,
bottom: AppSpacing.S8
})
.backgroundColor(this.selectedCategory === this.text
? this.palette().primary
: this.palette().cardBg)
.borderRadius(AppRadius.R16)这段代码的重点是:页面表达的是“选中态/普通态”,而不是直接写颜色值。
2.1 Token 不是变量表,而是设计语言
很多项目会把 Token 理解成“把颜色值挪到一个文件里”。这只能解决一小部分问题。真正有用的 Token 应该表达语义,例如:
| Token | 语义 | 使用位置 |
|---|---|---|
primary |
品牌主色 | 选中态、主按钮、重要标签 |
pageBg |
页面背景 | 页面根容器 |
cardBg |
卡片背景 | 人物卡片、事件卡片、设置项 |
textPrimary |
主文本 | 标题、正文重点 |
textSecondary |
次级文本 | 摘要、说明、辅助信息 |
divider |
分割线 | 列表、分组边界 |
也就是说,页面不应该知道“当前选中态是棕色”,页面只应该知道“这是 primary”。至于 primary 在浅色模式、深色模式、节日主题下分别是什么颜色,交给主题系统决定。
2.2 推荐的 Token 文件拆分
主题相关代码不要全部堆在一个文件里。一个更清晰的拆分方式是:
entry/src/main/ets/theme/
├── AppColors.ets
├── AppFontSize.ets
├── AppRadius.ets
├── AppSpacing.ets
└── AppTheme.ets其中 AppColors 负责颜色结构,AppTheme 负责根据深浅色状态返回具体调色板。
export interface AppColors {
pageBg: ResourceColor;
cardBg: ResourceColor;
primary: ResourceColor;
textPrimary: ResourceColor;
textSecondary: ResourceColor;
textInverse: ResourceColor;
divider: ResourceColor;
}字号、间距和圆角可以先用常量表达:
export class AppSpacing {
static readonly S4 = 4;
static readonly S8 = 8;
static readonly S12 = 12;
static readonly S16 = 16;
static readonly S20 = 20;
static readonly S24 = 24;
}
export class AppRadius {
static readonly R8 = 8;
static readonly R12 = 12;
static readonly R16 = 16;
static readonly R24 = 24;
}这样写的好处不是少打几个数字,而是页面之间能保持节奏一致。首页卡片、人物详情卡片、收藏列表项如果都使用同一组间距,整个 App 会自然统一。
三、深浅色切换的状态来源
深浅色模式可以设计为三种:
- 浅色
- 深色
- 跟随系统
页面侧只通过统一方法获取当前调色板:
private palette(): AppColors {
return AppTheme.palette(this.effectiveIsDark());
}这样页面不用关心主题策略,后续如果增加节日主题、护眼主题,也只需要扩展 AppTheme。
3.1 AppTheme 的实现方式
下面是一个简化后的主题实现。真实项目里颜色可以来自 resources,也可以先用十六进制值验证效果。
export class AppTheme {
static light(): AppColors {
return {
pageBg: '#F7F1E8',
cardBg: '#FFFFFF',
primary: '#8A4B2A',
textPrimary: '#2B2118',
textSecondary: '#7A6A5A',
textInverse: '#FFFFFF',
divider: '#E6D8C7'
};
}
static dark(): AppColors {
return {
pageBg: '#17130F',
cardBg: '#241C16',
primary: '#D6A15F',
textPrimary: '#F5E8D5',
textSecondary: '#B9A58D',
textInverse: '#1A120C',
divider: '#3A2B20'
};
}
static palette(isDark: boolean): AppColors {
return isDark ? AppTheme.dark() : AppTheme.light();
}
}这里要注意一点:深色模式不是简单把背景改黑。主色、次级文字、分割线和卡片背景都要重新校准,否则会出现“背景暗了,但卡片和文字仍然像浅色模式”的割裂感。
3.2 跟随系统与用户选择
主题模式建议设计成三态:
export enum ThemeMode {
Light = 'light',
Dark = 'dark',
System = 'system'
}页面渲染时使用 effectiveIsDark() 得到最终状态:
private effectiveIsDark(): boolean {
if (this.themeMode === ThemeMode.Dark) {
return true;
}
if (this.themeMode === ThemeMode.Light) {
return false;
}
return this.systemIsDark;
}这样设置页可以保留用户偏好,页面组件也不需要知道当前是“手动深色”还是“跟随系统深色”。
四、历史类应用的视觉建议
历史类 App 不适合高饱和、强科技感的视觉。这个项目采用:
- 暖色主色
- 纸张质感背景
- 低对比卡片阴影
- 大圆角卡片
- 图文混排
听书页背景图示例:

编辑
4.1 如何让历史感不变成“老旧感”
历史类应用很容易走向两个极端:一种是颜色过暗、纹理过重,像仿古网页;另一种是完全套用通用 SaaS 风格,缺少题材气质。这个项目的处理方式是:只在主色、插图、卡片层级和标题字重上体现历史感,交互结构仍然保持现代 App 的清晰和轻量。
| 设计项 | 推荐做法 | 避免做法 |
|---|---|---|
| 背景 | 低饱和暖色或深色底 | 大面积复杂纹理 |
| 卡片 | 轻阴影、清晰分组 | 边框过重、装饰过多 |
| 主色 | 棕金、陶土、墨色体系 | 高饱和荧光色 |
| 文字 | 标题稳重、正文清晰 | 全局书法字体 |
| 图片 | 真实页面截图和人物图 | 与内容无关的氛围图 |
视觉上“有题材感”即可,不要让题材感影响阅读效率。尤其是技术文章里展示 App 截图时,截图要能说明页面结构,而不是只当装饰。
五、组件层避免命名冲突
ArkUI 中很多名称本身就是内建属性或组件,如 width、height、border、onClick。自定义组件成员建议避开这些名称。
推荐:
@Component
struct EventCategoryChip {
text: string = '';
isDark: boolean = false;
@Link selectedCategory: string;
build() {
Text(this.text)
.onClick(() => {
this.selectedCategory = this.text;
})
}
}不推荐把成员命名为 onClick、size、border 等。
5.1 把主题注入组件,而不是让组件自己判断
如果每个组件都自己读取深浅色状态,会导致状态来源混乱。更推荐的方式是页面拿到 palette(),再传给子组件。
@Component
struct PersonCard {
person: PersonModel;
colors: AppColors;
build() {
Column() {
Text(this.person.name)
.fontSize(AppFontSize.F18)
.fontColor(this.colors.textPrimary)
Text(this.person.summary)
.fontSize(AppFontSize.F14)
.fontColor(this.colors.textSecondary)
.margin({ top: AppSpacing.S8 })
}
.padding(AppSpacing.S16)
.backgroundColor(this.colors.cardBg)
.borderRadius(AppRadius.R16)
}
}这样 PersonCard 仍然是纯展示组件,它不关心主题模式从哪里来,也不关心设置页怎么保存偏好。
5.2 组件状态命名建议
ArkUI 组件内部字段建议使用业务语义命名:
| 不推荐 | 推荐 |
|---|---|
size |
avatarSize / cardSize |
border |
borderColor / borderWidth |
onClick |
onSelect / onToggleFavorite |
color |
titleColor / badgeColor |
data |
personList / eventItems |
这样做能减少和系统属性、组件方法的冲突,也能让代码审查时更容易看懂组件职责。
六、落地清单
- 所有颜色走
AppTheme.palette() - 所有字号走
AppFontSize - 所有间距走
AppSpacing - 所有圆角走
AppRadius - 页面不要散落硬编码颜色
- 选中态、禁用态、强调态用语义字段表达
6.1 主题改造步骤
如果一个页面已经写了大量硬编码样式,可以按下面的顺序重构:
1. 先抽颜色,不动布局。
2. 再抽字号和间距,让卡片节奏统一。
3. 最后处理深色模式,逐个页面检查对比度。
4. 把高频样式沉淀成组件,例如 SectionTitle、PersonCard、InfoRow。
5. 保存主题偏好,并在 App 重启后恢复。
不要一上来就全局大改。主题系统影响范围很大,小步替换更稳。
6.2 自测清单
| 检查项 | 通过标准 |
|---|---|
| 浅色模式 | 背景、卡片、标题、正文、分割线层级清晰 |
| 深色模式 | 无纯黑糊成一片,次级文字仍可读 |
| 选中态 | Tab、筛选 Chip、按钮状态能明显区分 |
| 平板布局 | 卡片宽度和阅读行长不过度拉伸 |
| 截图展示 | 技术文章中的截图能说明布局,不只是封面 |
| 代码维护 | 页面内不再散落大量 #FFFFFF、12、16 |
在这个项目里,主题 Token 的收益不只体现在视觉统一,也体现在后续维护效率。比如要调整历史感主色,只需要改 AppTheme,不用逐个页面翻 .fontColor()。
七、小结
主题 Token 不是“设计洁癖”,而是中大型 ArkUI 应用的维护基础。它解决的是三个问题:页面风格统一、深浅色模式可控、后续改版成本可接受。
如果你也在做 HarmonyOS / ArkUI 内容型应用,可以先从颜色语义化开始:把 pageBg、cardBg、primary、textPrimary、textSecondary 这些基础 Token 建起来,再逐步扩展字号、间距和圆角。下一篇会继续讲首页搭建:如何用 Banner、快捷入口、人物卡片和事件索引组织历史内容。
更多推荐

所有评论(0)