refactor: 4层架构重构 + 饮食VLM接入 + 多项修复
- 后端: remaining_endpoints拆分为6个独立文件 - 后端: AI Agent Handler从ai_chat_endpoints抽取为7个独立处理器 - 后端: 食物识别prompt改为输出结构化JSON - 前端: 饮食识别从Mock替换为真实VLM API调用 - 前端: 首页图片上传URL修复(/api/upload→/api/files/upload) - 前端: 拍饮食按钮导航到独立DietCapturePage - 前端: 删除无用agent_bar.dart - 前端: 修复widget_test.dart过期属性名 - 前端: 恢复ServicePackageCard和详情页 - 新增6份实施文档(情况/问诊/报告/建档/日历/视觉统一)
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
|
||||
final dietProvider = NotifierProvider<DietNotifier, DietState>(DietNotifier.new);
|
||||
|
||||
@@ -41,12 +44,14 @@ class DietState {
|
||||
class FoodItem {
|
||||
final String id;
|
||||
String name;
|
||||
String portion;
|
||||
int calories;
|
||||
bool selected;
|
||||
|
||||
FoodItem({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.portion,
|
||||
required this.calories,
|
||||
this.selected = true,
|
||||
});
|
||||
@@ -60,36 +65,94 @@ class DietNotifier extends Notifier<DietState> {
|
||||
state = state.copyWith(imagePath: path);
|
||||
}
|
||||
|
||||
void analyzeImage() async {
|
||||
String? _analysisError;
|
||||
|
||||
Future<void> analyzeImage() async {
|
||||
state = state.copyWith(isAnalyzing: true);
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
final mockFoods = [
|
||||
FoodItem(id: '1', name: '米饭', calories: 150),
|
||||
FoodItem(id: '2', name: '番茄炒蛋', calories: 200),
|
||||
FoodItem(id: '3', name: '红烧肉', calories: 350),
|
||||
FoodItem(id: '4', name: '青菜', calories: 50),
|
||||
];
|
||||
state = state.copyWith(foods: mockFoods, isAnalyzing: false, healthScore: 3);
|
||||
_analysisError = null;
|
||||
try {
|
||||
final api = ref.read(apiClientProvider);
|
||||
final imageFile = File(state.imagePath!);
|
||||
|
||||
final formData = FormData.fromMap({
|
||||
'images': await MultipartFile.fromFile(
|
||||
imageFile.path,
|
||||
filename: imageFile.path.split('/').last,
|
||||
),
|
||||
});
|
||||
final res = await api.dio.post('/api/ai/analyze-food-image', data: formData);
|
||||
final data = res.data;
|
||||
if (data['code'] != 0) {
|
||||
_analysisError = data['message'] ?? '识别失败';
|
||||
state = state.copyWith(isAnalyzing: false);
|
||||
return;
|
||||
}
|
||||
|
||||
final raw = data['data'] as String? ?? '[]';
|
||||
final foods = _parseFoodItems(raw);
|
||||
state = state.copyWith(
|
||||
foods: foods,
|
||||
isAnalyzing: false,
|
||||
healthScore: foods.isNotEmpty ? 3 : null,
|
||||
);
|
||||
} catch (e) {
|
||||
_analysisError = '识别失败: $e';
|
||||
state = state.copyWith(isAnalyzing: false);
|
||||
}
|
||||
}
|
||||
|
||||
List<FoodItem> _parseFoodItems(String raw) {
|
||||
var json = raw.trim();
|
||||
if (json.startsWith('```')) {
|
||||
final start = json.indexOf('\n');
|
||||
if (start != -1) json = json.substring(start + 1);
|
||||
final end = json.lastIndexOf('```');
|
||||
if (end != -1) json = json.substring(0, end);
|
||||
json = json.trim();
|
||||
}
|
||||
try {
|
||||
final list = jsonDecode(json) as List;
|
||||
return list.asMap().entries.map((e) {
|
||||
final item = e.value as Map<String, dynamic>;
|
||||
return FoodItem(
|
||||
id: 'food_${DateTime.now().millisecondsSinceEpoch}_${e.key}',
|
||||
name: item['name']?.toString() ?? '未知食物',
|
||||
portion: item['portion']?.toString() ?? '',
|
||||
calories: (item['calories'] as num?)?.toInt() ?? 0,
|
||||
selected: true,
|
||||
);
|
||||
}).toList();
|
||||
} catch (_) {
|
||||
return [
|
||||
FoodItem(
|
||||
id: 'food_${DateTime.now().millisecondsSinceEpoch}',
|
||||
name: '识别结果(手动编辑)',
|
||||
portion: raw.length > 50 ? raw.substring(0, 50) : raw,
|
||||
calories: 0,
|
||||
selected: true,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
void updateFoodName(String id, String name) {
|
||||
final foods = state.foods.map((f) => f.id == id ? FoodItem(id: f.id, name: name, calories: f.calories, selected: f.selected) : f).toList();
|
||||
final foods = state.foods.map((f) => f.id == id ? FoodItem(id: f.id, name: name, portion: f.portion, calories: f.calories, selected: f.selected) : f).toList();
|
||||
state = state.copyWith(foods: foods);
|
||||
}
|
||||
|
||||
void updateFoodCalories(String id, int calories) {
|
||||
final foods = state.foods.map((f) => f.id == id ? FoodItem(id: f.id, name: f.name, calories: calories, selected: f.selected) : f).toList();
|
||||
final foods = state.foods.map((f) => f.id == id ? FoodItem(id: f.id, name: f.name, portion: f.portion, calories: calories, selected: f.selected) : f).toList();
|
||||
state = state.copyWith(foods: foods);
|
||||
}
|
||||
|
||||
void toggleFood(String id) {
|
||||
final foods = state.foods.map((f) => f.id == id ? FoodItem(id: f.id, name: f.name, calories: f.calories, selected: !f.selected) : f).toList();
|
||||
final foods = state.foods.map((f) => f.id == id ? FoodItem(id: f.id, name: f.name, portion: f.portion, calories: f.calories, selected: !f.selected) : f).toList();
|
||||
state = state.copyWith(foods: foods);
|
||||
}
|
||||
|
||||
void addFood() {
|
||||
final newId = '${DateTime.now().millisecondsSinceEpoch}';
|
||||
final foods = [...state.foods, FoodItem(id: newId, name: '新食物', calories: 100)];
|
||||
final foods = [...state.foods, FoodItem(id: newId, name: '新食物', portion: '', calories: 100)];
|
||||
state = state.copyWith(foods: foods);
|
||||
}
|
||||
|
||||
@@ -330,6 +393,8 @@ class DietCapturePage extends ConsumerWidget {
|
||||
onChanged: (v) => ref.read(dietProvider.notifier).updateFoodName(food.id, v),
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
if (food.portion.isNotEmpty)
|
||||
Text(food.portion, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
||||
Row(children: [
|
||||
const Text('热量:', style: TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
||||
SizedBox(
|
||||
|
||||
Reference in New Issue
Block a user