import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/app_theme.dart'; import '../../../core/navigation_provider.dart'; import '../../../providers/auth_provider.dart'; import '../../../providers/chat_provider.dart'; import '../../../providers/data_providers.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: AppTheme.primaryLight, borderRadius: BorderRadius.circular(40), ), child: const Icon(Icons.health_and_safety, size: 40, color: AppTheme.primary), ), const SizedBox(height: 16), Text('开始和 AI 健康管家对话吧', style: Theme.of(context).textTheme.bodyMedium), const SizedBox(height: 8), const Text('记录健康数据,获取专业建议', style: TextStyle(fontSize: 17, color: Color(0xFF9E9E9E))), ], ), ); } return ListView.builder( controller: scrollCtrl, reverse: true, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), itemCount: messages.length, itemBuilder: (context, index) { final msg = messages[messages.length - 1 - index]; return _buildMessageContent(context, ref, msg, chatState); }, ); } // ─── 消息分发 ───────────────────────────────────────────── Widget _buildMessageContent(BuildContext context, WidgetRef ref, ChatMessage msg, ChatState chatState) { switch (msg.type) { case MessageType.agentWelcome: final storedAgent = _parseAgentFromName(msg.metadata?['agent'] as String?); return _buildAgentWelcomeCard(context, ref, msg, storedAgent); case MessageType.taskCard: return _buildTaskCardInChat(context, ref); case MessageType.dataConfirm: return _buildDataConfirmCard(context, ref, msg); case MessageType.medicationConfirm: return _buildMedicationConfirmCard(context, ref, msg); case MessageType.dietAnalysis: return _buildDietAnalysisCard(context, msg); case MessageType.reportAnalysis: return _buildReportAnalysisCard(context, msg); case MessageType.quickOptions: return _buildQuickOptionsCard(context, msg); default: // 只有当前正在流式回复的文本消息才显示"正在分析" if (!msg.isUser && chatState.isStreaming && msg.content.isEmpty) { return _buildThinkingBubble(context, chatState.thinkingText); } return _buildTextBubble(context, msg); } } // ═══════════════════════════════════════════════════════════ // 1. AgentWelcomeCard — 智能体欢迎卡片 // ═══════════════════════════════════════════════════════════ Widget _buildAgentWelcomeCard(BuildContext context, WidgetRef ref, ChatMessage msg, ActiveAgent agent) { final info = _agentInfo(agent); final actions = agent.actions; final screenWidth = MediaQuery.of(context).size.width; final colors = _agentColors(agent); return Align( alignment: Alignment.centerLeft, child: Container( margin: const EdgeInsets.only(bottom: 12), constraints: BoxConstraints(maxWidth: screenWidth * 0.92), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow(color: colors.gradient[0].withAlpha(30), blurRadius: 16, offset: const Offset(0, 4)), ], ), clipBehavior: Clip.antiAlias, child: Column( mainAxisSize: MainAxisSize.min, children: [ // ── 渐变色头部 ── Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(20, 24, 16, 20), decoration: BoxDecoration( gradient: LinearGradient( colors: colors.gradient, begin: Alignment.topLeft, end: Alignment.bottomRight, ), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 48, height: 48, decoration: BoxDecoration( color: Colors.white.withAlpha(30), borderRadius: BorderRadius.circular(14), ), child: Icon(info.$1, size: 26, color: Colors.white), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(info.$2, style: const TextStyle(fontSize: 21, fontWeight: FontWeight.w700, color: Colors.white)), const SizedBox(height: 4), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3), decoration: BoxDecoration( color: Colors.white.withAlpha(25), borderRadius: BorderRadius.circular(10), ), child: Text(info.$3, style: const TextStyle(fontSize: 15, color: Color(0xFFE8E6FF))), ), ], ), ), ], ), ), // ── 医生列表(一行三列)── if (agent == ActiveAgent.consultation) ...[ Padding( padding: const EdgeInsets.fromLTRB(18, 18, 18, 4), child: _buildDoctorCards(ref), ), ] else ...[ Padding( padding: const EdgeInsets.fromLTRB(18, 18, 18, 4), child: Wrap( spacing: 10, runSpacing: 10, children: actions.map((a) => _agentActionBtn(a, screenWidth, context, ref, colors)).toList(), ), ), ], const SizedBox(height: 12), ], ), ), ); } Widget _agentActionBtn(_AgentAction a, double screenWidth, BuildContext context, WidgetRef ref, _AgentColors colors) { return InkWell( onTap: () { if (a.label == '服药打卡') { _medicationCheckIn(ref, context); } else if (a.action == 'pickFoodCamera' || a.action == 'pickFoodGallery') { ref.read(dietActionProvider.notifier).trigger(a.action!); } else if (a.route != null) { if (a.route == 'camera' || a.route == 'gallery') { ref.read(cameraActionProvider.notifier).trigger(a.route!); } else { pushRoute(ref, a.route!); } } }, borderRadius: BorderRadius.circular(14), child: Container( width: ((screenWidth - 72) / (a.isWide ? 2 : 3)) - 10, padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 8), decoration: BoxDecoration( color: colors.bg, borderRadius: BorderRadius.circular(14), border: Border.all(color: colors.border, width: 1), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 38, height: 38, decoration: BoxDecoration( color: colors.iconBg, borderRadius: BorderRadius.circular(11), ), child: Icon(a.icon, size: 23, color: colors.accent), ), const SizedBox(height: 7), Text(a.label, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF333333))), ], ), ), ); } Widget _buildDoctorCards(WidgetRef ref) { const doctors = [ {'name': '张明', 'title': '主任医师', 'dept': '心脏康复科', 'desc': '冠心病、高血压术后管理', 'id': '468b82e2-d95a-4436-bff6-a50eecf99a66'}, {'name': '李芳', 'title': '副主任医师', 'dept': '营养科', 'desc': '糖尿病、甲状腺疾病管理', 'id': 'd4148733-b538-4398-af17-0c7592fc0c2d'}, {'name': '王建国', 'title': '主任医师', 'dept': '心血管内科', 'desc': '术后营养指导、饮食方案制定', 'id': 'ef0953c9-eb63-4d03-b6d7-050a1897d4a3'}, ]; return Row( children: [ for (var i = 0; i < doctors.length; i++) ...[ if (i > 0) const SizedBox(width: 8), Expanded(child: _doctorCard(doctors[i], ref)), ], ], ); } Widget _doctorCard(Map doc, WidgetRef ref) { return InkWell( onTap: () => pushRoute(ref, 'consultation', params: {'id': doc['id']!}), borderRadius: BorderRadius.circular(12), child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppTheme.primaryLight), ), child: Column(mainAxisSize: MainAxisSize.min, children: [ CircleAvatar( radius: 22, backgroundColor: AppTheme.primaryLight, child: Text(doc['name']![0], style: const TextStyle(fontSize: 21, fontWeight: FontWeight.w600, color: AppTheme.primary)), ), const SizedBox(height: 6), Text(doc['name']!, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF333333))), Text(doc['title']!, style: const TextStyle(fontSize: 14, color: Color(0xFF999999))), const SizedBox(height: 2), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: AppTheme.primaryLight, borderRadius: BorderRadius.circular(4), ), child: Text(doc['dept']!, style: const TextStyle(fontSize: 13, color: AppTheme.primary)), ), const SizedBox(height: 4), Text( doc['desc']!, style: const TextStyle(fontSize: 13, color: Color(0xFF888888), height: 1.3), maxLines: 2, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, ), ]), ), ); } // ═══════════════════════════════════════════════════════════ // 2. DataConfirmCard — 增强版数据确认卡片 // ═══════════════════════════════════════════════════════════ Widget _buildDataConfirmCard(BuildContext context, WidgetRef ref, 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; final recordTime = meta?['recordTime'] as String? ?? ''; final unit = meta?['unit'] as String? ?? _getMetricUnit(metricType); return Align( alignment: Alignment.centerLeft, child: Container( margin: const EdgeInsets.only(bottom: 12), constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.92), decoration: BoxDecoration( color: const Color(0xFFFFFFFF), borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(15), blurRadius: 14, offset: const Offset(0, 4))], ), clipBehavior: Clip.antiAlias, child: Column(mainAxisSize: MainAxisSize.min, children: [ Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 10), decoration: const BoxDecoration(gradient: LinearGradient(colors: [Color(0xFF4CAF50), AppTheme.success])), child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.info_outline, size: 21, color: Colors.white), const SizedBox(width: 6), Text(metricType == 'exercise' ? '请确认运动计划' : '请确认录入', style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: Colors.white)), ]), ), Padding(padding: const EdgeInsets.all(18), child: Column(children: [ Align(alignment: Alignment.centerLeft, child: Text(recordTime.isNotEmpty ? recordTime : _formatTime(msg.createdAt), style: const TextStyle(fontSize: 15, color: Color(0xFF9E9E9E)))), const SizedBox(height: 14), Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration(color: const Color(0xFFF9F8FF), borderRadius: BorderRadius.circular(16)), child: Row(children: [ Container(width: 52, height: 52, decoration: BoxDecoration(color: AppTheme.primaryLight, borderRadius: BorderRadius.circular(14)), child: Center(child: Text(_getMetricIcon(metricType), style: const TextStyle(fontSize: 30)))), const SizedBox(width: 14), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(_getMetricName(metricType), style: const TextStyle(fontSize: 16, color: Color(0xFF888888))), const SizedBox(height: 4), RichText(text: TextSpan(children: [ TextSpan(text: value, style: TextStyle(fontSize: 32, fontWeight: FontWeight.w800, color: abnormal ? AppTheme.error : const Color(0xFF1A1A2E))), TextSpan(text: ' $unit', style: TextStyle(fontSize: 17, color: abnormal ? const Color(0xFFE53970) : const Color(0xFF999999))), ])), ])), ]), ), if (abnormal) ...[ const SizedBox(height: 12), Container(width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration(color: const Color(0xFFFFF3F0), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFFFDAD4))), child: const Row(children: [Icon(Icons.warning_amber_rounded, size: 21, color: AppTheme.error), SizedBox(width: 8), Expanded(child: Text('数值偏高,建议关注', style: TextStyle(fontSize: 16, color: AppTheme.error, fontWeight: FontWeight.w500)))])), ], const SizedBox(height: 18), if (msg.confirmed) SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: null, icon: const Icon(Icons.check_circle, size: 21), label: const Text('录入成功'), style: ElevatedButton.styleFrom( backgroundColor: AppTheme.success, foregroundColor: Colors.white, disabledBackgroundColor: AppTheme.success, disabledForegroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), padding: const EdgeInsets.symmetric(vertical: 12), ), ), ) else Row(children: [ Expanded(child: _cardFilledBtn('确认录入', Icons.check, onTap: () { msg.confirmed = true; ref.invalidate(latestHealthProvider); final notifier = ref.read(chatProvider.notifier); notifier.markNeedsRebuild(); })), ]), ])), ]), ), ); } // ═══════════════════════════════════════════════════════════ // 3. MedicationConfirmCard — 增强版用药确认卡片 // ═══════════════════════════════════════════════════════════ Widget _buildMedicationConfirmCard(BuildContext context, WidgetRef ref, ChatMessage msg) { final meta = msg.metadata; final name = meta?['name'] as String? ?? ''; final dosage = meta?['dosage'] as String? ?? ''; final time = meta?['time'] as String? ?? ''; final frequency = meta?['frequency'] as String? ?? ''; final duration = meta?['duration_days'] as int?; return Align( alignment: Alignment.centerLeft, child: Container( margin: const EdgeInsets.only(bottom: 12), constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.92), decoration: BoxDecoration( color: const Color(0xFFFFFFFF), borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(15), blurRadius: 14, offset: const Offset(0, 4))], ), clipBehavior: Clip.antiAlias, child: Column( mainAxisSize: MainAxisSize.min, children: [ // ── 药品头部 ── Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(18, 20, 18, 16), decoration: const BoxDecoration( gradient: LinearGradient(colors: [Color(0xFFE8F0FE), AppTheme.primaryLight]), ), child: Row( children: [ Container( width: 46, height: 46, decoration: BoxDecoration( color: const Color(0xFFFFF3E0), borderRadius: BorderRadius.circular(13), ), child: const Center(child: Text('💊', style: TextStyle(fontSize: 28))), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(name, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: Color(0xFF1A1A2E))), if (dosage.isNotEmpty) ...[ const SizedBox(height: 3), Text(dosage, style: const TextStyle(fontSize: 16, color: Color(0xFF777777))), ], ], ), ), ], ), ), Padding( padding: const EdgeInsets.fromLTRB(18, 16, 18, 6), child: Column( children: [ // 服药时间 & 频率 _medInfoRow(Icons.schedule_outlined, time.isNotEmpty ? '服药时间:$time' : ''), if (frequency.isNotEmpty) _medInfoRow(Icons.repeat, '频率:${_freqLabel(frequency)}'), if (duration != null && duration > 0) _medInfoRow(Icons.calendar_today_outlined, '服用天数:$duration 天'), const SizedBox(height: 16), if (msg.confirmed) SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: null, icon: const Icon(Icons.check_circle, size: 21), label: const Text('录入成功'), style: ElevatedButton.styleFrom( backgroundColor: AppTheme.success, foregroundColor: Colors.white, disabledBackgroundColor: AppTheme.success, disabledForegroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), padding: const EdgeInsets.symmetric(vertical: 12), ), ), ) else SizedBox(width: double.infinity, child: _cardFilledBtn('确认录入', Icons.check_circle_outlined, onTap: () { msg.confirmed = true; ref.invalidate(medicationListProvider); final notifier = ref.read(chatProvider.notifier); notifier.markNeedsRebuild(); })), const SizedBox(height: 8), ], ), ), ], ), ), ); } Widget _medInfoRow(IconData icon, String text) { return Padding( padding: const EdgeInsets.only(bottom: 10), child: Row( children: [ Icon(icon, size: 20, color: const Color(0xFF888888)), const SizedBox(width: 8), Text(text, style: const TextStyle(fontSize: 16, color: Color(0xFF444444))), ], ), ); } String _freqLabel(String freq) { switch (freq.toLowerCase()) { case 'daily': return '每天'; case 'everyotherday': return '隔天'; case 'weekly': return '每周'; default: return freq; } } // ═══════════════════════════════════════════════════════════ // 4. DietAnalysisCard — 增强版饮食分析卡片 // ═══════════════════════════════════════════════════════════ Widget _buildDietAnalysisCard(BuildContext context, ChatMessage msg) { final meta = msg.metadata; final foods = meta?['foods'] as List? ?? []; final totalCalories = meta?['totalCalories'] as int? ?? 0; final advice = meta?['advice'] as String? ?? '饮食均衡,多吃蔬菜水果,减少高油高糖食物摄入。'; return Align( alignment: Alignment.centerLeft, child: Container( margin: const EdgeInsets.only(bottom: 12), constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.92), decoration: BoxDecoration( color: const Color(0xFFFFFFFF), borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(15), blurRadius: 14, offset: const Offset(0, 4))], ), clipBehavior: Clip.antiAlias, child: Column( mainAxisSize: MainAxisSize.min, children: [ // ── 头部 ── Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 12), decoration: const BoxDecoration(gradient: LinearGradient(colors: [Color(0xFFFFF8E1), Color(0xFFFFF3E0)])), child: const Row(mainAxisAlignment: MainAxisAlignment.center, children: [ Text('🍽️ ', style: TextStyle(fontSize: 21)), Text('饮食分析结果', style: TextStyle(fontSize: 19, fontWeight: FontWeight.w700, color: Color(0xFF1A1A2E))), ]), ), Padding( padding: const EdgeInsets.all(16), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ // ── 总热量(仅 >0 时显示) ── if (totalCalories > 0) ...[ Center(child: Column(children: [ Text('$totalCalories', style: const TextStyle(fontSize: 40, fontWeight: FontWeight.w800, color: Color(0xFFFF8F00))), const Text('千卡 (kcal)', style: TextStyle(fontSize: 15, color: Color(0xFFAAAAAA))), ])), const SizedBox(height: 16), ], // ── 识别食物列表 ── if (foods.isNotEmpty) ...[ const Text('识别结果', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const SizedBox(height: 10), ...foods.map((food) { final f = food is Map ? food : {}; final name = f['name'] as String? ?? ''; final calories = f['calories'] as num? ?? 0; final portion = f['portion'] as String?; final nutrients = f['nutrients'] as String?; return Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.all(10), decoration: BoxDecoration(color: const Color(0xFFFAFAFA), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFF0F0F0))), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Expanded(child: Text(name, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A)))), if (calories > 0) Text('${calories is int ? calories : calories.toInt()} kcal', style: const TextStyle(fontSize: 16, color: Color(0xFF888888))), ]), if (portion != null && portion.isNotEmpty) Padding(padding: const EdgeInsets.only(top: 4), child: Text(portion, style: TextStyle(fontSize: 15, color: Colors.grey[500]))), if (nutrients != null && nutrients.isNotEmpty) Padding(padding: const EdgeInsets.only(top: 2), child: Text(nutrients, style: TextStyle(fontSize: 14, color: Colors.grey[500]))), ]), ); }), const SizedBox(height: 6), ], // ── AI 建议 ── const SizedBox(height: 14), const Text('AI 建议', style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const SizedBox(height: 6), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration(color: AppTheme.primaryLight, borderRadius: BorderRadius.circular(10)), child: Text(advice, style: const TextStyle(fontSize: 16, height: 1.6, color: Color(0xFF555555))), ), ]), ), ], ), ), ); } // ═══════════════════════════════════════════════════════════ // 5. ReportAnalysisCard — 增强版报告分析卡片 // ═══════════════════════════════════════════════════════════ Widget _buildReportAnalysisCard(BuildContext context, ChatMessage msg) { final meta = msg.metadata; final reportType = meta?['type'] as String? ?? '体检报告'; final reportDate = meta?['date'] as String? ?? ''; final indicators = meta?['indicators'] as List? ?? []; final summary = meta?['summary'] as String? ?? ''; return Align( alignment: Alignment.centerLeft, child: Container( margin: const EdgeInsets.only(bottom: 12), constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.92), decoration: BoxDecoration( color: const Color(0xFFFFFFFF), borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(15), blurRadius: 14, offset: const Offset(0, 4))], ), clipBehavior: Clip.antiAlias, child: Column( mainAxisSize: MainAxisSize.min, children: [ // ── 报告头部 ── Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(18, 18, 18, 14), decoration: const BoxDecoration( gradient: LinearGradient(colors: [Color(0xFFE8EAF6), Color(0xFFEDE7F6)]), ), child: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: const Color(0xFFC5CAE9), borderRadius: BorderRadius.circular(10), ), child: const Icon(Icons.description_outlined, size: 23, color: Color(0xFF3F51B5)), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(reportType, style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w700, color: Color(0xFF1A1A2E))), if (reportDate.isNotEmpty) Text(reportDate, style: const TextStyle(fontSize: 15, color: Color(0xFF888888))), ], ), ), ], ), ), Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 指标表格 Container( decoration: BoxDecoration( color: const Color(0xFFFAFAFC), borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFFEEEEEE), width: 1), ), child: Column( children: [ // 表头 Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: const BoxDecoration( color: Color(0xFFF5F4FA), borderRadius: BorderRadius.vertical(top: Radius.circular(12)), ), child: const Row( children: [ Expanded(flex: 2, child: Text('指标名称', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF666666)))), Expanded(flex: 1, child: Text('数值', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF666666)), textAlign: TextAlign.center)), Expanded(flex: 1, child: Text('状态', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF666666)), textAlign: TextAlign.center)), ], ), ), // 数据行 ...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'; final refRange = i['refRange'] as String? ?? ''; final isAbnormal = status != 'normal'; Color sc; switch (status) { case 'high': sc = AppTheme.error; break; case 'low': sc = const Color(0xFFF9A825); break; default: sc = AppTheme.success; } return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: isAbnormal ? const Color(0xFFFFF8F5) : Colors.transparent, border: Border( bottom: BorderSide(color: const Color(0xFFF0F0F0), width: 0.5), ), ), child: Row( children: [ Expanded( flex: 2, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(name, style: TextStyle(fontSize: 16, color: isAbnormal ? AppTheme.error : const Color(0xFF333333), fontWeight: isAbnormal ? FontWeight.w600 : FontWeight.normal)), if (refRange.isNotEmpty) Text('参考:$refRange', style: const TextStyle(fontSize: 13, color: Color(0xFFAAAAAA))), ], ), ), Expanded(flex: 1, child: Text(value, textAlign: TextAlign.center, style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: sc))), Expanded( flex: 1, child: Center( child: Container( width: 8, height: 8, decoration: BoxDecoration(shape: BoxShape.circle, color: sc), ), ), ), ], ), ); }), ], ), ), // AI 解读摘要 if (summary.isNotEmpty) ...[ const SizedBox(height: 14), Container( width: double.infinity, padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: const Color(0xFFF3EFFF), borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFFDDD8FF), width: 0.8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Row( children: [ Icon(Icons.auto_awesome, size: 19, color: AppTheme.primary), SizedBox(width: 6), Text('AI 解读摘要', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: AppTheme.primary)), ], ), const SizedBox(height: 8), Text(summary, style: const TextStyle(fontSize: 16, color: Color(0xFF555555), height: 1.5)), ], ), ), ], // 查看完整解读按钮 const SizedBox(height: 14), SizedBox( width: double.infinity, child: _cardFilledBtn('查看完整解读', Icons.article_outlined), ), const SizedBox(height: 4), ], ), ), ], ), ), ); } // 6. QuickOptionsCard — 优化样式 // ═══════════════════════════════════════════════════════════ 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.92), decoration: BoxDecoration( color: const Color(0xFFFFFFFF), borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(15), blurRadius: 14, offset: const Offset(0, 4))], ), child: Padding( padding: const EdgeInsets.fromLTRB(18, 16, 18, 14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(msg.content, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500, color: Color(0xFF1A1A2E))), const SizedBox(height: 14), Wrap(spacing: 8, runSpacing: 8, children: options.map((opt) { final o = opt as Map? ?? {}; return ElevatedButton( onPressed: () {}, style: ElevatedButton.styleFrom( backgroundColor: AppTheme.primaryLight, foregroundColor: AppTheme.primary, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 11), ), child: Text(o['label'] as String? ?? '', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), ); }).toList()), ], ), ), ), ); } // ═══════════════════════════════════════════════════════════ // 公共组件:思考气泡 & 文本气泡 // ═══════════════════════════════════════════════════════════ Widget _buildThinkingBubble(BuildContext context, String? thinkingText) { return Align( alignment: Alignment.centerLeft, child: Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.symmetric(horizontal: 18, 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(0xFFD8DCFD), width: 1.5), boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(12), blurRadius: 10, offset: const Offset(0, 3))], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 26, height: 26, padding: const EdgeInsets.all(5), decoration: BoxDecoration( color: AppTheme.primaryLight, borderRadius: BorderRadius.circular(13), ), child: const CircularProgressIndicator(strokeWidth: 2.2, color: AppTheme.primary), ), const SizedBox(width: 10), Text(thinkingText?.isNotEmpty == true ? thinkingText! : '正在分析...', style: const TextStyle(fontSize: 17, color: Color(0xFF999999))), ], ), ), ); } Widget _buildTextBubble(BuildContext context, ChatMessage msg) { final isUser = msg.isUser; final imageUrl = msg.metadata?['imageUrl'] as String?; final localPath = msg.metadata?['localImagePath'] as String?; final hasImage = imageUrl != null || localPath != null; 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.82), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( color: isUser ? AppTheme.primary : 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(0xFFD8DCFD), width: 1.5), boxShadow: isUser ? [] : [BoxShadow(color: AppTheme.primary.withAlpha(12), blurRadius: 10, offset: const Offset(0, 3))], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 文字内容 if (isUser) SelectableText(msg.content, style: const TextStyle(fontSize: 19, color: Colors.white, height: 1.4)) else MarkdownBody( data: msg.content, selectable: true, styleSheet: MarkdownStyleSheet( p: const TextStyle(fontSize: 19, color: Color(0xFF1A1A1A), height: 1.5), h1: const TextStyle(fontSize: 21, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)), h2: const TextStyle(fontSize: 19, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)), code: const TextStyle(fontSize: 17, backgroundColor: AppTheme.primaryLight), ), ), // 图片缩略图(在文字下方) if (hasImage) Padding( padding: const EdgeInsets.only(top: 8), child: GestureDetector( onTap: () => _showFullImage(context, localPath ?? imageUrl), child: ClipRRect( borderRadius: BorderRadius.circular(8), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 160, maxHeight: 120), child: localPath != null ? Image.file(File(localPath), fit: BoxFit.cover) : imageUrl != null ? Image.network(imageUrl, fit: BoxFit.cover, errorBuilder: (_, e, s) => Container( width: 80, height: 60, decoration: BoxDecoration(color: const Color(0xFFF0F0F0), borderRadius: BorderRadius.circular(8)), child: const Icon(Icons.image, size: 28, color: Color(0xFFBBBBBB)), )) : const SizedBox.shrink(), ), ), ), ), if (!isUser && msg.content.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 10), child: Row(children: [ const CircleAvatar(radius: 10, backgroundColor: AppTheme.primaryLight, child: Icon(Icons.chat_bubble_outline, size: 17, color: AppTheme.primary)), const SizedBox(width: 6), const Text('健康管家', style: TextStyle(fontSize: 15, color: Color(0xFF9E9E9E))), const SizedBox(width: 4), const Text('仅供参考', style: TextStyle(fontSize: 14, color: Color(0xFFCCCCCC))), ]), ), ], ), ), ); } // ═══════════════════════════════════════════════════════════ // 公共组件:通用按钮 // ═══════════════════════════════════════════════════════════ Widget _cardFilledBtn(String label, IconData icon, {VoidCallback? onTap}) { return ElevatedButton( onPressed: onTap ?? () {}, style: ElevatedButton.styleFrom( backgroundColor: AppTheme.primary, foregroundColor: Colors.white, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), padding: const EdgeInsets.symmetric(vertical: 11), ), child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, size: 19), const SizedBox(width: 5), Text(label, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500)), ]), ); } Widget _cardOutlineBtn(String label, IconData icon, {VoidCallback? onTap}) { return OutlinedButton( onPressed: onTap ?? () {}, style: OutlinedButton.styleFrom( foregroundColor: AppTheme.primary, side: const BorderSide(color: AppTheme.primary, width: 1.2), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), padding: const EdgeInsets.symmetric(vertical: 11), ), child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, size: 18), const SizedBox(width: 4), Text(label, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500)), ]), ); } // ═══════════════════════════════════════════════════════════ // 工具方法 // ═══════════════════════════════════════════════════════════ static void _showFullImage(BuildContext context, String? path) { if (path == null) return; showDialog( context: context, builder: (ctx) => Dialog( backgroundColor: Colors.black, insetPadding: const EdgeInsets.all(24), child: Stack(alignment: Alignment.center, children: [ ClipRRect(borderRadius: BorderRadius.circular(12), child: InteractiveViewer(child: path.startsWith('http') ? Image.network(path, fit: BoxFit.contain) : Image.file(File(path), fit: BoxFit.contain))), Positioned(top: 8, right: 8, child: GestureDetector( onTap: () => Navigator.pop(ctx), child: Container(padding: const EdgeInsets.all(6), decoration: BoxDecoration(color: Colors.white54, shape: BoxShape.circle), child: const Icon(Icons.close, size: 21)), )), ]), ), ); } String _formatTime(DateTime dt) { final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); final thatDay = DateTime(dt.year, dt.month, dt.day); if (thatDay == today) { return '今天 ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; } if (thatDay == today.subtract(const Duration(days: 1))) { return '昨天 ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; } return '${dt.month}/${dt.day} ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; } String _getMetricUnit(String type) { switch (type.toLowerCase()) { case 'blood_pressure': return 'mmHg'; case 'heart_rate': return 'bpm'; case 'glucose': return 'mmol/L'; case 'spo2': return '%'; case 'weight': return 'kg'; case 'exercise': return '分钟/天'; default: return ''; } } String _getMetricIcon(String type) { switch (type.toLowerCase()) { case 'blood_pressure': return '🩺'; case 'heart_rate': return '💓'; case 'glucose': return '💉'; case 'spo2': return '🫁'; case 'weight': return '⚖️'; case 'exercise': 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 '体重'; case 'exercise': return '运动计划'; default: return '健康指标'; } } static void _medicationCheckIn(WidgetRef ref, BuildContext context) async { try { final api = ref.read(apiClientProvider); final reminders = await ref.read(medicationReminderProvider.future); if (reminders.isEmpty) { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('暂无待服药记录'), backgroundColor: Color(0xFFFF9800)), ); return; } for (final m in reminders) { final id = m['id']?.toString() ?? ''; final time = m['scheduledTime']?.toString() ?? ''; if (id.isEmpty) continue; await api.post('/api/medications/$id/confirm-dose', data: {'scheduledTime': time, 'status': 'taken'}); } ref.invalidate(medicationReminderProvider); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('打卡成功 已记录 ${reminders.length} 项服药'), backgroundColor: AppTheme.success, ), ); } catch (e) { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('打卡失败:$e'), backgroundColor: Colors.red), ); } } static _AgentColors _agentColors(ActiveAgent agent) { return switch (agent) { ActiveAgent.consultation => _AgentColors( gradient: [const Color(0xFFC5D5F8), const Color(0xFFA0B8F0), const Color(0xFF7B98E0)], bg: const Color(0xFFF0F4FF), border: const Color(0xFFD8E0FA), iconBg: const Color(0xFFE4ECFC), accent: const Color(0xFF7B98E0), ), ActiveAgent.health => _AgentColors( gradient: [const Color(0xFFB8E6CF), const Color(0xFF8ED4AE), const Color(0xFF5FB88D)], bg: const Color(0xFFF0FAF4), border: const Color(0xFFD0ECD8), iconBg: const Color(0xFFE4F8EC), accent: const Color(0xFF5FB88D), ), ActiveAgent.diet => _AgentColors( gradient: [const Color(0xFFFFD8B8), const Color(0xFFFFC896), const Color(0xFFF0A060)], bg: const Color(0xFFFFF6F0), border: const Color(0xFFFFE8D4), iconBg: const Color(0xFFFFEEDC), accent: const Color(0xFFF0A060), ), ActiveAgent.medication => _AgentColors( gradient: [const Color(0xFFFFD4E0), const Color(0xFFFFB8CC), const Color(0xFFE898A8)], bg: const Color(0xFFFFF0F4), border: const Color(0xFFFFE0E8), iconBg: const Color(0xFFFFE8EE), accent: const Color(0xFFE898A8), ), ActiveAgent.report => _AgentColors( gradient: [const Color(0xFFD8D0F0), const Color(0xFFC4B8EC), const Color(0xFFA898D8)], bg: const Color(0xFFF8F4FF), border: const Color(0xFFECE4F8), iconBg: const Color(0xFFF0E8FC), accent: const Color(0xFFA898D8), ), ActiveAgent.exercise => _AgentColors( gradient: [const Color(0xFFB8E0E0), const Color(0xFF90D0D0), const Color(0xFF68B4B4)], bg: const Color(0xFFF0FAFA), border: const Color(0xFFD4ECEC), iconBg: const Color(0xFFE4F4F4), accent: const Color(0xFF68B4B4), ), _ => _AgentColors( gradient: [const Color(0xFFC5D0F8), const Color(0xFFA0B0F0), const Color(0xFF7B90E0)], bg: const Color(0xFFF5F5FF), border: const Color(0xFFE0E0F8), iconBg: const Color(0xFFEDEDFC), accent: const Color(0xFF7B90E0), ), }; } static (_AgentIcon, String, String) _agentInfo(ActiveAgent agent) { return switch (agent) { ActiveAgent.health => (Icons.favorite_border, '记数据', '录入血压、血糖、心率等日常指标'), ActiveAgent.diet => (Icons.restaurant, '拍饮食', '拍照识别食物热量和营养成分'), ActiveAgent.medication => (Icons.medication, '药管家', '管理药品、提醒服药、追踪用量'), ActiveAgent.consultation => (Icons.local_hospital, '问诊', '在线咨询医生,描述症状获取建议'), ActiveAgent.report => (Icons.assignment, '看报告', '上传体检报告,AI 辅助解读'), ActiveAgent.exercise => (Icons.directions_run, '运动', '制定运动计划,打卡记录进度'), _ => (Icons.smart_toy, 'AI 助手', '您的智能健康管家'), }; } static ActiveAgent _parseAgentFromName(String? name) { switch (name) { case 'consultation': return ActiveAgent.consultation; case 'health': return ActiveAgent.health; case 'diet': return ActiveAgent.diet; case 'medication': return ActiveAgent.medication; case 'report': return ActiveAgent.report; case 'exercise': return ActiveAgent.exercise; default: return ActiveAgent.default_; } } Widget _buildTaskCardInChat(BuildContext context, WidgetRef ref) { final health = ref.watch(latestHealthProvider); final reminders = ref.watch(medicationReminderProvider); return health.when( data: (data) => _taskCardBubble(context, ref, data, reminders), loading: () => _taskCardBubble(context, ref, {}, const AsyncValue.loading()), error: (_, e) => _taskCardBubble(context, ref, {}, const AsyncValue.loading()), ); } Widget _taskCardBubble(BuildContext context, WidgetRef ref, Map healthData, AsyncValue>> reminders) { final now = DateTime.now(); final tasks = []; // 1. 健康数据摘要行 final bp = healthData['BloodPressure']; final bpText = bp is Map ? '${bp['systolic'] ?? '--'}/${bp['diastolic'] ?? '--'}' : null; final hr = healthData['HeartRate']; final hrText = hr is int ? '$hr' : null; final bs = healthData['BloodSugar']; final bsText = bs is num ? '$bs' : null; final bo = healthData['BloodOxygen']; final boText = bo is num ? '$bo' : null; final wt = healthData['Weight']; final wtText = wt is num ? '$wt' : null; final allNull = bpText == null && hrText == null && bsText == null && boText == null && wtText == null; if (!allNull) { tasks.add(_taskRow(context, Icons.check_circle, '今日已记录', trailing: [ if (bpText != null) '血压 $bpText', if (hrText != null) '心率 $hrText', if (bsText != null) '血糖 $bsText', if (boText != null) '血氧 $boText', if (wtText != null) '体重 $wtText', ].join(' · '), status: 'done', onTap: () => pushRoute(ref, 'trend'))); } // 2. 用药提醒 — 一行汇总 reminders.whenOrNull(data: (meds) { final overdue = meds.where((m) => m['status'] == 'overdue').length; final upcoming = meds.where((m) => m['status'] == 'upcoming').length; final taken = meds.where((m) => m['status'] == 'taken').length; final total = overdue + upcoming + taken; if (total > 0) { final parts = []; if (overdue > 0) parts.add('$overdue项过期'); if (upcoming > 0) parts.add('$upcoming项待服'); if (taken > 0) parts.add('$taken项已服'); tasks.add(_taskRow( context, Icons.medication_rounded, '用药打卡 ($total项)', trailing: parts.join(' · '), status: overdue > 0 ? 'overdue' : upcoming > 0 ? 'pending' : 'done', onTap: () => pushRoute(ref, 'medCheckIn'), )); } }); final hasMeds = reminders.asData?.value != null && (reminders.asData!.value).isNotEmpty; if (!hasMeds) { tasks.add(_taskRow(context, Icons.medication_rounded, '暂无用药提醒', status: 'pending')); } // 3. 运动 — 使用 currentExercisePlanProvider 同步读取 final exercisePlan = ref.watch(currentExercisePlanProvider); exercisePlan.whenOrNull(data: (plan) { if (plan != null) { final items = (plan['items'] as List?)?.cast>() ?? []; final today = now.weekday % 7; // Dart: Mon=1...Sun=7 → C#: Sun=0 Mon=1...Sat=6 final todayItem = items.cast?>().firstWhere( (i) => i?['dayOfWeek'] == today, orElse: () => null); if (todayItem != null && todayItem['isRestDay'] != true) { final done = todayItem['isCompleted'] == true; final name = items.isNotEmpty ? (items.first['exerciseType']?.toString() ?? '运动') : '运动'; final dur = items.isNotEmpty ? (items.first['durationMinutes']?.toString() ?? '30') : '30'; tasks.add(_taskRow(context, Icons.directions_run, '$name $dur分钟', status: done ? 'done' : (now.hour >= 18 ? 'overdue' : 'pending'), onTap: () => pushRoute(ref, 'exercisePlan'), )); } } }); // 4. 异常指标 if (bp is Map) { final s = bp['systolic']; if (s is int && s >= 140) { tasks.add(_taskRow( context, Icons.warning_amber_rounded, '血压 $s/${bp['diastolic'] ?? '--'} 偏高', status: 'warning', onTap: () => pushRoute(ref, 'trend', params: {'type': 'blood_pressure'}), )); } } return Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(10), blurRadius: 8, offset: const Offset(0, 2))], ), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Icon(Icons.today, size: 21, color: AppTheme.primary), const SizedBox(width: 8), const Text('今日任务', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), ]), const SizedBox(height: 10), ...tasks, ]), ); } Widget _taskRow(BuildContext context, IconData icon, String label, {String status = 'pending', String? trailing, VoidCallback? onTap}) { final colors = {'done': AppTheme.success, 'warning': const Color(0xFFFF9800), 'pending': const Color(0xFF9E9E9E), 'overdue': AppTheme.error}; final icons = {'done': Icons.check_circle, 'warning': Icons.warning, 'pending': Icons.circle_outlined, 'overdue': Icons.error}; final isOverdue = status == 'overdue'; return Padding( padding: const EdgeInsets.only(bottom: 8), child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), decoration: BoxDecoration( color: isOverdue ? const Color(0xFFFFEBEE) : Colors.transparent, borderRadius: BorderRadius.circular(8), ), child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [ Container(width: 30, height: 30, decoration: BoxDecoration(color: AppTheme.primaryLight, borderRadius: BorderRadius.circular(8)), child: Icon(icon, size: 18, color: AppTheme.primary)), const SizedBox(width: 10), Expanded(child: Text(trailing ?? label, style: const TextStyle(fontSize: 16, color: Color(0xFF333333)))), Icon(icons[status] ?? Icons.circle_outlined, size: 21, color: colors[status] ?? Colors.grey), ]), ), ), ); } } // ════════════════════════════════════════════════════════════════ // 内部数据类 // ════════════════════════════════════════════════════════════════ typedef _AgentIcon = IconData; class _AgentColors { final List gradient; final Color bg; final Color border; final Color iconBg; final Color accent; const _AgentColors({ required this.gradient, required this.bg, required this.border, required this.iconBg, required this.accent, }); } class _AgentAction { final String label; final IconData icon; final bool isWide; final String? route; final String? action; const _AgentAction({required this.label, required this.icon, this.isWide = false, this.route, this.action}); } final _agentActions = >{ ActiveAgent.health: [ _AgentAction(label: '查看趋势', icon: Icons.trending_up, isWide: true, route: 'trend'), ], ActiveAgent.diet: [ _AgentAction(label: '拍照识别', icon: Icons.camera_alt_outlined, isWide: true, action: 'pickFoodCamera'), _AgentAction(label: '上传照片', icon: Icons.photo_library_outlined, isWide: true, action: 'pickFoodGallery'), ], ActiveAgent.medication: [ _AgentAction(label: '用药管理', icon: Icons.medication_liquid_outlined, isWide: true, route: 'medications'), _AgentAction(label: '服药打卡', icon: Icons.check_circle_outline, isWide: true), ], ActiveAgent.consultation: [], ActiveAgent.report: [ _AgentAction(label: '上传报告', icon: Icons.upload_file_outlined, isWide: true, route: 'reports'), _AgentAction(label: '查看历史', icon: Icons.history_outlined, isWide: true, route: 'reports'), ], ActiveAgent.exercise: [ _AgentAction(label: '查看计划', icon: Icons.calendar_month_outlined, isWide: true, route: 'exercisePlan'), _AgentAction(label: '新建计划', icon: Icons.add_task_outlined, isWide: true, route: 'exerciseCreate'), ], }; extension _AgentActionsExt on ActiveAgent { List<_AgentAction> get actions => _agentActions[this] ?? [const _AgentAction(label: '开始对话', icon: Icons.chat_outlined)]; } // ════════════════════════════════════════════════════════════════ // 可展开的 AI 建议小组件 // ════════════════════════════════════════════════════════════════ class _ExpandableAdvice extends StatefulWidget { final String advice; const _ExpandableAdvice({required this.advice}); @override State<_ExpandableAdvice> createState() => _ExpandableAdviceState(); } class _ExpandableAdviceState extends State<_ExpandableAdvice> { bool _expanded = false; @override Widget build(BuildContext context) { return GestureDetector( onTap: () => setState(() => _expanded = !_expanded), child: Container( width: double.infinity, padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: const Color(0xFFF7F5FF), borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFFE8E4FF), width: 0.8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.lightbulb_outline, size: 19, color: AppTheme.primary), const SizedBox(width: 6), const Text('AI 建议', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: AppTheme.primary)), const Spacer(), Icon(_expanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, size: 21, color: const Color(0xFFAAAAAA)), ], ), if (_expanded) ...[ const SizedBox(height: 10), Text(widget.advice, style: const TextStyle(fontSize: 16, color: Color(0xFF555555), height: 1.6)), ], ], ), ), ); } }