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

App34-体型雕刻家:健身训练计划的HarmonyOS开发实践

摘要

在全民健身热潮兴起的今天,科学的健身计划成为越来越多人的需求。App34「体型雕刻家」作为AI40智能应用工具箱中的健身类应用,致力于为用户提供个性化的健身训练计划。本文将深入探讨该应用的完整开发流程,从需求对齐到架构设计,从原子化任务拆解到自动化执行,全面展示鸿蒙生态下AI健身应用的开发实践。特别关注开发过程中遇到的关键技术问题——height/weight属性冲突的解决。


一、Align 对齐阶段

1.1 项目背景与需求分析

随着健康意识的提升,健身已成为现代人生活的重要组成部分。然而,对于大多数人来说,制定科学的训练计划是一项挑战:

  • 知识壁垒:缺乏专业的健身知识,不知道如何制定适合自己的计划
  • 个性化需求:不同身高、体重、目标、训练经验的人需要不同的方案
  • 时间限制:不知道如何在有限的时间内高效训练

App34「体型雕刻家」的核心需求是:根据用户的身高、体重、目标、训练天数和训练级别,生成个性化的一周训练计划

1.2 功能边界确认

边界项 In Scope Out Scope 说明
输入参数 身高、体重、目标(增肌/减脂)、训练天数、训练级别 年龄、性别、伤病情况 聚焦核心参数,简化用户操作
训练计划 一周7天的每日训练安排 单个训练动作指导视频 提供结构化计划,不包含视频内容
饮食规则 基础饮食建议(热量、蛋白质摄入等) 详细食谱 提供原则性指导,不包含具体菜单
目标指标 体重、体脂率、力量指标等 实时体测数据采集 提供目标设定,不包含数据采集功能
AI集成 API接口预留 真实AI服务调用 Mock数据先行,预留扩展空间

1.3 技术约束分析(ArkTS 严格模式)

约束项 影响分析 应对策略
禁止 any/unknown 需显式定义所有数据接口 定义 ExerciseItemDayPlanMetricItemFitnessResultFitnessScenario 接口
禁止解构赋值 函数参数需逐个声明 使用标准函数参数格式
禁止 for...in/for...of 使用 for 循环或 ForEach UI层使用 ForEach,逻辑层使用传统 for 循环
禁止 var 声明 统一使用 let/const 严格遵守变量声明规范
必须显式返回类型 所有函数需标注返回类型 添加 : void
Scroll 单子节点约束 Scroll 只能包含一个直接子组件 将内容包裹在 Column
ForEach 必须指定 keyGenerator 列表渲染需提供唯一标识 使用 (item) => item.name 或类似模式

1.4 验收标准

编号 验收标准 验证方法
AC-01 首页显示5个输入参数(身高、体重、目标、训练天数、训练级别) 启动应用查看界面
AC-02 身高和体重输入框仅接受数字 尝试输入非数字字符
AC-03 目标按钮组支持增肌/减脂切换 点击两个按钮观察UI变化
AC-04 训练天数输入框限制1-7范围 输入0或8验证边界处理
AC-05 训练级别按钮组支持新手/进阶切换 点击两个按钮观察UI变化
AC-06 点击"生成训练计划"按钮显示结果面板 填写参数后点击按钮
AC-07 结果面板包含目标指标、饮食规则、每周训练计划 检查结果区域完整性
AC-08 返回按钮可回到首页 点击返回按钮
AC-09 界面适配鸿蒙手机端 在不同分辨率设备上测试
AC-10 代码无编译错误,符合ArkTS规范 运行构建命令验证

1.5 关键决策点

决策1:参数数量设计

  • 方案A:精简参数(仅目标+训练天数)
  • 方案B:完整参数(身高+体重+目标+训练天数+训练级别)
  • 选择方案B:更多参数可以生成更个性化的训练计划,提升用户体验

决策2:数据匹配策略

  • 方案A:索引匹配(如App14的emotionIndex模式)
  • 方案B:字符串场景键匹配(身高_体重_目标_天数_级别)
  • 选择方案B:5个参数组合形成的场景空间较大,字符串键更直观,便于扩展和维护

决策3:变量命名策略

  • 方案A:使用height/weight作为状态变量名
  • 方案B:使用userHeight/userWeight作为状态变量名
  • 选择方案B:ArkUI组件内置height和weight属性,使用这些名称会导致冲突(详见4.3节)

决策4:异常处理策略

  • 方案A:无匹配场景时报错
  • 方案B:无匹配场景时使用默认数据(fallback)
  • 选择方案B:提升应用的健壮性,确保任何参数组合都能正常运行

二、Architect 架构阶段

2.1 五层架构设计

┌─────────────────────────────────────────────────────────┐
│                     UI Layer (ArkUI)                   │
│  ┌─────────────────────────────────────────────────┐    │
│  │  Header (返回按钮 + 标题)                      │    │
│  ├─────────────────────────────────────────────────┤    │
│  │  InputSection (身高/体重/目标/天数/级别输入)    │    │
│  ├─────────────────────────────────────────────────┤    │
│  │  ResultPanel (指标+饮食+训练计划)               │    │
│  └─────────────────────────────────────────────────┘    │
├─────────────────────────────────────────────────────────┤
│               State Management Layer                    │
│  ┌─────────────────────────────────────────────────┐    │
│  │  @State userHeight: number                     │    │
│  │  @State userWeight: number                    │    │
│  │  @State goal: string                           │    │
│  │  @State daysPerWeek: number                    │    │
│  │  @State level: string                          │    │
│  │  @State showResult: boolean                    │    │
│  │  @State currentResult: FitnessResult           │    │
│  └─────────────────────────────────────────────────┘    │
├─────────────────────────────────────────────────────────┤
│                Data Matching Layer                      │
│  ┌─────────────────────────────────────────────────┐    │
│  │  getMockData() - 场景键匹配算法                │    │
│  │  generateMockData() - 入口方法                 │    │
│  └─────────────────────────────────────────────────┘    │
├─────────────────────────────────────────────────────────┤
│              Data Model Layer                           │
│  ┌─────────────────────────────────────────────────┐    │
│  │  interface ExerciseItem                        │    │
│  │  interface DayPlan                             │    │
│  │  interface MetricItem                          │    │
│  │  interface FitnessResult                       │    │
│  │  interface FitnessScenario                     │    │
│  └─────────────────────────────────────────────────┘    │
├─────────────────────────────────────────────────────────┤
│                 Mock Data Layer                         │
│  ┌─────────────────────────────────────────────────┐    │
│  │  mockScenarios: FitnessScenario[]              │    │
│  │  场景1: 170_70_增肌_4_新手                    │    │
│  │  场景2: 165_80_减脂_5_进阶                    │    │
│  │  场景3: 180_85_增肌_5_进阶                    │    │
│  └─────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────┘

2.2 数据模型设计

ExerciseItem 接口:定义单个训练动作的数据结构

interface ExerciseItem {
  name: string;   // 动作名称,如"杠铃平板卧推"
  sets: string;   // 组数,如"4"
  reps: string;   // 次数,如"10-12"或"力竭"
  rest: string;   // 休息时间,如"90秒"
}

DayPlan 接口:定义单日训练计划的数据结构

interface DayPlan {
  day: string;          // 星期,如"周一"
  focus: string;        // 训练重点,如"胸肌+三头"
  exercises: ExerciseItem[]; // 训练动作列表
}

MetricItem 接口:定义目标指标的数据结构

interface MetricItem {
  name: string;   // 指标名称,如"体重目标"
  target: string; // 目标值,如"75"
  unit: string;   // 单位,如"kg"
}

FitnessResult 接口:定义完整训练方案的数据结构

interface FitnessResult {
  weekly: DayPlan[];   // 一周训练计划
  diet_rules: string[]; // 饮食规则
  metrics: MetricItem[]; // 目标指标
}

FitnessScenario 接口:定义场景与结果的映射关系

interface FitnessScenario {
  scenario: string;     // 场景键,如"170_70_增肌_4_新手"
  result: FitnessResult; // 对应结果
}

2.3 状态管理架构

状态变量 类型 作用 初始值
userHeight number 用户身高(cm) 170
userWeight number 用户体重(kg) 70
goal string 训练目标(增肌/减脂) ‘增肌’
daysPerWeek number 每周训练天数 4
level string 训练级别(新手/进阶) ‘新手’
showResult boolean 是否显示结果面板 false
currentResult FitnessResult 当前训练方案数据 空对象

关键设计决策currentResult 初始化为空对象而非 null,确保即使在没有数据时访问属性也不会报错。

2.4 场景键匹配策略

场景键采用下划线分隔的字符串格式:身高_体重_目标_天数_级别

示例

  • 170_70_增肌_4_新手 — 身高170cm、体重70kg、目标增肌、每周4天、新手级别
  • 165_80_减脂_5_进阶 — 身高165cm、体重80kg、目标减脂、每周5天、进阶级别

匹配算法

getMockData(): void {
  let key: string = this.userHeight.toString() + '_' + this.userWeight.toString() + '_' + this.goal + '_' + this.daysPerWeek.toString() + '_' + this.level;
  let found: FitnessScenario | null = null;
  for (let i = 0; i < this.mockScenarios.length; i++) {
    if (this.mockScenarios[i].scenario === key) {
      found = this.mockScenarios[i];
      break;
    }
  }
  if (found === null) {
    found = this.mockScenarios[0];
  }
  if (found !== null) {
    this.currentResult = found.result;
  }
  this.showResult = true;
}

设计优点

  1. 直观性:场景键直接反映参数组合,便于理解和维护
  2. 扩展性:新增场景只需添加新的 FitnessScenario 对象
  3. 健壮性:无匹配时自动使用默认数据,不会崩溃

三、Atomize 原子化阶段

3.1 原子任务分解

任务编号 任务名称 依赖 验收标准 预估时长
T1 定义 ExerciseItem 接口 接口包含 name、sets、reps、rest 字段 0.5h
T2 定义 DayPlan 接口 T1 接口包含 day、focus、exercises 字段 0.5h
T3 定义 MetricItem 接口 接口包含 name、target、unit 字段 0.5h
T4 定义 FitnessResult 接口 T2, T3 接口包含 weekly、diet_rules、metrics 字段 0.5h
T5 定义 FitnessScenario 接口 T4 接口包含 scenario、result 字段 0.5h
T6 定义 @State 状态变量 T4 7个状态变量正确声明和初始化 0.5h
T7 定义 mockScenarios 数据 T5 3个场景的完整mock数据 2h
T8 实现 getMockData 方法 T5, T6 正确匹配场景键并返回结果 1h
T9 实现 generateMockData 方法 T8 调用 getMockData 并更新状态 0.5h
T10 实现导航栏组件 返回按钮正常工作 0.5h
T11 实现输入区域 T6 5个输入参数的UI实现 2h
T12 实现结果面板 T6, T8 正确展示指标、饮食规则、训练计划 2h

3.2 Mock 数据策略深度解析

数据设计原则

  1. 场景多样性:覆盖不同目标(增肌/减脂)、不同级别(新手/进阶)、不同体型(165cm/170cm/180cm)
  2. 训练科学性:每个训练计划遵循健身科学原则(分化训练、渐进超负荷、休息恢复)
  3. 饮食配套:训练计划搭配相应的饮食规则,形成完整的健身方案
  4. 目标量化:提供可衡量的目标指标,帮助用户跟踪进度

3个场景的Mock数据详解

场景1:170_70_增肌_4_新手

  • 用户画像:身高170cm,体重70kg,BMI约24.2,目标增肌,每周4天训练,新手级别
  • 训练策略:三分化训练(胸+三头、背+二头、腿+肩),每周4天训练3天休息,强度适中
  • 饮食规则:每日热量盈余300-500kcal,蛋白质摄入2g/kg体重
  • 目标指标:体重75kg,体脂率15%,卧推1RM 80kg,深蹲1RM 100kg

场景2:165_80_减脂_5_进阶

  • 用户画像:身高165cm,体重80kg,BMI约29.4,目标减脂,每周5天训练,进阶级别
  • 训练策略:HIIT+力量训练结合,每日热量赤字500-800kcal,高强度间歇训练提升代谢
  • 饮食规则:碳水循环(训练日高碳,休息日低碳),蛋白质摄入2.2g/kg体重
  • 目标指标:体重70kg,体脂率12%,腰围80cm,5km跑25分钟

场景3:180_85_增肌_5_进阶

  • 用户画像:身高180cm,体重85kg,BMI约26.2,目标增肌,每周5天训练,进阶级别
  • 训练策略:五分化训练(胸、背、腿、肩、手臂),大重量训练为主,追求渐进超负荷
  • 饮食规则:每日热量盈余500-800kcal,蛋白质摄入2.5g/kg体重,睡前补充酪蛋白
  • 目标指标:体重92kg,体脂率15%,卧推1RM 120kg,深蹲1RM 160kg,硬拉1RM 180kg

3.3 参数空间分析

5个输入参数形成的参数空间:

  • 身高:理论上无限(但实际范围约150-200cm)
  • 体重:理论上无限(但实际范围约40-150kg)
  • 目标:2种(增肌、减脂)
  • 训练天数:7种(1-7天)
  • 训练级别:2种(新手、进阶)

覆盖率计算

  • 当前Mock数据:3个场景
  • 潜在场景数量:(身高范围)×(体重范围)× 2 × 7 × 2 ≈ 大量

覆盖策略

  • 当前阶段选择典型场景作为代表
  • 未来接入AI后,可实现全参数空间的个性化生成
  • Fallback机制确保任何参数组合都能正常运行

四、Approve 审批阶段

4.1 ArkTS 语法合规检查

检查项 状态 说明
any/unknown 类型 ✅ 通过 使用 interface 定义所有数据结构
无解构赋值 ✅ 通过 函数参数使用标准格式
for...in/for...of ✅ 通过 使用传统 for 循环和 ForEach
var 声明 ✅ 通过 使用 let 声明变量
显式返回类型 ✅ 通过 getMockDatagenerateMockData 均标注 : void
Scroll 单子节点 ✅ 通过 Scroll 包含单个 Column 子组件
ForEach keyGenerator ✅ 通过 使用隐式key或基于item属性的key

4.2 代码质量审查

优点

  1. 接口定义完整:5个接口设计合理,覆盖所有数据结构
  2. 场景键匹配算法健壮:包含fallback机制
  3. 参数输入验证完善:训练天数限制1-7范围,数字输入有默认值处理
  4. Mock数据丰富:3个场景涵盖不同目标和级别
  5. 代码结构清晰:方法职责单一,UI与逻辑分离

待改进项

  1. 缺少加载状态:当前无 loading 状态,用户点击按钮后无即时反馈
  2. 场景覆盖率低:仅3个场景,大部分参数组合会触发fallback
  3. 缺少详细的训练动作说明:仅显示动作名称,无动作要领说明

4.3 关键Bug分析:height/weight属性冲突

问题描述

在开发初期,代码中使用了 heightweight 作为状态变量名:

@State height: number = 170;
@State weight: number = 70;

运行后发现以下异常:

  • UI布局混乱,组件位置偏移
  • 状态更新后UI不响应
  • 部分组件尺寸异常

根因分析

ArkUI组件(如Column、Row、Text等)内置了 heightweight 属性,用于布局控制:

  • height:组件的高度属性
  • weight:flex布局中的权重属性

当开发者在自定义组件中声明同名的 @State 变量时,会产生命名冲突。ArkUI框架会将这些变量解释为布局属性而非状态变量,导致:

  1. 状态变量被当作布局属性处理
  2. 布局计算干扰了状态管理
  3. UI渲染出现不可预期的行为

修复方案

将变量名重命名为 userHeightuserWeight

@State userHeight: number = 170;
@State userWeight: number = 70;

同时更新所有引用位置:

  • this.heightthis.userHeight
  • this.weightthis.userWeight

影响范围

文件 修改内容
pages/app34/Index.ets 状态变量声明
pages/app34/Index.ets getMockData方法
pages/app34/Index.ets TextInput onChange回调
pages/app34/Index.ets build方法中的数据绑定

教训总结

  1. 避免使用ArkUI保留属性名:在声明状态变量时,避免使用height、weight、width、opacity等ArkUI组件的内置属性名
  2. 命名约定:建议在状态变量前添加前缀(如user、my、current等),明确区分状态变量和布局属性
  3. 代码审查重点:在代码审查时,特别关注状态变量命名是否与ArkUI属性冲突

4.4 场景键匹配算法审查

当前实现

getMockData(): void {
  let key: string = this.userHeight.toString() + '_' + this.userWeight.toString() + '_' + this.goal + '_' + this.daysPerWeek.toString() + '_' + this.level;
  let found: FitnessScenario | null = null;
  for (let i = 0; i < this.mockScenarios.length; i++) {
    if (this.mockScenarios[i].scenario === key) {
      found = this.mockScenarios[i];
      break;
    }
  }
  if (found === null) {
    found = this.mockScenarios[0];
  }
  if (found !== null) {
    this.currentResult = found.result;
  }
  this.showResult = true;
}

优点

  • 使用传统 for 循环,符合ArkTS规范
  • 包含fallback机制,提升健壮性
  • 算法时间复杂度O(n),对于少量场景数据足够高效

优化建议

对于大量场景数据,可以考虑使用Map数据结构提升查找效率:

private scenarioMap: Map<string, FitnessResult> = new Map();

aboutToAppear(): void {
  for (let i = 0; i < this.mockScenarios.length; i++) {
    this.scenarioMap.set(this.mockScenarios[i].scenario, this.mockScenarios[i].result);
  }
}

getMockData(): void {
  let key: string = this.userHeight.toString() + '_' + this.userWeight.toString() + '_' + this.goal + '_' + this.daysPerWeek.toString() + '_' + this.level;
  let result: FitnessResult | undefined = this.scenarioMap.get(key);
  if (result === undefined) {
    result = this.mockScenarios[0].result;
  }
  this.currentResult = result;
  this.showResult = true;
}

五、Automate 自动化执行阶段

5.1 输入区域实现

身高输入框

Column() {
  Text('身高 (cm)')
    .fontSize(14)
    .fontColor('#666666')
    .padding({ left: 16, top: 16, bottom: 4 })
  TextInput({ text: this.userHeight.toString() })
    .width('calc(100% - 32px)')
    .height(44)
    .backgroundColor('#FFFFFF')
    .borderRadius(8)
    .type(InputType.Number)
    .margin({ left: 16, right: 16 })
    .onChange((value: string) => {
      this.userHeight = parseInt(value) || 160;
    })
}
.width('100%')

设计要点

  • 使用 InputType.Number 限制仅接受数字输入
  • parseInt(value) || 160 处理空输入,确保有默认值

训练天数输入框(带边界检查)

TextInput({ text: this.daysPerWeek.toString() })
  .width('calc(100% - 32px)')
  .height(44)
  .backgroundColor('#FFFFFF')
  .borderRadius(8)
  .type(InputType.Number)
  .margin({ left: 16, right: 16 })
  .onChange((value: string) => {
    let num: number = parseInt(value) || 3;
    if (num < 1) { num = 1; }
    if (num > 7) { num = 7; }
    this.daysPerWeek = num;
  })

设计要点

  • 输入值限制在1-7范围内
  • 小于1时设为1,大于7时设为7
  • 确保输入始终有效

5.2 目标指标展示

Row() {
  ForEach(this.currentResult.metrics, (metric: MetricItem) => {
    Column() {
      Text(metric.target + metric.unit)
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FF6B6B')
      Text(metric.name)
        .fontSize(11)
        .fontColor('#999999')
        .padding({ top: 2 })
    }
    .alignItems(HorizontalAlign.Center)
    .margin({ left: 4, right: 4 })
  })
}
.width('100%')
.justifyContent(FlexAlign.SpaceAround)

设计要点

  • 使用 FlexAlign.SpaceAround 均匀分布指标卡片
  • 目标值使用主题色突出显示
  • 指标名称使用灰色小字,形成视觉层次

5.3 每周训练计划展示

ForEach(this.currentResult.weekly, (dayPlan: DayPlan) => {
  Column() {
    Row() {
      Text(dayPlan.day)
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FF6B6B')
        .backgroundColor('#FFF0F0')
        .borderRadius(4)
        .padding({ left: 8, right: 8, top: 2, bottom: 2 })
      Text('  ' + dayPlan.focus)
        .fontSize(14)
        .fontColor('#333333')
    }
    .padding({ bottom: 4 })

    ForEach(dayPlan.exercises, (ex: ExerciseItem) => {
      Text('  ' + ex.name + '  ' + ex.sets + '组 x ' + ex.reps + '  休息' + ex.rest)
        .fontSize(12)
        .fontColor('#666666')
        .padding({ bottom: 2 })
    })
  }
  .width('100%')
  .padding({ bottom: 8 })
  .margin({ bottom: 4 })
})

设计要点

  • 嵌套 ForEach 渲染周计划和每日训练动作
  • 日期使用主题色标签样式,突出显示
  • 训练动作使用缩进和灰色字体,形成清晰的层级

5.4 主题色设计

主题色选择#FF6B6B(珊瑚红)

设计理由

  1. 活力感:红色代表活力和激情,符合健身主题
  2. 与App3的区别:App3「肌动实验室」使用 #4ECDC4(青色),App34使用珊瑚红,区分两个健身应用
  3. 视觉冲击力:珊瑚红比纯红色更柔和,适合长时间使用

色彩体系

用途 颜色 说明
主色 #FF6B6B 按钮、标签、重点文字
背景 #F5F5F5 页面背景
卡片 #FFFFFF 内容卡片背景
标签背景 #FFF0F0 日期标签背景
文字 #333333 / #666666 / #999999 不同层级的文字

六、Assess 评估阶段

6.1 功能完成度评估

功能项 完成度 说明
参数输入 100% 5个输入参数,含验证逻辑
训练计划生成 100% 场景键匹配算法,含fallback机制
目标指标展示 100% 横向排列的指标卡片
饮食规则展示 100% 列表式展示
每周训练计划展示 100% 嵌套ForEach渲染
返回导航 100% 按钮正常工作
加载状态 0% 缺少loading动画
场景覆盖率 仅3个场景,大部分参数组合触发fallback

6.2 代码质量评估

维度 评分 说明
可读性 9/10 代码结构清晰,命名规范
可维护性 8/10 接口定义完整,但场景数据硬编码
性能 9/10 Mock数据本地加载,无网络请求
兼容性 9/10 符合ArkTS严格模式规范
安全性 8/10 无敏感数据,但缺少输入验证

6.3 技术亮点总结

  1. 场景键匹配模式:使用字符串键匹配,直观且易于扩展
  2. Fallback机制:提升应用健壮性,确保任何参数组合都能正常运行
  3. height/weight重命名修复:解决了ArkUI属性冲突问题
  4. 参数边界检查:训练天数限制1-7范围,增强数据有效性
  5. 完整的Mock数据:3个场景涵盖不同目标和级别,内容专业

6.3.1 与 App3 肌动实验室的对比分析

App34「体型雕刻家」和 App3「肌动实验室」同属AI40工具箱中的健身类应用,但在定位、功能和设计上存在显著差异:

对比维度 App34 体型雕刻家 App3 肌动实验室
核心定位 完整的一周训练计划 针对特定部位的训练动作推荐
输入参数 身高、体重、目标、训练天数、级别 训练部位、可用器械、训练时长
输出内容 7天完整训练计划+饮食规则+目标指标 单个部位的训练动作列表
数据模型 DayPlan + FitnessResult + FitnessScenario ExerciseItem + RoutineOutput
匹配策略 字符串场景键匹配 索引匹配
主题色 珊瑚红 #FF6B6B 青色 #4ECDC4
适用场景 系统性健身计划制定 单次训练动作选择
目标用户 有长期健身目标的用户 日常训练的用户

关键洞察

  1. 互补性:两个应用互补,App3适合日常训练,App34适合制定长期计划
  2. 数据复杂度:App34的数据模型更复杂,包含周计划、饮食规则、目标指标等
  3. 匹配策略差异:App3使用简单的索引匹配,App34使用更灵活的字符串场景键匹配
  4. 视觉区分:通过不同的主题色区分两个健身应用

6.4 改进方向

短期改进

  1. 添加加载状态动画:参考其他AI40应用的800ms延迟加载模式
  2. 扩展Mock场景:增加更多参数组合的场景数据
  3. 添加训练动作说明:为每个动作添加简要的动作要领

中期改进

  1. 接入真实AI服务,实现全参数空间的个性化训练计划生成
  2. 添加训练记录功能,跟踪用户训练进度
  3. 添加社交分享功能,支持分享训练计划

长期改进

  1. 集成鸿蒙健康数据,结合心率、睡眠等数据优化训练计划
  2. 开发鸿蒙PC端适配版本,支持更大屏幕的计划展示
  3. 探索鸿蒙Flutter框架混合开发方案,提升跨平台能力

6.5 健身训练理论背景

App34「体型雕刻家」的训练计划设计基于以下健身科学原理,确保训练方案的专业性和有效性:

6.5.1 分化训练原理

分化训练是将全身肌肉群分成不同部位,在不同时间进行训练的方法。这种方法可以:

  1. 提高训练强度:每次训练专注于少数肌群,可以使用更大的重量和更充分的刺激
  2. 促进恢复:不同肌群交替训练,给肌肉足够的时间修复和生长
  3. 避免过度训练:避免连续训练同一肌群导致的疲劳和受伤

App34的三个场景采用了不同的分化策略:

  • 新手增肌场景:三分化训练(胸+三头、背+二头、腿+肩),每周4天
  • 进阶减脂场景:全身训练+HIIT结合,每周5天
  • 进阶增肌场景:五分化训练(胸、背、腿、肩、手臂),每周5天

6.5.2 渐进超负荷原则

渐进超负荷是肌肉生长的核心原则,指逐步增加训练强度以刺激肌肉适应和生长。App34的训练计划体现了这一原则:

  1. 重量递增:进阶场景使用更大的重量(如6-8次范围),新手场景使用中等重量(10-12次范围)
  2. 组数递增:进阶场景增加训练组数(5组 vs 4组)
  3. 强度递增:进阶场景缩短休息时间,增加训练密度

6.5.3 训练量与训练强度的平衡

训练量(总次数×组数)和训练强度(重量/次数)是训练效果的两个关键因素:

场景 训练量 训练强度 适应人群
新手增肌 中高训练量,中等强度 10-15次范围,90秒休息 初学者,建立训练基础
进阶减脂 高训练量,中高强度 12-15次+HIIT,45-60秒休息 有基础,追求减脂效率
进阶增肌 中等训练量,高强度 6-8次范围,90-120秒休息 有经验,追求肌肉增长

6.5.4 休息与恢复的重要性

训练后的恢复时间直接影响肌肉生长和训练效果:

  1. 组间休息:力量训练需要60-120秒恢复ATP和磷酸肌酸
  2. 肌肉恢复:同一肌群需要48-72小时恢复时间
  3. 睡眠恢复:睡眠期间生长激素分泌达到峰值,是肌肉修复的关键时期

App34的训练计划充分考虑了休息安排:

  • 新手场景每周3天休息,确保充分恢复
  • 进阶场景安排主动恢复(拉伸、轻度有氧)
  • 休息日提供放松建议(泡沫轴按摩、瑜伽)

6.6 用户体验设计分析

6.6.1 交互设计原则

App34的交互设计遵循以下原则:

  1. 简洁性:5个输入参数,一键生成计划,减少用户操作步骤
  2. 反馈性:按钮点击有颜色变化反馈,参数选择即时显示
  3. 一致性:主题色贯穿整个界面,按钮样式统一
  4. 可读性:训练计划使用清晰的层级结构,便于阅读

6.6.2 用户旅程分析

用户进入 → 输入身高体重 → 选择目标(增肌/减脂) → 输入训练天数 → 选择训练级别 → 点击生成 → 查看结果

每个步骤的设计考量:

  • 输入阶段:使用数字输入框和按钮组,操作直观
  • 生成阶段:点击按钮后立即显示结果,无需等待
  • 查看阶段:目标指标横向展示,训练计划纵向展开,信息层次清晰

6.6.3 视觉设计语义

主题色珊瑚红(#FF6B6B)的语义传达:

  • 活力与激情:符合健身主题,激发用户的训练动力
  • 温暖与关怀:体现应用对用户健康的关注
  • 专业与自信:与App3肌动实验室的青色形成区分,各自定位清晰

6.7 6A工作流实践反思

通过App34「体型雕刻家」的开发,我们对6A工作流有了更深的理解:

Align阶段价值

  • 明确的功能边界和验收标准,避免开发过程中的需求变更
  • ArkTS严格模式的技术约束分析,提前做好技术准备

Architect阶段价值

  • 五层架构设计清晰,各层职责明确
  • 场景键匹配策略的设计,为后续扩展预留了空间

Atomize阶段价值

  • 原子任务分解确保开发进度可控
  • Mock数据策略的深度解析,确保数据质量

Approve阶段价值

  • height/weight属性冲突问题的发现和解决,避免了潜在的UI bug
  • 代码质量审查发现的改进点,为后续优化指明方向

Automate阶段价值

  • 参数验证逻辑的实现,提升了应用的健壮性
  • 完整的UI实现,确保用户体验符合设计预期

Assess阶段价值

  • 功能完成度评估,明确了当前的不足和改进方向
  • 技术亮点总结,为后续开发提供了可复用的经验

6.8 鸿蒙生态适配思考

鸿蒙PC端适配

随着鸿蒙PC端的普及,App34「体型雕刻家」可以在PC端提供更丰富的交互体验:

  1. 分栏布局:利用PC端更大的屏幕空间,将输入区域和结果区域放在左右分栏,用户可以同时看到参数和计划
  2. 打印功能:支持将训练计划打印出来,方便用户在健身房使用
  3. 拖拽排序:支持拖拽调整训练动作顺序,个性化定制计划
  4. 多窗口支持:支持在工作窗口旁打开训练计划,方便用户随时查看

鸿蒙Flutter框架对比

维度 ArkUI 鸿蒙Flutter框架
性能 原生渲染,性能更好 JIT/AOT编译,性能优秀
系统集成 与鸿蒙系统深度集成 通过插件桥接,集成度有限
分布式能力 原生支持分布式任务调度 需要额外开发
开发效率 声明式UI,学习曲线较陡 丰富的组件库,开发效率高
跨平台 仅限鸿蒙生态 支持iOS/Android/Web
社区生态 正在快速发展 成熟的社区和插件生态

选择建议

  • 对于专注鸿蒙生态的应用,推荐使用ArkUI,可以充分利用鸿蒙系统的分布式能力和原生性能优势
  • 对于需要跨平台发布的应用,可考虑鸿蒙Flutter框架,一套代码可以覆盖多个平台

6.9 开发过程中的关键决策回顾

在开发App34「体型雕刻家」的过程中,我们做出了以下关键决策,这些决策对应用的最终质量和用户体验产生了重要影响:

决策1:变量命名策略——height/weight vs userHeight/userWeight

选择:userHeight/userWeight

理由

  • ArkUI组件内置height和weight属性,使用这些名称会导致冲突
  • 冲突表现为UI布局混乱、状态更新不响应、组件尺寸异常
  • 修复后状态管理正常,UI渲染符合预期

决策2:场景匹配策略——索引匹配 vs 字符串场景键

选择:字符串场景键

理由

  • 5个参数组合形成的场景空间较大(身高、体重、目标、天数、级别)
  • 字符串键(如"170_70_增肌_4_新手")直观反映参数组合
  • 便于扩展和维护,新增场景只需添加新的FitnessScenario对象
  • 相比App14的索引匹配,更适合多参数场景

决策3:异常处理策略——报错 vs Fallback

选择:Fallback机制

理由

  • 当前仅有3个Mock场景,大部分参数组合会触发fallback
  • Fallback确保任何参数组合都能正常运行
  • 提升应用健壮性,避免因参数不匹配导致崩溃

决策4:数据初始化策略——null vs 空对象

选择:空对象初始化

理由

  • currentResult初始化为{ weekly: [], diet_rules: [], metrics: [] }
  • 避免空指针错误,即使没有数据也能安全访问属性
  • 简化代码逻辑,无需频繁的null检查

决策5:训练计划展示策略——展开式 vs 折叠式

选择:展开式

理由

  • 用户生成计划后希望看到完整的一周安排
  • 展开式展示便于用户浏览和参考
  • 训练计划内容适中,不会造成信息过载

6.10 健身应用技术发展趋势

随着AI和物联网技术的发展,健身应用正在经历以下技术趋势:

  1. AI个性化推荐:基于用户历史训练数据和身体数据,提供更精准的个性化训练计划
  2. 生物反馈集成:连接智能手表、心率带、体脂秤等设备,实时监测训练效果
  3. 虚拟现实训练:结合VR/AR技术,提供沉浸式训练体验
  4. 社交互动:建立用户社区,支持训练打卡、同伴激励、教练指导
  5. 端侧AI推理:在设备端运行AI模型,保护用户隐私,提升响应速度

App34「体型雕刻家」已经预留了AI API接口,可以在未来接入这些技术趋势,为用户提供更加智能、个性化的健身训练服务。


七、结语

App34「体型雕刻家」是一个专注于健身训练计划的AI应用,通过个性化的参数输入和专业的Mock数据,为用户提供完整的一周训练计划。在开发过程中,我们遇到了ArkUI属性冲突的关键问题,并通过变量重命名成功解决。

7.1 6A工作流实践反思

通过本次开发实践,我们总结了以下6A工作流的实践经验:

  1. Align阶段:需求对齐时要充分考虑技术约束,特别是ArkTS严格模式的限制
  2. Architect阶段:架构设计要兼顾扩展性和当前需求,避免过度设计
  3. Atomize阶段:任务分解要足够细粒度,确保每个任务都有明确的验收标准
  4. Approve阶段:代码审查要重点关注ArkUI属性冲突等常见问题
  5. Automate阶段:实现时要注重参数验证和异常处理,提升应用健壮性
  6. Assess阶段:评估时要全面分析优缺点,为后续改进提供方向

7.2 鸿蒙AI应用开发最佳实践

基于App34的开发经验,我们总结了以下鸿蒙AI应用开发的最佳实践:

  1. 变量命名约定:避免使用ArkUI组件的保留属性名(height、weight、width、opacity等),建议添加前缀(user、my、current等)
  2. Mock数据先行:在接入真实AI服务前,使用Mock数据确保应用的基本功能可用
  3. Fallback机制:实现完善的异常处理和fallback机制,确保应用在任何情况下都能正常运行
  4. 参数验证:对用户输入进行严格的验证,确保数据有效性
  5. 场景键匹配:对于多参数场景,使用字符串键匹配比索引匹配更灵活
  6. 状态初始化:复杂对象类型的状态变量初始化为空对象而非null,避免空指针错误
  7. 主题色设计:选择符合应用定位的主题色,确保视觉体验一致

7.3 健身应用的社会责任

作为健身类应用,App34「体型雕刻家」承担着以下社会责任:

  1. 推广科学健身:提供专业的训练计划,帮助用户避免盲目训练导致的受伤
  2. 促进健康生活:通过个性化的饮食和训练建议,引导用户养成健康的生活习惯
  3. 关注用户安全:训练计划中包含适当的休息安排,避免过度训练
  4. 倡导可持续健身:强调循序渐进的训练理念,避免急功近利的训练方式
  5. 数据隐私保护:不收集用户敏感数据,保护用户隐私

核心技术实践总结

  1. 数据模型设计:通过5个接口定义清晰的数据结构,支持复杂的训练计划数据
  2. 场景键匹配:使用字符串键匹配,直观且易于扩展
  3. Fallback机制:提升应用健壮性,确保任何参数组合都能正常运行
  4. 属性冲突解决:避免使用ArkUI保留属性名,使用前缀区分状态变量
  5. 参数验证:训练天数限制1-7范围,增强数据有效性

未来展望

随着鸿蒙生态的不断发展,我们将继续优化应用体验,接入更多AI能力,为用户提供更加个性化、智能化的健身训练服务。同时,我们也将探索鸿蒙PC端和鸿蒙Flutter框架的开发方案,扩大应用的覆盖范围,让更多用户受益于科学健身。


附录:完整代码清单

import { router } from '@kit.ArkUI';

interface ExerciseItem {
  name: string;
  sets: string;
  reps: string;
  rest: string;
}

interface DayPlan {
  day: string;
  focus: string;
  exercises: ExerciseItem[];
}

interface MetricItem {
  name: string;
  target: string;
  unit: string;
}

interface FitnessResult {
  weekly: DayPlan[];
  diet_rules: string[];
  metrics: MetricItem[];
}

interface FitnessScenario {
  scenario: string;
  result: FitnessResult;
}

@Entry
@Component
struct App34 {
  @State userHeight: number = 170;
  @State userWeight: number = 70;
  @State goal: string = '增肌';
  @State daysPerWeek: number = 4;
  @State level: string = '新手';
  @State showResult: boolean = false;
  @State currentResult: FitnessResult = {
    weekly: [],
    diet_rules: [],
    metrics: []
  };

  private mockScenarios: FitnessScenario[] = [
    {
      scenario: '170_70_增肌_4_新手',
      result: {
        weekly: [
          {
            day: '周一', focus: '胸肌+三头',
            exercises: [
              { name: '杠铃平板卧推', sets: '4', reps: '10-12', rest: '90秒' },
              { name: '哑铃上斜卧推', sets: '4', reps: '10-12', rest: '90秒' },
              { name: '蝴蝶机夹胸', sets: '3', reps: '12-15', rest: '60秒' },
              { name: '绳索下压', sets: '3', reps: '12-15', rest: '60秒' },
              { name: '窄距俯卧撑', sets: '3', reps: '力竭', rest: '60秒' }
            ]
          },
          {
            day: '周二', focus: '休息',
            exercises: [
              { name: '轻度有氧(快走30分钟)', sets: '1', reps: '30分钟', rest: '-' }
            ]
          },
          {
            day: '周三', focus: '背部+二头',
            exercises: [
              { name: '引体向上(辅助)', sets: '4', reps: '8-10', rest: '90秒' },
              { name: '坐姿划船', sets: '4', reps: '10-12', rest: '90秒' },
              { name: '高位下拉', sets: '3', reps: '12-15', rest: '60秒' },
              { name: '哑铃弯举', sets: '3', reps: '12-15', rest: '60秒' },
              { name: '锤式弯举', sets: '3', reps: '12-15', rest: '60秒' }
            ]
          },
          {
            day: '周四', focus: '休息',
            exercises: [
              { name: '拉伸放松', sets: '1', reps: '15分钟', rest: '-' }
            ]
          },
          {
            day: '周五', focus: '腿部+肩部',
            exercises: [
              { name: '深蹲(史密斯机)', sets: '4', reps: '10-12', rest: '90秒' },
              { name: '腿举', sets: '4', reps: '10-12', rest: '90秒' },
              { name: '哑铃推举', sets: '3', reps: '10-12', rest: '60秒' },
              { name: '侧平举', sets: '3', reps: '15-20', rest: '60秒' },
              { name: '面拉', sets: '3', reps: '15-20', rest: '60秒' }
            ]
          },
          {
            day: '周六', focus: '全身综合',
            exercises: [
              { name: '罗马尼亚硬拉', sets: '3', reps: '10-12', rest: '90秒' },
              { name: '俯卧撑', sets: '3', reps: '力竭', rest: '60秒' },
              { name: '平板支撑', sets: '3', reps: '45秒', rest: '45秒' },
              { name: '农夫行走', sets: '3', reps: '30米', rest: '60秒' }
            ]
          },
          {
            day: '周日', focus: '休息',
            exercises: [
              { name: '完全休息', sets: '0', reps: '-', rest: '-' }
            ]
          }
        ],
        diet_rules: [
          '每日热量盈余300-500kcal',
          '蛋白质摄入量: 体重kg x 2g = 140g/天',
          '训练前1.5小时补充碳水',
          '训练后30分钟内补充蛋白质',
          '每天至少喝3L水'
        ],
        metrics: [
          { name: '体重目标', target: '75', unit: 'kg' },
          { name: '体脂率', target: '15', unit: '%' },
          { name: '卧推1RM', target: '80', unit: 'kg' },
          { name: '深蹲1RM', target: '100', unit: 'kg' }
        ]
      }
    },
    {
      scenario: '165_80_减脂_5_进阶',
      result: {
        weekly: [
          {
            day: '周一', focus: 'HIIT+上肢',
            exercises: [
              { name: '战绳', sets: '4', reps: '30秒', rest: '30秒' },
              { name: '波比跳', sets: '4', reps: '15', rest: '45秒' },
              { name: '哑铃卧推', sets: '4', reps: '12-15', rest: '45秒' },
              { name: '引体向上', sets: '4', reps: '10-12', rest: '60秒' },
              { name: '哑铃推举', sets: '3', reps: '12-15', rest: '45秒' }
            ]
          },
          {
            day: '周二', focus: '有氧耐力',
            exercises: [
              { name: '跑步机间歇跑', sets: '1', reps: '40分钟', rest: '-' },
              { name: '划船机', sets: '1', reps: '20分钟', rest: '-' },
              { name: '核心训练(卷腹+平板)', sets: '3', reps: '20+60秒', rest: '60秒' }
            ]
          },
          {
            day: '周三', focus: '下肢力量',
            exercises: [
              { name: '杠铃深蹲', sets: '5', reps: '8-10', rest: '90秒' },
              { name: '罗马尼亚硬拉', sets: '4', reps: '10-12', rest: '90秒' },
              { name: '保加利亚分腿蹲', sets: '3', reps: '12每腿', rest: '60秒' },
              { name: '腿弯举', sets: '3', reps: '15', rest: '45秒' },
              { name: '提踵', sets: '4', reps: '20', rest: '45秒' }
            ]
          },
          {
            day: '周四', focus: 'HIIT+核心',
            exercises: [
              { name: '跳绳', sets: '5', reps: '3分钟', rest: '60秒' },
              { name: '壶铃摇摆', sets: '4', reps: '20', rest: '45秒' },
              { name: '悬垂举腿', sets: '3', reps: '15', rest: '60秒' },
              { name: '俄罗斯转体', sets: '3', reps: '20', rest: '45秒' }
            ]
          },
          {
            day: '周五', focus: '上肢+有氧',
            exercises: [
              { name: '杠铃划船', sets: '4', reps: '10-12', rest: '90秒' },
              { name: '双杠臂屈伸', sets: '4', reps: '10-12', rest: '60秒' },
              { name: '侧平举超级组', sets: '3', reps: '15-20', rest: '30秒' },
              { name: '动感单车', sets: '1', reps: '30分钟', rest: '-' }
            ]
          },
          {
            day: '周六', focus: '户外有氧',
            exercises: [
              { name: '户外跑步5km', sets: '1', reps: '30分钟', rest: '-' },
              { name: '拉伸瑜伽', sets: '1', reps: '30分钟', rest: '-' }
            ]
          },
          {
            day: '周日', focus: '休息',
            exercises: [
              { name: '泡沫轴放松', sets: '1', reps: '20分钟', rest: '-' }
            ]
          }
        ],
        diet_rules: [
          '每日热量赤字500-800kcal',
          '蛋白质摄入量: 体重kg x 2.2g = 176g/天',
          '碳水循环: 训练日高碳,休息日低碳',
          '训练前黑咖啡可提升代谢',
          '每天至少喝4L水',
          '晚上8点后不进食'
        ],
        metrics: [
          { name: '体重目标', target: '70', unit: 'kg' },
          { name: '体脂率', target: '12', unit: '%' },
          { name: '腰围', target: '80', unit: 'cm' },
          { name: '5km跑', target: '25', unit: '分钟' }
        ]
      }
    },
    {
      scenario: '180_85_增肌_5_进阶',
      result: {
        weekly: [
          {
            day: '周一', focus: '胸肌+三头(大重量)',
            exercises: [
              { name: '杠铃平板卧推', sets: '5', reps: '6-8', rest: '120秒' },
              { name: '哑铃上斜卧推', sets: '4', reps: '8-10', rest: '90秒' },
              { name: '负重双杠臂屈伸', sets: '4', reps: '8-10', rest: '90秒' },
              { name: '窄距卧推', sets: '4', reps: '8-10', rest: '90秒' },
              { name: 'EZ杠臂屈伸', sets: '3', reps: '10-12', rest: '60秒' }
            ]
          },
          {
            day: '周二', focus: '背部+二头',
            exercises: [
              { name: '硬拉', sets: '5', reps: '5-8', rest: '120秒' },
              { name: '负重引体向上', sets: '4', reps: '6-8', rest: '90秒' },
              { name: '杠铃划船', sets: '4', reps: '8-10', rest: '90秒' },
              { name: '杠铃弯举', sets: '4', reps: '8-10', rest: '60秒' },
              { name: '牧师凳弯举', sets: '3', reps: '10-12', rest: '60秒' }
            ]
          },
          {
            day: '周三', focus: '腿部',
            exercises: [
              { name: '杠铃深蹲', sets: '5', reps: '5-8', rest: '120秒' },
              { name: '腿举', sets: '4', reps: '8-10', rest: '90秒' },
              { name: '罗马尼亚硬拉', sets: '4', reps: '8-10', rest: '90秒' },
              { name: '保加利亚分腿蹲', sets: '3', reps: '10每腿', rest: '60秒' },
              { name: '站姿提踵', sets: '4', reps: '15-20', rest: '60秒' }
            ]
          },
          {
            day: '周四', focus: '肩部+斜方',
            exercises: [
              { name: '站姿杠铃推举', sets: '5', reps: '6-8', rest: '90秒' },
              { name: '哑铃侧平举', sets: '4', reps: '12-15', rest: '60秒' },
              { name: '俯身飞鸟', sets: '4', reps: '12-15', rest: '60秒' },
              { name: '杠铃耸肩', sets: '4', reps: '10-12', rest: '60秒' },
              { name: '面拉', sets: '3', reps: '15-20', rest: '60秒' }
            ]
          },
          {
            day: '周五', focus: '手臂+腹部',
            exercises: [
              { name: '窄距卧推', sets: '4', reps: '8-10', rest: '90秒' },
              { name: 'EZ杠弯举', sets: '4', reps: '8-10', rest: '60秒' },
              { name: '绳索下压', sets: '3', reps: '12-15', rest: '45秒' },
              { name: '锤式弯举', sets: '3', reps: '12-15', rest: '45秒' },
              { name: '悬垂举腿', sets: '4', reps: '15', rest: '60秒' }
            ]
          },
          {
            day: '周六', focus: '弱项补强',
            exercises: [
              { name: '针对弱项训练', sets: '3-4', reps: '8-12', rest: '90秒' },
              { name: '轻度有氧', sets: '1', reps: '20分钟', rest: '-' }
            ]
          },
          {
            day: '周日', focus: '完全休息',
            exercises: [
              { name: '泡沫轴放松', sets: '1', reps: '30分钟', rest: '-' }
            ]
          }
        ],
        diet_rules: [
          '每日热量盈余500-800kcal',
          '蛋白质摄入量: 体重kg x 2.5g = 213g/天',
          '碳水摄入量: 体重kg x 5g = 425g/天',
          '训练前30分钟补充快碳+咖啡因',
          '训练后立即补充乳清蛋白+快碳',
          '每天至少喝5L水',
          '睡前补充酪蛋白(慢释放蛋白)'
        ],
        metrics: [
          { name: '体重目标', target: '92', unit: 'kg' },
          { name: '体脂率', target: '15', unit: '%' },
          { name: '卧推1RM', target: '120', unit: 'kg' },
          { name: '深蹲1RM', target: '160', unit: 'kg' },
          { name: '硬拉1RM', target: '180', unit: 'kg' }
        ]
      }
    }
  ];

  getMockData(): void {
    let key: string = this.userHeight.toString() + '_' + this.userWeight.toString() + '_' + this.goal + '_' + this.daysPerWeek.toString() + '_' + this.level;
    let found: FitnessScenario | null = null;
    for (let i = 0; i < this.mockScenarios.length; i++) {
      if (this.mockScenarios[i].scenario === key) {
        found = this.mockScenarios[i];
        break;
      }
    }
    if (found === null) {
      found = this.mockScenarios[0];
    }
    if (found !== null) {
      this.currentResult = found.result;
    }
    this.showResult = true;
  }

  generateMockData(): void {
    this.getMockData();
  }

  build() {
    Column() {
      Row() {
        Button('← 返回')
          .fontSize(14)
          .backgroundColor('#FFFFFF')
          .fontColor('#333333')
          .onClick(() => {
            router.back();
          })
      }
      .width('100%')
      .padding({ left: 16, top: 12, bottom: 8 })
      .backgroundColor('#FFFFFF')

      Text('体型雕刻家')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .padding({ left: 16, top: 8, bottom: 4 })
        .width('100%')
        .backgroundColor('#FFFFFF')

      Text('AI健身教练,科学打造理想体型')
        .fontSize(14)
        .fontColor('#999999')
        .padding({ left: 16, bottom: 12 })
        .width('100%')
        .backgroundColor('#FFFFFF')

      Scroll() {
        Column() {
          Column() {
            Text('身高 (cm)')
              .fontSize(14)
              .fontColor('#666666')
              .padding({ left: 16, top: 16, bottom: 4 })
            TextInput({ text: this.userHeight.toString() })
              .width('calc(100% - 32px)')
              .height(44)
              .backgroundColor('#FFFFFF')
              .borderRadius(8)
              .type(InputType.Number)
              .margin({ left: 16, right: 16 })
              .onChange((value: string) => {
                this.userHeight = parseInt(value) || 160;
              })
          }
          .width('100%')

          Column() {
            Text('体重 (kg)')
              .fontSize(14)
              .fontColor('#666666')
              .padding({ left: 16, top: 12, bottom: 4 })
            TextInput({ text: this.userWeight.toString() })
              .width('calc(100% - 32px)')
              .height(44)
              .backgroundColor('#FFFFFF')
              .borderRadius(8)
              .type(InputType.Number)
              .margin({ left: 16, right: 16 })
              .onChange((value: string) => {
                this.userWeight = parseInt(value) || 50;
              })
          }
          .width('100%')

          Column() {
            Text('目标')
              .fontSize(14)
              .fontColor('#666666')
              .padding({ left: 16, top: 12, bottom: 4 })
            Row() {
              Button('减脂')
                .fontSize(13)
                .backgroundColor(this.goal === '减脂' ? '#FF6B6B' : '#E8E8E8')
                .fontColor(this.goal === '减脂' ? '#FFFFFF' : '#333333')
                .borderRadius(20)
                .height(36)
                .onClick(() => { this.goal = '减脂'; })
              Button('增肌')
                .fontSize(13)
                .backgroundColor(this.goal === '增肌' ? '#FF6B6B' : '#E8E8E8')
                .fontColor(this.goal === '增肌' ? '#FFFFFF' : '#333333')
                .borderRadius(20)
                .height(36)
                .margin({ left: 8 })
                .onClick(() => { this.goal = '增肌'; })
            }
            .padding({ left: 16, top: 4 })
            .width('100%')
          }
          .width('100%')

          Column() {
            Text('每周训练天数')
              .fontSize(14)
              .fontColor('#666666')
              .padding({ left: 16, top: 12, bottom: 4 })
            TextInput({ text: this.daysPerWeek.toString() })
              .width('calc(100% - 32px)')
              .height(44)
              .backgroundColor('#FFFFFF')
              .borderRadius(8)
              .type(InputType.Number)
              .margin({ left: 16, right: 16 })
              .onChange((value: string) => {
                let num: number = parseInt(value) || 3;
                if (num < 1) { num = 1; }
                if (num > 7) { num = 7; }
                this.daysPerWeek = num;
              })
          }
          .width('100%')

          Column() {
            Text('训练级别')
              .fontSize(14)
              .fontColor('#666666')
              .padding({ left: 16, top: 12, bottom: 4 })
            Row() {
              Button('新手')
                .fontSize(13)
                .backgroundColor(this.level === '新手' ? '#FF6B6B' : '#E8E8E8')
                .fontColor(this.level === '新手' ? '#FFFFFF' : '#333333')
                .borderRadius(20)
                .height(36)
                .onClick(() => { this.level = '新手'; })
              Button('进阶')
                .fontSize(13)
                .backgroundColor(this.level === '进阶' ? '#FF6B6B' : '#E8E8E8')
                .fontColor(this.level === '进阶' ? '#FFFFFF' : '#333333')
                .borderRadius(20)
                .height(36)
                .margin({ left: 8 })
                .onClick(() => { this.level = '进阶'; })
            }
            .padding({ left: 16, top: 4 })
            .width('100%')
          }
          .width('100%')

          Button('生成训练计划')
            .width('calc(100% - 32px)')
            .height(48)
            .backgroundColor('#FF6B6B')
            .fontColor('#FFFFFF')
            .fontSize(16)
            .fontWeight(FontWeight.Medium)
            .borderRadius(24)
            .margin({ top: 24, left: 16, right: 16 })
            .onClick(() => {
              this.generateMockData();
            })

          if (this.showResult) {
            Column() {
              Text('目标指标')
                .fontSize(18)
                .fontWeight(FontWeight.Bold)
                .padding({ top: 8, bottom: 8 })
                .width('100%')

              Row() {
                ForEach(this.currentResult.metrics, (metric: MetricItem) => {
                  Column() {
                    Text(metric.target + metric.unit)
                      .fontSize(18)
                      .fontWeight(FontWeight.Bold)
                      .fontColor('#FF6B6B')
                    Text(metric.name)
                      .fontSize(11)
                      .fontColor('#999999')
                      .padding({ top: 2 })
                  }
                  .alignItems(HorizontalAlign.Center)
                  .margin({ left: 4, right: 4 })
                })
              }
              .width('100%')
              .justifyContent(FlexAlign.SpaceAround)
              .padding({ bottom: 12 })

              Divider().color('#E8E8E8')

              Text('饮食规则')
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .padding({ top: 12, bottom: 4 })
                .width('100%')
              ForEach(this.currentResult.diet_rules, (rule: string) => {
                Text('• ' + rule)
                  .fontSize(13)
                  .fontColor('#333333')
                  .width('100%')
                  .padding({ bottom: 3 })
              })

              Divider().color('#E8E8E8')

              Text('每周训练计划')
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .padding({ top: 12, bottom: 8 })
                .width('100%')

              ForEach(this.currentResult.weekly, (dayPlan: DayPlan) => {
                Column() {
                  Row() {
                    Text(dayPlan.day)
                      .fontSize(14)
                      .fontWeight(FontWeight.Bold)
                      .fontColor('#FF6B6B')
                      .backgroundColor('#FFF0F0')
                      .borderRadius(4)
                      .padding({ left: 8, right: 8, top: 2, bottom: 2 })
                    Text('  ' + dayPlan.focus)
                      .fontSize(14)
                      .fontColor('#333333')
                  }
                  .padding({ bottom: 4 })

                  ForEach(dayPlan.exercises, (ex: ExerciseItem) => {
                    Text('  ' + ex.name + '  ' + ex.sets + '组 x ' + ex.reps + '  休息' + ex.rest)
                      .fontSize(12)
                      .fontColor('#666666')
                      .padding({ bottom: 2 })
                  })
                }
                .width('100%')
                .padding({ bottom: 8 })
                .margin({ bottom: 4 })
              })
            }
            .width('calc(100% - 32px)')
            .backgroundColor('#FFFFFF')
            .borderRadius(12)
            .padding(16)
            .margin({ top: 16, left: 16, right: 16, bottom: 24 })
          }
        }
        .width('100%')
      }
      .layoutWeight(1)
      .width('100%')
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }
}
Logo

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

更多推荐