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


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 |
需显式定义所有数据接口 | 定义 ExerciseItem、DayPlan、MetricItem、FitnessResult、FitnessScenario 接口 |
| 禁止解构赋值 | 函数参数需逐个声明 | 使用标准函数参数格式 |
禁止 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;
}
设计优点:
- 直观性:场景键直接反映参数组合,便于理解和维护
- 扩展性:新增场景只需添加新的
FitnessScenario对象 - 健壮性:无匹配时自动使用默认数据,不会崩溃
三、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 数据策略深度解析
数据设计原则:
- 场景多样性:覆盖不同目标(增肌/减脂)、不同级别(新手/进阶)、不同体型(165cm/170cm/180cm)
- 训练科学性:每个训练计划遵循健身科学原则(分化训练、渐进超负荷、休息恢复)
- 饮食配套:训练计划搭配相应的饮食规则,形成完整的健身方案
- 目标量化:提供可衡量的目标指标,帮助用户跟踪进度
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 声明变量 |
| 显式返回类型 | ✅ 通过 | getMockData 和 generateMockData 均标注 : void |
| Scroll 单子节点 | ✅ 通过 | Scroll 包含单个 Column 子组件 |
| ForEach keyGenerator | ✅ 通过 | 使用隐式key或基于item属性的key |
4.2 代码质量审查
优点:
- 接口定义完整:5个接口设计合理,覆盖所有数据结构
- 场景键匹配算法健壮:包含fallback机制
- 参数输入验证完善:训练天数限制1-7范围,数字输入有默认值处理
- Mock数据丰富:3个场景涵盖不同目标和级别
- 代码结构清晰:方法职责单一,UI与逻辑分离
待改进项:
- 缺少加载状态:当前无 loading 状态,用户点击按钮后无即时反馈
- 场景覆盖率低:仅3个场景,大部分参数组合会触发fallback
- 缺少详细的训练动作说明:仅显示动作名称,无动作要领说明
4.3 关键Bug分析:height/weight属性冲突
问题描述:
在开发初期,代码中使用了 height 和 weight 作为状态变量名:
@State height: number = 170;
@State weight: number = 70;
运行后发现以下异常:
- UI布局混乱,组件位置偏移
- 状态更新后UI不响应
- 部分组件尺寸异常
根因分析:
ArkUI组件(如Column、Row、Text等)内置了 height 和 weight 属性,用于布局控制:
height:组件的高度属性weight:flex布局中的权重属性
当开发者在自定义组件中声明同名的 @State 变量时,会产生命名冲突。ArkUI框架会将这些变量解释为布局属性而非状态变量,导致:
- 状态变量被当作布局属性处理
- 布局计算干扰了状态管理
- UI渲染出现不可预期的行为
修复方案:
将变量名重命名为 userHeight 和 userWeight:
@State userHeight: number = 170;
@State userWeight: number = 70;
同时更新所有引用位置:
this.height→this.userHeightthis.weight→this.userWeight
影响范围:
| 文件 | 修改内容 |
|---|---|
pages/app34/Index.ets |
状态变量声明 |
pages/app34/Index.ets |
getMockData方法 |
pages/app34/Index.ets |
TextInput onChange回调 |
pages/app34/Index.ets |
build方法中的数据绑定 |
教训总结:
- 避免使用ArkUI保留属性名:在声明状态变量时,避免使用height、weight、width、opacity等ArkUI组件的内置属性名
- 命名约定:建议在状态变量前添加前缀(如user、my、current等),明确区分状态变量和布局属性
- 代码审查重点:在代码审查时,特别关注状态变量命名是否与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(珊瑚红)
设计理由:
- 活力感:红色代表活力和激情,符合健身主题
- 与App3的区别:App3「肌动实验室」使用
#4ECDC4(青色),App34使用珊瑚红,区分两个健身应用 - 视觉冲击力:珊瑚红比纯红色更柔和,适合长时间使用
色彩体系:
| 用途 | 颜色 | 说明 |
|---|---|---|
| 主色 | #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 技术亮点总结
- 场景键匹配模式:使用字符串键匹配,直观且易于扩展
- Fallback机制:提升应用健壮性,确保任何参数组合都能正常运行
- height/weight重命名修复:解决了ArkUI属性冲突问题
- 参数边界检查:训练天数限制1-7范围,增强数据有效性
- 完整的Mock数据:3个场景涵盖不同目标和级别,内容专业
6.3.1 与 App3 肌动实验室的对比分析
App34「体型雕刻家」和 App3「肌动实验室」同属AI40工具箱中的健身类应用,但在定位、功能和设计上存在显著差异:
| 对比维度 | App34 体型雕刻家 | App3 肌动实验室 |
|---|---|---|
| 核心定位 | 完整的一周训练计划 | 针对特定部位的训练动作推荐 |
| 输入参数 | 身高、体重、目标、训练天数、级别 | 训练部位、可用器械、训练时长 |
| 输出内容 | 7天完整训练计划+饮食规则+目标指标 | 单个部位的训练动作列表 |
| 数据模型 | DayPlan + FitnessResult + FitnessScenario |
ExerciseItem + RoutineOutput |
| 匹配策略 | 字符串场景键匹配 | 索引匹配 |
| 主题色 | 珊瑚红 #FF6B6B | 青色 #4ECDC4 |
| 适用场景 | 系统性健身计划制定 | 单次训练动作选择 |
| 目标用户 | 有长期健身目标的用户 | 日常训练的用户 |
关键洞察:
- 互补性:两个应用互补,App3适合日常训练,App34适合制定长期计划
- 数据复杂度:App34的数据模型更复杂,包含周计划、饮食规则、目标指标等
- 匹配策略差异:App3使用简单的索引匹配,App34使用更灵活的字符串场景键匹配
- 视觉区分:通过不同的主题色区分两个健身应用
6.4 改进方向
短期改进:
- 添加加载状态动画:参考其他AI40应用的800ms延迟加载模式
- 扩展Mock场景:增加更多参数组合的场景数据
- 添加训练动作说明:为每个动作添加简要的动作要领
中期改进:
- 接入真实AI服务,实现全参数空间的个性化训练计划生成
- 添加训练记录功能,跟踪用户训练进度
- 添加社交分享功能,支持分享训练计划
长期改进:
- 集成鸿蒙健康数据,结合心率、睡眠等数据优化训练计划
- 开发鸿蒙PC端适配版本,支持更大屏幕的计划展示
- 探索鸿蒙Flutter框架混合开发方案,提升跨平台能力
6.5 健身训练理论背景
App34「体型雕刻家」的训练计划设计基于以下健身科学原理,确保训练方案的专业性和有效性:
6.5.1 分化训练原理
分化训练是将全身肌肉群分成不同部位,在不同时间进行训练的方法。这种方法可以:
- 提高训练强度:每次训练专注于少数肌群,可以使用更大的重量和更充分的刺激
- 促进恢复:不同肌群交替训练,给肌肉足够的时间修复和生长
- 避免过度训练:避免连续训练同一肌群导致的疲劳和受伤
App34的三个场景采用了不同的分化策略:
- 新手增肌场景:三分化训练(胸+三头、背+二头、腿+肩),每周4天
- 进阶减脂场景:全身训练+HIIT结合,每周5天
- 进阶增肌场景:五分化训练(胸、背、腿、肩、手臂),每周5天
6.5.2 渐进超负荷原则
渐进超负荷是肌肉生长的核心原则,指逐步增加训练强度以刺激肌肉适应和生长。App34的训练计划体现了这一原则:
- 重量递增:进阶场景使用更大的重量(如6-8次范围),新手场景使用中等重量(10-12次范围)
- 组数递增:进阶场景增加训练组数(5组 vs 4组)
- 强度递增:进阶场景缩短休息时间,增加训练密度
6.5.3 训练量与训练强度的平衡
训练量(总次数×组数)和训练强度(重量/次数)是训练效果的两个关键因素:
| 场景 | 训练量 | 训练强度 | 适应人群 |
|---|---|---|---|
| 新手增肌 | 中高训练量,中等强度 | 10-15次范围,90秒休息 | 初学者,建立训练基础 |
| 进阶减脂 | 高训练量,中高强度 | 12-15次+HIIT,45-60秒休息 | 有基础,追求减脂效率 |
| 进阶增肌 | 中等训练量,高强度 | 6-8次范围,90-120秒休息 | 有经验,追求肌肉增长 |
6.5.4 休息与恢复的重要性
训练后的恢复时间直接影响肌肉生长和训练效果:
- 组间休息:力量训练需要60-120秒恢复ATP和磷酸肌酸
- 肌肉恢复:同一肌群需要48-72小时恢复时间
- 睡眠恢复:睡眠期间生长激素分泌达到峰值,是肌肉修复的关键时期
App34的训练计划充分考虑了休息安排:
- 新手场景每周3天休息,确保充分恢复
- 进阶场景安排主动恢复(拉伸、轻度有氧)
- 休息日提供放松建议(泡沫轴按摩、瑜伽)
6.6 用户体验设计分析
6.6.1 交互设计原则
App34的交互设计遵循以下原则:
- 简洁性:5个输入参数,一键生成计划,减少用户操作步骤
- 反馈性:按钮点击有颜色变化反馈,参数选择即时显示
- 一致性:主题色贯穿整个界面,按钮样式统一
- 可读性:训练计划使用清晰的层级结构,便于阅读
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端提供更丰富的交互体验:
- 分栏布局:利用PC端更大的屏幕空间,将输入区域和结果区域放在左右分栏,用户可以同时看到参数和计划
- 打印功能:支持将训练计划打印出来,方便用户在健身房使用
- 拖拽排序:支持拖拽调整训练动作顺序,个性化定制计划
- 多窗口支持:支持在工作窗口旁打开训练计划,方便用户随时查看
鸿蒙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和物联网技术的发展,健身应用正在经历以下技术趋势:
- AI个性化推荐:基于用户历史训练数据和身体数据,提供更精准的个性化训练计划
- 生物反馈集成:连接智能手表、心率带、体脂秤等设备,实时监测训练效果
- 虚拟现实训练:结合VR/AR技术,提供沉浸式训练体验
- 社交互动:建立用户社区,支持训练打卡、同伴激励、教练指导
- 端侧AI推理:在设备端运行AI模型,保护用户隐私,提升响应速度
App34「体型雕刻家」已经预留了AI API接口,可以在未来接入这些技术趋势,为用户提供更加智能、个性化的健身训练服务。
七、结语
App34「体型雕刻家」是一个专注于健身训练计划的AI应用,通过个性化的参数输入和专业的Mock数据,为用户提供完整的一周训练计划。在开发过程中,我们遇到了ArkUI属性冲突的关键问题,并通过变量重命名成功解决。
7.1 6A工作流实践反思
通过本次开发实践,我们总结了以下6A工作流的实践经验:
- Align阶段:需求对齐时要充分考虑技术约束,特别是ArkTS严格模式的限制
- Architect阶段:架构设计要兼顾扩展性和当前需求,避免过度设计
- Atomize阶段:任务分解要足够细粒度,确保每个任务都有明确的验收标准
- Approve阶段:代码审查要重点关注ArkUI属性冲突等常见问题
- Automate阶段:实现时要注重参数验证和异常处理,提升应用健壮性
- Assess阶段:评估时要全面分析优缺点,为后续改进提供方向
7.2 鸿蒙AI应用开发最佳实践
基于App34的开发经验,我们总结了以下鸿蒙AI应用开发的最佳实践:
- 变量命名约定:避免使用ArkUI组件的保留属性名(height、weight、width、opacity等),建议添加前缀(user、my、current等)
- Mock数据先行:在接入真实AI服务前,使用Mock数据确保应用的基本功能可用
- Fallback机制:实现完善的异常处理和fallback机制,确保应用在任何情况下都能正常运行
- 参数验证:对用户输入进行严格的验证,确保数据有效性
- 场景键匹配:对于多参数场景,使用字符串键匹配比索引匹配更灵活
- 状态初始化:复杂对象类型的状态变量初始化为空对象而非null,避免空指针错误
- 主题色设计:选择符合应用定位的主题色,确保视觉体验一致
7.3 健身应用的社会责任
作为健身类应用,App34「体型雕刻家」承担着以下社会责任:
- 推广科学健身:提供专业的训练计划,帮助用户避免盲目训练导致的受伤
- 促进健康生活:通过个性化的饮食和训练建议,引导用户养成健康的生活习惯
- 关注用户安全:训练计划中包含适当的休息安排,避免过度训练
- 倡导可持续健身:强调循序渐进的训练理念,避免急功近利的训练方式
- 数据隐私保护:不收集用户敏感数据,保护用户隐私
核心技术实践总结:
- 数据模型设计:通过5个接口定义清晰的数据结构,支持复杂的训练计划数据
- 场景键匹配:使用字符串键匹配,直观且易于扩展
- Fallback机制:提升应用健壮性,确保任何参数组合都能正常运行
- 属性冲突解决:避免使用ArkUI保留属性名,使用前缀区分状态变量
- 参数验证:训练天数限制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')
}
}
更多推荐




所有评论(0)