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