28个Token重构鸿蒙App:企业级设计系统的搭建实践
本文是「食刻 (ShiKe)」技术系列第 4 篇,深入解析 **Design Token 企业级设计系统** —— 28 个设计变量 + BaseCard 通用容器 + @Extend 装饰器的三层架构实践。
·

本文是「食刻 (ShiKe)」技术系列第 4 篇,深入解析 Design Token 企业级设计系统 —— 28 个设计变量 + BaseCard 通用容器 + @Extend 装饰器的三层架构实践。
一、问题:硬编码样式为什么糟糕?
传统开发模式
// ❌ 每个页面都有这样的硬编码
Column() {
Text('标题')
.fontSize(16)
.fontColor('#1A1A1A') // 硬编码颜色
.margin({ left: 16 }) // 硬编码间距
.padding({ top: 12, bottom: 12 })
Text('正文')
.fontSize(14)
.fontColor('#666666') // 又一个硬编码颜色
}
硬编码的三大噩梦
- 维护地狱:改主色调要搜遍整个项目,改一处漏三处
- 不一致:A 页面间距 16,B 页面间距 14,UI 像拼凑的
- 换肤困难:想做暗色模式?重写所有页面,工作量爆炸
食刻的解决方案
Design Token + BaseCard + @Extend = 三层设计系统
二、第一层:AppTheme — 28 个 Design Token
什么是 Design Token?
Design Token 是设计决策的原子化存储。它把颜色、间距、字号等设计值抽离成命名变量,让代码引用变量而非具体值。
❌ 硬编码:fontColor('#1A1A1A')
✅ Token:fontColor(AppTheme.COLOR_TEXT_PRIMARY)
AppTheme.ets 核心代码
// theme/AppTheme.ets
export class AppTheme {
// ==================== 色彩 ====================
// 主色系
static readonly COLOR_PRIMARY = '#52B788'; // 草绿色(健康/积极)
static readonly COLOR_PRIMARY_DARK = '#40916C'; // 深绿
static readonly COLOR_PRIMARY_LIGHT = '#95D5B2'; // 浅绿
// 功能色
static readonly COLOR_SUCCESS = '#52B788'; // 成功
static readonly COLOR_WARNING = '#F4A261'; // 警告/超标
static readonly COLOR_DANGER = '#E07B5A'; // 危险/超量
static readonly COLOR_INFO = '#457B9D'; // 信息
// 文本色
static readonly COLOR_TEXT_PRIMARY = '#1A1A1A'; // 主文本
static readonly COLOR_TEXT_SECONDARY = '#666666'; // 次要文本
static readonly COLOR_TEXT_TERTIARY = '#999999'; // 占位文本
static readonly COLOR_TEXT_INVERSE = '#FFFFFF'; // 反色文本
// 背景色
static readonly COLOR_BG_PAGE = '#F5F6F8'; // 页面背景
static readonly COLOR_BG_CARD = '#FFFFFF'; // 卡片背景
static readonly COLOR_BG_NAVBAR = 'rgba(255,255,255,0.85)'; // 导航栏
// 边框色
static readonly COLOR_BORDER = '#E8E8E8';
static readonly COLOR_BORDER_LIGHT = '#F0F0F0';
// ==================== 间距 ====================
static readonly SPACING_XS = 4;
static readonly SPACING_SM = 8;
static readonly SPACING_MD = 12;
static readonly SPACING_LG = 16;
static readonly SPACING_XL = 24;
static readonly SPACING_XXL = 32;
// 页面水平边距
static readonly PAGE_HORIZONTAL = 16;
// ==================== 圆角 ====================
static readonly RADIUS_SM = 8;
static readonly RADIUS_MD = 12;
static readonly RADIUS_LG = 16;
static readonly RADIUS_XL = 24;
static readonly RADIUS_FULL = 9999; // 全圆角(胶囊按钮)
// ==================== 阴影 ====================
static readonly SHADOW_CARD = '0 2px 8px rgba(0,0,0,0.06)';
static readonly SHADOW_POPUP = '0 4px 16px rgba(0,0,0,0.1)';
static readonly SHADOW_BUTTON = '0 2px 4px rgba(82,183,136,0.3)';
// ==================== 字号 ====================
static readonly FONT_SIZE_XS = 10;
static readonly FONT_SIZE_SM = 12;
static readonly FONT_SIZE_BASE = 14;
static readonly FONT_SIZE_LG = 16;
static readonly FONT_SIZE_XL = 18;
static readonly FONT_SIZE_XXL = 24;
static readonly FONT_SIZE_XXXL = 32;
// ==================== 字重 ====================
static readonly FONT_WEIGHT_NORMAL = FontWeight.Normal;
static readonly FONT_WEIGHT_MEDIUM = FontWeight.Medium;
static readonly FONT_WEIGHT_BOLD = FontWeight.Bold;
}
Token 命名规范
我们遵循 W3C Design Token 社区规范:
{category}-{variant}-{state}
↓
COLOR_TEXT_PRIMARY
↓
语义化命名 > 视觉化命名
❌ COLOR_GREEN (#52B788)
✅ COLOR_PRIMARY (主色)
为什么?因为"绿色"是视觉描述,"主色"是语义含义。
改主题时,只需改 Token 值,语义不变,代码不动。
三、第二层:BaseCard — @BuilderParam 通用容器
问题
每个页面都要重复写卡片样式:
// ❌ 重复
Column() {
Text('标题')
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding(16)
.shadow('0 2px 8px rgba(0,0,0,0.06)')
解法:BaseCard 组件
// theme/BaseCard.ets
@Component
export struct BaseCard {
// ★ @BuilderParam:内容注入插槽 ★
@BuilderParam content: () => void = () => {};
padding: number = AppTheme.SPACING_LG;
build() {
Column() {
// ★ 注入的内容 ★
this.content();
}
.width('100%')
.backgroundColor(AppTheme.COLOR_BG_CARD)
.borderRadius(AppTheme.RADIUS_MD)
.padding(this.padding)
.shadow(AppTheme.SHADOW_CARD)
}
}
使用方式
// ❌ 之前:重复样式
Column() {
Text('标题')
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding(16)
.shadow('0 2px 8px rgba(0,0,0,0.06)')
// ✅ 现在:简洁复用
BaseCard() {
Text('标题')
}
// ✅ 带参数:自定义内边距
BaseCard({ padding: 20 }) {
Column() {
Text('更多内边距的卡片')
}
}
BaseCard 带来的改变
| 维度 | 之前 | 之后 |
|---|---|---|
| 代码行数 | 8 行 | 2 行 |
| 样式一致性 | 人为保证 | 自动保证 |
| 新增页面 | 复制粘贴改改改 | 5 分钟搞定 |
| 改全局圆角 | 搜 50 处 | 改 1 处 |
四、第三层:GlobalStyles — @Extend 排版装饰器
问题
相同类型的文本样式也要重复:
// ❌ 每个页面都要这样
Text('模块标题')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#1A1A1A')
Text('次要说明')
.fontSize(12)
.fontColor('#666666')
解法:@Extend 装饰器
// theme/GlobalStyles.ets
// 模块标题:16px 加粗主色
@Extend(Text) function moduleTitle() {
.fontSize(AppTheme.FONT_SIZE_LG)
.fontWeight(AppTheme.FONT_WEIGHT_BOLD)
.fontColor(AppTheme.COLOR_TEXT_PRIMARY)
}
// 次要文本:12px 灰色
@Extend(Text) function mutedText() {
.fontSize(AppTheme.FONT_SIZE_SM)
.fontColor(AppTheme.COLOR_TEXT_SECONDARY)
}
// 主要按钮样式
@Extend(Button) function primaryButton() {
.fontSize(AppTheme.FONT_SIZE_BASE)
.fontWeight(AppTheme.FONT_WEIGHT_MEDIUM)
.fontColor(AppTheme.COLOR_TEXT_INVERSE)
.backgroundColor(AppTheme.COLOR_PRIMARY)
.borderRadius(AppTheme.RADIUS_FULL)
.shadow(AppTheme.SHADOW_BUTTON)
}
// 次要按钮样式
@Extend(Button) function secondaryButton() {
.fontSize(AppTheme.FONT_SIZE_BASE)
.fontWeight(AppTheme.FONT_WEIGHT_MEDIUM)
.fontColor(AppTheme.COLOR_PRIMARY)
.backgroundColor(Color.Transparent)
.border({
width: 1,
color: AppTheme.COLOR_PRIMARY,
radius: AppTheme.RADIUS_FULL
})
}
使用方式
// 之前
Text('模块标题')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#1A1A1A')
// 现在
Text('模块标题')
.moduleTitle()
// 之前
Button('确认')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#FFFFFF')
.backgroundColor('#52B788')
// 现在
Button('确认')
.primaryButton()
五、三层系统协作示例
完整页面代码
// pages/StatisticsPage.ets
@Entry
@Component
struct StatisticsPage {
@State monthlyData: MonthlyStats = new MonthlyStats();
aboutToAppear(): void {
this.monthlyData = HeatmapViewModel.loadMonthData();
}
build() {
Column() {
// ★ 第1层:BaseCard 通用容器 ★
BaseCard() {
Column({ space: AppTheme.SPACING_MD }) {
// ★ 第2层:AppTheme Token 配色 ★
Text('月度热量统计')
.moduleTitle() // ★ 第3层:GlobalStyles 排版 ★
// 热力图组件
HeatmapGrid({ data: this.monthlyData.heatmapData })
// 统计数据
Row() {
StatItem({ label: '平均摄入', value: '1650', unit: 'kcal' })
StatItem({ label: '最高摄入', value: '2300', unit: 'kcal' })
StatItem({ label: '记录天数', value: '28', unit: '天' })
}
.justifyContent(FlexAlign.SpaceEvenly)
.width('100%')
}
}
// 第二个卡片
BaseCard({ padding: AppTheme.SPACING_XL }) {
Column() {
Text('营养分布')
.moduleTitle()
NutrientRings({ data: this.monthlyData.nutrients })
}
}
}
.padding({
left: AppTheme.PAGE_HORIZONTAL,
right: AppTheme.PAGE_HORIZONTAL,
top: AppTheme.SPACING_LG
})
.backgroundColor(AppTheme.COLOR_BG_PAGE)
}
}
代码行数对比
| 维度 | 硬编码方式 | 三层系统 |
|---|---|---|
| 单个页面行数 | ~150 行 | ~80 行 |
| 颜色引用 | 10+ 处硬编码 | 0 处 |
| 新增页面时间 | 30 分钟 | 5 分钟 |
| 全局改主题 | 不可能 | 改 28 个 Token |
六、暗色模式支持
有了 Design Token,暗色模式只需改 Token 值:
// theme/AppTheme.ets
// 亮色主题
export class AppThemeLight {
static readonly COLOR_BG_PAGE = '#F5F6F8';
static readonly COLOR_BG_CARD = '#FFFFFF';
static readonly COLOR_TEXT_PRIMARY = '#1A1A1A';
}
// 暗色主题
export class AppThemeDark {
static readonly COLOR_BG_PAGE = '#121212';
static readonly COLOR_BG_CARD = '#1E1E1E';
static readonly COLOR_TEXT_PRIMARY = '#FFFFFF';
}
// 根据系统设置切换
@State currentTheme: typeof AppThemeLight = AppThemeLight;
// 页面中使用
Column() {
Text('标题')
.fontColor(this.currentTheme.COLOR_TEXT_PRIMARY)
.backgroundColor(this.currentTheme.COLOR_BG_CARD)
}
七、总结
三层设计系统核心价值
- Token 层:设计决策原子存储,28 个变量在
AppTheme.ets - BaseCard 层:
@BuilderParam通用容器,1 个组件复用所有卡片 - @Extend 层:排版样式复用,4 个装饰器统一文本/按钮样式
效果:
- 新增页面:5 分钟(继承 + 写业务)
- 全局改主题:改 28 个 Token
- 样式一致性:代码级别保证
- 零硬编码:100% Token 引用
系列总结
「食刻 (ShiKe)」技术系列完结
技术系列目录:
- 第 1 篇 | AI 双引擎架构 | 豆包 + DeepSeek 协同方案
- 第 2 篇 | Neumorphism 环形图 | 7 层 ArkUI 组件叠加
- 第 3 篇 | 全场景 Widget | 跨进程数据同步 + 一键直达
- 第 4 篇 | Design Token 系统 | 28 Token + BaseCard + @Extend
项目地址:https://atomgit.com/VON-/cxs-demo1
📌 项目仓库:https://atomgit.com/VON-/cxs-demo1
更多推荐




所有评论(0)