App3-肌动实验室:健身训练计划生成的 HarmonyOS 开发实践

摘要

「肌动实验室」是 AI40 智能应用工具箱中编号为 3 的健康运动类应用,核心功能是根据用户选择的训练部位、器械类型和训练时长,智能生成一份结构化的健身训练计划,包含动作列表(含组数、次数、休息时间)、总时长预估和训练提示。应用内置四组精确匹配的 Mock 数据场景和一组通用兜底方案,覆盖了从徒手全身训练到弹力带核心训练等多种典型健身场景,同时预留了完整的 AI API 调用桩(Stub)模式,为后续接入云端大模型或鸿蒙原生 AI 能力做好了架构准备。

本文从 6A 工作流(Align-Architect-Atomize-Approve-Automate-Assess)的视角,系统性地记录了该应用从需求对齐到最终交付的完整开发过程。文章深入分析了 ArkTS 严格模式下的类型定义策略、场景键(Scenario Key)精确匹配的 Mock 数据设计、自定义下拉选择器的交互实现、表格化结果展示的架构设计,以及 HarmonyOS 鸿蒙平台特有的编译约束与应对方案。同时,本文对鸿蒙PC端的适配潜力、鸿蒙Flutter框架的跨平台扩展思路进行了前瞻性探讨,为 HarmonyOS 应用开发者提供了一套可复用的生成式 AI 应用开发范式。

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

一、Align 对齐阶段:从模糊需求到精确规范

1.1 应用背景与需求分析

其核心定位是:一个面向健身爱好者的智能训练计划生成器。用户无需具备专业的健身知识,只需选择三个参数——训练部位、可用器械和训练时长——即可获得一份完整的、可执行的训练计划。

与套件中其他应用(如「衣品进化室」的穿搭推荐、「味蕾探险家」的菜单生成)相比,「肌动实验室」具有以下几个显著特征:

第一,数据结构的「表格化」特性。 训练计划的核心输出是一个动作列表,每个动作包含四个维度的信息:动作名称(name)、组数(sets)、次数(reps)和休息时间(rest)。这与穿搭推荐的单体描述文本或菜单生成的卡片式方案在展示方式上有本质区别——它天然适合以表格形式呈现,需要设计专门的表头行和数据行组件。

第二,参数组合的「关联性」较强。 训练部位和器械类型之间并非完全独立:例如,"胸"和"哑铃"的组合可以生成哑铃飞鸟等针对性动作,而"全身"和"徒手"的组合则适合生成开合跳、深蹲等全身性动作。这种关联性意味着 Mock 数据的场景设计需要经过深思熟虑,选择有代表性的组合来覆盖主要的健身场景。

第三,输出内容的「专业性」要求。 训练计划涉及运动科学知识,每个动作的组数、次数、休息时间需要符合运动训练的基本原则。例如,大肌群训练(胸、背、腿)通常需要更长的组间休息时间(60秒),而小肌群或核心训练可以缩短休息时间(30秒)。这些专业细节需要在 Mock 数据设计中体现出来。

第四,交互方式的「下拉选择器」模式。 与 App2 的文本输入和多选标签不同,App3 的交互方式回归到基础的下拉选择器模式。但这里有一个关键区别:App3 实现了完全自定义的下拉选择器,而非使用系统原生 Picker 组件。这是因为系统 Picker 在视觉风格上难以与整体 UI 设计保持一致,且交互方式(滚动轮盘)在健身场景下不够直观——用户更希望看到所有选项一目了然,点击即可选中。

1.2 功能边界确认

经过需求对齐,我们确定了「肌动实验室」的功能边界:

核心功能(in scope):

  • 用户选择训练部位,从 6 个选项中选择一个:胸、背、腿、肩、核心、全身
  • 用户选择器械类型,从 4 个选项中选择一个:徒手、哑铃、器械、弹力带
  • 用户输入训练时长(分钟),使用数字键盘输入
  • 点击"AI 生成"按钮后,系统生成一份训练计划,包含:动作列表(表格形式,含动作名称、组数、次数、休息时间)、总时长预估、训练提示
  • 支持 Loading 状态展示(LoadingProgress 组件 + 文字提示)
  • 支持空状态引导(未生成结果时显示提示文案)

不在范围内(out of scope):

  • 不提供动作视频演示或 GIF 动图(需要多媒体资源支持)
  • 不提供训练历史记录和进度追踪(这是「体型雕刻家」App34 的职责)
  • 不支持用户自定义添加动作或编辑已有动作(保持生成式应用的简洁性)
  • 不提供训练倒计时或计时器功能(这是「心流计时器」App4 的职责)
  • 不涉及营养配餐建议(这是「营养方程式」App33 的职责)
  • 不提供运动损伤风险评估和动作纠正建议(需要视觉识别能力)

交互流程: 参数选择(两个下拉选择器 + 一个文本输入框) --> 点击"AI 生成"按钮 --> 显示 Loading 动画(800ms 模拟延迟) --> 展示结果表格。这是一个标准的"输入 --> 处理 --> 输出"三段式交互,用户操作路径清晰,无需额外引导。

离线策略: 内置 4 组精确匹配的 Mock 数据场景,加上 1 组通用兜底方案。4 组精确匹配场景为:

  • 场景一:全身 + 徒手(最通用的健身场景,适合居家健身)
  • 场景二:胸 + 哑铃(针对性力量训练,是健身房最常见的训练组合之一)
  • 场景三:腿 + 器械(器械训练安全性高,适合初学者)
  • 场景四:核心 + 弹力带(功能性训练,强调核心稳定性和控制力)

这 4 个场景覆盖了 4 种器械的完整枚举,同时覆盖了 6 种训练部位中最重要的 4 种(全身、胸、腿、核心)。兜底方案使用通用模板(热身 + 主项动作 + 辅助动作 + 冷却拉伸),确保任何参数组合都能获得可用的训练计划。

1.3 ArkTS 严格模式下的技术约束

在 Align 阶段,必须明确 ArkTS 严格模式对 App3 开发实践的具体影响。以下是本应用需要特别关注的技术约束:

约束项 说明 对 App3 的影响
禁止 any/unknown 类型 所有类型必须显式声明 定义 ExerciseItemRoutineOutput 两个 interface,所有字段显式标注类型
禁止索引访问 obj["key"] 必须使用 obj.key 语法 访问 outputData.routine 时使用点语法,不能使用 outputData["routine"]
禁止解构赋值/解构声明 必须使用临时变量逐字段操作 generateMockData() 中,不能使用 const { part, equipment } = this,必须逐字段赋值
禁止 var 关键字 必须使用 let 所有局部变量使用 let 声明
禁止 for..in 循环 必须使用常规 for 循环 遍历 routine 数组时使用 ForEach 组件
禁止函数表达式 必须使用箭头函数 所有回调使用 (): void => {} 形式
禁止嵌套函数 必须使用 lambda(箭头函数) generateMockData() 内部不使用嵌套 function,全部使用箭头函数
Scroll 组件单子节点约束 Scroll 内只能有一个直接子组件 使用 Column 包裹所有可滚动内容
@State 属性名约束 不能与 ArkUI 内置属性名冲突 避免使用 heightwidthcolor 等作为状态变量名
禁止 includes 不可靠使用 数组方法在 ArkTS 中可能受限 在自定义下拉选择器中,使用显式判断而非数组方法
禁止 as const 断言 必须使用字面量显式类型标注 所有常量数组使用 string[] 显式类型标注

特别值得关注的是自定义下拉选择器的实现约束。在 ArkTS 中,由于不支持动态布局变更(对象的布局在编译时已知且运行时不可更改),我们无法像 Web 前端那样通过 v-if + CSS position: absolute 叠加层来实现下拉菜单。ArkUI 的解决方案是通过条件渲染(if 语句)动态插入/移除下拉选项面板,利用 Column 组件的流式布局实现选项的展开和收起。这种方式的优势在于与 ArkUI 声明式范式完全兼容,无需额外的定位计算;劣势在于选项面板的插入会推动下方内容下移,而非 Web 端常见的浮层覆盖效果。这是一个需要与 UI 设计师沟通确认的交互决策点。

1.4 验收标准

基于以上分析,制定以下可测试的验收标准:

编号 验收项 验收标准 验证方式 优先级
AC-01 训练部位选择 6 个选项可正常展示、点击选中、视觉反馈正确(蓝色文字 + 对勾 + 高亮背景) 逐个点击验证 P0
AC-02 器械类型选择 4 个选项可正常展示、点击选中、视觉反馈正确 逐个点击验证 P0
AC-03 训练时长输入 支持数字输入,TextInput 的 type 为 InputType.Number 输入数字验证 P0
AC-04 下拉选择器交互 点击展开/收起,选中后自动收起,两个选择器互不影响 交替操作验证 P0
AC-05 训练计划生成 点击生成按钮后,根据参数不同返回不同的训练计划 4 组场景参数测试 P0
AC-06 结果完整性 包含动作列表(≥5 个动作)、总时长、训练提示 结果展示验证 P0
AC-07 表格展示 动作名称、组数、次数、休息时间四列对齐,斑马纹行背景 视觉检查 P1
AC-08 Loading 状态 生成过程中显示 LoadingProgress 组件 + “AI正在生成训练计划…” 文字 点击后立即检查 P1
AC-09 空状态提示 未生成结果时显示 “请选择训练部位、器械和时长后点击生成” 引导文案 初始状态检查 P1
AC-10 返回导航 点击"← 返回"可正常回到首页 router.back() 验证 P0
AC-11 ArkTS 编译通过 零编译错误,通过 DevEco Studio 构建 hvigor 构建 P0
AC-12 AI 接口预留 代码中存在完整的 callAIAPI() 注释方法,取消注释后可直接使用 代码审查 P2
AC-13 离线可用 无网络环境下正常生成结果 飞行模式测试 P1

二、Architect 架构阶段:从共识到系统设计

2.1 整体架构概览

「肌动实验室」采用 HarmonyOS 单页面架构,遵循 ArkUI 声明式 UI 范式。整体架构分为三层:

┌──────────────────────────────────────────────────────────────────┐
│                      UI Layer (View)                              │
│  ┌──────────┐  ┌──────────────┐  ┌──────────────┐  ┌──────────┐ │
│  │ 返回导航栏 │  │ 参数选择区    │  │ 生成按钮      │  │ 结果展示区│ │
│  │ (Row)     │  │ (Column)     │  │ (Button)     │  │ (Column)  │ │
│  │           │  │              │  │              │  │           │ │
│  │ ← 返回    │  │ 训练部位/器械 │  │ [AI 生成]    │  │ 总时间     │ │
│  │ 肌动实验室 │  │ 训练时长     │  │              │  │ 动作表格   │ │
│  │           │  │              │  │              │  │ 训练提示   │ │
│  └──────────┘  └──────────────┘  └──────────────┘  └──────────┘ │
├──────────────────────────────────────────────────────────────────┤
│                    State Layer (ViewModel)                        │
│  ┌───────────────┐  ┌──────────────┐  ┌───────────────────────┐ │
│  │ @State selected│  │ @State output│  │ @State isLoading      │ │
│  │ Part/Equipment│  │ Data         │  │ @State hasResult      │ │
│  │ @State minutes │  │ (RoutineOutput│  │                       │ │
│  │               │  │  | null)      │  │                       │ │
│  └───────────────┘  └──────────────┘  └───────────────────────┘ │
├──────────────────────────────────────────────────────────────────┤
│                     Data Layer (Model)                            │
│  ┌──────────────┐  ┌───────────────────────────────────────────┐ │
│  │ ExerciseItem  │  │ generateMockData()                        │ │
│  │ interface     │  │ - Scenario Key 精确匹配                    │ │
│  │               │  │ - 4 组精确场景 + 1 组兜底                  │ │
│  │ RoutineOutput │  │                                           │ │
│  │ interface     │  │ AI API Stub: callAIAPI() [注释]            │ │
│  └──────────────┘  └───────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘

UI Layer(视图层): 负责所有视觉元素的渲染,包括返回导航栏、参数选择区(两个自定义下拉选择器 + 训练时长输入框)、生成按钮、Loading 状态展示、结果展示区(总时间卡片、动作表格、训练提示)。所有 UI 组件通过 build() 方法中的声明式代码组织,使用 if 条件渲染控制不同状态下的 UI 展示。

State Layer(视图模型层): 使用 @State 装饰器管理五个核心状态变量:selectedPart(训练部位)、selectedEquipment(器械类型)、minutes(训练时长)、outputData(生成结果,类型为 RoutineOutput | null)、isLoading(加载状态)、hasResult(是否已有结果)。状态变量的变化自动触发 UI 的重新渲染。

Data Layer(数据层): 包含两个接口定义(ExerciseItemRoutineOutput)和两个核心方法(generateMockData()onGenerate())。generateMockData() 实现了场景键精确匹配的 Mock 数据策略,通过 if-else 链式判断匹配用户参数并返回对应的预置数据。onGenerate() 作为生成流程的调度器,负责设置 Loading 状态、触发 Mock 数据生成、并在 setTimeout 回调中更新 UI 状态。

2.2 数据接口设计

App3 的数据模型非常简洁,核心是两个接口:

interface ExerciseItem {
  name: string;    // 动作名称,如 "俯卧撑"、"开合跳"、"哑铃卧推"
  sets: string;    // 训练组数,如 "3组"、"4组"、"1组"
  reps: string;    // 每组次数/时长,如 "15次"、"30秒"、"力竭"、"每侧12次"
  rest: string;    // 组间休息时间,如 "30秒"、"45秒"、"60秒"、"-"
}

interface RoutineOutput {
  routine: ExerciseItem[];   // 动作列表
  total_min: string;          // 总时长预估,如 "约30分钟"、"约35分钟"
  tips: string;               // 训练提示,包含动作标准和注意事项
}

这两个接口设计体现了几个值得注意的决策:

字段类型全部使用 string 而非 number 这是经过深思熟虑的选择。表面上看,sets(组数)和 rest(休息时间)似乎更适合使用 number 类型,但实际数据中存在大量非纯数字的情况:组数中有"3组"、“4组"这样带单位的字符串,次数中有"力竭”(表示做到力竭为止)、“每侧12次”(不对称动作)、“30秒”(以时间为单位而非次数)、“5分钟”(拉伸动作),休息时间中有"-"(表示无组间休息或拉伸动作不适用)。这些非标数据使得 string 成为唯一合理的选择——它既承载了数据值,也承载了展示单位,避免了在 UI 层进行繁琐的类型转换和单位拼接。

不使用 number | string 的联合类型。 在标准 TypeScript 中,我们可能会使用 reps: number | string 这种联合类型来处理"12次"和"力竭"的不同场景。但在 ArkTS 的严格模式下,联合类型虽然可用,但会增加代码复杂度——每次访问 reps 字段时都需要进行类型守卫判断。使用统一的 string 类型简化了数据模型,也简化了 UI 层的渲染逻辑,所有字段都可以直接通过 Text 组件展示。

RoutineOutput 使用 | null 而非可选属性。 我们将 outputData 的类型定义为 RoutineOutput | null 而非 RoutineOutput | undefined,这是因为 ArkTS 不推荐使用 undefined(对象布局在编译时确定,删除属性无意义)。使用 null 作为"无数据"的哨兵值,在模板中通过 if (this.outputData !== null) 进行条件渲染,这是一种更符合 ArkTS 语义的做法。

tips 字段的设计考量。 训练提示(tips)是一个富文本字符串,包含了针对特定训练场景的动作标准、安全提示和恢复建议。我们没有将其拆分为数组(如 tips: string[]),因为提示内容通常是一个连贯的段落,拆分为数组反而会破坏阅读的流畅性。在 UI 展示中,我们将 tips 放在一个带有暖黄色背景(#FFF9E6)的卡片中,与训练计划表格形成视觉上的区分——表格是"怎么做"(how),提示是"为什么"和"注意什么"(why and caution)。

2.3 状态管理设计

App3 的状态管理基于 @State 装饰器,共定义了 6 个状态变量和 2 个实例变量:

@State selectedPart: string = '全身';       // 当前选中的训练部位
@State selectedEquipment: string = '徒手';  // 当前选中的器械类型
@State minutes: string = '30';              // 训练时长(分钟)
@State outputData: RoutineOutput | null = null;  // 生成结果
@State isLoading: boolean = false;          // 是否正在加载
@State hasResult: boolean = false;          // 是否已有结果

private partOptions: string[] = ['胸', '背', '腿', '肩', '核心', '全身'];
private equipmentOptions: string[] = ['徒手', '哑铃', '器械', '弹力带'];
private showPartPicker: boolean = false;    // 部位选择器是否展开
private showEquipmentPicker: boolean = false;  // 器械选择器是否展开

状态变量分类:

  • 参数输入状态selectedPartselectedEquipmentminutes):这三个变量代表用户的输入参数,任何变化都会立即反映在 UI 上——下拉选择器显示当前选中值,TextInput 显示当前输入值。
  • 结果展示状态outputDatahasResult):这两个变量控制结果区域的显隐和内容。hasResult 用于区分"未生成结果"和"已生成结果"两种状态,而 outputData 携带具体的数据内容。
  • 加载状态isLoading):控制 Loading 动画的显隐,在生成过程中为 true,生成完成后为 false
  • UI 交互状态showPartPickershowEquipmentPicker):这两个变量使用 private(非 @State)修饰,因为它们的变化不需要触发全局 UI 重新渲染——它们只影响下拉选项面板的显隐,而 ArkUI 的 if 条件渲染可以通过条件表达式的值变化自动更新局部视图。

为什么 showPartPickershowEquipmentPicker 使用 private 而非 @State 这是一个关键的技术决策。在 ArkUI 中,@State 装饰的变量变化会触发整个 build() 方法的重新执行,而 private 变量的变化不会触发。对于下拉选择器这种局部交互,只需要重新渲染选项面板所在的那一小块区域即可,不需要触发整棵 UI 树的重新构建。实际上,由于 ArkUI 的 if 条件渲染在表达式值变化时会自动更新对应的局部视图,private 变量完全可以驱动这种局部条件渲染。使用 private 而非 @State 减少了不必要的重渲染,对性能有正向影响。

状态流转图:

初始状态                              用户操作
┌──────────────┐                  ┌──────────────┐
│ isLoading:   │                  │ 选择训练部位  │
│   false      │─────────────────>│ 选择器械类型  │
│ hasResult:   │                  │ 输入训练时长  │
│   false      │                  └──────┬───────┘
│ outputData:  │                         │
│   null       │                  ┌──────▼───────┐
└──────────────┘                  │ 点击"AI生成"  │
       ▲                          └──────┬───────┘
       │                                 │
       │                          ┌──────▼───────┐
       │                          │ isLoading:   │
       │                          │   true       │
       │                          │ hasResult:   │
       │                          │   false      │
       │                          │ 显示Loading  │
       │                          └──────┬───────┘
       │                                 │ setTimeout 800ms
       │                          ┌──────▼───────┐
       │                          │ generateMock │
       │                          │ Data()       │
       │                          └──────┬───────┘
       │                                 │
       │                          ┌──────▼───────┐
       └──────────────────────────│ isLoading:   │
         用户修改参数后重新生成     │   false      │
                                  │ hasResult:   │
                                  │   true       │
                                  │ outputData:  │
                                  │   Routine    │
                                  │ 显示结果表格  │
                                  └──────────────┘

2.4 UI 组件树设计

App3 的组件树结构如下:

App3 (根组件)
├── Column (根容器)
│   ├── Row (顶部导航栏)
│   │   ├── Text("← 返回") + onClick(router.back)
│   │   ├── Text("肌动实验室") [标题]
│   │   └── Text("") [占位,保持标题居中]
│   │
│   └── Scroll (可滚动区域)
│       └── Column (Scroll 的唯一子组件)
│           ├── Text("训练部位") [标签]
│           ├── Row (部位选择器 - 触发器)
│           │   ├── Text(selectedPart) [当前选择]
│           │   └── Text("▼") [下拉箭头]
│           ├── [if showPartPicker] Column (部位选项面板)
│           │   └── ForEach(partOptions) -> Row (选项行)
│           │       ├── Text(选项名称)
│           │       └── [if 选中] Text("✓") [对勾]
│           │
│           ├── Text("器械选择") [标签]
│           ├── Row (器械选择器 - 触发器)
│           │   ├── Text(selectedEquipment) [当前选择]
│           │   └── Text("▼") [下拉箭头]
│           ├── [if showEquipmentPicker] Column (器械选项面板)
│           │   └── ForEach(equipmentOptions) -> Row (选项行)
│           │       ├── Text(选项名称)
│           │       └── [if 选中] Text("✓") [对勾]
│           │
│           ├── Text("训练时长(分钟)") [标签]
│           ├── TextInput (数字输入框)
│           │
│           ├── Row (按钮容器)
│           │   └── Button("AI 生成") + onClick(onGenerate)
│           │
│           ├── [if isLoading] Column (Loading 状态)
│           │   ├── LoadingProgress()
│           │   └── Text("AI正在生成训练计划...")
│           │
│           ├── [if hasResult && outputData !== null] Column (结果区域)
│           │   ├── Text("训练计划") [标题]
│           │   ├── Row (总时间卡片)
│           │   │   └── Text("总计时间:" + total_min)
│           │   ├── Column (动作表格)
│           │   │   ├── Row (表头行)
│           │   │   │   ├── Text("动作名称")
│           │   │   │   ├── Text("组数")
│           │   │   │   ├── Text("次数")
│           │   │   │   └── Text("休息")
│           │   │   └── ForEach(routine) -> Row (数据行)
│           │   │       ├── Text(name) [斑马纹背景]
│           │   │       ├── Text(sets)
│           │   │       ├── Text(reps)
│           │   │       └── Text(rest)
│           │   ├── Text("训练提示") [标签]
│           │   └── Text(tips) [暖黄色背景卡片]
│           │
│           └── [else if !isLoading] Text("请选择训练部位...") [空状态提示]

2.5 数据流设计

App3 的数据流是典型的单向数据流(Unidirectional Data Flow),遵循"用户操作 --> 状态更新 --> UI 重新渲染"的模式:

用户点击选项
    │
    ▼
onClick 回调
    │
    ├──> this.selectedPart = item        (更新状态)
    ├──> this.showPartPicker = false     (更新状态)
    │
    ▼
ArkUI 检测到 @State 变量变化
    │
    ▼
build() 方法重新执行
    │
    ├──> Text(this.selectedPart) 显示新的选中值
    ├──> showPartPicker 为 false,选项面板消失
    │
    ▼
UI 更新完成

生成流程的数据流:

用户点击 "AI 生成" 按钮
    │
    ▼
onGenerate() 被调用
    │
    ├──> this.isLoading = true
    ├──> this.hasResult = false
    │
    ▼
build() 重新执行,显示 LoadingProgress
    │
    ▼
setTimeout 800ms 后执行回调
    │
    ├──> this.generateMockData()
    │     ├──> 读取 this.selectedPart
    │     ├──> 读取 this.selectedEquipment
    │     ├──> 场景键匹配 (if-else 链)
    │     └──> this.outputData = 匹配的预置数据
    │
    ├──> this.isLoading = false
    ├──> this.hasResult = true
    │
    ▼
build() 重新执行,显示结果表格

关键数据流原则:

  1. 状态是唯一真相源(Single Source of Truth)。 UI 展示的内容完全由 @State 变量决定,不存在 UI 层面直接操作 DOM 的情况。
  2. 状态更新是原子的。 每次用户操作(点击选项、点击生成按钮)只触发一次状态更新,通过事件回调集中处理。
  3. 数据派生而非存储。 总时间卡片中的"总计时间"文本是 this.outputData.total_min 的直接引用,而非再次计算;斑马纹背景色通过 index % 2 === 0 在渲染时计算,而非存储在数据中。

三、Atomize 原子化阶段:任务拆解与关键实现

3.1 原子任务拆解

将 App3 的开发任务拆解为以下原子任务,按依赖关系排序:

T1: 定义数据接口 ExerciseItem 和 RoutineOutput
    └──> T2: 定义 @State 状态变量和实例变量
         ├──> T3: 实现自定义下拉选择器(部位选择器)
         │    ├──> T3a: 部件触发器 Row(显示当前值 + ▼)
         │    ├──> T3b: 选项面板 Column + ForEach
         │    └──> T3c: 选项选中逻辑 + 视觉反馈
         ├──> T4: 实现自定义下拉选择器(器械选择器)
         │    └──> (复用 T3 的模式)
         ├──> T5: 实现训练时长输入框 TextInput
         ├──> T6: 实现 generateMockData() 方法
         │    ├──> T6a: 场景一:全身 + 徒手
         │    ├──> T6b: 场景二:胸 + 哑铃
         │    ├──> T6c: 场景三:腿 + 器械
         │    ├──> T6d: 场景四:核心 + 弹力带
         │    └──> T6e: 兜底通用场景
         ├──> T7: 实现 onGenerate() 方法(含 Loading 控制)
         ├──> T8: 实现 Loading 状态 UI
         ├──> T9: 实现结果表格 UI(含表头 + 数据行 + 斑马纹)
         ├──> T10: 实现训练提示卡片 UI
         ├──> T11: 实现空状态引导 UI
         ├──> T12: 实现返回导航栏
         ├──> T13: 预留 AI API 调用桩(注释代码)
         └──> T14: aboutToAppear() 生命周期初始化

3.2 Mock 数据设计策略:场景键精确匹配

App3 的 Mock 数据策略采用了场景键(Scenario Key)精确匹配模式,这是一种在 AI40 工具箱中广泛使用的离线数据策略。其核心思想是:将用户选择的所有参数按照固定顺序拼接为一个逻辑组合,然后在预置的场景数组中精确匹配,匹配成功则返回对应的预置数据,匹配失败则回退到兜底数据。

App3 的场景键设计:

场景键 = selectedPart + " + " + selectedEquipment

场景一: "全身 + 徒手"  → 7 个动作的全身体能训练
场景二: "胸 + 哑铃"    → 6 个动作的针对性力量训练
场景三: "腿 + 器械"    → 7 个动作的器械安全训练
场景四: "核心 + 弹力带" → 7 个动作的功能性训练
兜底:  其他所有组合    → 6 个动作的通用模板

匹配逻辑实现:

generateMockData(): void {
  const part: string = this.selectedPart;
  const equipment: string = this.selectedEquipment;

  if (part === '全身' && equipment === '徒手') {
    // 场景一:全身徒手训练
    this.outputData = { /* ... */ };
  } else if (part === '胸' && equipment === '哑铃') {
    // 场景二:胸部哑铃训练
    this.outputData = { /* ... */ };
  } else if (part === '腿' && equipment === '器械') {
    // 场景三:腿部器械训练
    this.outputData = { /* ... */ };
  } else if (part === '核心' && equipment === '弹力带') {
    // 场景四:核心弹力带训练
    this.outputData = { /* ... */ };
  } else {
    // 兜底通用场景
    this.outputData = { /* ... */ };
  }
}

场景选择的策略分析:

选择这 4 个场景作为精确匹配,而非随机选择,是有其设计逻辑的:

  1. 场景一(全身 + 徒手):这是最"通用"的健身场景。徒手训练不需要任何器械,适合居家健身、出差旅行等场景。全身训练覆盖了大肌群和核心肌群,是最基础的训练模式。选择这个场景作为入口场景,符合"最低门槛"的产品设计原则——任何用户都可以立即开始。

  2. 场景二(胸 + 哑铃):胸肌训练是力量训练中最受欢迎的板块之一,哑铃则是最常见的家庭健身器械。这个场景面向有一定健身基础的用户,展示了哑铃训练的多样性和针对性。

  3. 场景三(腿 + 器械):腿部训练是健身中"又爱又恨"的部分,但器械训练(如倒蹬机、腿弯举机)大大降低了技术门槛和受伤风险。这个场景面向愿意去健身房的用户,展示了器械训练的安全性和便利性。

  4. 场景四(核心 + 弹力带):核心训练和弹力带训练是功能性训练的代表,强调动作控制和身体稳定性。这个场景面向有一定训练经验的用户,展示了弹力带的多样用途。

场景覆盖的完整性分析:

总共有 6 个部位 × 4 种器械 = 24 种组合。4 个精确场景 + 1 个兜底场景的实际覆盖情况:

  • 精确匹配覆盖:4 种组合(16.7%)
  • 兜底覆盖:20 种组合(83.3%)
  • 整体覆盖:24 种组合(100%)

虽然精确匹配覆盖比例不高(16.7%),但被覆盖的 4 种组合是最高频的使用场景。兜底场景虽然给出了通用模板,但模板中包含了"热身运动"、“主项动作”、“辅助动作”、"冷却拉伸"等通用结构,适用于任何训练场景,确保用户不会获得"无匹配"的错误结果。

3.3 自定义下拉选择器的实现细节

App3 最核心的交互组件是自定义下拉选择器。它在 ArkUI 中的实现方式与 Web 前端有显著差异,主要体现在以下几个方面:

触发器的设计:

Row() {
  Text(this.selectedPart)
    .fontSize(14)
    .fontColor('#333333')
    .layoutWeight(1)
  Text('▼')
    .fontSize(12)
    .fontColor('#8E8E93')
}
.width('100%')
.padding({ left: 12, right: 12, top: 10, bottom: 10 })
.backgroundColor('#FFFFFF')
.borderRadius(8)
.border({ width: 1, color: '#D1D1D6' })
.onClick((): void => {
  this.showPartPicker = !this.showPartPicker;
})

触发器是一个 Row 组件,左侧显示当前选中的值,右侧显示三角形箭头(▼)。点击触发器时,切换 showPartPicker 的布尔值,从而控制选项面板的显隐。

选项面板的设计:

if (this.showPartPicker) {
  Column() {
    ForEach(this.partOptions, (item: string): void => {
      Row() {
        Text(item)
          .fontSize(14)
          .fontColor(item === this.selectedPart ? '#007AFF' : '#333333')
          .layoutWeight(1)
        if (item === this.selectedPart) {
          Text('✓')
            .fontSize(16)
            .fontColor('#007AFF')
        }
      }
      .width('100%')
      .padding({ left: 12, right: 12, top: 10, bottom: 10 })
      .backgroundColor(item === this.selectedPart ? '#F0F8FF' : '#FFFFFF')
      .onClick((): void => {
        this.selectedPart = item;
        this.showPartPicker = false;
      })
    })
  }
  .width('100%')
  .backgroundColor('#FFFFFF')
  .borderRadius(8)
  .border({ width: 1, color: '#D1D1D6' })
  .margin({ top: 4 })
}

选项面板的设计细节:

  1. 条件渲染:通过 if (this.showPartPicker) 控制选项面板的显隐。当 showPartPickertrue 时,面板作为一个 Column 插入到触发器下方,推动下方内容下移。
  2. 选中状态高亮:当前选中的选项使用蓝色文字(#007AFF)、浅蓝色背景(#F0F8FF)和右侧对勾(✓)三个视觉元素来标识选中状态。未选中的选项使用默认的灰色文字(#333333)和白色背景。
  3. 选中后自动收起:在选项的 onClick 回调中,同时执行 this.selectedPart = item(更新选中值)和 this.showPartPicker = false(收起面板),确保用户选择一个选项后面板自动关闭,无需额外操作。
  4. 面板与触发器之间的视觉连接:面板的 borderRadius 为 8,与触发器保持一致;面板的 margin({ top: 4 }) 在触发器和面板之间创建了 4px 的间距,视觉上区分了"触发器"和"面板"两个区域。

与 Web 前端下拉选择器的对比:

特性 Web 前端(CSS) ArkUI(App3 实现)
定位方式 position: absolute + z-index 浮层 流式布局,面板插入到内容流中
遮罩层 点击遮罩层关闭(可选) 无遮罩层,点击选项后自动关闭
动画 CSS transition/animation 控制展开/收起 条件渲染,无过渡动画(可后续通过 animateTo 增强)
渲染性能 选项始终在 DOM 中,通过 visibility/display 控制 条件渲染,面板不存在时完全不渲染,节省内存
多个选择器 Tab 键切换焦点 通过独立的 showPartPickershowEquipmentPicker 控制

3.4 结果表格的斑马纹实现

结果表格的斑马纹效果通过 ForEachindex 参数实现:

ForEach(this.outputData.routine, (item: ExerciseItem, index: number): void => {
  Row() {
    // ... 表格单元格内容
  }
  .width('100%')
  .padding({ left: 12, right: 12, top: 10, bottom: 10 })
  .backgroundColor(index % 2 === 0 ? '#FFFFFF' : '#FAFAFA')
})

index % 2 === 0 判断当前行是否为偶数行:偶数行使用白色背景(#FFFFFF),奇数行使用浅灰色背景(#FAFAFA)。这种交替的背景色可以有效提高表格的可读性,特别是在行数较多时(7-8 行),帮助用户快速区分不同行,减少视觉疲劳。

表头与数据行的区分:

表头行使用独立的背景色(#F5F5F5),圆角只应用于顶部(borderRadius({ topLeft: 8, topRight: 8 })),与数据行的圆角形成视觉上的连续性。表头的文字颜色(#8E8E93)也比数据行(#333333)更浅,形成"标题-内容"的层级关系。

3.5 AI API 调用桩模式

App3 在代码中预留了完整的 AI API 调用桩(Stub),以注释形式存在:

// async callAIAPI(): Promise<void> {
//   const response = await fetch('https://api.example.com/workout', {
//     method: 'POST',
//     header: { 'Content-Type': 'application/json' },
//     extraData: JSON.stringify({
//       part: this.selectedPart,
//       equipment: this.selectedEquipment,
//       minutes: parseInt(this.minutes)
//     })
//   });
//   const result: RoutineOutput = await response.json();
//   this.outputData = result;
// }

这个桩代码的设计体现了几个关键考虑:

  1. 使用 async/await 而非 Promise.then():在 ArkTS 中,async/await 是推荐的异步处理方式,不支持生成器函数。
  2. 显式返回类型 Promise<void>:ArkTS 要求显式标注函数返回类型,特别是在涉及异步操作时。
  3. parseInt(this.minutes) 的类型转换this.minutesstring 类型(因为 TextInput 返回的是字符串),但 AI API 期望的是数字类型,因此需要显式转换。
  4. fetch API 的使用:HarmonyOS 提供了 @ohos.net.http 模块用于网络请求,但 fetch 是更通用的 Web API。在实际对接时,可能需要替换为 HarmonyOS 原生的 HTTP 请求 API。
  5. extraData 字段:这是 HarmonyOS 的 fetch API 特有的字段名,用于传递请求体,等价于标准 Web API 中的 body

Mock 数据到 AI 接口的切换策略:

当需要从 Mock 数据切换到真实 AI 接口时,只需修改 onGenerate() 方法:

// 当前版本:使用 Mock 数据
onGenerate(): void {
  this.isLoading = true;
  this.hasResult = false;
  setTimeout((): void => {
    this.generateMockData();
    this.isLoading = false;
    this.hasResult = true;
  }, 800);
}

// 未来版本:使用 AI 接口
// onGenerate(): void {
//   this.isLoading = true;
//   this.hasResult = false;
//   this.callAIAPI().then((): void => {
//     this.isLoading = false;
//     this.hasResult = true;
//   }).catch((err: Error): void => {
//     console.error('AI API call failed: ' + err.message);
//     this.isLoading = false;
//     // 回退到 Mock 数据
//     this.generateMockData();
//     this.hasResult = true;
//   });
// }

四、Approve 审批阶段:ArkTS 合规审计与代码质量审查

4.1 ArkTS 严格模式合规审计

对 App3 的源代码进行逐项 ArkTS 合规审计,结果如下:

审计项 规则要求 代码现状 合规状态
禁止 any/unknown 所有类型必须显式声明 所有接口字段、方法参数、返回值均有显式类型标注 通过
禁止解构赋值 必须逐字段赋值 const part: string = this.selectedPart 使用逐字段赋值 通过
禁止 var 必须使用 let 所有变量声明使用 letconst 通过
禁止 for..in 必须使用 for 循环或 ForEach 数组遍历全部使用 ForEach 组件 通过
禁止索引访问 必须使用 obj.field 所有属性访问使用点语法,如 this.outputData.routine 通过
禁止函数表达式 必须使用箭头函数 所有回调使用 (): void => {} 形式 通过
禁止嵌套函数 必须使用 lambda 未使用嵌套 function 通过
禁止 as const 必须使用显式类型 未使用 as const,常量数组使用 string[] 显式标注 通过
禁止 typeof 用于类型标注 必须显式声明 未使用 typeof 类型标注 通过
禁止 is 运算符 必须使用 instanceof 未使用 is 运算符 通过
Scroll 单子组件 Scroll 内只能有一个直接子组件 Scroll 内只有一个 Column 作为直接子组件 通过
禁止接口方法签名冲突 同名方法不可区分仅靠返回类型 两个 interface 的方法名均不冲突 通过
禁止对象字面量直接用作类型 必须显式声明 class 或 interface ExerciseItemRoutineOutput 均为显式 interface 通过
显式返回类型 函数返回类型必须显式标注 所有方法均标注了返回类型(voidPromise<void> 通过

4.2 代码质量审查

代码结构分析:

App3 的源代码共 432 行,结构清晰,分为以下几个区域:

  • 第 1 行:import 语句
  • 第 3-14 行:接口定义(ExerciseItem、RoutineOutput)
  • 第 16-36 行:组件装饰器、状态变量和实例变量声明
  • 第 38-112 行:generateMockData() 方法(75 行,占代码总量的 17.4%)
  • 第 114-126 行:AI API 调用桩(注释)
  • 第 128-136 行:onGenerate() 方法
  • 第 138-431 行:build() 方法(294 行,占代码总量的 68.1%)

Mock 数据代码密集度分析:

generateMockData() 方法虽然只有 75 行,但包含了 4 组完整场景数据 + 1 组兜底数据,每组数据包含 6-8 个动作。如果未来需要扩展更多场景,该方法会迅速膨胀。可以考虑将 Mock 数据抽取为独立的数据常量,通过场景键映射来获取:

// 未来重构方向
const MOCK_SCENARIOS: Record<string, RoutineOutput> = {
  '全身_徒手': { /* ... */ },
  '胸_哑铃': { /* ... */ },
  '腿_器械': { /* ... */ },
  '核心_弹力带': { /* ... */ }
};

generateMockData(): void {
  const key: string = this.selectedPart + '_' + this.selectedEquipment;
  this.outputData = MOCK_SCENARIOS[key] || DEFAULT_SCENARIO;
}

但由于 ArkTS 对 Record 类型的支持有限,且 Record<K, V> 的索引表达式 rec[index] 的类型为 V | undefined,需要额外的类型守卫,因此当前版本使用 if-else 链式判断是更安全和直接的选择。

build() 方法复杂度分析:

build() 方法包含 294 行代码,是代码总量最大的部分。这反映了 ArkUI 声明式 UI 的一个特点:UI 描述代码通常比逻辑代码更冗长。但 294 行的方法仍然过长,建议通过 @Builder 方法将可复用的 UI 片段提取出来,例如:

// 提取下拉选择器为 @Builder 方法
@Builder
DropdownSelector(label: string, selectedValue: string, 
                 options: string[], isOpen: boolean, 
                 onToggle: () => void, onSelect: (item: string) => void) {
  // ... 下拉选择器的完整 UI 代码
}

这样可以将 build() 方法中的两个下拉选择器(约 80 行代码)压缩为两行 this.DropdownSelector(...) 调用。

4.3 性能分析

重渲染范围分析:

在 ArkUI 中,@State 变量的任何变化都会触发整个 build() 方法的重新执行。在 App3 中,主要的性能关注点在于:

  1. 下拉选择器展开/收起时的重渲染。showPartPickershowEquipmentPicker 变化时,由于这两个变量是 private 而非 @State,它们的变化不会触发 build() 全局重新执行。这是一个正确的性能优化决策。

  2. 结果表格的 ForEach 渲染。outputData 变化时,ForEach 会重新渲染整个表格。由于表格只有 6-8 行,每行只有 4 个 Text 组件,渲染开销很小,不需要额外的性能优化。

  3. Loading 与结果的状态切换。 isLoadingtrue 变为 falsehasResult 变为 true 时,会触发一次完整的 build() 重新执行。这涉及 Loading 组件的移除和结果表格的插入,是正常的 UI 更新开销。

未使用的性能优化点:

  • renderGroup(true):虽然训练计划表格和选项面板都不涉及复杂动画,但如果未来需要为下拉选择器添加展开/收起动画,可以考虑对选项面板设置 renderGroup(true) 以减少渲染批次。
  • @State@Prop 的权衡:当前版本没有使用 @Prop,因为所有状态都在同一个组件中管理。如果未来将下拉选择器抽取为独立组件,需要使用 @Prop 传递选中值和 @Link 双向绑定状态。

五、Automate 自动化执行阶段:关键 UI 实现细节

5.1 返回导航栏的实现

返回导航栏采用经典的三段式 Row 布局:

Row() {
  Text('← 返回')
    .fontSize(16)
    .fontColor('#007AFF')
    .onClick((): void => { router.back(); })
  Text('肌动实验室')
    .fontSize(18)
    .fontWeight(FontWeight.Bold)
    .layoutWeight(1)
    .textAlign(TextAlign.Center)
  Text('').width(60)
}
.width('100%')
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
.backgroundColor('#FFFFFF')

设计要点:

  • 左侧:返回按钮,使用 iOS 风格的蓝色文字(#007AFF),点击后调用 router.back() 返回上一页
  • 中间:应用标题,使用 layoutWeight(1) 占据剩余空间,文字居中显示
  • 右侧:一个宽度为 60px 的空白 Text,用于保持标题在视觉上的居中。60px 是一个经验值,与左侧返回按钮的预估宽度(约 60px)相匹配,确保标题在屏幕中央而非剩余空间中央

5.2 主题色选择

App3 的视觉主题色为 #4ECDC4(一种清新的青绿色),这个颜色贯穿了应用的主要视觉元素:

  • 生成按钮背景色:#4ECDC4
  • LoadingProgress 颜色:#4ECDC4
  • 总时间卡片文字色:#4ECDC4
  • 表格中组数(sets)列的文字色:#4ECDC4

选择青绿色作为健身应用的主题色是有设计考量的:

  • 绿色系天然与"健康"、“运动”、"活力"关联,符合健身应用的产品定位
  • #4ECDC4 是一种偏冷的中性绿色,既不过于鲜艳(避免视觉疲劳),也不过于暗淡(保持活力感)
  • 在白色背景(#FFFFFF)和浅灰色背景(#F5F5F5)上,#4ECDC4 的对比度足够,确保文字可读性

5.3 Loading 状态处理

Loading 状态的处理采用了状态切换 + 条件渲染的模式:

// 生成按钮点击 --> 进入 Loading 状态
onGenerate(): void {
  this.isLoading = true;     // 显示 Loading 动画
  this.hasResult = false;     // 隐藏之前的结果(如果有)
  setTimeout((): void => {
    this.generateMockData();  // 生成 Mock 数据
    this.isLoading = false;   // 隐藏 Loading 动画
    this.hasResult = true;    // 显示结果
  }, 800);
}

// UI 层:条件渲染
if (this.isLoading) {
  Column() {
    LoadingProgress()
      .width(40)
      .height(40)
      .color('#4ECDC4')
    Text('AI正在生成训练计划...')
      .fontSize(14)
      .fontColor('#8E8E93')
      .margin({ top: 8 })
  }
}

800ms 延迟的设计考量:

800ms 的 setTimeout 延迟模拟了 AI 接口的响应时间。这个数值的选择考虑了以下因素:

  • 对于用户来说,800ms 是一个"有感知但不烦躁"的等待时间——它足够让用户感受到"AI 正在工作",但又不至于让用户认为应用卡住了
  • 真实的 AI 接口调用通常需要 2-5 秒,800ms 是一个"乐观估计"但并非不切实际(某些轻量级模型或本地模型可以达到这个速度)
  • LoadingProgress(旋转动画)需要至少 500ms 才能让用户看到完整的视觉反馈,800ms 确保用户能够看到至少一圈完整的旋转

5.4 条件渲染的三种状态

App3 的 UI 在生成按钮下方有三种互斥的显示状态,通过 if 条件渲染实现:

// 状态一:Loading 状态
if (this.isLoading) { /* LoadingProgress + 文字 */ }

// 状态二:结果展示状态
if (this.hasResult && this.outputData !== null) { /* 结果表格 + 训练提示 */ }

// 状态三:空状态(非 Loading 且无结果)
else if (!this.isLoading) { /* 引导提示文字 */ }

三种状态的优先级: Loading 状态优先级最高,它在任何时候都会覆盖结果展示和空状态。结果展示状态次之,仅当 hasResulttrueoutputData 不为 null 时显示。空状态是兜底,仅在非 Loading 且无结果时显示。

空状态设计: 空状态展示的文案是"请选择训练部位、器械和时长后点击生成",这是一个引导性文案,告诉用户下一步应该做什么。这与 Loading 状态的"AI正在生成训练计划…"(告知用户正在发生什么)和结果展示状态(展示已完成的内容)形成了完整的"引导 --> 进行中 --> 已完成"的用户体验闭环。

5.5 训练提示卡片的视觉设计

训练提示卡片使用暖黄色背景(#FFF9E6),与训练计划表格的白色/灰色冷色调形成对比:

Text(this.outputData.tips)
  .fontSize(13)
  .fontColor('#666666')
  .width('100%')
  .backgroundColor('#FFF9E6')
  .borderRadius(8)
  .padding(12)
  .lineHeight(20)

设计考量:

  • #FFF9E6 是一种浅暖黄色,在白色背景上既能与表格区分,又不会过于突兀。暖黄色天然带有"提示"、"警告"但非"危险"的语义暗示
  • 文字颜色 #666666 比标准正文颜色 #333333 更浅,传达"这是辅助信息"的层级感
  • lineHeight(20) 确保多行文字有足够的行间距,提升可读性
  • 字体大小 13px 比表格数据(13px)略小,形成"正文 > 辅助信息"的视觉层级

六、Assess 评估阶段:功能完成度与扩展性分析

6.1 功能完成度评估

对照验收标准,逐项评估 App3 的功能完成度:

编号 验收项 完成情况 评分
AC-01 训练部位选择 6 个选项,下拉选择器交互正常,选中逻辑正确,视觉反馈完整 10/10
AC-02 器械类型选择 4 个选项,下拉选择器交互正常,选中逻辑正确,视觉反馈完整 10/10
AC-03 训练时长输入 数字键盘输入,TextInput 组件正确配置 10/10
AC-04 下拉选择器交互 展开/收起正常,选中自动收起,两个选择器独立,无相互干扰 10/10
AC-05 训练计划生成 4 组场景参数 + 1 组兜底,全部返回不同的训练计划 10/10
AC-06 结果完整性 每组结果包含 ≥5 个动作、总时长、训练提示 10/10
AC-07 表格展示 四列对齐,斑马纹行背景,表头与数据行区分清晰 9/10
AC-08 Loading 状态 LoadingProgress 组件 + 文字提示,800ms 延迟合理 10/10
AC-09 空状态提示 引导文案清晰,位置合理 10/10
AC-10 返回导航 router.back() 正常返回首页 10/10
AC-11 ArkTS 编译通过 零编译错误,零 ArkTS 规范违规 10/10
AC-12 AI 接口预留 callAIAPI() 桩代码完整,参数映射正确 10/10
AC-13 离线可用 所有数据来自本地 Mock,无网络依赖 10/10

综合评分:129/130(99.2%)

AC-07 扣 1 分的原因是:表格列宽使用 layoutWeight 均分,但"动作名称"列(weight=2)和"组数/次数/休息"列(weight=1)的比例为 2:1:1:1。在一些长动作名称(如"弹力带平板支撑划船"7 个字)的情况下,2:1 的比例可能导致文字换行,影响表格美观性。建议将比例调整为 3:1:1:1 或对动作名称进行截断处理。

6.2 与同类应用的对比分析

在 AI40 工具箱中,App3 属于"健康运动"分类,与以下应用存在功能关联:

应用 功能 与 App3 的关系
App3-肌动实验室 健身训练计划生成 本应用
App4-心流计时器 番茄工作法时间表 互补:App3 生成"做什么",App4 管理"何时做"
App34-体型雕刻家 健身训练计划 上下游:App3 是通用训练计划生成,App34 是个性化训练计划
App33-营养方程式 营养餐单规划 上下游:训练 + 饮食是健身的完整闭环

与 App4 的对比:

  • App4 是"时间管理"工具,核心是时间分配算法(番茄钟规则 25+5+15)
  • App3 是"内容生成"工具,核心是场景匹配和内容填充(动作列表 + 训练提示)
  • 两者互补:用户可以先使用 App3 生成训练计划,再使用 App4 将训练计划转化为番茄钟时间表

与 App34 的对比:

  • App34 是 App3 的"进阶版",可以理解为 App3 是"入门级"训练计划生成,App34 是"专业级"训练计划生成
  • App3 的输入参数是"训练部位 + 器械 + 时长",App34 的输入参数可能包括"身高体重、体脂率、训练目标、训练经验"等更细粒度的个人数据
  • App3 的输出是"6-8 个动作的列表",App34 的输出可能是"周期化训练计划"(如 8 周增肌计划)

6.3 扩展性分析

短期扩展(1-2 周内可完成):

  1. 增加更多场景数据。 当前 4 个精确场景 + 1 个兜底场景,可以扩展到 8-10 个精确场景,覆盖更多"部位 × 器械"的组合,如"背 + 哑铃"、“肩 + 弹力带”、"腿 + 徒手"等。每个新场景只需要在 generateMockData() 中添加一个 else if 分支,工作量约 15-20 分钟每个场景。

  2. 训练时长参数的实际应用。 当前训练时长(minutes)参数虽然支持输入,但在 Mock 数据生成中并未实际使用——所有场景的 total_min 都是预设的固定值。可以在 generateMockData() 中根据 this.minutes 的值动态调整动作数量和组数,使训练计划更贴合用户的时间预算。

  3. 下拉选择器抽取为 @Builder 方法。 将两个下拉选择器(约 80 行代码)提取为 @Builder 方法,减少 build() 方法的代码量,提高可维护性。

中期扩展(1-2 月内可完成):

  1. 接入真实 AI 接口。 取消 callAIAPI() 的注释,对接 HarmonyOS 的 @ohos.net.http 模块或云端大模型 API。需要考虑的技术点包括:API 认证(API Key 管理)、错误处理(网络超时、服务不可用时的回退策略)、请求/响应格式转换(将 ArkTS 的 interface 类型映射到 JSON Schema)。

  2. 动画增强。 为下拉选择器的展开/收起添加 animateTo 过渡动画,为结果表格的展示添加淡入效果,提升用户体验。在实现动画时,需要注意 ArkUI 动画规范中的约束:不可以在动画过程中频繁改变 widthheightpaddingmargin 等布局属性,建议使用 transform(如 scaletranslate)和 opacity 来实现动画效果。

  3. 训练历史记录。 使用 HarmonyOS 的 @ohos.data.preferences@ohos.data.relationalStore 存储用户的训练历史,支持查看历史记录和统计训练频次。

长期扩展(3-6 月内可完成):

  1. 鸿蒙PC端适配。 当前 App3 的 UI 设计主要面向手机竖屏,在鸿蒙PC端大屏上,可以考虑将"参数选择"和"结果展示"分为左右两栏布局,提升大屏使用体验。同时,大屏的鼠标交互与手机的触摸交互有差异,需要适配下拉选择器的悬停态和键盘操作。

  2. 鸿蒙Flutter框架跨平台扩展。 如果未来需要将 App3 扩展到 iOS 和 Android 平台,可以考虑使用鸿蒙Flutter框架进行跨平台开发。鸿蒙Flutter框架提供了与 ArkUI 类似的声明式 UI 范式,但在组件 API 和状态管理上有差异。需要将 ArkTS 的 @State 装饰器迁移到 Flutter 的 setStateProvider/Riverpod 状态管理方案,将 ForEach 组件迁移到 Flutter 的 ListView.builder

  3. 动作视频库集成。 为每个训练动作关联视频教程或 GIF 动图,用户点击动作名称时可以查看动作演示。这需要引入多媒体资源管理和视频播放组件(@ohos.multimedia.media)。

6.4 技术经验总结

本应用的三大技术亮点:

  1. 自定义下拉选择器的 ArkUI 实现方案。 通过条件渲染(if)和 ForEach 组件,在 ArkUI 的声明式范式中实现了类似 Web 前端的下拉选择器交互,无需依赖系统原生 Picker 组件,视觉风格完全可控。

  2. 场景键精确匹配的 Mock 数据策略。 通过 if-else 链式判断 + 场景键匹配,实现了简单、直观、可维护的离线数据策略。每个场景的数据独立管理,新增场景不影响已有场景,兜底方案确保任何参数组合都能获得可用结果。

  3. 表格化结果展示的斑马纹实现。 利用 ForEachindex 参数,通过 index % 2 === 0 实现了简洁的斑马纹效果,无需额外的状态管理或 CSS 类名切换。

本应用的三个改进方向:

  1. build() 方法过长(294 行)。 建议通过 @Builder 方法将下拉选择器、结果表格、训练提示等可复用 UI 片段提取出来,提高代码可读性和可维护性。

  2. Mock 数据未使用训练时长参数。 当前 minutes 参数在 Mock 数据生成中未实际使用,建议在 generateMockData() 中根据训练时长动态调整动作数量和休息时间,使训练计划更贴合用户需求。

  3. 缺少输入校验。 当前 minutes 的 TextInput 未做输入校验,用户可以输入空字符串、0 或负数。建议在 onGenerate() 中添加输入校验逻辑,确保训练时长是一个有效的正整数。


七、结语

「肌动实验室」作为 AI40 智能应用工具箱中的第 3 号应用,以健身训练计划生成为核心功能,通过三个简洁的参数(训练部位、器械类型、训练时长)驱动,为用户提供专业、可执行的训练方案。在技术实现上,它展示了 HarmonyOS 鸿蒙平台下 ArkTS + ArkUI 的典型开发模式:显式类型定义、@State 状态管理、条件渲染、ForEach 列表渲染、自定义下拉选择器交互、场景键精确匹配的 Mock 数据策略、以及 AI API 调用桩的架构预留。

本文从 6A 工作流的视角,完整记录了从需求对齐到最终评估的全过程。在 Align 阶段,我们明确了功能边界和技术约束,制定了 13 项可测试的验收标准;在 Architect 阶段,我们设计了数据接口、状态管理方案和 UI 组件树,确保了架构的清晰性和可扩展性;在 Atomize 阶段,我们将任务拆解为 14 个原子任务,深入分析了 Mock 数据策略、下拉选择器实现和斑马纹表格等关键实现细节;在 Approve 阶段,我们进行了全面的 ArkTS 合规审计和代码质量审查,确认了零违规的代码质量;在 Automate 阶段,我们详细阐述了导航栏、主题色、Loading 状态、条件渲染和训练提示卡片等 UI 实现细节;在 Assess 阶段,我们评估了功能完成度(99.2%),分析了与同类应用的对比关系,并规划了短期、中期和长期三个维度的扩展方向。

展望未来,随着 HarmonyOS 生态的持续发展,鸿蒙PC端的大屏适配和鸿蒙Flutter框架的跨平台扩展将为「肌动实验室」带来更广阔的应用场景。当 AI API 调用桩被激活,接入真实的大模型服务后,应用将能够根据用户的个性化需求(如训练经验、伤病情况、具体目标)生成更加精准和定制化的训练计划,真正实现"千人千面"的智能健身指导。这不仅是一个技术演进的过程,更是 AI 技术从"工具"到"伙伴"的角色转变——从被动地生成训练计划,到主动地理解用户需求、提供个性化建议、陪伴用户完成健身旅程。

在 AI40 工具箱的整体架构中,「肌动实验室」与「心流计时器」(时间管理)、「体型雕刻家」(个性化训练)、「营养方程式」(饮食规划)共同构成了一个完整的"健康运动"生态闭环。当这四个应用的数据和功能被整合到一个统一的用户画像中,它们将不再各自为战,而是协同工作——训练计划自动转化为时间表,训练数据反哺个性化建议,饮食方案与训练强度动态匹配——最终实现从"工具集合"到"智能伙伴"的质变。这正是 AI40 项目的终极愿景:让 AI 不再是冰冷的代码,而是温暖的生活助手。


相关文章:

Logo

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

更多推荐