import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../api/health_api.dart'; import '../../models/health.dart'; import '../../providers/health_provider.dart'; import '../../utils/toast.dart'; import '../../widgets/state_views.dart'; import 'food_picker_page.dart'; /// 今日饮食:营养汇总 + 按餐次分组明细 + 增删改。 class DietPage extends ConsumerStatefulWidget { const DietPage({super.key}); @override ConsumerState createState() => _DietPageState(); } class _DietPageState extends ConsumerState { Future _addTo(String mealType) async { final added = await Navigator.of(context).push( MaterialPageRoute( builder: (_) => FoodPickerPage(mealType: mealType), ), ); if (added == true) { ref.invalidate(dietTodayProvider); ref.invalidate(dietTodayTotalsProvider); } } @override Widget build(BuildContext context) { final async = ref.watch(dietTodayProvider); return Scaffold( backgroundColor: const Color(0xFFF7F4F1), appBar: AppBar( title: const Text('今日饮食'), backgroundColor: const Color(0xFFF7F4F1), elevation: 0, scrolledUnderElevation: 0, foregroundColor: const Color(0xFF5D4037), ), body: switch (async) { AsyncValue(hasError: true) => Center( child: ErrorView( message: '饮食记录加载失败', onRetry: () => ref.invalidate(dietTodayProvider), ), ), AsyncValue(:final value?) => RefreshIndicator( color: const Color(0xFFC8956E), onRefresh: () async { ref.invalidate(dietTodayProvider); await ref.read(dietTodayProvider.future).catchError((_) => DietDay.empty); }, child: _body(value), ), _ => const Center( child: CircularProgressIndicator(color: Color(0xFFC8956E)), ), }, ); } Widget _body(DietDay day) { return ListView( padding: const EdgeInsets.fromLTRB(14, 6, 14, 24), children: [ _summaryCard(day.totals), const SizedBox(height: 12), for (final meal in day.meals) ...[ _mealCard(meal), const SizedBox(height: 12), ], ], ); } // ---------------- 营养汇总 ---------------- Widget _summaryCard(NutritionTotals totals) { return Container( padding: const EdgeInsets.fromLTRB(16, 14, 16, 16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('今日摄入', style: TextStyle(fontSize: 12, color: Color(0xFF8D6E63))), const SizedBox(height: 6), Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Text( _fmt(totals.energyKcal), style: const TextStyle( fontSize: 32, fontWeight: FontWeight.bold, color: Color(0xFF5D4037), ), ), const SizedBox(width: 4), const Text('kcal', style: TextStyle(fontSize: 13, color: Color(0xFF8D6E63))), const Spacer(), Text( '${totals.itemCount} 条记录', style: const TextStyle( fontSize: 12, color: Color(0xFFBCAAA4)), ), ], ), const SizedBox(height: 14), // 营养看板只显示这 5 项 —— 表里有 36 个字段, // 胡萝卜素、碘、锰这些日常记录用不上,全摆出来只会糊住 Row( children: [ Expanded(child: _nutrient('蛋白质', totals.protein, 'g')), Expanded(child: _nutrient('脂肪', totals.fat, 'g')), Expanded(child: _nutrient('碳水', totals.carbohydrate, 'g')), Expanded(child: _nutrient('纤维', totals.dietaryFiber, 'g')), Expanded(child: _nutrient('钠', totals.sodium, 'mg')), ], ), ], ), ); } Widget _nutrient(String label, double value, String unit) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: const TextStyle( fontSize: 11, color: Color(0xFF8D6E63))), const SizedBox(height: 3), Text( '${_fmt(value)}$unit', style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF5D4037), ), ), ], ); } // ---------------- 餐次 ---------------- Widget _mealCard(MealGroup meal) { return Container( padding: const EdgeInsets.fromLTRB(16, 14, 10, 12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( mealLabel(meal.mealType), style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF5D4037), ), ), const SizedBox(width: 8), if (meal.items.isNotEmpty) Text( '${_fmt(meal.totals.energyKcal)} kcal', style: const TextStyle( fontSize: 12, color: Color(0xFF8D6E63)), ), const Spacer(), TextButton.icon( onPressed: () => _addTo(meal.mealType), icon: const Icon(Icons.add, size: 15), label: const Text('添加', style: TextStyle(fontSize: 13)), style: TextButton.styleFrom( foregroundColor: const Color(0xFFC8956E), padding: const EdgeInsets.symmetric(horizontal: 8), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), ), ], ), if (meal.items.isEmpty) const Padding( padding: EdgeInsets.only(top: 4, bottom: 6), child: Text('还没记', style: TextStyle( fontSize: 12.5, color: Color(0xFFBCAAA4))), ) else ...meal.items.map((item) => _dietRow(item)), ], ), ); } Widget _dietRow(DietRecordItem item) { return Padding( padding: const EdgeInsets.only(top: 8), child: Row( children: [ Expanded( child: Text( item.foodName, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 13.5, color: Color(0xFF5D4037)), ), ), const SizedBox(width: 8), // 点份量可以改克数 GestureDetector( onTap: () => _editAmount(item), child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: const Color(0xFFF7F4F1), borderRadius: BorderRadius.circular(6), ), child: Text( '${_fmt(item.amountGrams)}g', style: const TextStyle( fontSize: 11.5, color: Color(0xFF8D6E63)), ), ), ), const SizedBox(width: 8), SizedBox( width: 62, child: Text( '${_fmt(item.energyKcal)} kcal', textAlign: TextAlign.right, style: const TextStyle( fontSize: 11.5, color: Color(0xFF8D6E63)), ), ), IconButton( onPressed: () => _delete(item), icon: const Icon(Icons.close, size: 15), color: const Color(0xFFD7CCC8), visualDensity: VisualDensity.compact, padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 28, minHeight: 28), ), ], ), ); } Future _editAmount(DietRecordItem item) async { final ctrl = TextEditingController(text: _fmt(item.amountGrams)); final grams = await showDialog( context: context, builder: (ctx) => AlertDialog( title: Text(item.foodName, style: const TextStyle(fontSize: 16)), content: TextField( controller: ctrl, autofocus: true, keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: const InputDecoration( suffixText: 'g', labelText: '份量', helperText: '营养值会跟着重算', ), ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), child: const Text('取消'), ), TextButton( onPressed: () { final value = double.tryParse(ctrl.text.trim()); Navigator.of(ctx).pop(value); }, child: const Text('确定'), ), ], ), ); ctrl.dispose(); if (grams == null) return; if (grams <= 0) { if (!mounted) return; showToast(context, '份量必须大于 0', error: true); return; } try { await ref.read(healthApiProvider).updateDietAmount(item.id, grams); if (!mounted) return; ref.invalidate(dietTodayProvider); ref.invalidate(dietTodayTotalsProvider); } catch (e) { if (!mounted) return; showToast(context, e.toString(), error: true); } } Future _delete(DietRecordItem item) async { try { await ref.read(healthApiProvider).deleteDiet(item.id); if (!mounted) return; ref.invalidate(dietTodayProvider); ref.invalidate(dietTodayTotalsProvider); showToast(context, '已删除', icon: 'check'); } catch (e) { if (!mounted) return; showToast(context, e.toString(), error: true); } } } // ================================================================== String _fmt(num value) { if (value == value.roundToDouble()) return value.round().toString(); return value.toStringAsFixed(1); }