import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../core/network/api_exception.dart'; import '../core/network/dio_client.dart'; import '../models/health.dart'; import '../models/json_util.dart'; final healthApiProvider = Provider((ref) => HealthApi(ref.watch(dioProvider))); /// 「强宝健康」接口:体重 / 减重目标 / 饮食 / 食物库 /// /// 后端路径都挂在 /api/health/ 下,dio 的 baseUrl 已含 /api。 class HealthApi { final Dio _dio; HealthApi(this._dio); // ================================================================ // 体重 // ================================================================ /// 记一次体重。同一天再记就是覆盖(后端保证)。 /// /// [note] 不传 = 不改动已有备注;传空串 = 清空。 Future saveWeight({ required double weight, DateTime? date, String? note, }) async { try { await _dio.post('/health/weight', data: { 'weight': weight, if (date != null) 'recordDate': _dateText(date), 'note': ?note, }); } on DioException catch (e) { throw _asApiException(e, '体重保存失败'); } } /// 折线图数据。[days] 是时间窗(默认 90 天)。 Future weightChart({int days = 90}) async { try { final res = await _dio.get('/health/weight/chart', queryParameters: {'days': days}); return WeightChart.fromJson(_asMap(res.data)); } on DioException catch (e) { throw _asApiException(e, '体重数据加载失败'); } } Future> weightHistory() async { try { final res = await _dio.get('/health/weight/history'); return asMapListFrom(res.data).map(WeightRecordItem.fromJson).toList(); } on DioException catch (e) { throw _asApiException(e, '历史记录加载失败'); } } Future deleteWeight(int id) async { try { await _dio.delete('/health/weight/$id'); } on DioException catch (e) { throw _asApiException(e, '删除失败'); } } // ================================================================ // 减重目标 // ================================================================ Future goal() async { try { final res = await _dio.get('/health/weight/goal'); return WeightGoal.fromJson(_asMap(res.data)); } on DioException catch (e) { throw _asApiException(e, '目标加载失败'); } } /// 设置目标。会覆盖当前进行中的目标。 Future saveGoal({ required double startWeight, required double targetWeight, required DateTime startDate, required DateTime targetDate, }) async { try { final res = await _dio.post('/health/weight/goal', data: { 'startWeight': startWeight, 'targetWeight': targetWeight, 'startDate': _dateText(startDate), 'targetDate': _dateText(targetDate), }); return WeightGoal.fromJson(_asMap(res.data)); } on DioException catch (e) { throw _asApiException(e, '目标保存失败'); } } // ================================================================ // 饮食 // ================================================================ /// 某天的饮食明细 + 全天合计 Future dietDay({DateTime? date}) async { try { final res = await _dio.get('/health/diet/day', queryParameters: { if (date != null) 'date': _dateText(date), }); return DietDay.fromJson(_asMap(res.data)); } on DioException catch (e) { throw _asApiException(e, '饮食记录加载失败'); } } /// 今日热量合计(首页卡片用) Future todayTotals() async { try { final res = await _dio.get('/health/diet/today'); final data = _asMap(res.data); // 这个接口只回能量和条数,别的字段是 0 return NutritionTotals( energyKcal: asDouble(data['energyKcal']), itemCount: asInt(data['itemCount']), ); } on DioException catch (e) { throw _asApiException(e, '今日热量加载失败'); } } /// 加一条饮食记录。营养值由服务端算,这里只给食物 + 克数。 Future addDiet({ required int foodId, required double grams, required String mealType, DateTime? date, }) async { try { await _dio.post('/health/diet', data: { 'foodId': foodId, 'amountGrams': grams, 'mealType': mealType, if (date != null) 'recordDate': _dateText(date), }); } on DioException catch (e) { throw _asApiException(e, '添加失败'); } } /// 改份量(营养值会跟着重算) Future updateDietAmount(int id, double grams) async { try { await _dio.put('/health/diet/$id', data: {'amountGrams': grams}); } on DioException catch (e) { throw _asApiException(e, '修改失败'); } } Future deleteDiet(int id) async { try { await _dio.delete('/health/diet/$id'); } on DioException catch (e) { throw _asApiException(e, '删除失败'); } } // ================================================================ // 食物库 // ================================================================ Future> searchFood(String keyword) async { try { final res = await _dio.get('/health/food/search', queryParameters: {'keyword': keyword}); return asMapListFrom(res.data).map(FoodItem.fromJson).toList(); } on DioException catch (e) { throw _asApiException(e, '搜索失败'); } } /// 分类浏览。[category] 为空时返回一级分类列表, /// 传了就返回该分类下的食物 —— 后端按参数切换返回类型。 Future> foodCategories() async { try { final res = await _dio.get('/health/food/browse'); return asMapListFrom(res.data).map(FoodCategory.fromJson).toList(); } on DioException catch (e) { throw _asApiException(e, '分类加载失败'); } } Future> foodByCategory(String category, {String? subCategory}) async { try { final res = await _dio.get('/health/food/browse', queryParameters: { 'category': category, 'subCategory': ?subCategory, }); return asMapListFrom(res.data).map(FoodItem.fromJson).toList(); } on DioException catch (e) { throw _asApiException(e, '食物加载失败'); } } Future> foodSubCategories(String category) async { try { final res = await _dio.get('/health/food/sub-categories', queryParameters: {'category': category}); return asMapListFrom(res.data).map(FoodCategory.fromJson).toList(); } on DioException catch (e) { throw _asApiException(e, '子分类加载失败'); } } // ================================================================ static 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'; } ApiException _asApiException(DioException e, String fallback) { final err = e.error; return err is ApiException ? err : ApiException(-1, fallback); } Map _asMap(Object? data) { if (data is Map) { // 饮食接口是 { success, data: {...} } 包了一层 if (data['data'] is Map) { return Map.from(data['data'] as Map); } return data; } return const {}; } } /// 后端这类接口把数组放在 { success, list: [...] } 里, /// 直接从 data 上取会拿到 null,这里统一拆一层。 List> asMapListFrom(Object? data) { if (data is Map) { final list = data['list'] ?? data['data']; if (list is List) { return list.whereType>().toList(); } } if (data is List) { return data.whereType>().toList(); } return const []; }