强宝健康模块
This commit is contained in:
@@ -0,0 +1,428 @@
|
||||
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] ?? '加餐';
|
||||
Reference in New Issue
Block a user