diff --git a/assets/icons/health.svg b/assets/icons/health.svg
new file mode 100644
index 0000000..a8d869a
--- /dev/null
+++ b/assets/icons/health.svg
@@ -0,0 +1,7 @@
+
diff --git a/lib/api/health_api.dart b/lib/api/health_api.dart
new file mode 100644
index 0000000..5671c92
--- /dev/null
+++ b/lib/api/health_api.dart
@@ -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((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