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:
MingNian
2026-06-04 13:49:43 +08:00
parent c2399b952f
commit 0e0e8ce72b
29 changed files with 1621 additions and 3361 deletions

View File

@@ -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(