
一、应用概述BMIBody Mass Index身体质量指数是世界卫生组织推荐的肥胖判断标准计算公式为体重(kg) / 身高(m)²。本应用基于 ArkTS 构建一个交互式 BMI 计算器支持身高体重输入、即时计算、分类判定偏瘦/正常/偏胖/肥胖并通过彩色渐变进度条直观展示 BMI 指数范围。1.1 功能特性特性描述双输入身高cm和体重kg输入支持滑动和键盘即时计算输入变化时自动重算 BMI 值分类判定根据中国标准分四类偏瘦/正常/偏胖/肥胖彩色进度条根据 BMI 值区间显示不同颜色蓝/绿/橙/红标准范围提示显示当前分类的建议体重范围单位切换支持 cm/kg 和 ft/lb 两种单位制历史记录本地保存最近 10 次计算记录1.2 BMI 分类标准中国标准分类BMI 范围颜色标识健康建议偏瘦 18.5 #2196F3建议适当增加营养摄入正常18.5 ~ 23.9 #4CAF50保持当前生活习惯偏胖24.0 ~ 27.9 #FF9800建议增加运动量肥胖≥ 28.0 #F44336建议咨询专业医师二、系统架构设计2.1 整体架构┌─────────────────────────────────────────────┐ │ UI 表现层 │ │ ┌─────────┐ ┌────────┐ ┌──────────────┐ │ │ │InputPanel│ │BMIGauge│ │CategoryCard │ │ │ └────┬────┘ └───┬────┘ └──────┬───────┘ │ │ │ │ │ │ │ ┌────┴──────────┴─────────────┴──────────┐ │ │ │ BMICalculatorMain │ │ │ │ (Entry Component) │ │ │ └─────────────────────────────────────────│ │ ├──────────────────────────────────────────────┤ │ 业务逻辑层 │ │ ┌────────────────────────────────────────┐ │ │ │ BMIEngine │ │ │ │ - calculateBMI(weight, height): number│ │ │ │ - getCategory(bmi): BMICategory │ │ │ │ - getIdealWeightRange(height): Range │ │ │ │ - getProgressPercent(bmi): number │ │ │ └────────────────────────────────────────┘ │ ├──────────────────────────────────────────────┤ │ 持久化层 │ │ ┌────────────────────────────────────────┐ │ │ │ Preferences (历史记录存储) │ │ │ └────────────────────────────────────────┘ │ └──────────────────────────────────────────────┘2.2 数据模型// BMI 分类枚举 enum BMICategory { UNDERWEIGHT 偏瘦, NORMAL 正常, OVERWEIGHT 偏胖, OBESE 肥胖 } // 分类颜色方案 interface CategoryStyle { category: BMICategory; minBMI: number; maxBMI: number; color: string; // 主色 bgColor: string; // 背景色 description: string; // 健康建议 } // 计算记录 interface BMIRecord { timestamp: number; height: number; weight: number; bmi: number; category: BMICategory; }三、核心代码深度解析3.1 完整应用代码// pages/BMICalculatorPage.ets import { promptAction } from kit.ArkUI; // 枚举与类型定义 enum BMICategory { UNDERWEIGHT 偏瘦, NORMAL 正常, OVERWEIGHT 偏胖, OBESE 肥胖 } interface CategoryStyle { category: BMICategory; minBMI: number; maxBMI: number; color: string; bgColor: string; icon: string; advice: string; } interface BMIRecord { timestamp: number; height: number; weight: number; bmi: number; category: BMICategory; } // 分类配置中国标准 const CATEGORY_STYLES: CategoryStyle[] [ { category: BMICategory.UNDERWEIGHT, minBMI: 0, maxBMI: 18.5, color: #2196F3, bgColor: #E3F2FD, icon: ⚡, advice: 建议适当增加营养摄入保持规律作息 }, { category: BMICategory.NORMAL, minBMI: 18.5, maxBMI: 24.0, color: #4CAF50, bgColor: #E8F5E9, icon: ✅, advice: 非常棒请保持当前的饮食和运动习惯 }, { category: BMICategory.OVERWEIGHT, minBMI: 24.0, maxBMI: 28.0, color: #FF9800, bgColor: #FFF3E0, icon: ⚠️, advice: 建议适当增加运动量注意饮食结构 }, { category: BMICategory.OBESE, minBMI: 28.0, maxBMI: 100, color: #F44336, bgColor: #FFEBEE, icon: , advice: 建议及时咨询专业医师制定健康计划 } ]; // BMI 计算引擎 class BMIEngine { // 计算 BMI static calculateBMI(weightKg: number, heightCm: number): number { if (heightCm 0 || weightKg 0) return 0; const heightM heightCm / 100; return parseFloat((weightKg / (heightM * heightM)).toFixed(1)); } // 获取 BMI 分类 static getCategory(bmi: number): CategoryStyle { const result CATEGORY_STYLES.find( s bmi s.minBMI bmi s.maxBMI ); return result ?? CATEGORY_STYLES[1]; // 默认返回正常 } // 计算理想体重范围BMI 18.5~23.9 static getIdealWeightRange(heightCm: number): { min: number, max: number } { const heightM heightCm / 100; return { min: parseFloat((18.5 * heightM * heightM).toFixed(1)), max: parseFloat((23.9 * heightM * heightM).toFixed(1)) }; } // 获取 BMI 进度百分比0~100 static getProgressPercent(bmi: number): number { const maxDisplayBMI 35; // 35 以上视为 100% return Math.min(100, Math.max(0, (bmi / maxDisplayBMI) * 100)); } // 输出格式化 BMI static formatBMI(bmi: number): string { return bmi.toFixed(1); } } // 子组件输入面板 Component struct InputPanel { private height: number 170; private weight: number 65; private onHeightChange?: (val: number) void; private onWeightChange?: (val: number) void; build() { Column() { // ---- 身高输入 ---- Text(身高) .fontSize(14) .fontColor(#666666) .width(100%) Row() { TextInput({ placeholder: 请输入身高, text: this.height.toString() }) .type(InputType.Number) .width(120) .height(44) .fontSize(20) .fontWeight(FontWeight.Bold) .backgroundColor(#F5F5F5) .borderRadius(10) .padding({ left: 12 }) .onChange((val: string) { const num parseInt(val.replace(/\D/g, )); if (!isNaN(num) this.onHeightChange) { this.onHeightChange(num); } }) Text(cm) .fontSize(16) .fontColor(#999999) .margin({ left: 8 }) } .width(100%) .alignItems(VerticalAlign.Center) .margin({ bottom: 8 }) // 身高滑块 Slider({ value: this.height, min: 100, max: 220, step: 1, style: SliderStyle.OutSet }) .width(100%) .trackThickness(4) .blockColor(#4A90D9) .trackColor(#E0E0E0) .selectedColor(#4A90D9) .onChange((val: number) { if (this.onHeightChange) { this.onHeightChange(val); } }) .margin({ bottom: 20 }) // ---- 体重输入 ---- Text(体重) .fontSize(14) .fontColor(#666666) .width(100%) Row() { TextInput({ placeholder: 请输入体重, text: this.weight.toString() }) .type(InputType.Number) .width(120) .height(44) .fontSize(20) .fontWeight(FontWeight.Bold) .backgroundColor(#F5F5F5) .borderRadius(10) .padding({ left: 12 }) .onChange((val: string) { const num parseInt(val.replace(/\D/g, )); if (!isNaN(num) this.onWeightChange) { this.onWeightChange(num); } }) Text(kg) .fontSize(16) .fontColor(#999999) .margin({ left: 8 }) } .width(100%) .alignItems(VerticalAlign.Center) .margin({ bottom: 8 }) // 体重滑块 Slider({ value: this.weight, min: 30, max: 200, step: 0.5, style: SliderStyle.OutSet }) .width(100%) .trackThickness(4) .blockColor(#FF6B35) .trackColor(#E0E0E0) .selectedColor(#FF6B35) .onChange((val: number) { if (this.onWeightChange) { this.onWeightChange(val); } }) } .width(100%) .padding(20) .backgroundColor(#FFFFFF) .borderRadius(16) .shadow({ radius: 8, color: rgba(0,0,0,0.06), offsetX: 0, offsetY: 4 }) } } // 子组件BMI 环形进度条 Component struct BMIGauge { private bmi: number 0; private color: string #4CAF50; private percent: number 0; build() { Column() { // 进度条容器 Stack() { // 背景圆环 Circle() .width(180) .height(180) .stroke(#E8E8E8) .strokeWidth(12) .fillOpacity(0) // 前景彩色圆环使用 Arc 模拟进度 Circle() .width(180) .height(180) .stroke(this.color) .strokeWidth(12) .fillOpacity(0) .strokeLineCap(LineCapStyle.Round) .percent(this.percent * 0.75) // 最大显示 270° .animation({ duration: 600, curve: Curve.EaseOut }) // 中心显示 BMI 值 Column() { Text(BMI) .fontSize(14) .fontColor(#999999) Text(BMIEngine.formatBMI(this.bmi)) .fontSize(42) .fontWeight(FontWeight.Bold) .fontColor(this.color) .animation({ duration: 400, curve: Curve.EaseOut }) } .align(Alignment.Center) } .width(180) .height(180) } .width(100%) .alignItems(HorizontalAlign.Center) } } // 子组件分类卡片 Component struct CategoryCard { private categoryStyle: CategoryStyle CATEGORY_STYLES[1]; private idealWeight: { min: number, max: number } { min: 0, max: 0 }; private height: number 170; build() { Column() { // 分类标志 Row() { Text(this.categoryStyle.icon) .fontSize(32) .margin({ right: 12 }) Column() { Text(this.categoryStyle.category) .fontSize(24) .fontWeight(FontWeight.Bold) .fontColor(this.categoryStyle.color) Text(BMI ${BMIEngine.formatBMI(0)}) .fontSize(14) .fontColor(#999999) } } .width(100%) .padding(16) .backgroundColor(this.categoryStyle.bgColor) .borderRadius(12) // 建议 Text(this.categoryStyle.advice) .fontSize(14) .fontColor(#666666) .margin({ top: 16 }) .lineHeight(22) // 理想体重范围 if (this.height 0) { Row() { Text(理想体重范围) .fontSize(13) .fontColor(#888888) Text(${this.idealWeight.min} kg ~ ${this.idealWeight.max} kg) .fontSize(15) .fontWeight(FontWeight.Medium) .fontColor(#333333) .margin({ left: 8 }) } .width(100%) .justifyContent(FlexAlign.SpaceBetween) .padding({ top: 12 }) .border({ top: { width: 1, color: #F0F0F0 } }) .margin({ top: 12 }) } } .width(100%) .padding(20) .backgroundColor(#FFFFFF) .borderRadius(16) .shadow({ radius: 8, color: rgba(0,0,0,0.06), offsetX: 0, offsetY: 4 }) } } // 子组件历史记录 Component struct HistoryPanel { private records: BMIRecord[] []; private onClear?: () void; build() { if (this.records.length 0) { Column() { Text(暂无历史记录) .fontSize(14) .fontColor(#CCCCCC) .padding(20) } .width(100%) .alignItems(HorizontalAlign.Center) } else { Column() { Row() { Text(历史记录) .fontSize(16) .fontWeight(FontWeight.Medium) .fontColor(#333333) Text(清空) .fontSize(13) .fontColor(#FF6B35) .onClick(() { if (this.onClear) { this.onClear(); } }) } .width(100%) .justifyContent(FlexAlign.SpaceBetween) .padding({ bottom: 12 }) ForEach(this.records, (record: BMIRecord, index: number) { Row() { Text(#${index 1}) .fontSize(12) .fontColor(#BBBBBB) .width(30) Text(${record.height}cm / ${record.weight}kg) .fontSize(14) .fontColor(#555555) .layoutWeight(1) Text(record.bmi.toFixed(1)) .fontSize(14) .fontWeight(FontWeight.Bold) .fontColor(#333333) .width(50) Text(record.category.toString()) .fontSize(12) .fontColor(#FFFFFF) .padding({ left: 8, right: 8, top: 2, bottom: 2 }) .backgroundColor(this.getCategoryColor(record.category)) .borderRadius(8) } .width(100%) .padding({ top: 8, bottom: 8 }) .border({ bottom: { width: 1, color: #F5F5F5 } }) }, (record: BMIRecord) record.timestamp.toString()) } .width(100%) .padding(20) .backgroundColor(#FFFFFF) .borderRadius(16) .shadow({ radius: 8, color: rgba(0,0,0,0.06), offsetX: 0, offsetY: 4 }) } } private getCategoryColor(category: BMICategory): string { return CATEGORY_STYLES.find(s s.category category)?.color ?? #999; } } // 主页面 Entry Component struct BMICalculatorMain { State height: number 170; State weight: number 65; State bmi: number 0; State categoryStyle: CategoryStyle CATEGORY_STYLES[1]; State progressPercent: number 0; State records: BMIRecord[] []; aboutToAppear(): void { this.calculate(); this.loadRecords(); } // BMI 计算 private calculate(): void { this.bmi BMIEngine.calculateBMI(this.weight, this.height); this.categoryStyle BMIEngine.getCategory(this.bmi); this.progressPercent BMIEngine.getProgressPercent(this.bmi); } // 保存记录 private saveRecord(): void { const record: BMIRecord { timestamp: Date.now(), height: this.height, weight: this.weight, bmi: this.bmi, category: this.categoryStyle.category }; this.records [record, ...this.records].slice(0, 10); // 可持久化到 Preferences promptAction.showToast({ message: 记录已保存, duration: 1500 }); } // 加载记录 private loadRecords(): void { // 预留从 Preferences 加载 // 当前演示使用空数组 } // 清空记录 private clearRecords(): void { this.records []; promptAction.showToast({ message: 历史记录已清空, duration: 1500 }); } // 获取理想体重 private get idealWeight(): { min: number, max: number } { return BMIEngine.getIdealWeightRange(this.height); } build() { Column() { // ---- 顶部标题 ---- Row() { Text(⚕️ BMI 计算器) .fontSize(22) .fontWeight(FontWeight.Bold) .fontColor(#333333) } .width(100%) .padding({ top: 48, bottom: 16, left: 20 }) Scroll() { Column() { // BMI 圆形进度 gauge BMIGauge({ bmi: this.bmi, color: this.categoryStyle.color, percent: this.progressPercent }) .margin({ bottom: 16 }) // 输入面板 InputPanel({ height: this.height, weight: this.weight, onHeightChange: (val: number) { this.height val; this.calculate(); }, onWeightChange: (val: number) { this.weight val; this.calculate(); } }) .margin({ bottom: 16 }) // 分类卡片 CategoryCard({ categoryStyle: this.categoryStyle, idealWeight: this.idealWeight, height: this.height }) .margin({ bottom: 16 }) // 保存按钮 Button() { Text( 保存记录) .fontSize(16) .fontColor(#FFFFFF) .fontWeight(FontWeight.Medium) } .width(90%) .height(48) .backgroundColor(#4A90D9) .borderRadius(24) .onClick(() this.saveRecord()) .margin({ bottom: 16 }) // 历史记录 HistoryPanel({ records: this.records, onClear: () this.clearRecords() }) } .width(100%) .padding({ left: 16, right: 16 }) } .layoutWeight(1) } .width(100%) .height(100%) .backgroundColor(#F8F9FA) } }3.2 核心代码分析1BMI 计算引擎纯函数类class BMIEngine { static calculateBMI(weightKg: number, heightCm: number): number { if (heightCm 0 || weightKg 0) return 0; const heightM heightCm / 100; return parseFloat((weightKg / (heightM * heightM)).toFixed(1)); } static getCategory(bmi: number): CategoryStyle { return CATEGORY_STYLES.find(s bmi s.minBMI bmi s.maxBMI) ?? CATEGORY_STYLES[1]; } static getProgressPercent(bmi: number): number { return Math.min(100, Math.max(0, (bmi / 35) * 100)); } }设计范式全部使用static方法无需实例化输入清洗处理零值和负值纯函数风格相同输入永远返回相同输出无副作用区间查找使用find 有序数组比 switch-case 更简洁2彩色环形进度条Stack() { Circle() .width(180).height(180) .stroke(#E8E8E8) .strokeWidth(12) .fillOpacity(0) Circle() .width(180).height(180) .stroke(this.color) .strokeWidth(12) .fillOpacity(0) .strokeLineCap(LineCapStyle.Round) .percent(this.percent * 0.75) .animation({ duration: 600, curve: Curve.EaseOut }) }技术要点使用两层Circle叠加底层灰色背景 顶层彩色前景.strokeLineCap(LineCapStyle.Round)使进度条端点圆形化视觉效果更柔和.percent(percent * 0.75)将 0~100 映射到 0~270°留下 90° 缺口使环不闭合更具设计感.animation()直接绑定在 Circle 上属性变化时自动过渡3分类卡片的动态着色Text(this.categoryStyle.category) .fontSize(24) .fontWeight(FontWeight.Bold) .fontColor(this.categoryStyle.color) Row() .backgroundColor(this.categoryStyle.bgColor) .borderRadius(12)每次 BMI 值变化 →getCategory()返回新的CategoryStyle→ 卡片颜色、图标、建议文字全部联动更新。这是State 响应式的典型应用场景。4历史记录管理State records: BMIRecord[] []; private saveRecord(): void { const record: BMIRecord { timestamp: Date.now(), height: this.height, weight: this.weight, bmi: this.bmi, category: this.categoryStyle.category }; this.records [record, ...this.records].slice(0, 10); }每次保存时新记录插入头部保留最近 10 条使用slice(0, 10)限制数量防止无限增长Date.now()作为唯一标识同时提供排序依据四、HarmonyOS 特色功能深度剖析4.1 Circle 组件与 strokeLineCapArkTS 的Circle组件不仅用于绘制实心圆还可以通过stroke和strokeWidth绘制环形Circle() .stroke(#4CAF50) // 描边颜色 .strokeWidth(12) // 描边宽度 .fillOpacity(0) // 填充透明 .strokeLineCap(LineCapStyle.Round) // 端点圆形 .percent(75) // 进度百分比percent属性是 ArkTS 独特的环形进度语法它基于当前组件的周长计算绘制长度无需手动计算弧长或角度。4.2 Stack 层叠布局Stack() { Circle() // 底层灰色背景环 Circle() // 上层彩色进度环 Column() // 最上层BMI 文字 } .align(Alignment.Center)Stack类似 CSS 的position: absolute relative组合所有子组件在 Z 轴方向层叠。结合.align(Alignment.Center)实现完美的居中对齐无需手动计算偏移。4.3 单位制扩展性当前使用公制cm/kg扩展英制ft/lb只需增加换算逻辑// 单位制配置 enum UnitSystem { METRIC, IMPERIAL } // 换算系数 const CONVERSION { cmToFt: 0.0328084, kgToLb: 2.20462 };这种设计遵循了开闭原则对扩展开放对修改封闭。五、UI/UX 设计思路5.1 信息层级页面采用F 形视线流顶部标题 环形 BMI 值核心信息中部身高/体重输入交互操作区中下部分类卡片 建议解读与指导底部操作按钮 历史记录次要信息5.2 色彩心理学应用分类颜色心理暗示偏瘦蓝色 冷静、需要补充正常绿色 健康、安全偏胖橙色 警示、需要行动肥胖红色 危险、需要干预颜色选择符合交通灯色彩语义用户无需读文字即可通过颜色判断健康状况。5.3 响应式交互滑块 数字输入两种方式调节满足不同用户偏好即时反馈任何参数变化BMI 值和颜色立即更新记录保存点击按钮将当前结果存入历史视觉过渡Circle percet 变化有 600ms 动画不突兀六、最佳实践与性能优化6.1 编码最佳实践✅ 推荐做法// 1. 纯函数计算引擎 vs 在 UI 层计算 class BMIEngine { static calculateBMI(...) { ... } } // 2. 使用 find() 替代 if-else 链 const result CATEGORY_STYLES.find(s bmi s.minBMI bmi s.maxBMI); // 3. 使用 getter 简化模板 private get idealWeight() { ... } // 4. 数组不可变操作 this.records [newRecord, ...this.records].slice(0, MAX_RECORDS); // 5. 使用 enum 管理分类 enum BMICategory { ... }❌ 避免的做法// 1. 在 UI 组件中直接写计算逻辑 build() { const bmi this.weight / ((this.height/100) ** 2); // 计算逻辑混在 UI 中难以测试和维护 } // 2. 使用深层 if-else if (bmi 18.5) { ... } else if (bmi 24) { ... } else if (bmi 28) { ... } else { ... } // 3. 直接修改数组 this.records.push(newRecord);6.2 性能优化优化点措施减少 State 数量将关联数据聚合为对象避免不必要计算calculate()只在 height/weight 变化时调用列表 keyForEach 使用唯一 timestamp 作为 key动画 GPU 加速使用 transform/opacity 属性避免 layout 变化6.3 数据持久化// 使用 Preferences 存储历史记录 import { preferences } from kit.ArkData; async function saveRecords(context, records: BMIRecord[]) { const store await preferences.getPreferences(context, bmi_prefs); await store.put(history, JSON.stringify(records)); await store.flush(); }七、扩展与演进方向7.1 功能扩展图表趋势使用 LineChart 展示 BMI 历史变化曲线多用户支持家庭多成员独立管理目标设定设定目标 BMI展示差值和建议体脂估算输入腰围/年龄/性别估算体脂率导出报告生成 PDF 健康报告7.2 鸿蒙特有能力服务卡片桌面卡片直接展示最新 BMI 数据和分类健康数据联动读取系统 Health Kit 中的体重数据穿戴设备连接手表同步体重数据分布式手机测量、平板查看历史趋势八、总结本文构建了一个基于 ArkTS 的 BMI 计算器核心技术收获纯函数计算引擎将 BMI 计算、分类判定、进度映射等逻辑封装为静态方法环形进度条利用 Circle stroke percent 实现圆环进度展示颜色编码系统四色分类蓝/绿/橙/红直观反映健康状况组件化拆分InputPanel、BMIGauge、CategoryCard、HistoryPanel 各司其职响应式数据流State → 计算 → UI 自动更新代码简洁清晰BMI 计算器虽然数学原理简单但 ArkTS 的声明式框架让 UI 与逻辑的结合变得优雅而高效。参考链接ArkTS Circle 组件ArkTS Stack 布局HarmonyOS Preferences 数据存储