糖画坊:用鸿蒙 ArkTS 浇筑一份「一勺糖浆千般造化」的琥珀焦糖 UI
糖画坊:用鸿蒙 ArkTS 浇筑一份「一勺糖浆千般造化」的琥珀焦糖 UI
一、写在前面
糖画,是街头巷尾最有人间烟火气的非遗手艺。老艺人舀一勺滚烫的糖浆,手腕一抖,龙飞凤舞、花鸟鱼虫便跃然板上。而这一份代码,正是把这份"琥珀焦糖"的质感搬进了鸿蒙世界:整份工程以焦糖棕与琥珀金为主色,构建了一个名为"糖画坊"的完整单页应用,涵盖了本周销量看板、糖画陈列、配方簿、糖色谱、订单台账、客评口碑六大数据场景,并且内置了三个可交互的模态弹窗。
从工程结构上看,这是一个非常典型的 ArkTS 声明式 UI 案例:interface 定义数据契约、enum 定义页签枚举、@Observed 修饰的类承载业务数据、@Entry @Component 修饰的结构体承载页面、@Builder 抽离可复用的布局片段。它不是最复杂的工程,却是理解"状态驱动渲染"这条主线的最佳范本。下文会沿着代码的物理顺序逐段拆解,把每一块拼图为什么这样写、数据如何流动、界面如何被状态驱动,都讲透。

二、项目概览与技术要点
在进入逐段分析之前,先用一张架构图把整份代码的"骨架"立起来。整份代码可以划分成四个层次:类型与常量层(色板、枚举、接口、图表数据)、数据模型层(被观察的数据类)、工具函数层(各类取色映射)、界面层(主组件、弹窗构建器、内容子组件)。
@Observe -----------------------^ Expecting 'AMP', 'COLON', 'PIPE', 'TESTSTR', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', got 'LINK_ID'
这张图揭示了一个关键事实:界面层的数据全部来自 SugarData 单例对象,子组件通过 @ObjectLink 与其建立双向绑定,主组件通过 @State 管理页签和弹窗的可见性。这四层各司其职,互不越界,正是工程可读性的来源。
三、逐段代码解析
3.1 主题色板:SugarPalette 接口与 COLORS 常量
interface SugarPalette {
primary: string;
primaryLight: string;
primaryDark: string;
accent: string;
accentLight: string;
bg: string;
cardBg: string;
cardAlt: string;
textPrimary: string;
textSecondary: string;
textHint: string;
border: string;
line: string;
success: string;
warning: string;
danger: string;
white: string;
amber: string;
caramel: string;
}
const COLORS: SugarPalette = {
primary: '#B5651D',
primaryLight: '#E0A458',
primaryDark: '#6B3410',
accent: '#D4A017',
accentLight: '#F0C75E',
bg: '#FBF3E4',
cardBg: '#FFFFFF',
cardAlt: '#F4E6CC',
textPrimary: '#4A2C0E',
textSecondary: '#7A5A3A',
textHint: '#B89A6E',
border: '#E6CFA0',
line: '#EFE0C4',
success: '#5A7D4C',
warning: '#D4A017',
danger: '#A8362A',
white: '#FFFFFF',
amber: '#D4A017',
caramel: '#B5651D'
};
逐点拆解:
第一,用接口先定义"色板长什么样",再用常量实现它。这是 TypeScript/ArkTS 中非常推荐的写法:SugarPalette 接口相当于一份"设计规范清单",把界面里可能用到的所有语义色全部登记在册;COLORS 常量则给出具体色值。后续任何组件里写 .backgroundColor(COLORS.cardAlt),语义一目了然,而不会出现满屏裸色值这种难以维护的局面。
第二,色板设计遵循"主色—辅助色—背景—文字—状态色"的完整体系。primary/primaryLight/primaryDark 构成焦糖棕的主色梯度,accent/accentLight 是琥珀金强调色,bg/cardBg/cardAlt 负责页面与卡片背景的层次,textPrimary/textSecondary/textHint 控制文字的三级信息密度,success/warning/danger 是状态色,最后 amber 与 caramel 是呼应主题的两个点缀色。这样一套色板,既保证了全应用视觉统一,也为后续的"状态色映射函数"提供了取色素材。
第三,色值的选取本身就在讲故事:#B5651D 是烤过的焦糖色,#D4A017 是琥珀的透亮金黄,#6B3410 是糖浆熬老后的深褐,#FBF3E4 是米纸般的暖底。颜色不再是随机的 HEX 值,而是主题语境的延伸。
3.2 页签体系:枚举、页签元数据与内容接口
enum SugarTab {
PAINT = 0,
RECIPE = 1,
COLOR = 2,
ORDER = 3,
REVIEW = 4
}
interface STabMeta {
key: string;
icon: string;
label: string;
}
const TAB_LIST: STabMeta[] = [
{ key: 'paint', icon: '🍭', label: '糖画' },
{ key: 'recipe', icon: '📒', label: '配方' },
{ key: 'color', icon: '🎨', label: '糖色' },
{ key: 'order', icon: '📦', label: '订单' },
{ key: 'review', icon: '⭐', label: '客评' }
];
逐点拆解:
其一,enum SugarTab 把五个页签的索引语义化。代码里 this.curTab === SugarTab.PAINT 比 this.curTab === 0 可读性强得多,而且当页签顺序调整时,枚举名不会跟着错位,这是用枚举替代魔法数字的最大收益。
其二,STabMeta 接口把"一个页签需要哪些信息"固定下来:key 用于 ForEach 的键值生成(保证列表复用时的身份稳定),icon 是 emoji 图标,label 是中文文案。TAB_LIST 数组把五个页签的数据集中管理,底部导航栏直接 ForEach(TAB_LIST) 渲染,将来加一个页签只需往数组里塞一条记录,无需改动渲染逻辑。
其三,紧随其后定义了五组内容实体的接口:PaintItem(糖画:名称、形制、匠级、价格、emoji)、RecipeItem(配方:糖水比、温度、工艺)、SugarColorItem(糖色:色温、用途、色值)、SugarOrderItem(订单:地区、数量、金额)、SugarReviewItem(客评:评分、日期、内容、标签)。这些接口既是数据的"形状契约",也决定了列表项 UI 要展示哪些字段——接口先行,UI 后行,是数据驱动渲染的基础。
3.3 图表数据常量:WEEK_SOLD 与 COLOR_SHARE
const WEEK_SOLD: WeekSugarMeta[] = [
{ day: '周一', value: 22 },
{ day: '周二', value: 28 },
{ day: '周三', value: 34 },
{ day: '周四', value: 40 },
{ day: '周五', value: 62 },
{ day: '周六', value: 88 },
{ day: '周日', value: 76 }
];
const COLOR_SHARE: ColorShareMeta[] = [
{ name: '金黄糖', pct: 40, color: '#D4A017' },
{ name: '焦糖', pct: 28, color: '#B5651D' },
{ name: '琥珀糖', pct: 20, color: '#E0A458' },
{ name: '深焦糖', pct: 12, color: '#6B3410' }
];

逐点拆解:
这两组常量是"假数据驱动的可视化"的典型写法。WEEK_SOLD 记录一周七天的销量,value 既用于柱子的高度(.height(w.value)),也用于文字展示;数据里"周六 88 支"的峰值直接决定了页面右上角提示语"周六88支"的来源。COLOR_SHARE 则是糖色占比数据,每项自带 color 字段,使得色块、图例、文字三者颜色天然一致。把图表数据抽成模块级常量而非散落在组件内部,好处是数据与渲染解耦,后续接真实接口时只需替换数据来源。
3.4 数据模型层:@Observed 修饰的 SugarData
@Observed
export class SugarData {
paints: PaintItem[] = [ /* 12 条糖画数据 */ ];
recipes: RecipeItem[] = [ /* 12 条配方数据 */ ];
colors: SugarColorItem[] = [ /* 12 条糖色数据 */ ];
orders: SugarOrderItem[] = [ /* 12 条订单数据 */ ];
reviews: SugarReviewItem[] = [ /* 12 条客评数据 */ ];
}
逐点拆解:
这里要重点讲透 @Observed 与 @ObjectLink 的配合。@Observed 装饰器让 SugarData 成为可观察对象:当它的属性(比如 paints 数组)被修改时,所有通过 @ObjectLink 绑定它的子组件都会收到通知并自动重绘。这份代码里,主组件持有 @State data: SugarData = new SugarData(),然后以 SugarPaintContent({ data: this.data, ... }) 的形式传给五个子组件,子组件内部用 @ObjectLink data: SugarData 接收。@State 保证数据在组件树顶层存活,@ObjectLink 保证子组件对数据的修改能"反向通知"其他观察者,这就是"单一数据源、多端同步"的 V 模型。
每个数组恰好 12 条数据,这不是巧合:它让五个页签在视觉上"势均力敌",滚动列表都有足够的内容填充。数据里刻意埋了细节——比如糖龙匠级 95、价格 38 元,糖十二生肖 128 元套装——这些数值后续会被取色函数消费,形成"数值→颜色"的映射链。
3.5 工具函数层:四组取色映射
function getPaintColor(level: number): string {
if (level >= 95) {
return '#A8362A';
} else if (level >= 90) {
return '#D4A017';
} else if (level >= 85) {
return '#B5651D';
}
return '#E0A458';
}
function getRecipeColor(skill: string): string {
if (skill === '拉丝' || skill === '拉白' || skill === '熬色') {
return '#D4A017';
} else if (skill === '挂霜' || skill === '琉璃' || skill === '切块') {
return '#B5651D';
}
return '#E0A458';
}
function getOrderColor(amount: number): string {
if (amount >= 90000) {
return '#A8362A';
} else if (amount >= 60000) {
return '#D4A017';
} else if (amount >= 40000) {
return '#B5651D';
}
return '#7A5A3A';
}
function getReviewTagColor(tag: string): string {
if (tag === '拉丝不断' || tag === '远渡飘香' || tag === '秘方地道' || tag === '节庆千单') {
return '#D4A017';
} else if (tag === '伴手有面' || tag === '展馆打卡' || tag === '一夜爆单') {
return '#B5651D';
} else if (tag === '配茶一绝' || tag === '造型萌' || tag === '日销百件' || tag === '祝寿添彩' || tag === '课堂爆满') {
return '#E0A458';
}
return '#7A5A3A';
}
逐点拆解:
这四个函数是"数值/文本 → 主题色"的映射器,集中体现了阈值分级与白名单分级两种映射策略。
getPaintColor 与 getOrderColor 采用阈值分级:匠级 ≥95 显示深红 #A8362A,≥90 显示琥珀金,≥85 显示焦糖棕,其余显示浅琥珀;订单金额 ≥9 万、≥6 万、≥4 万依次降级。这种写法的妙处在于,同一个颜色同时承担了"视觉分级"与"信息提示"两个职责——列表里扫一眼颜色,就知道哪件糖画最稀有、哪张订单最大。
getRecipeColor 与 getReviewTagColor 采用白名单分级:把工艺名、客评标签按语义归类到固定色值。白名单写法虽然不通用(每加一个标签都要改函数),但在数据量固定、语义明确的场景下反而最可控——颜色由"业务含义"决定,而不是由数值大小决定。
把取色逻辑收敛成独立函数还有一个隐性收益:颜色计算只发生一次、只在一处,后续如果主题要换色系(比如从焦糖改成抹茶),只需改函数返回值,全应用联动。
3.6 主组件 SugarApp:状态中枢与弹窗构建器
@Entry
@Component
struct SugarApp {
@State curTab: number = 0;
@State data: SugarData = new SugarData();
@State showAddPaint: boolean = false;
@State showDeleteRecipe: boolean = false;
@State showEditColor: boolean = false;
@State delRecipeName: string = '';
@State editColorName: string = '';
@State sugarPulse: boolean = false;
@State flame: boolean = false;
...
}
逐点拆解:
SugarApp 是整个应用的状态中枢,@State 变量可以分成三组:
- 导航状态:
curTab记录当前页签索引,驱动内容区条件渲染; - 弹窗状态:
showAddPaint / showDeleteRecipe / showEditColor三个布尔值控制弹窗显隐,delRecipeName / editColorName记录"要删除的配方名"“要编辑的糖色名”,让弹窗知道自己在操作谁; - 动画状态:
sugarPulse / flame控制顶栏糖葫芦的缩放脉冲与火焰的明暗。
这种"状态分组"的习惯很重要:把互不相关的状态拆开,各自独立更新,才能避免一个状态变化触发整棵组件树重绘。
接着看三个弹窗构建器中最具代表性的 addPaintModal:
@Builder addPaintModal() {
Stack() {
this.modalOverlay(() => { this.showAddPaint = false; })
Column() {
Row() {
Text('🍭 新制糖画').fontSize(16).fontWeight(FontWeight.Bold)
Column().layoutWeight(1)
Text('✕').onClick(() => { this.showAddPaint = false; })
}
...
Row() {
Text('再想想').onClick(() => { this.showAddPaint = false; })
Text('确认制糖').onClick(() => { this.showAddPaint = false; })
}
}
.width('86%').backgroundColor(COLORS.cardBg).borderRadius(18)
}
.width('100%').height('100%').position({ x: 0, y: 0 })
}
逐点拆解:
弹窗的构建是这套代码里最值得反复咀嚼的部分。它把"弹窗"拆成了三层:
第一层是遮罩。modalOverlay(onClose) 是一个参数化 @Builder:它接受一个"关闭回调",渲染一个半透明黑色全屏蒙层,点击蒙层即触发回调。注意回调由调用方注入,所以遮罩组件本身完全不关心"关闭后要做什么",只负责"点击了我就要通知你"。这是控制反转在 UI 构建中的体现。
第二层是卡片容器。宽度 86%、白底、圆角 18、最大高度限制 80%,配合遮罩构成经典的居中弹窗形态。constraintSize({ maxHeight: '80%' }) 是鸿蒙特有的尺寸约束写法,防止内容过长时溢出屏幕。
第三层是内容与操作区。标题行用 Column().layoutWeight(1) 作为弹性占位把标题和关闭按钮推到两端;信息行采用"标签 + 值"的双列布局,标签固定宽度、值随内容自适应;底部操作行用 FlexAlign.End 右对齐,主次按钮通过实底(确认)与描边(再想想)区分权重。
值得一提的是,三个弹窗(新增糖画、撤下配方、调校糖色)结构高度同构——都是"标题 + 信息行 + 双按钮",只是文案与字段不同。作者没有把它们压成一个泛化组件,而是各自独立成 @Builder。这个取舍在示例代码里是可接受的:可读性优先于 DRY 原则,三个弹窗各自独立、一目了然,也方便单独调整某个弹窗的布局。
3.7 顶栏 topBar:动态装饰与交互动画
@Builder topBar() {
Column() {
Row() {
Column() {
Text('🍭 糖画坊').fontSize(20).fontWeight(FontWeight.Bold)
Text('Sugar · 一勺糖浆 千般造化').fontSize(10)
}
Row() {
Text('🍬').scale({ x: this.sugarPulse ? 1.25 : 0.9, y: ... })
.animation({ duration: 500, curve: Curve.EaseOut })
.onClick(() => { this.sugarPulse = !this.sugarPulse; })
Text('🔥').opacity(this.flame ? 1.0 : 0.4)
.animation({ duration: 400, curve: Curve.EaseOut })
.onClick(() => { this.flame = !this.flame; })
Text('🌡️')
}
}
Row() {
ForEach([0, 1, 2, 3, 4, 5, 6, 7], (r: number) => { /* 装饰圆点 */ })
}
Row() { /* 分割线 + 标语 */ }
}
.backgroundColor(COLORS.primaryDark)
}
逐点拆解:
topBar 是"深色底 + 三行结构"的顶栏:第一行是标题与交互小图标,第二行是 8 个交替色圆点组成的装饰条,第三行是"线—标语—线"的分割组件。交互亮点在两个可点击的 emoji 上:
- 🍬 糖葫芦点击后在
scale1.25 与 0.9 之间切换,配合 500ms 的Curve.EaseOut缓出曲线,形成"鼓起又回落"的呼吸感; - 🔥 火焰点击后在透明度 1.0 与 0.4 之间切换,模拟火苗的明暗。
这里的精髓是用 @State 布尔值驱动属性插值:this.sugarPulse ? 1.25 : 0.9 不是一次性取值,而是随着状态切换,由 animation() 在旧值和新值之间补间。声明式框架下做动画,不需要手动操作时间轴,只需声明"目标状态",中间过程交给框架。
3.8 主构建函数 build():页签切换与弹窗挂载
build() {
Column() {
this.topBar()
if (this.curTab === SugarTab.PAINT) {
SugarPaintContent({ data: this.data, onAdd: () => { this.showAddPaint = true; } })
} else if (this.curTab === SugarTab.RECIPE) {
SugarRecipeContent({ data: this.data, onDel: (n: string) => {
this.delRecipeName = n;
this.showDeleteRecipe = true;
} })
} else if (this.curTab === SugarTab.COLOR) {
SugarColorContent({ data: this.data, onEdit: (n: string) => {
this.editColorName = n;
this.showEditColor = true;
} })
} else if (this.curTab === SugarTab.ORDER) {
SugarOrderContent({ data: this.data })
} else {
SugarReviewContent({ data: this.data })
}
Column().width('100%').height(3).backgroundColor(COLORS.primary)
Row() { ForEach(TAB_LIST, (t, idx) => { /* 底部导航 */ }) }
if (this.showAddPaint) { this.addPaintModal() }
if (this.showDeleteRecipe) { this.deleteRecipeModal() }
if (this.showEditColor) { this.editColorModal() }
}
.backgroundColor(COLORS.bg)
}
逐点拆解:
build() 的布局是标准的"三段式":顶部顶栏、中间内容区、底部导航栏,弹窗以覆盖层形式挂在最外层 Column 之后。
内容区的 if / else if 链把 curTab 映射到五个子组件,这是条件渲染最直观的形态:切换页签就是改一个 @State,框架自动销毁旧组件、创建新组件。注意回调参数的传递方式——SugarPaintContent 收到 onAdd 回调、SugarRecipeContent 收到 onDel(n)、SugarColorContent 收到 onEdit(n),它们统一通过"回调上抛"的方式把子组件的操作意图传回主组件,由主组件统一修改状态。子组件不直接改弹窗状态,只喊"我要加料/我要删配方",具体怎么弹窗由父组件决定,这让组件间通信方向保持单向清晰。
底部导航的渲染同样值得注意:ForEach(TAB_LIST, ...) 遍历页签元数据生成五个导航项,选中态通过 this.curTab === idx 三重联动——文字颜色、加粗权重、背景色一起变化,形成明确的"我被选中"视觉反馈。背景色 COLORS.accent + '1F' 是带透明度的十六进制拼接:'#D4A017' + '1F' 得到 8 位色值,末尾两位 1F 是约 12% 的透明度。这是这套代码里反复使用的小技巧,值得记住。
3.9 通用标签组件 SugarTag
@Component
struct SugarTag {
@Prop text: string;
@Prop color: string;
build() {
Text(this.text)
.fontSize(9)
.fontColor(this.color)
.padding({ left: 7, right: 7, top: 2, bottom: 2 })
.backgroundColor(this.color + '1F')
.borderRadius(9)
.border({ width: 1, color: this.color + '40' })
}
}
逐点拆解:
SugarTag 是一个 10 行左右的迷你胶囊标签:文字用 @Prop 传入,颜色由调用方指定,背景用"同色低透明度",边框用"同色中透明度"。@Prop 与 @State 的区别在这里体现得很清楚:@Prop 是单向传递的只读属性,父组件改值后子组件同步更新,但子组件内部改它不会反向影响父组件,适合这种"纯展示"型小组件。this.color + '1F' 这种透明度拼色的写法让标签永远与传入色保持同色系,不用额外计算浅色版本。
3.10 内容子组件:以 SugarPaintContent 为代表的列表页
@Component
struct SugarPaintContent {
@ObjectLink data: SugarData;
onAdd: () => void = () => {};
build() {
Scroll() {
Column() {
// 卡片一:本周糖画销量柱状图
Column() {
Row() {
Text('📈 本周糖画销量')
Text('周六88支')
}
Row() {
ForEach(WEEK_SOLD, (w: WeekSugarMeta) => {
Column() {
Text(w.value + '')
Column().width(18).height(w.value).backgroundColor(...)
Text(w.day)
}.layoutWeight(1)
}, (w) => w.day)
}
.height(150).alignItems(VerticalAlign.Bottom)
}
// 列表头:糖画陈列 + 新增按钮
Row() {
Text('🍭 糖画陈列')
Column().layoutWeight(1)
Text('点击➕制糖')
Text('➕').onClick(() => { this.onAdd(); })
}
// 列表项
ForEach(this.data.paints, (p: PaintItem, i: number) => {
Row() { /* 圆形图标 + 名称标签 + 价格 */ }
.backgroundColor(i % 2 === 0 ? COLORS.cardBg : COLORS.cardAlt)
}, (p, i) => 'pt' + p.name + i)
}
}
.scrollable(ScrollDirection.Vertical).scrollBar(BarState.Off)
}
}
逐点拆解:
SugarPaintContent 的结构是"统计卡片 + 列表"的复合体,拆开看有三层:
第一层是柱状图卡片。没有引入任何图表库,纯用 ForEach 生成 7 根柱子:每根柱子是一个 Column,内部自上而下是数值文字、矩形色块(宽度固定 18、高度等于销量值)、星期文字。整行 height(150) 加 alignItems(VerticalAlign.Bottom),让所有柱子底部对齐,形成天然坐标系。销量 ≥60 的柱子用强调色,其余用浅色,峰值一目了然——"周六 88 支"的数字和页头提示语互相印证。
第二层是列表头。标题 + 弹性占位 + 提示语 + 加号按钮,onClick 直接调用父组件传入的 this.onAdd() 回调。点击加号,弹窗由主组件打开,子组件只管"报告事件"。
第三层是列表项。每个糖画条目是"圆形色块(emoji + 匠级)| 名称与标签 | 价格"的三段式行布局。圆形色块的背景和描边都来自 getPaintColor(p.level),颜色深浅直接反映匠级高低;名称行下叠两个 SugarTag 展示形制与匠级。列表行背景用 i % 2 === 0 做斑马纹——偶数列白底、奇数列浅米底,缓解长列表的阅读疲劳。ForEach 的第三个参数是键值生成器,'pt' + p.name + i 保证每条数据有稳定身份,是列表复用与 diff 的基础。
3.11 其余内容子组件:同构下的差异点
剩余四个子组件与 SugarPaintContent 共享同一套骨架,但各有差异:
SugarRecipeContent 的头部卡片换成糖色占比的圆点图——四个色块圆点 + 百分比 + 名称,用 layoutWeight(1) 平分宽度;列表项是"圆形容器 + 配方名(含糖水比、删除按钮)+ 温度工艺行"。删除按钮调用 onDel(r.name) 上抛。
SugarColorContent 没有统计卡片,直接铺满 12 条糖色记录;每条是"色值圆片 + 名称 + 色温用途行"。这里出现了一个细节:色值文字的颜色会判断 c.hex 是否为深色(如 #3A2A1A、#4A2C0E、#6B3410),深色用主文字色、浅色用色值本身,保证深底浅字、浅底深字的可读性。
SugarOrderContent 与 SugarReviewContent 则是最纯粹的列表页:订单条目展示"emoji + 名称 + 地区数量金额",金额通过 getOrderColor 着色;客评条目展示"头像 + 昵称日期 + 星级 + 标签 + 评价内容",星级用 '⭐'.repeat(r.score) 生成——一行代码把数字评分转成视觉星标,是字符串方法在 UI 中的妙用。
3.12 完整数据流:从点击到重绘
把上面的代码串起来,一条完整的数据流闭环就清晰了。用一张时序图来收束本节:
这条闭环印证了本项目的核心设计:状态在顶层(主组件),数据在模型(数据类),展示在底层(内容组件),一切变更都通过状态驱动回流。理解了这个闭环,也就理解了 ArkUI 声明式开发的精髓。
四、写在最后
回顾整份代码,它用大约一千行 ArkTS 呈现了一个完整、可交互、有主题质感的非遗糖画应用。抛开题材的趣味性,单从工程角度,这份代码至少有五个值得吸收的经验:
其一,分层清晰。类型与常量、数据模型、工具函数、界面组件四个层次边界分明,每个文件的职责单一,这对任何规模的工程都是健康的地基。
其二,数据驱动渲染。从 @Observed 数据类到 @State 导航状态再到 @ObjectLink 子组件绑定,状态与 UI 之间是单向清晰的映射关系,改数据即改界面,不需要手动操作 DOM 或组件实例。
其三,约定优于配置的视觉体系。一套色板 + 一组取色映射函数,让"数值大小""文本语义"与"颜色深浅"建立可复用的映射,整份代码几乎没有散落的裸色值。
其四,回调上抛的通信模式。子组件通过回调把操作意图上抛给父组件,父组件集中管理弹窗等全局状态,既避免了状态分散,也让子组件保持纯粹。
其五,把动画当状态用。无论是糖葫芦的缩放、火焰的明暗,还是弹窗的淡入,都通过 @State 布尔值加 animation() 完成,声明式动画的优雅在这份代码里体现得淋漓尽致。
当然,代码里也有可以继续演进的地方:比如三个弹窗的重复布局可以考虑抽象成通用弹窗组件;取色函数里的魔法色值可以收敛进 COLORS;斑马纹列表可以封装成通用列表项。但瑕不掩瑜,作为一篇理解 ArkTS 声明式 UI 的教材,这份代码足够完整、足够自洽。
五、技术要点对比表
| 对比维度 | 类型/常量层 | 数据模型层 | 工具函数层 | 界面层 |
|---|---|---|---|---|
| 核心载体 | 接口 + enum + 常量数组 | @Observed 类 |
纯函数 | @Entry @Component 结构体 |
| 代表元素 | SugarPalette、SugarTab、TAB_LIST |
SugarData 五个数据数组 |
getPaintColor 等四个取色函数 |
SugarApp 及五个内容子组件 |
| 数据方向 | 静态定义 | 可被观察、可被修改 | 输入数据输出颜色 | 消费数据、驱动渲染 |
| 取色策略 | 色板统一管理 | — | 阈值分级 + 白名单分级 | 调用函数着色 |
| 通信方式 | 无 | @State 顶层持有 |
无状态 | @ObjectLink 绑定 + 回调上抛 |
| 典型技巧 | 透明度十六进制拼接 | 12 条数据等量填充 | 数值映射语义色 | 斑马纹、repeat() 生成星级、layoutWeight 弹性布局 |
| 易扩展点 | 加色、加页签 | 换真实接口数据 | 换主题色系 | 抽通用弹窗/列表组件 |
更多推荐



所有评论(0)