feat: 问诊对话+建档引导+报告+日历+趋势页重写+视觉统一
- 问诊对话页: 新建consultation_provider, 完整AI分身聊天UI - 首次建档引导: OnboardingPrompt+ManageArchiveTool+前端卡片 - 报告模块: POST端点 + report_agent_handler接VLM - 健康日历: GET /api/calendar端点 + 前端接API - 趋势页重写: 删除Mock数据 + 真实API + 手动录入 - 饮食保存: submit按钮接后端API - 侧边栏: 健康概览横排 + 统一紫色配色 - 运动计划: 修复JSON反序列化(record→手动解析) - VLM: prompt优化 + 模型切qwen3-vl-plus + 1280px压缩 - UI颜色: 加深主色 8B9CF7→6C5CE7 - 个人信息页精简 + 健康档案可编辑重设计 - 保洁: 删除无用agent_bar + 删除意见反馈/关于我们 - 新增AI提示词文档
This commit is contained in:
@@ -15,6 +15,7 @@ class DietState {
|
||||
final String mealType;
|
||||
final bool isAnalyzing;
|
||||
final int? healthScore;
|
||||
final String? errorMessage;
|
||||
|
||||
DietState({
|
||||
this.imagePath,
|
||||
@@ -22,6 +23,7 @@ class DietState {
|
||||
this.mealType = 'lunch',
|
||||
this.isAnalyzing = false,
|
||||
this.healthScore,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
DietState copyWith({
|
||||
@@ -30,6 +32,7 @@ class DietState {
|
||||
String? mealType,
|
||||
bool? isAnalyzing,
|
||||
int? healthScore,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return DietState(
|
||||
imagePath: imagePath ?? this.imagePath,
|
||||
@@ -37,6 +40,7 @@ class DietState {
|
||||
mealType: mealType ?? this.mealType,
|
||||
isAnalyzing: isAnalyzing ?? this.isAnalyzing,
|
||||
healthScore: healthScore ?? this.healthScore,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -65,11 +69,8 @@ class DietNotifier extends Notifier<DietState> {
|
||||
state = state.copyWith(imagePath: path);
|
||||
}
|
||||
|
||||
String? _analysisError;
|
||||
|
||||
Future<void> analyzeImage() async {
|
||||
state = state.copyWith(isAnalyzing: true);
|
||||
_analysisError = null;
|
||||
state = state.copyWith(isAnalyzing: true, errorMessage: null);
|
||||
try {
|
||||
final api = ref.read(apiClientProvider);
|
||||
final imageFile = File(state.imagePath!);
|
||||
@@ -83,8 +84,7 @@ class DietNotifier extends Notifier<DietState> {
|
||||
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);
|
||||
state = state.copyWith(isAnalyzing: false, errorMessage: data['message'] ?? '识别失败');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -96,8 +96,7 @@ class DietNotifier extends Notifier<DietState> {
|
||||
healthScore: foods.isNotEmpty ? 3 : null,
|
||||
);
|
||||
} catch (e) {
|
||||
_analysisError = '识别失败: $e';
|
||||
state = state.copyWith(isAnalyzing: false);
|
||||
state = state.copyWith(isAnalyzing: false, errorMessage: '识别失败,请重试');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,13 +167,43 @@ class DietNotifier extends Notifier<DietState> {
|
||||
void reset() {
|
||||
state = DietState();
|
||||
}
|
||||
|
||||
Future<void> saveRecord() async {
|
||||
final selectedFoods = state.foods.where((f) => f.selected).toList();
|
||||
if (selectedFoods.isEmpty) return;
|
||||
|
||||
final api = ref.read(apiClientProvider);
|
||||
final mealMap = {'breakfast': 'Breakfast', 'lunch': 'Lunch', 'dinner': 'Dinner', 'snack': 'Snack'};
|
||||
final totalCal = selectedFoods.fold<int>(0, (s, f) => s + f.calories);
|
||||
|
||||
await api.post('/api/diet-records', data: {
|
||||
'mealType': mealMap[state.mealType] ?? 'Lunch',
|
||||
'totalCalories': totalCal,
|
||||
'healthScore': state.healthScore ?? 3,
|
||||
'recordedAt': DateTime.now().toIso8601String().substring(0, 10),
|
||||
'foodItems': selectedFoods.asMap().entries.map((e) => {
|
||||
'name': e.value.name,
|
||||
'portion': e.value.portion,
|
||||
'calories': e.value.calories,
|
||||
'sortOrder': e.key,
|
||||
}).toList(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class DietCapturePage extends ConsumerWidget {
|
||||
class DietCapturePage extends ConsumerStatefulWidget {
|
||||
const DietCapturePage({super.key});
|
||||
@override ConsumerState<DietCapturePage> createState() => _DietCapturePageState();
|
||||
}
|
||||
|
||||
class _DietCapturePageState extends ConsumerState<DietCapturePage> {
|
||||
@override void initState() {
|
||||
super.initState();
|
||||
Future.microtask(() => ref.read(dietProvider.notifier).reset());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
final state = ref.watch(dietProvider);
|
||||
|
||||
return Scaffold(
|
||||
@@ -261,7 +290,7 @@ class DietCapturePage extends ConsumerWidget {
|
||||
const SizedBox(height: 20),
|
||||
_buildMealSelector(context, ref),
|
||||
const SizedBox(height: 20),
|
||||
if (state.isAnalyzing) _buildAnalyzingIndicator() else _buildFoodList(context, ref),
|
||||
if (state.isAnalyzing) _buildAnalyzingIndicator(state) else _buildFoodList(context, ref),
|
||||
if (!state.isAnalyzing && state.foods.isNotEmpty) ...[
|
||||
const SizedBox(height: 20),
|
||||
_buildNutritionSummary(totalCalories),
|
||||
@@ -323,7 +352,7 @@ class DietCapturePage extends ConsumerWidget {
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _buildAnalyzingIndicator() {
|
||||
Widget _buildAnalyzingIndicator(DietState state) {
|
||||
return Center(
|
||||
child: Column(children: [
|
||||
Container(
|
||||
@@ -331,13 +360,17 @@ class DietCapturePage extends ConsumerWidget {
|
||||
height: 60,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF0F2FF),
|
||||
color: const Color(0xFFEDEAFF),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: const CircularProgressIndicator(strokeWidth: 3, color: Color(0xFF8B9CF7)),
|
||||
child: const CircularProgressIndicator(strokeWidth: 3, color: Color(0xFF6C5CE7)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text('AI 正在识别食物...', style: TextStyle(fontSize: 16, color: Color(0xFF666666))),
|
||||
if (state.errorMessage != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(state.errorMessage!, style: const TextStyle(fontSize: 13, color: Color(0xFFE53935)), textAlign: TextAlign.center),
|
||||
],
|
||||
]),
|
||||
);
|
||||
}
|
||||
@@ -493,12 +526,24 @@ class DietCapturePage extends ConsumerWidget {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text('饮食记录已保存 ✅'),
|
||||
backgroundColor: Color(0xFF8B9CF7),
|
||||
));
|
||||
popRoute(ref);
|
||||
onPressed: () async {
|
||||
try {
|
||||
await ref.read(dietProvider.notifier).saveRecord();
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text('饮食记录已保存 ✅'),
|
||||
backgroundColor: Color(0xFF43A047),
|
||||
));
|
||||
popRoute(ref);
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text('保存失败: $e'),
|
||||
backgroundColor: const Color(0xFFE53935),
|
||||
));
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Text('保存记录'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
|
||||
Reference in New Issue
Block a user