import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/navigation_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: const Color(0xFFF0F2FF), borderRadius: BorderRadius.circular(40), ), child: const Icon(Icons.health_and_safety, size: 40, color: Color(0xFF8B9CF7)), ), const SizedBox(height: 16), Text('开始和 AI 健康管家对话吧', style: Theme.of(context).textTheme.bodyMedium), const SizedBox(height: 8), const Text('记录健康数据,获取专业建议', style: TextStyle(fontSize: 14, 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, 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: // 只有当前正在流式回复的文本消息才显示"正在分析" 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: 18, 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: 12, color: Color(0xFFE8E6FF))), ), ], ), ), ], ), ), // ── 快捷操作按钮网格 ── Padding( padding: const EdgeInsets.fromLTRB(18, 18, 18, 4), child: Wrap( spacing: 10, runSpacing: 10, children: agent == ActiveAgent.consultation ? _buildDoctorCards(screenWidth, ref) : 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.action == 'createPlan') { _createExercisePlan(ref, context); } else if (a.action == 'checkIn') { _exerciseCheckIn(ref, context); } else if (a.label == '服药打卡') { _medicationCheckIn(ref, context); } 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: 20, color: colors.accent), ), const SizedBox(height: 7), Text(a.label, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500, color: Color(0xFF333333))), ], ), ), ); } List _buildDoctorCards(double screenWidth, WidgetRef ref) { const doctors = [ {'name': '张医生', 'title': '主任医师', 'dept': '心内科', 'desc': '冠心病、高血压术后管理', 'id': 'doc_1'}, {'name': '李医生', 'title': '副主任医师', 'dept': '内分泌科', 'desc': '糖尿病、甲状腺疾病管理', 'id': 'doc_2'}, {'name': '王医生', 'title': '主治医师', 'dept': '营养科', 'desc': '术后营养指导、饮食方案制定', 'id': 'doc_3'}, ]; return doctors.map((d) => _doctorCard(d, screenWidth, ref)).toList(); } Widget _doctorCard(Map doc, double screenWidth, WidgetRef ref) { return InkWell( onTap: () => pushRoute(ref, 'consultation', params: {'id': doc['id']!}), borderRadius: BorderRadius.circular(14), child: Container( width: screenWidth * 0.38, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFF0F2FF)), ), child: Column(children: [ CircleAvatar( radius: 24, backgroundColor: const Color(0xFFF0F2FF), child: Text(doc['name']![0], style: const TextStyle(fontSize: 20, color: Color(0xFF8B9CF7))), ), const SizedBox(height: 8), Text(doc['name']!, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)), Text(doc['title']!, style: const TextStyle(fontSize: 11, color: Color(0xFF999999))), const SizedBox(height: 2), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: const Color(0xFFF0F2FF), borderRadius: BorderRadius.circular(4), ), child: Text(doc['dept']!, style: const TextStyle(fontSize: 10, color: Color(0xFF8B9CF7))), ), const SizedBox(height: 6), Text( doc['desc']!, style: const TextStyle(fontSize: 10, color: Color(0xFF888888), height: 1.3), maxLines: 2, overflow: TextOverflow.ellipsis, ), ]), ), ); } // ═══════════════════════════════════════════════════════════ // 2. DataConfirmCard — 增强版数据确认卡片 // ═══════════════════════════════════════════════════════════ 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; final recordTime = meta?['recordTime'] as String? ?? ''; final unit = meta?['unit'] as String? ?? _getMetricUnit(metricType); final trend = meta?['trend'] as List? ?? [0.6, 0.8, 0.5]; return Align( alignment: Alignment.centerLeft, child: Container( margin: const EdgeInsets.only(bottom: 12), constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.88), decoration: BoxDecoration( color: const Color(0xFFFFFFFF), borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: const Color(0xFF8B9CF7).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), Color(0xFF43A047)]), ), child: const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.check_circle, size: 18, color: Colors.white), SizedBox(width: 6), Text('✓ 数据已记录', style: TextStyle(fontSize: 14, 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: 12, 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: const Color(0xFFEDEAFF), borderRadius: BorderRadius.circular(14), ), child: Center(child: Text(_getMetricIcon(metricType), style: const TextStyle(fontSize: 26))), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(_getMetricName(metricType), style: const TextStyle(fontSize: 13, color: Color(0xFF888888))), const SizedBox(height: 4), RichText( text: TextSpan( children: [ TextSpan(text: value, style: TextStyle(fontSize: 28, fontWeight: FontWeight.w800, color: abnormal ? const Color(0xFFE53935) : const Color(0xFF1A1A2E))), TextSpan(text: ' $unit', style: TextStyle(fontSize: 14, 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), width: 1), ), child: const Row( children: [ Icon(Icons.warning_amber_rounded, size: 18, color: Color(0xFFE53935)), SizedBox(width: 8), Expanded(child: Text('⚠️ 数值偏高,建议关注', style: TextStyle(fontSize: 13, color: Color(0xFFE53935), fontWeight: FontWeight.w500))), ], ), ), ], // 迷你趋势图(最近3次) const SizedBox(height: 16), Row( children: [ const Text('近期趋势', style: TextStyle(fontSize: 12, color: Color(0xFFAAAAAA))), const Spacer(), const Text('最近3次', style: TextStyle(fontSize: 11, color: Color(0xFFCCCCCC))), ], ), const SizedBox(height: 8), SizedBox( height: 36, child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: trend.asMap().entries.map((e) { final h = (e.value * 32).clamp(6.0, 32.0); return Padding( padding: EdgeInsets.only(right: e.key < trend.length - 1 ? 10 : 0), child: Column( mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: [ Container( width: 22, height: h, decoration: BoxDecoration( color: e.key == trend.length - 1 ? const Color(0xFF8B9CF7) : const Color(0xFFD5D0FF), borderRadius: BorderRadius.circular(5), ), ), const SizedBox(height: 4), Text('${e.key + 1}', style: const TextStyle(fontSize: 9, color: Color(0xFFBBBBBB))), ], ), ); }).toList(), ), ), // 底部操作按钮 const SizedBox(height: 18), Row(children: [ Expanded(child: _cardOutlineBtn('编辑', Icons.edit_outlined)), const SizedBox(width: 8), Expanded(child: _cardFilledBtn('确认', Icons.check)), const SizedBox(width: 8), Expanded(child: _cardOutlineBtn('查看详情', Icons.trending_up_outlined)), ]), ], ), ), ], ), ), ); } // ═══════════════════════════════════════════════════════════ // 3. MedicationConfirmCard — 增强版用药确认卡片 // ═══════════════════════════════════════════════════════════ 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? ?? ''; final frequency = meta?['frequency'] as String? ?? ''; final remaining = meta?['remaining'] as double? ?? 0.65; return Align( alignment: Alignment.centerLeft, child: Container( margin: const EdgeInsets.only(bottom: 12), constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.88), decoration: BoxDecoration( color: const Color(0xFFFFFFFF), borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: const Color(0xFF8B9CF7).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), Color(0xFFF0F2FF)]), ), 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: 24))), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(name, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w700, color: Color(0xFF1A1A2E))), if (dosage.isNotEmpty) ...[ const SizedBox(height: 3), Text(dosage, style: const TextStyle(fontSize: 13, 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, '频率:$frequency'), // 剩余药量进度条 const SizedBox(height: 14), Row( children: [ const Text('剩余药量', style: TextStyle(fontSize: 13, color: Color(0xFF666666))), const Spacer(), Text('${(remaining * 100).toInt()}%', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF8B9CF7))), ], ), const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular(6), child: LinearProgressIndicator( value: remaining, minHeight: 8, backgroundColor: const Color(0xFFEFEDFF), valueColor: const AlwaysStoppedAnimation(Color(0xFF8B9CF7)), ), ), // 操作按钮 const SizedBox(height: 18), Row(children: [ Expanded(child: _cardFilledBtn('确认服药', Icons.check_circle_outline)), const SizedBox(width: 8), Expanded(child: _cardOutlineBtn('跳过', Icons.skip_next)), const SizedBox(width: 8), Expanded(child: _cardOutlineBtn('设置提醒', Icons.notifications_none_outlined)), ]), const SizedBox(height: 8), ], ), ), ], ), ), ); } Widget _medInfoRow(IconData icon, String text) { return Padding( padding: const EdgeInsets.only(bottom: 10), child: Row( children: [ Icon(icon, size: 17, color: const Color(0xFF888888)), const SizedBox(width: 8), Text(text, style: const TextStyle(fontSize: 13, color: Color(0xFF444444))), ], ), ); } // ═══════════════════════════════════════════════════════════ // 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.88), decoration: BoxDecoration( color: const Color(0xFFFFFFFF), borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: const Color(0xFF8B9CF7).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: 18)), Text('饮食分析结果', style: TextStyle(fontSize: 16, 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: 36, fontWeight: FontWeight.w800, color: Color(0xFFFF8F00))), const Text('千卡 (kcal)', style: TextStyle(fontSize: 12, color: Color(0xFFAAAAAA))), ])), const SizedBox(height: 16), ], // ── 识别食物列表 ── if (foods.isNotEmpty) ...[ const Text('识别结果', style: TextStyle(fontSize: 15, 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: 14, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A)))), if (calories > 0) Text('${calories is int ? calories : calories.toInt()} kcal', style: const TextStyle(fontSize: 13, color: Color(0xFF888888))), ]), if (portion != null && portion.isNotEmpty) Padding(padding: const EdgeInsets.only(top: 4), child: Text(portion, style: TextStyle(fontSize: 12, color: Colors.grey[500]))), if (nutrients != null && nutrients.isNotEmpty) Padding(padding: const EdgeInsets.only(top: 2), child: Text(nutrients, style: TextStyle(fontSize: 11, color: Colors.grey[500]))), ]), ); }), const SizedBox(height: 6), ], // ── AI 建议 ── const SizedBox(height: 14), const Text('AI 建议', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const SizedBox(height: 6), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration(color: const Color(0xFFF0F2FF), borderRadius: BorderRadius.circular(10)), child: Text(advice, style: const TextStyle(fontSize: 13, 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.88), decoration: BoxDecoration( color: const Color(0xFFFFFFFF), borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: const Color(0xFF8B9CF7).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: 20, color: Color(0xFF3F51B5)), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(reportType, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: Color(0xFF1A1A2E))), if (reportDate.isNotEmpty) Text(reportDate, style: const TextStyle(fontSize: 12, 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: 11, fontWeight: FontWeight.w600, color: Color(0xFF666666)))), Expanded(flex: 1, child: Text('数值', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: Color(0xFF666666)), textAlign: TextAlign.center)), Expanded(flex: 1, child: Text('状态', style: TextStyle(fontSize: 11, 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 = const Color(0xFFE53935); break; case 'low': sc = const Color(0xFFF9A825); break; default: sc = const Color(0xFF43A047); } 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: 13, color: isAbnormal ? const Color(0xFFE53935) : const Color(0xFF333333), fontWeight: isAbnormal ? FontWeight.w600 : FontWeight.normal)), if (refRange.isNotEmpty) Text('参考:$refRange', style: const TextStyle(fontSize: 10, color: Color(0xFFAAAAAA))), ], ), ), Expanded(flex: 1, child: Text(value, textAlign: TextAlign.center, style: TextStyle(fontSize: 13, 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: 16, color: Color(0xFF8B9CF7)), SizedBox(width: 6), Text('AI 解读摘要', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF8B9CF7))), ], ), const SizedBox(height: 8), Text(summary, style: const TextStyle(fontSize: 13, 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.88), decoration: BoxDecoration( color: const Color(0xFFFFFFFF), borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: const Color(0xFF8B9CF7).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: 15, 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: const Color(0xFFF0F2FF), foregroundColor: const Color(0xFF8B9CF7), 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: 13, 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: const Color(0xFF8B9CF7).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: const Color(0xFFF0F2FF), borderRadius: BorderRadius.circular(13), ), child: const CircularProgressIndicator(strokeWidth: 2.2, color: Color(0xFF8B9CF7)), ), const SizedBox(width: 10), Text(thinkingText?.isNotEmpty == true ? thinkingText! : '正在分析...', style: const TextStyle(fontSize: 14, 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 ? const Color(0xFF8B9CF7) : 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: const Color(0xFF8B9CF7).withAlpha(12), blurRadius: 10, offset: const Offset(0, 3))], ), 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: Color(0xFFF0F2FF)), ), ), // 图片缩略图(在文字下方) 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: 24, 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: Color(0xFFF0F2FF), child: Icon(Icons.chat_bubble_outline, size: 14, color: Color(0xFF8B9CF7))), const SizedBox(width: 6), const Text('健康管家', style: TextStyle(fontSize: 12, color: Color(0xFF9E9E9E))), const SizedBox(width: 4), const Text('仅供参考', style: TextStyle(fontSize: 11, color: Color(0xFFCCCCCC))), ]), ), ], ), ), ); } // ═══════════════════════════════════════════════════════════ // 公共组件:通用按钮 // ═══════════════════════════════════════════════════════════ Widget _cardFilledBtn(String label, IconData icon) { return ElevatedButton( onPressed: () {}, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF8B9CF7), 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: 16), const SizedBox(width: 5), Text(label, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500)), ]), ); } Widget _cardOutlineBtn(String label, IconData icon) { return OutlinedButton( onPressed: () {}, style: OutlinedButton.styleFrom( foregroundColor: const Color(0xFF8B9CF7), side: const BorderSide(color: Color(0xFF8B9CF7), width: 1.2), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), padding: const EdgeInsets.symmetric(vertical: 11), ), child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, size: 15), const SizedBox(width: 4), Text(label, style: const TextStyle(fontSize: 12, 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: 18)), )), ]), ), ); } 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'; 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 '⚖️'; 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 '健康指标'; } } static void _medicationCheckIn(WidgetRef ref, BuildContext context) async { try { final service = ref.read(medicationServiceProvider); 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) { await service.confirm(m['id']?.toString() ?? ''); } ref.invalidate(medicationReminderProvider); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('打卡成功 已记录 ${reminders.length} 项服药'), backgroundColor: const Color(0xFF43A047), ), ); } catch (e) { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('打卡失败:$e'), backgroundColor: Colors.red), ); } } static void _createExercisePlan(WidgetRef ref, BuildContext context) async { try { final service = ref.read(exerciseServiceProvider); final today = DateTime.now(); final monday = today.subtract(Duration(days: today.weekday - 1)); final items = List.generate(7, (i) => { 'dayOfWeek': i, 'exerciseType': i == 2 || i == 5 ? '休息' : '散步', 'durationMinutes': i == 2 || i == 5 ? 0 : 30, 'isRestDay': i == 2 || i == 5, }); await service.createPlan({ 'weekStartDate': '${monday.year}-${monday.month.toString().padLeft(2, '0')}-${monday.day.toString().padLeft(2, '0')}', 'items': items, }); ref.invalidate(currentExercisePlanProvider); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('运动计划已创建'), backgroundColor: Color(0xFF43A047)), ); pushRoute(ref, 'exercisePlan'); } catch (e) { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('创建失败:$e'), backgroundColor: Colors.red), ); } } static void _exerciseCheckIn(WidgetRef ref, BuildContext context) async { try { final plan = await ref.read(currentExercisePlanProvider.future); if (plan == null || plan.isEmpty) { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('请先创建运动计划'), backgroundColor: Color(0xFFFF9800)), ); return; } final items = (plan['items'] as List?)?.cast>() ?? []; final today = DateTime.now().weekday - 1; final todayItem = items.cast?>().firstWhere( (i) => i?['dayOfWeek'] == today && i?['isRestDay'] != true, orElse: () => null, ); if (todayItem == null) { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('今天休息日'), backgroundColor: Color(0xFFFF9800)), ); return; } final service = ref.read(exerciseServiceProvider); await service.checkIn(todayItem['id']?.toString() ?? ''); ref.invalidate(currentExercisePlanProvider); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('打卡成功'), backgroundColor: Color(0xFF43A047)), ); } 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) { for (final m in meds) { final name = m['name'] ?? ''; final dosage = m['dosage'] ?? ''; final times = (m['timeOfDay'] as List?)?.map((t) => t.toString().substring(0, 5)).join(', ') ?? ''; final medOverdue = now.hour >= 8; tasks.add(_taskRow( context, Icons.medication_rounded, '$name $dosage ($times)', status: medOverdue ? 'overdue' : 'pending', onTap: () {}, )); } }); if (tasks.length <= (allNull ? 0 : 1)) { tasks.add(_taskRow(context, Icons.medication_rounded, '暂无用药提醒', status: 'pending')); } // 3. 运动 final exOverdue = now.hour >= 18; tasks.add(_taskRow( context, Icons.directions_run, '今日待运动:散步 30 分钟', status: exOverdue ? 'overdue' : 'pending', onTap: () => pushRoute(ref, 'exercisePlan'), )); // 4. 测量 tasks.add(_taskRow( context, Icons.today, '今日测量:血压', status: 'pending', onTap: () => pushRoute(ref, 'trend', params: {'type': 'blood_pressure'}), )); // 5. 异常指标 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: const Color(0xFF8B9CF7).withAlpha(10), blurRadius: 8, offset: const Offset(0, 2))], ), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Icon(Icons.today, size: 18, color: const Color(0xFF8B9CF7)), const SizedBox(width: 8), const Text('今日任务', style: TextStyle(fontSize: 15, 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': const Color(0xFF43A047), 'warning': const Color(0xFFFF9800), 'pending': const Color(0xFF9E9E9E), 'overdue': const Color(0xFFE53935)}; 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: const Color(0xFFF0F2FF), borderRadius: BorderRadius.circular(8)), child: Icon(icon, size: 15, color: const Color(0xFF8B9CF7))), const SizedBox(width: 10), Expanded(child: Text(trailing ?? label, style: const TextStyle(fontSize: 13, color: Color(0xFF333333)))), Icon(icons[status] ?? Icons.circle_outlined, size: 18, 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.monitor_heart_outlined, route: 'trend'), _AgentAction(label: '录入血糖', icon: Icons.bloodtype_outlined, route: 'trend'), _AgentAction(label: '录入心率', icon: Icons.favorite_border, route: 'trend'), _AgentAction(label: '录入血氧', icon: Icons.air_outlined, route: 'trend'), _AgentAction(label: '录入体重', icon: Icons.monitor_weight_outlined, route: 'trend'), ], ActiveAgent.diet: [ _AgentAction(label: '拍照识别', icon: Icons.camera_alt_outlined, isWide: true, route: 'dietCapture'), _AgentAction(label: '上传照片', icon: Icons.photo_library_outlined, isWide: true, route: 'dietCapture'), ], 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, action: 'createPlan'), _AgentAction(label: '今日打卡', icon: Icons.fact_check_outlined, isWide: true, action: 'checkIn'), ], }; 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: 16, color: Color(0xFF8B9CF7)), const SizedBox(width: 6), const Text('AI 建议', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF8B9CF7))), const Spacer(), Icon(_expanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, size: 18, color: const Color(0xFFAAAAAA)), ], ), if (_expanded) ...[ const SizedBox(height: 10), Text(widget.advice, style: const TextStyle(fontSize: 13, color: Color(0xFF555555), height: 1.6)), ], ], ), ), ); } }