From 7d5d95d5e08afd68b08f6c1ff446c33ffa810bb1 Mon Sep 17 00:00:00 2001 From: Developer Date: Fri, 24 Apr 2026 05:05:26 +0800 Subject: [PATCH] release 24 --- CHANGELOG.md | 67 ++ lib/src/models/app/tool_item_model.dart | 8 +- .../body_analysis_calculator.dart | 24 + .../body_analysis/body_analysis_cards.dart | 289 ++++++- .../body_analysis/body_analysis_page.dart | 703 ++++++++++++------ .../tools/health/body_analysis/core_tab.dart | 156 ++-- .../tools/health/body_analysis/fun_tab.dart | 555 +++++++------- .../health/body_analysis/science_tab.dart | 447 +++++------ 8 files changed, 1485 insertions(+), 764 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16ac06d..3633965 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,73 @@ 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 ### 🐛 修复 — 排敏助手页面一直转圈 + 闪退 + Obx异常 diff --git a/lib/src/models/app/tool_item_model.dart b/lib/src/models/app/tool_item_model.dart index 651123d..85d9411 100644 --- a/lib/src/models/app/tool_item_model.dart +++ b/lib/src/models/app/tool_item_model.dart @@ -375,7 +375,7 @@ class ToolRegistry { ), ToolItem( id: 'anti_allergy', - name: 'Anti 敏宝排敏助手', + name: '敏宝排敏助手', icon: '🛡️', needsNetwork: false, category: 'health', @@ -395,11 +395,7 @@ class ToolRegistry { category: 'health', route: '/tools/body-analysis', description: '基于身高体重年龄分析25+项科学指标和趣味统计', - waterfallSlot: WaterfallSlotConfig( - show: true, - priority: 1, - badge: 'NEW', - ), + waterfallSlot: WaterfallSlotConfig(show: true, priority: 1, badge: 'NEW'), ), ]; } diff --git a/lib/src/pages/tools/health/body_analysis/body_analysis_calculator.dart b/lib/src/pages/tools/health/body_analysis/body_analysis_calculator.dart index 229bf95..77536d1 100644 --- a/lib/src/pages/tools/health/body_analysis/body_analysis_calculator.dart +++ b/lib/src/pages/tools/health/body_analysis/body_analysis_calculator.dart @@ -36,6 +36,15 @@ class WaterSchedule { 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 { final Gender gender; final double age; @@ -423,6 +432,21 @@ class BodyAnalysisCalculator { return stepsPerDay * strideLength * 365.25 * age; } + List 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 muscleGainCalories => tdee + 300; diff --git a/lib/src/pages/tools/health/body_analysis/body_analysis_cards.dart b/lib/src/pages/tools/health/body_analysis/body_analysis_cards.dart index a63b55a..63b6192 100644 --- a/lib/src/pages/tools/health/body_analysis/body_analysis_cards.dart +++ b/lib/src/pages/tools/health/body_analysis/body_analysis_cards.dart @@ -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 { final bool isDark; final BodyAnalysisCalculator calc; @@ -482,6 +609,10 @@ class GoalCaloriesCard extends StatelessWidget { @override Widget build(BuildContext context) { + final isOverweight = calc.bmi >= 28; + final isUnderweight = calc.bmi < 18.5; + final showFatLoss = !isUnderweight; + final showMuscleGain = !isOverweight; return GlassContainer( padding: const EdgeInsets.all(DesignTokens.space4), child: Column( @@ -505,29 +636,36 @@ class GoalCaloriesCard extends StatelessWidget { isDark: isDark, title: '摄入需求', content: - '基于TDEE计算不同目标的每日摄入量:\n\n' + '基于TDEE和BMI计算不同目标的每日摄入量:\n\n' '减脂: TDEE - 500 kcal(每周约减0.5kg)\n' '增肌: TDEE + 300 kcal(配合力量训练)\n' '维持: TDEE(保持当前体重)\n\n' + '根据BMI智能推荐:\n' + '偏瘦(BMI<18.5): 隐藏减脂选项\n' + '肥胖(BMI≥28): 隐藏增肌选项\n\n' '建议不要低于BMR,否则影响基础代谢。', ), ], ), const SizedBox(height: DesignTokens.space3), - _buildGoalRow( - '🔥 减脂', - calc.fatLossCalories, - DesignTokens.orange, - 'kcal/天', - ), - const SizedBox(height: DesignTokens.space2), - _buildGoalRow( - '💪 增肌', - calc.muscleGainCalories, - DesignTokens.green, - 'kcal/天', - ), - const SizedBox(height: DesignTokens.space2), + if (showFatLoss) ...[ + _buildGoalRow( + '🔥 减脂', + calc.fatLossCalories, + DesignTokens.orange, + 'kcal/天', + ), + if (showMuscleGain) const SizedBox(height: DesignTokens.space2), + ], + if (showMuscleGain) ...[ + _buildGoalRow( + '💪 增肌', + calc.muscleGainCalories, + DesignTokens.green, + 'kcal/天', + ), + const SizedBox(height: DesignTokens.space2), + ], _buildGoalRow( '⚖️ 维持', calc.maintenanceCalories, @@ -579,6 +717,10 @@ class ProteinCard extends StatelessWidget { @override Widget build(BuildContext context) { + final isOverweight = calc.bmi >= 28; + final isUnderweight = calc.bmi < 18.5; + final showFatLoss = !isUnderweight; + final showMuscleGain = !isOverweight; return GlassContainer( padding: const EdgeInsets.all(DesignTokens.space4), child: Column( @@ -611,10 +753,18 @@ class ProteinCard extends StatelessWidget { ], ), const SizedBox(height: DesignTokens.space3), - _buildGoalRow('🔥 减脂', calc.proteinForFatLoss, DesignTokens.orange), - const SizedBox(height: DesignTokens.space2), - _buildGoalRow('💪 增肌', calc.proteinForMuscleGain, DesignTokens.green), - const SizedBox(height: DesignTokens.space2), + if (showFatLoss) ...[ + _buildGoalRow('🔥 减脂', calc.proteinForFatLoss, DesignTokens.orange), + if (showMuscleGain) const SizedBox(height: DesignTokens.space2), + ], + if (showMuscleGain) ...[ + _buildGoalRow( + '💪 增肌', + calc.proteinForMuscleGain, + DesignTokens.green, + ), + const SizedBox(height: DesignTokens.space2), + ], _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, + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/pages/tools/health/body_analysis/body_analysis_page.dart b/lib/src/pages/tools/health/body_analysis/body_analysis_page.dart index af0e530..434b253 100644 --- a/lib/src/pages/tools/health/body_analysis/body_analysis_page.dart +++ b/lib/src/pages/tools/health/body_analysis/body_analysis_page.dart @@ -47,6 +47,27 @@ class _BodyAnalysisPageState extends State 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 void initState() { super.initState(); @@ -69,17 +90,17 @@ class _BodyAnalysisPageState extends State } BodyAnalysisCalculator get _calc => BodyAnalysisCalculator( - gender: _gender, - age: double.tryParse(_ageController.text) ?? 0, - height: double.tryParse(_heightController.text) ?? 0, - weight: double.tryParse(_weightController.text) ?? 0, - waist: double.tryParse(_waistController.text), - hip: double.tryParse(_hipController.text), - neck: double.tryParse(_neckController.text), - wrist: double.tryParse(_wristController.text), - activityLevel: _activityLevel, - bodyFatInput: double.tryParse(_bodyFatController.text), - ); + gender: _gender, + age: double.tryParse(_ageController.text) ?? 0, + height: double.tryParse(_heightController.text) ?? 0, + weight: double.tryParse(_weightController.text) ?? 0, + waist: double.tryParse(_waistController.text), + hip: double.tryParse(_hipController.text), + neck: double.tryParse(_neckController.text), + wrist: double.tryParse(_wristController.text), + activityLevel: _activityLevel, + bodyFatInput: double.tryParse(_bodyFatController.text), + ); bool get _isValid => _calc.isValid; @@ -161,14 +182,17 @@ class _BodyAnalysisPageState extends State mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildInfoSection('🔒 隐私声明', - '所有数据仅在您的设备本地计算,不会联网上传,不收集任何个人信息。'), + _buildInfoSection('🔒 隐私声明', '所有数据仅在您的设备本地计算,不会联网上传,不收集任何个人信息。'), SizedBox(height: 10), - _buildInfoSection('📐 算法来源', - '• BMR: Harris-Benedict / Mifflin-St Jeor 公式\n• BMI: WHO 国际标准\n• 体脂率: US Navy Method / BMI估算法\n• TDEE: 活动系数法\n• 心率区间: Karvonen 公式\n• 喝水量: 基于体重与活动量估算'), + _buildInfoSection( + '📐 算法来源', + '• BMR: Harris-Benedict / Mifflin-St Jeor 公式\n• BMI: WHO 国际标准\n• 体脂率: US Navy Method / BMI估算法\n• TDEE: 活动系数法\n• 心率区间: Karvonen 公式\n• 喝水量: 基于体重与活动量估算', + ), SizedBox(height: 10), - _buildInfoSection('⚠️ 免责声明', - '本工具仅供学习参考,不作为医疗诊断依据。如有健康问题请咨询专业医生。禁止用于商业用途。'), + _buildInfoSection( + '⚠️ 免责声明', + '本工具仅供学习参考,不作为医疗诊断依据。如有健康问题请咨询专业医生。禁止用于商业用途。', + ), ], ), ), @@ -187,8 +211,10 @@ class _BodyAnalysisPageState extends State return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(title, - style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600)), + Text( + title, + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600), + ), SizedBox(height: 4), Text(content, style: TextStyle(fontSize: 13, height: 1.4)), ], @@ -200,8 +226,9 @@ class _BodyAnalysisPageState extends State final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark; return CupertinoPageScaffold( - backgroundColor: - isDark ? DarkDesignTokens.background : DesignTokens.background, + backgroundColor: isDark + ? DarkDesignTokens.background + : DesignTokens.background, navigationBar: CupertinoNavigationBar( middle: const Text('🧬 身体分析'), trailing: Row( @@ -223,69 +250,251 @@ class _BodyAnalysisPageState extends State border: null, ), child: SafeArea( - child: ListView( - padding: const EdgeInsets.all(DesignTokens.space4), - children: [ - _buildRequiredInputs(isDark), - const SizedBox(height: DesignTokens.space3), - _buildOptionalToggle(isDark), - if (_showOptional) ...[ - const SizedBox(height: DesignTokens.space3), - _buildOptionalInputs(isDark), - ], - const SizedBox(height: DesignTokens.space4), - SizedBox( - width: double.infinity, - child: CupertinoButton.filled( - borderRadius: DesignTokens.borderRadiusLg, - onPressed: _isValid ? _analyze : null, - child: const Text('🧬 开始分析'), - ), - ), - if (_hasResult && _isValid) ...[ - const SizedBox(height: DesignTokens.space5), - 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, - ); - }, - ), - const SizedBox(height: DesignTokens.space4), - _buildSwipeableTabs(isDark), - ], - ], + child: _hasResult && _isValid + ? _buildResultLayout(isDark) + : _buildInputLayout(isDark), + ), + ); + } + + Widget _buildInputLayout(bool isDark) { + return ListView( + padding: const EdgeInsets.all(DesignTokens.space4), + children: [ + _buildRequiredInputs(isDark), + const SizedBox(height: DesignTokens.space3), + _buildOptionalToggle(isDark), + if (_showOptional) ...[ + const SizedBox(height: DesignTokens.space3), + _buildOptionalInputs(isDark), + ], + const SizedBox(height: DesignTokens.space4), + SizedBox( + width: double.infinity, + child: CupertinoButton.filled( + borderRadius: DesignTokens.borderRadiusLg, + onPressed: _isValid ? _analyze : null, + child: const Text('🧬 开始分析'), + ), ), + ], + ); + } + + 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) { final calc = _calc; - return SizedBox( - height: _selectedTab == 0 - ? 1200 - : _selectedTab == 1 - ? 1000 - : 900, - child: PageView( - controller: _pageController, - onPageChanged: (i) => setState(() => _selectedTab = i), - children: [ - CoreMetricsTab(isDark: isDark, calc: calc), - ScienceMetricsTab(isDark: isDark, calc: calc), - FunStatsTab(isDark: isDark, calc: calc), - ], - ), + final glowColor = DesignTokens.dynamicPrimary; + return Stack( + children: [ + NotificationListener( + onNotification: (notification) { + _onOverscroll(notification); + return false; + }, + child: PageView( + controller: _pageController, + onPageChanged: (i) => setState(() => _selectedTab = i), + children: [ + 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 onTap: _saveData, child: Container( padding: const EdgeInsets.symmetric( - horizontal: DesignTokens.space2, vertical: DesignTokens.space1), + horizontal: DesignTokens.space2, + vertical: DesignTokens.space1, + ), decoration: BoxDecoration( color: DesignTokens.dynamicPrimary.withValues(alpha: 0.1), borderRadius: DesignTokens.borderRadiusSm, @@ -319,14 +530,20 @@ class _BodyAnalysisPageState extends State child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(CupertinoIcons.floppy_disk, - size: 14, color: DesignTokens.dynamicPrimary), + Icon( + CupertinoIcons.floppy_disk, + size: 14, + color: DesignTokens.dynamicPrimary, + ), const SizedBox(width: 4), - Text('保存', - style: TextStyle( - fontSize: DesignTokens.fontXs, - fontWeight: FontWeight.w500, - color: DesignTokens.dynamicPrimary)), + Text( + '保存', + style: TextStyle( + fontSize: DesignTokens.fontXs, + fontWeight: FontWeight.w500, + color: DesignTokens.dynamicPrimary, + ), + ), ], ), ), @@ -336,7 +553,9 @@ class _BodyAnalysisPageState extends State onTap: () => setState(() => _isInputHidden = !_isInputHidden), child: Container( padding: const EdgeInsets.symmetric( - horizontal: DesignTokens.space2, vertical: DesignTokens.space1), + horizontal: DesignTokens.space2, + vertical: DesignTokens.space1, + ), decoration: BoxDecoration( color: DesignTokens.dynamicPrimary.withValues(alpha: 0.1), borderRadius: DesignTokens.borderRadiusSm, @@ -345,17 +564,21 @@ class _BodyAnalysisPageState extends State mainAxisSize: MainAxisSize.min, children: [ Icon( - _isInputHidden - ? CupertinoIcons.eye_slash - : CupertinoIcons.eye, - size: 14, - color: DesignTokens.dynamicPrimary), + _isInputHidden + ? CupertinoIcons.eye_slash + : CupertinoIcons.eye, + size: 14, + color: DesignTokens.dynamicPrimary, + ), const SizedBox(width: 4), - Text(_isInputHidden ? '显示' : '隐藏', - style: TextStyle( - fontSize: DesignTokens.fontXs, - fontWeight: FontWeight.w500, - color: DesignTokens.dynamicPrimary)), + Text( + _isInputHidden ? '显示' : '隐藏', + style: TextStyle( + fontSize: DesignTokens.fontXs, + fontWeight: FontWeight.w500, + color: DesignTokens.dynamicPrimary, + ), + ), ], ), ), @@ -374,16 +597,20 @@ class _BodyAnalysisPageState extends State ), child: Row( children: [ - Icon(CupertinoIcons.eye_slash, - size: 16, - color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3), + Icon( + CupertinoIcons.eye_slash, + size: 16, + color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, + ), const SizedBox(width: DesignTokens.space2), Text( '信息已隐藏,点击"显示"查看', style: TextStyle( - fontSize: DesignTokens.fontSm, - color: - isDark ? DarkDesignTokens.text3 : DesignTokens.text3), + fontSize: DesignTokens.fontSm, + color: isDark + ? DarkDesignTokens.text3 + : DesignTokens.text3, + ), ), ], ), @@ -393,25 +620,28 @@ class _BodyAnalysisPageState extends State _buildGenderSelector(isDark), const SizedBox(height: DesignTokens.space3), _buildInputField( - isDark: isDark, - controller: _ageController, - label: '年龄', - unit: '岁', - placeholder: '请输入年龄'), + isDark: isDark, + controller: _ageController, + label: '年龄', + unit: '岁', + placeholder: '请输入年龄', + ), const SizedBox(height: DesignTokens.space3), _buildInputField( - isDark: isDark, - controller: _heightController, - label: '身高', - unit: 'cm', - placeholder: '请输入身高'), + isDark: isDark, + controller: _heightController, + label: '身高', + unit: 'cm', + placeholder: '请输入身高', + ), const SizedBox(height: DesignTokens.space3), _buildInputField( - isDark: isDark, - controller: _weightController, - label: '体重', - unit: 'kg', - placeholder: '请输入体重'), + isDark: isDark, + controller: _weightController, + label: '体重', + unit: 'kg', + placeholder: '请输入体重', + ), ], ], ), @@ -422,10 +652,13 @@ class _BodyAnalysisPageState extends State return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('性别', - style: TextStyle( - fontSize: DesignTokens.fontSm, - color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2)), + Text( + '性别', + style: TextStyle( + fontSize: DesignTokens.fontSm, + color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2, + ), + ), const SizedBox(height: DesignTokens.space2), SizedBox( width: double.infinity, @@ -435,25 +668,29 @@ class _BodyAnalysisPageState extends State children: { Gender.male: Padding( padding: const EdgeInsets.symmetric( - horizontal: DesignTokens.space4, - vertical: DesignTokens.space2), - child: Text('👨 男性', - style: TextStyle( - fontSize: DesignTokens.fontMd, - color: isDark - ? DarkDesignTokens.text1 - : DesignTokens.text1)), + horizontal: DesignTokens.space4, + vertical: DesignTokens.space2, + ), + child: Text( + '👨 男性', + style: TextStyle( + fontSize: DesignTokens.fontMd, + color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1, + ), + ), ), Gender.female: Padding( padding: const EdgeInsets.symmetric( - horizontal: DesignTokens.space4, - vertical: DesignTokens.space2), - child: Text('👩 女性', - style: TextStyle( - fontSize: DesignTokens.fontMd, - color: isDark - ? DarkDesignTokens.text1 - : DesignTokens.text1)), + horizontal: DesignTokens.space4, + vertical: DesignTokens.space2, + ), + child: Text( + '👩 女性', + style: TextStyle( + fontSize: DesignTokens.fontMd, + color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1, + ), + ), ), }, ), @@ -467,29 +704,36 @@ class _BodyAnalysisPageState extends State onTap: () => setState(() => _showOptional = !_showOptional), child: Container( padding: const EdgeInsets.symmetric( - horizontal: DesignTokens.space4, vertical: DesignTokens.space3), + horizontal: DesignTokens.space4, + vertical: DesignTokens.space3, + ), decoration: BoxDecoration( color: isDark ? DarkDesignTokens.card : DesignTokens.card, borderRadius: DesignTokens.borderRadiusLg, border: Border.all( - color: isDark ? DarkDesignTokens.glassBorder : DesignTokens.glassBorder), + color: isDark + ? DarkDesignTokens.glassBorder + : DesignTokens.glassBorder, + ), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( - _showOptional - ? CupertinoIcons.chevron_up - : CupertinoIcons.chevron_down, - size: 16, - color: DesignTokens.dynamicPrimary), + _showOptional + ? CupertinoIcons.chevron_up + : CupertinoIcons.chevron_down, + size: 16, + color: DesignTokens.dynamicPrimary, + ), const SizedBox(width: DesignTokens.space2), Text( '更多参数(选填,可解锁更多指标)', style: TextStyle( - fontSize: DesignTokens.fontMd, - fontWeight: FontWeight.w500, - color: DesignTokens.dynamicPrimary), + fontSize: DesignTokens.fontMd, + fontWeight: FontWeight.w500, + color: DesignTokens.dynamicPrimary, + ), ), ], ), @@ -503,53 +747,64 @@ class _BodyAnalysisPageState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('选填参数', - style: TextStyle( - fontSize: DesignTokens.fontLg, - fontWeight: FontWeight.w600, - color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1)), + Text( + '选填参数', + style: TextStyle( + fontSize: DesignTokens.fontLg, + fontWeight: FontWeight.w600, + color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1, + ), + ), const SizedBox(height: DesignTokens.space1), - Text('填写越多,分析越精确', - style: TextStyle( - fontSize: DesignTokens.fontSm, - color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2)), + Text( + '填写越多,分析越精确', + style: TextStyle( + fontSize: DesignTokens.fontSm, + color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2, + ), + ), const SizedBox(height: DesignTokens.space3), _buildInputField( - isDark: isDark, - controller: _waistController, - label: '腰围', - unit: 'cm', - placeholder: '选填'), + isDark: isDark, + controller: _waistController, + label: '腰围', + unit: 'cm', + placeholder: '选填', + ), const SizedBox(height: DesignTokens.space3), _buildInputField( - isDark: isDark, - controller: _hipController, - label: '臀围', - unit: 'cm', - placeholder: '选填'), + isDark: isDark, + controller: _hipController, + label: '臀围', + unit: 'cm', + placeholder: '选填', + ), const SizedBox(height: DesignTokens.space3), _buildInputField( - isDark: isDark, - controller: _neckController, - label: '颈围', - unit: 'cm', - placeholder: '选填'), + isDark: isDark, + controller: _neckController, + label: '颈围', + unit: 'cm', + placeholder: '选填', + ), const SizedBox(height: DesignTokens.space3), _buildInputField( - isDark: isDark, - controller: _wristController, - label: '手腕围', - unit: 'cm', - placeholder: '选填'), + isDark: isDark, + controller: _wristController, + label: '手腕围', + unit: 'cm', + placeholder: '选填', + ), const SizedBox(height: DesignTokens.space3), _buildActivitySelector(isDark), const SizedBox(height: DesignTokens.space3), _buildInputField( - isDark: isDark, - controller: _bodyFatController, - label: '已知体脂率', - unit: '%', - placeholder: '选填,跳过自动估算'), + isDark: isDark, + controller: _bodyFatController, + label: '已知体脂率', + unit: '%', + placeholder: '选填,跳过自动估算', + ), ], ), ); @@ -567,10 +822,13 @@ class _BodyAnalysisPageState extends State return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('活动等级', - style: TextStyle( - fontSize: DesignTokens.fontSm, - color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2)), + Text( + '活动等级', + style: TextStyle( + fontSize: DesignTokens.fontSm, + color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2, + ), + ), const SizedBox(height: DesignTokens.space2), Container( padding: const EdgeInsets.all(DesignTokens.space1), @@ -587,8 +845,9 @@ class _BodyAnalysisPageState extends State onTap: () => setState(() => _activityLevel = e.$1), child: Container( padding: const EdgeInsets.symmetric( - horizontal: DesignTokens.space3, - vertical: DesignTokens.space2 + 2), + horizontal: DesignTokens.space3, + vertical: DesignTokens.space2 + 2, + ), decoration: BoxDecoration( color: isSelected ? (isDark ? DarkDesignTokens.card : DesignTokens.card) @@ -598,23 +857,30 @@ class _BodyAnalysisPageState extends State ), child: Row( children: [ - Text(e.$2, - style: TextStyle( - fontSize: DesignTokens.fontMd, - fontWeight: - isSelected ? FontWeight.w600 : FontWeight.w400, - color: isSelected - ? DesignTokens.dynamicPrimary - : (isDark - ? DarkDesignTokens.text2 - : DesignTokens.text2))), + Text( + e.$2, + style: TextStyle( + fontSize: DesignTokens.fontMd, + fontWeight: isSelected + ? FontWeight.w600 + : FontWeight.w400, + color: isSelected + ? DesignTokens.dynamicPrimary + : (isDark + ? DarkDesignTokens.text2 + : DesignTokens.text2), + ), + ), const SizedBox(width: DesignTokens.space2), - Text(e.$3, - style: TextStyle( - fontSize: DesignTokens.fontSm, - color: isDark - ? DarkDesignTokens.text3 - : DesignTokens.text3)), + Text( + e.$3, + style: TextStyle( + fontSize: DesignTokens.fontSm, + color: isDark + ? DarkDesignTokens.text3 + : DesignTokens.text3, + ), + ), ], ), ), @@ -636,10 +902,13 @@ class _BodyAnalysisPageState extends State return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(label, - style: TextStyle( - fontSize: DesignTokens.fontSm, - color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2)), + Text( + label, + style: TextStyle( + fontSize: DesignTokens.fontSm, + color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2, + ), + ), const SizedBox(height: DesignTokens.space1), Row( children: [ @@ -647,7 +916,9 @@ class _BodyAnalysisPageState extends State child: CupertinoTextField( controller: controller, placeholder: placeholder, - keyboardType: const TextInputType.numberWithOptions(decimal: true), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), padding: const EdgeInsets.all(DesignTokens.space3), decoration: BoxDecoration( color: isDark @@ -661,18 +932,22 @@ class _BodyAnalysisPageState extends State const SizedBox(width: DesignTokens.space2), Container( padding: const EdgeInsets.symmetric( - horizontal: DesignTokens.space3, vertical: DesignTokens.space2), + horizontal: DesignTokens.space3, + vertical: DesignTokens.space2, + ), decoration: BoxDecoration( color: isDark ? DarkDesignTokens.text3.withValues(alpha: 0.1) : DesignTokens.background, borderRadius: DesignTokens.borderRadiusMd, ), - child: Text(unit, - style: TextStyle( - fontSize: DesignTokens.fontMd, - color: - isDark ? DarkDesignTokens.text2 : DesignTokens.text2)), + child: Text( + unit, + style: TextStyle( + fontSize: DesignTokens.fontMd, + color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2, + ), + ), ), ], ), diff --git a/lib/src/pages/tools/health/body_analysis/core_tab.dart b/lib/src/pages/tools/health/body_analysis/core_tab.dart index 30bc2e6..afd2ca2 100644 --- a/lib/src/pages/tools/health/body_analysis/core_tab.dart +++ b/lib/src/pages/tools/health/body_analysis/core_tab.dart @@ -3,7 +3,7 @@ * 名称: 身体分析核心指标Tab * 作用: 展示喝水/BMR/TDEE/BMI/体脂率/心率区间/摄入需求/蛋白质等核心指标 * 创建: 2026-04-24 - * 更新: 2026-04-24 从 body_analysis_page.dart 拆分 + * 更新: 2026-04-24 修复溢出+增加滑动提示 */ import 'package:flutter/cupertino.dart'; @@ -19,63 +19,103 @@ class CoreMetricsTab extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - children: [ - WaterCard(isDark: isDark, calc: calc), - const SizedBox(height: DesignTokens.space3), - MetricCard( - isDark: isDark, - emoji: '🔥', - title: '基础代谢率 (BMR)', - value: calc.bmr.toStringAsFixed(0), - unit: 'kcal/天', - color: DesignTokens.orange, - infoTitle: '基础代谢率 (BMR)', - infoContent: '基础代谢率是人体在安静状态下维持生命所需的最低能量消耗。' - '采用Mifflin-St Jeor公式计算,被认为是目前最准确的BMR估算公式。\n\n' - '男性: BMR = 10×体重(kg) + 6.25×身高(cm) - 5×年龄 + 5\n' - '女性: BMR = 10×体重(kg) + 6.25×身高(cm) - 5×年龄 - 161', - ), - const SizedBox(height: DesignTokens.space3), - MetricCard( - isDark: isDark, - emoji: '🏃', - title: '每日总能量消耗 (TDEE)', - value: calc.tdee.toStringAsFixed(0), - unit: 'kcal/天', - color: DesignTokens.green, - infoTitle: '每日总能量消耗 (TDEE)', - infoContent: 'TDEE = BMR × 活动系数,表示你每天实际消耗的总热量。\n\n' - '活动系数:\n' - '久坐: 1.2 | 轻度: 1.375 | 中度: 1.55\n' - '高度: 1.725 | 极高: 1.9\n\n' - '减脂建议: TDEE - 500 kcal\n' - '增肌建议: TDEE + 300 kcal', - ), - const SizedBox(height: DesignTokens.space3), - MetricCard( - isDark: isDark, - emoji: '📊', - title: '身体质量指数 (BMI)', - value: calc.bmi.toStringAsFixed(1), - unit: calc.bmiCategory, - color: Color(calc.bmiColorValue), - infoTitle: '身体质量指数 (BMI)', - infoContent: 'BMI = 体重(kg) / 身高(m)²\n\n' - '分类标准(中国):\n' - '< 18.5 偏瘦 | 18.5-23.9 正常\n' - '24-27.9 偏胖 | ≥ 28 肥胖\n\n' - '注意:BMI未考虑肌肉量和体脂分布,运动员等人群可能不准确。', - ), - 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), - ], + return SingleChildScrollView( + physics: const ClampingScrollPhysics(), + child: Column( + children: [ + WaterCard(isDark: isDark, calc: calc), + const SizedBox(height: DesignTokens.space3), + MetricCard( + isDark: isDark, + emoji: '🔥', + title: '基础代谢率 (BMR)', + value: calc.bmr.toStringAsFixed(0), + unit: 'kcal/天', + color: DesignTokens.orange, + infoTitle: '基础代谢率 (BMR)', + infoContent: + '基础代谢率是人体在安静状态下维持生命所需的最低能量消耗。' + '采用Mifflin-St Jeor公式计算,被认为是目前最准确的BMR估算公式。\n\n' + '男性: BMR = 10×体重(kg) + 6.25×身高(cm) - 5×年龄 + 5\n' + '女性: BMR = 10×体重(kg) + 6.25×身高(cm) - 5×年龄 - 161', + ), + const SizedBox(height: DesignTokens.space3), + MetricCard( + isDark: isDark, + emoji: '🏃', + title: '每日总能量消耗 (TDEE)', + value: calc.tdee.toStringAsFixed(0), + unit: 'kcal/天', + color: DesignTokens.green, + infoTitle: '每日总能量消耗 (TDEE)', + infoContent: + 'TDEE = BMR × 活动系数,表示你每天实际消耗的总热量。\n\n' + '活动系数:\n' + '久坐: 1.2 | 轻度: 1.375 | 中度: 1.55\n' + '高度: 1.725 | 极高: 1.9\n\n' + '减脂建议: TDEE - 500 kcal\n' + '增肌建议: TDEE + 300 kcal', + ), + const SizedBox(height: DesignTokens.space3), + MetricCard( + isDark: isDark, + emoji: '📊', + title: '身体质量指数 (BMI)', + value: calc.bmi.toStringAsFixed(1), + unit: calc.bmiCategory, + color: Color(calc.bmiColorValue), + infoTitle: '身体质量指数 (BMI)', + infoContent: + 'BMI = 体重(kg) / 身高(m)²\n\n' + '分类标准(中国):\n' + '< 18.5 偏瘦 | 18.5-23.9 正常\n' + '24-27.9 偏胖 | ≥ 28 肥胖\n\n' + '注意:BMI未考虑肌肉量和体脂分布,运动员等人群可能不准确。', + ), + const SizedBox(height: DesignTokens.space3), + FatBurningHeartRateCard(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, + ), + ], + ), ); } } diff --git a/lib/src/pages/tools/health/body_analysis/fun_tab.dart b/lib/src/pages/tools/health/body_analysis/fun_tab.dart index 789b156..69ebc09 100644 --- a/lib/src/pages/tools/health/body_analysis/fun_tab.dart +++ b/lib/src/pages/tools/health/body_analysis/fun_tab.dart @@ -3,7 +3,7 @@ * 名称: 身体分析趣味统计Tab * 作用: 展示心跳/呼吸/眨眼/DNA/原子等趣味统计数据 * 创建: 2026-04-24 - * 更新: 2026-04-24 从 body_analysis_page.dart 拆分 + * 更新: 2026-04-24 修复溢出+增加滑动提示 */ import 'package:flutter/cupertino.dart'; @@ -19,264 +19,301 @@ class FunStatsTab extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - children: [ - MetricCard( - isDark: isDark, - emoji: '🫀', - title: '今日心跳次数', - value: calc.formatNumber(calc.dailyHeartbeats.toDouble()), - unit: '次', - color: DesignTokens.red, - infoTitle: '心跳次数', - infoContent: - '平均心率因年龄而异:\n' - '婴儿约130次/分 | 儿童约90次/分\n' - '成人约72次/分\n\n' - '心脏每天跳动约10万次,一生约25-30亿次。', - ), - const SizedBox(height: DesignTokens.space3), - MetricCard( - isDark: isDark, - emoji: '🫁', - title: '今日呼吸次数', - value: calc.formatNumber(calc.dailyBreaths.toDouble()), - unit: '次', - color: DesignTokens.blue, - infoTitle: '呼吸次数', - infoContent: - '成人平均呼吸频率约16次/分钟。\n\n' - '每次呼吸约吸入0.5升空气,\n' - '每天呼吸约23,040次,\n' - '吸入空气约11,520升。', - ), - const SizedBox(height: DesignTokens.space3), - MetricCard( - isDark: isDark, - emoji: '👁️', - title: '今日眨眼次数', - value: calc.formatNumber(calc.dailyBlinks.toDouble()), - unit: '次', - color: DesignTokens.teal, - infoTitle: '眨眼次数', - infoContent: - '人类平均每分钟眨眼15-20次,\n' - '每天约14,400次。\n\n' - '每次眨眼约0.1-0.4秒,\n' - '眨眼可以清洁和润滑眼球,' - '同时大脑会利用眨眼间隙重新调整注意力。', - ), - const SizedBox(height: DesignTokens.space3), - MetricCard( - isDark: isDark, - emoji: '🩸', - title: '今日心脏泵血量', - value: calc.dailyBloodPumped.toStringAsFixed(0), - unit: '升', - color: DesignTokens.red, - infoTitle: '心脏泵血量', - infoContent: - '心脏每次跳动泵出约70ml血液,\n' - '每天泵血约7,000-10,000升。\n\n' - '这个量足以填满一个标准游泳池的1/250。', - ), - const SizedBox(height: DesignTokens.space3), - MetricCard( - isDark: isDark, - emoji: '🌬️', - title: '今日呼吸空气体积', - value: calc.dailyAirVolume.toStringAsFixed(0), - unit: '升', - color: DesignTokens.blue, - infoTitle: '呼吸空气体积', - infoContent: - '每次呼吸约0.5升,每分钟16次,\n' - '每天约吸入11,520升空气。\n\n' - '这相当于约12立方米,' - '足够充满一个小型储藏室。', - ), - const SizedBox(height: DesignTokens.space3), - MetricCard( - isDark: isDark, - emoji: '💧', - title: '今日唾液分泌量', - value: calc.dailySaliva.toStringAsFixed(1), - unit: '升', - color: DesignTokens.green, - infoTitle: '唾液分泌量', - infoContent: - '人体每天分泌约1-1.5升唾液。\n\n' - '唾液含有淀粉酶帮助消化,' - '溶菌酶帮助杀菌,' - '还能保持口腔湿润、帮助味觉感知。\n' - '一生约分泌25,000-40,000升唾液。', - ), - const SizedBox(height: DesignTokens.space3), - MetricCard( - isDark: isDark, - emoji: '🧬', - title: '体内DNA总长度', - value: calc.formatDistance(calc.totalDnaLength), - unit: '', - color: DesignTokens.purple, - infoTitle: 'DNA总长度', - infoContent: - '人体约有37.2万亿个细胞,\n' - '每个细胞含约2米长的DNA。\n\n' - '全部DNA展开总长度约744亿公里,\n' - '约为地球到太阳距离的500倍!\n' - '但DNA直径仅2纳米,比头发丝细10万倍。', - ), - const SizedBox(height: DesignTokens.space3), - MetricCard( - isDark: isDark, - emoji: '🌍', - title: '身体原子数量', - value: '~7×10²⁷', - unit: '个原子', - color: DesignTokens.gold, - infoTitle: '身体原子数量', - infoContent: - '成年人体内约有7×10²⁷个原子,\n' - '即7后面跟27个零。\n\n' - '组成比例:\n' - '氧65% | 碳18% | 氢10% | 氮3%\n' - '其余为钙、磷、钾、硫等微量元素。\n' - '每年约98%的原子会被替换。', - ), - const SizedBox(height: DesignTokens.space3), - MetricCard( - isDark: isDark, - emoji: '💇', - title: '一生头发生长总长度', - value: calc.formatDistance(calc.lifetimeHairLength), - unit: '', - color: DesignTokens.orange, - infoTitle: '头发生长', - infoContent: - '头发平均每月生长约1.25厘米,\n' - '每年约15厘米。\n\n' - '人类头皮约有10万个毛囊,\n' - '每天正常脱落50-100根头发。\n' - '金发人群平均有15万个毛囊,\n' - '红发人群约9万个。', - ), - const SizedBox(height: DesignTokens.space3), - MetricCard( - isDark: isDark, - emoji: '💅', - title: '一生指甲生长总长度', - value: calc.formatDistance(calc.lifetimeNailLength), - unit: '', - color: DesignTokens.teal, - infoTitle: '指甲生长', - infoContent: - '手指甲每月生长约3.5毫米,\n' - '脚趾甲生长速度约手指甲的1/3。\n\n' - '指甲由角蛋白组成,与头发成分相同。\n' - '惯用手的指甲生长更快,\n' - '夏天比冬天生长更快。', - ), - const SizedBox(height: DesignTokens.space3), - MetricCard( - isDark: isDark, - emoji: '⏱️', - title: '一生心跳总数', - value: calc.formatNumber(calc.lifetimeHeartbeats), - unit: '次(基于年龄)', - color: DesignTokens.red, - infoTitle: '一生心跳总数', - infoContent: - '基于你的年龄和平均心率估算。\n\n' - '如果活到80岁,心脏大约跳动30亿次。\n' - '心脏是人体中最勤劳的器官,\n' - '从胚胎4周开始跳动,直到生命终结。', - ), - const SizedBox(height: DesignTokens.space3), - MetricCard( - isDark: isDark, - emoji: '🫁', - title: '一生呼吸总次数', - value: calc.formatNumber(calc.lifetimeBreaths), - unit: '次(基于年龄)', - color: DesignTokens.blue, - infoTitle: '一生呼吸总次数', - infoContent: - '基于你的年龄和平均呼吸频率估算。\n\n' - '成人每分钟约呼吸16次,\n' - '每小时960次,每天约23,040次。\n' - '如果活到80岁,大约呼吸6-7亿次。', - ), - const SizedBox(height: DesignTokens.space3), - MetricCard( - isDark: isDark, - emoji: '👁️', - title: '一生眨眼总次数', - value: calc.formatNumber(calc.lifetimeBlinks), - unit: '次(基于年龄)', - color: DesignTokens.teal, - infoTitle: '一生眨眼总次数', - infoContent: - '基于你的年龄和平均眨眼频率估算。\n\n' - '每天约14,400次眨眼,\n' - '一生约4亿次眨眼。\n\n' - '有趣的是,每次眨眼约0.1-0.4秒,\n' - '一生中约有5天的时间在眨眼中度过。', - ), - const SizedBox(height: DesignTokens.space3), - MetricCard( - isDark: isDark, - emoji: '🧴', - title: '一生皮肤细胞脱落量', - value: calc.formatNumber(calc.lifetimeSkinCells), - unit: '个(基于年龄)', - color: DesignTokens.purple, - infoTitle: '皮肤细胞脱落', - infoContent: - '人体每天约脱落3-5千万个皮肤细胞,\n' - '取中间值约4千万个/天。\n\n' - '皮肤是人体最大的器官,\n' - '每27天左右皮肤就会完全更新一次。\n' - '一生脱落的皮肤细胞总重量约18-23公斤,\n' - '相当于一个小孩的体重!', - ), - const SizedBox(height: DesignTokens.space3), - MetricCard( - isDark: isDark, - emoji: '🍽️', - title: '一生消耗食物总重量', - value: calc.formatNumber(calc.lifetimeFoodWeight), - unit: '吨(基于年龄)', - color: DesignTokens.orange, - infoTitle: '一生食物消耗', - infoContent: - '基于你的TDEE和年龄估算。\n\n' - '假设食物平均热量密度约2 kcal/g,\n' - '一个成年人一生大约吃掉25-30吨食物,\n' - '相当于约5头大象的重量!\n\n' - '其中约60%为碳水化合物和脂肪,\n' - '15%为蛋白质,25%为水分和其他。', - ), - const SizedBox(height: DesignTokens.space3), - MetricCard( - isDark: isDark, - emoji: '🚶', - title: '一生行走距离估算', - value: calc.formatDistance(calc.lifetimeWalkingDistance * 100), - unit: '', - color: DesignTokens.green, - infoTitle: '一生行走距离', - infoContent: - '基于你的年龄、身高和活动水平估算。\n\n' - '步幅 ≈ 身高 × 0.415\n' - '每日步数根据年龄调整:\n' - '<10岁 12000步 | 10-30岁 8000步\n' - '30-50岁 7000步 | 50-70岁 5000步\n' - '70+岁 3000步\n\n' - '一个成年人一生大约走11万公里,\n' - '相当于绕地球2.75圈!', - ), - ], + return SingleChildScrollView( + physics: const ClampingScrollPhysics(), + child: Column( + children: [ + MetricCard( + isDark: isDark, + emoji: '🫀', + title: '今日心跳次数', + value: calc.formatNumber(calc.dailyHeartbeats.toDouble()), + unit: '次', + color: DesignTokens.red, + infoTitle: '心跳次数', + infoContent: + '平均心率因年龄而异:\n' + '婴儿约130次/分 | 儿童约90次/分\n' + '成人约72次/分\n\n' + '心脏每天跳动约10万次,一生约25-30亿次。', + ), + const SizedBox(height: DesignTokens.space3), + MetricCard( + isDark: isDark, + emoji: '🫁', + title: '今日呼吸次数', + value: calc.formatNumber(calc.dailyBreaths.toDouble()), + unit: '次', + color: DesignTokens.blue, + infoTitle: '呼吸次数', + infoContent: + '成人平均呼吸频率约16次/分钟。\n\n' + '每次呼吸约吸入0.5升空气,\n' + '每天呼吸约23,040次,\n' + '吸入空气约11,520升。', + ), + const SizedBox(height: DesignTokens.space3), + MetricCard( + isDark: isDark, + emoji: '👁️', + title: '今日眨眼次数', + value: calc.formatNumber(calc.dailyBlinks.toDouble()), + unit: '次', + color: DesignTokens.teal, + infoTitle: '眨眼次数', + infoContent: + '人类平均每分钟眨眼15-20次,\n' + '每天约14,400次。\n\n' + '每次眨眼约0.1-0.4秒,\n' + '眨眼可以清洁和润滑眼球,' + '同时大脑会利用眨眼间隙重新调整注意力。', + ), + const SizedBox(height: DesignTokens.space3), + MetricCard( + isDark: isDark, + emoji: '🩸', + title: '今日心脏泵血量', + value: calc.dailyBloodPumped.toStringAsFixed(0), + unit: '升', + color: DesignTokens.red, + infoTitle: '心脏泵血量', + infoContent: + '心脏每次跳动泵出约70ml血液,\n' + '每天泵血约7,000-10,000升。\n\n' + '这个量足以填满一个标准游泳池的1/250。', + ), + const SizedBox(height: DesignTokens.space3), + MetricCard( + isDark: isDark, + emoji: '🌬️', + title: '今日呼吸空气体积', + value: calc.dailyAirVolume.toStringAsFixed(0), + unit: '升', + color: DesignTokens.blue, + infoTitle: '呼吸空气体积', + infoContent: + '每次呼吸约0.5升,每分钟16次,\n' + '每天约吸入11,520升空气。\n\n' + '这相当于约12立方米,' + '足够充满一个小型储藏室。', + ), + const SizedBox(height: DesignTokens.space3), + MetricCard( + isDark: isDark, + emoji: '💧', + title: '今日唾液分泌量', + value: calc.dailySaliva.toStringAsFixed(1), + unit: '升', + color: DesignTokens.green, + infoTitle: '唾液分泌量', + infoContent: + '人体每天分泌约1-1.5升唾液。\n\n' + '唾液含有淀粉酶帮助消化,' + '溶菌酶帮助杀菌,' + '还能保持口腔湿润、帮助味觉感知。\n' + '一生约分泌25,000-40,000升唾液。', + ), + const SizedBox(height: DesignTokens.space3), + MetricCard( + isDark: isDark, + emoji: '🧬', + title: '体内DNA总长度', + value: calc.formatDistance(calc.totalDnaLength), + unit: '', + color: DesignTokens.purple, + infoTitle: 'DNA总长度', + infoContent: + '人体约有37.2万亿个细胞,\n' + '每个细胞含约2米长的DNA。\n\n' + '全部DNA展开总长度约744亿公里,\n' + '约为地球到太阳距离的500倍!\n' + '但DNA直径仅2纳米,比头发丝细10万倍。', + ), + const SizedBox(height: DesignTokens.space3), + MetricCard( + isDark: isDark, + emoji: '🌍', + title: '身体原子数量', + value: '~7×10²⁷', + unit: '个原子', + color: DesignTokens.gold, + infoTitle: '身体原子数量', + infoContent: + '成年人体内约有7×10²⁷个原子,\n' + '即7后面跟27个零。\n\n' + '组成比例:\n' + '氧65% | 碳18% | 氢10% | 氮3%\n' + '其余为钙、磷、钾、硫等微量元素。\n' + '每年约98%的原子会被替换。', + ), + const SizedBox(height: DesignTokens.space3), + MetricCard( + isDark: isDark, + emoji: '💇', + title: '一生头发生长总长度', + value: calc.formatDistance(calc.lifetimeHairLength), + unit: '', + color: DesignTokens.orange, + infoTitle: '头发生长', + infoContent: + '头发平均每月生长约1.25厘米,\n' + '每年约15厘米。\n\n' + '人类头皮约有10万个毛囊,\n' + '每天正常脱落50-100根头发。\n' + '金发人群平均有15万个毛囊,\n' + '红发人群约9万个。', + ), + const SizedBox(height: DesignTokens.space3), + MetricCard( + isDark: isDark, + emoji: '💅', + title: '一生指甲生长总长度', + value: calc.formatDistance(calc.lifetimeNailLength), + unit: '', + color: DesignTokens.teal, + infoTitle: '指甲生长', + infoContent: + '手指甲每月生长约3.5毫米,\n' + '脚趾甲生长速度约手指甲的1/3。\n\n' + '指甲由角蛋白组成,与头发成分相同。\n' + '惯用手的指甲生长更快,\n' + '夏天比冬天生长更快。', + ), + const SizedBox(height: DesignTokens.space3), + MetricCard( + isDark: isDark, + emoji: '⏱️', + title: '一生心跳总数', + value: calc.formatNumber(calc.lifetimeHeartbeats), + unit: '次(基于年龄)', + color: DesignTokens.red, + infoTitle: '一生心跳总数', + infoContent: + '基于你的年龄和平均心率估算。\n\n' + '如果活到80岁,心脏大约跳动30亿次。\n' + '心脏是人体中最勤劳的器官,\n' + '从胚胎4周开始跳动,直到生命终结。', + ), + const SizedBox(height: DesignTokens.space3), + MetricCard( + isDark: isDark, + emoji: '🫁', + title: '一生呼吸总次数', + value: calc.formatNumber(calc.lifetimeBreaths), + unit: '次(基于年龄)', + color: DesignTokens.blue, + infoTitle: '一生呼吸总次数', + infoContent: + '基于你的年龄和平均呼吸频率估算。\n\n' + '成人每分钟约呼吸16次,\n' + '每小时960次,每天约23,040次。\n' + '如果活到80岁,大约呼吸6-7亿次。', + ), + const SizedBox(height: DesignTokens.space3), + MetricCard( + isDark: isDark, + emoji: '👁️', + title: '一生眨眼总次数', + value: calc.formatNumber(calc.lifetimeBlinks), + unit: '次(基于年龄)', + color: DesignTokens.teal, + infoTitle: '一生眨眼总次数', + infoContent: + '基于你的年龄和平均眨眼频率估算。\n\n' + '每天约14,400次眨眼,\n' + '一生约4亿次眨眼。\n\n' + '有趣的是,每次眨眼约0.1-0.4秒,\n' + '一生中约有5天的时间在眨眼中度过。', + ), + const SizedBox(height: DesignTokens.space3), + MetricCard( + isDark: isDark, + emoji: '🧴', + title: '一生皮肤细胞脱落量', + value: calc.formatNumber(calc.lifetimeSkinCells), + unit: '个(基于年龄)', + color: DesignTokens.purple, + infoTitle: '皮肤细胞脱落', + infoContent: + '人体每天约脱落3-5千万个皮肤细胞,\n' + '取中间值约4千万个/天。\n\n' + '皮肤是人体最大的器官,\n' + '每27天左右皮肤就会完全更新一次。\n' + '一生脱落的皮肤细胞总重量约18-23公斤,\n' + '相当于一个小孩的体重!', + ), + const SizedBox(height: DesignTokens.space3), + MetricCard( + isDark: isDark, + emoji: '🍽️', + title: '一生消耗食物总重量', + value: calc.formatNumber(calc.lifetimeFoodWeight), + unit: '吨(基于年龄)', + color: DesignTokens.orange, + infoTitle: '一生食物消耗', + infoContent: + '基于你的TDEE和年龄估算。\n\n' + '假设食物平均热量密度约2 kcal/g,\n' + '一个成年人一生大约吃掉25-30吨食物,\n' + '相当于约5头大象的重量!\n\n' + '其中约60%为碳水化合物和脂肪,\n' + '15%为蛋白质,25%为水分和其他。', + ), + const SizedBox(height: DesignTokens.space3), + MetricCard( + isDark: isDark, + emoji: '🚶', + title: '一生行走距离估算', + value: calc.formatDistance(calc.lifetimeWalkingDistance * 100), + unit: '', + color: DesignTokens.green, + infoTitle: '一生行走距离', + infoContent: + '基于你的年龄、身高和活动水平估算。\n\n' + '步幅 ≈ 身高 × 0.415\n' + '每日步数根据年龄调整:\n' + '<10岁 12000步 | 10-30岁 8000步\n' + '30-50岁 7000步 | 50-70岁 5000步\n' + '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, + ), + ], + ), ); } } diff --git a/lib/src/pages/tools/health/body_analysis/science_tab.dart b/lib/src/pages/tools/health/body_analysis/science_tab.dart index 24a387f..8e17793 100644 --- a/lib/src/pages/tools/health/body_analysis/science_tab.dart +++ b/lib/src/pages/tools/health/body_analysis/science_tab.dart @@ -3,7 +3,7 @@ * 名称: 身体分析科学指数Tab * 作用: 展示骨骼/血液/去脂体重/理想体重/代谢年龄/体表面积等科学指标 * 创建: 2026-04-24 - * 更新: 2026-04-24 从 body_analysis_page.dart 拆分 + * 更新: 2026-04-24 修复溢出+增加滑动提示 */ import 'package:flutter/cupertino.dart'; @@ -23,232 +23,263 @@ class ScienceMetricsTab extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - children: [ - MetricCard( - isDark: isDark, - 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) ...[ + return SingleChildScrollView( + physics: const ClampingScrollPhysics(), + child: Column( + children: [ 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)', + emoji: '🦴', + title: '骨骼重量估算', + value: calc.boneWeight.toStringAsFixed(1), + unit: 'kg(约体重15%)', + color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2, + infoTitle: '骨骼重量', infoContent: - '腰围身高比 = 腰围 / 身高\n\n' - '该指标比BMI更能反映内脏脂肪堆积情况。\n' - '健康标准:< 0.5\n' - '0.5-0.6:风险增加\n' - '> 0.6:高风险', + '人体骨骼约占体重的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( + 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), - ], - 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)', + title: 'Ponderal 体重指数', + value: calc.ponderalIndex.toStringAsFixed(1), + unit: 'kg/m³(${calc.ponderalCategory})', + color: DesignTokens.blue, + infoTitle: 'Ponderal Index (Rohrer指数)', infoContent: - '腰臀比 = 腰围 / 臀围\n\n' - '男性:< 0.9 健康 | 0.9-1.0 风险 | > 1.0 高风险\n' - '女性:< 0.8 健康 | 0.8-0.85 风险 | > 0.85 高风险\n\n' - '腰臀比反映脂肪分布,苹果型身材(腹部脂肪多)健康风险更高。', + '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), - ], - if (calc.frameSize.isNotEmpty) ...[ MetricCard( isDark: isDark, - emoji: '🏷️', - title: '骨架类型', - value: calc.frameSize, - unit: '身高/手腕围 = ${calc.frameRatio.toStringAsFixed(1)}', - color: DesignTokens.purple, - infoTitle: '骨架类型', + emoji: '⚡', + title: '基础代谢效率', + value: calc.bmrPerKg.toStringAsFixed(1), + unit: 'kcal/kg/天(${calc.bmrEfficiency})', + color: DesignTokens.orange, + infoTitle: '基础代谢效率 (BMR/kg)', infoContent: - '基于身高与手腕围的比值判定骨架大小。\n\n' - '男性:> 10.4 小骨架 | 9.6-10.4 中等 | < 9.6 大骨架\n' - '女性:> 11.0 小骨架 | 10.1-11.0 中等 | < 10.1 大骨架\n\n' - '骨架大小会影响理想体重的判定标准。', + '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), + ], + const SizedBox(height: DesignTokens.space5), + _buildSwipeHint(isDark), ], - 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, + ), + ); + } + + 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, + ), ), - 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), - 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), + const SizedBox(width: DesignTokens.space2), + Icon(CupertinoIcons.chevron_right, + size: 14, + color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3), ], - ], + ), ); } }