feat: 全功能前后端联调完成,47/47 测试通过

前端:
- 新增 DietCapturePage 独立拍照识别页
- 5种消息卡片类型完整实现(数据确认/用药/饮食/报告/快捷选项)
- 任务卡片区:异常警告+数据摘要+自动折叠
- 侧滑抽屉:历史对话列表+对话管理
- 运动计划:进度卡片+创建计划+每日打卡
- 报告页:拍照/相册/PDF上传+分析
- 面板按钮补全血氧/体重录入
- UI 升级:紫色主题+动画+气泡样式
- 全部迁移 Riverpod 3.x API

后端:
- 新增 _UpdateMessageTypeAndMetadata,Tool Calling 自动映射消息类型
- SSE answer 事件携带 type 字段
- 提示词优化(移除"阿福",语气规则归位)
- 运动计划支持 AI 创建和打卡

测试:
- 新增 full_e2e_test.py 全流程测试(认证/数据CRUD/6个Agent对话/VLM/报告)
This commit is contained in:
MingNian
2026-06-02 20:31:22 +08:00
parent 498708e568
commit c6395ea9b4
12 changed files with 2631 additions and 126 deletions

View File

@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../core/navigation_provider.dart';
import '../providers/auth_provider.dart';
import '../providers/data_providers.dart';
import '../providers/chat_provider.dart';
/// 侧滑抽屉——健康概览 + 历史对话 + 菜单
class HealthDrawer extends ConsumerWidget {
@@ -13,6 +14,7 @@ class HealthDrawer extends ConsumerWidget {
final auth = ref.watch(authProvider);
final user = auth.user;
final latestHealth = ref.watch(latestHealthProvider);
final conversations = ref.watch(conversationListProvider);
return Drawer(
child: SafeArea(
@@ -67,9 +69,28 @@ class HealthDrawer extends ConsumerWidget {
const Divider(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: Text('历史对话', style: Theme.of(context).textTheme.labelMedium!.copyWith(fontWeight: FontWeight.w600)),
child: Row(children: [
Text('历史对话', style: Theme.of(context).textTheme.labelMedium!.copyWith(fontWeight: FontWeight.w600)),
const Spacer(),
TextButton(onPressed: () => ref.invalidate(conversationListProvider), child: const Text('刷新', style: TextStyle(fontSize: 12, color: Color(0xFF635BFF)))),
]),
),
Expanded(
child: conversations.when(
data: (items) {
if (items.isEmpty) {
return const Center(child: Text('暂无历史对话', style: TextStyle(color: Color(0xFF999999), fontSize: 14)));
}
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 8),
itemCount: items.length,
itemBuilder: (ctx, i) => _ConversationItem(item: items[i], ref: ref),
);
},
loading: () => const Center(child: CircularProgressIndicator(strokeWidth: 2)),
error: (_, __) => const Center(child: Text('加载失败', style: TextStyle(color: Color(0xFF999999), fontSize: 14))),
),
),
const Expanded(child: Center(child: Text('暂无历史对话', style: TextStyle(color: Color(0xFF999999), fontSize: 14)))),
const Divider(),
_DrawerItem(icon: Icons.logout, label: '退出登录', onTap: () async {
@@ -117,3 +138,94 @@ class _HealthMetric extends StatelessWidget {
onTap: onTap,
);
}
class _ConversationItem extends ConsumerWidget {
final ConversationItem item;
final WidgetRef ref;
const _ConversationItem({required this.item, required this.ref});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFFF8F7FF),
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: const Color(0xFFEDEBFF),
borderRadius: BorderRadius.circular(10),
),
child: Icon(_getAgentIcon(item.agent), size: 18, color: const Color(0xFF635BFF)),
),
title: Text(item.title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
subtitle: Text(item.lastMessage, style: TextStyle(fontSize: 12, color: Colors.grey[500])),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_formatTime(item.updatedAt), style: const TextStyle(fontSize: 10, color: Color(0xFFCCCCCC))),
const SizedBox(height: 4),
PopupMenuButton<int>(
icon: const Icon(Icons.more_vert, size: 16, color: Color(0xFFCCCCCC)),
itemBuilder: (_) => [
const PopupMenuItem(value: 1, child: Text('继续聊')),
const PopupMenuItem(value: 2, child: Text('删除')),
],
onSelected: (v) async {
if (v == 1) {
ref.read(chatProvider.notifier).setAgent(item.agent);
Navigator.pop(context);
} else if (v == 2) {
final ok = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('删除对话'),
content: const Text('确定删除该对话?'),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')),
TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('确定')),
],
),
);
if (ok == true) {
ref.invalidate(conversationListProvider);
}
}
},
),
],
),
onTap: () {
ref.read(chatProvider.notifier).setAgent(item.agent);
Navigator.pop(context);
},
dense: true,
),
);
}
IconData _getAgentIcon(ActiveAgent agent) {
switch (agent) {
case ActiveAgent.health: return Icons.health_and_safety;
case ActiveAgent.diet: return Icons.restaurant;
case ActiveAgent.medication: return Icons.medication;
case ActiveAgent.report: return Icons.file_open;
case ActiveAgent.exercise: return Icons.directions_run;
case ActiveAgent.consultation: return Icons.chat;
default: return Icons.chat_bubble_outline;
}
}
String _formatTime(DateTime time) {
final now = DateTime.now();
final diff = now.difference(time);
if (diff.inMinutes < 60) return '${diff.inMinutes}分钟前';
if (diff.inHours < 24) return '${diff.inHours}小时前';
if (diff.inDays < 7) return '${diff.inDays}天前';
return '${time.month}/${time.day}';
}
}