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:
@@ -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}';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user