release 24

This commit is contained in:
Developer
2026-04-24 05:05:26 +08:00
parent 5e979d7115
commit 7d5d95d5e0
8 changed files with 1485 additions and 764 deletions

View File

@@ -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'),
),
];
}

View File

@@ -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<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 muscleGainCalories => tdee + 300;

View File

@@ -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,
),
),
],
),
);
}
}

View File

@@ -47,6 +47,27 @@ class _BodyAnalysisPageState extends State<BodyAnalysisPage>
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<BodyAnalysisPage>
}
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<BodyAnalysisPage>
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<BodyAnalysisPage>
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<BodyAnalysisPage>
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<BodyAnalysisPage>
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<OverscrollNotification>(
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<BodyAnalysisPage>
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<BodyAnalysisPage>
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<BodyAnalysisPage>
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<BodyAnalysisPage>
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<BodyAnalysisPage>
),
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<BodyAnalysisPage>
_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<BodyAnalysisPage>
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<BodyAnalysisPage>
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<BodyAnalysisPage>
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<BodyAnalysisPage>
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<BodyAnalysisPage>
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<BodyAnalysisPage>
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<BodyAnalysisPage>
),
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<BodyAnalysisPage>
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<BodyAnalysisPage>
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<BodyAnalysisPage>
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,
),
),
),
],
),

View File

@@ -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,
),
],
),
);
}
}

View File

@@ -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,
),
],
),
);
}
}

View File

@@ -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: '',
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: '',
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),
],
],
),
);
}
}