一、技术前言

在这里插入图片描述

在全民健身与运动健康数字化浪潮中,运动健康管理应用正经历从"被动记录"到"主动陪伴"的深刻变革。从晨间慢跑 5km 的卡路里消耗追踪,到 HIIT 爆发训练的间歇计时;从训练提醒通知的精准触达,到团课教练口令的实时字幕转写——每一个运动场景都需要匹配不同的数据可视化方案、通知策略和语音辅助能力。传统运动健康应用面临三大挑战:训练通知铃声千篇一律无法激发运动热情、团课跟练时外教口令语言不通导致动作脱节、训练进度缺乏直观可视化导致用户难以感知进阶轨迹。

在这里插入图片描述
HarmonyOS ArkUI 框架为这些挑战提供了系统级解决方案。ArkUI 的声明式 UI 范式通过 @Component 封装可复用组件、@State 管理响应式状态、@Builder 拆分复杂 UI 结构,天然适合"训练-课程-提醒"三层架构的组件化拆分。@Observed 装饰器让数据模型字段级变化被 UI 感知,实现"训练状态更新即视图刷新"的流畅体验;ForEach 配合唯一键值列表渲染,让训练清单、课程宫格、铃声库等动态列表的更新高效且无闪烁。

在这里插入图片描述
本平台深度融合 HarmonyOS 6.1.1 的三大前沿特性。Notification Kit(API 24)实现沙箱自定义铃声能力链——通过 buildWavBytes 函数生成正弦波 44 字节头 PCM 音频字节,经 saveRingToSandbox 写入 EL1 沙箱 filesDir 目录,再通过 fileUri.getUriFromPath 将沙箱路径转为 uri,最终以 'uri::' + uri 格式填入 NotificationRequest.sound 字段,突破以往只能使用 rawfile 文件名的限制,让每条训练提醒都能携带用户自定义的运动激励铃声。Speech Kit(API 24)的 AI 字幕组件引入 sourceLanguagetargetLanguagefontSizefontColor 四个新字段,支持中英双向翻译、中英双语对照、四档字号调节和五色字体预设,配合 writeAudio 方法以 640 字节 PCM 块写入音频流实现实时语音转字幕,让外教团课口令不再有语言障碍。Canvas 绘制实现今日完成率进度环 drawRing 与近 12 周训练时长折线图 drawLine,通过 CanvasRenderingContext2DarccreateLinearGradientfillText 等 API 绘制背景环、渐变填充区域和数据点,breath 状态联动进度环弧长在 0.85~1.0 区间波动、折线末点半径放大、柱状图高度微缩,模拟数据实时复盘的呼吸感动画。

在这里插入图片描述

二、整体架构流程图

Page1207 主组件

headerMain 头部品牌标题

内容区 6 Tab 切换

tabBar 底部导航

弹窗系统 add/edit/del

Tab0 训练
统计三格+完成率进度环+周时长折线+训练清单

Tab1 课程
四列宫格+时长大卡横滑

Tab2 提醒
训练提醒时间轴+授权卡+发布历史

Tab3 铃音
正弦波生成器+铃声库列表

Tab4 字幕
AI字幕五区块+场景推荐

Tab5 我的
身份渐变大卡+月度柱状+周报复盘

Notification Kit
沙箱自定义铃声 sound

Speech Kit
AI字幕四新字段

Canvas 绘制
进度环+折线图

panelAdd 新建提醒

panelEdit 编辑提醒

panelDel 删除确认

架构以 Page1207 为根组件,使用 Stack 容器层叠:底层 Column 纵向排列头部品牌标题区、分割线、Scroll 内容区和底部 Tab 栏,顶层是三个独立弹窗(panelAdd/panelEdit/panelDel 各自条件渲染)。内容区通过 currentTab 状态变量在 6 个 @Builder 方法间切换,三大特性分散在训练(Canvas 进度环与折线图)、铃音(Notification Kit 沙箱铃声)和字幕(Speech Kit AI 字幕)三个 Tab 上,所有状态变量统一声明在组件顶层实现跨 Tab 共享。呼吸动画定时器 breath 每秒翻转一次,联动进度环弧长、折线末点半径、柱状图高度和状态点透明度,赋予整个页面"活"的生命感。

在这里插入图片描述

三、色彩体系设计

3.1 ColorPalette 接口定义

interface ColorPalette {
  bg: string;        // 页面底色·夜跑黑
  card: string;      // 卡片底色·炭咖
  title: string;     // 主标题·暖白
  sub: string;       // 次级文字·燕麦
  text3: string;     // 弱化文字·灰咖
  orange: string;    // 主题色·活力橙
  orangeD: string;   // 主题色深·深橙
  green: string;     // 辅色·青柠绿
  blue: string;      // 辅色·湖蓝
  red: string;       // 辅色·警示红
  line: string;      // 分割线
  tabOn: string;     // Tab 激活色
  mask: string;      // 弹窗遮罩
  greenFade: string; // 青柠渐隐(折线图渐变终点)
  gradA: string;     // 渐变起点·深橙棕(身份大卡)
  gradB: string;     // 渐变终点·活力橙(身份大卡)
  onMain: string;    // 主色按钮上的深字
}

ColorPalette 接口集中声明了页面所有颜色字段,共计 17 个色值。这种接口先行、常量后随的设计模式,使得颜色体系具备类型安全约束——任何拼写错误在编译期即可被发现,而非等到运行时渲染出错误颜色才暴露问题。接口中不仅包含常规的背景、卡片、文字三阶色,还专门为特定场景定义了语义化字段:greenFade 用于折线图渐变终点营造青柠向透明渐隐的效果;gradA/gradB 用于身份大卡的深橙棕到活力橙渐变;onMain 用于活力橙按钮上的深色文字保证对比度。

3.2 COLORS 常量逐色分析

const COLORS: ColorPalette = {
  bg: '#12100E',
  card: '#1E1B18',
  dark: '#262220',
  title: '#F5EFE6',
  sub: '#C8BBA8',
  text3: '#8A7F70',
  orange: '#FF7A3C',
  orangeD: '#E05E20',
  green: '#9CD04F',
  blue: '#4FA3E3',
  red: '#E85B5B',
  line: '#332E29',
  tabOn: '#FF7A3C',
  mask: 'rgba(0,0,0,0.6)',
  greenFade: 'rgba(156,208,79,0.06)',
  gradA: '#3B2210',
  gradB: '#5A2E0C',
  onMain: '#241305'
};

色彩体系以"夜跑黑 + 活力橙 + 青柠绿"为核心三角对比。bg(#12100E)是极深的暖黑底色,模拟夜跑时路灯下的暗光环境,比纯黑多了暖调使其不显冰冷;card(#1E1B18)是炭咖色卡片底,比背景亮一档形成层次;dark(#262220)是更深一档的容器底色,用于统计格和标签底。三阶背景色构成从页面底到卡片到内嵌容器的视觉纵深。

在这里插入图片描述
title(#F5EFE6)是暖白主标题色,在深色背景上形成高对比;sub(#C8BBA8)是燕麦色次级文字,层次柔和;text3(#8A7F70)是灰咖色弱化文字,辅助信息不抢视觉。三阶文字色确保信息层级分明。

orange(#FF7A3C)是活力橙主色,代表运动场上的能量与激情,用于按钮主色、进度强调和 Tab 选中态;orangeD(#E05E20)是深橙,用于柱状图非末月柱体,与活力橙形成同色系深浅对比;green(#9CD04F)是青柠绿辅色,代表已完成与通过状态,用于进度弧、完成标记和上升指标;blue(#4FA3E3)是湖蓝辅色,用于字幕语言状态标识;red(#E85B5B)是警示红,用于未授权状态和删除操作。

值得注意的是 Tab 选中色 tabOn 直接使用 orange(活力橙)而非另设色值,这是因为活力橙在夜跑黑背景上对比度极高,用户视觉定位迅速。onMain(#241305)是极深的暖棕色,专门用于活力橙按钮上的文字,保证深底浅字的可读性。greenFade 使用 rgba 半透明格式,在折线图渐变终点营造青柠绿向几乎透明过渡的效果,让数据填充区域不显沉闷。身份大卡的 gradA(#3B2210)到 gradB(#5A2E0C)的 135° 渐变模拟从深橙棕到活力橙的光照过渡,呼应运动场景中的汗水与荣耀。

四、Tab 元数据与辅助数据

4.1 底部导航 Tab 定义

interface TabMeta {
  icon: string;   // Tab 图标
  label: string;  // Tab 标签
}

const TAB_LIST: TabMeta[] = [
  { icon: '💪', label: '训练' },
  { icon: '🏋️', label: '课程' },
  { icon: '⏰', label: '提醒' },
  { icon: '🔔', label: '铃音' },
  { icon: '🗣', label: '字幕' },
  { icon: '👤', label: '我的' }
];

底部导航采用 6 Tab 单排布局,使用 Emoji 作为图标避免了图片资源加载开销,同时保持了跨平台一致的渲染效果。6 个 Tab 覆盖运动健康管理的完整链路:训练(今日执行)→ 课程(学习进阶)→ 提醒(节奏管理)→ 铃音(通知定制)→ 字幕(语音辅助)→ 我的(数据复盘),形成从执行到复盘的闭环。TabMeta 接口的 iconlabel 字段在 ForEach 中通过 'tab-' + tab.label 作为唯一键值,保证 Tab 切换时列表渲染高效且不闪烁。

在这里插入图片描述

4.2 语言选项与字号预设

interface LangOption {
  code: string;
  name: string;
}

const SRC_LANGS: LangOption[] = [
  { code: 'zh', name: '中文' },
  { code: 'en', name: '英文' }
];

const TGT_LANGS_EN: LangOption[] = [
  { code: 'zh', name: '中文' },
  { code: 'en', name: '英文' },
  { code: 'zh-en', name: '中英双语' }
];

const SIZE_OPTIONS: SizeOption[] = [
  { size: AICaptionFontSize.SMALL, name: '小号' },
  { size: AICaptionFontSize.NORMAL, name: '标准' },
  { size: AICaptionFontSize.BIG, name: '大号' },
  { size: AICaptionFontSize.LARGE, name: '超大' }
];

const CAPTION_FONT_COLORS: string[] = ['#FFFFFF', '#FFE9B0', '#9CE8B5', '#9CD0FF', '#FFB3C1'];

语言选项分为源语言和目标语言两组。源语言仅支持中文和英文两种,而目标语言在英文源时支持中文、英文、中英双语三种翻译方向。这种分组设计源于 AI 字幕的翻译逻辑:中文源时目标语言锁定为 ‘zh’(无翻译方向),英文源时可以选择翻译为中文、保留英文或双语对照。SIZE_OPTIONS 使用 AICaptionFontSize 枚举而非数字,体现了类型安全的设计理念——字号选项不是任意数值,而是框架预定义的四个离散档位。CAPTION_FONT_COLORS 五色预设(经典白、暖黄、薄荷绿、天蓝、樱粉)覆盖了不同运动场景下的字幕可读性需求,如在深色团课视频背景上白色和暖黄更清晰,在明亮户外场景下薄荷绿和天蓝更醒目。

4.3 图表数据与周报复盘

const LINE_DATA: number[] = [180, 165, 210, 240, 195, 260, 230, 205, 250, 275, 220, 245];
const LINE_LABELS: string[] = ['W1', 'W2', 'W3', 'W4', 'W5', 'W6', 'W7', 'W8', 'W9', 'W10', 'W11', 'W12'];

const MONTH_IDX: number[] = [0, 1, 2, 3, 4, 5];
const MONTH_NAME: string[] = ['03', '04', '05', '06', '07', '08'];
const MONTH_CAL: number[] = [860, 920, 1100, 1020, 1240, 1180];
const MONTH_MAX: number = 1400;

const WEEK_REVIEW: WeekRow[] = [
  { icon: '🏃', label: '有氧总时长', value: '186 分钟', delta: '+12%' },
  { icon: '🏋️', label: '力量训练量', value: '9.4 吨', delta: '+8%' },
  { icon: '🔥', label: '周消耗热量', value: '5820 千卡', delta: '+6%' },
  { icon: '😴', label: '深睡平均', value: '2.1 小时', delta: '-3%' },
  { icon: '🫀', label: '静息心率', value: '56 次/分', delta: '-2' },
  { icon: '🦵', label: '腿部酸痛度', value: '轻度', delta: '正常' }
];

图表数据采用常量数组预定义,避免了运行时计算开销。LINE_DATA 提供 12 周训练时长数据点,配合 LINE_LABELS 横轴标签,通过 Canvas drawLine 方法绘制折线图。MONTH_CAL 提供 6 个月卡路里消耗数据,MONTH_MAX 作为柱高归一化基准值(1400 千卡),柱状图通过 MONTH_CAL[idx] / MONTH_MAX * 78 换算为像素高度。WEEK_REVIEW 周报复盘清单涵盖了有氧、力量、热量、睡眠、心率、酸痛度六个维度,是运动健康管理的全景指标,delta 字段的 +/- 前缀驱动 deltaColor 函数返回青柠绿或警示红,让进步与退步一目了然。

4.4 铃声时长预设与重复类型

const RING_DURATION_PRESETS: number[] = [900, 1200, 1500];
const REPEAT_TAGS: string[] = ['每天', '工作日', '周末', '单次'];

铃声时长预设提供 900ms、1200ms、1500ms 三档,对应短促、标准、悠长三种铃声风格,满足不同运动场景的提醒需求——短促适合间歇训练提醒,悠长适合晨跑唤醒。重复类型 chips 提供四种重复策略,在新建和编辑提醒弹窗中复用,保证交互一致性。

五、工具函数

5.1 时间戳函数

function nowTime(): string {
  const d = new Date();
  const h = String(d.getHours()).padStart(2, '0');
  const m = String(d.getMinutes()).padStart(2, '0');
  const s = String(d.getSeconds()).padStart(2, '0');
  return h + ':' + m + ':' + s;
}

nowTime 函数返回 HH:mm:ss 格式的当前时间字符串,用于通知发布历史的时间戳。使用 padStart(2, '0') 保证时、分、秒始终是两位数,避免凌晨时段显示为 “6:3:1” 这样的不规范格式。该函数在 NoticeLog 构造函数中被调用,每次新增发布记录时自动记录时间。

5.2 正弦波 WAV 音频生成函数

function buildWavBytes(freq: number, durationMs: number): ArrayBuffer {
  const sampleRate = 44100;
  const numSamples = Math.floor(sampleRate * durationMs / 1000);
  const dataSize = numSamples * 2;
  const buf = new ArrayBuffer(44 + dataSize);
  const view = new DataView(buf);
  // ... RIFF 头 + fmt 子块 + data 子块 + PCM 采样数据
  return buf;
}

buildWavBytes 是特性 A 的核心音频生成函数,构造符合 WAV 规范的音频字节流。函数以 44100Hz 采样率、16bit 位深、单声道 PCM 编码生成正弦波音频。44 字节的 WAV 头部包含 RIFF 标识、文件大小、WAVE 格式标识、fmt 子块(编码格式、声道数、采样率、字节率、块对齐、位深)和 data 子块标识。音频数据部分通过正弦函数 Math.sin(2 * Math.PI * freq * t) 生成波形,并叠加起音包络 env(前 20ms 渐入避免爆音)和自然衰减 decay(模拟铃声的衰减特性),最终通过 view.setInt16 以小端序写入 16bit 采样值。这个函数让用户无需任何音频文件资源,即可在应用内实时生成可播放的铃声。

5.3 WAV 文件大小估算函数

function wavSizeText(durationMs: number): string {
  const bytes = 44 + Math.floor(44100 * durationMs / 1000) * 2;
  return Math.round(bytes / 1024).toString() + ' KB';
}

wavSizeText 根据时长估算 WAV 文件大小并返回 XX KB 格式文本,用于铃声库列表中显示文件大小。计算公式为 44 字节头加上采样数据量(采样率 × 时长秒数 × 2 字节/采样),再除以 1024 转为 KB。例如 1200ms 的铃声约 104KB,让用户在导入沙箱前对文件体积有直观预期。

5.4 状态配色函数群

function statusColor(status: string): string {
  if (status === '已完成') { return COLORS.green; }
  if (status === '进行中') { return COLORS.orange; }
  return COLORS.text3;
}

function levelColor(level: string): string {
  if (level === '入门') { return COLORS.green; }
  if (level === '进阶') { return COLORS.orange; }
  return COLORS.red;
}

function deltaColor(delta: string): string {
  if (delta.indexOf('+') === 0) { return COLORS.green; }
  if (delta.indexOf('-') === 0) { return COLORS.red; }
  return COLORS.text3;
}

function langName(code: string): string {
  if (code === 'zh') { return '中文'; }
  if (code === 'en') { return '英文'; }
  if (code === 'zh-en') { return '中英双语'; }
  return code;
}

四个状态配色函数构成了业务语义到视觉色彩的映射体系。statusColor 将训练状态三态映射为青柠绿(已完成)、活力橙(进行中)、灰咖(待开始);levelColor 将课程难度三档映射为青柠绿(入门)、活力橙(进阶)、警示红(高级),利用色彩暖度递增暗示难度递增;deltaColor 通过字符串前缀检测将环比变化映射为青柠绿(上升)、警示红(下降)、灰咖(持平),让周报复盘的进步退步一眼可辨;langName 将语言码转为展示名,用于字幕场景卡的语言组合标签。这些函数将"业务语义→色彩"的映射逻辑从 UI 层抽离,使得颜色策略集中可维护。

5.5 四列宫格分片函数

function chunkGrid(list: CourseItem[], size: number): CourseItem[][] {
  const rows: CourseItem[][] = [];
  for (let i = 0; i < list.length; i += size) {
    rows.push(list.slice(i, i + size));
  }
  return rows;
}

chunkGrid 函数将一维课程列表切分为每行 size 个的二维数组,在常量期预计算为 COURSE_ROWS,避免在 ForEach 渲染过程中动态切片造成的性能开销。这种"预计算 + 常量缓存"的模式是 ArkUI 列表渲染性能优化的典型手段——将可预计算的数据处理前置到模块加载阶段,让渲染阶段只做纯展示。

六、数据模型层

6.1 WorkoutItem 训练条目模型

@Observed
export class WorkoutItem {
  name: string;     // 训练项目名
  duration: string; // 时长
  cal: string;      // 消耗卡路里
  status: string;   // 状态:已完成/进行中/待开始

  constructor(name: string, duration: string, cal: string, status: string) {
    this.name = name;
    this.duration = duration;
    this.cal = cal;
    this.status = status;
  }
}

WorkoutItem 是训练 Tab 清单的数据模型,使用 @Observed 装饰器声明,使其字段变化能被 UI 感知。四个字段分别对应训练项目名、时长、卡路里消耗和完成状态。Mock 数据 WORKOUT_LIST 提供 7 条晨训日课表数据,从晨间慢跑到泡沫轴筋膜松解覆盖完整训练流程,状态分布为 2 已完成、1 进行中、4 待开始,配合 todayDone(5)和 todayTotal(7)驱动进度环显示。

6.2 CourseItem 课程条目模型

@Observed
export class CourseItem {
  icon: string;     // 课程图标
  name: string;     // 课程名
  level: string;    // 难度:入门/进阶/高级
  duration: string; // 课程时长

  constructor(icon: string, name: string, level: string, duration: string) { ... }
}

CourseItem 是课程 Tab 的数据模型,包含图标、名称、难度等级和时长四个字段。Mock 数据 COURSE_LIST 提供 8 门课程,覆盖晨跑燃脂、撸铁全身、睡前冥想、拳击有氧、柔韧拉伸、动感单车、HIIT 爆发、游泳塑形等不同运动类型,难度从入门到高级分布均匀。8 门课通过 chunkGrid 切分为 2 行 × 4 列的宫格布局,同时在横滑大卡中完整展示。

6.3 RemindItem 训练提醒模型

@Observed
export class RemindItem {
  time: string;    // 提醒时间
  title: string;   // 提醒标题
  repeat: string;  // 重复类型:每天/工作日/周末/单次
  on: boolean;     // 是否开启

  constructor(time: string, title: string, repeat: string, on: boolean) { ... }
}

RemindItem 是提醒 Tab 时间轴和弹窗系统共同绑定的业务实体,on 布尔字段驱动时间轴节点的开关状态和颜色变化。Mock 数据 REMIND_LIST 提供 6 条一天训练节奏提醒,从 06:30 晨跑唤醒到 21:00 体测复盘,on 字段分布为 4 开启 2 关闭,初始状态可由用户通过 Toggle 开关翻转。该模型贯穿提醒 Tab 的全部交互——新建弹窗创建新实例、编辑弹窗回填字段、删除弹窗移除条目,形成完整的 CRUD 闭环。

6.4 RingItem 铃声库条目模型

@Observed
export class RingItem {
  name: string;       // 铃声名
  file: string;       // 沙箱文件名
  freq: number;       // 生成频率 Hz
  duration: number;   // 时长 ms
  size: string;       // 文件大小展示
  inSandbox: boolean; // 是否已写入沙箱 EL1

  constructor(name: string, file: string, freq: number,
    duration: number, size: string, inSandbox: boolean) { ... }
}

RingItem 是铃音 Tab 铃声坊的数据模型,核心字段 inSandbox 标记该铃声是否已生成 WAV 文件并写入 EL1 沙箱目录。初始 Mock 数据 RING_LIST 提供 5 条铃声(破晓鸟鸣、心跳鼓点、冲刺号角、青柠滴答、静夜风铃),频率从 520Hz 到 1320Hz 覆盖低中高音域,inSandbox 初始均为 false,需用户主动点击"生成到沙箱"触发 importRing 方法完成落盘。size 字段初始为 ‘—’,导入沙箱后通过 wavSizeText 更新为实际文件大小。

6.5 NoticeLog 通知发布历史模型

@Observed
export class NoticeLog {
  title: string;   // 通知标题
  text: string;    // 通知正文
  time: string;    // 发布时间

  constructor(title: string, text: string) {
    this.title = title;
    this.text = text;
    this.time = nowTime();
  }
}

NoticeLog 是通知发布历史的轻量模型,构造函数中自动调用 nowTime() 记录发布时间戳。该模型通过 addNoticeLog 方法以 unshift 方式插入列表头部(最新在前),并限制最多保留 6 条历史记录,超出时 pop 移除尾部最旧记录,形成滑动窗口式的发布日志。

6.6 CaptionScene 字幕场景模型

@Observed
export class CaptionScene {
  scene: string;   // 场景名
  desc: string;    // 场景说明
  src: string;     // 推荐源语言
  tgt: string;     // 推荐目标语言

  constructor(scene: string, desc: string, src: string, tgt: string) { ... }
}

CaptionScene 是字幕 Tab 场景推荐卡的数据模型,每条记录包含场景名、说明和推荐的语言组合。Mock 数据 SCENE_LIST 提供 6 个运动训练行业场景:跟练口令双语(HIIT 外教)、教练讲解转写(团课中文)、晨跑英语听力(播客配字幕)、冥想引导双语、赛事解说转写、康复指导跟读。用户点击场景卡时,applyScene 方法将推荐语言组合应用到 srcLangtgtLang 状态,实现一键配置字幕参数。

七、组件主体结构

7.1 @State 状态变量体系

@Entry
@Component
struct Page1207 {
  /************* 基础 UI 状态 *************/
  @State currentTab: number = 0;
  @State breath: boolean = false;
  @State timer: number = -1;

  /************* 弹窗状态(三态统一) *************/
  @State addModal: boolean = false;
  @State editModal: boolean = false;
  @State delModal: boolean = false;
  @State editIdx: number = -1;
  @State delIdx: number = -1;

  /************* 训练业务状态 *************/
  @State workoutList: WorkoutItem[] = WORKOUT_LIST;
  @State remindList: RemindItem[] = REMIND_LIST;
  @State todayTotal: number = 7;
  @State todayDone: number = 5;
  @State streakDays: number = 26;
  @State todayCal: number = 826;

  /************* 特性 A 状态(Notification Kit) *************/
  @State granted: boolean = false;
  @State notifyId: number = 100;
  @State currentRingIdx: number = 0;
  @State ringList: RingItem[] = RING_LIST;
  @State noticeLogs: NoticeLog[] = [];
  @State sandboxCount: number = 0;
  @State genFreq: number = 880;
  @State genDuration: number = 1200;

  /************* 特性 B 状态(Speech Kit AI 字幕) *************/
  private captionController: AICaptionController = new AICaptionController();
  @State captionShown: boolean = false;
  @State srcLang: string = 'zh';
  @State tgtLang: string = 'zh';
  @State captionSize: AICaptionFontSize = AICaptionFontSize.NORMAL;
  @State captionColor: string = CAPTION_FONT_COLORS[0];
  @State captionReady: boolean = false;
  @State captionErrMsg: string = '';
  @State captionFed: number = 0;
  @State sceneList: CaptionScene[] = SCENE_LIST;

  /************* 特性 C 状态(Canvas 绘制) *************/
  private ringCtx: CanvasRenderingContext2D = new CanvasRenderingContext2D(new RenderingContextSettings(true));
  private lineCtx: CanvasRenderingContext2D = new CanvasRenderingContext2D(new RenderingContextSettings(true));
  @State ringReady: boolean = false;
  @State lineReady: boolean = false;

  /************* 提醒表单状态 *************/
  @State formTime: string = '';
  @State formTitle: string = '';
  @State formRepeat: string = '每天';
  @State editTime: string = '';
  @State editTitle: string = '';
  @State editRepeat: string = '每天';
}

组件状态变量按功能模块分组声明,共计 30+ 个 @State 变量覆盖基础 UI、弹窗、训练业务、三大特性和表单六大类。这种分组声明方式使状态来源清晰可追溯,便于维护时快速定位。值得注意的是 captionController 和两个 Canvas 上下文使用 private 而非 @State——AICaptionController 是控制器实例而非响应式数据,Canvas 上下文是绘制句柄而非 UI 驱动状态,它们不需要触发视图重渲染。ringReadylineReady 标记 Canvas onReady 回调完成,确保 setInterval 重绘不会在画布未就绪时执行。

7.2 生命周期方法

aboutToAppear() {
  // 查询通知授权状态(Notification Kit)
  notificationManager.isNotificationEnabled().then((enabled: boolean) => {
    this.granted = enabled;
  }).catch(() => {
    this.granted = false;
  });
  // 呼吸动画:breath 每秒翻转,并手动调用 Canvas 绘制方法实现无闪烁重绘
  this.timer = setInterval(() => {
    this.breath = !this.breath;
    if (this.ringReady) { this.drawRing(); }
    if (this.lineReady) { this.drawLine(); }
  }, 1000);
}

aboutToDisappear() {
  clearInterval(this.timer);
}

aboutToAppear 在组件实例创建后、build 执行前调用,完成两项初始化:一是通过 notificationManager.isNotificationEnabled() 异步查询通知授权状态并更新 granted 状态变量,驱动头部胶囊和提醒授权卡的颜色与文案;二是启动 setInterval 每秒翻转 breath 布尔值,并手动调用 drawRingdrawLine 实现画布无闪烁重绘。这种"状态翻转驱动 UI 动画 + 手动重绘驱动 Canvas 动画"的双轨制,是因为 Canvas 绘制不依赖 @State 响应式触发,必须显式调用绘制方法。aboutToDisappear 在组件销毁时清理定时器,避免内存泄漏。

八、头部区域详解

@Builder
headerMain() {
  Column({ space: 10 }) {
    Row() {
      Column({ space: 4 }) {
        Text('跃动计划').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
        Text(this.headerSub()).fontSize(11).fontColor(COLORS.sub).maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
      }.alignItems(HorizontalAlign.Start).layoutWeight(1)

      Circle({ width: 10, height: 10 }).fill(COLORS.orange)
        .opacity(this.breath ? 0.95 : 0.45)
    }.width('100%')

    Row({ space: 6 }) {
      // 通知授权状态胶囊
      Row({ space: 4 }) {
        Circle({ width: 5, height: 5 }).fill(this.granted ? COLORS.green : COLORS.red)
        Text(this.granted ? '通知已授权' : '通知未授权').fontSize(9).fontColor(COLORS.sub)
      }.padding({ left: 8, right: 8, top: 4, bottom: 4 }).backgroundColor(COLORS.dark).borderRadius(10)

      // 当前铃声胶囊
      Row({ space: 4 }) {
        Circle({ width: 5, height: 5 }).fill(COLORS.orange)
        Text(this.currentRingIdx >= 0 && this.currentRingIdx < this.ringList.length
          ? this.ringList[this.currentRingIdx].name : '未设铃声').fontSize(9).fontColor(COLORS.sub)
          .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
      }.padding({ left: 8, right: 8, top: 4, bottom: 4 }).backgroundColor(COLORS.dark).borderRadius(10)

      // 字幕语言胶囊
      Row({ space: 4 }) {
        Circle({ width: 5, height: 5 }).fill(COLORS.blue)
        Text('字幕 ' + this.srcLang + '→' + this.tgtLang).fontSize(9).fontColor(COLORS.sub)
      }.padding({ left: 8, right: 8, top: 4, bottom: 4 }).backgroundColor(COLORS.dark).borderRadius(10)
    }.width('100%')
  }.width('100%').padding({ left: 14, right: 14, top: 12, bottom: 10 }).backgroundColor(COLORS.bg)
}

头部区域采用 Column 纵向两段式布局:上段是品牌标题行,下段是三特性状态胶囊行。品牌标题行使用 Row 横向排列左侧标题副标题列和右侧呼吸圆点——“跃动计划"以 20px 加粗暖白主标题展示,副标题通过 headerSub() 方法根据 currentTab 返回 Tab 联动文案(如训练台返回"训练台 · 今日 7 项完成 5 项”,课程库返回"课程库 · 8 门精品课任意练"),使头部信息随 Tab 切换动态变化。右侧呼吸圆点以活力橙填充,透明度随 breath 在 0.95~0.45 间波动,模拟心跳脉冲的视觉隐喻。

三特性状态胶囊行展示通知授权、当前铃声和字幕语言三个核心状态的实时概览。通知授权胶囊的圆点颜色由 granted 状态驱动——已授权青柠绿、未授权警示红,让用户一眼可知通知权限状态。当前铃声胶囊显示 currentRingIdx 对应的铃声名,圆点固定活力橙标识该特性。字幕语言胶囊以 srcLang→tgtLang 格式展示当前语言组合(如 zh→zhen→zh-en),圆点湖蓝色与训练统计格的蓝色呼应。三个胶囊均使用 COLORS.dark 底色和 borderRadius(10) 圆角,形成统一的视觉语言。

九、Tab0 训练台详解

@Builder
tabTrain() {
  Column({ space: 12 }) {
    // 今日概览统计三格
    Row({ space: 8 }) {
      this.statCell('🔥', '今日消耗', this.todayCal.toString(), '千卡', COLORS.orange)
      this.statCell('📅', '连续打卡', this.streakDays.toString(), '天', COLORS.green)
      this.statCell('⏱', '训练时长', '52', '分钟', COLORS.blue)
    }.width('100%')

    // 今日完成率进度环卡(特性 C:Canvas drawRing 呼吸联动)
    Row({ space: 14 }) {
      Canvas(this.ringCtx).width(180).height(180)
        .onReady(() => { this.ringReady = true; this.drawRing(); })
      Column({ space: 8 }) { /* 完成率说明 */ }.layoutWeight(1).alignItems(HorizontalAlign.Start)
    }.width('100%').padding(14).backgroundColor(COLORS.card).borderRadius(12).alignItems(VerticalAlign.Center)

    // 近 12 周训练时长折线卡
    Column({ space: 8 }) {
      Row() { Text('📈 近 12 周训练时长')... }
      Canvas(this.lineCtx).width('100%').height(170)
        .onReady(() => { this.lineReady = true; this.drawLine(); })
    }.width('100%').padding(14).backgroundColor(COLORS.card).borderRadius(12)

    // 今日训练清单
    Column({ space: 10 }) {
      ForEach(this.workoutList, (item: WorkoutItem, idx: number) => {
        Row({ space: 10 }) {
          Circle({ width: 8, height: 8 }).fill(statusColor(item.status))
            .opacity(item.status === '进行中' && this.breath ? 1 : 0.55)
          Column({ space: 4 }) {
            Text(item.name)...
            Row({ space: 6 }) {
              Text('⏱ ' + item.duration)...
              Text('🔥 ' + item.cal).fontColor(COLORS.orange)
            }
          }.layoutWeight(1).alignItems(HorizontalAlign.Start)
          Text(item.status).fontSize(10).fontColor(statusColor(item.status))
            .padding({...}).backgroundColor(COLORS.dark).borderRadius(9)
        }.width('100%').padding(12).backgroundColor(COLORS.card).borderRadius(10).alignItems(VerticalAlign.Center)
      }, (item: WorkoutItem, idx: number) => 'workout-' + idx.toString())
    }.width('100%')
  }.width('100%')
}

训练台是 Tab0 的核心展示区,自上而下分为四个功能区块。第一区是今日概览统计三格,通过 statCell Builder 复用渲染"图标+标签+数值大字+单位"的四元素统计卡片,分别展示今日消耗卡路里(826 千卡,活力橙)、连续打卡天数(26 天,青柠绿)、训练时长(52 分钟,湖蓝),三色对应三种运动维度的视觉编码。

第二区是今日完成率进度环卡,这是特性 C 的 Canvas 核心展示。Canvas 组件以 180×180 像素尺寸渲染,onReady 回调置位 ringReady 并首绘 drawRing()。进度环右侧是完成率说明列,包含"今日完成率"标题、"7 项计划已完成 5 项"明细和图例(已完成青柠绿、待开始灰咖),让用户在环图之外获得精确数据。

第三区是近 12 周训练时长折线图卡,同样是特性 C 的 Canvas 展示。Canvaswidth('100%') 自适应宽度、170px 高度渲染 drawLine() 绘制的折线图,包含背景网格、青柠渐变填充区域、折线和数据点,12 个数据点展示近三个月的训练时长趋势。

第四区是今日训练清单,通过 ForEach 遍历 workoutList 渲染 7 条训练条目。每条包含状态指示点(颜色由 statusColor 函数映射,进行中状态透明度随 breath 闪烁)、训练名、时长、卡路里和状态胶囊。ForEach 的键值函数 'workout-' + idx.toString() 保证列表渲染的稳定性。

9.1 统计三格 Builder

@Builder
statCell(icon: string, label: string, value: string, unit: string, color: string) {
  Column({ space: 4 }) {
    Row({ space: 4 }) {
      Text(icon).fontSize(12)
      Text(label).fontSize(10).fontColor(COLORS.sub)
    }
    Row({ space: 3 }) {
      Text(value).fontSize(20).fontWeight(FontWeight.Bold).fontColor(color)
      Text(unit).fontSize(9).fontColor(COLORS.text3)
    }
  }.layoutWeight(1).padding({ top: 10, bottom: 10 }).backgroundColor(COLORS.card)
    .borderRadius(12).alignItems(HorizontalAlign.Center)
}

statCell 是统计三格的复用 Builder,接收图标、标签、数值、单位和颜色五个参数,通过 layoutWeight(1) 等分三格宽度。数值以 20px 加粗大字展示,颜色由调用方传入实现维度差异化编码(橙/绿/蓝),单位以 9px 灰咖小字附在数值右侧,视觉层次分明。

十、Tab1 课程库详解

@Builder
tabCourse() {
  Column({ space: 12 }) {
    // 四列宫格课程入口
    Column({ space: 10 }) {
      Row() {
        Text('🏋️ 精选课程宫格').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
        Column().layoutWeight(1)
        Text('点击快速开练').fontSize(9).fontColor(COLORS.text3)
      }.width('100%')

      ForEach(COURSE_ROWS, (row: CourseItem[]) => {
        Row({ space: 8 }) {
          ForEach(row, (course: CourseItem) => {
            Column({ space: 6 }) {
              Text(course.icon).fontSize(24)
              Text(course.name).fontSize(10).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
                .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
              Text(course.level).fontSize(8).fontColor(levelColor(course.level))
            }.layoutWeight(1).padding({ top: 12, bottom: 12 }).backgroundColor(COLORS.card)
              .borderRadius(12).alignItems(HorizontalAlign.Center)
          }, (course: CourseItem) => 'grid-' + course.name)
        }.width('100%')
      }, (row: CourseItem[]) => 'row-' + row[0].name)
    }.width('100%')

    // 课程时长大卡横滑
    Column({ space: 10 }) {
      Scroll() {
        Row({ space: 10 }) {
          ForEach(COURSE_LIST, (course: CourseItem) => {
            Column({ space: 8 }) {
              Text(course.icon).fontSize(30)
              Text(course.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
              Text(course.duration).fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.orange)
              Text(course.level + ' · ' + langName('zh') + '教练带练').fontSize(9).fontColor(COLORS.sub)
            }.width(150).padding(14).backgroundColor(COLORS.card).borderRadius(12).alignItems(HorizontalAlign.Start)
          }, (course: CourseItem) => 'big-' + course.name)
        }.padding({ bottom: 4 })
      }.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).width('100%')
    }.width('100%')
  }.width('100%')
}

课程库 Tab 分为上下两区:四列宫格入口和横滑大卡。四列宫格通过预计算的 COURSE_ROWS 二维数组渲染,外层 ForEach 遍历行、内层 ForEach 遍历列,每格展示课程图标(24px)、名称(10px 加粗)和难度标签(8px,颜色由 levelColor 映射——入门青柠绿、进阶活力橙、高级警示红)。宫格设计让 8 门课程在两行内一览无余,适合快速选择。

横滑大卡区使用 Scroll 容器配合 scrollable(ScrollDirection.Horizontal) 实现横向滚动,每张大卡固定 150px 宽度,展示更大的课程图标(30px)、课程名(13px)、时长大字(18px 活力橙)和"难度 · 中文教练带练"副信息。横滑设计适合单卡聚焦浏览,用户可逐张滑动查看课程详情。两种布局方式互补——宫格适合全局概览,横滑适合逐个细看,满足不同选择偏好。

十一、Tab2 提醒台详解

提醒台是特性 A 的主舞台,整合了通知授权、训练提醒时间轴、发布训练提醒和发布历史四个区块。

11.1 通知授权卡

Row({ space: 10 }) {
  Circle({ width: 10, height: 10 }).fill(this.granted ? COLORS.green : COLORS.red)
    .opacity(this.breath ? 1 : 0.55)
  Column({ space: 3 }) {
    Text(this.granted ? '通知已授权' : '通知未授权').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
    Text(this.granted ? '训练提醒将通过系统通知送达,配合沙箱自定义铃声'
      : '授权后才能接收训练提醒,曾拒绝会拉起设置页').fontSize(10).fontColor(COLORS.sub).maxLines(2)
  }.layoutWeight(1).alignItems(HorizontalAlign.Start)
  Text(this.granted ? '已开启' : '去授权').fontSize(11).fontWeight(FontWeight.Bold)
    .fontColor(this.granted ? COLORS.green : COLORS.onMain)
    .backgroundColor(this.granted ? COLORS.dark : COLORS.orange).borderRadius(12)
    .onClick(() => { this.requestAuth(); })
}.width('100%').padding(14).backgroundColor(COLORS.card).borderRadius(12).alignItems(VerticalAlign.Center)

通知授权卡是特性 A 的权限入口。左侧状态圆点颜色由 granted 状态驱动,透明度随 breath 呼吸闪烁,让"未授权"状态视觉上更显紧迫。中间文案根据授权状态切换——已授权说明"训练提醒将通过系统通知送达,配合沙箱自定义铃声",未授权引导"授权后才能接收训练提醒,曾拒绝会拉起设置页"。右侧按钮已授权时显示"已开启"(青柠绿文字、灰咖底),未授权时显示"去授权"(深棕字、活力橙底),点击调用 requestAuth() 方法发起授权请求。

11.2 训练提醒时间轴

ForEach(this.remindList, (item: RemindItem, idx: number) => {
  Row() {
    // 时间列
    Column() {
      Text(item.time).fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
      Text(item.repeat).fontSize(8).fontColor(COLORS.text3)
    }.width(50).height('100%').justifyContent(FlexAlign.Center)

    // 竖线轴 + 节点圆点
    Column() {
      Circle({ width: 8, height: 8 }).fill(item.on ? COLORS.orange : COLORS.text3)
        .opacity(item.on && this.breath ? 1 : 0.6)
      Column().width(2).layoutWeight(1).backgroundColor(COLORS.line)
    }.width(14).height('100%').alignItems(HorizontalAlign.Center)

    // 提醒卡片
    Row({ space: 8 }) {
      Column({ space: 4 }) {
        Text(item.title)...
        Text(item.on ? '提醒已开启 · ' + item.repeat + '重复' : '提醒已关闭 · 可开启恢复')...
      }.layoutWeight(1).alignItems(HorizontalAlign.Start)
      Toggle({ type: ToggleType.Switch, isOn: item.on }).width(40).selectedColor(COLORS.orange)
        .onChange((isOn: boolean) => { this.toggleRemind(idx, isOn); })
      Text('✏️').onClick(() => { this.openEditRemind(idx); })
      Text('🗑').onClick(() => { this.delIdx = idx; this.delModal = true; })
    }.layoutWeight(1).height('100%').padding({...}).backgroundColor(COLORS.card).borderRadius(10)
  }.height(72).width('100%').margin({ bottom: 6 })
}, (item: RemindItem, idx: number) => 'remind-' + idx.toString())

时间轴是提醒台的核心视觉,每行固定 height(72) 高度,横向分为三段:时间列(50px 宽,展示提醒时间和重复类型)、竖线轴(14px 宽,顶部圆点 + 竖线 layoutWeight(1) 填满行高)、提醒卡片(占满剩余宽度)。竖线轴的圆点颜色由 item.on 驱动——开启活力橙、关闭灰咖,开启时圆点透明度随 breath 闪烁。竖线使用 Column().width(2).layoutWeight(1) 实现从圆点下方延伸到行底的连接线效果,形成时间轴的视觉串联。

提醒卡片内包含标题副信息列、Toggle 开关、编辑按钮和删除按钮。Toggle 的 selectedColor 设为活力橙,onChange 回调调用 toggleRemind 翻转开关状态。编辑按钮点击调用 openEditRemind 回填表单字段并打开编辑弹窗,删除按钮点击设置 delIdx 并打开删除确认弹窗。这种"时间轴 + CRUD 操作"的交互模式,让提醒管理既直观又高效。

11.3 发布历史

if (this.noticeLogs.length === 0) {
  Text('暂无发布记录,点上方「发布训练提醒」试听沙箱铃声').fontSize(10).fontColor(COLORS.text3)
    .padding({ top: 16, bottom: 16 }).width('100%').textAlign(TextAlign.Center)
} else {
  List({ space: 8 }) {
    ForEach(this.noticeLogs, (log: NoticeLog, idx: number) => {
      ListItem() {
        Row({ space: 10 }) {
          Circle({ width: 6, height: 6 }).fill(COLORS.blue)
          Column({ space: 3 }) {
            Text(log.title)...
            Text(log.text)...
          }.layoutWeight(1).alignItems(HorizontalAlign.Start)
          Text(log.time).fontSize(9).fontColor(COLORS.text3)
        }.width('100%').padding(10).backgroundColor(COLORS.card).borderRadius(10)
      }
    }, (log: NoticeLog, idx: number) => 'notice-' + idx.toString())
  }.width('100%')
}

发布历史区使用 List + ListItem + ForEach 渲染 noticeLogs 数组,空状态显示引导文案,非空状态以列表展示每条发布记录的标题、正文和时间戳。unshift 插入保证最新记录在最前,pop 移除保证最多 6 条,形成滑动窗口日志。每条记录左侧湖蓝圆点标识,时间戳右对齐灰咖小字,视觉层次清晰。

十二、Tab3 铃声坊详解

铃声坊是特性 A 的核心展示区,整合了当前铃声状态、正弦波生成器和铃声库列表三个区块。

12.1 正弦波生成器

Slider({ value: this.genFreq, min: 440, max: 1320, step: 20, style: SliderStyle.OutSet }).width('100%').height(30)
  .blockColor(COLORS.orange).trackColor(COLORS.dark).selectedColor(COLORS.orange)
  .onChange((value: number, mode: SliderChangeMode) => { this.genFreq = value; })

Row({ space: 8 }) {
  ForEach(RING_DURATION_PRESETS, (ms: number) => {
    Text(ms.toString() + 'ms').fontSize(10).fontWeight(FontWeight.Bold)
      .fontColor(this.genDuration === ms ? COLORS.onMain : COLORS.sub)
      .backgroundColor(this.genDuration === ms ? COLORS.orange : COLORS.dark).borderRadius(10)
      .onClick(() => { this.genDuration = ms; })
  }, (ms: number) => 'dur-' + ms.toString())
}.width('100%')

Text('生成 44 字节头 PCM 正弦波并写入沙箱')...
  .backgroundColor(COLORS.orange).borderRadius(11)
  .onClick(() => { this.genCustomRing(); })

正弦波生成器是铃声坊的交互核心,提供频率 Slider(440~1320Hz,步进 20Hz)和时长预设档位(900/1200/1500ms)两组参数控制。Slider 的 blockColorselectedColor 均为活力橙,trackColor 为灰咖,与深色主题统一。时长档位以 chips 形式展示,选中态活力橙底深棕字、未选中态灰咖底燕麦字。点击"生成"按钮调用 genCustomRing() 方法,该方法使用当前 genFreqgenDuration 参数创建新的 RingItem,通过 saveRingToSandbox 生成 WAV 字节并写入 EL1 沙箱,最终更新铃声库列表。

12.2 铃声库列表

ForEach(this.ringList, (ring: RingItem, idx: number) => {
  Column({ space: 8 }) {
    Row({ space: 10 }) {
      Text('🎵').fontSize(18)
      Column({ space: 3 }) {
        Text((idx === this.currentRingIdx ? '✓ ' : '') + ring.name).fontSize(12)
          .fontColor(idx === this.currentRingIdx ? COLORS.orange : COLORS.title)
        Text(ring.freq.toString() + ' Hz · ' + ring.duration.toString() + ' ms · ' + ring.size).fontSize(9).fontColor(COLORS.sub)
      }.layoutWeight(1).alignItems(HorizontalAlign.Start)
      Text(ring.inSandbox ? '已入沙箱' : '未导入').fontSize(9)
        .fontColor(ring.inSandbox ? COLORS.green : COLORS.text3)
        .backgroundColor(COLORS.dark).borderRadius(9)
    }.width('100%').alignItems(VerticalAlign.Center)

    Row({ space: 8 }) {
      Text(ring.inSandbox ? '重新生成到沙箱' : '生成到沙箱')...
        .onClick(() => { this.importRing(idx); })
      Text(idx === this.currentRingIdx ? '当前铃声' : '设为默认铃声')...
        .onClick(() => { this.setCurrentRing(idx); })
    }.width('100%')
  }.width('100%').padding(12).backgroundColor(COLORS.card).borderRadius(10)
}, (ring: RingItem, idx: number) => 'ring-' + idx.toString())

铃声库列表展示 5 条预置铃声和用户自定义生成的铃声。每条铃声卡上方是信息行——铃声名前缀 标记当前默认铃声(活力橙字色),副信息展示频率、时长和文件大小,右侧状态标签"已入沙箱"(青柠绿)或"未导入"(灰咖)。下方是双按钮行——"生成到沙箱"调用 importRing 方法生成 WAV 并写入 EL1,"设为默认铃声"调用 setCurrentRing 方法(未导入时自动先导入)。当前铃声的按钮显示"当前铃声"灰化不可点击,避免重复设置。

十三、Tab4 AI 字幕详解

AI 字幕 Tab 是特性 B 的核心展示区,分为实时预览、语言设置、字号四档、五色预设和场景推荐五个区块。

13.1 AI 字幕实时预览

AICaptionComponent({
  isShown: this.captionShown,
  controller: this.captionController,
  options: this.buildCaptionOptions()
}).width('100%').height(110).borderRadius(10)

Row({ space: 8 }) {
  Button(this.captionShown ? '隐藏字幕' : '开启字幕').fontSize(12).height(32)
    .backgroundColor(COLORS.tabOn).fontColor(COLORS.onMain)
    .onClick(() => { this.captionShown = !this.captionShown; })
  Button('写入演示音频').fontSize(12).height(32).backgroundColor(COLORS.dark).fontColor(COLORS.sub)
    .onClick(() => { this.feedAudioStream(); })
  Column().layoutWeight(1)
  Text('已写 ' + this.captionFed.toString() + ' 块').fontSize(9).fontColor(COLORS.text3)
}.width('100%')

AICaptionComponent 是 Speech Kit 的 AI 字幕组件,接收三个参数:isShown@Link 双向绑定(父组件直接传 @State 引用,子组件内部修改会同步回父组件),controllerAICaptionController 实例用于 writeAudio 写入音频流,options 通过 buildCaptionOptions() 方法组装包含四新字段的配置对象。预览卡上方状态行显示"已就绪"或"初始化中",下方双按钮分别控制字幕显示和写入演示音频流。feedAudioStream 方法生成 640 字节 PCM 块(16kHz/16bit/单声道,约 20ms 音频),通过 captionController.writeAudio 写入字幕服务,每次写入递增 captionFed 计数器。

13.2 语言联动设置

// 源语言 chips
ForEach(SRC_LANGS, (lang: LangOption) => {
  Text(lang.name).fontSize(10).fontWeight(FontWeight.Bold)
    .fontColor(this.srcLang === lang.code ? COLORS.onMain : COLORS.sub)
    .backgroundColor(this.srcLang === lang.code ? COLORS.orange : COLORS.dark).borderRadius(10)
    .onClick(() => { this.switchSourceLang(lang.code); })
}, (lang: LangOption) => 'src-' + lang.code)

// 目标语言:中文源锁定,英文源三选
if (this.srcLang === 'zh') {
  Text('中文(锁定)').fontColor(COLORS.text3).backgroundColor(COLORS.dark)...
  Text('中文源仅支持目标 zh').fontSize(9).fontColor(COLORS.text3)
} else {
  ForEach(TGT_LANGS_EN, (lang: LangOption) => {
    Text(lang.name)...
      .onClick(() => { this.tgtLang = lang.code; })
  }, (lang: LangOption) => 'tgt-' + lang.code)
}

语言设置采用联动设计:源语言切换时通过 switchSourceLang 方法联动目标语言——中文源时目标语言锁定为 ‘zh’(显示"中文(锁定)"灰化标签和说明文案),英文源时默认切换为 ‘zh-en’(中英双语)并开放三选。这种联动逻辑源于 AI 字幕的翻译能力约束——中文源时无翻译方向(字幕即原文),英文源时支持翻译为中文、保留英文或双语对照。选中态 chips 以活力橙底深棕字展示,未选中态以灰咖底燕麦字展示,视觉差异清晰。

13.3 字号与颜色预设

// 字号四档
ForEach(SIZE_OPTIONS, (opt: SizeOption) => {
  Text(opt.name).fontSize(11).fontWeight(FontWeight.Bold)
    .fontColor(this.captionSize === opt.size ? COLORS.onMain : COLORS.sub)
    .backgroundColor(this.captionSize === opt.size ? COLORS.orange : COLORS.dark).borderRadius(10)
    .onClick(() => { this.captionSize = opt.size; })
}, (opt: SizeOption) => 'size-' + opt.name)

// 五色预设
ForEach(CAPTION_FONT_COLORS, (color: string, idx: number) => {
  Stack() {
    Circle({ width: 26, height: 26 }).fill(color)
    if (this.captionColor === color) {
      Text('✓').fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.bg)
    }
  }.alignContent(Alignment.Center)
    .onClick(() => { this.captionColor = color; })
}, (color: string, idx: number) => 'font-color-' + idx.toString())

字号四档使用 AICaptionFontSize 枚举值(SMALL/NORMAL/BIG/LARGE),以等宽 chips 横排展示,选中态活力橙。五色预设使用 Stack 叠加圆形色块和选中 标记,5 个圆形(经典白、暖黄、薄荷绿、天蓝、樱粉)横排展示,点击即设为 captionColor。字号和颜色的 @State 变化会触发 buildCaptionOptions() 重新组装配置对象并传入 AICaptionComponent,实现字幕样式实时预览。

13.4 字幕场景推荐

ForEach(this.sceneList, (scene: CaptionScene, idx: number) => {
  Row({ space: 10 }) {
    Text('🎯').fontSize(16)
    Column({ space: 3 }) {
      Text(scene.scene).fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
      Text(scene.desc).fontSize(9).fontColor(COLORS.sub).maxLines(2)
    }.layoutWeight(1).alignItems(HorizontalAlign.Start)
    Text(langName(scene.src) + ' → ' + langName(scene.tgt)).fontSize(9)
      .fontColor(this.srcLang === scene.src && this.tgtLang === scene.tgt ? COLORS.green : COLORS.text3)
      .backgroundColor(COLORS.dark).borderRadius(9)
  }.width('100%').padding(12).backgroundColor(COLORS.card).borderRadius(10)
  .onClick(() => { this.applyScene(scene); })
}, (scene: CaptionScene, idx: number) => 'scene-' + idx.toString())

场景推荐卡列表展示 6 个运动训练行业场景,每条包含场景名、说明和推荐语言组合标签。标签颜色由当前语言设置是否匹配推荐组合驱动——匹配时青柠绿(表示已应用),不匹配时灰咖。点击场景卡调用 applyScene 方法,将推荐语言组合应用到 srcLangtgtLang,实现一键配置字幕参数。这种场景化推荐降低了用户理解语言参数的认知成本——用户只需选择运动场景,系统自动配置最优语言组合。

十四、Tab5 我的详解

我的 Tab 分为身份渐变大卡、月度柱状图和周报复盘三个区块。

14.1 身份渐变大卡

Column({ space: 12 }) {
  Row({ space: 12 }) {
    Text('🏃').fontSize(34)
    Column({ space: 4 }) {
      Text('晨曦跑者 · Leo').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
      Text('Lv.6 体能进阶 · 已坚持 ' + this.streakDays.toString() + ' 周').fontSize(10).fontColor(COLORS.onMain)
    }.layoutWeight(1).alignItems(HorizontalAlign.Start)
  }.width('100%')

  Row({ space: 8 }) {
    this.idStat('累计里程', '1284 km')
    this.idStat('周均消耗', '962 千卡')
    this.idStat('获得徽章', '14 枚')
  }.width('100%')
}.width('100%').padding(16).borderRadius(14)
.linearGradient({ angle: 135, colors: [[COLORS.gradA, 0.0], [COLORS.gradB, 1.0]] })

身份渐变大卡使用 linearGradient 以 135° 角从 gradA(#3B2210 深橙棕)渐变到 gradB(#5A2E0C 活力橙),模拟运动场景中汗水与荣耀交织的光照效果。大卡上方是用户身份行——跑步 Emoji(34px)、昵称"晨曦跑者 · Leo"(16px 加粗暖白)和等级"Lv.6 体能进阶 · 已坚持 26 周"(10px 深棕字 onMain)。下方三格统计通过 idStat Builder 复用渲染"数值+标签"格式,展示累计里程、周均消耗和获得徽章。深棕字在橙色渐变背景上保证可读性,体现 onMain 色值的场景化设计。

14.2 周报复盘清单

ForEach(WEEK_REVIEW, (row: WeekRow, idx: number) => {
  Row({ space: 10 }) {
    Text(row.icon).fontSize(16)
    Text(row.label).fontSize(12).fontColor(COLORS.title).layoutWeight(1).maxLines(1)
    Text(row.value).fontSize(11).fontColor(COLORS.sub)
    Text(row.delta).fontSize(10).fontWeight(FontWeight.Bold).fontColor(deltaColor(row.delta))
      .width(52).textAlign(TextAlign.Center)
  }.width('100%').padding({ top: 10, bottom: 10 }).backgroundColor(COLORS.card).borderRadius(10)
}, (row: WeekRow, idx: number) => 'week-' + idx.toString())

周报复盘清单展示 6 项运动健康指标:有氧总时长(+12% 青柠绿)、力量训练量(+8% 青柠绿)、周消耗热量(+6% 青柠绿)、深睡平均(-3% 警示红)、静息心率(-2 警示红)、腿部酸痛度(正常 灰咖)。delta 字段通过 deltaColor 函数映射颜色——+ 前缀青柠绿(进步)、- 前缀警示红(退步)、其他灰咖(持平),让用户一眼可辨本周训练成效。值得注意的是静息心率 -2 显示为红色,这在运动健康语境中是"进步"(心率降低代表心肺功能提升),但代码按前缀逻辑统一映射为红色,体现了配色函数的通用性与业务语义的差异。

十五、图表卡片实现原理

15.1 进度环 drawRing

drawRing() {
  const ctx = this.ringCtx;
  ctx.antialias = true;
  const cx = 90, cy = 90, r = 66;
  const progress = this.todayDone / this.todayTotal;
  const breathVal = this.breath ? 1.0 : 0.85;

  // ① 背景环
  ctx.beginPath();
  ctx.arc(cx, cy, r, 0, Math.PI * 2);
  ctx.strokeStyle = COLORS.dark;
  ctx.lineWidth = 12;
  ctx.stroke();

  // ② 进度弧(从 12 点方向起笔)
  ctx.beginPath();
  ctx.arc(cx, cy, r, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * progress * breathVal);
  ctx.strokeStyle = COLORS.green;
  ctx.lineWidth = 12;
  ctx.lineCap = 'round';
  ctx.stroke();

  // ③ 中心百分比大字
  ctx.fillStyle = COLORS.title;
  ctx.font = 'bold 26px sans-serif';
  ctx.textAlign = 'center';
  ctx.fillText(Math.round(progress * 100).toString() + '%', cx, cy + 2);

  // ④ 中心副标签
  ctx.font = '10px sans-serif';
  ctx.fillStyle = COLORS.text3;
  ctx.fillText('今日完成率', cx, cy + 24);
}

drawRing 方法分四步绘制进度环。第一步绘制背景环——以圆心 (90,90)、半径 66 的完整圆,strokeStyle 为灰咖 dark 色,线宽 12px。第二步绘制进度弧——从 12 点方向 -Math.PI/2 起笔,弧长为 Math.PI * 2 * progress * breathVal,其中 progress 是完成率(5/7≈0.71),breathVal 是呼吸系数(1.0 或 0.85),strokeStyle 为青柠绿,lineCap 为 round 让弧端圆润。第三步绘制中心百分比大字——26px 加粗暖白,textAlign 居中。第四步绘制副标签"今日完成率"——10px 灰咖小字。breath 系数让弧长在 85%~100% 区间波动,模拟数据实时复盘的呼吸感。

15.2 折线图 drawLine

drawLine() {
  const ctx = this.lineCtx;
  ctx.antialias = true;
  const w = ctx.width > 0 ? ctx.width : 340;
  const h = 170, pad = 26, max = 320, n = LINE_DATA.length;
  const stepX = (w - pad * 2) / (n - 1);
  const plotH = h - pad * 2 - 14;
  const baseY = h - pad - 14;

  // ① 背景网格
  for (let i = 0; i <= 3; i++) {
    const y = pad + plotH * i / 3;
    ctx.beginPath();
    ctx.moveTo(pad, y);
    ctx.lineTo(w - pad, y);
    ctx.stroke();
  }

  // ② 青柠渐变填充区域
  const grad = ctx.createLinearGradient(0, pad, 0, baseY);
  grad.addColorStop(0, COLORS.green);
  grad.addColorStop(1, COLORS.greenFade);
  // ... 闭合路径填充

  // ③ 折线
  // ... moveTo + lineTo 连接 12 个数据点

  // ④ 数据点(末点半径随 breath 放大)
  for (let i = 0; i < n; i++) {
    ctx.arc(x, y, i === n - 1 ? (this.breath ? 4.5 : 3.5) : 3, 0, Math.PI * 2);
    ctx.fillStyle = COLORS.card;
    ctx.fill();
    ctx.strokeStyle = COLORS.green;
    ctx.stroke();
  }

  // ⑤ 横轴周标签(隔点绘制)
  for (let i = 0; i < n; i += 2) {
    ctx.fillText(LINE_LABELS[i], x, h - 4);
  }
}

drawLine 方法分五步绘制折线图。第一步绘制 3 条横线背景网格,strokeStyle 为分割线色。第二步绘制青柠渐变填充区域——通过 createLinearGradient 创建从 green(顶部不透明青柠绿)到 greenFade(底部几乎透明)的垂直渐变,沿 12 个数据点闭合路径填充,营造数据面积的可视化效果。第三步绘制折线本体——moveTo 到第一个点,后续 lineTo 连接,strokeStyle 青柠绿线宽 2px。第四步绘制数据点——每个点以灰咖填充、青柠绿描边的圆形绘制,末点(最新一周)半径随 breath 在 3.5~4.5px 放大,形成"当前焦点"的视觉强调。第五步隔点绘制横轴 W1~W12 标签,避免 12 个标签拥挤。

15.3 月度柱状图 chartCard

@Builder
chartCard() {
  Column({ space: 10 }) {
    Row() {
      Text('📊 近 6 个月卡路里消耗')...
      Text('单位:千卡')...
    }.width('100%')

    Row({ space: 10 }) {
      ForEach(MONTH_IDX, (idx: number) => {
        Column({ space: 6 }) {
          Column().width('100%').height(this.barHeight(MONTH_CAL[idx]))
            .backgroundColor(idx === MONTH_IDX.length - 1 ? COLORS.orange : COLORS.orangeD)
            .borderRadius(4)
          Text(MONTH_NAME[idx]).fontSize(9).fontColor(COLORS.text3)
        }.layoutWeight(1).alignItems(HorizontalAlign.Center)
      }, (idx: number) => 'month-' + idx.toString())
    }.width('100%').height(110).alignItems(VerticalAlign.Bottom)
  }.width('100%').padding(14).backgroundColor(COLORS.card).borderRadius(12)
}

barHeight(v: number): number {
  const base = Math.round(v / MONTH_MAX * 78);
  return this.breath ? base : Math.round(base * 0.93);
}

月度柱状图采用传统 Column + ForEach 方式渲染(非 Canvas),6 根柱子等宽排列,高度由 barHeight 方法计算——MONTH_CAL[idx] / MONTH_MAX * 78 将卡路里值归一化映射为最大 78px 高度。barHeight 方法内嵌呼吸动画:breath 为 true 时返回基准高度,为 false 时缩小到 93%,形成柱体高度微缩呼吸。末月柱子使用活力橙 orange,其余月份使用深橙 orangeD,同色系深浅对比突出当前月。alignItems(VerticalAlign.Bottom) 让所有柱子底部对齐,形成统一的柱状图基线。

十六、底部 Tab 栏

@Builder
tabBar() {
  Row() {
    ForEach(TAB_LIST, (tab: TabMeta, idx: number) => {
      Column({ space: 3 }) {
        Text(tab.icon).fontSize(20).opacity(this.currentTab === idx && this.breath ? 1 : 0.75)
        Text(tab.label).fontSize(9).fontColor(this.currentTab === idx ? COLORS.tabOn : COLORS.text3)
      }.layoutWeight(1).padding({ top: 8, bottom: 8 })
        .onClick(() => { this.currentTab = idx; })
    }, (tab: TabMeta) => 'tab-' + tab.label)
  }.width('100%').backgroundColor(COLORS.card)
}

底部 Tab 栏使用 Row + ForEach 渲染 6 个 Tab,每个 Tab 通过 layoutWeight(1) 等分宽度。选中 Tab 的图标透明度随 breath 在 1.0~0.75 间波动(呼吸闪烁),标签文字颜色为活力橙 tabOn;未选中 Tab 图标固定 0.75 透明度,标签为灰咖 text3。点击 Tab 调用 this.currentTab = idx 切换内容区,触发 build 重新渲染对应的 @Builder 方法。底部栏背景色为卡片炭咖 card,与页面背景 bg 形成层次区分。

十七、弹窗系统

17.1 遮罩层

@Builder
modalOverlay(onClose: () => void) {
  Column().width('100%').height('100%').backgroundColor(COLORS.mask)
    .onClick(() => { onClose(); })
}

modalOverlay 是三个弹窗共用的全屏遮罩层,使用 rgba(0,0,0,0.6) 半透明黑色覆盖全屏,点击遮罩调用 onClose 回调关闭弹窗。该 Builder 接收闭包参数,实现了弹窗关闭逻辑的复用。

17.2 新建提醒弹窗

@Builder
panelAdd(onClose: () => void) {
  Stack() {
    this.modalOverlay(onClose)
    Column({ space: 12 }) {
      Text('新建训练提醒')...

      Column({ space: 6 }) {
        Text('提醒时间')...
        TextInput({ text: this.formTime, placeholder: '如:07:30' })
          .onChange((value: string) => { this.formTime = value; })
      }.width('100%').alignItems(HorizontalAlign.Start)

      Column({ space: 6 }) {
        Text('提醒标题')...
        TextInput({ text: this.formTitle, placeholder: '如:晨间快走唤醒' })
          .onChange((value: string) => { this.formTitle = value; })
      }.width('100%').alignItems(HorizontalAlign.Start)

      Column({ space: 6 }) {
        Text('重复类型')...
        Row({ space: 8 }) {
          ForEach(REPEAT_TAGS, (tag: string) => {
            Text(tag)...
              .backgroundColor(this.formRepeat === tag ? COLORS.orange : COLORS.dark)
              .onClick(() => { this.formRepeat = tag; })
          }, (tag: string) => 'form-repeat-' + tag)
        }.width('100%')
      }.width('100%').alignItems(HorizontalAlign.Start)

      Row({ space: 10 }) {
        Text('取消').onClick(() => { onClose(); })
        Text('创建').onClick(() => { this.saveRemind(); })
      }.width('100%')
    }.width('78%').padding(16).backgroundColor(COLORS.card).borderRadius(14)
  }.width('100%').height('100%').alignContent(Alignment.Center)
}

新建提醒弹窗使用 Stack 层叠遮罩层和弹窗主体,主体宽度 78% 居中显示。弹窗包含三个表单字段:提醒时间(TextInput)、提醒标题(TextInput)和重复类型(四选 chips)。表单状态绑定 formTimeformTitleformRepeat 三个 @State 变量。底部"取消"按钮调用 onClose 关闭弹窗,"创建"按钮调用 saveRemind 方法——该方法以表单字段创建新 RemindItem(空字段用默认值兜底),push 到 remindList,递增 todayTotal,清空表单并关闭弹窗。

17.3 编辑提醒弹窗

编辑提醒弹窗结构与新建弹窗一致,区别在于表单字段绑定 editTimeeditTitleeditRepeat 三个 @State 变量,且在 openEditRemind 方法中已回填当前提醒的原值。底部"保存修改"按钮调用 updateRemind 方法——该方法更新 editIdx 对应提醒的字段(空输入不覆盖原值),触发 remindList.slice() 刷新数组引用,关闭弹窗。

17.4 删除确认弹窗

@Builder
panelDel(onClose: () => void) {
  Stack() {
    this.modalOverlay(onClose)
    Column({ space: 12 }) {
      Text('删除训练提醒').fontSize(15).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
      Text('确认删除该条训练提醒吗?删除后时间轴将移除该节点,今日计划数同步减少,且不可恢复。')
        .fontSize(10).fontColor(COLORS.sub).maxLines(2)
      Row({ space: 10 }) {
        Text('取消').backgroundColor(COLORS.dark)
          .onClick(() => { onClose(); })
        Text('确认删除').backgroundColor(COLORS.red)
          .onClick(() => { this.delRemind(); })
      }.width('100%')
    }.width('78%').padding(16).backgroundColor(COLORS.card).borderRadius(14)
  }.width('100%').height('100%').alignContent(Alignment.Center)
}

删除确认弹窗是轻量确认对话框,展示删除警示文案。"取消"按钮灰咖底,"确认删除"按钮警示红底,点击确认调用 delRemind 方法——该方法从 remindListsplice 移除 delIdx 对应条目,递减 todayTotal,关闭弹窗。三态弹窗系统通过统一的 Stack + modalOverlay + Column 结构和 onClose 闭包参数,实现了交互模式的一致性和代码复用。

十八、功能模块对比表

Tab功能定位核心数据模型布局特征主题色应用关键交互关联特性
训练今日训练执行与进度可视化WorkoutItem统计三格+进度环+折线图+清单青柠绿进度弧、活力橙卡路里ForEach 清单渲染、breath 联动状态点闪烁Canvas 绘制(进度环+折线图)
课程精选课程浏览与选择CourseItem四列宫格+横滑大卡难度三色编码(绿/橙/红)双布局互补(宫格概览+横滑细看)chunkGrid 预计算分片
提醒训练提醒管理与通知发布RemindItem、NoticeLog授权卡+双按钮+时间轴+历史橙色 Toggle、红绿授权状态CRUD 闭环、Toggle 开关、发布通知Notification Kit(沙箱铃声 sound)
铃音沙箱铃声生成与管理RingItem状态卡+生成器+铃声库橙色 Slider、绿色沙箱状态Slider 调频、chips 选时长、生成落盘Notification Kit(WAV 生成+EL1 写入)
字幕AI 字幕配置与场景推荐CaptionScene预览+语言+字号+颜色+场景橙色选中态、五色色块语言联动、字号切换、颜色选色、场景一键应用Speech Kit(AI 字幕四新字段)
我的身份数据与周报复盘WeekRow渐变大卡+柱状图+清单深橙渐变、橙绿柱体、delta 三色breath 柱高波动、delta 配色Canvas 渐变、柱状图呼吸动画

十九、总结与展望

本文深度解析了"跃动计划"运动健康管理平台的完整 ArkUI 组件化设计。该平台以"夜跑黑 + 活力橙 + 青柠绿"三色体系构建沉浸式深色运动美学,通过 6 个功能各异的 Tab 覆盖了从训练执行到数据复盘的运动健康管理全链路。在技术架构上,平台展现了三个层次的设计智慧。

第一层是色彩与数据模型的设计智慧。 ColorPalette 接口先行、COLORS 常量后随的类型安全色彩体系,配合 statusColorlevelColordeltaColor 等业务语义到视觉色彩的映射函数群,让颜色策略集中可维护。6 个 @Observed 数据模型(WorkoutItem、CourseItem、RemindItem、RingItem、NoticeLog、CaptionScene)覆盖了训练、课程、提醒、铃声、通知、字幕六大业务实体,字段级响应式更新保证了数据变化即时反映到视图。

第二层是三大 HarmonyOS 前沿特性的深度整合智慧。 Notification Kit 的沙箱自定义铃声能力链——从 buildWavBytes 正弦波生成到 saveRingToSandbox EL1 落盘再到 fileUri.getUriFromPath + 'uri::' 前缀拼接——让每条训练提醒都能携带用户自定义的运动激励铃声,突破了传统通知铃声千篇一律的限制。Speech Kit 的 AI 字幕四新字段(sourceLanguage/targetLanguage/fontSize/fontColor)配合 writeAudio 640 字节 PCM 块写入,让外教团课口令不再有语言障碍。Canvas 绘制的进度环 drawRing 和折线图 drawLine 通过 breath 状态联动实现呼吸感动画,赋予数据可视化以生命力。

第三层是组件化与交互模式的设计智慧。 @Builder 拆分了头部、6 个 Tab、统计格、身份格、柱状图、底部栏、遮罩和三态弹窗等十余个 UI 单元,配合 @State 分组声明的 30+ 状态变量,实现了视图与状态的清晰分离。弹窗系统通过 modalOverlay + onClose 闭包参数实现三态统一,时间轴通过固定行高 + layoutWeight 竖线实现节点串联,呼吸动画通过 setInterval + Canvas 手动重绘实现无闪烁渲染——这些细节都体现了对 ArkUI 框架特性的深入理解和工程化实践。

展望未来,该平台可在以下方向持续进阶:一是接入 Health Sensor Kit 实现真实心率、步频、配速等运动数据的实时采集,替代当前的 Mock 数据;二是引入 Distribution Kit 的跨端流转能力,让训练数据在手机、手表、平板间无缝同步;三是利用 Intelligence Kit 的端侧大模型能力,根据用户训练历史自动生成个性化训练计划和智能复盘报告;四是扩展 Canvas 可视化维度,增加配速分布热力图、心率区间饼图、训练负荷雷达图等多维数据视图。随着 HarmonyOS 生态的持续演进,运动健康管理应用将拥有更广阔的创新空间,让每一次跃动都被精准记录、被智能解读、被热情激励。

附录: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.1Release✅ 已安装

界面顶部提示:“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 246.1.1.100Release✅ 已安装
API Version 236.1.0.28Beta1未安装
API Version 226.0.2.112Release未安装

安装路径示例:D:\DevTools\ArkUI-X\sdk

说明:ArkUI-X 允许开发者使用一套 ArkTS 主代码,同时构建多平台应用。如果仅开发 HarmonyOS 原生应用,无需额外安装 ArkUI-X SDK。

在这里插入图片描述


三、小结

步骤操作关键点
创建项目欢迎页 → 新建项目 → 选择 Empty Ability 模板 → 配置项目信息 → 完成使用 Stage 模型 + ArkTS 语言
查看 SDK设置 → HarmonyOS SDKSDK 已内置,无需手动安装
跨平台扩展设置 → ArkUI-X根据需要安装对应 API 版本

至此,DevEco Studio 的项目创建与 SDK 环境确认全部完成,可以开始 HarmonyOS 应用的功能开发。


本文基于 DevEco Studio 6.1.1 Release 版本编写,不同版本界面可能存在细微差异。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐