import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../providers/chat_provider.dart'; /// 对话消息列表 class ChatMessagesView extends ConsumerWidget { final ScrollController scrollCtrl; final List messages; const ChatMessagesView({super.key, required this.scrollCtrl, required this.messages}); @override Widget build(BuildContext context, WidgetRef ref) { final chatState = ref.watch(chatProvider); if (messages.isEmpty) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 80, height: 80, decoration: BoxDecoration( color: const Color(0xFFEDEBFF), borderRadius: BorderRadius.circular(40), ), child: const Icon(Icons.health_and_safety, size: 40, color: Color(0xFF635BFF)), ), const SizedBox(height: 16), Text('开始和 AI 健康管家对话吧', style: Theme.of(context).textTheme.bodyMedium), const SizedBox(height: 8), Text('记录健康数据,获取专业建议', style: TextStyle(fontSize: 14, color: Colors.grey[400])), ], ), ); } return ListView.builder( controller: scrollCtrl, reverse: true, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), itemCount: messages.length, itemBuilder: (context, index) { final msg = messages[messages.length - 1 - index]; return _buildMessageContent(context, msg, chatState); }, ); } Widget _buildMessageContent(BuildContext context, ChatMessage msg, ChatState chatState) { final isUser = msg.isUser; if (!isUser && chatState.isStreaming && msg.content.isEmpty) { return _buildThinkingBubble(context, chatState.thinkingText); } switch (msg.type) { case MessageType.dataConfirm: return _buildDataConfirmCard(context, msg); case MessageType.medicationConfirm: return _buildMedicationConfirmCard(context, msg); case MessageType.dietAnalysis: return _buildDietAnalysisCard(context, msg); case MessageType.reportAnalysis: return _buildReportAnalysisCard(context, msg); case MessageType.quickOptions: return _buildQuickOptionsCard(context, msg); default: return _buildTextBubble(context, msg); } } Widget _buildThinkingBubble(BuildContext context, String? thinkingText) { return Align( alignment: Alignment.centerLeft, child: Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( color: const Color(0xFFFEFEFF), borderRadius: const BorderRadius.only(topLeft: Radius.circular(4), topRight: Radius.circular(20), bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)), border: Border.all(color: const Color(0xFFE8E6FF), width: 1.5), boxShadow: const [BoxShadow(color: Color(0xFF635BFF), blurRadius: 4, offset: Offset(0, 2))], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 24, height: 24, padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: const Color(0xFFEDEBFF), borderRadius: BorderRadius.circular(12), ), child: const CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF635BFF)), ), const SizedBox(width: 10), const Text('正在分析...', style: TextStyle(fontSize: 14, color: Color(0xFF999999))), ], ), ), ); } Widget _buildTextBubble(BuildContext context, ChatMessage msg) { final isUser = msg.isUser; return Align( alignment: isUser ? Alignment.centerRight : Alignment.centerLeft, child: Container( margin: const EdgeInsets.only(bottom: 12), constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.85), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( color: isUser ? const Color(0xFF635BFF) : const Color(0xFFFEFEFF), borderRadius: BorderRadius.only( topLeft: Radius.circular(isUser ? 20 : 4), topRight: Radius.circular(isUser ? 4 : 20), bottomLeft: const Radius.circular(20), bottomRight: const Radius.circular(20), ), border: isUser ? null : Border.all(color: const Color(0xFFE8E6FF), width: 1.5), boxShadow: isUser ? [] : [BoxShadow(color: const Color(0xFF635BFF).withAlpha(8), blurRadius: 4, offset: const Offset(0, 2))], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (isUser) Text(msg.content, style: const TextStyle(fontSize: 16, color: Colors.white, height: 1.4)) else MarkdownBody( data: msg.content, selectable: true, styleSheet: MarkdownStyleSheet( p: const TextStyle(fontSize: 16, color: Color(0xFF1A1A1A), height: 1.5), h1: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)), h2: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)), code: const TextStyle(fontSize: 14, backgroundColor: Colors.grey), ), ), if (!isUser && !msg.content.isEmpty) Padding( padding: const EdgeInsets.only(top: 10), child: Row(children: [ const CircleAvatar(radius: 10, backgroundColor: Color(0xFFEDEBFF), child: Icon(Icons.chat_bubble_outline, size: 14, color: Color(0xFF635BFF))), const SizedBox(width: 6), Text('健康管家', style: TextStyle(fontSize: 12, color: Colors.grey[400])), const SizedBox(width: 4), Text('仅供参考', style: TextStyle(fontSize: 11, color: Colors.grey[300])), ]), ), ], ), ), ); } Widget _buildDataConfirmCard(BuildContext context, ChatMessage msg) { final meta = msg.metadata; final metricType = meta?['type'] as String? ?? ''; final value = meta?['value'] as String? ?? ''; final abnormal = meta?['abnormal'] as bool? ?? false; return Align( alignment: Alignment.centerLeft, child: Container( margin: const EdgeInsets.only(bottom: 12), constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.85), decoration: BoxDecoration( color: const Color(0xFFFEFEFF), borderRadius: BorderRadius.circular(20), border: Border.all(color: const Color(0xFFE8E6FF), width: 1.5), boxShadow: [BoxShadow(color: const Color(0xFF635BFF).withAlpha(8), blurRadius: 4, offset: const Offset(0, 2))], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.all(16), decoration: const BoxDecoration( color: Color(0xFFF5F3FF), borderRadius: BorderRadius.only(topLeft: Radius.circular(20), topRight: Radius.circular(20)), ), child: Row(children: [ const Icon(Icons.check_circle, size: 20, color: Color(0xFF43A047)), const SizedBox(width: 8), const Text('已记录', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF43A047))), ]), ), Padding( padding: const EdgeInsets.all(16), child: Column(children: [ Row(children: [ Text( _getMetricIcon(metricType), style: const TextStyle(fontSize: 24), ), const SizedBox(width: 12), Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(_getMetricName(metricType), style: const TextStyle(fontSize: 14, color: Color(0xFF666666))), const SizedBox(height: 4), Text(value, style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: abnormal ? const Color(0xFFE53935) : const Color(0xFF1A1A1A))), ]), const Spacer(), if (abnormal) const Icon(Icons.warning_amber, size: 20, color: Color(0xFFE53935)), ]), if (abnormal) const Padding( padding: EdgeInsets.only(top: 12), child: Text('⚠️ 数值超出正常范围,请关注', style: TextStyle(fontSize: 14, color: Color(0xFFE53935))), ), const SizedBox(height: 12), Row(children: [ Expanded( child: OutlinedButton( onPressed: () {}, child: const Text('编辑'), style: OutlinedButton.styleFrom( foregroundColor: const Color(0xFF635BFF), side: const BorderSide(color: Color(0xFF635BFF)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), ), ), ), const SizedBox(width: 8), Expanded( child: ElevatedButton( onPressed: () {}, child: const Text('确认'), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF635BFF), foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), ), ), ), ]), ]), ), ], ), ), ); } Widget _buildMedicationConfirmCard(BuildContext context, ChatMessage msg) { final meta = msg.metadata; final name = meta?['name'] as String? ?? ''; final dosage = meta?['dosage'] as String? ?? ''; final time = meta?['time'] as String? ?? ''; return Align( alignment: Alignment.centerLeft, child: Container( margin: const EdgeInsets.only(bottom: 12), constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.85), decoration: BoxDecoration( color: const Color(0xFFFEFEFF), borderRadius: BorderRadius.circular(20), border: Border.all(color: const Color(0xFFE8E6FF), width: 1.5), boxShadow: [BoxShadow(color: const Color(0xFF635BFF).withAlpha(8), blurRadius: 4, offset: const Offset(0, 2))], ), child: Padding( padding: const EdgeInsets.all(16), child: Column(children: [ Row(children: [ const Text('💊', style: TextStyle(fontSize: 28)), const SizedBox(width: 12), Expanded( child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(name, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600)), if (dosage.isNotEmpty) Text(dosage, style: const TextStyle(fontSize: 14, color: Color(0xFF666666))), if (time.isNotEmpty) Text('每天 $time', style: const TextStyle(fontSize: 14, color: Color(0xFF666666))), ]), ), ]), const SizedBox(height: 16), const Text('需要调整吗?', style: TextStyle(fontSize: 14, color: Color(0xFF666666))), const SizedBox(height: 12), Row(children: [ Expanded(child: _medBtn('确认', Icons.check, Colors.white, const Color(0xFF635BFF))), const SizedBox(width: 8), Expanded(child: _medBtn('修改时间', Icons.access_time, const Color(0xFF635BFF), Colors.white)), const SizedBox(width: 8), Expanded(child: _medBtn('改剂量', Icons.edit, const Color(0xFF635BFF), Colors.white)), ]), ]), ), ), ); } Widget _medBtn(String label, IconData icon, Color textColor, Color bgColor) { return ElevatedButton( onPressed: () {}, child: Row(children: [Icon(icon, size: 16), const SizedBox(width: 4), Text(label, style: TextStyle(fontSize: 12))]), style: ElevatedButton.styleFrom( backgroundColor: bgColor, foregroundColor: textColor, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), padding: const EdgeInsets.symmetric(vertical: 10), ), ); } Widget _buildDietAnalysisCard(BuildContext context, ChatMessage msg) { final meta = msg.metadata; final foods = meta?['foods'] as List? ?? []; final totalCalories = meta?['totalCalories'] as int? ?? 0; final rating = meta?['rating'] as int? ?? 0; final warnings = meta?['warnings'] as List? ?? []; return Align( alignment: Alignment.centerLeft, child: Container( margin: const EdgeInsets.only(bottom: 12), constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.85), decoration: BoxDecoration( color: const Color(0xFFFEFEFF), borderRadius: BorderRadius.circular(20), border: Border.all(color: const Color(0xFFE8E6FF), width: 1.5), boxShadow: [BoxShadow(color: const Color(0xFF635BFF).withAlpha(8), blurRadius: 4, offset: const Offset(0, 2))], ), child: Padding( padding: const EdgeInsets.all(16), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('🍽️ 饮食分析', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), const SizedBox(height: 12), Column(children: foods.map((food) { final f = food as Map? ?? {}; return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Row(children: [ Text(f['name'] as String? ?? '', style: const TextStyle(fontSize: 14)), const Spacer(), Text('${f['calories'] ?? 0} kcal', style: TextStyle(fontSize: 14, color: Colors.grey[500])), ]), ); }).toList()), const SizedBox(height: 12), Row(children: [ const Text('总热量', style: TextStyle(fontSize: 14, color: Color(0xFF666666))), const Spacer(), Text('$totalCalories kcal', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), ]), const SizedBox(height: 12), Row(children: [ const Text('健康评分', style: TextStyle(fontSize: 14, color: Color(0xFF666666))), const SizedBox(width: 8), Row(children: List.generate(5, (i) => Icon(Icons.star, size: 16, color: i < rating ? const Color(0xFFFFB800) : Colors.grey[300]))), ]), if (warnings.isNotEmpty) ...[ const SizedBox(height: 12), ...warnings.map((w) => Padding( padding: const EdgeInsets.only(bottom: 4), child: Text('⚠️ $w', style: TextStyle(fontSize: 14, color: const Color(0xFFE53935))), )), ], const SizedBox(height: 12), const Text('建议:饮食均衡,多吃蔬菜水果', style: TextStyle(fontSize: 14, color: Color(0xFF666666))), ]), ), ), ); } Widget _buildReportAnalysisCard(BuildContext context, ChatMessage msg) { final meta = msg.metadata; final reportType = meta?['type'] as String? ?? ''; final indicators = meta?['indicators'] as List? ?? []; return Align( alignment: Alignment.centerLeft, child: Container( margin: const EdgeInsets.only(bottom: 12), constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.85), decoration: BoxDecoration( color: const Color(0xFFFEFEFF), borderRadius: BorderRadius.circular(20), border: Border.all(color: const Color(0xFFE8E6FF), width: 1.5), boxShadow: [BoxShadow(color: const Color(0xFF635BFF).withAlpha(8), blurRadius: 4, offset: const Offset(0, 2))], ), child: Padding( padding: const EdgeInsets.all(16), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ const Text('📋', style: TextStyle(fontSize: 20)), const SizedBox(width: 8), Text(reportType, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), ]), const SizedBox(height: 12), const Text('AI 预解读结果', style: TextStyle(fontSize: 14, color: Color(0xFF666666))), const SizedBox(height: 8), Column(children: indicators.map((ind) { final i = ind as Map? ?? {}; final name = i['name'] as String? ?? ''; final value = i['value'] as String? ?? ''; final status = i['status'] as String? ?? 'normal'; Color statusColor; switch (status) { case 'high': statusColor = const Color(0xFFE53935); break; case 'low': statusColor = const Color(0xFFF9A825); break; default: statusColor = const Color(0xFF43A047); } return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Row(children: [ Expanded(child: Text(name, style: const TextStyle(fontSize: 14))), Text(value, style: TextStyle(fontSize: 14, color: statusColor, fontWeight: FontWeight.w600)), const SizedBox(width: 8), Icon(status == 'normal' ? Icons.check_circle : Icons.warning_amber, size: 16, color: statusColor), ]), ); }).toList()), const SizedBox(height: 12), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: const Color(0xFFFEF3C7), borderRadius: BorderRadius.circular(12), ), child: const Text('⚠️ AI 预解读,待医生确认', style: TextStyle(fontSize: 13, color: Color(0xFFD97706))), ), const SizedBox(height: 12), Center( child: OutlinedButton( onPressed: () {}, child: const Text('查看原始图片'), style: OutlinedButton.styleFrom( foregroundColor: const Color(0xFF635BFF), side: const BorderSide(color: Color(0xFF635BFF)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), ), ), ), ]), ), ), ); } Widget _buildQuickOptionsCard(BuildContext context, ChatMessage msg) { final meta = msg.metadata; final options = meta?['options'] as List? ?? []; return Align( alignment: Alignment.centerLeft, child: Container( margin: const EdgeInsets.only(bottom: 12), constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.85), decoration: BoxDecoration( color: const Color(0xFFFEFEFF), borderRadius: BorderRadius.circular(20), border: Border.all(color: const Color(0xFFE8E6FF), width: 1.5), boxShadow: [BoxShadow(color: const Color(0xFF635BFF).withAlpha(8), blurRadius: 4, offset: const Offset(0, 2))], ), child: Padding( padding: const EdgeInsets.all(16), child: Column(children: [ Text(msg.content, style: const TextStyle(fontSize: 16, color: Color(0xFF1A1A1A))), const SizedBox(height: 12), Wrap(spacing: 8, runSpacing: 8, children: options.map((opt) { final o = opt as Map? ?? {}; return ElevatedButton( onPressed: () {}, child: Text(o['label'] as String? ?? '', style: const TextStyle(fontSize: 14)), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFF5F3FF), foregroundColor: const Color(0xFF635BFF), elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), ), ); }).toList()), ]), ), ), ); } String _getMetricIcon(String type) { switch (type.toLowerCase()) { case 'blood_pressure': return '🩺'; case 'heart_rate': return '💓'; case 'glucose': return '💉'; case 'spo2': return '🫁'; case 'weight': return '⚖️'; default: return '📊'; } } String _getMetricName(String type) { switch (type.toLowerCase()) { case 'blood_pressure': return '血压'; case 'heart_rate': return '心率'; case 'glucose': return '血糖'; case 'spo2': return '血氧'; case 'weight': return '体重'; default: return '健康指标'; } } }