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:
@@ -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 '饮食不太健康,建议多吃蔬菜';
|
||||
|
||||
Reference in New Issue
Block a user