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 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 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 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 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 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 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 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 portions; factory FoodItem.fromJson(Map 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 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 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 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 items; factory MealGroup.fromJson(Map json) { return MealGroup( mealType: asNonEmpty(json['mealType'], 'snack'), totals: NutritionTotals.fromJson( (json['totals'] as Map?)?.cast() ?? 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 meals; static const empty = DietDay(date: null, totals: NutritionTotals.zero, meals: []); factory DietDay.fromJson(Map json) { return DietDay( date: WeightPoint.tryParse(json['date']), totals: NutritionTotals.fromJson( (json['totals'] as Map?)?.cast() ?? const {}), meals: asMapList(json['meals']).map(MealGroup.fromJson).toList(), ); } } /// 餐次的中文名与显示顺序 const Map kMealLabels = { 'breakfast': '早餐', 'lunch': '午餐', 'dinner': '晚餐', 'snack': '加餐', }; String mealLabel(String mealType) => kMealLabels[mealType] ?? '加餐';