import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../api/health_api.dart'; import '../../providers/health_provider.dart'; import '../../utils/toast.dart'; /// 设置减重目标 + 展示「每周该减多少」。 /// /// 这个页面的核心价值是第二块:把「三个月减 7kg」翻译成「每周 0.54kg」—— /// 前者没有可执行性,后者才有。 class GoalSetupPage extends ConsumerStatefulWidget { const GoalSetupPage({super.key}); @override ConsumerState createState() => _GoalSetupPageState(); } class _GoalSetupPageState extends ConsumerState { final _startCtrl = TextEditingController(); final _targetCtrl = TextEditingController(); DateTime _startDate = DateTime.now(); DateTime _targetDate = DateTime.now().add(const Duration(days: 90)); bool _saving = false; bool _loaded = false; @override void initState() { super.initState(); // 已有目标就预填,让「想改一改」不用重敲 Future.microtask(() async { try { final goal = await ref.read(healthApiProvider).goal(); if (!mounted) return; if (goal.hasGoal) { setState(() { _startCtrl.text = _num(goal.startWeight); _targetCtrl.text = _num(goal.targetWeight); _startDate = goal.startDate ?? _startDate; _targetDate = goal.targetDate ?? _targetDate; }); } else { // 没设过目标时,用最近一次体重当起始值 final chart = await ref.read(healthApiProvider).weightChart(); if (!mounted) return; final current = chart.currentWeight; if (current != null) { setState(() => _startCtrl.text = _num(current)); } } } catch (_) { // 拿不到就不预填,用户手填即可 } finally { if (mounted) setState(() => _loaded = true); } }); } @override void dispose() { _startCtrl.dispose(); _targetCtrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFF7F4F1), appBar: AppBar( title: const Text('减重目标'), backgroundColor: const Color(0xFFF7F4F1), elevation: 0, scrolledUnderElevation: 0, foregroundColor: const Color(0xFF5D4037), ), body: !_loaded ? const Center( child: CircularProgressIndicator(color: Color(0xFFC8956E)), ) : ListView( padding: const EdgeInsets.fromLTRB(14, 6, 14, 24), children: [ _formCard(), const SizedBox(height: 12), _planCard(), const SizedBox(height: 12), _weeklyTableCard(), const SizedBox(height: 20), _saveButton(), ], ), ); } // ---------------- 输入区 ---------------- Widget _formCard() { return Container( padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), ), child: Column( children: [ Row( children: [ Expanded( child: _numberField( controller: _startCtrl, label: '起始体重', hint: '65.0', ), ), const SizedBox(width: 14), Expanded( child: _numberField( controller: _targetCtrl, label: '目标体重', hint: '58.0', ), ), ], ), const SizedBox(height: 4), _dateRow('开始日期', _startDate, (d) => setState(() => _startDate = d)), _dateRow('目标日期', _targetDate, (d) => setState(() => _targetDate = d)), const SizedBox(height: 4), ], ), ); } Widget _numberField({ required TextEditingController controller, required String label, required String hint, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: const TextStyle(fontSize: 12, color: Color(0xFF8D6E63))), const SizedBox(height: 4), TextField( controller: controller, keyboardType: const TextInputType.numberWithOptions(decimal: true), onChanged: (_) => setState(() {}), style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Color(0xFF5D4037)), decoration: InputDecoration( hintText: hint, hintStyle: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Color(0xFFD7CCC8)), suffixText: 'kg', suffixStyle: const TextStyle(fontSize: 12, color: Color(0xFF8D6E63)), isDense: true, contentPadding: const EdgeInsets.symmetric(vertical: 6), enabledBorder: const UnderlineInputBorder( borderSide: BorderSide(color: Color(0xFFE0D5CC)), ), focusedBorder: const UnderlineInputBorder( borderSide: BorderSide(color: Color(0xFFC8956E), width: 1.6), ), ), ), ], ); } Widget _dateRow(String label, DateTime value, ValueChanged onPick) { return InkWell( onTap: () => _pickDate(value, onPick), child: Padding( padding: const EdgeInsets.symmetric(vertical: 13), child: Row( children: [ Text(label, style: const TextStyle( fontSize: 13, color: Color(0xFF8D6E63))), const Spacer(), Text( _dateText(value), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF5D4037), ), ), const SizedBox(width: 4), const Icon(Icons.chevron_right, size: 18, color: Color(0xFFBCAAA4)), ], ), ), ); } Future _pickDate( DateTime initial, ValueChanged onPick) async { final picked = await showDatePicker( context: context, initialDate: initial, firstDate: DateTime(2020), lastDate: DateTime(2035), ); if (picked != null) onPick(picked); } // ---------------- 计划预览 ---------------- /// 本地先算一遍计划,让用户改数字时立刻看到「每周该减多少」, /// 不用等保存。算法和后端一致(总周数向上取整)。 ({int weeks, double weeklyLoss, bool valid}) _computePlan() { final start = double.tryParse(_startCtrl.text.trim()); final target = double.tryParse(_targetCtrl.text.trim()); if (start == null || target == null || start <= target) { return (weeks: 0, weeklyLoss: 0, valid: false); } final days = _targetDate.difference(_startDate).inDays; if (days <= 0) return (weeks: 0, weeklyLoss: 0, valid: false); final weeks = (days / 7).ceil(); final weekly = (start - target) / weeks; return (weeks: weeks, weeklyLoss: weekly, valid: true); } Widget _planCard() { final plan = _computePlan(); return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), ), child: !plan.valid ? const Text( '填好起始体重和目标体重\n(目标要低于起始)就能算出每周该减多少', style: TextStyle( fontSize: 13, height: 1.7, color: Color(0xFFBCAAA4)), ) : Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('按这个目标', style: TextStyle( fontSize: 12, color: Color(0xFF8D6E63))), const SizedBox(height: 8), Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Text( '每周 ${plan.weeklyLoss.toStringAsFixed(2)}', style: const TextStyle( fontSize: 26, fontWeight: FontWeight.bold, color: Color(0xFFC8956E), ), ), const SizedBox(width: 4), const Text('kg', style: TextStyle( fontSize: 14, color: Color(0xFF8D6E63))), const Spacer(), Text( '共 ${plan.weeks} 周', style: const TextStyle( fontSize: 12.5, color: Color(0xFF8D6E63)), ), ], ), if (plan.weeklyLoss > 1) ...[ const SizedBox(height: 12), Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 8), decoration: BoxDecoration( color: const Color(0xFFF8ECD2), borderRadius: BorderRadius.circular(8), ), child: const Row( children: [ Icon(Icons.info_outline, size: 14, color: Color(0xFFD9A93C)), SizedBox(width: 6), Expanded( child: Text( '每周超过 1kg 通常掉的是肌肉和水分,' '建议把目标日期往后放一放', style: TextStyle( fontSize: 11.5, height: 1.5, color: Color(0xFF8D6E63)), ), ), ], ), ), ], ], ), ); } /// 每周目标体重表 —— 让「每周 0.54kg」变成一串能对照的数字 Widget _weeklyTableCard() { final plan = _computePlan(); if (!plan.valid) return const SizedBox.shrink(); final start = double.parse(_startCtrl.text.trim()); return Container( padding: const EdgeInsets.fromLTRB(16, 16, 16, 6), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '每周目标体重', style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF5D4037), ), ), const SizedBox(height: 4), const Text( '对照着看,就能知道自己落后了还是超前了', style: TextStyle(fontSize: 11.5, color: Color(0xFFBCAAA4)), ), const SizedBox(height: 10), // 只列前 8 周和最后一周:全列出来(13 周以上)会很长且没人看 ..._weekRows(start, plan.weeks, plan.weeklyLoss), ], ), ); } List _weekRows(double start, int weeks, double weekly) { final rows = []; final showAll = weeks <= 8; final indices = showAll ? List.generate(weeks, (i) => i + 1) : [1, 2, 3, 4, 5, 6, 7, 8, weeks]; int? previous; for (final week in indices) { // 中间省略时插一行省略号 if (previous != null && week != previous + 1) { rows.add(const Padding( padding: EdgeInsets.symmetric(vertical: 6), child: Center( child: Text('···', style: TextStyle(color: Color(0xFFBCAAA4), fontSize: 13)), ), )); } previous = week; final ideal = start - weekly * week; rows.add( Padding( padding: const EdgeInsets.only(bottom: 9), child: Row( children: [ SizedBox( width: 62, child: Text( '第 $week 周', style: const TextStyle( fontSize: 12.5, color: Color(0xFF8D6E63)), ), ), Expanded( child: Text( '${ideal.toStringAsFixed(1)} kg', style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF5D4037), ), ), ), Text( '累计减 ${(weekly * week).toStringAsFixed(1)} kg', style: const TextStyle( fontSize: 11.5, color: Color(0xFFBCAAA4)), ), ], ), ), ); } return rows; } // ---------------- 保存 ---------------- Widget _saveButton() { return SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _saving ? null : _save, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFC8956E), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(24), ), ), child: Text( _saving ? '保存中…' : '保存目标', style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600), ), ), ); } Future _save() async { final start = double.tryParse(_startCtrl.text.trim()); final target = double.tryParse(_targetCtrl.text.trim()); if (start == null || target == null) { showToast(context, '请填写起始体重和目标体重', error: true); return; } if (start < 20 || start > 300 || target < 20 || target > 300) { showToast(context, '体重需在 20 ~ 300 kg 之间', error: true); return; } if (target >= start) { showToast(context, '目标体重要低于起始体重', error: true); return; } if (!_targetDate.isAfter(_startDate)) { showToast(context, '目标日期要晚于开始日期', error: true); return; } setState(() => _saving = true); try { await ref.read(healthApiProvider).saveGoal( startWeight: start, targetWeight: target, startDate: _startDate, targetDate: _targetDate, ); if (!mounted) return; ref.invalidate(weightGoalProvider); ref.invalidate(weightChartProvider); showToast(context, '目标已保存', icon: 'check'); Navigator.of(context).pop(); } catch (e) { if (!mounted) return; showToast(context, e.toString(), error: true); } finally { if (mounted) setState(() => _saving = false); } } } String _num(num value) { if (value == value.roundToDouble()) return value.round().toString(); return value.toStringAsFixed(1); } String _dateText(DateTime date) { final m = date.month.toString().padLeft(2, '0'); final d = date.day.toString().padLeft(2, '0'); return '${date.year}-$m-$d'; }