1005 lines
37 KiB
Dart
1005 lines
37 KiB
Dart
/*
|
||
* 文件: weight_manage_page.dart
|
||
* 名称: 体重管理页面
|
||
* 作用: 记录体重、折线图趋势、目标设定、周期统计、BMI跳转
|
||
* 更新: 2026-04-16 初始创建
|
||
*/
|
||
|
||
import 'package:flutter/cupertino.dart';
|
||
import 'package:get/get.dart';
|
||
import 'package:mom_kitchen/src/config/design_tokens.dart';
|
||
import 'package:mom_kitchen/src/controllers/tools/weight_controller.dart';
|
||
import 'package:mom_kitchen/src/models/user/weight_record_model.dart';
|
||
import 'package:mom_kitchen/src/widgets/charts_widgets.dart';
|
||
|
||
class WeightManagePage extends GetView<WeightController> {
|
||
const WeightManagePage({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark;
|
||
|
||
return CupertinoPageScaffold(
|
||
backgroundColor: isDark
|
||
? DarkDesignTokens.background
|
||
: DesignTokens.background,
|
||
navigationBar: CupertinoNavigationBar(
|
||
middle: Text(
|
||
'⚖️ 体重管理',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontLg,
|
||
fontWeight: FontWeight.w600,
|
||
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
|
||
),
|
||
),
|
||
trailing: CupertinoButton(
|
||
padding: EdgeInsets.zero,
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: DesignTokens.space2,
|
||
vertical: DesignTokens.space1,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: DesignTokens.dynamicPrimary.withValues(alpha: 0.1),
|
||
borderRadius: DesignTokens.borderRadiusSm,
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(
|
||
CupertinoIcons.chart_bar,
|
||
size: 14,
|
||
color: DesignTokens.dynamicPrimary,
|
||
),
|
||
const SizedBox(width: 4),
|
||
Text(
|
||
'BMI',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontSm,
|
||
fontWeight: FontWeight.w600,
|
||
color: DesignTokens.dynamicPrimary,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
onPressed: () => Get.toNamed('/bmi-calculator'),
|
||
),
|
||
backgroundColor: isDark
|
||
? DarkDesignTokens.background.withValues(alpha: 0.9)
|
||
: DesignTokens.background.withValues(alpha: 0.9),
|
||
border: null,
|
||
),
|
||
child: SafeArea(
|
||
child: Obx(
|
||
() => SingleChildScrollView(
|
||
physics: const BouncingScrollPhysics(),
|
||
padding: const EdgeInsets.all(DesignTokens.space4),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
_buildStatCards(isDark),
|
||
const SizedBox(height: DesignTokens.space4),
|
||
_buildRecordForm(isDark),
|
||
const SizedBox(height: DesignTokens.space4),
|
||
_buildChartSection(isDark),
|
||
const SizedBox(height: DesignTokens.space4),
|
||
_buildStatsSection(isDark),
|
||
const SizedBox(height: DesignTokens.space4),
|
||
_buildGoalSection(isDark),
|
||
const SizedBox(height: DesignTokens.space4),
|
||
_buildHistoryList(isDark),
|
||
const SizedBox(height: DesignTokens.space6),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildStatCards(bool isDark) {
|
||
return Row(
|
||
children: [
|
||
Expanded(
|
||
child: _statCard(
|
||
'当前体重',
|
||
controller.currentWeight != null
|
||
? controller.displayWeight(controller.currentWeight!)
|
||
: '--',
|
||
isDark,
|
||
DesignTokens.dynamicPrimary,
|
||
),
|
||
),
|
||
const SizedBox(width: DesignTokens.space2),
|
||
Expanded(
|
||
child: _statCard(
|
||
'目标体重',
|
||
'${controller.goalWeight.value.toStringAsFixed(1)} kg',
|
||
isDark,
|
||
DesignTokens.orange,
|
||
),
|
||
),
|
||
const SizedBox(width: DesignTokens.space2),
|
||
Expanded(
|
||
child: Obx(() {
|
||
final change = controller.changeFromLast;
|
||
String label = '--';
|
||
Color color = DesignTokens.text3;
|
||
if (change != null) {
|
||
final absChange = change.abs();
|
||
label =
|
||
'${change > 0 ? '↑' : '↓'}${absChange.toStringAsFixed(1)}kg';
|
||
color = change > 0 ? DesignTokens.red : DesignTokens.green;
|
||
}
|
||
return _statCard('变化', label, isDark, color);
|
||
}),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _statCard(String title, String value, bool isDark, Color accentColor) {
|
||
return Container(
|
||
padding: const EdgeInsets.all(DesignTokens.space3),
|
||
decoration: BoxDecoration(
|
||
color: isDark ? DarkDesignTokens.card : DesignTokens.card,
|
||
borderRadius: DesignTokens.borderRadiusMd,
|
||
border: Border.all(color: accentColor.withValues(alpha: 0.2)),
|
||
),
|
||
child: Column(
|
||
children: [
|
||
Text(
|
||
title,
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontXs,
|
||
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
|
||
),
|
||
),
|
||
const SizedBox(height: DesignTokens.space1),
|
||
Text(
|
||
value,
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontLg,
|
||
fontWeight: FontWeight.w700,
|
||
color: accentColor,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildChartSection(bool isDark) {
|
||
return Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.all(DesignTokens.space4),
|
||
decoration: BoxDecoration(
|
||
color: isDark ? DarkDesignTokens.card : DesignTokens.card,
|
||
borderRadius: DesignTokens.borderRadiusLg,
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Text('📈 ', style: TextStyle(fontSize: DesignTokens.fontMd)),
|
||
Text(
|
||
'体重趋势',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontMd,
|
||
fontWeight: FontWeight.w600,
|
||
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
|
||
),
|
||
),
|
||
const Spacer(),
|
||
Obx(
|
||
() => GestureDetector(
|
||
onTap: () => controller.toggleUnit(),
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: DesignTokens.space2,
|
||
vertical: DesignTokens.space1,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: DesignTokens.dynamicPrimary.withValues(alpha: 0.1),
|
||
borderRadius: DesignTokens.borderRadiusFull,
|
||
),
|
||
child: Text(
|
||
controller.unitMode.value == 'kg' ? 'kg ⇄ 斤' : '斤 ⇄ kg',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontXs,
|
||
fontWeight: FontWeight.w600,
|
||
color: DesignTokens.dynamicPrimary,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: DesignTokens.space3),
|
||
Obx(
|
||
() => WeightLineChart(
|
||
data: controller.chartData,
|
||
goalValue: controller.goalWeight.value,
|
||
isDark: isDark,
|
||
unitLabel: controller.unitMode.value == 'kg' ? 'kg' : '斤',
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildStatsSection(bool isDark) {
|
||
return Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.all(DesignTokens.space4),
|
||
decoration: BoxDecoration(
|
||
color: isDark ? DarkDesignTokens.card : DesignTokens.card,
|
||
borderRadius: DesignTokens.borderRadiusLg,
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'📊 趋势分析',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontMd,
|
||
fontWeight: FontWeight.w600,
|
||
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
|
||
),
|
||
),
|
||
const SizedBox(height: DesignTokens.space3),
|
||
Obx(() {
|
||
final weekly = controller.getWeeklyStats();
|
||
final monthly = controller.getMonthlyStats();
|
||
return _statsGrid(weekly, monthly, isDark);
|
||
}),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _statsGrid(
|
||
Map<String, dynamic> weekly,
|
||
Map<String, dynamic> monthly,
|
||
bool isDark,
|
||
) {
|
||
final items = [
|
||
_StatsItem('本周平均', weekly['avg'], 'kg'),
|
||
_StatsItem('本周最高', weekly['max'], 'kg'),
|
||
_StatsItem('本周最低', weekly['min'], 'kg'),
|
||
_StatsItem('本周变化', weekly['change'], 'kg', showSign: true),
|
||
_StatsItem('本月平均', monthly['avg'], 'kg'),
|
||
_StatsItem('本月最高', monthly['max'], 'kg'),
|
||
_StatsItem('本月最低', monthly['min'], 'kg'),
|
||
_StatsItem('本月变化', monthly['change'], 'kg', showSign: true),
|
||
];
|
||
return Wrap(
|
||
spacing: DesignTokens.space2,
|
||
runSpacing: DesignTokens.space2,
|
||
children: items.map((item) {
|
||
final val = item.value;
|
||
final text = item.showSign
|
||
? (val >= 0 ? '+$val' : '$val')
|
||
: val.toStringAsFixed(1);
|
||
final color = item.showSign
|
||
? (val <= 0 ? DesignTokens.green : DesignTokens.red)
|
||
: (isDark ? DarkDesignTokens.text1 : DesignTokens.text1);
|
||
return SizedBox(
|
||
width:
|
||
(Get.width - DesignTokens.space4 * 2 - DesignTokens.space2 * 3) /
|
||
4,
|
||
child: Container(
|
||
padding: const EdgeInsets.all(DesignTokens.space2),
|
||
decoration: BoxDecoration(
|
||
color: isDark
|
||
? DarkDesignTokens.glass
|
||
: DesignTokens.text3.withValues(alpha: 0.04),
|
||
borderRadius: DesignTokens.borderRadiusSm,
|
||
),
|
||
child: Column(
|
||
children: [
|
||
Text(
|
||
item.label,
|
||
style: TextStyle(
|
||
fontSize: 10,
|
||
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
|
||
),
|
||
),
|
||
const SizedBox(height: 2),
|
||
Text(
|
||
'$text ${item.unit}',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontSm,
|
||
fontWeight: FontWeight.w700,
|
||
color: color,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}).toList(),
|
||
);
|
||
}
|
||
|
||
Widget _buildGoalSection(bool isDark) {
|
||
return Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.all(DesignTokens.space4),
|
||
decoration: BoxDecoration(
|
||
color: isDark ? DarkDesignTokens.card : DesignTokens.card,
|
||
borderRadius: DesignTokens.borderRadiusLg,
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Text('⚙️ ', style: TextStyle(fontSize: DesignTokens.fontMd)),
|
||
Text(
|
||
'目标体重',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontMd,
|
||
fontWeight: FontWeight.w600,
|
||
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
|
||
),
|
||
),
|
||
const Spacer(),
|
||
Obx(
|
||
() => Text(
|
||
'${controller.goalWeight.value.toStringAsFixed(1)} kg',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontMd,
|
||
fontWeight: FontWeight.w700,
|
||
color: DesignTokens.orange,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: DesignTokens.space3),
|
||
Obx(
|
||
() => CupertinoSlider(
|
||
value: controller.goalWeight.value,
|
||
min: 30,
|
||
max: 200,
|
||
divisions: 170,
|
||
onChanged: (v) => controller.updateGoal(v),
|
||
),
|
||
),
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: DesignTokens.space2,
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text(
|
||
'30kg',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontXs,
|
||
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
|
||
),
|
||
),
|
||
Text(
|
||
'200kg',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontXs,
|
||
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildRecordForm(bool isDark) {
|
||
return Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.all(DesignTokens.space4),
|
||
decoration: BoxDecoration(
|
||
gradient: LinearGradient(
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
colors: [
|
||
DesignTokens.dynamicPrimary.withValues(alpha: 0.08),
|
||
DesignTokens.secondary.withValues(alpha: 0.06),
|
||
],
|
||
),
|
||
borderRadius: DesignTokens.borderRadiusLg,
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'➕ 记录体重',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontMd,
|
||
fontWeight: FontWeight.w600,
|
||
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
|
||
),
|
||
),
|
||
const SizedBox(height: DesignTokens.space3),
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
flex: 3,
|
||
child: Container(
|
||
height: 44,
|
||
decoration: BoxDecoration(
|
||
color: isDark
|
||
? DarkDesignTokens.glass
|
||
: CupertinoColors.white,
|
||
borderRadius: DesignTokens.borderRadiusMd,
|
||
border: Border.all(
|
||
color: isDark
|
||
? DarkDesignTokens.glassBorder
|
||
: DesignTokens.text3.withValues(alpha: 0.15),
|
||
),
|
||
),
|
||
child: CupertinoTextField(
|
||
controller: controller.weightInput,
|
||
placeholder: '输入体重',
|
||
keyboardType: const TextInputType.numberWithOptions(
|
||
decimal: true,
|
||
),
|
||
placeholderStyle: TextStyle(
|
||
fontSize: DesignTokens.fontMd,
|
||
color: isDark
|
||
? DarkDesignTokens.text3
|
||
: DesignTokens.text3,
|
||
),
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontMd,
|
||
color: isDark
|
||
? DarkDesignTokens.text1
|
||
: DesignTokens.text1,
|
||
),
|
||
decoration: null,
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: DesignTokens.space2),
|
||
Expanded(
|
||
child: Obx(
|
||
() => GestureDetector(
|
||
onTap: () {
|
||
if (controller.unitMode.value != 'kg') {
|
||
controller.toggleUnit();
|
||
}
|
||
},
|
||
child: Container(
|
||
height: 44,
|
||
decoration: BoxDecoration(
|
||
color: controller.unitMode.value == 'kg'
|
||
? DesignTokens.dynamicPrimary
|
||
: (isDark
|
||
? DarkDesignTokens.glass
|
||
: CupertinoColors.white),
|
||
borderRadius: DesignTokens.borderRadiusMd,
|
||
border: controller.unitMode.value != 'kg'
|
||
? Border.all(
|
||
color: isDark
|
||
? DarkDesignTokens.glassBorder
|
||
: DesignTokens.text3.withValues(
|
||
alpha: 0.15,
|
||
),
|
||
)
|
||
: null,
|
||
),
|
||
child: Center(
|
||
child: Text(
|
||
'kg',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontSm,
|
||
fontWeight: FontWeight.w600,
|
||
color: controller.unitMode.value == 'kg'
|
||
? CupertinoColors.white
|
||
: (isDark
|
||
? DarkDesignTokens.text2
|
||
: DesignTokens.text2),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: DesignTokens.space2),
|
||
Expanded(
|
||
child: Obx(
|
||
() => GestureDetector(
|
||
onTap: () {
|
||
if (controller.unitMode.value != 'jin') {
|
||
controller.toggleUnit();
|
||
}
|
||
},
|
||
child: Container(
|
||
height: 44,
|
||
decoration: BoxDecoration(
|
||
color: controller.unitMode.value == 'jin'
|
||
? DesignTokens.dynamicPrimary
|
||
: (isDark
|
||
? DarkDesignTokens.glass
|
||
: CupertinoColors.white),
|
||
borderRadius: DesignTokens.borderRadiusMd,
|
||
border: controller.unitMode.value != 'jin'
|
||
? Border.all(
|
||
color: isDark
|
||
? DarkDesignTokens.glassBorder
|
||
: DesignTokens.text3.withValues(
|
||
alpha: 0.15,
|
||
),
|
||
)
|
||
: null,
|
||
),
|
||
child: Center(
|
||
child: Text(
|
||
'斤',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontSm,
|
||
fontWeight: FontWeight.w600,
|
||
color: controller.unitMode.value == 'jin'
|
||
? CupertinoColors.white
|
||
: (isDark
|
||
? DarkDesignTokens.text2
|
||
: DesignTokens.text2),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: DesignTokens.space3),
|
||
Obx(
|
||
() => CupertinoSlidingSegmentedControl<String>(
|
||
groupValue: controller.selectedTiming.value,
|
||
children: {
|
||
'morning': Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||
child: Text('🌅早晨'),
|
||
),
|
||
'before_meal': Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||
child: Text('🍽️饭前'),
|
||
),
|
||
'after_meal': Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||
child: Text('😋饭后'),
|
||
),
|
||
},
|
||
onValueChanged: (v) => controller.selectedTiming.value = v!,
|
||
),
|
||
),
|
||
const SizedBox(height: DesignTokens.space3),
|
||
Container(
|
||
height: 44,
|
||
decoration: BoxDecoration(
|
||
color: isDark ? DarkDesignTokens.glass : CupertinoColors.white,
|
||
borderRadius: DesignTokens.borderRadiusMd,
|
||
border: Border.all(
|
||
color: isDark
|
||
? DarkDesignTokens.glassBorder
|
||
: DesignTokens.text3.withValues(alpha: 0.15),
|
||
),
|
||
),
|
||
child: CupertinoTextField(
|
||
controller: controller.noteInput,
|
||
placeholder: '备注信息(可选)',
|
||
placeholderStyle: TextStyle(
|
||
fontSize: DesignTokens.fontMd,
|
||
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
|
||
),
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontMd,
|
||
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
|
||
),
|
||
decoration: null,
|
||
),
|
||
),
|
||
const SizedBox(height: DesignTokens.space3),
|
||
SizedBox(
|
||
width: double.infinity,
|
||
height: 44,
|
||
child: CupertinoButton.filled(
|
||
borderRadius: DesignTokens.borderRadiusMd,
|
||
onPressed: () => controller.addRecord(),
|
||
child: const Text('💾 保存记录'),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildHistoryList(bool isDark) {
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Text(
|
||
'📋 历史记录',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontMd,
|
||
fontWeight: FontWeight.w600,
|
||
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
|
||
),
|
||
),
|
||
const Spacer(),
|
||
Obx(
|
||
() => Text(
|
||
'${controller.records.length} 条',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontXs,
|
||
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: DesignTokens.space2),
|
||
Obx(() {
|
||
if (controller.records.isEmpty) {
|
||
return Center(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(DesignTokens.space6),
|
||
child: Column(
|
||
children: [
|
||
const Text('📝', style: TextStyle(fontSize: 40)),
|
||
const SizedBox(height: DesignTokens.space2),
|
||
Text(
|
||
'还没有记录',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontMd,
|
||
color: isDark
|
||
? DarkDesignTokens.text2
|
||
: DesignTokens.text2,
|
||
),
|
||
),
|
||
Text(
|
||
'添加第一条体重记录开始追踪',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontSm,
|
||
color: isDark
|
||
? DarkDesignTokens.text3
|
||
: DesignTokens.text3,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
return Column(
|
||
children: controller.sortedRecords.take(20).map((record) {
|
||
return Dismissible(
|
||
key: ValueKey(record.id),
|
||
direction: DismissDirection.endToStart,
|
||
background: Container(
|
||
alignment: Alignment.centerLeft,
|
||
padding: const EdgeInsets.only(left: DesignTokens.space4),
|
||
decoration: BoxDecoration(
|
||
color: DesignTokens.red,
|
||
borderRadius: DesignTokens.borderRadiusMd,
|
||
),
|
||
child: const Icon(
|
||
CupertinoIcons.trash,
|
||
color: CupertinoColors.white,
|
||
),
|
||
),
|
||
confirmDismiss: (direction) async {
|
||
return await showCupertinoDialog<bool>(
|
||
context: Get.context!,
|
||
builder: (ctx) => CupertinoAlertDialog(
|
||
title: const Text('确认删除'),
|
||
content: Text(
|
||
'删除 ${record.createdAt.toString().substring(5, 16)} 的记录?',
|
||
),
|
||
actions: [
|
||
CupertinoDialogAction(
|
||
child: const Text('取消'),
|
||
onPressed: () => Navigator.pop(ctx, false),
|
||
),
|
||
CupertinoDialogAction(
|
||
isDestructiveAction: true,
|
||
child: const Text('删除'),
|
||
onPressed: () => Navigator.pop(ctx, true),
|
||
),
|
||
],
|
||
),
|
||
) ??
|
||
false;
|
||
},
|
||
onDismissed: (_) => controller.deleteRecord(record.id),
|
||
child: GestureDetector(
|
||
onTap: () => _showEditDialog(Get.context!, record, isDark),
|
||
child: Container(
|
||
margin: const EdgeInsets.only(bottom: DesignTokens.space2),
|
||
padding: const EdgeInsets.all(DesignTokens.space3),
|
||
decoration: BoxDecoration(
|
||
color: isDark ? DarkDesignTokens.card : DesignTokens.card,
|
||
borderRadius: DesignTokens.borderRadiusMd,
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Text(
|
||
WeightRecord.timingEmoji[record.timing] ?? '📝',
|
||
style: const TextStyle(fontSize: 20),
|
||
),
|
||
const SizedBox(width: DesignTokens.space2),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
record.createdAt.toString().substring(0, 16),
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontXs,
|
||
color: isDark
|
||
? DarkDesignTokens.text3
|
||
: DesignTokens.text3,
|
||
),
|
||
),
|
||
Text(
|
||
WeightRecord.timingDisplay[record.timing] ?? '',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontSm,
|
||
color: isDark
|
||
? DarkDesignTokens.text2
|
||
: DesignTokens.text2,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
Text(
|
||
controller.displayWeight(record.weightKg),
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontLg,
|
||
fontWeight: FontWeight.w700,
|
||
color: isDark
|
||
? DarkDesignTokens.text1
|
||
: DesignTokens.text1,
|
||
),
|
||
),
|
||
const SizedBox(width: DesignTokens.space1),
|
||
Icon(
|
||
CupertinoIcons.pencil_outline,
|
||
size: 14,
|
||
color: isDark
|
||
? DarkDesignTokens.text3
|
||
: DesignTokens.text3,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}).toList(),
|
||
);
|
||
}),
|
||
],
|
||
);
|
||
}
|
||
|
||
void _showEditDialog(BuildContext context, WeightRecord record, bool isDark) {
|
||
final weightCtrl = TextEditingController(
|
||
text: controller.unitMode.value == 'kg'
|
||
? record.weightKg.toStringAsFixed(1)
|
||
: WeightRecord.kgToJin(record.weightKg).toStringAsFixed(1),
|
||
);
|
||
final noteCtrl = TextEditingController(text: record.note ?? '');
|
||
String editTiming = record.timing;
|
||
String editUnit = controller.unitMode.value;
|
||
|
||
showCupertinoDialog(
|
||
context: context,
|
||
builder: (ctx) => StatefulBuilder(
|
||
builder: (context, setDialogState) {
|
||
return CupertinoAlertDialog(
|
||
title: const Text('✏️ 编辑记录'),
|
||
content: Container(
|
||
width: 280,
|
||
constraints: const BoxConstraints(maxHeight: 320),
|
||
child: SingleChildScrollView(
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
flex: 3,
|
||
child: Container(
|
||
height: 36,
|
||
decoration: BoxDecoration(
|
||
color: isDark
|
||
? DarkDesignTokens.glass
|
||
: CupertinoColors.white,
|
||
borderRadius: DesignTokens.borderRadiusSm,
|
||
border: Border.all(
|
||
color: DesignTokens.dynamicPrimary.withValues(
|
||
alpha: 0.3,
|
||
),
|
||
),
|
||
),
|
||
child: CupertinoTextField(
|
||
controller: weightCtrl,
|
||
keyboardType:
|
||
const TextInputType.numberWithOptions(
|
||
decimal: true,
|
||
),
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontSm,
|
||
color: isDark
|
||
? DarkDesignTokens.text1
|
||
: DesignTokens.text1,
|
||
),
|
||
decoration: null,
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: DesignTokens.space2),
|
||
GestureDetector(
|
||
onTap: () {
|
||
setDialogState(() {
|
||
final val = double.tryParse(weightCtrl.text) ?? 0;
|
||
if (editUnit == 'kg') {
|
||
weightCtrl.text = WeightRecord.kgToJin(
|
||
val,
|
||
).toStringAsFixed(1);
|
||
editUnit = 'jin';
|
||
} else {
|
||
weightCtrl.text = WeightRecord.jinToKg(
|
||
val,
|
||
).toStringAsFixed(1);
|
||
editUnit = 'kg';
|
||
}
|
||
});
|
||
},
|
||
child: Container(
|
||
height: 36,
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: DesignTokens.space2,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: DesignTokens.dynamicPrimary.withValues(
|
||
alpha: 0.1,
|
||
),
|
||
borderRadius: DesignTokens.borderRadiusSm,
|
||
),
|
||
child: Center(
|
||
child: Text(
|
||
editUnit.toUpperCase(),
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontXs,
|
||
fontWeight: FontWeight.w600,
|
||
color: DesignTokens.dynamicPrimary,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: DesignTokens.space2),
|
||
CupertinoSlidingSegmentedControl<String>(
|
||
groupValue: editTiming,
|
||
children: {
|
||
'morning': Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||
child: Text(
|
||
'🌅早晨',
|
||
style: const TextStyle(fontSize: 12),
|
||
),
|
||
),
|
||
'before_meal': Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||
child: Text(
|
||
'🍽️饭前',
|
||
style: const TextStyle(fontSize: 12),
|
||
),
|
||
),
|
||
'after_meal': Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||
child: Text(
|
||
'😋饭后',
|
||
style: const TextStyle(fontSize: 12),
|
||
),
|
||
),
|
||
},
|
||
onValueChanged: (v) {
|
||
setDialogState(() {
|
||
editTiming = v!;
|
||
});
|
||
},
|
||
),
|
||
const SizedBox(height: DesignTokens.space2),
|
||
Container(
|
||
height: 36,
|
||
decoration: BoxDecoration(
|
||
color: isDark
|
||
? DarkDesignTokens.glass
|
||
: CupertinoColors.white,
|
||
borderRadius: DesignTokens.borderRadiusSm,
|
||
border: Border.all(
|
||
color: DesignTokens.text3.withValues(alpha: 0.15),
|
||
),
|
||
),
|
||
child: CupertinoTextField(
|
||
controller: noteCtrl,
|
||
placeholder: '备注(可选)',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontSm,
|
||
color: isDark
|
||
? DarkDesignTokens.text1
|
||
: DesignTokens.text1,
|
||
),
|
||
decoration: null,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
actions: [
|
||
CupertinoDialogAction(
|
||
child: const Text('取消'),
|
||
onPressed: () {
|
||
weightCtrl.dispose();
|
||
noteCtrl.dispose();
|
||
Navigator.pop(ctx);
|
||
},
|
||
),
|
||
CupertinoDialogAction(
|
||
isDefaultAction: true,
|
||
child: const Text('保存'),
|
||
onPressed: () {
|
||
final w = double.tryParse(weightCtrl.text);
|
||
if (w == null || w <= 0 || w > 500) {
|
||
Navigator.pop(ctx);
|
||
weightCtrl.dispose();
|
||
noteCtrl.dispose();
|
||
return;
|
||
}
|
||
final kg = editUnit == 'jin' ? WeightRecord.jinToKg(w) : w;
|
||
controller.updateRecord(
|
||
record.id,
|
||
weightKg: kg,
|
||
timing: editTiming,
|
||
note: noteCtrl.text.trim().isEmpty
|
||
? null
|
||
: noteCtrl.text.trim(),
|
||
);
|
||
weightCtrl.dispose();
|
||
noteCtrl.dispose();
|
||
Navigator.pop(ctx);
|
||
},
|
||
),
|
||
],
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _StatsItem {
|
||
final String label;
|
||
final double value;
|
||
final String unit;
|
||
final bool showSign;
|
||
|
||
const _StatsItem(this.label, this.value, this.unit, {this.showSign = false});
|
||
}
|