一、技术前言

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 不引入新的组件实例,开销极低,非常适合用于页面内的区块拆分。本应用的 headerSearchtabCoursetabTrainer 等十余个 @Builder 函数将一个 1048 行的大型页面拆分成高内聚低耦合的语义区块。

1.3 关键技术决策

回顾整个源码,可以提炼出以下几项关键技术决策:

第一,单页面多场景的 Tab 路由策略。该应用没有为每个功能模块创建独立页面,而是将六个场景统一收纳在一个 @Entry 入口组件内,通过 currentTab 数值索引切换 @Builder 函数。这种做法的优势在于状态共享简单(同一份 breath、同一份弹窗状态可被所有 Tab 复用)、页面跳转零开销、适合"信息总览型"应用;代价是单文件行数较多,需要靠注释分区与 @Builder 拆分保证可读性。

第二,深色赛博朋克视觉语言。背景采用 #0D1117(GitHub 深色主题同款底色),主强调色用 #39FF14(霓虹绿)与 #00D4FF(电光蓝),辅以 #FFD700(金色)做价格与星级、#FF3366(玫红)做点赞与删除。这种配色方案在健身类应用中形成鲜明的"运动燃脂"暗示——绿色代表完成与达标,蓝色代表预约与进行中,金色代表价格与价值,红色代表警告与热度,色彩语义高度自洽。

第三,状态-色彩映射函数化。将"已预约→蓝""待确认→金"这类业务语义到视觉语义的映射独立成 scheduleColorpostColor 两个工具函数,避免在每个 @Builder 内重复书写 if-else,提高了可维护性。

第四,呼吸动画的极简实现。没有使用 ArkUI 的 animateToanimation 修饰器,而是直接用 setInterval 每 1000ms 翻转一个布尔值 breath,再让按钮背景色与柱状图高度对 breath 求值。这种做法虽不符合声明式动画的最佳实践,但胜在直观、可控、便于演示。


在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

二、整体架构流程图

2.1 Mermaid 架构图

组件主体 Page801

数据模型层

工具函数层

色彩与常量层

COLORS 常量 16 色

TAB_LIST 6 项 Tab 元数据

MONTH_IDX / WORKOUT_VAL / HEAT_DAYS / WEEK_NAMES

scheduleColor 排期状态→颜色

postColor 帖子分类→颜色

CourseItem 课程 6 字段

TrainerItem 教练 6 字段

ScheduleItem 排期 5 字段

StatItem 数据 4 字段

PostItem 帖子 6 字段

GoalItem 目标 5 字段

@State 状态变量群

aboutToAppear / aboutToDisappear 生命周期

build 根布局

headerSearch 头部区

6 个 Tab Builder

chartCard 月度图表

tabBar 底部 Tab 栏

modalOverlay 遮罩层

panelAdd / panelEdit / panelDel

2.2 架构解读

从架构图可以清晰看到,整个应用呈现出经典的**“四层倒金字塔”**结构,自下而上依次为:色彩与常量层 → 工具函数层 → 数据模型层 → 组件主体层。

色彩与常量层是整个应用的"基因层",定义了视觉风格与基础数据。COLORS 常量一经定义便贯穿所有 Builder;TAB_LIST 决定了底部导航的结构;MONTH_IDXWORKOUT_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 种颜色按"语义角色"命名而非"视觉特征"命名——bgcardchiptitlesubtext3 描述的是颜色的用途(背景、卡片、芯片标签、标题、副标题、三级文字),而不是 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、打卡热力图空格、进度条底色、时间轴连线。明度介于 bgcard 之间,形成微妙的层次。
  • 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 个字段:课程名、教练名、时长、难度等级、消耗卡路里、价格。字段类型混合了 stringnumber——duration 用字符串 '45min' 直接携带单位便于展示,caloriesprice 用 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 完全不同:教练名、专业方向、评分、学员数、技能标签、头像。ratingnumber(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' 这种月-日 时:分格式,直接展示无需格式化;statusscheduleColor 的入参;location'A厅' 'B厅' 简化场地编码。排期数据 6 项,状态分布为"已预约×2、待确认×1、已完成×2、已取消×1",正好覆盖四种颜色映射,是典型的"演示样本"。

6.4 StatItem 数据模型

@Observed export class StatItem {
  label: string;
  value: string;
  unit: string;
  trend: string;
}

4 字段:标签、数值、单位、趋势。值得玩味的是 valuestring 而非 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() 渲染;categorypostColor 的入参。帖子数据 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.greenCOLORS.greenD 之间切换,月度图表的柱高会在 WORKOUT_VAL[i] * 0.8* 0.6 之间切换,形成"呼吸"动效。

aboutToDisappearclearInterval(this.timer) 是必须的清理动作——如果不清理,组件销毁后定时器仍在运行,会尝试修改已销毁组件的 breath,可能引发内存泄漏或异常。这是 HarmonyOS 应用开发的标准范式。

值得讨论的是:用 setInterval 实现动画在 ArkUI 中并非最佳实践。ArkUI 提供了 animateToanimation 修饰器来实现声明式动画,性能更好、更流畅。但本应用选择 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,纵向排列五大区块:

  1. headerSearch()——头部区,含 logo、搜索栏、热力图、快捷按钮。
  2. Divider——1px 分割线,颜色 line,把头部与主内容区视觉分隔。
  3. Scroll() { Column() {...} }——可滚动主内容区,.layoutWeight(1) 让它占据剩余高度,.scrollBar(BarState.Off) 隐藏滚动条保持视觉纯净。内部 Column 根据 currentTab 条件渲染六个 Tab 之一,末尾追加 chartCard() 月度图表——注意图表始终出现在所有 Tab 之下,是"通用尾部"。
  4. tabBar()——底部 Tab 栏,固定 56px 高。
  5. 三个弹窗条件渲染——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,右侧是 TextInputTextInput 设置 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,四档颜色形成"未打卡→轻度→中度→高强度"的递进色阶。

ForEachkeyGeneratori.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))形成主次对比:

  • "快速开始"实色按钮:背景色随 breathgreengreenD 间切换,形成呼吸光效;文字用 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 主结构是经典的双列瀑布流:两个等宽 ColumnlayoutWeight(1)),各 ForEach 渲染 3 个 courseCard,列间间距通过 margin({ right: 5 })margin({ left: 5 }) 合计 10px 形成。ForEachkeyGeneratoritem.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,背景 chipborderRadius(36)(直径一半)形成正圆,内部居中放 40px emoji。圆形是人物头像的视觉惯例,强化"人"的语义。

中部信息区:纵向排列"姓名(18px Bold)→ 专业(13px sub)→ 星级+学员数(一行三段)→ 标签胶囊"。其中星级用 ⭐ emoji + 金色数字 4.9,学员数用灰色 1280 学员,标签胶囊用 10px 蓝字 + chip 背景 + 8px 圆角。这一区信息密度极高但层次分明:粗体姓名抓住眼球,副色专业说明,灰色学员数辅助,蓝色标签做分类。

右侧预约按钮:12px 深底字 + 霓虹绿背景 + 14px 圆角胶囊。颜色对比强烈,是明显的"行动召唤"按钮。

整个卡片 16px 圆角比课程卡的 12px 更大,是因为大卡需要更柔和的视觉;padding 14 比 10 更宽,给信息更多呼吸。点击触发 editIdx = idxeditModal = 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 = idxdelModal = 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 的复用单元,接收 StatItemidx 双参数。它的视觉特色是顶部 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 间切换,底色 linevalue 计算是关键:

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 个等宽 ColumnlayoutWeight(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)
}

panelEditpanelAdd 结构高度相似,差异在三处:

  • 标题:“✏️ 编辑”。
  • 当前索引展示:用 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 没有一个布局重复:双列卡片、单列大卡、时间轴、双列大数字、左色条列表、进度条清单,每一项都针对其业务场景做了专属设计。这种"一场景一布局"的策略是行业应用级别的视觉投入。

复用策略分层——courseCardstatCard 在各自 Tab 内复用;modalOverlay 在三个弹窗间复用;scheduleColorpostColor 在多个 Builder 间复用。复用粒度从"卡片级"到"函数级"形成三层抽象。

色彩语义统一——所有模块的状态色都遵循同一套色彩语言:完成=green、预约=blue、价值=gold、警示=red、信息=sub/text3。用户跨 Tab 浏览时不会出现颜色语义冲突。

交互闭环完整——每个可操作 Tab 都有"点击→弹窗→关闭"的完整闭环:课程/教练/我的→editModal,排期→delModal,社区/我的底部→addModal。三个弹窗覆盖了增删改全场景。

动画克制——全应用只有 chartCard 与 headerSearch 的"快速开始"按钮使用了 breath 动画,其他模块均为静态。这种克制避免了视觉噪音,让呼吸动效成为"焦点引导"而非"全局干扰"。


十九、总结与展望

19.1 设计总结

回顾整个「律动空间」智慧健身管理平台的源码,可以提炼出五条核心设计经验:

第一,色彩即语义。本应用没有把颜色当作装饰,而是把颜色当作信息载体。霓虹绿=完成、电光蓝=预约、金色=价值、玫红=警示,这套语义系统贯穿所有 Tab 与弹窗,让用户在跨模块浏览时凭借颜色即可快速识别状态。scheduleColorpostColor 两个工具函数是这套语义的"中央字典",所有 Builder 通过它们查询颜色,避免了语义漂移。

第二,布局即业务。六个 Tab 的布局差异不是随意的视觉变化,而是业务特性的视觉投射:课程是"商品",所以用瀑布流双列卡片便于浏览;教练是"人物",所以用大卡突出个人形象;排期是"时间序列",所以用时间轴呈现顺序;数据是"指标",所以用大数字卡突出数值;社区是"内容流",所以用左色条列表分类;我的是"目标",所以用进度条可视化达成度。每一处布局选择都能追溯到业务需求。

第三,复用即效率@Builder 函数是 ArkUI 的轻量级复用单元,本应用通过 courseCardstatCardmodalOverlay 三个复用 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、用户信息等分层管理。

方向二:子组件拆分。把 courseCardstatCard 等从 @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 将自动执行以下操作:

  1. 生成项目骨架(Stage 模型目录结构)
  2. 执行 ohpm install 安装依赖
  3. 运行 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 版本编写,不同版本界面可能存在细微差异。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐