Files
Aq-Accounting-Flutter/lib/models/health.dart
T
2026-09-28 16:40:32 +08:00

429 lines
12 KiB
Dart

import 'json_util.dart';
/// 「强宝健康」模块的数据模型。
///
/// 后端接口一律给 kcal(换算在服务端做掉了),所以这里不会再出现千焦 ——
/// 前端只有一个能量口径。
// ==================================================================
// 体重
// ==================================================================
/// 一个体重数据点(折线图用)
class WeightPoint {
const WeightPoint({required this.date, required this.weight});
final DateTime date;
final double weight;
factory WeightPoint.fromJson(Map<String, dynamic> json) {
return WeightPoint(
date: tryParse(json['date']) ?? DateTime.now(),
weight: asDouble(json['weight']),
);
}
static DateTime? tryParse(Object? value) {
final text = asString(value).trim();
if (text.isEmpty) return null;
return DateTime.tryParse(text);
}
}
/// 体重折线图数据
class WeightChart {
const WeightChart({
required this.points,
required this.currentWeight,
required this.delta,
required this.maxWeight,
required this.minWeight,
});
final List<WeightPoint> points;
/// 最近一次体重,没记过就是 null
final double? currentWeight;
/// 与上一次记录的差值。正数 = 涨了。只有一个点时是 null。
final double? delta;
/// Y 轴范围(后端已把目标体重也算进来,避免理想线画在可见区外)
final double? maxWeight;
final double? minWeight;
static const empty = WeightChart(
points: [],
currentWeight: null,
delta: null,
maxWeight: null,
minWeight: null,
);
bool get hasData => points.isNotEmpty;
factory WeightChart.fromJson(Map<String, dynamic> json) {
return WeightChart(
points: asMapList(json['points']).map(WeightPoint.fromJson).toList(),
currentWeight: asNullableDouble(json['currentWeight']),
delta: asNullableDouble(json['delta']),
maxWeight: asNullableDouble(json['maxWeight']),
minWeight: asNullableDouble(json['minWeight']),
);
}
}
/// 一条体重记录(历史列表用)
class WeightRecordItem {
const WeightRecordItem({
required this.id,
required this.date,
required this.weight,
required this.note,
});
final int id;
final DateTime? date;
final double weight;
final String note;
factory WeightRecordItem.fromJson(Map<String, dynamic> json) {
return WeightRecordItem(
id: asInt(json['id']),
date: WeightPoint.tryParse(json['recordDate']),
weight: asDouble(json['weight']),
note: asString(json['note']),
);
}
}
// ==================================================================
// 减重目标
// ==================================================================
/// 减重目标 + 由它推出来的一整套「每周该减多少」
class WeightGoal {
const WeightGoal({
required this.hasGoal,
required this.startWeight,
required this.targetWeight,
required this.startDate,
required this.targetDate,
required this.totalWeeks,
required this.weeklyLoss,
required this.weeklyLossUnhealthy,
required this.idealCurve,
required this.currentWeight,
required this.lostWeight,
required this.remainWeight,
required this.weightProgress,
required this.timeProgress,
required this.id,
});
final bool hasGoal;
final int? id;
final double startWeight;
final double targetWeight;
final DateTime? startDate;
final DateTime? targetDate;
/// 总周数(后端向上取整)
final int totalWeeks;
/// 每周应减 kg
final double weeklyLoss;
/// 每周超过 1kg,界面上要提示「掉太快」
final bool weeklyLossUnhealthy;
/// 理想体重曲线(每周一个点),画在体重折线图上当参照
final List<WeightPoint> idealCurve;
final double? currentWeight;
/// 已减 / 还差。没记过体重时是 null。
final double? lostWeight;
final double? remainWeight;
/// 0.0 ~ 1.0
final double weightProgress;
final double timeProgress;
static const none = WeightGoal(
hasGoal: false,
startWeight: 0,
targetWeight: 0,
startDate: null,
targetDate: null,
totalWeeks: 0,
weeklyLoss: 0,
weeklyLossUnhealthy: false,
idealCurve: [],
currentWeight: null,
lostWeight: null,
remainWeight: null,
weightProgress: 0,
timeProgress: 0,
id: null,
);
factory WeightGoal.fromJson(Map<String, dynamic> json) {
if (json['hasGoal'] != true) return none;
return WeightGoal(
hasGoal: true,
id: asNullableInt(json['id']),
startWeight: asDouble(json['startWeight']),
targetWeight: asDouble(json['targetWeight']),
startDate: WeightPoint.tryParse(json['startDate']),
targetDate: WeightPoint.tryParse(json['targetDate']),
totalWeeks: asInt(json['totalWeeks']),
weeklyLoss: asDouble(json['weeklyLoss']),
weeklyLossUnhealthy: json['weeklyLossUnhealthy'] == true,
idealCurve:
asMapList(json['idealCurve']).map(WeightPoint.fromJson).toList(),
currentWeight: asNullableDouble(json['currentWeight']),
lostWeight: asNullableDouble(json['lostWeight']),
remainWeight: asNullableDouble(json['remainWeight']),
weightProgress: asDouble(json['weightProgress']),
timeProgress: asDouble(json['timeProgress']),
);
}
}
// ==================================================================
// 食物 + 饮食
// ==================================================================
/// 一个份量档位,如「一碗 150g」
class FoodPortion {
const FoodPortion({required this.label, required this.grams});
final String label;
final int grams;
factory FoodPortion.fromJson(Map<String, dynamic> json) {
return FoodPortion(
label: asString(json['label']),
grams: asInt(json['grams']),
);
}
}
/// 一条食物(营养值都是「每 100g」)
class FoodItem {
const FoodItem({
required this.id,
required this.name,
required this.alias,
required this.categoryName,
required this.subCategoryName,
required this.ediblePercent,
required this.energyKcal,
required this.protein,
required this.fat,
required this.carbohydrate,
required this.dietaryFiber,
required this.sodium,
required this.portions,
});
final int id;
final String name;
final String alias;
final String categoryName;
final String subCategoryName;
/// 食部 %。展示用参考,不参与热量换算。
final double? ediblePercent;
/// 每 100g 的能量 kcal(后端已经把 kJ 换掉了)
final double? energyKcal;
final double? protein;
final double? fat;
final double? carbohydrate;
final double? dietaryFiber;
final double? sodium;
/// 份量档位。后端保证非空。
final List<FoodPortion> portions;
factory FoodItem.fromJson(Map<String, dynamic> json) {
return FoodItem(
id: asInt(json['id']),
name: asNonEmpty(json['name'], '未命名食物'),
alias: asString(json['alias']),
categoryName: asString(json['categoryName']),
subCategoryName: asString(json['subCategoryName']),
ediblePercent: asNullableDouble(json['ediblePercent']),
energyKcal: asNullableDouble(json['energyKcal']),
protein: asNullableDouble(json['protein']),
fat: asNullableDouble(json['fat']),
carbohydrate: asNullableDouble(json['carbohydrate']),
dietaryFiber: asNullableDouble(json['dietaryFiber']),
sodium: asNullableDouble(json['sodium']),
portions:
asMapList(json['portions']).map(FoodPortion.fromJson).toList(),
);
}
/// 按份量算这条食物实际摄入的热量(前端预览用,服务端会独立再算一次)
double kcalFor(num grams) {
if (energyKcal == null || grams <= 0) return 0;
return energyKcal! * grams / 100;
}
}
/// 食物分类 + 该分类下的食物数
class FoodCategory {
const FoodCategory({required this.name, required this.count});
final String name;
final int count;
factory FoodCategory.fromJson(Map<String, dynamic> json) {
return FoodCategory(
name: asNonEmpty(json['name'], '未分类'),
count: asInt(json['count']),
);
}
}
/// 一条饮食记录
class DietRecordItem {
const DietRecordItem({
required this.id,
required this.mealType,
required this.foodName,
required this.amountGrams,
required this.energyKcal,
required this.protein,
required this.fat,
required this.carbohydrate,
required this.dietaryFiber,
required this.sodium,
});
final int id;
final String mealType;
final String foodName;
final double amountGrams;
final double energyKcal;
final double protein;
final double fat;
final double carbohydrate;
final double dietaryFiber;
final double sodium;
factory DietRecordItem.fromJson(Map<String, dynamic> json) {
return DietRecordItem(
id: asInt(json['id']),
mealType: asNonEmpty(json['mealType'], 'snack'),
foodName: asNonEmpty(json['foodName'], '未命名食物'),
amountGrams: asDouble(json['amountGrams']),
energyKcal: asDouble(json['energyKcal']),
protein: asDouble(json['protein']),
fat: asDouble(json['fat']),
carbohydrate: asDouble(json['carbohydrate']),
dietaryFiber: asDouble(json['dietaryFiber']),
sodium: asDouble(json['sodium']),
);
}
}
/// 营养合计
class NutritionTotals {
const NutritionTotals({
this.energyKcal = 0,
this.protein = 0,
this.fat = 0,
this.carbohydrate = 0,
this.dietaryFiber = 0,
this.sodium = 0,
this.itemCount = 0,
});
final double energyKcal;
final double protein;
final double fat;
final double carbohydrate;
final double dietaryFiber;
final double sodium;
final int itemCount;
static const zero = NutritionTotals();
factory NutritionTotals.fromJson(Map<String, dynamic> json) {
return NutritionTotals(
energyKcal: asDouble(json['energyKcal']),
protein: asDouble(json['protein']),
fat: asDouble(json['fat']),
carbohydrate: asDouble(json['carbohydrate']),
dietaryFiber: asDouble(json['dietaryFiber']),
sodium: asDouble(json['sodium']),
itemCount: asInt(json['itemCount']),
);
}
}
/// 一个餐次的分组
class MealGroup {
const MealGroup({
required this.mealType,
required this.totals,
required this.items,
});
final String mealType;
final NutritionTotals totals;
final List<DietRecordItem> items;
factory MealGroup.fromJson(Map<String, dynamic> json) {
return MealGroup(
mealType: asNonEmpty(json['mealType'], 'snack'),
totals: NutritionTotals.fromJson(
(json['totals'] as Map?)?.cast<String, dynamic>() ?? const {}),
items: asMapList(json['items']).map(DietRecordItem.fromJson).toList(),
);
}
}
/// 某一天的饮食情况
class DietDay {
const DietDay({
required this.date,
required this.totals,
required this.meals,
});
final DateTime? date;
final NutritionTotals totals;
/// 固定 4 个餐次(后端保留了空餐次,前端不用补位)
final List<MealGroup> meals;
static const empty = DietDay(date: null, totals: NutritionTotals.zero, meals: []);
factory DietDay.fromJson(Map<String, dynamic> json) {
return DietDay(
date: WeightPoint.tryParse(json['date']),
totals: NutritionTotals.fromJson(
(json['totals'] as Map?)?.cast<String, dynamic>() ?? const {}),
meals: asMapList(json['meals']).map(MealGroup.fromJson).toList(),
);
}
}
/// 餐次的中文名与显示顺序
const Map<String, String> kMealLabels = {
'breakfast': '早餐',
'lunch': '午餐',
'dinner': '晚餐',
'snack': '加餐',
};
String mealLabel(String mealType) => kMealLabels[mealType] ?? '加餐';