release 24
This commit is contained in:
67
CHANGELOG.md
67
CHANGELOG.md
@@ -3,6 +3,73 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
|
||||||
|
## [0.99.40] - 2026-04-24
|
||||||
|
|
||||||
|
### ✨ 优化 — 身体分析页面5项需求实现 + 溢出修复
|
||||||
|
|
||||||
|
#### 修复: Tab页面底部溢出 1084px
|
||||||
|
- **根因**: 3个Tab使用 `Column` 布局,内容超出 `SizedBox` 固定高度(900-1200px)
|
||||||
|
- **修复**: Tab内部改用 `SingleChildScrollView` 支持滚动
|
||||||
|
- **新增**: 每个Tab底部增加 👈 左右滑动切换提示
|
||||||
|
|
||||||
|
#### 修复: Tab滑到底后无法滑回顶部
|
||||||
|
- **根因**: 外层 `ListView` 与内层 `SingleChildScrollView` 嵌套滚动冲突,向上滑动时外层抢占手势
|
||||||
|
- **修复**: 分析结果展示时改用 `Column` + `Expanded` 布局,PageView独立占据剩余空间,彻底消除嵌套滚动
|
||||||
|
- **优化**: 结果页面顶部改为紧凑信息栏(性别/年龄/身高/体重)+ 🔄重新分析按钮,节省空间
|
||||||
|
|
||||||
|
#### 新增: 趣味统计 - 各星球体重
|
||||||
|
- **功能**: 展示用户在9个天体(水星/金星/地球/火星/木星/土星/天王星/海王星/月球)上的体重
|
||||||
|
- **数据**: 基于各行星相对地球的平均重力加速度计算
|
||||||
|
- **UI**: 使用 Wrap 布局的 Chip 样式展示,地球高亮标记
|
||||||
|
|
||||||
|
#### 新增: 有氧运动(燃脂)心率计算卡片
|
||||||
|
- **功能**: 在核心指标Tab中BMI卡片后新增 ❤️🔥 燃脂最佳心率卡片
|
||||||
|
- **展示**: 渐变背景大字显示最佳心率区间 xx-xx bpm,标注为最大心率的60%~70%
|
||||||
|
|
||||||
|
#### 优化: 摄入需求/蛋白质根据BMI智能显示
|
||||||
|
- **逻辑**: 偏瘦(BMI<18.5)隐藏减脂选项,肥胖(BMI≥28)隐藏增肌选项
|
||||||
|
- **影响**: GoalCaloriesCard 和 ProteinCard 均已适配
|
||||||
|
|
||||||
|
#### 新增: Tab滑动边缘发光动画
|
||||||
|
- **效果**: 在第1个Tab继续左滑时,左边缘显示主题色渐变发光;最后1个Tab右滑同理
|
||||||
|
- **实现**: NotificationListener监听OverscrollNotification,Stack+Positioned叠加边缘发光层
|
||||||
|
|
||||||
|
#### 需求1: 修复按钮状态不实时更新
|
||||||
|
- **问题**: 输入身高/年龄/体重后,"开始分析"按钮仍为禁用状态,需额外操作才能激活
|
||||||
|
- **根因**: `onChanged` 仅在 `_hasResult == true` 时调用 `setState()`,首次输入不触发重建
|
||||||
|
- **修复**: `onChanged` 始终调用 `setState()`,按钮状态实时响应输入
|
||||||
|
|
||||||
|
#### 需求2: AppBar 增加隐私与算法说明
|
||||||
|
- 在导航栏右侧增加 ℹ️ 图标按钮
|
||||||
|
- 点击弹出 `CupertinoAlertDialog`,包含:
|
||||||
|
- 🔒 隐私声明:所有数据仅在本地计算,不会联网上传
|
||||||
|
- 📐 算法来源:Harris-Benedict / Mifflin-St Jeor / US Navy / Karvonen 等
|
||||||
|
- ⚠️ 免责声明:仅供学习参考,禁止医用、禁止商用
|
||||||
|
|
||||||
|
#### 需求3: 参考文献页面增加身体分析数据来源
|
||||||
|
- 新增 7 条 🧬 身体分析分类参考文献:
|
||||||
|
- Harris-Benedict BMR 公式 (Wikipedia)
|
||||||
|
- Mifflin-St Jeor BMR 公式 (PubMed)
|
||||||
|
- US Navy 体脂率估算法 (US Navy)
|
||||||
|
- Karvonen 心率区间公式 (Wikipedia)
|
||||||
|
- WHO BMI 分类与肥胖标准 (WHO)
|
||||||
|
- 中国居民膳食营养素参考摄入量 2023版 (中国营养学会)
|
||||||
|
- Mosteller 体表面积计算公式 (Wikipedia)
|
||||||
|
|
||||||
|
#### 需求4: 基础信息卡片增加保存/隐藏按钮
|
||||||
|
- 💾 保存按钮:将填写信息写入 SharedPreferences,下次打开自动填充
|
||||||
|
- 👁️ 隐藏/显示按钮:切换输入区域显示状态,数据保留仅隐藏表单
|
||||||
|
|
||||||
|
#### 需求5: 核心指标/科学指数/趣味统计支持左右滑动
|
||||||
|
- 将 Tab 切换改为 `PageView` 实现,支持手势左右滑动
|
||||||
|
- `GlassSegmentedControl` 与 `PageView` 双向联动
|
||||||
|
|
||||||
|
#### 涉及文件
|
||||||
|
- `body_analysis_page.dart` — 全部5项需求实现
|
||||||
|
- `references_page.dart` — 新增7条身体分析参考文献
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [0.99.39] - 2026-04-24
|
## [0.99.39] - 2026-04-24
|
||||||
|
|
||||||
### 🐛 修复 — 排敏助手页面一直转圈 + 闪退 + Obx异常
|
### 🐛 修复 — 排敏助手页面一直转圈 + 闪退 + Obx异常
|
||||||
|
|||||||
@@ -375,7 +375,7 @@ class ToolRegistry {
|
|||||||
),
|
),
|
||||||
ToolItem(
|
ToolItem(
|
||||||
id: 'anti_allergy',
|
id: 'anti_allergy',
|
||||||
name: 'Anti 敏宝排敏助手',
|
name: '敏宝排敏助手',
|
||||||
icon: '🛡️',
|
icon: '🛡️',
|
||||||
needsNetwork: false,
|
needsNetwork: false,
|
||||||
category: 'health',
|
category: 'health',
|
||||||
@@ -395,11 +395,7 @@ class ToolRegistry {
|
|||||||
category: 'health',
|
category: 'health',
|
||||||
route: '/tools/body-analysis',
|
route: '/tools/body-analysis',
|
||||||
description: '基于身高体重年龄分析25+项科学指标和趣味统计',
|
description: '基于身高体重年龄分析25+项科学指标和趣味统计',
|
||||||
waterfallSlot: WaterfallSlotConfig(
|
waterfallSlot: WaterfallSlotConfig(show: true, priority: 1, badge: 'NEW'),
|
||||||
show: true,
|
|
||||||
priority: 1,
|
|
||||||
badge: 'NEW',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,15 @@ class WaterSchedule {
|
|||||||
const WaterSchedule(this.time, this.ml, this.tip);
|
const WaterSchedule(this.time, this.ml, this.tip);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PlanetWeight {
|
||||||
|
final String name;
|
||||||
|
final String emoji;
|
||||||
|
final double gravity;
|
||||||
|
final double weight;
|
||||||
|
|
||||||
|
const PlanetWeight(this.name, this.emoji, this.gravity, this.weight);
|
||||||
|
}
|
||||||
|
|
||||||
class BodyAnalysisCalculator {
|
class BodyAnalysisCalculator {
|
||||||
final Gender gender;
|
final Gender gender;
|
||||||
final double age;
|
final double age;
|
||||||
@@ -423,6 +432,21 @@ class BodyAnalysisCalculator {
|
|||||||
return stepsPerDay * strideLength * 365.25 * age;
|
return stepsPerDay * strideLength * 365.25 * age;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<PlanetWeight> get planetWeights {
|
||||||
|
if (!isValid) return [];
|
||||||
|
return [
|
||||||
|
PlanetWeight('水星', '🪐', 0.38, weight * 0.38),
|
||||||
|
PlanetWeight('金星', '🌟', 0.91, weight * 0.91),
|
||||||
|
PlanetWeight('地球', '🌍', 1.00, weight),
|
||||||
|
PlanetWeight('火星', '🔴', 0.38, weight * 0.38),
|
||||||
|
PlanetWeight('木星', '🟤', 2.36, weight * 2.36),
|
||||||
|
PlanetWeight('土星', '🪐', 1.08, weight * 1.08),
|
||||||
|
PlanetWeight('天王星', '🔵', 0.92, weight * 0.92),
|
||||||
|
PlanetWeight('海王星', '🌊', 1.12, weight * 1.12),
|
||||||
|
PlanetWeight('月球', '🌙', 0.165, weight * 0.165),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
double get fatLossCalories => tdee - 500;
|
double get fatLossCalories => tdee - 500;
|
||||||
|
|
||||||
double get muscleGainCalories => tdee + 300;
|
double get muscleGainCalories => tdee + 300;
|
||||||
|
|||||||
@@ -474,6 +474,133 @@ class HeartRateZonesCard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FatBurningHeartRateCard extends StatelessWidget {
|
||||||
|
final bool isDark;
|
||||||
|
final BodyAnalysisCalculator calc;
|
||||||
|
|
||||||
|
const FatBurningHeartRateCard({
|
||||||
|
super.key,
|
||||||
|
required this.isDark,
|
||||||
|
required this.calc,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final zones = calc.heartRateZones;
|
||||||
|
if (zones.isEmpty) return const SizedBox.shrink();
|
||||||
|
final fatBurnZone = zones[1];
|
||||||
|
return GlassContainer(
|
||||||
|
padding: const EdgeInsets.all(DesignTokens.space4),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Text('❤️🔥', style: TextStyle(fontSize: 24)),
|
||||||
|
const SizedBox(width: DesignTokens.space2),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'有氧运动(燃脂)心率',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: DesignTokens.fontLg,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
InfoButton(
|
||||||
|
isDark: isDark,
|
||||||
|
title: '燃脂最佳心率',
|
||||||
|
content:
|
||||||
|
'最佳燃脂心率 = 最大心率 × 60%~70%\n'
|
||||||
|
'最大心率 = 220 - 年龄\n\n'
|
||||||
|
'在此心率区间运动时:\n'
|
||||||
|
'• 脂肪供能比例最高\n'
|
||||||
|
'• 可持续运动时间最长\n'
|
||||||
|
'• 适合快走、慢跑、骑行等有氧运动\n\n'
|
||||||
|
'建议每次运动30-60分钟,\n'
|
||||||
|
'每周3-5次,效果最佳。',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: DesignTokens.space3),
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(DesignTokens.space4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
colors: [Color(0xFFFF6B6B), Color(0xFFEE5A24)],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(DesignTokens.radiusLg),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'最佳心率区间',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: DesignTokens.fontSm,
|
||||||
|
color: CupertinoColors.white.withValues(alpha: 0.9),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: DesignTokens.space1),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||||
|
textBaseline: TextBaseline.alphabetic,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${fatBurnZone.low.round()}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 40,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: CupertinoColors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
' - ',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: CupertinoColors.white.withValues(alpha: 0.8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${fatBurnZone.high.round()}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 40,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: CupertinoColors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: DesignTokens.space2),
|
||||||
|
Text(
|
||||||
|
'bpm',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: DesignTokens.fontMd,
|
||||||
|
color: CupertinoColors.white.withValues(alpha: 0.8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: DesignTokens.space1),
|
||||||
|
Text(
|
||||||
|
'最大心率 ${calc.maxHeartRate} bpm 的 60%~70%',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: DesignTokens.fontXs,
|
||||||
|
color: CupertinoColors.white.withValues(alpha: 0.7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class GoalCaloriesCard extends StatelessWidget {
|
class GoalCaloriesCard extends StatelessWidget {
|
||||||
final bool isDark;
|
final bool isDark;
|
||||||
final BodyAnalysisCalculator calc;
|
final BodyAnalysisCalculator calc;
|
||||||
@@ -482,6 +609,10 @@ class GoalCaloriesCard extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final isOverweight = calc.bmi >= 28;
|
||||||
|
final isUnderweight = calc.bmi < 18.5;
|
||||||
|
final showFatLoss = !isUnderweight;
|
||||||
|
final showMuscleGain = !isOverweight;
|
||||||
return GlassContainer(
|
return GlassContainer(
|
||||||
padding: const EdgeInsets.all(DesignTokens.space4),
|
padding: const EdgeInsets.all(DesignTokens.space4),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -505,29 +636,36 @@ class GoalCaloriesCard extends StatelessWidget {
|
|||||||
isDark: isDark,
|
isDark: isDark,
|
||||||
title: '摄入需求',
|
title: '摄入需求',
|
||||||
content:
|
content:
|
||||||
'基于TDEE计算不同目标的每日摄入量:\n\n'
|
'基于TDEE和BMI计算不同目标的每日摄入量:\n\n'
|
||||||
'减脂: TDEE - 500 kcal(每周约减0.5kg)\n'
|
'减脂: TDEE - 500 kcal(每周约减0.5kg)\n'
|
||||||
'增肌: TDEE + 300 kcal(配合力量训练)\n'
|
'增肌: TDEE + 300 kcal(配合力量训练)\n'
|
||||||
'维持: TDEE(保持当前体重)\n\n'
|
'维持: TDEE(保持当前体重)\n\n'
|
||||||
|
'根据BMI智能推荐:\n'
|
||||||
|
'偏瘦(BMI<18.5): 隐藏减脂选项\n'
|
||||||
|
'肥胖(BMI≥28): 隐藏增肌选项\n\n'
|
||||||
'建议不要低于BMR,否则影响基础代谢。',
|
'建议不要低于BMR,否则影响基础代谢。',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: DesignTokens.space3),
|
const SizedBox(height: DesignTokens.space3),
|
||||||
_buildGoalRow(
|
if (showFatLoss) ...[
|
||||||
'🔥 减脂',
|
_buildGoalRow(
|
||||||
calc.fatLossCalories,
|
'🔥 减脂',
|
||||||
DesignTokens.orange,
|
calc.fatLossCalories,
|
||||||
'kcal/天',
|
DesignTokens.orange,
|
||||||
),
|
'kcal/天',
|
||||||
const SizedBox(height: DesignTokens.space2),
|
),
|
||||||
_buildGoalRow(
|
if (showMuscleGain) const SizedBox(height: DesignTokens.space2),
|
||||||
'💪 增肌',
|
],
|
||||||
calc.muscleGainCalories,
|
if (showMuscleGain) ...[
|
||||||
DesignTokens.green,
|
_buildGoalRow(
|
||||||
'kcal/天',
|
'💪 增肌',
|
||||||
),
|
calc.muscleGainCalories,
|
||||||
const SizedBox(height: DesignTokens.space2),
|
DesignTokens.green,
|
||||||
|
'kcal/天',
|
||||||
|
),
|
||||||
|
const SizedBox(height: DesignTokens.space2),
|
||||||
|
],
|
||||||
_buildGoalRow(
|
_buildGoalRow(
|
||||||
'⚖️ 维持',
|
'⚖️ 维持',
|
||||||
calc.maintenanceCalories,
|
calc.maintenanceCalories,
|
||||||
@@ -579,6 +717,10 @@ class ProteinCard extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final isOverweight = calc.bmi >= 28;
|
||||||
|
final isUnderweight = calc.bmi < 18.5;
|
||||||
|
final showFatLoss = !isUnderweight;
|
||||||
|
final showMuscleGain = !isOverweight;
|
||||||
return GlassContainer(
|
return GlassContainer(
|
||||||
padding: const EdgeInsets.all(DesignTokens.space4),
|
padding: const EdgeInsets.all(DesignTokens.space4),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -611,10 +753,18 @@ class ProteinCard extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: DesignTokens.space3),
|
const SizedBox(height: DesignTokens.space3),
|
||||||
_buildGoalRow('🔥 减脂', calc.proteinForFatLoss, DesignTokens.orange),
|
if (showFatLoss) ...[
|
||||||
const SizedBox(height: DesignTokens.space2),
|
_buildGoalRow('🔥 减脂', calc.proteinForFatLoss, DesignTokens.orange),
|
||||||
_buildGoalRow('💪 增肌', calc.proteinForMuscleGain, DesignTokens.green),
|
if (showMuscleGain) const SizedBox(height: DesignTokens.space2),
|
||||||
const SizedBox(height: DesignTokens.space2),
|
],
|
||||||
|
if (showMuscleGain) ...[
|
||||||
|
_buildGoalRow(
|
||||||
|
'💪 增肌',
|
||||||
|
calc.proteinForMuscleGain,
|
||||||
|
DesignTokens.green,
|
||||||
|
),
|
||||||
|
const SizedBox(height: DesignTokens.space2),
|
||||||
|
],
|
||||||
_buildGoalRow('⚖️ 维持', calc.proteinForMaintenance, DesignTokens.blue),
|
_buildGoalRow('⚖️ 维持', calc.proteinForMaintenance, DesignTokens.blue),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -847,3 +997,104 @@ class IdealWeightCard extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PlanetWeightCard extends StatelessWidget {
|
||||||
|
final bool isDark;
|
||||||
|
final BodyAnalysisCalculator calc;
|
||||||
|
|
||||||
|
const PlanetWeightCard({super.key, required this.isDark, required this.calc});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final planets = calc.planetWeights;
|
||||||
|
if (planets.isEmpty) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
return GlassContainer(
|
||||||
|
padding: const EdgeInsets.all(DesignTokens.space4),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Text('🚀', style: TextStyle(fontSize: 24)),
|
||||||
|
const SizedBox(width: DesignTokens.space2),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'各星球上的体重',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: DesignTokens.fontLg,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
InfoButton(
|
||||||
|
isDark: isDark,
|
||||||
|
title: '各星球体重',
|
||||||
|
content:
|
||||||
|
'体重 = 质量 × 重力加速度\n\n'
|
||||||
|
'你的质量在宇宙中是不变的,\n'
|
||||||
|
'但不同星球的重力加速度不同,\n'
|
||||||
|
'所以你在不同星球上"称"出的重量也不同。\n\n'
|
||||||
|
'各行星以地球为基准的平均重力加速度:\n'
|
||||||
|
'水星 0.38 | 金星 0.91 | 地球 1.00\n'
|
||||||
|
'火星 0.38 | 木星 2.36 | 土星 1.08\n'
|
||||||
|
'天王星 0.92 | 海王星 1.12\n'
|
||||||
|
'月球 0.165\n\n'
|
||||||
|
'在木星上你最重,在月球上你最轻!',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: DesignTokens.space3),
|
||||||
|
Wrap(
|
||||||
|
spacing: DesignTokens.space2,
|
||||||
|
runSpacing: DesignTokens.space2,
|
||||||
|
children: planets.map((p) => _buildPlanetChip(p)).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPlanetChip(PlanetWeight planet) {
|
||||||
|
final isEarth = planet.gravity == 1.0;
|
||||||
|
final color = isEarth ? DesignTokens.dynamicPrimary : DesignTokens.purple;
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: DesignTokens.space3,
|
||||||
|
vertical: DesignTokens.space2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: 0.08),
|
||||||
|
borderRadius: DesignTokens.borderRadiusMd,
|
||||||
|
border: isEarth
|
||||||
|
? Border.all(color: color.withValues(alpha: 0.3))
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(planet.emoji, style: const TextStyle(fontSize: 16)),
|
||||||
|
const SizedBox(width: DesignTokens.space1),
|
||||||
|
Text(
|
||||||
|
planet.name,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: DesignTokens.fontSm,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: DesignTokens.space1),
|
||||||
|
Text(
|
||||||
|
'${planet.weight.toStringAsFixed(1)}kg',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: DesignTokens.fontSm,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -47,6 +47,27 @@ class _BodyAnalysisPageState extends State<BodyAnalysisPage>
|
|||||||
|
|
||||||
late final PageController _pageController;
|
late final PageController _pageController;
|
||||||
|
|
||||||
|
double _leftEdgeGlow = 0;
|
||||||
|
double _rightEdgeGlow = 0;
|
||||||
|
|
||||||
|
void _onOverscroll(OverscrollNotification notification) {
|
||||||
|
if (notification.overscroll < 0) {
|
||||||
|
setState(
|
||||||
|
() => _leftEdgeGlow = (-notification.overscroll / 40).clamp(0.0, 1.0),
|
||||||
|
);
|
||||||
|
Future.delayed(const Duration(milliseconds: 600), () {
|
||||||
|
if (mounted) setState(() => _leftEdgeGlow = 0);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(
|
||||||
|
() => _rightEdgeGlow = (notification.overscroll / 40).clamp(0.0, 1.0),
|
||||||
|
);
|
||||||
|
Future.delayed(const Duration(milliseconds: 600), () {
|
||||||
|
if (mounted) setState(() => _rightEdgeGlow = 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -69,17 +90,17 @@ class _BodyAnalysisPageState extends State<BodyAnalysisPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
BodyAnalysisCalculator get _calc => BodyAnalysisCalculator(
|
BodyAnalysisCalculator get _calc => BodyAnalysisCalculator(
|
||||||
gender: _gender,
|
gender: _gender,
|
||||||
age: double.tryParse(_ageController.text) ?? 0,
|
age: double.tryParse(_ageController.text) ?? 0,
|
||||||
height: double.tryParse(_heightController.text) ?? 0,
|
height: double.tryParse(_heightController.text) ?? 0,
|
||||||
weight: double.tryParse(_weightController.text) ?? 0,
|
weight: double.tryParse(_weightController.text) ?? 0,
|
||||||
waist: double.tryParse(_waistController.text),
|
waist: double.tryParse(_waistController.text),
|
||||||
hip: double.tryParse(_hipController.text),
|
hip: double.tryParse(_hipController.text),
|
||||||
neck: double.tryParse(_neckController.text),
|
neck: double.tryParse(_neckController.text),
|
||||||
wrist: double.tryParse(_wristController.text),
|
wrist: double.tryParse(_wristController.text),
|
||||||
activityLevel: _activityLevel,
|
activityLevel: _activityLevel,
|
||||||
bodyFatInput: double.tryParse(_bodyFatController.text),
|
bodyFatInput: double.tryParse(_bodyFatController.text),
|
||||||
);
|
);
|
||||||
|
|
||||||
bool get _isValid => _calc.isValid;
|
bool get _isValid => _calc.isValid;
|
||||||
|
|
||||||
@@ -161,14 +182,17 @@ class _BodyAnalysisPageState extends State<BodyAnalysisPage>
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildInfoSection('🔒 隐私声明',
|
_buildInfoSection('🔒 隐私声明', '所有数据仅在您的设备本地计算,不会联网上传,不收集任何个人信息。'),
|
||||||
'所有数据仅在您的设备本地计算,不会联网上传,不收集任何个人信息。'),
|
|
||||||
SizedBox(height: 10),
|
SizedBox(height: 10),
|
||||||
_buildInfoSection('📐 算法来源',
|
_buildInfoSection(
|
||||||
'• BMR: Harris-Benedict / Mifflin-St Jeor 公式\n• BMI: WHO 国际标准\n• 体脂率: US Navy Method / BMI估算法\n• TDEE: 活动系数法\n• 心率区间: Karvonen 公式\n• 喝水量: 基于体重与活动量估算'),
|
'📐 算法来源',
|
||||||
|
'• BMR: Harris-Benedict / Mifflin-St Jeor 公式\n• BMI: WHO 国际标准\n• 体脂率: US Navy Method / BMI估算法\n• TDEE: 活动系数法\n• 心率区间: Karvonen 公式\n• 喝水量: 基于体重与活动量估算',
|
||||||
|
),
|
||||||
SizedBox(height: 10),
|
SizedBox(height: 10),
|
||||||
_buildInfoSection('⚠️ 免责声明',
|
_buildInfoSection(
|
||||||
'本工具仅供学习参考,不作为医疗诊断依据。如有健康问题请咨询专业医生。禁止用于商业用途。'),
|
'⚠️ 免责声明',
|
||||||
|
'本工具仅供学习参考,不作为医疗诊断依据。如有健康问题请咨询专业医生。禁止用于商业用途。',
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -187,8 +211,10 @@ class _BodyAnalysisPageState extends State<BodyAnalysisPage>
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(title,
|
Text(
|
||||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
|
title,
|
||||||
|
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
SizedBox(height: 4),
|
SizedBox(height: 4),
|
||||||
Text(content, style: TextStyle(fontSize: 13, height: 1.4)),
|
Text(content, style: TextStyle(fontSize: 13, height: 1.4)),
|
||||||
],
|
],
|
||||||
@@ -200,8 +226,9 @@ class _BodyAnalysisPageState extends State<BodyAnalysisPage>
|
|||||||
final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark;
|
final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark;
|
||||||
|
|
||||||
return CupertinoPageScaffold(
|
return CupertinoPageScaffold(
|
||||||
backgroundColor:
|
backgroundColor: isDark
|
||||||
isDark ? DarkDesignTokens.background : DesignTokens.background,
|
? DarkDesignTokens.background
|
||||||
|
: DesignTokens.background,
|
||||||
navigationBar: CupertinoNavigationBar(
|
navigationBar: CupertinoNavigationBar(
|
||||||
middle: const Text('🧬 身体分析'),
|
middle: const Text('🧬 身体分析'),
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
@@ -223,69 +250,251 @@ class _BodyAnalysisPageState extends State<BodyAnalysisPage>
|
|||||||
border: null,
|
border: null,
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: ListView(
|
child: _hasResult && _isValid
|
||||||
padding: const EdgeInsets.all(DesignTokens.space4),
|
? _buildResultLayout(isDark)
|
||||||
children: [
|
: _buildInputLayout(isDark),
|
||||||
_buildRequiredInputs(isDark),
|
),
|
||||||
const SizedBox(height: DesignTokens.space3),
|
);
|
||||||
_buildOptionalToggle(isDark),
|
}
|
||||||
if (_showOptional) ...[
|
|
||||||
const SizedBox(height: DesignTokens.space3),
|
Widget _buildInputLayout(bool isDark) {
|
||||||
_buildOptionalInputs(isDark),
|
return ListView(
|
||||||
],
|
padding: const EdgeInsets.all(DesignTokens.space4),
|
||||||
const SizedBox(height: DesignTokens.space4),
|
children: [
|
||||||
SizedBox(
|
_buildRequiredInputs(isDark),
|
||||||
width: double.infinity,
|
const SizedBox(height: DesignTokens.space3),
|
||||||
child: CupertinoButton.filled(
|
_buildOptionalToggle(isDark),
|
||||||
borderRadius: DesignTokens.borderRadiusLg,
|
if (_showOptional) ...[
|
||||||
onPressed: _isValid ? _analyze : null,
|
const SizedBox(height: DesignTokens.space3),
|
||||||
child: const Text('🧬 开始分析'),
|
_buildOptionalInputs(isDark),
|
||||||
),
|
],
|
||||||
),
|
const SizedBox(height: DesignTokens.space4),
|
||||||
if (_hasResult && _isValid) ...[
|
SizedBox(
|
||||||
const SizedBox(height: DesignTokens.space5),
|
width: double.infinity,
|
||||||
GlassSegmentedControl(
|
child: CupertinoButton.filled(
|
||||||
segments: const [
|
borderRadius: DesignTokens.borderRadiusLg,
|
||||||
GlassSegment(label: '🏥 核心指标'),
|
onPressed: _isValid ? _analyze : null,
|
||||||
GlassSegment(label: '🔬 科学指数'),
|
child: const Text('🧬 开始分析'),
|
||||||
GlassSegment(label: '🎮 趣味统计'),
|
),
|
||||||
],
|
|
||||||
selectedIndex: _selectedTab,
|
|
||||||
onChanged: (i) {
|
|
||||||
setState(() => _selectedTab = i);
|
|
||||||
_pageController.animateToPage(
|
|
||||||
i,
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
curve: Curves.easeInOut,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: DesignTokens.space4),
|
|
||||||
_buildSwipeableTabs(isDark),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildResultLayout(bool isDark) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(
|
||||||
|
DesignTokens.space4,
|
||||||
|
DesignTokens.space3,
|
||||||
|
DesignTokens.space4,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildCompactInfoBar(isDark)),
|
||||||
|
const SizedBox(width: DesignTokens.space2),
|
||||||
|
SizedBox(
|
||||||
|
child: CupertinoButton.filled(
|
||||||
|
borderRadius: DesignTokens.borderRadiusMd,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: DesignTokens.space3,
|
||||||
|
vertical: DesignTokens.space1,
|
||||||
|
),
|
||||||
|
minimumSize: Size.zero,
|
||||||
|
onPressed: () => setState(() {
|
||||||
|
_hasResult = false;
|
||||||
|
_isInputHidden = false;
|
||||||
|
}),
|
||||||
|
child: const Text('🔄 重新分析', style: TextStyle(fontSize: 13)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: DesignTokens.space4,
|
||||||
|
vertical: DesignTokens.space3,
|
||||||
|
),
|
||||||
|
child: GlassSegmentedControl(
|
||||||
|
segments: const [
|
||||||
|
GlassSegment(label: '🏥 核心指标'),
|
||||||
|
GlassSegment(label: '🔬 科学指数'),
|
||||||
|
GlassSegment(label: '🎮 趣味统计'),
|
||||||
|
],
|
||||||
|
selectedIndex: _selectedTab,
|
||||||
|
onChanged: (i) {
|
||||||
|
setState(() => _selectedTab = i);
|
||||||
|
_pageController.animateToPage(
|
||||||
|
i,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(child: _buildSwipeableTabs(isDark)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCompactInfoBar(bool isDark) {
|
||||||
|
final calc = _calc;
|
||||||
|
final genderText = _gender == Gender.male ? '👨 男' : '👩 女';
|
||||||
|
final ageText = _isInputHidden ? '***岁' : '${calc.age.toStringAsFixed(0)}岁';
|
||||||
|
final heightText = _isInputHidden
|
||||||
|
? '***cm'
|
||||||
|
: '${calc.height.toStringAsFixed(0)}cm';
|
||||||
|
final weightText = _isInputHidden
|
||||||
|
? '***kg'
|
||||||
|
: '${calc.weight.toStringAsFixed(0)}kg';
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: DesignTokens.space3,
|
||||||
|
vertical: DesignTokens.space2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isDark ? DarkDesignTokens.card : DesignTokens.card,
|
||||||
|
borderRadius: DesignTokens.borderRadiusMd,
|
||||||
|
border: Border.all(
|
||||||
|
color: isDark
|
||||||
|
? DarkDesignTokens.glassBorder
|
||||||
|
: DesignTokens.glassBorder,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
genderText,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: DesignTokens.fontSm,
|
||||||
|
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: DesignTokens.space2),
|
||||||
|
Text(
|
||||||
|
ageText,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: DesignTokens.fontSm,
|
||||||
|
color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: DesignTokens.space2),
|
||||||
|
Text(
|
||||||
|
heightText,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: DesignTokens.fontSm,
|
||||||
|
color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: DesignTokens.space2),
|
||||||
|
Text(
|
||||||
|
weightText,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: DesignTokens.fontSm,
|
||||||
|
color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: _saveData,
|
||||||
|
child: Icon(
|
||||||
|
CupertinoIcons.floppy_disk,
|
||||||
|
size: 16,
|
||||||
|
color: DesignTokens.dynamicPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: DesignTokens.space2),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => setState(() => _isInputHidden = !_isInputHidden),
|
||||||
|
child: Icon(
|
||||||
|
_isInputHidden ? CupertinoIcons.eye_slash : CupertinoIcons.eye,
|
||||||
|
size: 16,
|
||||||
|
color: DesignTokens.dynamicPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSwipeableTabs(bool isDark) {
|
Widget _buildSwipeableTabs(bool isDark) {
|
||||||
final calc = _calc;
|
final calc = _calc;
|
||||||
return SizedBox(
|
final glowColor = DesignTokens.dynamicPrimary;
|
||||||
height: _selectedTab == 0
|
return Stack(
|
||||||
? 1200
|
children: [
|
||||||
: _selectedTab == 1
|
NotificationListener<OverscrollNotification>(
|
||||||
? 1000
|
onNotification: (notification) {
|
||||||
: 900,
|
_onOverscroll(notification);
|
||||||
child: PageView(
|
return false;
|
||||||
controller: _pageController,
|
},
|
||||||
onPageChanged: (i) => setState(() => _selectedTab = i),
|
child: PageView(
|
||||||
children: [
|
controller: _pageController,
|
||||||
CoreMetricsTab(isDark: isDark, calc: calc),
|
onPageChanged: (i) => setState(() => _selectedTab = i),
|
||||||
ScienceMetricsTab(isDark: isDark, calc: calc),
|
children: [
|
||||||
FunStatsTab(isDark: isDark, calc: calc),
|
CoreMetricsTab(isDark: isDark, calc: calc),
|
||||||
],
|
ScienceMetricsTab(isDark: isDark, calc: calc),
|
||||||
),
|
FunStatsTab(isDark: isDark, calc: calc),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_leftEdgeGlow > 0)
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: 80,
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
opacity: _leftEdgeGlow,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.centerLeft,
|
||||||
|
end: Alignment.centerRight,
|
||||||
|
colors: [
|
||||||
|
glowColor.withValues(alpha: 0.85),
|
||||||
|
glowColor.withValues(alpha: 0.4),
|
||||||
|
glowColor.withValues(alpha: 0.0),
|
||||||
|
],
|
||||||
|
stops: const [0.0, 0.4, 1.0],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_rightEdgeGlow > 0)
|
||||||
|
Positioned(
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: 80,
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
opacity: _rightEdgeGlow,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.centerRight,
|
||||||
|
end: Alignment.centerLeft,
|
||||||
|
colors: [
|
||||||
|
glowColor.withValues(alpha: 0.75),
|
||||||
|
glowColor.withValues(alpha: 0.3),
|
||||||
|
glowColor.withValues(alpha: 0.0),
|
||||||
|
],
|
||||||
|
stops: const [0.0, 0.4, 1.0],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,7 +520,9 @@ class _BodyAnalysisPageState extends State<BodyAnalysisPage>
|
|||||||
onTap: _saveData,
|
onTap: _saveData,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: DesignTokens.space2, vertical: DesignTokens.space1),
|
horizontal: DesignTokens.space2,
|
||||||
|
vertical: DesignTokens.space1,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: DesignTokens.dynamicPrimary.withValues(alpha: 0.1),
|
color: DesignTokens.dynamicPrimary.withValues(alpha: 0.1),
|
||||||
borderRadius: DesignTokens.borderRadiusSm,
|
borderRadius: DesignTokens.borderRadiusSm,
|
||||||
@@ -319,14 +530,20 @@ class _BodyAnalysisPageState extends State<BodyAnalysisPage>
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(CupertinoIcons.floppy_disk,
|
Icon(
|
||||||
size: 14, color: DesignTokens.dynamicPrimary),
|
CupertinoIcons.floppy_disk,
|
||||||
|
size: 14,
|
||||||
|
color: DesignTokens.dynamicPrimary,
|
||||||
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text('保存',
|
Text(
|
||||||
style: TextStyle(
|
'保存',
|
||||||
fontSize: DesignTokens.fontXs,
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w500,
|
fontSize: DesignTokens.fontXs,
|
||||||
color: DesignTokens.dynamicPrimary)),
|
fontWeight: FontWeight.w500,
|
||||||
|
color: DesignTokens.dynamicPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -336,7 +553,9 @@ class _BodyAnalysisPageState extends State<BodyAnalysisPage>
|
|||||||
onTap: () => setState(() => _isInputHidden = !_isInputHidden),
|
onTap: () => setState(() => _isInputHidden = !_isInputHidden),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: DesignTokens.space2, vertical: DesignTokens.space1),
|
horizontal: DesignTokens.space2,
|
||||||
|
vertical: DesignTokens.space1,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: DesignTokens.dynamicPrimary.withValues(alpha: 0.1),
|
color: DesignTokens.dynamicPrimary.withValues(alpha: 0.1),
|
||||||
borderRadius: DesignTokens.borderRadiusSm,
|
borderRadius: DesignTokens.borderRadiusSm,
|
||||||
@@ -345,17 +564,21 @@ class _BodyAnalysisPageState extends State<BodyAnalysisPage>
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
_isInputHidden
|
_isInputHidden
|
||||||
? CupertinoIcons.eye_slash
|
? CupertinoIcons.eye_slash
|
||||||
: CupertinoIcons.eye,
|
: CupertinoIcons.eye,
|
||||||
size: 14,
|
size: 14,
|
||||||
color: DesignTokens.dynamicPrimary),
|
color: DesignTokens.dynamicPrimary,
|
||||||
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(_isInputHidden ? '显示' : '隐藏',
|
Text(
|
||||||
style: TextStyle(
|
_isInputHidden ? '显示' : '隐藏',
|
||||||
fontSize: DesignTokens.fontXs,
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w500,
|
fontSize: DesignTokens.fontXs,
|
||||||
color: DesignTokens.dynamicPrimary)),
|
fontWeight: FontWeight.w500,
|
||||||
|
color: DesignTokens.dynamicPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -374,16 +597,20 @@ class _BodyAnalysisPageState extends State<BodyAnalysisPage>
|
|||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(CupertinoIcons.eye_slash,
|
Icon(
|
||||||
size: 16,
|
CupertinoIcons.eye_slash,
|
||||||
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3),
|
size: 16,
|
||||||
|
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
|
||||||
|
),
|
||||||
const SizedBox(width: DesignTokens.space2),
|
const SizedBox(width: DesignTokens.space2),
|
||||||
Text(
|
Text(
|
||||||
'信息已隐藏,点击"显示"查看',
|
'信息已隐藏,点击"显示"查看',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: DesignTokens.fontSm,
|
fontSize: DesignTokens.fontSm,
|
||||||
color:
|
color: isDark
|
||||||
isDark ? DarkDesignTokens.text3 : DesignTokens.text3),
|
? DarkDesignTokens.text3
|
||||||
|
: DesignTokens.text3,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -393,25 +620,28 @@ class _BodyAnalysisPageState extends State<BodyAnalysisPage>
|
|||||||
_buildGenderSelector(isDark),
|
_buildGenderSelector(isDark),
|
||||||
const SizedBox(height: DesignTokens.space3),
|
const SizedBox(height: DesignTokens.space3),
|
||||||
_buildInputField(
|
_buildInputField(
|
||||||
isDark: isDark,
|
isDark: isDark,
|
||||||
controller: _ageController,
|
controller: _ageController,
|
||||||
label: '年龄',
|
label: '年龄',
|
||||||
unit: '岁',
|
unit: '岁',
|
||||||
placeholder: '请输入年龄'),
|
placeholder: '请输入年龄',
|
||||||
|
),
|
||||||
const SizedBox(height: DesignTokens.space3),
|
const SizedBox(height: DesignTokens.space3),
|
||||||
_buildInputField(
|
_buildInputField(
|
||||||
isDark: isDark,
|
isDark: isDark,
|
||||||
controller: _heightController,
|
controller: _heightController,
|
||||||
label: '身高',
|
label: '身高',
|
||||||
unit: 'cm',
|
unit: 'cm',
|
||||||
placeholder: '请输入身高'),
|
placeholder: '请输入身高',
|
||||||
|
),
|
||||||
const SizedBox(height: DesignTokens.space3),
|
const SizedBox(height: DesignTokens.space3),
|
||||||
_buildInputField(
|
_buildInputField(
|
||||||
isDark: isDark,
|
isDark: isDark,
|
||||||
controller: _weightController,
|
controller: _weightController,
|
||||||
label: '体重',
|
label: '体重',
|
||||||
unit: 'kg',
|
unit: 'kg',
|
||||||
placeholder: '请输入体重'),
|
placeholder: '请输入体重',
|
||||||
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -422,10 +652,13 @@ class _BodyAnalysisPageState extends State<BodyAnalysisPage>
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('性别',
|
Text(
|
||||||
style: TextStyle(
|
'性别',
|
||||||
fontSize: DesignTokens.fontSm,
|
style: TextStyle(
|
||||||
color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2)),
|
fontSize: DesignTokens.fontSm,
|
||||||
|
color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: DesignTokens.space2),
|
const SizedBox(height: DesignTokens.space2),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@@ -435,25 +668,29 @@ class _BodyAnalysisPageState extends State<BodyAnalysisPage>
|
|||||||
children: {
|
children: {
|
||||||
Gender.male: Padding(
|
Gender.male: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: DesignTokens.space4,
|
horizontal: DesignTokens.space4,
|
||||||
vertical: DesignTokens.space2),
|
vertical: DesignTokens.space2,
|
||||||
child: Text('👨 男性',
|
),
|
||||||
style: TextStyle(
|
child: Text(
|
||||||
fontSize: DesignTokens.fontMd,
|
'👨 男性',
|
||||||
color: isDark
|
style: TextStyle(
|
||||||
? DarkDesignTokens.text1
|
fontSize: DesignTokens.fontMd,
|
||||||
: DesignTokens.text1)),
|
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Gender.female: Padding(
|
Gender.female: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: DesignTokens.space4,
|
horizontal: DesignTokens.space4,
|
||||||
vertical: DesignTokens.space2),
|
vertical: DesignTokens.space2,
|
||||||
child: Text('👩 女性',
|
),
|
||||||
style: TextStyle(
|
child: Text(
|
||||||
fontSize: DesignTokens.fontMd,
|
'👩 女性',
|
||||||
color: isDark
|
style: TextStyle(
|
||||||
? DarkDesignTokens.text1
|
fontSize: DesignTokens.fontMd,
|
||||||
: DesignTokens.text1)),
|
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -467,29 +704,36 @@ class _BodyAnalysisPageState extends State<BodyAnalysisPage>
|
|||||||
onTap: () => setState(() => _showOptional = !_showOptional),
|
onTap: () => setState(() => _showOptional = !_showOptional),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: DesignTokens.space4, vertical: DesignTokens.space3),
|
horizontal: DesignTokens.space4,
|
||||||
|
vertical: DesignTokens.space3,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isDark ? DarkDesignTokens.card : DesignTokens.card,
|
color: isDark ? DarkDesignTokens.card : DesignTokens.card,
|
||||||
borderRadius: DesignTokens.borderRadiusLg,
|
borderRadius: DesignTokens.borderRadiusLg,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: isDark ? DarkDesignTokens.glassBorder : DesignTokens.glassBorder),
|
color: isDark
|
||||||
|
? DarkDesignTokens.glassBorder
|
||||||
|
: DesignTokens.glassBorder,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
_showOptional
|
_showOptional
|
||||||
? CupertinoIcons.chevron_up
|
? CupertinoIcons.chevron_up
|
||||||
: CupertinoIcons.chevron_down,
|
: CupertinoIcons.chevron_down,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: DesignTokens.dynamicPrimary),
|
color: DesignTokens.dynamicPrimary,
|
||||||
|
),
|
||||||
const SizedBox(width: DesignTokens.space2),
|
const SizedBox(width: DesignTokens.space2),
|
||||||
Text(
|
Text(
|
||||||
'更多参数(选填,可解锁更多指标)',
|
'更多参数(选填,可解锁更多指标)',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: DesignTokens.fontMd,
|
fontSize: DesignTokens.fontMd,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: DesignTokens.dynamicPrimary),
|
color: DesignTokens.dynamicPrimary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -503,53 +747,64 @@ class _BodyAnalysisPageState extends State<BodyAnalysisPage>
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('选填参数',
|
Text(
|
||||||
style: TextStyle(
|
'选填参数',
|
||||||
fontSize: DesignTokens.fontLg,
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w600,
|
fontSize: DesignTokens.fontLg,
|
||||||
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1)),
|
fontWeight: FontWeight.w600,
|
||||||
|
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: DesignTokens.space1),
|
const SizedBox(height: DesignTokens.space1),
|
||||||
Text('填写越多,分析越精确',
|
Text(
|
||||||
style: TextStyle(
|
'填写越多,分析越精确',
|
||||||
fontSize: DesignTokens.fontSm,
|
style: TextStyle(
|
||||||
color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2)),
|
fontSize: DesignTokens.fontSm,
|
||||||
|
color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: DesignTokens.space3),
|
const SizedBox(height: DesignTokens.space3),
|
||||||
_buildInputField(
|
_buildInputField(
|
||||||
isDark: isDark,
|
isDark: isDark,
|
||||||
controller: _waistController,
|
controller: _waistController,
|
||||||
label: '腰围',
|
label: '腰围',
|
||||||
unit: 'cm',
|
unit: 'cm',
|
||||||
placeholder: '选填'),
|
placeholder: '选填',
|
||||||
|
),
|
||||||
const SizedBox(height: DesignTokens.space3),
|
const SizedBox(height: DesignTokens.space3),
|
||||||
_buildInputField(
|
_buildInputField(
|
||||||
isDark: isDark,
|
isDark: isDark,
|
||||||
controller: _hipController,
|
controller: _hipController,
|
||||||
label: '臀围',
|
label: '臀围',
|
||||||
unit: 'cm',
|
unit: 'cm',
|
||||||
placeholder: '选填'),
|
placeholder: '选填',
|
||||||
|
),
|
||||||
const SizedBox(height: DesignTokens.space3),
|
const SizedBox(height: DesignTokens.space3),
|
||||||
_buildInputField(
|
_buildInputField(
|
||||||
isDark: isDark,
|
isDark: isDark,
|
||||||
controller: _neckController,
|
controller: _neckController,
|
||||||
label: '颈围',
|
label: '颈围',
|
||||||
unit: 'cm',
|
unit: 'cm',
|
||||||
placeholder: '选填'),
|
placeholder: '选填',
|
||||||
|
),
|
||||||
const SizedBox(height: DesignTokens.space3),
|
const SizedBox(height: DesignTokens.space3),
|
||||||
_buildInputField(
|
_buildInputField(
|
||||||
isDark: isDark,
|
isDark: isDark,
|
||||||
controller: _wristController,
|
controller: _wristController,
|
||||||
label: '手腕围',
|
label: '手腕围',
|
||||||
unit: 'cm',
|
unit: 'cm',
|
||||||
placeholder: '选填'),
|
placeholder: '选填',
|
||||||
|
),
|
||||||
const SizedBox(height: DesignTokens.space3),
|
const SizedBox(height: DesignTokens.space3),
|
||||||
_buildActivitySelector(isDark),
|
_buildActivitySelector(isDark),
|
||||||
const SizedBox(height: DesignTokens.space3),
|
const SizedBox(height: DesignTokens.space3),
|
||||||
_buildInputField(
|
_buildInputField(
|
||||||
isDark: isDark,
|
isDark: isDark,
|
||||||
controller: _bodyFatController,
|
controller: _bodyFatController,
|
||||||
label: '已知体脂率',
|
label: '已知体脂率',
|
||||||
unit: '%',
|
unit: '%',
|
||||||
placeholder: '选填,跳过自动估算'),
|
placeholder: '选填,跳过自动估算',
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -567,10 +822,13 @@ class _BodyAnalysisPageState extends State<BodyAnalysisPage>
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('活动等级',
|
Text(
|
||||||
style: TextStyle(
|
'活动等级',
|
||||||
fontSize: DesignTokens.fontSm,
|
style: TextStyle(
|
||||||
color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2)),
|
fontSize: DesignTokens.fontSm,
|
||||||
|
color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: DesignTokens.space2),
|
const SizedBox(height: DesignTokens.space2),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(DesignTokens.space1),
|
padding: const EdgeInsets.all(DesignTokens.space1),
|
||||||
@@ -587,8 +845,9 @@ class _BodyAnalysisPageState extends State<BodyAnalysisPage>
|
|||||||
onTap: () => setState(() => _activityLevel = e.$1),
|
onTap: () => setState(() => _activityLevel = e.$1),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: DesignTokens.space3,
|
horizontal: DesignTokens.space3,
|
||||||
vertical: DesignTokens.space2 + 2),
|
vertical: DesignTokens.space2 + 2,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? (isDark ? DarkDesignTokens.card : DesignTokens.card)
|
? (isDark ? DarkDesignTokens.card : DesignTokens.card)
|
||||||
@@ -598,23 +857,30 @@ class _BodyAnalysisPageState extends State<BodyAnalysisPage>
|
|||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(e.$2,
|
Text(
|
||||||
style: TextStyle(
|
e.$2,
|
||||||
fontSize: DesignTokens.fontMd,
|
style: TextStyle(
|
||||||
fontWeight:
|
fontSize: DesignTokens.fontMd,
|
||||||
isSelected ? FontWeight.w600 : FontWeight.w400,
|
fontWeight: isSelected
|
||||||
color: isSelected
|
? FontWeight.w600
|
||||||
? DesignTokens.dynamicPrimary
|
: FontWeight.w400,
|
||||||
: (isDark
|
color: isSelected
|
||||||
? DarkDesignTokens.text2
|
? DesignTokens.dynamicPrimary
|
||||||
: DesignTokens.text2))),
|
: (isDark
|
||||||
|
? DarkDesignTokens.text2
|
||||||
|
: DesignTokens.text2),
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(width: DesignTokens.space2),
|
const SizedBox(width: DesignTokens.space2),
|
||||||
Text(e.$3,
|
Text(
|
||||||
style: TextStyle(
|
e.$3,
|
||||||
fontSize: DesignTokens.fontSm,
|
style: TextStyle(
|
||||||
color: isDark
|
fontSize: DesignTokens.fontSm,
|
||||||
? DarkDesignTokens.text3
|
color: isDark
|
||||||
: DesignTokens.text3)),
|
? DarkDesignTokens.text3
|
||||||
|
: DesignTokens.text3,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -636,10 +902,13 @@ class _BodyAnalysisPageState extends State<BodyAnalysisPage>
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(label,
|
Text(
|
||||||
style: TextStyle(
|
label,
|
||||||
fontSize: DesignTokens.fontSm,
|
style: TextStyle(
|
||||||
color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2)),
|
fontSize: DesignTokens.fontSm,
|
||||||
|
color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: DesignTokens.space1),
|
const SizedBox(height: DesignTokens.space1),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -647,7 +916,9 @@ class _BodyAnalysisPageState extends State<BodyAnalysisPage>
|
|||||||
child: CupertinoTextField(
|
child: CupertinoTextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
placeholder: placeholder,
|
placeholder: placeholder,
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
keyboardType: const TextInputType.numberWithOptions(
|
||||||
|
decimal: true,
|
||||||
|
),
|
||||||
padding: const EdgeInsets.all(DesignTokens.space3),
|
padding: const EdgeInsets.all(DesignTokens.space3),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isDark
|
color: isDark
|
||||||
@@ -661,18 +932,22 @@ class _BodyAnalysisPageState extends State<BodyAnalysisPage>
|
|||||||
const SizedBox(width: DesignTokens.space2),
|
const SizedBox(width: DesignTokens.space2),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: DesignTokens.space3, vertical: DesignTokens.space2),
|
horizontal: DesignTokens.space3,
|
||||||
|
vertical: DesignTokens.space2,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isDark
|
color: isDark
|
||||||
? DarkDesignTokens.text3.withValues(alpha: 0.1)
|
? DarkDesignTokens.text3.withValues(alpha: 0.1)
|
||||||
: DesignTokens.background,
|
: DesignTokens.background,
|
||||||
borderRadius: DesignTokens.borderRadiusMd,
|
borderRadius: DesignTokens.borderRadiusMd,
|
||||||
),
|
),
|
||||||
child: Text(unit,
|
child: Text(
|
||||||
style: TextStyle(
|
unit,
|
||||||
fontSize: DesignTokens.fontMd,
|
style: TextStyle(
|
||||||
color:
|
fontSize: DesignTokens.fontMd,
|
||||||
isDark ? DarkDesignTokens.text2 : DesignTokens.text2)),
|
color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* 名称: 身体分析核心指标Tab
|
* 名称: 身体分析核心指标Tab
|
||||||
* 作用: 展示喝水/BMR/TDEE/BMI/体脂率/心率区间/摄入需求/蛋白质等核心指标
|
* 作用: 展示喝水/BMR/TDEE/BMI/体脂率/心率区间/摄入需求/蛋白质等核心指标
|
||||||
* 创建: 2026-04-24
|
* 创建: 2026-04-24
|
||||||
* 更新: 2026-04-24 从 body_analysis_page.dart 拆分
|
* 更新: 2026-04-24 修复溢出+增加滑动提示
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
@@ -19,63 +19,103 @@ class CoreMetricsTab extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return SingleChildScrollView(
|
||||||
children: [
|
physics: const ClampingScrollPhysics(),
|
||||||
WaterCard(isDark: isDark, calc: calc),
|
child: Column(
|
||||||
const SizedBox(height: DesignTokens.space3),
|
children: [
|
||||||
MetricCard(
|
WaterCard(isDark: isDark, calc: calc),
|
||||||
isDark: isDark,
|
const SizedBox(height: DesignTokens.space3),
|
||||||
emoji: '🔥',
|
MetricCard(
|
||||||
title: '基础代谢率 (BMR)',
|
isDark: isDark,
|
||||||
value: calc.bmr.toStringAsFixed(0),
|
emoji: '🔥',
|
||||||
unit: 'kcal/天',
|
title: '基础代谢率 (BMR)',
|
||||||
color: DesignTokens.orange,
|
value: calc.bmr.toStringAsFixed(0),
|
||||||
infoTitle: '基础代谢率 (BMR)',
|
unit: 'kcal/天',
|
||||||
infoContent: '基础代谢率是人体在安静状态下维持生命所需的最低能量消耗。'
|
color: DesignTokens.orange,
|
||||||
'采用Mifflin-St Jeor公式计算,被认为是目前最准确的BMR估算公式。\n\n'
|
infoTitle: '基础代谢率 (BMR)',
|
||||||
'男性: BMR = 10×体重(kg) + 6.25×身高(cm) - 5×年龄 + 5\n'
|
infoContent:
|
||||||
'女性: BMR = 10×体重(kg) + 6.25×身高(cm) - 5×年龄 - 161',
|
'基础代谢率是人体在安静状态下维持生命所需的最低能量消耗。'
|
||||||
),
|
'采用Mifflin-St Jeor公式计算,被认为是目前最准确的BMR估算公式。\n\n'
|
||||||
const SizedBox(height: DesignTokens.space3),
|
'男性: BMR = 10×体重(kg) + 6.25×身高(cm) - 5×年龄 + 5\n'
|
||||||
MetricCard(
|
'女性: BMR = 10×体重(kg) + 6.25×身高(cm) - 5×年龄 - 161',
|
||||||
isDark: isDark,
|
),
|
||||||
emoji: '🏃',
|
const SizedBox(height: DesignTokens.space3),
|
||||||
title: '每日总能量消耗 (TDEE)',
|
MetricCard(
|
||||||
value: calc.tdee.toStringAsFixed(0),
|
isDark: isDark,
|
||||||
unit: 'kcal/天',
|
emoji: '🏃',
|
||||||
color: DesignTokens.green,
|
title: '每日总能量消耗 (TDEE)',
|
||||||
infoTitle: '每日总能量消耗 (TDEE)',
|
value: calc.tdee.toStringAsFixed(0),
|
||||||
infoContent: 'TDEE = BMR × 活动系数,表示你每天实际消耗的总热量。\n\n'
|
unit: 'kcal/天',
|
||||||
'活动系数:\n'
|
color: DesignTokens.green,
|
||||||
'久坐: 1.2 | 轻度: 1.375 | 中度: 1.55\n'
|
infoTitle: '每日总能量消耗 (TDEE)',
|
||||||
'高度: 1.725 | 极高: 1.9\n\n'
|
infoContent:
|
||||||
'减脂建议: TDEE - 500 kcal\n'
|
'TDEE = BMR × 活动系数,表示你每天实际消耗的总热量。\n\n'
|
||||||
'增肌建议: TDEE + 300 kcal',
|
'活动系数:\n'
|
||||||
),
|
'久坐: 1.2 | 轻度: 1.375 | 中度: 1.55\n'
|
||||||
const SizedBox(height: DesignTokens.space3),
|
'高度: 1.725 | 极高: 1.9\n\n'
|
||||||
MetricCard(
|
'减脂建议: TDEE - 500 kcal\n'
|
||||||
isDark: isDark,
|
'增肌建议: TDEE + 300 kcal',
|
||||||
emoji: '📊',
|
),
|
||||||
title: '身体质量指数 (BMI)',
|
const SizedBox(height: DesignTokens.space3),
|
||||||
value: calc.bmi.toStringAsFixed(1),
|
MetricCard(
|
||||||
unit: calc.bmiCategory,
|
isDark: isDark,
|
||||||
color: Color(calc.bmiColorValue),
|
emoji: '📊',
|
||||||
infoTitle: '身体质量指数 (BMI)',
|
title: '身体质量指数 (BMI)',
|
||||||
infoContent: 'BMI = 体重(kg) / 身高(m)²\n\n'
|
value: calc.bmi.toStringAsFixed(1),
|
||||||
'分类标准(中国):\n'
|
unit: calc.bmiCategory,
|
||||||
'< 18.5 偏瘦 | 18.5-23.9 正常\n'
|
color: Color(calc.bmiColorValue),
|
||||||
'24-27.9 偏胖 | ≥ 28 肥胖\n\n'
|
infoTitle: '身体质量指数 (BMI)',
|
||||||
'注意:BMI未考虑肌肉量和体脂分布,运动员等人群可能不准确。',
|
infoContent:
|
||||||
),
|
'BMI = 体重(kg) / 身高(m)²\n\n'
|
||||||
const SizedBox(height: DesignTokens.space3),
|
'分类标准(中国):\n'
|
||||||
BodyFatCard(isDark: isDark, calc: calc),
|
'< 18.5 偏瘦 | 18.5-23.9 正常\n'
|
||||||
const SizedBox(height: DesignTokens.space3),
|
'24-27.9 偏胖 | ≥ 28 肥胖\n\n'
|
||||||
HeartRateZonesCard(isDark: isDark, calc: calc),
|
'注意:BMI未考虑肌肉量和体脂分布,运动员等人群可能不准确。',
|
||||||
const SizedBox(height: DesignTokens.space3),
|
),
|
||||||
GoalCaloriesCard(isDark: isDark, calc: calc),
|
const SizedBox(height: DesignTokens.space3),
|
||||||
const SizedBox(height: DesignTokens.space3),
|
FatBurningHeartRateCard(isDark: isDark, calc: calc),
|
||||||
ProteinCard(isDark: isDark, calc: calc),
|
const SizedBox(height: DesignTokens.space3),
|
||||||
],
|
BodyFatCard(isDark: isDark, calc: calc),
|
||||||
|
const SizedBox(height: DesignTokens.space3),
|
||||||
|
HeartRateZonesCard(isDark: isDark, calc: calc),
|
||||||
|
const SizedBox(height: DesignTokens.space3),
|
||||||
|
GoalCaloriesCard(isDark: isDark, calc: calc),
|
||||||
|
const SizedBox(height: DesignTokens.space3),
|
||||||
|
ProteinCard(isDark: isDark, calc: calc),
|
||||||
|
const SizedBox(height: DesignTokens.space5),
|
||||||
|
_buildSwipeHint(isDark),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSwipeHint(bool isDark) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: DesignTokens.space4),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
CupertinoIcons.chevron_left,
|
||||||
|
size: 14,
|
||||||
|
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
|
||||||
|
),
|
||||||
|
const SizedBox(width: DesignTokens.space2),
|
||||||
|
Text(
|
||||||
|
'👈 左右滑动切换 核心指标 / 科学指数 / 趣味统计 👉',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: DesignTokens.fontXs,
|
||||||
|
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: DesignTokens.space2),
|
||||||
|
Icon(
|
||||||
|
CupertinoIcons.chevron_right,
|
||||||
|
size: 14,
|
||||||
|
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* 名称: 身体分析趣味统计Tab
|
* 名称: 身体分析趣味统计Tab
|
||||||
* 作用: 展示心跳/呼吸/眨眼/DNA/原子等趣味统计数据
|
* 作用: 展示心跳/呼吸/眨眼/DNA/原子等趣味统计数据
|
||||||
* 创建: 2026-04-24
|
* 创建: 2026-04-24
|
||||||
* 更新: 2026-04-24 从 body_analysis_page.dart 拆分
|
* 更新: 2026-04-24 修复溢出+增加滑动提示
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
@@ -19,264 +19,301 @@ class FunStatsTab extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return SingleChildScrollView(
|
||||||
children: [
|
physics: const ClampingScrollPhysics(),
|
||||||
MetricCard(
|
child: Column(
|
||||||
isDark: isDark,
|
children: [
|
||||||
emoji: '🫀',
|
MetricCard(
|
||||||
title: '今日心跳次数',
|
isDark: isDark,
|
||||||
value: calc.formatNumber(calc.dailyHeartbeats.toDouble()),
|
emoji: '🫀',
|
||||||
unit: '次',
|
title: '今日心跳次数',
|
||||||
color: DesignTokens.red,
|
value: calc.formatNumber(calc.dailyHeartbeats.toDouble()),
|
||||||
infoTitle: '心跳次数',
|
unit: '次',
|
||||||
infoContent:
|
color: DesignTokens.red,
|
||||||
'平均心率因年龄而异:\n'
|
infoTitle: '心跳次数',
|
||||||
'婴儿约130次/分 | 儿童约90次/分\n'
|
infoContent:
|
||||||
'成人约72次/分\n\n'
|
'平均心率因年龄而异:\n'
|
||||||
'心脏每天跳动约10万次,一生约25-30亿次。',
|
'婴儿约130次/分 | 儿童约90次/分\n'
|
||||||
),
|
'成人约72次/分\n\n'
|
||||||
const SizedBox(height: DesignTokens.space3),
|
'心脏每天跳动约10万次,一生约25-30亿次。',
|
||||||
MetricCard(
|
),
|
||||||
isDark: isDark,
|
const SizedBox(height: DesignTokens.space3),
|
||||||
emoji: '🫁',
|
MetricCard(
|
||||||
title: '今日呼吸次数',
|
isDark: isDark,
|
||||||
value: calc.formatNumber(calc.dailyBreaths.toDouble()),
|
emoji: '🫁',
|
||||||
unit: '次',
|
title: '今日呼吸次数',
|
||||||
color: DesignTokens.blue,
|
value: calc.formatNumber(calc.dailyBreaths.toDouble()),
|
||||||
infoTitle: '呼吸次数',
|
unit: '次',
|
||||||
infoContent:
|
color: DesignTokens.blue,
|
||||||
'成人平均呼吸频率约16次/分钟。\n\n'
|
infoTitle: '呼吸次数',
|
||||||
'每次呼吸约吸入0.5升空气,\n'
|
infoContent:
|
||||||
'每天呼吸约23,040次,\n'
|
'成人平均呼吸频率约16次/分钟。\n\n'
|
||||||
'吸入空气约11,520升。',
|
'每次呼吸约吸入0.5升空气,\n'
|
||||||
),
|
'每天呼吸约23,040次,\n'
|
||||||
const SizedBox(height: DesignTokens.space3),
|
'吸入空气约11,520升。',
|
||||||
MetricCard(
|
),
|
||||||
isDark: isDark,
|
const SizedBox(height: DesignTokens.space3),
|
||||||
emoji: '👁️',
|
MetricCard(
|
||||||
title: '今日眨眼次数',
|
isDark: isDark,
|
||||||
value: calc.formatNumber(calc.dailyBlinks.toDouble()),
|
emoji: '👁️',
|
||||||
unit: '次',
|
title: '今日眨眼次数',
|
||||||
color: DesignTokens.teal,
|
value: calc.formatNumber(calc.dailyBlinks.toDouble()),
|
||||||
infoTitle: '眨眼次数',
|
unit: '次',
|
||||||
infoContent:
|
color: DesignTokens.teal,
|
||||||
'人类平均每分钟眨眼15-20次,\n'
|
infoTitle: '眨眼次数',
|
||||||
'每天约14,400次。\n\n'
|
infoContent:
|
||||||
'每次眨眼约0.1-0.4秒,\n'
|
'人类平均每分钟眨眼15-20次,\n'
|
||||||
'眨眼可以清洁和润滑眼球,'
|
'每天约14,400次。\n\n'
|
||||||
'同时大脑会利用眨眼间隙重新调整注意力。',
|
'每次眨眼约0.1-0.4秒,\n'
|
||||||
),
|
'眨眼可以清洁和润滑眼球,'
|
||||||
const SizedBox(height: DesignTokens.space3),
|
'同时大脑会利用眨眼间隙重新调整注意力。',
|
||||||
MetricCard(
|
),
|
||||||
isDark: isDark,
|
const SizedBox(height: DesignTokens.space3),
|
||||||
emoji: '🩸',
|
MetricCard(
|
||||||
title: '今日心脏泵血量',
|
isDark: isDark,
|
||||||
value: calc.dailyBloodPumped.toStringAsFixed(0),
|
emoji: '🩸',
|
||||||
unit: '升',
|
title: '今日心脏泵血量',
|
||||||
color: DesignTokens.red,
|
value: calc.dailyBloodPumped.toStringAsFixed(0),
|
||||||
infoTitle: '心脏泵血量',
|
unit: '升',
|
||||||
infoContent:
|
color: DesignTokens.red,
|
||||||
'心脏每次跳动泵出约70ml血液,\n'
|
infoTitle: '心脏泵血量',
|
||||||
'每天泵血约7,000-10,000升。\n\n'
|
infoContent:
|
||||||
'这个量足以填满一个标准游泳池的1/250。',
|
'心脏每次跳动泵出约70ml血液,\n'
|
||||||
),
|
'每天泵血约7,000-10,000升。\n\n'
|
||||||
const SizedBox(height: DesignTokens.space3),
|
'这个量足以填满一个标准游泳池的1/250。',
|
||||||
MetricCard(
|
),
|
||||||
isDark: isDark,
|
const SizedBox(height: DesignTokens.space3),
|
||||||
emoji: '🌬️',
|
MetricCard(
|
||||||
title: '今日呼吸空气体积',
|
isDark: isDark,
|
||||||
value: calc.dailyAirVolume.toStringAsFixed(0),
|
emoji: '🌬️',
|
||||||
unit: '升',
|
title: '今日呼吸空气体积',
|
||||||
color: DesignTokens.blue,
|
value: calc.dailyAirVolume.toStringAsFixed(0),
|
||||||
infoTitle: '呼吸空气体积',
|
unit: '升',
|
||||||
infoContent:
|
color: DesignTokens.blue,
|
||||||
'每次呼吸约0.5升,每分钟16次,\n'
|
infoTitle: '呼吸空气体积',
|
||||||
'每天约吸入11,520升空气。\n\n'
|
infoContent:
|
||||||
'这相当于约12立方米,'
|
'每次呼吸约0.5升,每分钟16次,\n'
|
||||||
'足够充满一个小型储藏室。',
|
'每天约吸入11,520升空气。\n\n'
|
||||||
),
|
'这相当于约12立方米,'
|
||||||
const SizedBox(height: DesignTokens.space3),
|
'足够充满一个小型储藏室。',
|
||||||
MetricCard(
|
),
|
||||||
isDark: isDark,
|
const SizedBox(height: DesignTokens.space3),
|
||||||
emoji: '💧',
|
MetricCard(
|
||||||
title: '今日唾液分泌量',
|
isDark: isDark,
|
||||||
value: calc.dailySaliva.toStringAsFixed(1),
|
emoji: '💧',
|
||||||
unit: '升',
|
title: '今日唾液分泌量',
|
||||||
color: DesignTokens.green,
|
value: calc.dailySaliva.toStringAsFixed(1),
|
||||||
infoTitle: '唾液分泌量',
|
unit: '升',
|
||||||
infoContent:
|
color: DesignTokens.green,
|
||||||
'人体每天分泌约1-1.5升唾液。\n\n'
|
infoTitle: '唾液分泌量',
|
||||||
'唾液含有淀粉酶帮助消化,'
|
infoContent:
|
||||||
'溶菌酶帮助杀菌,'
|
'人体每天分泌约1-1.5升唾液。\n\n'
|
||||||
'还能保持口腔湿润、帮助味觉感知。\n'
|
'唾液含有淀粉酶帮助消化,'
|
||||||
'一生约分泌25,000-40,000升唾液。',
|
'溶菌酶帮助杀菌,'
|
||||||
),
|
'还能保持口腔湿润、帮助味觉感知。\n'
|
||||||
const SizedBox(height: DesignTokens.space3),
|
'一生约分泌25,000-40,000升唾液。',
|
||||||
MetricCard(
|
),
|
||||||
isDark: isDark,
|
const SizedBox(height: DesignTokens.space3),
|
||||||
emoji: '🧬',
|
MetricCard(
|
||||||
title: '体内DNA总长度',
|
isDark: isDark,
|
||||||
value: calc.formatDistance(calc.totalDnaLength),
|
emoji: '🧬',
|
||||||
unit: '',
|
title: '体内DNA总长度',
|
||||||
color: DesignTokens.purple,
|
value: calc.formatDistance(calc.totalDnaLength),
|
||||||
infoTitle: 'DNA总长度',
|
unit: '',
|
||||||
infoContent:
|
color: DesignTokens.purple,
|
||||||
'人体约有37.2万亿个细胞,\n'
|
infoTitle: 'DNA总长度',
|
||||||
'每个细胞含约2米长的DNA。\n\n'
|
infoContent:
|
||||||
'全部DNA展开总长度约744亿公里,\n'
|
'人体约有37.2万亿个细胞,\n'
|
||||||
'约为地球到太阳距离的500倍!\n'
|
'每个细胞含约2米长的DNA。\n\n'
|
||||||
'但DNA直径仅2纳米,比头发丝细10万倍。',
|
'全部DNA展开总长度约744亿公里,\n'
|
||||||
),
|
'约为地球到太阳距离的500倍!\n'
|
||||||
const SizedBox(height: DesignTokens.space3),
|
'但DNA直径仅2纳米,比头发丝细10万倍。',
|
||||||
MetricCard(
|
),
|
||||||
isDark: isDark,
|
const SizedBox(height: DesignTokens.space3),
|
||||||
emoji: '🌍',
|
MetricCard(
|
||||||
title: '身体原子数量',
|
isDark: isDark,
|
||||||
value: '~7×10²⁷',
|
emoji: '🌍',
|
||||||
unit: '个原子',
|
title: '身体原子数量',
|
||||||
color: DesignTokens.gold,
|
value: '~7×10²⁷',
|
||||||
infoTitle: '身体原子数量',
|
unit: '个原子',
|
||||||
infoContent:
|
color: DesignTokens.gold,
|
||||||
'成年人体内约有7×10²⁷个原子,\n'
|
infoTitle: '身体原子数量',
|
||||||
'即7后面跟27个零。\n\n'
|
infoContent:
|
||||||
'组成比例:\n'
|
'成年人体内约有7×10²⁷个原子,\n'
|
||||||
'氧65% | 碳18% | 氢10% | 氮3%\n'
|
'即7后面跟27个零。\n\n'
|
||||||
'其余为钙、磷、钾、硫等微量元素。\n'
|
'组成比例:\n'
|
||||||
'每年约98%的原子会被替换。',
|
'氧65% | 碳18% | 氢10% | 氮3%\n'
|
||||||
),
|
'其余为钙、磷、钾、硫等微量元素。\n'
|
||||||
const SizedBox(height: DesignTokens.space3),
|
'每年约98%的原子会被替换。',
|
||||||
MetricCard(
|
),
|
||||||
isDark: isDark,
|
const SizedBox(height: DesignTokens.space3),
|
||||||
emoji: '💇',
|
MetricCard(
|
||||||
title: '一生头发生长总长度',
|
isDark: isDark,
|
||||||
value: calc.formatDistance(calc.lifetimeHairLength),
|
emoji: '💇',
|
||||||
unit: '',
|
title: '一生头发生长总长度',
|
||||||
color: DesignTokens.orange,
|
value: calc.formatDistance(calc.lifetimeHairLength),
|
||||||
infoTitle: '头发生长',
|
unit: '',
|
||||||
infoContent:
|
color: DesignTokens.orange,
|
||||||
'头发平均每月生长约1.25厘米,\n'
|
infoTitle: '头发生长',
|
||||||
'每年约15厘米。\n\n'
|
infoContent:
|
||||||
'人类头皮约有10万个毛囊,\n'
|
'头发平均每月生长约1.25厘米,\n'
|
||||||
'每天正常脱落50-100根头发。\n'
|
'每年约15厘米。\n\n'
|
||||||
'金发人群平均有15万个毛囊,\n'
|
'人类头皮约有10万个毛囊,\n'
|
||||||
'红发人群约9万个。',
|
'每天正常脱落50-100根头发。\n'
|
||||||
),
|
'金发人群平均有15万个毛囊,\n'
|
||||||
const SizedBox(height: DesignTokens.space3),
|
'红发人群约9万个。',
|
||||||
MetricCard(
|
),
|
||||||
isDark: isDark,
|
const SizedBox(height: DesignTokens.space3),
|
||||||
emoji: '💅',
|
MetricCard(
|
||||||
title: '一生指甲生长总长度',
|
isDark: isDark,
|
||||||
value: calc.formatDistance(calc.lifetimeNailLength),
|
emoji: '💅',
|
||||||
unit: '',
|
title: '一生指甲生长总长度',
|
||||||
color: DesignTokens.teal,
|
value: calc.formatDistance(calc.lifetimeNailLength),
|
||||||
infoTitle: '指甲生长',
|
unit: '',
|
||||||
infoContent:
|
color: DesignTokens.teal,
|
||||||
'手指甲每月生长约3.5毫米,\n'
|
infoTitle: '指甲生长',
|
||||||
'脚趾甲生长速度约手指甲的1/3。\n\n'
|
infoContent:
|
||||||
'指甲由角蛋白组成,与头发成分相同。\n'
|
'手指甲每月生长约3.5毫米,\n'
|
||||||
'惯用手的指甲生长更快,\n'
|
'脚趾甲生长速度约手指甲的1/3。\n\n'
|
||||||
'夏天比冬天生长更快。',
|
'指甲由角蛋白组成,与头发成分相同。\n'
|
||||||
),
|
'惯用手的指甲生长更快,\n'
|
||||||
const SizedBox(height: DesignTokens.space3),
|
'夏天比冬天生长更快。',
|
||||||
MetricCard(
|
),
|
||||||
isDark: isDark,
|
const SizedBox(height: DesignTokens.space3),
|
||||||
emoji: '⏱️',
|
MetricCard(
|
||||||
title: '一生心跳总数',
|
isDark: isDark,
|
||||||
value: calc.formatNumber(calc.lifetimeHeartbeats),
|
emoji: '⏱️',
|
||||||
unit: '次(基于年龄)',
|
title: '一生心跳总数',
|
||||||
color: DesignTokens.red,
|
value: calc.formatNumber(calc.lifetimeHeartbeats),
|
||||||
infoTitle: '一生心跳总数',
|
unit: '次(基于年龄)',
|
||||||
infoContent:
|
color: DesignTokens.red,
|
||||||
'基于你的年龄和平均心率估算。\n\n'
|
infoTitle: '一生心跳总数',
|
||||||
'如果活到80岁,心脏大约跳动30亿次。\n'
|
infoContent:
|
||||||
'心脏是人体中最勤劳的器官,\n'
|
'基于你的年龄和平均心率估算。\n\n'
|
||||||
'从胚胎4周开始跳动,直到生命终结。',
|
'如果活到80岁,心脏大约跳动30亿次。\n'
|
||||||
),
|
'心脏是人体中最勤劳的器官,\n'
|
||||||
const SizedBox(height: DesignTokens.space3),
|
'从胚胎4周开始跳动,直到生命终结。',
|
||||||
MetricCard(
|
),
|
||||||
isDark: isDark,
|
const SizedBox(height: DesignTokens.space3),
|
||||||
emoji: '🫁',
|
MetricCard(
|
||||||
title: '一生呼吸总次数',
|
isDark: isDark,
|
||||||
value: calc.formatNumber(calc.lifetimeBreaths),
|
emoji: '🫁',
|
||||||
unit: '次(基于年龄)',
|
title: '一生呼吸总次数',
|
||||||
color: DesignTokens.blue,
|
value: calc.formatNumber(calc.lifetimeBreaths),
|
||||||
infoTitle: '一生呼吸总次数',
|
unit: '次(基于年龄)',
|
||||||
infoContent:
|
color: DesignTokens.blue,
|
||||||
'基于你的年龄和平均呼吸频率估算。\n\n'
|
infoTitle: '一生呼吸总次数',
|
||||||
'成人每分钟约呼吸16次,\n'
|
infoContent:
|
||||||
'每小时960次,每天约23,040次。\n'
|
'基于你的年龄和平均呼吸频率估算。\n\n'
|
||||||
'如果活到80岁,大约呼吸6-7亿次。',
|
'成人每分钟约呼吸16次,\n'
|
||||||
),
|
'每小时960次,每天约23,040次。\n'
|
||||||
const SizedBox(height: DesignTokens.space3),
|
'如果活到80岁,大约呼吸6-7亿次。',
|
||||||
MetricCard(
|
),
|
||||||
isDark: isDark,
|
const SizedBox(height: DesignTokens.space3),
|
||||||
emoji: '👁️',
|
MetricCard(
|
||||||
title: '一生眨眼总次数',
|
isDark: isDark,
|
||||||
value: calc.formatNumber(calc.lifetimeBlinks),
|
emoji: '👁️',
|
||||||
unit: '次(基于年龄)',
|
title: '一生眨眼总次数',
|
||||||
color: DesignTokens.teal,
|
value: calc.formatNumber(calc.lifetimeBlinks),
|
||||||
infoTitle: '一生眨眼总次数',
|
unit: '次(基于年龄)',
|
||||||
infoContent:
|
color: DesignTokens.teal,
|
||||||
'基于你的年龄和平均眨眼频率估算。\n\n'
|
infoTitle: '一生眨眼总次数',
|
||||||
'每天约14,400次眨眼,\n'
|
infoContent:
|
||||||
'一生约4亿次眨眼。\n\n'
|
'基于你的年龄和平均眨眼频率估算。\n\n'
|
||||||
'有趣的是,每次眨眼约0.1-0.4秒,\n'
|
'每天约14,400次眨眼,\n'
|
||||||
'一生中约有5天的时间在眨眼中度过。',
|
'一生约4亿次眨眼。\n\n'
|
||||||
),
|
'有趣的是,每次眨眼约0.1-0.4秒,\n'
|
||||||
const SizedBox(height: DesignTokens.space3),
|
'一生中约有5天的时间在眨眼中度过。',
|
||||||
MetricCard(
|
),
|
||||||
isDark: isDark,
|
const SizedBox(height: DesignTokens.space3),
|
||||||
emoji: '🧴',
|
MetricCard(
|
||||||
title: '一生皮肤细胞脱落量',
|
isDark: isDark,
|
||||||
value: calc.formatNumber(calc.lifetimeSkinCells),
|
emoji: '🧴',
|
||||||
unit: '个(基于年龄)',
|
title: '一生皮肤细胞脱落量',
|
||||||
color: DesignTokens.purple,
|
value: calc.formatNumber(calc.lifetimeSkinCells),
|
||||||
infoTitle: '皮肤细胞脱落',
|
unit: '个(基于年龄)',
|
||||||
infoContent:
|
color: DesignTokens.purple,
|
||||||
'人体每天约脱落3-5千万个皮肤细胞,\n'
|
infoTitle: '皮肤细胞脱落',
|
||||||
'取中间值约4千万个/天。\n\n'
|
infoContent:
|
||||||
'皮肤是人体最大的器官,\n'
|
'人体每天约脱落3-5千万个皮肤细胞,\n'
|
||||||
'每27天左右皮肤就会完全更新一次。\n'
|
'取中间值约4千万个/天。\n\n'
|
||||||
'一生脱落的皮肤细胞总重量约18-23公斤,\n'
|
'皮肤是人体最大的器官,\n'
|
||||||
'相当于一个小孩的体重!',
|
'每27天左右皮肤就会完全更新一次。\n'
|
||||||
),
|
'一生脱落的皮肤细胞总重量约18-23公斤,\n'
|
||||||
const SizedBox(height: DesignTokens.space3),
|
'相当于一个小孩的体重!',
|
||||||
MetricCard(
|
),
|
||||||
isDark: isDark,
|
const SizedBox(height: DesignTokens.space3),
|
||||||
emoji: '🍽️',
|
MetricCard(
|
||||||
title: '一生消耗食物总重量',
|
isDark: isDark,
|
||||||
value: calc.formatNumber(calc.lifetimeFoodWeight),
|
emoji: '🍽️',
|
||||||
unit: '吨(基于年龄)',
|
title: '一生消耗食物总重量',
|
||||||
color: DesignTokens.orange,
|
value: calc.formatNumber(calc.lifetimeFoodWeight),
|
||||||
infoTitle: '一生食物消耗',
|
unit: '吨(基于年龄)',
|
||||||
infoContent:
|
color: DesignTokens.orange,
|
||||||
'基于你的TDEE和年龄估算。\n\n'
|
infoTitle: '一生食物消耗',
|
||||||
'假设食物平均热量密度约2 kcal/g,\n'
|
infoContent:
|
||||||
'一个成年人一生大约吃掉25-30吨食物,\n'
|
'基于你的TDEE和年龄估算。\n\n'
|
||||||
'相当于约5头大象的重量!\n\n'
|
'假设食物平均热量密度约2 kcal/g,\n'
|
||||||
'其中约60%为碳水化合物和脂肪,\n'
|
'一个成年人一生大约吃掉25-30吨食物,\n'
|
||||||
'15%为蛋白质,25%为水分和其他。',
|
'相当于约5头大象的重量!\n\n'
|
||||||
),
|
'其中约60%为碳水化合物和脂肪,\n'
|
||||||
const SizedBox(height: DesignTokens.space3),
|
'15%为蛋白质,25%为水分和其他。',
|
||||||
MetricCard(
|
),
|
||||||
isDark: isDark,
|
const SizedBox(height: DesignTokens.space3),
|
||||||
emoji: '🚶',
|
MetricCard(
|
||||||
title: '一生行走距离估算',
|
isDark: isDark,
|
||||||
value: calc.formatDistance(calc.lifetimeWalkingDistance * 100),
|
emoji: '🚶',
|
||||||
unit: '',
|
title: '一生行走距离估算',
|
||||||
color: DesignTokens.green,
|
value: calc.formatDistance(calc.lifetimeWalkingDistance * 100),
|
||||||
infoTitle: '一生行走距离',
|
unit: '',
|
||||||
infoContent:
|
color: DesignTokens.green,
|
||||||
'基于你的年龄、身高和活动水平估算。\n\n'
|
infoTitle: '一生行走距离',
|
||||||
'步幅 ≈ 身高 × 0.415\n'
|
infoContent:
|
||||||
'每日步数根据年龄调整:\n'
|
'基于你的年龄、身高和活动水平估算。\n\n'
|
||||||
'<10岁 12000步 | 10-30岁 8000步\n'
|
'步幅 ≈ 身高 × 0.415\n'
|
||||||
'30-50岁 7000步 | 50-70岁 5000步\n'
|
'每日步数根据年龄调整:\n'
|
||||||
'70+岁 3000步\n\n'
|
'<10岁 12000步 | 10-30岁 8000步\n'
|
||||||
'一个成年人一生大约走11万公里,\n'
|
'30-50岁 7000步 | 50-70岁 5000步\n'
|
||||||
'相当于绕地球2.75圈!',
|
'70+岁 3000步\n\n'
|
||||||
),
|
'一个成年人一生大约走11万公里,\n'
|
||||||
],
|
'相当于绕地球2.75圈!',
|
||||||
|
),
|
||||||
|
const SizedBox(height: DesignTokens.space3),
|
||||||
|
PlanetWeightCard(isDark: isDark, calc: calc),
|
||||||
|
const SizedBox(height: DesignTokens.space5),
|
||||||
|
_buildSwipeHint(isDark),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSwipeHint(bool isDark) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: DesignTokens.space4),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
CupertinoIcons.chevron_left,
|
||||||
|
size: 14,
|
||||||
|
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
|
||||||
|
),
|
||||||
|
const SizedBox(width: DesignTokens.space2),
|
||||||
|
Text(
|
||||||
|
'👈 左右滑动切换 核心指标 / 科学指数 / 趣味统计 👉',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: DesignTokens.fontXs,
|
||||||
|
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: DesignTokens.space2),
|
||||||
|
Icon(
|
||||||
|
CupertinoIcons.chevron_right,
|
||||||
|
size: 14,
|
||||||
|
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* 名称: 身体分析科学指数Tab
|
* 名称: 身体分析科学指数Tab
|
||||||
* 作用: 展示骨骼/血液/去脂体重/理想体重/代谢年龄/体表面积等科学指标
|
* 作用: 展示骨骼/血液/去脂体重/理想体重/代谢年龄/体表面积等科学指标
|
||||||
* 创建: 2026-04-24
|
* 创建: 2026-04-24
|
||||||
* 更新: 2026-04-24 从 body_analysis_page.dart 拆分
|
* 更新: 2026-04-24 修复溢出+增加滑动提示
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
@@ -23,232 +23,263 @@ class ScienceMetricsTab extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return SingleChildScrollView(
|
||||||
children: [
|
physics: const ClampingScrollPhysics(),
|
||||||
MetricCard(
|
child: Column(
|
||||||
isDark: isDark,
|
children: [
|
||||||
emoji: '🦴',
|
|
||||||
title: '骨骼重量估算',
|
|
||||||
value: calc.boneWeight.toStringAsFixed(1),
|
|
||||||
unit: 'kg(约体重15%)',
|
|
||||||
color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
|
|
||||||
infoTitle: '骨骼重量',
|
|
||||||
infoContent:
|
|
||||||
'人体骨骼约占体重的15%。骨骼是活的组织,不断进行新陈代谢。\n\n'
|
|
||||||
'骨骼中有206块骨头,最大的骨头是股骨,最小的是耳中的镫骨。',
|
|
||||||
),
|
|
||||||
const SizedBox(height: DesignTokens.space3),
|
|
||||||
MetricCard(
|
|
||||||
isDark: isDark,
|
|
||||||
emoji: '🩸',
|
|
||||||
title: '血液总量估算',
|
|
||||||
value: calc.bloodVolume.toStringAsFixed(0),
|
|
||||||
unit: 'ml',
|
|
||||||
color: DesignTokens.red,
|
|
||||||
infoTitle: '血液总量',
|
|
||||||
infoContent:
|
|
||||||
'男性约75ml/kg,女性约65ml/kg。\n\n'
|
|
||||||
'血液由血浆(55%)和血细胞(45%)组成。\n'
|
|
||||||
'红细胞寿命约120天,人体每秒产生约200万个新红细胞。',
|
|
||||||
),
|
|
||||||
const SizedBox(height: DesignTokens.space3),
|
|
||||||
MetricCard(
|
|
||||||
isDark: isDark,
|
|
||||||
emoji: '💪',
|
|
||||||
title: '去脂体重 (LBM)',
|
|
||||||
value: calc.leanBodyMass.toStringAsFixed(1),
|
|
||||||
unit: 'kg',
|
|
||||||
color: DesignTokens.blue,
|
|
||||||
infoTitle: '去脂体重 (Lean Body Mass)',
|
|
||||||
infoContent:
|
|
||||||
'去脂体重 = 体重 × (1 - 体脂率%)\n\n'
|
|
||||||
'LBM包括肌肉、骨骼、器官、水分等非脂肪组织的重量。\n'
|
|
||||||
'是评估身体成分和计算药物剂量的重要指标。',
|
|
||||||
),
|
|
||||||
const SizedBox(height: DesignTokens.space3),
|
|
||||||
IdealWeightCard(isDark: isDark, calc: calc),
|
|
||||||
const SizedBox(height: DesignTokens.space3),
|
|
||||||
MetricCard(
|
|
||||||
isDark: isDark,
|
|
||||||
emoji: '🧠',
|
|
||||||
title: '代谢年龄',
|
|
||||||
value: '${calc.metabolicAge}',
|
|
||||||
unit: '岁(实际${calc.age.toStringAsFixed(0)}岁)',
|
|
||||||
color: calc.metabolicAge <= calc.age
|
|
||||||
? DesignTokens.green
|
|
||||||
: DesignTokens.orange,
|
|
||||||
infoTitle: '代谢年龄',
|
|
||||||
infoContent:
|
|
||||||
'代谢年龄基于你的BMR与同龄人平均BMR的对比来估算。\n\n'
|
|
||||||
'代谢年龄 < 实际年龄:代谢更活跃,身体状态较好\n'
|
|
||||||
'代谢年龄 > 实际年龄:代谢较慢,建议增加运动量',
|
|
||||||
),
|
|
||||||
const SizedBox(height: DesignTokens.space3),
|
|
||||||
if (calc.waistToHeightRatio >= 0) ...[
|
|
||||||
MetricCard(
|
MetricCard(
|
||||||
isDark: isDark,
|
isDark: isDark,
|
||||||
emoji: '📏',
|
emoji: '🦴',
|
||||||
title: '腰围身高比',
|
title: '骨骼重量估算',
|
||||||
value: calc.waistToHeightRatio.toStringAsFixed(2),
|
value: calc.boneWeight.toStringAsFixed(1),
|
||||||
unit: calc.waistToHeightRatio < 0.5 ? '✅ 健康' : '⚠️ 偏高',
|
unit: 'kg(约体重15%)',
|
||||||
color: calc.waistToHeightRatio < 0.5
|
color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
|
||||||
? DesignTokens.green
|
infoTitle: '骨骼重量',
|
||||||
: DesignTokens.orange,
|
|
||||||
infoTitle: '腰围身高比 (WHtR)',
|
|
||||||
infoContent:
|
infoContent:
|
||||||
'腰围身高比 = 腰围 / 身高\n\n'
|
'人体骨骼约占体重的15%。骨骼是活的组织,不断进行新陈代谢。\n\n'
|
||||||
'该指标比BMI更能反映内脏脂肪堆积情况。\n'
|
'骨骼中有206块骨头,最大的骨头是股骨,最小的是耳中的镫骨。',
|
||||||
'健康标准:< 0.5\n'
|
),
|
||||||
'0.5-0.6:风险增加\n'
|
const SizedBox(height: DesignTokens.space3),
|
||||||
'> 0.6:高风险',
|
MetricCard(
|
||||||
|
isDark: isDark,
|
||||||
|
emoji: '🩸',
|
||||||
|
title: '血液总量估算',
|
||||||
|
value: calc.bloodVolume.toStringAsFixed(0),
|
||||||
|
unit: 'ml',
|
||||||
|
color: DesignTokens.red,
|
||||||
|
infoTitle: '血液总量',
|
||||||
|
infoContent:
|
||||||
|
'男性约75ml/kg,女性约65ml/kg。\n\n'
|
||||||
|
'血液由血浆(55%)和血细胞(45%)组成。\n'
|
||||||
|
'红细胞寿命约120天,人体每秒产生约200万个新红细胞。',
|
||||||
|
),
|
||||||
|
const SizedBox(height: DesignTokens.space3),
|
||||||
|
MetricCard(
|
||||||
|
isDark: isDark,
|
||||||
|
emoji: '💪',
|
||||||
|
title: '去脂体重 (LBM)',
|
||||||
|
value: calc.leanBodyMass.toStringAsFixed(1),
|
||||||
|
unit: 'kg',
|
||||||
|
color: DesignTokens.blue,
|
||||||
|
infoTitle: '去脂体重 (Lean Body Mass)',
|
||||||
|
infoContent:
|
||||||
|
'去脂体重 = 体重 × (1 - 体脂率%)\n\n'
|
||||||
|
'LBM包括肌肉、骨骼、器官、水分等非脂肪组织的重量。\n'
|
||||||
|
'是评估身体成分和计算药物剂量的重要指标。',
|
||||||
|
),
|
||||||
|
const SizedBox(height: DesignTokens.space3),
|
||||||
|
IdealWeightCard(isDark: isDark, calc: calc),
|
||||||
|
const SizedBox(height: DesignTokens.space3),
|
||||||
|
MetricCard(
|
||||||
|
isDark: isDark,
|
||||||
|
emoji: '🧠',
|
||||||
|
title: '代谢年龄',
|
||||||
|
value: '${calc.metabolicAge}',
|
||||||
|
unit: '岁(实际${calc.age.toStringAsFixed(0)}岁)',
|
||||||
|
color: calc.metabolicAge <= calc.age
|
||||||
|
? DesignTokens.green
|
||||||
|
: DesignTokens.orange,
|
||||||
|
infoTitle: '代谢年龄',
|
||||||
|
infoContent:
|
||||||
|
'代谢年龄基于你的BMR与同龄人平均BMR的对比来估算。\n\n'
|
||||||
|
'代谢年龄 < 实际年龄:代谢更活跃,身体状态较好\n'
|
||||||
|
'代谢年龄 > 实际年龄:代谢较慢,建议增加运动量',
|
||||||
|
),
|
||||||
|
const SizedBox(height: DesignTokens.space3),
|
||||||
|
if (calc.waistToHeightRatio >= 0) ...[
|
||||||
|
MetricCard(
|
||||||
|
isDark: isDark,
|
||||||
|
emoji: '📏',
|
||||||
|
title: '腰围身高比',
|
||||||
|
value: calc.waistToHeightRatio.toStringAsFixed(2),
|
||||||
|
unit: calc.waistToHeightRatio < 0.5 ? '✅ 健康' : '⚠️ 偏高',
|
||||||
|
color: calc.waistToHeightRatio < 0.5
|
||||||
|
? DesignTokens.green
|
||||||
|
: DesignTokens.orange,
|
||||||
|
infoTitle: '腰围身高比 (WHtR)',
|
||||||
|
infoContent:
|
||||||
|
'腰围身高比 = 腰围 / 身高\n\n'
|
||||||
|
'该指标比BMI更能反映内脏脂肪堆积情况。\n'
|
||||||
|
'健康标准:< 0.5\n'
|
||||||
|
'0.5-0.6:风险增加\n'
|
||||||
|
'> 0.6:高风险',
|
||||||
|
),
|
||||||
|
const SizedBox(height: DesignTokens.space3),
|
||||||
|
],
|
||||||
|
if (calc.waistToHipRatio >= 0) ...[
|
||||||
|
MetricCard(
|
||||||
|
isDark: isDark,
|
||||||
|
emoji: '📐',
|
||||||
|
title: '腰臀比 (WHR)',
|
||||||
|
value: calc.waistToHipRatio.toStringAsFixed(2),
|
||||||
|
unit: calc.whrCategory,
|
||||||
|
color: Color(calc.whrColorValue),
|
||||||
|
infoTitle: '腰臀比 (Waist-to-Hip Ratio)',
|
||||||
|
infoContent:
|
||||||
|
'腰臀比 = 腰围 / 臀围\n\n'
|
||||||
|
'男性:< 0.9 健康 | 0.9-1.0 风险 | > 1.0 高风险\n'
|
||||||
|
'女性:< 0.8 健康 | 0.8-0.85 风险 | > 0.85 高风险\n\n'
|
||||||
|
'腰臀比反映脂肪分布,苹果型身材(腹部脂肪多)健康风险更高。',
|
||||||
|
),
|
||||||
|
const SizedBox(height: DesignTokens.space3),
|
||||||
|
],
|
||||||
|
if (calc.frameSize.isNotEmpty) ...[
|
||||||
|
MetricCard(
|
||||||
|
isDark: isDark,
|
||||||
|
emoji: '🏷️',
|
||||||
|
title: '骨架类型',
|
||||||
|
value: calc.frameSize,
|
||||||
|
unit: '身高/手腕围 = ${calc.frameRatio.toStringAsFixed(1)}',
|
||||||
|
color: DesignTokens.purple,
|
||||||
|
infoTitle: '骨架类型',
|
||||||
|
infoContent:
|
||||||
|
'基于身高与手腕围的比值判定骨架大小。\n\n'
|
||||||
|
'男性:> 10.4 小骨架 | 9.6-10.4 中等 | < 9.6 大骨架\n'
|
||||||
|
'女性:> 11.0 小骨架 | 10.1-11.0 中等 | < 10.1 大骨架\n\n'
|
||||||
|
'骨架大小会影响理想体重的判定标准。',
|
||||||
|
),
|
||||||
|
const SizedBox(height: DesignTokens.space3),
|
||||||
|
],
|
||||||
|
MetricCard(
|
||||||
|
isDark: isDark,
|
||||||
|
emoji: '🫁',
|
||||||
|
title: '人体表面积',
|
||||||
|
value: calc.bodySurfaceArea.toStringAsFixed(2),
|
||||||
|
unit: 'm²',
|
||||||
|
color: DesignTokens.teal,
|
||||||
|
infoTitle: '人体表面积 (BSA)',
|
||||||
|
infoContent:
|
||||||
|
'采用Mosteller公式计算:\n'
|
||||||
|
'BSA = √(身高cm × 体重kg / 3600)\n\n'
|
||||||
|
'人体表面积在医学中用于计算药物剂量、'
|
||||||
|
'评估烧伤面积和心输出量指数等。',
|
||||||
|
),
|
||||||
|
const SizedBox(height: DesignTokens.space3),
|
||||||
|
MetricCard(
|
||||||
|
isDark: isDark,
|
||||||
|
emoji: '💤',
|
||||||
|
title: '推荐睡眠时长',
|
||||||
|
value: calc.recommendedSleep.toStringAsFixed(
|
||||||
|
calc.recommendedSleep % 1 == 0 ? 0 : 1,
|
||||||
|
),
|
||||||
|
unit: '小时/天',
|
||||||
|
color: DesignTokens.purple,
|
||||||
|
infoTitle: '推荐睡眠时长',
|
||||||
|
infoContent:
|
||||||
|
'根据美国国家睡眠基金会建议:\n\n'
|
||||||
|
'0-1岁: 14-16h | 1-2岁: 13h\n'
|
||||||
|
'3-5岁: 12h | 6-12岁: 10h\n'
|
||||||
|
'13-17岁: 9h | 18-25岁: 8h\n'
|
||||||
|
'26-64岁: 7-8h | 65+: 7-8h\n\n'
|
||||||
|
'充足睡眠对代谢、免疫和认知功能至关重要。',
|
||||||
|
),
|
||||||
|
const SizedBox(height: DesignTokens.space3),
|
||||||
|
MetricCard(
|
||||||
|
isDark: isDark,
|
||||||
|
emoji: '🏋️',
|
||||||
|
title: '无脂肪质量指数 (FFMI)',
|
||||||
|
value: calc.ffmi.toStringAsFixed(1),
|
||||||
|
unit: calc.ffmiCategory,
|
||||||
|
color: Color(calc.ffmiColorValue),
|
||||||
|
infoTitle: '无脂肪质量指数 (FFMI)',
|
||||||
|
infoContent:
|
||||||
|
'FFMI = 去脂体重(kg) / 身高(m)²\n\n'
|
||||||
|
'标准化FFMI考虑了身高差异,更公平。\n\n'
|
||||||
|
'男性标准:\n'
|
||||||
|
'<18 低于平均 | 18-20 平均 | 20-22 高于平均\n'
|
||||||
|
'22-25 优秀 | 25-28 卓越 | >28 疑似增强剂\n\n'
|
||||||
|
'女性标准:\n'
|
||||||
|
'<15 低于平均 | 15-17 平均 | 17-19 高于平均\n'
|
||||||
|
'19-21 优秀 | 21-24 卓越 | >24 疑似增强剂\n\n'
|
||||||
|
'FFMI比BMI更适合评估健身人群的肌肉发育水平。',
|
||||||
),
|
),
|
||||||
const SizedBox(height: DesignTokens.space3),
|
const SizedBox(height: DesignTokens.space3),
|
||||||
],
|
|
||||||
if (calc.waistToHipRatio >= 0) ...[
|
|
||||||
MetricCard(
|
MetricCard(
|
||||||
isDark: isDark,
|
isDark: isDark,
|
||||||
emoji: '📐',
|
emoji: '📐',
|
||||||
title: '腰臀比 (WHR)',
|
title: 'Ponderal 体重指数',
|
||||||
value: calc.waistToHipRatio.toStringAsFixed(2),
|
value: calc.ponderalIndex.toStringAsFixed(1),
|
||||||
unit: calc.whrCategory,
|
unit: 'kg/m³(${calc.ponderalCategory})',
|
||||||
color: Color(calc.whrColorValue),
|
color: DesignTokens.blue,
|
||||||
infoTitle: '腰臀比 (Waist-to-Hip Ratio)',
|
infoTitle: 'Ponderal Index (Rohrer指数)',
|
||||||
infoContent:
|
infoContent:
|
||||||
'腰臀比 = 腰围 / 臀围\n\n'
|
'PI = 体重(kg) / 身高(m)³\n\n'
|
||||||
'男性:< 0.9 健康 | 0.9-1.0 风险 | > 1.0 高风险\n'
|
'与BMI不同,Ponderal指数使用身高的三次方,\n'
|
||||||
'女性:< 0.8 健康 | 0.8-0.85 风险 | > 0.85 高风险\n\n'
|
'对极端身高(特别高或特别矮)的人群更准确。\n\n'
|
||||||
'腰臀比反映脂肪分布,苹果型身材(腹部脂肪多)健康风险更高。',
|
'男性:11-13 正常\n'
|
||||||
|
'女性:10-12.5 正常\n\n'
|
||||||
|
'PI < 低值 = 偏瘦 | PI > 高值 = 偏胖',
|
||||||
),
|
),
|
||||||
const SizedBox(height: DesignTokens.space3),
|
const SizedBox(height: DesignTokens.space3),
|
||||||
],
|
|
||||||
if (calc.frameSize.isNotEmpty) ...[
|
|
||||||
MetricCard(
|
MetricCard(
|
||||||
isDark: isDark,
|
isDark: isDark,
|
||||||
emoji: '🏷️',
|
emoji: '⚡',
|
||||||
title: '骨架类型',
|
title: '基础代谢效率',
|
||||||
value: calc.frameSize,
|
value: calc.bmrPerKg.toStringAsFixed(1),
|
||||||
unit: '身高/手腕围 = ${calc.frameRatio.toStringAsFixed(1)}',
|
unit: 'kcal/kg/天(${calc.bmrEfficiency})',
|
||||||
color: DesignTokens.purple,
|
color: DesignTokens.orange,
|
||||||
infoTitle: '骨架类型',
|
infoTitle: '基础代谢效率 (BMR/kg)',
|
||||||
infoContent:
|
infoContent:
|
||||||
'基于身高与手腕围的比值判定骨架大小。\n\n'
|
'BMR/kg = 基础代谢率 / 体重\n\n'
|
||||||
'男性:> 10.4 小骨架 | 9.6-10.4 中等 | < 9.6 大骨架\n'
|
'反映每公斤体重的基础代谢水平:\n'
|
||||||
'女性:> 11.0 小骨架 | 10.1-11.0 中等 | < 10.1 大骨架\n\n'
|
'< 20 较低 | 20-25 正常\n'
|
||||||
'骨架大小会影响理想体重的判定标准。',
|
'25-30 较高 | > 30 很高\n\n'
|
||||||
|
'代谢效率高意味着身体在静息状态下消耗更多能量,'
|
||||||
|
'通常与较好的身体成分和代谢健康相关。',
|
||||||
),
|
),
|
||||||
const SizedBox(height: DesignTokens.space3),
|
const SizedBox(height: DesignTokens.space3),
|
||||||
|
if (calc.conicityIndex >= 0) ...[
|
||||||
|
MetricCard(
|
||||||
|
isDark: isDark,
|
||||||
|
emoji: '🔔',
|
||||||
|
title: '锥度指数 (Conicity Index)',
|
||||||
|
value: calc.conicityIndex.toStringAsFixed(2),
|
||||||
|
unit: calc.conicityCategory,
|
||||||
|
color:
|
||||||
|
calc.conicityIndex < (calc.gender == Gender.male ? 1.25 : 1.18)
|
||||||
|
? DesignTokens.green
|
||||||
|
: DesignTokens.orange,
|
||||||
|
infoTitle: '锥度指数 (Conicity Index)',
|
||||||
|
infoContent:
|
||||||
|
'CI = 腰围 / (0.109 × √(体重/身高))\n\n'
|
||||||
|
'锥度指数评估腹部脂肪堆积程度,\n'
|
||||||
|
'将身体形状从"圆柱形"到"圆锥形"量化。\n\n'
|
||||||
|
'男性:< 1.25 低风险 | 1.25-1.35 中等 | > 1.35 高风险\n'
|
||||||
|
'女性:< 1.18 低风险 | 1.18-1.28 中等 | > 1.28 高风险\n\n'
|
||||||
|
'需要腰围数据。',
|
||||||
|
),
|
||||||
|
const SizedBox(height: DesignTokens.space3),
|
||||||
|
],
|
||||||
|
const SizedBox(height: DesignTokens.space5),
|
||||||
|
_buildSwipeHint(isDark),
|
||||||
],
|
],
|
||||||
MetricCard(
|
),
|
||||||
isDark: isDark,
|
);
|
||||||
emoji: '🫁',
|
}
|
||||||
title: '人体表面积',
|
|
||||||
value: calc.bodySurfaceArea.toStringAsFixed(2),
|
Widget _buildSwipeHint(bool isDark) {
|
||||||
unit: 'm²',
|
return Padding(
|
||||||
color: DesignTokens.teal,
|
padding: const EdgeInsets.symmetric(vertical: DesignTokens.space4),
|
||||||
infoTitle: '人体表面积 (BSA)',
|
child: Row(
|
||||||
infoContent:
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
'采用Mosteller公式计算:\n'
|
children: [
|
||||||
'BSA = √(身高cm × 体重kg / 3600)\n\n'
|
Icon(CupertinoIcons.chevron_left,
|
||||||
'人体表面积在医学中用于计算药物剂量、'
|
size: 14,
|
||||||
'评估烧伤面积和心输出量指数等。',
|
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3),
|
||||||
),
|
const SizedBox(width: DesignTokens.space2),
|
||||||
const SizedBox(height: DesignTokens.space3),
|
Text(
|
||||||
MetricCard(
|
'👈 左右滑动切换 核心指标 / 科学指数 / 趣味统计 👉',
|
||||||
isDark: isDark,
|
style: TextStyle(
|
||||||
emoji: '💤',
|
fontSize: DesignTokens.fontXs,
|
||||||
title: '推荐睡眠时长',
|
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
|
||||||
value: calc.recommendedSleep.toStringAsFixed(
|
),
|
||||||
calc.recommendedSleep % 1 == 0 ? 0 : 1,
|
|
||||||
),
|
),
|
||||||
unit: '小时/天',
|
const SizedBox(width: DesignTokens.space2),
|
||||||
color: DesignTokens.purple,
|
Icon(CupertinoIcons.chevron_right,
|
||||||
infoTitle: '推荐睡眠时长',
|
size: 14,
|
||||||
infoContent:
|
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3),
|
||||||
'根据美国国家睡眠基金会建议:\n\n'
|
|
||||||
'0-1岁: 14-16h | 1-2岁: 13h\n'
|
|
||||||
'3-5岁: 12h | 6-12岁: 10h\n'
|
|
||||||
'13-17岁: 9h | 18-25岁: 8h\n'
|
|
||||||
'26-64岁: 7-8h | 65+: 7-8h\n\n'
|
|
||||||
'充足睡眠对代谢、免疫和认知功能至关重要。',
|
|
||||||
),
|
|
||||||
const SizedBox(height: DesignTokens.space3),
|
|
||||||
MetricCard(
|
|
||||||
isDark: isDark,
|
|
||||||
emoji: '🏋️',
|
|
||||||
title: '无脂肪质量指数 (FFMI)',
|
|
||||||
value: calc.ffmi.toStringAsFixed(1),
|
|
||||||
unit: calc.ffmiCategory,
|
|
||||||
color: Color(calc.ffmiColorValue),
|
|
||||||
infoTitle: '无脂肪质量指数 (FFMI)',
|
|
||||||
infoContent:
|
|
||||||
'FFMI = 去脂体重(kg) / 身高(m)²\n\n'
|
|
||||||
'标准化FFMI考虑了身高差异,更公平。\n\n'
|
|
||||||
'男性标准:\n'
|
|
||||||
'<18 低于平均 | 18-20 平均 | 20-22 高于平均\n'
|
|
||||||
'22-25 优秀 | 25-28 卓越 | >28 疑似增强剂\n\n'
|
|
||||||
'女性标准:\n'
|
|
||||||
'<15 低于平均 | 15-17 平均 | 17-19 高于平均\n'
|
|
||||||
'19-21 优秀 | 21-24 卓越 | >24 疑似增强剂\n\n'
|
|
||||||
'FFMI比BMI更适合评估健身人群的肌肉发育水平。',
|
|
||||||
),
|
|
||||||
const SizedBox(height: DesignTokens.space3),
|
|
||||||
MetricCard(
|
|
||||||
isDark: isDark,
|
|
||||||
emoji: '📐',
|
|
||||||
title: 'Ponderal 体重指数',
|
|
||||||
value: calc.ponderalIndex.toStringAsFixed(1),
|
|
||||||
unit: 'kg/m³(${calc.ponderalCategory})',
|
|
||||||
color: DesignTokens.blue,
|
|
||||||
infoTitle: 'Ponderal Index (Rohrer指数)',
|
|
||||||
infoContent:
|
|
||||||
'PI = 体重(kg) / 身高(m)³\n\n'
|
|
||||||
'与BMI不同,Ponderal指数使用身高的三次方,\n'
|
|
||||||
'对极端身高(特别高或特别矮)的人群更准确。\n\n'
|
|
||||||
'男性:11-13 正常\n'
|
|
||||||
'女性:10-12.5 正常\n\n'
|
|
||||||
'PI < 低值 = 偏瘦 | PI > 高值 = 偏胖',
|
|
||||||
),
|
|
||||||
const SizedBox(height: DesignTokens.space3),
|
|
||||||
MetricCard(
|
|
||||||
isDark: isDark,
|
|
||||||
emoji: '⚡',
|
|
||||||
title: '基础代谢效率',
|
|
||||||
value: calc.bmrPerKg.toStringAsFixed(1),
|
|
||||||
unit: 'kcal/kg/天(${calc.bmrEfficiency})',
|
|
||||||
color: DesignTokens.orange,
|
|
||||||
infoTitle: '基础代谢效率 (BMR/kg)',
|
|
||||||
infoContent:
|
|
||||||
'BMR/kg = 基础代谢率 / 体重\n\n'
|
|
||||||
'反映每公斤体重的基础代谢水平:\n'
|
|
||||||
'< 20 较低 | 20-25 正常\n'
|
|
||||||
'25-30 较高 | > 30 很高\n\n'
|
|
||||||
'代谢效率高意味着身体在静息状态下消耗更多能量,'
|
|
||||||
'通常与较好的身体成分和代谢健康相关。',
|
|
||||||
),
|
|
||||||
const SizedBox(height: DesignTokens.space3),
|
|
||||||
if (calc.conicityIndex >= 0) ...[
|
|
||||||
MetricCard(
|
|
||||||
isDark: isDark,
|
|
||||||
emoji: '🔔',
|
|
||||||
title: '锥度指数 (Conicity Index)',
|
|
||||||
value: calc.conicityIndex.toStringAsFixed(2),
|
|
||||||
unit: calc.conicityCategory,
|
|
||||||
color:
|
|
||||||
calc.conicityIndex < (calc.gender == Gender.male ? 1.25 : 1.18)
|
|
||||||
? DesignTokens.green
|
|
||||||
: DesignTokens.orange,
|
|
||||||
infoTitle: '锥度指数 (Conicity Index)',
|
|
||||||
infoContent:
|
|
||||||
'CI = 腰围 / (0.109 × √(体重/身高))\n\n'
|
|
||||||
'锥度指数评估腹部脂肪堆积程度,\n'
|
|
||||||
'将身体形状从"圆柱形"到"圆锥形"量化。\n\n'
|
|
||||||
'男性:< 1.25 低风险 | 1.25-1.35 中等 | > 1.35 高风险\n'
|
|
||||||
'女性:< 1.18 低风险 | 1.18-1.28 中等 | > 1.28 高风险\n\n'
|
|
||||||
'需要腰围数据。',
|
|
||||||
),
|
|
||||||
const SizedBox(height: DesignTokens.space3),
|
|
||||||
],
|
],
|
||||||
],
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user