fix: VLM识别修复 + 饮食CRUD + 侧边栏美化 + UI优化

- VLM: ChatMessage.Content string→object, 修复视觉content双重序列化
- 饮食: diet/medication端点 record→手动JSON解析, 修复循环引用
- 饮食记录: 左滑删除 + 去评分 + AI饮食评语(DeepSeek)
- 侧边栏: 功能区统一Row+Expanded, 服务包compact模式
- 历史对话: 点击加载历史消息
- 登录页: 居中布局, 去底部空白
- UI: 主色加深#6C5CE7, maxWidth:1024防图片超大
This commit is contained in:
MingNian
2026-06-04 16:27:03 +08:00
parent c44917b8e9
commit b944a31983
12 changed files with 413 additions and 305 deletions

View File

@@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import '../../core/navigation_provider.dart';
import '../../providers/auth_provider.dart';
import '../../utils/sse_handler.dart';
final dietProvider = NotifierProvider<DietNotifier, DietState>(DietNotifier.new);
@@ -16,6 +17,7 @@ class DietState {
final bool isAnalyzing;
final int? healthScore;
final String? errorMessage;
final String? commentary;
DietState({
this.imagePath,
@@ -24,6 +26,7 @@ class DietState {
this.isAnalyzing = false,
this.healthScore,
this.errorMessage,
this.commentary,
});
DietState copyWith({
@@ -33,6 +36,7 @@ class DietState {
bool? isAnalyzing,
int? healthScore,
String? errorMessage,
String? commentary,
}) {
return DietState(
imagePath: imagePath ?? this.imagePath,
@@ -41,6 +45,7 @@ class DietState {
isAnalyzing: isAnalyzing ?? this.isAnalyzing,
healthScore: healthScore ?? this.healthScore,
errorMessage: errorMessage ?? this.errorMessage,
commentary: commentary ?? this.commentary,
);
}
}
@@ -92,16 +97,34 @@ class DietNotifier extends Notifier<DietState> {
final raw = data['data'] as String? ?? '[]';
final foods = _parseFoodItems(raw);
state = state.copyWith(
foods: foods,
isAnalyzing: false,
healthScore: foods.isNotEmpty ? 3 : null,
);
state = state.copyWith(foods: foods, isAnalyzing: false, healthScore: foods.isNotEmpty ? 3 : null);
if (foods.isNotEmpty) _fetchCommentary(foods);
} catch (e) {
state = state.copyWith(isAnalyzing: false, errorMessage: '识别失败,请重试');
}
}
Future<void> _fetchCommentary(List<FoodItem> foods) async {
try {
final api = ref.read(apiClientProvider);
final token = await api.accessToken;
if (token == null) return;
final names = foods.map((f) => '${f.name}(${f.portion},${f.calories}kcal)').join('');
final stream = SseHandler.connect(
agentType: 'default',
message: '我刚才吃了这些:$names。请结合我的健康档案给我简短的饮食评价和建议50字以内',
token: token,
);
String text = '';
await for (final event in stream) {
if (event['action'] == 'answer') text += (event['data'] as String?) ?? '';
if (event['action'] == 'status') {
if (text.isNotEmpty) state = state.copyWith(commentary: text.trim());
}
}
} catch (_) {}
}
List<FoodItem> _parseFoodItems(String raw) {
var json = raw.trim();
if (json.startsWith('```')) {
@@ -274,7 +297,7 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
Future<void> _pickImage(BuildContext context, WidgetRef ref, ImageSource source) async {
final picker = ImagePicker();
final picked = await picker.pickImage(source: source, imageQuality: 85);
final picked = await picker.pickImage(source: source, imageQuality: 80, maxWidth: 1024, maxHeight: 1024);
if (picked != null) {
ref.read(dietProvider.notifier).setImage(picked.path);
ref.read(dietProvider.notifier).analyzeImage();
@@ -289,17 +312,21 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
padding: const EdgeInsets.all(16),
child: Column(children: [
_buildImagePreview(state.imagePath!),
const SizedBox(height: 20),
const SizedBox(height: 16),
_buildMealSelector(context, ref),
const SizedBox(height: 20),
if (state.isAnalyzing) _buildAnalyzingIndicator(state) else _buildFoodList(context, ref),
if (!state.isAnalyzing && state.foods.isNotEmpty) ...[
const SizedBox(height: 20),
_buildNutritionSummary(totalCalories),
const SizedBox(height: 20),
_buildHealthScore(state.healthScore ?? 0),
const SizedBox(height: 30),
_buildSubmitButton(context, ref),
const SizedBox(height: 16),
if (state.isAnalyzing) _buildAnalyzingIndicator(state) else ...[
_buildFoodList(context, ref),
if (state.foods.isNotEmpty) ...[
const SizedBox(height: 16),
_buildNutritionSummary(totalCalories),
if (state.commentary != null && state.commentary!.isNotEmpty) ...[
const SizedBox(height: 16),
_buildAiCommentary(state.commentary!),
],
const SizedBox(height: 24),
_buildSubmitButton(context, ref),
],
],
]),
);
@@ -502,6 +529,21 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
);
}
Widget _buildAiCommentary(String text) {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
gradient: const LinearGradient(colors: [Color(0xFFEDEAFF), Color(0xFFF5F3FF)], begin: Alignment.topLeft, end: Alignment.bottomRight),
borderRadius: BorderRadius.circular(16),
),
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
Container(width: 32, height: 32, decoration: BoxDecoration(color: const Color(0xFF6C5CE7).withAlpha(20), borderRadius: BorderRadius.circular(10)), child: const Icon(Icons.auto_awesome, size: 16, color: Color(0xFF6C5CE7))),
const SizedBox(width: 10),
Expanded(child: Text(text, style: const TextStyle(fontSize: 14, color: Color(0xFF444444), height: 1.5))),
]),
);
}
String _getScoreComment(int score) {
switch (score) {
case 1: return '饮食不太健康,建议多吃蔬菜';