霓虹绿与电光蓝的视觉碰撞——HarmonyOS ArkUI 智慧健身平台多场景组件化设计实践
一、技术前言
1.1 行业背景:智慧健身的数字化重构
随着全民健康意识觉醒与可穿戴设备的普及,健身行业正经历从"线下门店运营"向"线上线下一体化数字平台"的深刻转型。一款现代化的健身管理应用,需要同时服务于三类核心用户群体:终端会员(需要浏览课程、预约教练、追踪数据、社交分享)、教练群体(需要展示专业标签、管理学员、维护个人品牌)以及运营方(需要数据看板、社区运营、目标管理)。这三类需求在同一个页面上共存,要求开发者具备极高的信息架构组织能力与视觉层次把控能力。
「律动空间」正是面向上述需求设计的一站式智慧健身管理平台。它将课程库、教练库、排期日历、运动数据中心、健身社区、个人目标管理六大模块整合在单一页面内,通过底部六 Tab 切换实现模块间的快速流转。与传统电商或资讯类应用不同,健身类应用对数据可视化(卡路里消耗、完成率、连续打卡天数)和状态色彩编码(已预约/待确认/已完成/已取消)有极高的依赖度,这直接影响着信息层的颜色系统设计。
1.2 ArkUI 框架核心特性
ArkUI(方舟 UI 框架)是华为为 HarmonyOS(鸿蒙操作系统)打造的一套声明式 UI 开发范式,其核心设计理念可以用三个关键词概括:声明式、状态驱动、跨设备。
声明式 UI 范式:与传统命令式 UI(如 Android 的 findViewById + setText)不同,ArkUI 采用类似于 SwiftUI 的声明式语法,开发者只需描述"界面应该是什么样子",框架自动负责 UI 的差异计算与更新。以本应用为例,Column() { Text('律动空间')... } 这样的代码描述了一个纵向容器内含文本的结构,开发者无需关心视图的创建与回收时机。
状态驱动刷新:ArkUI 的 @State、@Prop、@Link、@Observed、@ObjectLink 等装饰器构成了完整的状态管理体系。当被装饰的状态变量发生变化时,框架会自动触发依赖该状态的 UI 组件重新渲染。本应用中,@State currentTab: number = 0 的每一次变化都会驱动六个 Tab 内容区的条件渲染切换;@State breath: boolean 的 1 秒周期翻转则会驱动"快速开始"按钮的背景色与月度图表柱高产生呼吸动效。
组件化与 Builder:ArkUI 通过 @Component 装饰 struct 形成可复用组件,通过 @Builder 装饰方法形成轻量级 UI 构建函数。@Builder 不引入新的组件实例,开销极低,非常适合用于页面内的区块拆分。本应用的 headerSearch、tabCourse、tabTrainer 等十余个 @Builder 函数将一个 1048 行的大型页面拆分成高内聚低耦合的语义区块。
1.3 关键技术决策
回顾整个源码,可以提炼出以下几项关键技术决策:
第一,单页面多场景的 Tab 路由策略。该应用没有为每个功能模块创建独立页面,而是将六个场景统一收纳在一个 @Entry 入口组件内,通过 currentTab 数值索引切换 @Builder 函数。这种做法的优势在于状态共享简单(同一份 breath、同一份弹窗状态可被所有 Tab 复用)、页面跳转零开销、适合"信息总览型"应用;代价是单文件行数较多,需要靠注释分区与 @Builder 拆分保证可读性。
第二,深色赛博朋克视觉语言。背景采用 #0D1117(GitHub 深色主题同款底色),主强调色用 #39FF14(霓虹绿)与 #00D4FF(电光蓝),辅以 #FFD700(金色)做价格与星级、#FF3366(玫红)做点赞与删除。这种配色方案在健身类应用中形成鲜明的"运动燃脂"暗示——绿色代表完成与达标,蓝色代表预约与进行中,金色代表价格与价值,红色代表警告与热度,色彩语义高度自洽。
第三,状态-色彩映射函数化。将"已预约→蓝""待确认→金"这类业务语义到视觉语义的映射独立成 scheduleColor 与 postColor 两个工具函数,避免在每个 @Builder 内重复书写 if-else,提高了可维护性。
第四,呼吸动画的极简实现。没有使用 ArkUI 的 animateTo 或 animation 修饰器,而是直接用 setInterval 每 1000ms 翻转一个布尔值 breath,再让按钮背景色与柱状图高度对 breath 求值。这种做法虽不符合声明式动画的最佳实践,但胜在直观、可控、便于演示。






二、整体架构流程图
2.1 Mermaid 架构图
2.2 架构解读
从架构图可以清晰看到,整个应用呈现出经典的**“四层倒金字塔”**结构,自下而上依次为:色彩与常量层 → 工具函数层 → 数据模型层 → 组件主体层。
色彩与常量层是整个应用的"基因层",定义了视觉风格与基础数据。COLORS 常量一经定义便贯穿所有 Builder;TAB_LIST 决定了底部导航的结构;MONTH_IDX、WORKOUT_VAL 等数组为头部打卡热力图与月度趋势图表提供静态数据源。
工具函数层是业务语义到视觉语义的桥梁。scheduleColor 把"已预约/待确认/已完成/已取消"四种排期状态映射为四种颜色;postColor 把"打卡/饮食/问答/分享"四类帖子映射为四类颜色。这两个函数在 Tab Builder 内被反复调用,是保持色彩一致性的关键。
数据模型层由 6 个 @Observed 装饰的 class 组成,每个 class 对应一个业务实体。@Observed 让 class 实例具备可观测性——当其属性变化时,依赖它的 @ObjectLink 子组件会自动刷新。本应用虽未进一步拆分子组件使用 @ObjectLink,但 @Observed 的存在为后续功能演进(如把课程卡片拆成独立子组件)预留了扩展空间。
组件主体层是 Page801 这个 @Entry @Component,它持有所有 @State、定义生命周期、在 build() 内编排头部、Tab 区、图表、Tab 栏、弹窗五大区块。aboutToAppear 中启动呼吸定时器,aboutToDisappear 中清除定时器,遵循了"资源在 appear 时申请、在 disappear 时释放"的标准模式。
数据流向是单向的:常量/模型 → Builder → 渲染输出;用户的点击事件则反向流动:onClick → 修改 @State → 触发依赖该 State 的 Builder 重新执行 → 视觉更新。这种单向数据流保证了状态可预测性。
三、色彩体系设计
3.1 ColorPalette 接口
interface ColorPalette {
bg: string;
card: string;
chip: string;
title: string;
sub: string;
text3: string;
green: string;
greenD: string;
blue: string;
blueD: string;
red: string;
gold: string;
line: string;
tabOn: string;
mask: string;
}
这是色彩系统的"契约"。通过定义一个 interface 而不是直接使用字面量对象,带来了三重好处:
第一重好处是类型安全。TypeScript 编译器会在编译期检查 COLORS 常量是否完整覆盖 interface 中声明的所有字段,任何遗漏(比如忘记定义 mask)都会触发编译错误,避免了运行时 undefined 颜色导致的渲染异常。
第二重好处是命名约束。interface 把 15 种颜色按"语义角色"命名而非"视觉特征"命名——bg、card、chip、title、sub、text3 描述的是颜色的用途(背景、卡片、芯片标签、标题、副标题、三级文字),而不是 red/blue/green 这种色名。这种命名方式让 Builder 代码读起来像在设计文档:.fontColor(COLORS.title) 比 .fontColor(COLORS.e8f6ff) 更具语义价值。
第三重好处是可替换性。如果未来要做"主题切换"功能(日间/夜间/节日主题),只需替换 COLORS 常量的赋值即可,所有引用 COLORS.xxx 的地方自动跟随,无需修改任何 Builder。
3.2 COLORS 常量逐色分析
const COLORS: ColorPalette = {
bg: '#0D1117',
card: '#1A2332',
chip: '#1A3A2E',
title: '#E8F6FF',
sub: '#7AB0C8',
text3: '#4A6275',
green: '#39FF14',
greenD: '#22CC11',
blue: '#00D4FF',
blueD: '#0099CC',
red: '#FF3366',
gold: '#FFD700',
line: '#1E2D3D',
tabOn: '#39FF14',
mask: 'rgba(0,0,0,0.65)',
};
下面对照源码逐项解读这 15 个颜色字段的视觉意图与使用场景:
bg: '#0D1117'——应用底色。这是 GitHub 深色主题的标志性底色,RGB(13,17,23),几乎接近纯黑但略带蓝调。与纯黑#000000相比,它在 OLED 屏幕上能保留更多层次感,避免出现"洞穴式"的视觉塌陷。整个build()根Column用它做backgroundColor。card: '#1A2332'——卡片底色。RGB(26,35,50),比bg略亮一档,是所有课程卡、教练卡、目标卡等容器组件的统一底色,与bg形成约 5% 的明度差,正好能区分"前景容器"与"背景"。chip: '#1A3A2E'——芯片标签色。RGB(26,58,46) 是一种偏绿的深色,用于搜索框背景、教练头像圆形容器、标签底色。其绿色调与主色green形成同色系呼应,又不至于喧宾夺主。title: '#E8F6FF'——主标题色。RGB(232,246,255) 是一种略偏蓝的近白色,相比纯白#FFFFFF更柔和,长时间阅读不易疲劳。所有 18px Bold 的标题文字都使用此色。sub: '#7AB0C8'——副标题色。RGB(122,176,200) 是一种灰蓝色,用于 12-14px 的副标题、说明文字、教练专业方向等次级信息。text3: '#4A6275'——三级文字色。RGB(74,98,117) 更暗,用于最弱化的辅助文字:星期标签、时间戳、"近6个月"提示等。green: '#39FF14'——霓虹绿主色。RGB(57,255,20) 是整个应用最跳脱的颜色,饱和度极高,代表"完成"“达标”“快速开始”。它用作呼吸动画的亮态、Tab 选中色、进度条主色、kcal 数值色。greenD: '#22CC11'——深霓虹绿。RGB(34,204,17) 比主绿暗一档,用于呼吸动画的暗态、热力图"轻度打卡"等级、社区"分享"分类色。blue: '#00D4FF'——电光蓝主色。RGB(0,212,255) 与霓虹绿并列为主色,代表"预约"“进行中”“数据卡片偶数索引”。用于"我的排期"按钮描边、教练标签文字、数据渐变端点。blueD: '#0099CC'——深电光蓝。RGB(0,153,204),本应用中未直接使用,预留为渐变色阶或暗态备用。red: '#FF3366'——玫红警示色。RGB(255,51,102) 用于点赞数、删除按钮、"清除已完成"链接、已取消状态。它比纯红#FF0000更柔和现代,避免视觉攻击性。gold: '#FFD4FF'——实际为#FFD700标准金色。用于价格(¥59)、教练星级评分(4.9)、待确认状态色,传递"价值"与"待结算"双重语义。line: '#1E2D3D'——分割线色。RGB(30,45,61),用于 Divider、打卡热力图空格、进度条底色、时间轴连线。明度介于bg与card之间,形成微妙的层次。tabOn: '#39FF14'——Tab 选中色。与green同值,单独命名是为了语义清晰——它专指底部 Tab 栏选中态,未来若需独立调整可与green解耦。mask: 'rgba(0,0,0,0.65)'——遮罩色。65% 不透明度的黑色,用于弹窗背景遮罩,让下层内容模糊化但不完全遮蔽,符合移动端弹窗的视觉惯例。
整体而言,这套色彩系统的精妙之处在于"两主两辅一警示一价值"的语义分工:霓虹绿与电光蓝构成主色双核(分别象征"完成态"与"预约态"),其深色版本做呼吸态与渐变端点,玫红承担警示,金色承担价值,其余灰阶系颜色构建层次。这是一套高度自洽的行业级色彩语言。
四、Tab 元数据与辅助数据
4.1 Tab 元数据定义
interface TabMeta {
icon: string;
label: string;
}
const TAB_LIST: TabMeta[] = [
{ icon: '🏋️', label: '课程' },
{ icon: '💪', label: '教练' },
{ icon: '📅', label: '排期' },
{ icon: '📊', label: '数据' },
{ icon: '🔥', label: '社区' },
{ icon: '👤', label: '我的' },
];
TabMeta 接口定义了底部 Tab 的两项核心属性:icon(emoji 图标)与 label(中文文字)。这里使用 emoji 而非 IconFont 字体图标,是一个有趣的取舍——emoji 跨平台渲染一致、无需额外字体资源、色彩丰富,缺点是不同设备厂商的 emoji 风格可能略有差异。对于一个行业应用演示页面来说,emoji 的"零依赖"优势足以压倒其"风格不统一"的劣势。
TAB_LIST 数组按"业务流"顺序排列:课程(浏览库)→ 教练(了解人)→ 排期(订时间)→ 数据(看效果)→ 社区(找同伴)→ 我的(管目标),恰好对应了一名会员从"发现"到"行动"再到"复盘"再到"养成"的完整闭环。这种顺序设计不是随意的,而是基于"用户旅程地图"的精心编排。
值得注意的细节是,TabMeta 数组在 tabBar Builder 内通过 ForEach 渲染,且 keyGenerator 使用 tab.label(中文字符串)作为唯一键。这意味着如果两个 Tab 的 label 相同会导致渲染冲突——本应用 6 个 label 互不相同,故安全。
4.2 图表辅助数据
const MONTH_IDX: number[] = [0, 1, 2, 3, 4, 5];
const MONTH_NAME: string[] = ['03', '04', '05', '06', '07', '08'];
const WORKOUT_VAL: number[] = [42, 55, 68, 51, 73, 86];
这三组数组共同驱动月度锻炼趋势柱状图。MONTH_IDX 是索引序列,长度 6 对应近 6 个月;MONTH_NAME 是月份标签;WORKOUT_VAL 是每月锻炼次数,呈"3→4→5→6→7→8 月"递增趋势,4 月略回调,整体上升,最后达到 86 次峰值——这暗示用户坚持锻炼的成长曲线,非常符合健身应用"持续进步"的产品价值观。
柱状图用 ForEach(MONTH_IDX, (i: number) => ...) 遍历,用 i 作为索引同时取 WORKOUT_VAL[i] 与 MONTH_NAME[i]。这种"用索引数组驱动多数据数组"的写法在 ArkUI 1.x 阶段比较常见,本质上等价于 zip(WORKOUT_VAL, MONTH_NAME),优点是数据解耦、可独立修改。
4.3 头部打卡热力图数据
const HEAT_DAYS: number[] = [1, 0, 2, 1, 3, 2, 0];
const WEEK_NAMES: string[] = ['一', '二', '三', '四', '五', '六', '日'];
HEAT_DAYS 是本周 7 天的打卡强度,取值 0/1/2/3,分别对应四种颜色:0→line(未打卡)、1→greenD(轻度)、2→green(中度)、3→blue(高强度)。本例中周一=1、周二=0、周三=2、周四=1、周五=3、周六=2、周日=0,描绘了一位"周中规律、周末波动"的健身爱好者作息模式。
WEEK_NAMES 用单字中文"一二三四五六日"做列标签,节省横向空间,是移动端紧凑布局的常见手法。
五、工具函数
function scheduleColor(s: string): string {
if (s === '已预约') return COLORS.blue;
if (s === '待确认') return COLORS.gold;
if (s === '已完成') return COLORS.green;
if (s === '已取消') return COLORS.red;
return COLORS.text3;
}
function postColor(c: string): string {
if (c === '打卡') return COLORS.green;
if (c === '饮食') return COLORS.blue;
if (c === '问答') return COLORS.gold;
if (c === '分享') return COLORS.greenD;
return COLORS.text3;
}
这两个函数是业务语义到视觉语义的映射器,体现了"函数式封装"的设计思想。
5.1 scheduleColor:排期状态映射
scheduleColor 接收一个状态字符串,返回对应的颜色字符串。其映射逻辑是:
- 已预约 → 电光蓝
#00D4FF:表示"已锁定但未发生",是进行中的冷色态。 - 待确认 → 金色
#FFD700:表示"需关注、待处理",金色天然具有"提醒"含义。 - 已完成 → 霓虹绿
#39FF14:表示"达成",绿色是健身应用中最具正向情绪的颜色。 - 已取消 → 玫红
#FF3366:表示"失败/终止",警示色。 - 其他 → 三级灰
text3:兜底分支,保证健壮性。
这个函数在 tabSchedule Builder 内被两处调用:一是时间轴节点的 Circle 填充色,二是排期状态文字的 fontColor。两处复用同一函数确保了节点圆点与状态文字颜色完全一致——这是视觉一致性的关键保障。
5.2 postColor:帖子分类映射
postColor 接收帖子分类字符串,返回对应颜色:
- 打卡 → 霓虹绿:与"完成"语义对齐,打卡即完成。
- 饮食 → 电光蓝:饮食类内容偏向"指导/资讯",用冷色。
- 问答 → 金色:提问需要被解答,用提醒色。
- 分享 → 深霓虹绿:与"打卡"近色但更暗,区分同源不同态。
- 其他 → 灰色兜底。
这个函数在 tabCommunity 中被用于左侧 4px 色条与分类标签文字两处,形成"色条-标签"双重视觉锚点,用户扫一眼即可定位感兴趣的帖子类型。
两个函数共同的设计哲学是:把易变的业务映射逻辑从 UI 代码中剥离。未来若产品决定"待确认"改成橙色,只需修改 scheduleColor 一行,无需在所有 Builder 中检索替换——这就是函数式封装带来的可维护性红利。
六、数据模型层
本应用定义了 6 个 @Observed 装饰的 class,每个对应一个业务实体。@Observed 装饰器让 class 实例的属性变化可被框架观测,是 ArkUI 二阶状态管理的基础。
6.1 CourseItem 课程模型
@Observed export class CourseItem {
name: string;
coach: string;
duration: string;
level: string;
calories: number;
price: number;
constructor(name: string, coach: string, duration: string, level: string, calories: number, price: number) {
this.name = name; this.coach = coach; this.duration = duration;
this.level = level; this.calories = calories; this.price = price;
}
}
CourseItem 共 6 个字段:课程名、教练名、时长、难度等级、消耗卡路里、价格。字段类型混合了 string 与 number——duration 用字符串 '45min' 直接携带单位便于展示,calories 与 price 用 number 便于后续计算或排序。constructor 把所有字段在构造时赋值,形成不可变值对象语义(虽然 TS 不强制 immutable,但使用模式接近)。
源码紧接着定义了 COURSE_LIST(6 项完整列表)与 COURSE_LEFT/COURSE_RIGHT(左右两列各 3 项的切片)。把列表拆成左右两列是"双列卡片瀑布流"布局的核心技巧,比在 Builder 内用 i % 2 分组更直观可控:
const COURSE_LIST: CourseItem[] = [
new CourseItem('HIIT燃脂', '王猛', '45min', '高级', 480, 59),
new CourseItem('瑜伽拉伸', '林静', '60min', '初级', 220, 39),
// ...
];
const COURSE_LEFT: CourseItem[] = [COURSE_LIST[0], COURSE_LIST[2], COURSE_LIST[4]];
const COURSE_RIGHT: CourseItem[] = [COURSE_LIST[1], COURSE_LIST[3], COURSE_LIST[5]];
值得注意的课程数据设计:课程难度覆盖初/中/高三档(瑜伽=初级、普拉提=中级、HIIT=高级),时长从 40min 到 60min,卡路里从 220 到 520,价格从 39 到 59——这是一组覆盖典型健身课程生态的"语义均衡"数据,便于演示页面的视觉差异。
6.2 TrainerItem 教练模型
@Observed export class TrainerItem {
name: string;
specialty: string;
rating: number;
students: number;
tags: string;
avatar: string;
// constructor 省略
}
TrainerItem 同样 6 字段,但语义与 CourseItem 完全不同:教练名、专业方向、评分、学员数、技能标签、头像。rating 用 number(4.9/4.8/4.7/4.9/4.8/4.6)便于数值比较与排序;students 用 number 便于千分位格式化;tags 是 '燃脂|力量' 这种管道分隔字符串,UI 层直接渲染而不解析;avatar 用 emoji '🏋️' 而非图片 URL,再次体现"零资源依赖"的设计取舍。
教练列表 6 项覆盖 HIIT、瑜伽、搏击、单车、普拉提、交叉训练六大主流健身方向,每个教练的 rating 都在 4.6-4.9 之间,符合"明星教练"的定位——这是产品语境对数据建模的指导。
6.3 ScheduleItem 排期模型
@Observed export class ScheduleItem {
course: string;
coach: string;
time: string;
status: string;
location: string;
}
5 字段:课程名、教练、时间、状态、场地。time 用 '08-28 09:00' 这种月-日 时:分格式,直接展示无需格式化;status 是 scheduleColor 的入参;location 用 'A厅' 'B厅' 简化场地编码。排期数据 6 项,状态分布为"已预约×2、待确认×1、已完成×2、已取消×1",正好覆盖四种颜色映射,是典型的"演示样本"。
6.4 StatItem 数据模型
@Observed export class StatItem {
label: string;
value: string;
unit: string;
trend: string;
}
4 字段:标签、数值、单位、趋势。值得玩味的是 value 用 string 而非 number——这是因为 '24,580' 这种带千分位逗号的展示形态在 number 类型下需要额外格式化,直接用字符串存储"显示态"值,把格式化责任前置到数据层,UI 层零处理。trend 字段 '+12%' '+3天' 也是"显示态"字符串。这种设计牺牲了数值运算能力,换取了渲染零成本——对于演示型应用是合理取舍。
STAT_LIST 4 项分别对应总锻炼次数、卡路里、连续打卡、完成率,覆盖健身用户最关心的四个核心指标。同样被切分为 STAT_LEFT(第 0、2 项)与 STAT_RIGHT(第 1、3 项)两列。
6.5 PostItem 帖子模型
@Observed export class PostItem {
author: string;
avatar: string;
content: string;
likes: number;
comments: number;
category: string;
}
6 字段:作者、头像、内容、点赞数、评论数、分类。likes/comments 用 number 是因为后续要用 toString() 渲染;category 是 postColor 的入参。帖子数据 6 项覆盖打卡、饮食、问答、分享四类,每类至少一条,内容均为健身场景的真实文案(“5公里PB达成”“深蹲膝盖疼”),让演示页面具备"产品 demo 级"的真实感。
6.6 GoalItem 目标模型
@Observed export class GoalItem {
title: string;
current: number;
target: number;
unit: string;
streak: number;
}
5 字段:目标标题、当前值、目标值、单位、连续天数。这是唯一一个"双数值字段"模型(current/target),用于在 UI 层计算进度百分比。streak(连续天数)是健身应用特有的激励字段——它把"坚持"量化,是行为养成类产品的核心数据。目标数据 5 项覆盖次数、卡路里、体脂率、里程、睡眠,全面反映健身用户的复合目标体系。
值得特别说明的是体脂率目标:new GoalItem('体脂率', 17.5, 15, '%', 30),current(17.5) > target(15),表示"减脂目标"——当前体脂高于目标,需要下降。这种"反向目标"在 UI 层会触发一个特殊处理:
value: item.current > item.target ? (item.target / item.current * 100) : (item.current / item.target * 100),
即当 current 超过 target 时,进度按 target/current 计算,避免出现 >100% 的视觉异常。这是数据模型与 UI 逻辑协同设计的典型案例。
七、组件主体结构
7.1 @State 变量群
@Entry
@Component
struct Page801 {
@State currentTab: number = 0;
@State addModal: boolean = false;
@State editModal: boolean = false;
@State delModal: boolean = false;
@State editIdx: number = -1;
@State delIdx: number = -1;
@State breath: boolean = false;
timer: number = -1;
@State courseList: CourseItem[] = COURSE_LIST;
@State trainerList: TrainerItem[] = TRAINER_LIST;
@State scheduleList: ScheduleItem[] = SCHEDULE_LIST;
@State statList: StatItem[] = STAT_LIST;
@State postList: PostItem[] = POST_LIST;
@State goalList: GoalItem[] = GOAL_LIST;
}
Page801 是整个页面的入口组件,由 @Entry @Component 双装饰器声明。@Entry 标记它是路由入口,@Component 声明它是一个可复用组件。
@State 变量群可分为四组:
第一组:UI 路由与弹窗状态——currentTab(当前 Tab 索引,0-5)、addModal/editModal/delModal(三个弹窗开关)、editIdx/delIdx(编辑/删除索引,-1 表示未选中)。这 6 个变量构成了交互层的全部状态。
第二组:动画状态——breath 布尔值,每秒翻转一次,驱动呼吸动效。
第三组:定时器句柄——timer: number = -1,注意它没有 @State 装饰。这是因为 timer 只是 setInterval 返回的 ID,不参与 UI 渲染,无需被框架观测。把它声明为普通成员变量而非 @State 是性能优化的细节——避免无意义的状态观测开销。
第四组:业务数据列表——6 个 @State 数组分别承载 6 类数据。把它们声明为 @State 让数组本身可被观测:未来如果实现 this.courseList.push(...) 添加新课程,UI 会自动刷新。当前演示中数据是静态的,但这种声明为未来扩展留下了空间。
7.2 生命周期
aboutToAppear() {
this.timer = setInterval(() => {
this.breath = !this.breath;
}, 1000);
}
aboutToDisappear() {
clearInterval(this.timer);
}
ArkUI 组件有三个核心生命周期:aboutToAppear(创建后、build 前)、build(每次状态变化触发)、aboutToDisappear(销毁前)。
本应用在 aboutToAppear 启动了一个 1000ms 周期的 setInterval,回调中翻转 breath 的布尔值。由于 breath 是 @State,每次翻转都会触发依赖它的 UI 重新渲染——具体来说,"快速开始"按钮的背景色会在 COLORS.green 与 COLORS.greenD 之间切换,月度图表的柱高会在 WORKOUT_VAL[i] * 0.8 与 * 0.6 之间切换,形成"呼吸"动效。
aboutToDisappear 中 clearInterval(this.timer) 是必须的清理动作——如果不清理,组件销毁后定时器仍在运行,会尝试修改已销毁组件的 breath,可能引发内存泄漏或异常。这是 HarmonyOS 应用开发的标准范式。
值得讨论的是:用 setInterval 实现动画在 ArkUI 中并非最佳实践。ArkUI 提供了 animateTo 与 animation 修饰器来实现声明式动画,性能更好、更流畅。但本应用选择 setInterval 方案有其教学价值——它把"状态-渲染"的因果关系展示得极为直观:状态改变 → UI 重渲染,没有任何"魔法"。
7.3 build() 根布局
build() {
Column() {
this.headerSearch()
Divider().strokeWidth(1).color(COLORS.line)
Scroll() {
Column() {
if (this.currentTab === 0) { this.tabCourse() }
else if (this.currentTab === 1) { this.tabTrainer() }
else if (this.currentTab === 2) { this.tabSchedule() }
else if (this.currentTab === 3) { this.tabData() }
else if (this.currentTab === 4) { this.tabCommunity() }
else if (this.currentTab === 5) { this.tabMine() }
this.chartCard()
}
}
.scrollBar(BarState.Off)
.layoutWeight(1)
this.tabBar()
if (this.addModal) { this.panelAdd(() => { this.addModal = false }) }
if (this.editModal) { this.panelEdit(() => { this.editModal = false }) }
if (this.delModal) { this.panelDel(() => { this.delModal = false }) }
}
.backgroundColor(COLORS.bg)
.height('100%')
}
build() 是组件的渲染入口,返回一个根组件树。本应用的根是 Column,纵向排列五大区块:
headerSearch()——头部区,含 logo、搜索栏、热力图、快捷按钮。Divider——1px 分割线,颜色line,把头部与主内容区视觉分隔。Scroll() { Column() {...} }——可滚动主内容区,.layoutWeight(1)让它占据剩余高度,.scrollBar(BarState.Off)隐藏滚动条保持视觉纯净。内部Column根据currentTab条件渲染六个 Tab 之一,末尾追加chartCard()月度图表——注意图表始终出现在所有 Tab 之下,是"通用尾部"。tabBar()——底部 Tab 栏,固定 56px 高。- 三个弹窗条件渲染——
if (this.addModal)等三处。弹窗通过@Builder函数 + 回调闭包实现:传入() => { this.addModal = false }作为onClose回调,弹窗内部"取消"与"确认"按钮都会调用它关闭弹窗。这种"状态驱动 + 回调关闭"的模式是 ArkUI 弹窗的标准实现。
整个根布局的精妙之处在于"固定头 + 滚动中 + 固定底 + 浮动弹窗"四层叠加:头部和底部固定不动保证品牌识别与导航稳定,中间内容随 Tab 切换滚动,弹窗浮在最顶层覆盖一切。这是移动端行业应用的经典布局范式。
八、头部区域详解
头部 headerSearch Builder 是用户进入页面后看到的第一屏,承载了品牌识别、搜索入口、状态总览、快捷操作四重职能。
8.1 品牌行
Row() {
Text('律动空间')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.title)
Text('🔥').fontSize(20).margin({ left: 4 })
Blank()
Text('🔔').fontSize(20)
}
.width('100%')
.padding({ left: 16, right: 16, top: 12, bottom: 8 })
这是一个三段式品牌行:左侧"律动空间"22px Bold 主标题 + 火焰 emoji 副标记,右侧🔔通知铃铛,中间用 Blank() 占位把两端推开。Blank() 是 ArkUI 特有的"弹性占位"组件,相当于 Flex 布局中的 flex: 1 空白,是两端对齐布局的最简洁写法。padding 用 16/12/8 三档值,体现"上紧下松"的呼吸节奏。
8.2 搜索栏
Row() {
Text('🔍').fontSize(16).margin({ left: 12, right: 6 })
TextInput({ placeholder: '搜索课程、教练...' })
.fontSize(14)
.fontColor(COLORS.title)
.placeholderColor(COLORS.text3)
.backgroundColor(Color.Transparent)
.layoutWeight(1)
.height(36)
}
.width('92%')
.height(40)
.backgroundColor(COLORS.chip)
.borderRadius(20)
.margin({ bottom: 10 })
搜索栏是一个圆角胶囊形容器(borderRadius(20) 配 height(40)),背景用 chip 深绿色。内部左侧放大镜 emoji,右侧是 TextInput。TextInput 设置 backgroundColor(Color.Transparent) 让它融入外层 chip 背景,layoutWeight(1) 撑满剩余宽度。placeholder 文案"搜索课程、教练…"明确告知可搜索的两大对象,是 UX 文案的精细化设计。
8.3 本周打卡热力图
Row() {
Text('本周打卡').fontSize(13).fontColor(COLORS.sub)
Blank()
ForEach(HEAT_DAYS, (v: number, i: number) => {
Column() {
Column()
.width(24).height(24).borderRadius(5)
.backgroundColor(v === 0 ? COLORS.line : v === 1 ? COLORS.greenD : v === 2 ? COLORS.green : COLORS.blue)
Text(WEEK_NAMES[i])
.fontSize(10).fontColor(COLORS.text3).margin({ top: 3 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}, (v: number, i: number) => i.toString())
}
.width('92%').margin({ bottom: 10 })
这是一个迷你版 GitHub 贡献热力图:左侧"本周打卡"标签,右侧 7 个 24×24 圆角方块,每个方块下方对齐星期单字。颜色映射用嵌套三元表达式 v === 0 ? line : v === 1 ? greenD : v === 2 ? green : blue,四档颜色形成"未打卡→轻度→中度→高强度"的递进色阶。
ForEach 的 keyGenerator 用 i.toString() 即索引字符串,保证 7 个元素唯一。每个 Column 用 .layoutWeight(1) 均分宽度,.alignItems(HorizontalAlign.Center) 让色块与星期文字水平居中对齐。这是一段紧凑而精致的可视化代码——用 20 行实现了完整的周打卡热力图。
8.4 快捷按钮组
Row() {
Button('快速开始')
.fontSize(15).fontColor(COLORS.bg)
.backgroundColor(this.breath ? COLORS.green : COLORS.greenD)
.borderRadius(20).height(40).layoutWeight(1)
.onClick(() => { this.currentTab = 0 })
Button('我的排期')
.fontSize(15).fontColor(COLORS.blue)
.backgroundColor(Color.Transparent)
.borderWidth(1).borderColor(COLORS.blue)
.borderRadius(20).height(40).layoutWeight(1)
.margin({ left: 10 })
.onClick(() => { this.currentTab = 2 })
}
.width('92%').margin({ bottom: 8 })
两个等宽按钮(layoutWeight(1))形成主次对比:
- "快速开始"实色按钮:背景色随
breath在green与greenD间切换,形成呼吸光效;文字用COLORS.bg(深底色)在亮绿背景上保证对比度。点击切换到 Tab 0(课程)。 - "我的排期"描边按钮:透明背景 + 1px 电光蓝描边,文字也是电光蓝。视觉上是"次按钮"。点击切换到 Tab 2(排期)。
两个按钮的视觉权重对比鲜明:实色亮绿 > 描边电光蓝,引导用户优先点击"快速开始"——这是产品运营层"鼓励开始锻炼"的意图在 UI 上的体现。
九、Tab 0:课程——双列卡片瀑布流
9.1 课程卡片复用单元
@Builder courseCard(item: CourseItem) {
Column() {
Row() {
Text(item.level === '高级' ? '🔥' : item.level === '中级' ? '💪' : '🌱').fontSize(32)
}
.width('100%').height(72)
.justifyContent(FlexAlign.Center)
.backgroundColor(COLORS.chip)
.borderRadius({ topLeft: 12, topRight: 12 })
Column() {
Text(item.name)
.fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
Text(item.coach + ' · ' + item.duration)
.fontSize(11).fontColor(COLORS.sub)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 2 })
Row() {
Text(item.calories + 'kcal').fontSize(11).fontColor(COLORS.green)
Blank()
Text('¥' + item.price)
.fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.gold)
}
.width('100%').margin({ top: 6 })
}
.width('100%').padding(10)
}
.backgroundColor(COLORS.card)
.borderRadius(12)
.margin({ bottom: 10 })
.onClick(() => { this.editModal = true })
}
courseCard 是一个独立 @Builder 函数,接收 CourseItem 参数,是课程 Tab 的复用单元。其结构分三段:
顶部 72px 图标区:背景 chip 深绿,仅顶部两角圆角(topLeft: 12, topRight: 12)形成"上半圆角下半直角"的视觉。内部根据 level 三元映射 emoji:高级→🔥(火焰象征高强度)、中级→💪(肌肉象征力量)、初级→🌱(嫩芽象征成长)。这是一组极其贴合健身语境的视觉隐喻。
中部信息区:课程名 14px Bold + 教练·时长副标题 11px。两个 Text 都设了 maxLines(1) 与 textOverflow(Ellipsis),保证长文本单行省略——这是卡片瀑布流防止高度错位的必备处理。
底部价格行:左侧 kcal 用霓虹绿(呼应"燃脂"语义),右侧价格用金色(呼应"价值"语义),中间 Blank() 推开。两个颜色形成"消耗 vs 付出"的视觉对照。
整个卡片 12px 圆角、card 底色、10px 内边距、10px 底外边距,点击触发 editModal = true(编辑弹窗)。这是"浏览-编辑"交互闭环的入口。
9.2 tabCourse 主结构
@Builder tabCourse() {
Column() {
Text('精品课程').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
.margin({ left: 16, top: 12, bottom: 8 })
Row() {
Column() {
ForEach(COURSE_LEFT, (item: CourseItem) => { this.courseCard(item) }, (item: CourseItem) => item.name)
}.layoutWeight(1).margin({ right: 5 })
Column() {
ForEach(COURSE_RIGHT, (item: CourseItem) => { this.courseCard(item) }, (item: CourseItem) => item.name)
}.layoutWeight(1).margin({ left: 5 })
}
.width('100%').padding({ left: 12, right: 12 })
Row() {
Button('新增课程')
.fontSize(13).fontColor(COLORS.bg).backgroundColor(COLORS.green)
.borderRadius(20).height(36)
.onClick(() => { this.addModal = true })
Blank()
Text('查看全部 >').fontSize(12).fontColor(COLORS.sub)
}
.width('92%').margin({ top: 4, bottom: 12 })
}
}
Tab 0 主结构是经典的双列瀑布流:两个等宽 Column(layoutWeight(1)),各 ForEach 渲染 3 个 courseCard,列间间距通过 margin({ right: 5 }) 与 margin({ left: 5 }) 合计 10px 形成。ForEach 的 keyGenerator 用 item.name 课程名作唯一键。
顶部"精品课程"标题与底部"新增课程"+"查看全部 >"操作行构成"标题-列表-操作"三段式。新增按钮触发 addModal,是"新增"交互闭环的入口。整个 Tab 设计紧凑,信息密度高,是健身应用"课程商城"的标准形态。
十、Tab 1:教练——人物大卡列表
@Builder tabTrainer() {
Column() {
Text('明星教练').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
.margin({ left: 16, top: 12, bottom: 10 })
ForEach(this.trainerList, (item: TrainerItem, idx: number) => {
Row() {
Column() {
Text(item.avatar).fontSize(40)
}
.width(72).height(72)
.backgroundColor(COLORS.chip).borderRadius(36)
.justifyContent(FlexAlign.Center)
Column() {
Text(item.name).fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text(item.specialty).fontSize(13).fontColor(COLORS.sub).margin({ top: 3 })
Row() {
Text('⭐').fontSize(12)
Text(item.rating.toString()).fontSize(13).fontColor(COLORS.gold).margin({ left: 2 })
Text(item.students + '学员').fontSize(12).fontColor(COLORS.text3).margin({ left: 10 })
}.margin({ top: 4 })
Text(item.tags).fontSize(10).fontColor(COLORS.blue)
.backgroundColor(COLORS.chip).borderRadius(8)
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.margin({ top: 4 })
}
.margin({ left: 14 })
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column() {
Text('预约').fontSize(12).fontColor(COLORS.bg)
.backgroundColor(COLORS.green).borderRadius(14)
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
}
}
.width('92%').padding(14)
.backgroundColor(COLORS.card).borderRadius(16)
.margin({ bottom: 10 })
.onClick(() => { this.editIdx = idx; this.editModal = true })
}, (item: TrainerItem) => item.name)
}
.padding({ bottom: 12 })
}
Tab 1 是单列大卡布局,每张卡承载一名教练的完整信息。卡片结构是"左 72×72 圆形头像 + 中部信息区 + 右侧预约按钮"的三段式横向布局:
左侧圆形头像:72×72 的 Column,背景 chip,borderRadius(36)(直径一半)形成正圆,内部居中放 40px emoji。圆形是人物头像的视觉惯例,强化"人"的语义。
中部信息区:纵向排列"姓名(18px Bold)→ 专业(13px sub)→ 星级+学员数(一行三段)→ 标签胶囊"。其中星级用 ⭐ emoji + 金色数字 4.9,学员数用灰色 1280 学员,标签胶囊用 10px 蓝字 + chip 背景 + 8px 圆角。这一区信息密度极高但层次分明:粗体姓名抓住眼球,副色专业说明,灰色学员数辅助,蓝色标签做分类。
右侧预约按钮:12px 深底字 + 霓虹绿背景 + 14px 圆角胶囊。颜色对比强烈,是明显的"行动召唤"按钮。
整个卡片 16px 圆角比课程卡的 12px 更大,是因为大卡需要更柔和的视觉;padding 14 比 10 更宽,给信息更多呼吸。点击触发 editIdx = idx 与 editModal = true,记录被点击的教练索引供编辑弹窗使用。
十一、Tab 2:排期——状态时间轴
@Builder tabSchedule() {
Column() {
Text('课程排期').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
.margin({ left: 16, top: 12, bottom: 10 })
Column() {
ForEach(this.scheduleList, (item: ScheduleItem, idx: number) => {
Row() {
Column() {
Row() {
Column() {
Circle({ width: 12, height: 12 })
.fill(scheduleColor(item.status))
.stroke(COLORS.card)
.strokeWidth(2)
}
}.width(20)
Column() {
if (idx < this.scheduleList.length - 1) {
Column()
.width(2).layoutWeight(1).backgroundColor(COLORS.line)
}
}.width(20).layoutWeight(1)
}
.width(20).height(72).margin({ right: 10 })
Column() {
Row() {
Text(item.course).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text(item.status).fontSize(11).fontColor(scheduleColor(item.status)).fontWeight(FontWeight.Bold)
}.width('100%')
Row() {
Text('👨🏫 ' + item.coach).fontSize(12).fontColor(COLORS.sub)
Text('📍 ' + item.location).fontSize(12).fontColor(COLORS.sub).margin({ left: 10 })
}.width('100%').margin({ top: 4 })
Text('🕐 ' + item.time).fontSize(12).fontColor(COLORS.text3).margin({ top: 2 })
}
.layoutWeight(1).height('100%').justifyContent(FlexAlign.Center)
}
.width('92%').height(72).margin({ bottom: 6 })
.onClick(() => { this.delIdx = idx; this.delModal = true })
}, (item: ScheduleItem) => item.course)
}
.width('92%').padding({ left: 12, right: 12, top: 8, bottom: 8 })
.backgroundColor(COLORS.card).borderRadius(12)
}
.padding({ bottom: 12 })
}
Tab 2 是最复杂的布局——状态时间轴。整个排期列表外层包了一个 card 背景的大容器,内部通过 ForEach 渲染 6 行,每行 72px 高,结构是"左侧时间轴节点+连线 + 右侧排期信息"。
时间轴左侧列(20px 宽):上半部分是 Circle(12×12),填充色由 scheduleColor(item.status) 决定,描边用 card 色 2px——让圆点从连线中"凸出"形成节点感。下半部分是 2px 宽的连线,用 line 色,条件渲染:if (idx < length - 1) 表示最后一项不画连线,形成时间轴的"终止"视觉。layoutWeight(1) 让连线撑满剩余高度。
右侧信息列:三行内容——课程名+状态色标(同行)、教练+场地(同行)、时间戳。状态文字的 fontColor 复用 scheduleColor,与圆点同色,形成"圆点-状态文字"的视觉呼应。教练前缀👨🏫、场地前缀📍、时间前缀🕐 用 emoji 替代 IconFont,与前述设计哲学一致。
点击触发 delIdx = idx 与 delModal = true——排期项的点击行为是"删除"而非"编辑",这与课程/教练 Tab 不同,体现了排期"已发生即归档、未发生可取消"的业务特性。
整个时间轴用 72px 固定行高 + 6px 底外边距形成稳定节奏,是数据列表的精细化处理。
十二、Tab 3:数据——渐变大数字卡
12.1 statCard 复用单元
@Builder statCard(item: StatItem, idx: number) {
Column() {
Column()
.width('100%').height(3).borderRadius(2)
.linearGradient({
direction: GradientDirection.Right,
colors: [[idx % 2 === 0 ? COLORS.green : COLORS.blue, 0], [idx % 2 === 0 ? COLORS.blue : COLORS.green, 1]]
})
Text(item.label).fontSize(12).fontColor(COLORS.sub).margin({ top: 8 })
Row() {
Text(item.value).fontSize(40).fontWeight(FontWeight.Bold)
.fontColor(idx % 2 === 0 ? COLORS.green : COLORS.blue)
Text(item.unit).fontSize(14).fontColor(COLORS.text3)
.margin({ left: 4, bottom: 6 })
}
.alignItems(VerticalAlign.Bottom).margin({ top: 4 })
Text(item.trend).fontSize(12).fontColor(COLORS.green).margin({ top: 4 })
}
.width('100%').padding(14)
.backgroundColor(COLORS.card).borderRadius(12)
.margin({ bottom: 10 })
}
statCard 是 Tab 3 的复用单元,接收 StatItem 与 idx 双参数。它的视觉特色是顶部 3px 高的渐变色条 + 40px 巨大数字。
顶部渐变条:linearGradient 从左到右,colors 数组定义两段——idx % 2 === 0 时是 [green, blue],否则 [blue, green]。这种"奇偶交替渐变方向"让左右两列卡片形成镜像对称的视觉节奏。
大数字行:item.value 40px Bold,颜色同样按 idx % 2 在 green 与 blue 间切换。单位 14px 灰色,与底部对齐(alignItems(VerticalAlign.Bottom) + bottom: 6 微调),形成"大数字 + 小单位"的财务报表式视觉。
趋势行:item.trend(如 +12%)用霓虹绿,传递正向增长信号。
12.2 tabData 主结构
@Builder tabData() {
Column() {
Text('运动数据').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
.margin({ left: 16, top: 12, bottom: 10 })
Row() {
Column() {
ForEach(STAT_LEFT, (item: StatItem, idx: number) => { this.statCard(item, idx) }, (item: StatItem) => item.label)
}.layoutWeight(1).margin({ right: 5 })
Column() {
ForEach(STAT_RIGHT, (item: StatItem, idx: number) => { this.statCard(item, idx) }, (item: StatItem) => item.label)
}.layoutWeight(1).margin({ left: 5 })
}
.width('100%').padding({ left: 12, right: 12 })
}
.padding({ bottom: 12 })
}
主结构与课程 Tab 类似的双列布局,但每张卡内部从"图文卡"变为"大数字卡"。左侧 STAT_LEFT(总锻炼/连续打卡),右侧 STAT_RIGHT(卡路里/完成率),每列 2 张卡共 4 张。这是数据看板型应用的典型形态。
十三、Tab 4:社区——左色条列表
@Builder tabCommunity() {
Column() {
Text('健身社区').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
.margin({ left: 16, top: 12, bottom: 10 })
ForEach(this.postList, (item: PostItem, idx: number) => {
Row() {
Column()
.width(4).height('100%')
.backgroundColor(postColor(item.category))
.borderRadius({ topLeft: 4, bottomLeft: 4 })
Column() {
Row() {
Column() {
Text(item.avatar).fontSize(28)
}
.width(40).height(40)
.backgroundColor(COLORS.chip).borderRadius(20)
.justifyContent(FlexAlign.Center)
Column() {
Text(item.author).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text(item.category).fontSize(11).fontColor(postColor(item.category)).margin({ top: 2 })
}
.margin({ left: 8 }).alignItems(HorizontalAlign.Start)
Blank()
Text('👍').fontSize(14)
Text(item.likes.toString()).fontSize(12).fontColor(COLORS.red).margin({ left: 4 })
Text('💬').fontSize(14).margin({ left: 10 })
Text(item.comments.toString()).fontSize(12).fontColor(COLORS.sub).margin({ left: 4 })
}
.width('100%')
Text(item.content)
.fontSize(14).fontColor(COLORS.title)
.maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 8 })
}
.layoutWeight(1)
.padding({ left: 12, right: 12, top: 10, bottom: 10 })
}
.width('92%').backgroundColor(COLORS.card).borderRadius(12)
.margin({ bottom: 8 })
}, (item: PostItem) => item.content)
Row() {
Button('发帖分享')
.fontSize(13).fontColor(COLORS.bg).backgroundColor(COLORS.green)
.borderRadius(20).height(36)
.onClick(() => { this.addModal = true })
}
.width('100%').justifyContent(FlexAlign.Center)
.margin({ top: 4, bottom: 12 })
}
}
Tab 4 是社区帖子列表,每条帖子的卡片左侧有 4px 宽的彩色色条——这是"左色条列表"布局命名的由来。色条颜色由 postColor(item.category) 决定,与卡片内部分类标签同色,形成"色条-标签"双重锚点。
卡片结构是"色条 + 内容区"横向布局,内容区分两段:
头部行:圆形 emoji 头像(40×40)+ 作者名+分类标签 + 右侧点赞数(玫红)与评论数(副灰)。点赞用👍、评论用💬,数字分别用 red 与 sub 颜色区分,红色点赞暗示"热度"。
正文行:14px title 色,maxLines(2) 限制两行,超出省略。这是社区流式信息列表的标准截断处理。
底部"发帖分享"按钮居中(justifyContent(FlexAlign.Center)),触发 addModal。整个 Tab 设计紧凑、信息层级清晰,是典型的内容社区形态。
十四、Tab 5:我的——进度条清单
@Builder tabMine() {
Column() {
Text('我的目标').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
.margin({ left: 16, top: 12, bottom: 10 })
ForEach(this.goalList, (item: GoalItem, idx: number) => {
Column() {
Row() {
Text(item.title).fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text(item.current + '/' + item.target + ' ' + item.unit)
.fontSize(13).fontColor(COLORS.sub)
}.width('100%')
Progress({
value: item.current > item.target ? (item.target / item.current * 100) : (item.current / item.target * 100),
total: 100,
type: ProgressType.Linear
})
.width('100%').height(8)
.color(idx % 2 === 0 ? COLORS.green : COLORS.blue)
.backgroundColor(COLORS.line).borderRadius(4)
.margin({ top: 8 })
Row() {
Text('连续 ' + item.streak + ' 天').fontSize(11).fontColor(COLORS.text3)
Blank()
Text(item.current >= item.target ? '已达标 ✓' : '进行中')
.fontSize(11)
.fontColor(item.current >= item.target ? COLORS.green : COLORS.gold)
}.width('100%').margin({ top: 6 })
}
.width('92%').padding(14)
.backgroundColor(COLORS.card).borderRadius(12)
.margin({ bottom: 8 })
.onClick(() => { this.editIdx = idx; this.editModal = true })
}, (item: GoalItem) => item.title)
Row() {
Button('添加目标')
.fontSize(13).fontColor(COLORS.bg).backgroundColor(COLORS.green)
.borderRadius(20).height(36)
.onClick(() => { this.addModal = true })
Blank()
Text('清除已完成 >')
.fontSize(13).fontColor(COLORS.red)
.onClick(() => { this.delModal = true })
}
.width('92%').margin({ top: 4, bottom: 12 })
}
}
Tab 5 是个人目标管理,每条卡片包含三段:
标题行:左侧目标名(15px Bold)+ 右侧"current/target unit"数值(13px sub)。
进度条:ArkUI 内置 Progress 组件,type: ProgressType.Linear 线性进度条,8px 高,主色按 idx % 2 在 green 与 blue 间切换,底色 line。value 计算是关键:
item.current > item.target ? (item.target / item.current * 100) : (item.current / item.target * 100)
当 current < target(正常进度),用 current/target×100;当 current > target(如体脂率目标 17.5 → 15 的"减脂"场景),用 target/current×100 防止超过 100%。这个分支处理体现了对"两种目标语义"(增量目标 vs 减量目标)的细致思考。
状态行:左侧"连续 N 天"灰色,右侧"已达标 ✓"(绿色)或"进行中"(金色)。current >= item.target 的判断决定显示哪种状态,颜色与文字同步切换——这是状态-色彩映射的第三处应用(前两处是 scheduleColor、postColor),但这次是内联表达式而非独立函数,因为只在单处使用。
底部"添加目标"绿按钮 + "清除已完成 >"红色文字链接,分别触发 addModal 与 delModal,构成目标管理的完整增删闭环。
十五、图表卡片——月度柱状图
@Builder chartCard() {
Column() {
Row() {
Text('📊 月度锻炼趋势').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text('近6个月').fontSize(12).fontColor(COLORS.text3)
}.width('100%').margin({ bottom: 12 })
Row() {
ForEach(MONTH_IDX, (i: number) => {
Column() {
Column()
.width(20)
.height(this.breath ? WORKOUT_VAL[i] * 0.8 : WORKOUT_VAL[i] * 0.6)
.backgroundColor(i % 2 === 0 ? COLORS.green : COLORS.blue)
.borderRadius({ topLeft: 6, topRight: 6 })
.margin({ top: 8 })
Text(WORKOUT_VAL[i].toString()).fontSize(10).fontColor(COLORS.sub).margin({ top: 4 })
Text(MONTH_NAME[i]).fontSize(10).fontColor(COLORS.text3).margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
}, (i: number) => i.toString())
}
.width('100%').height(120).alignItems(VerticalAlign.Bottom)
}
.width('92%').padding(16)
.backgroundColor(COLORS.card).borderRadius(12)
.margin({ top: 8, bottom: 16 })
}
chartCard 是一个纯 ArkUI 组件堆叠实现的简易柱状图——没有用任何图表库,仅用 Column 的高度变化模拟柱体。这是"零依赖可视化"的极致体现。
头部行:左侧"📊 月度锻炼趋势"15px Bold 标题,右侧"近6个月"12px 灰字。
柱状图区(120px 高):6 个等宽 Column(layoutWeight(1)),每个 Column 内:
- 柱体:20px 宽,高度由
this.breath ? WORKOUT_VAL[i] * 0.8 : WORKOUT_VAL[i] * 0.6决定。breath翻转时高度在 80% 与 60% 系数间切换,形成"呼吸"动效。颜色按i % 2在 green 与 blue 间切换,形成绿蓝交替的视觉节奏。 - 数值:
WORKOUT_VAL[i]原值(42/55/68/51/73/86),10px sub 色。 - 月份:
MONTH_NAME[i](03-08),10px text3 色。
整个 Row 用 .alignItems(VerticalAlign.Bottom) 让所有柱体底部对齐——这是柱状图正确渲染的关键,否则柱体会从顶部下挂,视觉完全错误。
这个图表卡片的精妙之处在于:用 30 行 ArkUI 原生组件实现了一个具备动画、双色交替、底部对齐、数值标签、月份标签的完整柱状图,零第三方依赖。对于简单可视化需求,这种"手搓"方案比引入图表库更轻量、更可控。
十六、底部 Tab 栏
@Builder tabBar() {
Row() {
ForEach(TAB_LIST, (tab: TabMeta, idx: number) => {
Column() {
Text(tab.icon).fontSize(this.currentTab === idx ? 24 : 20)
Text(tab.label)
.fontSize(11)
.fontColor(this.currentTab === idx ? COLORS.tabOn : COLORS.text3)
.fontWeight(this.currentTab === idx ? FontWeight.Bold : FontWeight.Normal)
.margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
.onClick(() => { this.currentTab = idx })
}, (tab: TabMeta) => tab.label)
}
.width('100%').height(56)
.backgroundColor(COLORS.card)
.padding({ top: 6, bottom: 6 })
.border({ width: { top: 1 }, color: COLORS.line })
}
底部 Tab 栏是应用导航的核心,56px 高,6 个等宽 Tab。每个 Tab 是纵向"图标+文字"布局,选中态与未选中态有三重视觉差异:
- 图标字号:选中 24px,未选中 20px。字号放大形成"凸出"视觉。
- 文字颜色:选中
tabOn(霓虹绿),未选中text3(深灰)。色彩反差强烈。 - 文字粗细:选中
Bold,未选中Normal。粗细进一步强化。
三重差异叠加,让选中 Tab 在视觉上"跃出"一行,用户无需仔细辨认即可定位当前位置——这是移动端导航设计的精细功夫。
border({ width: { top: 1 }, color: COLORS.line }) 只在顶部画 1px 边框线,把 Tab 栏与上方内容区分隔。这是"半边描边"技巧,避免四周边框显得过重。
点击 onClick(() => { this.currentTab = idx }) 修改 currentTab,触发主内容区 if-else 链重新渲染对应 Tab——这是整个应用的核心交互闭环。
十七、弹窗系统
弹窗系统是本应用"增删改"交互的承载层,由 1 个通用遮罩 + 3 个业务弹窗组成。
17.1 通用遮罩层 modalOverlay
@Builder modalOverlay(onClose: () => void) {
Stack() {
Column()
.width('100%').height('100%')
.backgroundColor(COLORS.mask)
}
.width('100%').height('100%')
.alignContent(Alignment.Center)
.onClick(() => onClose())
}
modalOverlay 是一个 @Builder 函数,接收 onClose 回调。它用 Stack 全屏铺一层 mask(65% 黑色透明)遮罩,点击任意位置触发 onClose 关闭弹窗。alignContent(Alignment.Center) 让后续业务弹窗内容居中显示。
这种"遮罩+内容"的双层结构是 ArkUI 弹窗的标准实现——遮罩负责"点击外部关闭"的交互,业务弹窗负责具体内容,两者通过 Stack 叠加。把遮罩独立成 Builder 函数,避免三个业务弹窗各自重复实现遮罩逻辑,是 DRY 原则的体现。
17.2 新增弹窗 panelAdd
@Builder panelAdd(onClose: () => void) {
Stack() {
this.modalOverlay(onClose)
Column() {
Text('➕ 新增').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.title).margin({ bottom: 16 })
Column() {
Text('名称').fontSize(13).fontColor(COLORS.sub)
TextInput({ placeholder: '请输入名称' })
.fontSize(14).fontColor(COLORS.title).placeholderColor(COLORS.text3)
.backgroundColor(COLORS.chip).borderRadius(8).height(40).margin({ top: 6 })
}.width('100%').margin({ bottom: 12 })
Column() {
Text('数值').fontSize(13).fontColor(COLORS.sub)
TextInput({ placeholder: '请输入数值' })
.fontSize(14).fontColor(COLORS.title).placeholderColor(COLORS.text3)
.backgroundColor(COLORS.chip).borderRadius(8).height(40).margin({ top: 6 })
}.width('100%').margin({ bottom: 16 })
Row() {
Button('取消').fontSize(14).fontColor(COLORS.sub)
.backgroundColor(COLORS.chip).borderRadius(20).height(40).layoutWeight(1)
.onClick(() => onClose())
Button('确认新增').fontSize(14).fontColor(COLORS.bg)
.backgroundColor(COLORS.green).borderRadius(20).height(40).layoutWeight(1)
.margin({ left: 10 })
.onClick(() => onClose())
}.width('100%')
}
.width('80%').padding(24)
.backgroundColor(COLORS.card).borderRadius(20)
}
.width('100%').height('100%')
.alignContent(Alignment.Center)
}
panelAdd 是新增弹窗,结构为"遮罩 + 80% 宽卡片"。卡片内三段:
- 标题:"➕ 新增"18px Bold。
- 名称输入组:标签 + TextInput,输入框 chip 背景、8 圆角、40 高。
- 数值输入组:同上结构。
- 按钮行:取消(chip 背景,sub 字色)+ 确认新增(绿背景,bg 字色),两个
layoutWeight(1)等宽,10px 间距。
确认按钮用 COLORS.green 强调"正向操作",取消按钮用 chip 弱化。两个按钮都调用 onClose 关闭弹窗——这是演示页面的简化处理,实际生产环境确认按钮会先执行业务逻辑(如 this.courseList.push(...))再关闭。
17.3 编辑弹窗 panelEdit
@Builder panelEdit(onClose: () => void) {
Stack() {
this.modalOverlay(onClose)
Column() {
Text('✏️ 编辑').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.title).margin({ bottom: 16 })
Column() {
Text('当前索引').fontSize(13).fontColor(COLORS.sub)
Text(this.editIdx >= 0 ? this.editIdx.toString() : '-')
.fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.green).margin({ top: 4 })
}.width('100%').margin({ bottom: 12 })
Column() {
Text('修改内容').fontSize(13).fontColor(COLORS.sub)
TextInput({ placeholder: '请输入修改内容' })
.fontSize(14).fontColor(COLORS.title).placeholderColor(COLORS.text3)
.backgroundColor(COLORS.chip).borderRadius(8).height(40).margin({ top: 6 })
}.width('100%').margin({ bottom: 16 })
Row() {
Button('取消').fontSize(14).fontColor(COLORS.sub)
.backgroundColor(COLORS.chip).borderRadius(20).height(40).layoutWeight(1)
.onClick(() => onClose())
Button('保存').fontSize(14).fontColor(COLORS.bg)
.backgroundColor(COLORS.blue).borderRadius(20).height(40).layoutWeight(1)
.margin({ left: 10 })
.onClick(() => onClose())
}.width('100%')
}
.width('80%').padding(24)
.backgroundColor(COLORS.card).borderRadius(20)
}
.width('100%').height('100%')
.alignContent(Alignment.Center)
}
panelEdit 与 panelAdd 结构高度相似,差异在三处:
- 标题:“✏️ 编辑”。
- 当前索引展示:用
this.editIdx >= 0 ? this.editIdx.toString() : '-'显示被编辑项的索引,绿色 16px Bold。这把editIdx状态可视化,让用户知道正在编辑第几条。 - 保存按钮:颜色是
COLORS.blue(电光蓝),与新增的绿色形成差异。蓝色对应"修改/预约"语义,与全局色彩语言自洽。
17.4 删除弹窗 panelDel
@Builder panelDel(onClose: () => void) {
Stack() {
this.modalOverlay(onClose)
Column() {
Text('🗑️ 确认删除').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.title).margin({ bottom: 12 })
Text(this.delIdx >= 0 ? '确定要删除第 ' + (this.delIdx + 1) + ' 条记录吗?' : '确定要删除吗?')
.fontSize(14).fontColor(COLORS.sub).textAlign(TextAlign.Center).margin({ bottom: 20 })
Row() {
Button('取消').fontSize(14).fontColor(COLORS.sub)
.backgroundColor(COLORS.chip).borderRadius(20).height(40).layoutWeight(1)
.onClick(() => onClose())
Button('确认删除').fontSize(14).fontColor(COLORS.title)
.backgroundColor(COLORS.red).borderRadius(20).height(40).layoutWeight(1)
.margin({ left: 10 })
.onClick(() => onClose())
}.width('100%')
}
.width('76%').padding(24)
.backgroundColor(COLORS.card).borderRadius(20)
}
.width('100%').height('100%')
.alignContent(Alignment.Center)
}
panelDel 是删除确认弹窗,比新增/编辑更窄(76% 宽),因为内容简单——只有标题、提示文字、按钮行。
提示文字用 this.delIdx >= 0 ? '确定要删除第 ' + (this.delIdx + 1) + ' 条记录吗?' : '确定要删除吗?',把 delIdx 转化为"第 N 条"的人类可读文案,是状态到文案的精细转化。+1 是因为 delIdx 是 0-based 索引,用户视角是 1-based。
确认删除按钮用 COLORS.red 玫红,文字用 COLORS.title 近白——红底白字是"危险操作"的视觉惯例。取消按钮同前两个弹窗,弱化处理。
三个弹窗的"取消-确认"按钮颜色形成清晰语义梯度:取消=chip灰,新增确认=green,编辑保存=blue,删除确认=red。这是一套完整的状态-色彩语言体系。
十八、功能模块对比表
下表从八个维度对比六个 Tab 与图表卡、底部 Tab 栏、弹窗系统九大模块的设计差异:
| 模块 | 布局方式 | 数据模型 | 字段数 | 核心操作 | 动画效果 | 状态颜色 | 数据量 | 特殊组件 |
|---|---|---|---|---|---|---|---|---|
| Tab 0 课程 | 双列卡片瀑布流 | CourseItem | 6 | onClick→editModal | 无 | level→emoji 映射 | 6 项(左右各 3) | courseCard 复用 Builder |
| Tab 1 教练 | 单列人物大卡 | TrainerItem | 6 | onClick→editModal(editIdx) | 无 | rating→gold | 6 项 | 圆形头像 72×72 |
| Tab 2 排期 | 状态时间轴 | ScheduleItem | 5 | onClick→delModal(delIdx) | 无 | scheduleColor 4 态 | 6 项 | Circle+连线 条件渲染 |
| Tab 3 数据 | 双列渐变大数字卡 | StatItem | 4 | 无(展示型) | 无 | idx%2→green/blue | 4 项(左右各 2) | linearGradient 渐变条 |
| Tab 4 社区 | 左色条列表 | PostItem | 6 | onClick(预留) | 无 | postColor 4 类 | 6 项 | 4px 色条+emoji 头像 |
| Tab 5 我的 | 进度条清单 | GoalItem | 5 | onClick→editModal(editIdx) | 无 | streak→达标色 | 5 项 | Progress 组件 |
| 图表卡 | 6 柱底部对齐 | WORKOUT_VAL/ MONTH_NAME | 2 数组 | 无 | breath→柱高 0.6/0.8 系数 | idx%2→green/blue | 6 月数据 | 纯组件堆叠柱状图 |
| 底部 Tab 栏 | 6 等宽单排 | TabMeta | 2 | onClick→currentTab | 无 | currentTab→tabOn 色 | 6 项 | emoji+文字双行 |
| 弹窗系统 | Stack 遮罩+卡片 | 无(依赖 editIdx/delIdx) | - | onClose 回调 | 无 | 取消/新增/编辑/删除 4 色 | 3 个弹窗 | modalOverlay 复用遮罩 |
从对比表可以提炼几条关键规律:
布局差异化——六个 Tab 没有一个布局重复:双列卡片、单列大卡、时间轴、双列大数字、左色条列表、进度条清单,每一项都针对其业务场景做了专属设计。这种"一场景一布局"的策略是行业应用级别的视觉投入。
复用策略分层——courseCard、statCard 在各自 Tab 内复用;modalOverlay 在三个弹窗间复用;scheduleColor、postColor 在多个 Builder 间复用。复用粒度从"卡片级"到"函数级"形成三层抽象。
色彩语义统一——所有模块的状态色都遵循同一套色彩语言:完成=green、预约=blue、价值=gold、警示=red、信息=sub/text3。用户跨 Tab 浏览时不会出现颜色语义冲突。
交互闭环完整——每个可操作 Tab 都有"点击→弹窗→关闭"的完整闭环:课程/教练/我的→editModal,排期→delModal,社区/我的底部→addModal。三个弹窗覆盖了增删改全场景。
动画克制——全应用只有 chartCard 与 headerSearch 的"快速开始"按钮使用了 breath 动画,其他模块均为静态。这种克制避免了视觉噪音,让呼吸动效成为"焦点引导"而非"全局干扰"。
十九、总结与展望
19.1 设计总结
回顾整个「律动空间」智慧健身管理平台的源码,可以提炼出五条核心设计经验:
第一,色彩即语义。本应用没有把颜色当作装饰,而是把颜色当作信息载体。霓虹绿=完成、电光蓝=预约、金色=价值、玫红=警示,这套语义系统贯穿所有 Tab 与弹窗,让用户在跨模块浏览时凭借颜色即可快速识别状态。scheduleColor 与 postColor 两个工具函数是这套语义的"中央字典",所有 Builder 通过它们查询颜色,避免了语义漂移。
第二,布局即业务。六个 Tab 的布局差异不是随意的视觉变化,而是业务特性的视觉投射:课程是"商品",所以用瀑布流双列卡片便于浏览;教练是"人物",所以用大卡突出个人形象;排期是"时间序列",所以用时间轴呈现顺序;数据是"指标",所以用大数字卡突出数值;社区是"内容流",所以用左色条列表分类;我的是"目标",所以用进度条可视化达成度。每一处布局选择都能追溯到业务需求。
第三,复用即效率。@Builder 函数是 ArkUI 的轻量级复用单元,本应用通过 courseCard、statCard、modalOverlay 三个复用 Builder,把六个 Tab + 三个弹窗的代码量压缩到 1048 行。如果不用复用,仅课程卡重复 6 次、统计卡重复 4 次、遮罩重复 3 次,代码量将膨胀至 1500 行以上。复用不仅减少代码,更重要的是保证了视觉一致性——所有课程卡肯定长得一样,因为它们用同一个 Builder。
第四,状态即驱动。@State currentTab 一个变量驱动整个主内容区的 Tab 切换;@State breath 一个变量驱动两处呼吸动画;@State addModal/editModal/delModal 三个布尔驱动三个弹窗的显隐。整个应用的交互由约 10 个 @State 变量驱动,状态空间极小,可预测性极高。这是 ArkUI 声明式范式的最大红利——开发者只需关心"状态是什么",框架自动处理"UI 怎么变"。
第五,演示与生产的边界。本应用是演示型页面,数据是静态的 const,弹窗确认按钮只调用 onClose 不执行业务逻辑,breath 动画用 setInterval 而非 animateTo。这些都是"演示优先"的取舍。在真实生产环境中,需要把 const 替换为接口请求、把弹窗确认替换为真实的 CRUD 调用、把 setInterval 替换为 animation 修饰器、把 @Observed 配合 @ObjectLink 拆分子组件实现细粒度刷新。理解这些"演示与生产的边界",是从源码学习到工程实践的关键跃迁。
19.2 技术展望
基于本应用的架构,可以展望几个方向的演进:
方向一:状态管理升级。当前所有状态集中在 Page801 一个组件内,当业务膨胀到几十个 Tab 时会出现"状态爆炸"。可以引入 @Provide/@Consume 跨组件状态注入,或引入 AppStorage 全局状态管理,把弹窗状态、当前 Tab、用户信息等分层管理。
方向二:子组件拆分。把 courseCard、statCard 等从 @Builder 升级为独立的 @Component,配合 @ObjectLink 让每个卡片自观测自身数据变化,避免整个 Tab 重渲染。这对长列表性能优化尤为关键。
方向三:动画系统重构。用 animateTo 重写呼吸动画,从"setInterval 翻转布尔"升级为"声明式过渡",性能更优、过渡更平滑。还可以引入 @AnimatableExtend 自定义动画属性,实现更复杂的形变。
方向四:数据层接入。用 HTTP 请求替换静态 const,引入 Promise 与 async/await;用 @Observed + @ObjectLink 实现数据双向绑定;用本地 Preferences 持久化用户偏好(如主题色、常用 Tab)。
方向五:主题切换。基于已有的 ColorPalette 接口,定义多套 COLORS 常量(日间/夜间/赛博朋克/极简),通过 AppStorage 切换,所有 Builder 自动跟随。这套设计已经为多主题预留了扩展点。
方向六:响应式布局。引入 Breakpoint 与 GridCol/GridRow 实现多设备适配,让"双列卡片"在折叠屏展开时变为三列,在智慧屏上变为四列,真正发挥 HarmonyOS 跨设备能力。
19.3 结语
「律动空间」这个 1048 行的演示页面,是 HarmonyOS ArkUI 声明式范式在行业应用场景下的一份高质量样本。它证明了:用极简的语法(struct + @State + @Builder)、极少的依赖(零第三方库、零图片资源、emoji 替代图标),就能构建出视觉鲜明、布局多样、交互完整、语义自洽的行业级页面。
霓虹绿与电光蓝的视觉碰撞,不仅仅是两种颜色的并列,更是"完成态"与"预约态"两种业务语义的视觉化呈现。每一处颜色选择背后都有业务理由,每一处布局差异背后都有场景考量。这种"视觉服务于业务、业务驱动视觉"的设计哲学,值得每一位行业应用开发者深入体会。
当鸿蒙生态持续扩张、ArkUI 持续演进的今天,希望本文对源码的逐段拆解,能帮助开发者不仅"看懂"这份代码,更"看透"代码背后的设计思考,进而在自己的行业应用中构建出既有视觉冲击力、又有业务表达力的高质量页面。这,就是技术博文拆解源码的最终价值所在。
附录:DevEco Studio 创建新项目与查看 SDK 版本
本章节演示如何使用 DevEco Studio 创建一个 HarmonyOS 新项目,并查看当前 IDE 已安装的 SDK 版本,适合作为其他技术博文的补充操作指南。
一、创建新项目
1.1 进入欢迎界面
启动 DevEco Studio 后,首先看到的是欢迎界面。左侧导航栏默认选中 “项目”,右侧提供三个主要入口:
- 新建项目:从头创建新项目
- 打开项目:打开本地已有项目
- 克隆仓库:从 Git 等版本控制拉取代码
点击 “新建项目” 按钮,进入项目创建向导。

1.2 选择项目模板
在弹出的"新建项目"对话框中,左侧分类标签提供了两种项目类型:
| 类型 | 说明 |
|---|---|
| 应用(Application) | 开发标准的 HarmonyOS 应用,具备完整的 Ability 生命周期 |
| 元服务(Atomic Service) | 开发轻量级的原子化服务,无需安装即可使用 |
选择 “应用” 标签后,右侧展示多种模板。对于大多数场景,推荐选择 “Empty Ability” —— 这是一个最基础的入门模板,仅包含 Hello World 功能,适合从零开始构建应用。

1.3 配置项目信息
点击 “下一步” 后,进入项目配置界面,需要填写以下核心参数:
| 配置项 | 示例值 | 说明 |
|---|---|---|
| 项目名称(Project name) | rollboat |
应用的项目名称,建议使用英文命名 |
| 包名(Bundle name) | com.rollboat.myapplication |
应用唯一标识,采用反向域名格式 |
| 保存路径(Save location) | D:\CodeFactory\rollboat |
项目本地存储路径,避免使用中文和空格 |
| 兼容 SDK(Compatible SDK) | 6.1.1(24) |
目标 HarmonyOS API 版本,点击"查看参考"可了解各版本差异 |
| 模块名称(Module name) | entry |
主模块名称,默认 entry 为应用入口模块 |
| 设备类型(Device types) | ☑ Phone | 勾选目标设备:Phone / Tablet / 2in1 / Car / Wearable / TV |
右侧预览区会实时展示当前模板的默认效果 —— 一个居中显示的 “Hello World” 文本。

1.4 完成创建
确认配置无误后,点击右下角 “完成” 按钮,IDE 将自动执行以下操作:
- 生成项目骨架(Stage 模型目录结构)
- 执行
ohpm install安装依赖 - 运行 Hvigor 构建初始化(
Build Init)
构建日志中显示 “退出代码为 0” 表示项目初始化成功。

1.5 项目结构概览
创建完成后,左侧项目面板展示的是标准的 Stage 模型 目录结构:
rollboat/
├── .hvigor/ # Hvigor 构建工具缓存
├── .idea/ # IDE 配置文件
├── AppScope/ # 应用级全局配置
│ └── app.json5
├── entry/ # 主模块(入口模块)
│ ├── src/main/ets/
│ │ ├── entryability/ # Ability 生命周期管理
│ │ │ └── EntryAbility.ets
│ │ └── pages/ # UI 页面
│ │ └── Index.ets # 首页(默认 Hello World)
│ ├── src/main/resources/ # 资源文件
│ ├── module.json5 # 模块配置
│ └── build-profile.json5 # 构建配置
├── oh_modules/ # OHPM 依赖包
├── build-profile.json5 # 工程构建配置
├── hvigorfile.ts # Hvigor 构建脚本
└── oh-package.json5 # 包管理配置
核心文件 Index.ets 的默认代码如下,采用 ArkTS 声明式 UI 语法:
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
build() {
RelativeContainer() {
Text(this.message)
.id('HelloWorld')
.fontSize($r('app.float.page_text_font_size'))
.fontWeight(FontWeight.Bold)
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
.onClick(() => {
this.message = 'Welcome';
})
}
.height('100%')
.width('100%')
}
}
| 关键语法 | 作用 |
|---|---|
@Entry |
标记为页面入口,可用于路由跳转 |
@Component |
声明为自定义组件 |
@State |
状态变量,数据变更时自动触发 UI 刷新 |
RelativeContainer |
相对布局容器,替代传统线性布局 |
.onClick() |
点击事件,此处点击后文本变为 “Welcome” |
打开右侧 Previewer(预览器),选择 Phone 设备,即可实时预览 Hello World 效果,无需连接真机或启动模拟器。

二、查看 SDK 版本
2.1 查看 HarmonyOS SDK
DevEco Studio 安装时已内置 HarmonyOS SDK,无需单独下载。通过以下路径查看:
文件 → 设置 → HarmonyOS SDK(或快捷键
Ctrl + Alt + S搜索 “HarmonyOS SDK”)
在设置面板中,可以看到当前已安装的 SDK 版本信息:
| 名称 | 阶段 | 状态 |
|---|---|---|
| HarmonyOS 6.1.1 | Release | ✅ 已安装 |
界面顶部提示:“HarmonyOS SDK 已经包含在 IDE,无需单独安装”,省去了手动配置 SDK 的繁琐步骤。

2.2 查看 ArkUI-X SDK(跨平台扩展)
如果项目需要将 ArkUI 框架扩展到多个 OS 平台(Android / iOS / OpenHarmony),还需要配置 ArkUI-X SDK。路径如下:
文件 → 设置 → 语言和框架 → ArkUI-X
在这里可以查看已安装和可选的 ArkUI-X SDK 版本:
| 版本 | SDK 版本号 | 阶段 | 状态 |
|---|---|---|---|
| API Version 24 | 6.1.1.100 | Release | ✅ 已安装 |
| API Version 23 | 6.1.0.28 | Beta1 | 未安装 |
| API Version 22 | 6.0.2.112 | Release | 未安装 |
安装路径示例:D:\DevTools\ArkUI-X\sdk
说明:ArkUI-X 允许开发者使用一套 ArkTS 主代码,同时构建多平台应用。如果仅开发 HarmonyOS 原生应用,无需额外安装 ArkUI-X SDK。

三、小结
| 步骤 | 操作 | 关键点 |
|---|---|---|
| 创建项目 | 欢迎页 → 新建项目 → 选择 Empty Ability 模板 → 配置项目信息 → 完成 | 使用 Stage 模型 + ArkTS 语言 |
| 查看 SDK | 设置 → HarmonyOS SDK | SDK 已内置,无需手动安装 |
| 跨平台扩展 | 设置 → ArkUI-X | 根据需要安装对应 API 版本 |
至此,DevEco Studio 的项目创建与 SDK 环境确认全部完成,可以开始 HarmonyOS 应用的功能开发。
本文基于 DevEco Studio 6.1.1 Release 版本编写,不同版本界面可能存在细微差异。
更多推荐



所有评论(0)