Files
2026-09-28 16:40:32 +08:00

258 lines
8.0 KiB
Dart

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<HealthApi>((ref) => HealthApi(ref.watch(dioProvider)));
/// 「强宝健康」接口:体重 / 减重目标 / 饮食 / 食物库
///
/// 后端路径都挂在 /api/health/ 下,dio 的 baseUrl 已含 /api。
class HealthApi {
final Dio _dio;
HealthApi(this._dio);
// ================================================================
// 体重
// ================================================================
/// 记一次体重。同一天再记就是覆盖(后端保证)。
///
/// [note] 不传 = 不改动已有备注;传空串 = 清空。
Future<void> 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> 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<List<WeightRecordItem>> 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<void> deleteWeight(int id) async {
try {
await _dio.delete('/health/weight/$id');
} on DioException catch (e) {
throw _asApiException(e, '删除失败');
}
}
// ================================================================
// 减重目标
// ================================================================
Future<WeightGoal> 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<WeightGoal> 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> 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<NutritionTotals> 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<void> 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<void> updateDietAmount(int id, double grams) async {
try {
await _dio.put('/health/diet/$id', data: {'amountGrams': grams});
} on DioException catch (e) {
throw _asApiException(e, '修改失败');
}
}
Future<void> deleteDiet(int id) async {
try {
await _dio.delete('/health/diet/$id');
} on DioException catch (e) {
throw _asApiException(e, '删除失败');
}
}
// ================================================================
// 食物库
// ================================================================
Future<List<FoodItem>> 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<List<FoodCategory>> 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<List<FoodItem>> 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<List<FoodCategory>> 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<String, dynamic> _asMap(Object? data) {
if (data is Map<String, dynamic>) {
// 饮食接口是 { success, data: {...} } 包了一层
if (data['data'] is Map) {
return Map<String, dynamic>.from(data['data'] as Map);
}
return data;
}
return const {};
}
}
/// 后端这类接口把数组放在 { success, list: [...] } 里,
/// 直接从 data 上取会拿到 null,这里统一拆一层。
List<Map<String, dynamic>> asMapListFrom(Object? data) {
if (data is Map) {
final list = data['list'] ?? data['data'];
if (list is List) {
return list.whereType<Map<String, dynamic>>().toList();
}
}
if (data is List) {
return data.whereType<Map<String, dynamic>>().toList();
}
return const [];
}