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_colors.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 _AnimatedCardEntry extends StatefulWidget { final Widget child; const _AnimatedCardEntry({required this.child}); @override State<_AnimatedCardEntry> createState() => _AnimatedCardEntryState(); } class _AnimatedCardEntryState extends State<_AnimatedCardEntry> with SingleTickerProviderStateMixin { late final _ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 600)); late final _slide = Tween(begin: const Offset(0, 0.12), end: Offset.zero).animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeOutCubic)); late final _fade = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeOut)); @override void initState() { super.initState(); _ctrl.forward(); } @override void dispose() { _ctrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return FadeTransition(opacity: _fade, child: SlideTransition(position: _slide, child: widget.child)); } } /// 对话消息列表 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: AppColors.textHint)), ], ), ); } return ListView.builder( controller: scrollCtrl, reverse: false, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), itemCount: messages.length, itemBuilder: (context, index) { final msg = messages[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); default: if (!msg.isUser && chatState.isStreaming && msg.content.length < 5) { return _buildThinkingBubble(context, chatState.thinkingText); } return _buildTextBubble(context, msg, chatState); } } // ═══════════════════════════════════════════════════════════ // 1. AgentWelcomeCard — 智能体欢迎卡片(蚂蚁阿福风格 + 美化版) // ═══════════════════════════════════════════════════════════ Widget _buildAgentWelcomeCard(BuildContext context, WidgetRef ref, ChatMessage msg, ActiveAgent agent) { final info = _agentInfo(agent); final actions = agent.actions; final features = _getAgentFeatures(agent); final screenWidth = MediaQuery.of(context).size.width; final agentColors = _agentColors(agent); final iconGradient = LinearGradient( colors: agentColors.gradient, begin: Alignment.topLeft, end: Alignment.bottomRight, ); return _AnimatedCardEntry(child: Align( alignment: Alignment.centerLeft, child: Container( margin: const EdgeInsets.only(bottom: 16), constraints: BoxConstraints(maxWidth: screenWidth * 0.95), decoration: BoxDecoration( color: AppColors.cardBackground, borderRadius: BorderRadius.circular(28), border: Border.all(color: Color(0xFFE2E8F0), width: 2), boxShadow: AppColors.cardShadow, ), clipBehavior: Clip.antiAlias, child: Column( mainAxisSize: MainAxisSize.min, children: [ // ── 标题区域 ── Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(20, 22, 20, 20), decoration: BoxDecoration( gradient: LinearGradient( colors: [ agentColors.border, agentColors.bg, ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 大图标容器 Container( width: 64, height: 64, decoration: BoxDecoration( gradient: iconGradient, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: agentColors.accent.withAlpha(77), blurRadius: 14, offset: const Offset(0, 5), ), ], ), child: Icon(info.$1, size: 32, color: Colors.white), ), const SizedBox(width: 18), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // AI 助手标签 Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: Colors.white.withAlpha(160), borderRadius: BorderRadius.circular(10), boxShadow: AppColors.cardShadowLight, ), child: Row(mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.auto_awesome, size: 14, color: agentColors.accent), const SizedBox(width: 4), Text('AI 智能助手', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: agentColors.accent)), ]), ), const SizedBox(height: 10), Text(info.$2, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w800, color: AppColors.textPrimary, letterSpacing: -0.3)), const SizedBox(height: 8), Text(info.$3, style: const TextStyle(fontSize: 16, color: AppColors.textSecondary, height: 1.4)), ], ), ), ], ), ), // ── 功能特性区 ── if (features.isNotEmpty) ...[ Padding( padding: const EdgeInsets.fromLTRB(16, 14, 16, 0), child: features.any((f) => f.imageAsset != null) ? Container( decoration: BoxDecoration( gradient: iconGradient, borderRadius: BorderRadius.circular(20), ), padding: const EdgeInsets.all(2), child: ClipRRect( borderRadius: BorderRadius.circular(18), child: Image.asset( features.firstWhere((f) => f.imageAsset != null).imageAsset!, width: double.infinity, height: 170, fit: BoxFit.fill, ), ), ) : Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration(color: AppColors.cardInner, borderRadius: BorderRadius.circular(20), border: Border.all(color: AppColors.border)), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Container(width: 4, height: 16, decoration: BoxDecoration(color: agentColors.accent, borderRadius: BorderRadius.circular(2))), const SizedBox(width: 8), const Text('核心功能', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.textSecondary)), ]), const SizedBox(height: 10), Row(children: features.map((feature) { return Expanded( child: Container( margin: EdgeInsets.symmetric(horizontal: 3), padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 6), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)), child: Column(children: [ Container(width: 40, height: 40, decoration: BoxDecoration(color: agentColors.iconBg, borderRadius: BorderRadius.circular(10)), child: Icon(feature.icon, size: 20, color: agentColors.accent)), const SizedBox(height: 6), Text(feature.title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.textPrimary)), Text(feature.desc, style: const TextStyle(fontSize: 11, color: AppColors.textHint)), ]), ), ); }).toList()), ]), ), ), ], // ── 医生选择区(问诊专用)── if (agent == ActiveAgent.consultation) ...[ const SizedBox(height: 14), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: _buildDoctorCards(ref, agentColors), ), ], // ── 快捷操作按钮区 ── if (actions.isNotEmpty) ...[ const SizedBox(height: 14), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Container( width: 6, height: 18, decoration: BoxDecoration( gradient: iconGradient, borderRadius: BorderRadius.circular(3), ), ), const SizedBox(width: 8), Text('快捷操作', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: AppColors.textPrimary)), ]), const SizedBox(height: 12), Row( children: [ for (int i = 0; i < actions.length; i++) ...[ if (i > 0) const SizedBox(width: 10), Expanded(child: _buildActionButton(actions[i], context, ref, agentColors)), ], ], ), ], ), ), ], // ── 底部提示 ── const SizedBox(height: 16), Padding( padding: const EdgeInsets.fromLTRB(20, 0, 20, 20), child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( color: agentColors.iconBg, borderRadius: BorderRadius.circular(12), ), child: Row(children: [ Icon(Icons.lightbulb_outline, size: 18, color: agentColors.accent), const SizedBox(width: 8), Expanded(child: Text(_agentTip(agent), style: TextStyle(fontSize: 15, color: AppColors.textPrimary.withAlpha(180), fontWeight: FontWeight.w500))), ]), ), ), ], ), ), ), ); } Widget _buildActionButton(_AgentAction a, BuildContext context, WidgetRef ref, _AgentColors colors) { return InkWell( onTap: () { if (a.label == '服药打卡') { pushRoute(ref, 'medCheckIn'); } 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(20), child: Container( padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all(color: AppColors.borderLight, width: 1.5), boxShadow: AppColors.cardShadow, ), child: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: colors.iconBg, borderRadius: BorderRadius.circular(12), ), child: Icon(a.icon, size: 22, color: colors.accent), ), const SizedBox(width: 12), Expanded(child: Text(a.label, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.textPrimary))), Icon(Icons.chevron_right, size: 18, color: AppColors.border), ], ), ), ); } List<_AgentFeature> _getAgentFeatures(ActiveAgent agent) { return switch (agent) { ActiveAgent.health => [ _AgentFeature(Icons.trending_up, '趋势分析', '查看历史数据', imageAsset: 'assets/images/Data.png'), _AgentFeature(Icons.add, '快速录入', '血压心率血糖'), ], ActiveAgent.diet => [ _AgentFeature(Icons.camera_alt, '拍照识别', '自动识别食物', imageAsset: 'assets/images/diet.png'), _AgentFeature(Icons.bar_chart, '营养分析', '热量营养成分'), ], ActiveAgent.medication => [ _AgentFeature(Icons.list, '用药清单', '管理所有药品', imageAsset: 'assets/images/Medicine.png'), _AgentFeature(Icons.alarm, '智能提醒', '按时服药提醒'), ], ActiveAgent.consultation => [ _AgentFeature(Icons.message, '在线问诊', '专业医生解答'), _AgentFeature(Icons.local_hospital, '科室选择', '精准匹配'), ], ActiveAgent.report => [ _AgentFeature(Icons.upload, '上传报告', '支持多种格式', imageAsset: 'assets/images/Report Analysis.png'), _AgentFeature(Icons.search, 'AI解读', '智能分析报告'), ], ActiveAgent.exercise => [ _AgentFeature(Icons.calendar_month, '运动计划', '科学规划', imageAsset: 'assets/images/sport.png'), _AgentFeature(Icons.check_circle, '打卡记录', '坚持完成'), ], _ => [ _AgentFeature(Icons.health_and_safety, '健康管理', '全方位呵护'), _AgentFeature(Icons.chat, '智能咨询', '随时答疑'), ], }; } Widget _buildDoctorCards(WidgetRef ref, _AgentColors colors) { 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, colors)), ], ], ); } Widget _doctorCard(Map doc, WidgetRef ref, _AgentColors colors) { 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: colors.border), ), child: Column(mainAxisSize: MainAxisSize.min, children: [ CircleAvatar( radius: 22, backgroundColor: colors.iconBg, child: Text(doc['name']![0], style: TextStyle(fontSize: 21, fontWeight: FontWeight.w600, color: colors.accent)), ), const SizedBox(height: 6), Text(doc['name']!, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.textPrimary)), Text(doc['title']!, style: const TextStyle(fontSize: 14, color: AppColors.textHint)), const SizedBox(height: 2), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: colors.iconBg, borderRadius: BorderRadius.circular(4), ), child: Text(doc['dept']!, style: TextStyle(fontSize: 13, color: colors.accent)), ), const SizedBox(height: 4), Text( doc['desc']!, style: const TextStyle(fontSize: 13, color: AppColors.textSecondary, 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 backendType = meta['type'] as String? ?? ''; final screenWidth = MediaQuery.of(context).size.width; // 识别是哪种数据类型 final isMedication = backendType == 'medication' || (meta['name'] != null && meta['name'].toString().isNotEmpty && backendType != 'exercise'); final isExercise = backendType == 'exercise'; final isHealth = !isMedication && !isExercise; // 根据类型获取显示字段 String title = '数据确认'; IconData titleIcon = Icons.assignment; String mainLabel = ''; String mainValue = ''; String mainUnit = ''; bool abnormal = meta['abnormal'] as bool? ?? false; String recordTime = meta['recordTime'] as String? ?? ''; List<_ConfirmField> fields = []; if (isMedication) { // 药品录入 — 主展示区已显示药名+剂量,详情只列额外信息 title = '药品录入确认'; titleIcon = Icons.medication_liquid_outlined; mainLabel = meta['name'] as String? ?? ''; mainValue = meta['dosage'] as String? ?? ''; mainUnit = ''; if (recordTime.isEmpty) recordTime = _formatTime(msg.createdAt); final time = meta['time'] as String? ?? ''; if (time.isNotEmpty) { final timeValue = meta['服药时间'] as String? ?? time; fields.add(_ConfirmField(label: '服药时间', value: timeValue, editable: true)); } final frequency = meta['frequency'] as String? ?? ''; if (frequency.isNotEmpty) { final freqValue = meta['频率'] as String? ?? _freqLabel(frequency); fields.add(_ConfirmField(label: '频率', value: freqValue, editable: true)); } final duration = meta['duration_days'] as int?; if (duration != null && duration > 0) { final durationValue = meta['服用天数'] as String? ?? '$duration 天'; fields.add(_ConfirmField(label: '服用天数', value: durationValue, editable: true)); } } else if (isExercise) { // 运动计划 — 主展示区大号显示运动名,字段列时长/频率/天数 title = '运动计划确认'; titleIcon = Icons.directions_run_outlined; final exName = (meta['value'] ?? meta['name'] ?? meta['exerciseType'] ?? meta['运动类型'] ?? '').toString(); final exUnitRaw = (meta['unit'] ?? meta['duration_unit'] ?? meta['durationUnit'] ?? '').toString(); final exDaysRaw = meta['durationDays'] ?? meta['day_count']; final exDays = int.tryParse(exDaysRaw?.toString() ?? '') ?? 0; // 解析 "每天30分钟" → 频率=每天, 时长=30分钟 final freqMatch = RegExp(r'^(每天|每周|每月)').firstMatch(exUnitRaw); final durMatch = RegExp(r'(\d+分钟)').firstMatch(exUnitRaw); final freq = freqMatch?.group(1) ?? ''; final dur = durMatch?.group(1) ?? exUnitRaw; mainLabel = '运动计划'; mainValue = exName.isNotEmpty ? exName : '运动计划'; mainUnit = ''; if (recordTime.isEmpty) recordTime = _formatTime(msg.createdAt); if (dur.isNotEmpty) { fields.add(_ConfirmField(label: '运动时长', value: dur, editable: true)); } if (freq.isNotEmpty) { fields.add(_ConfirmField(label: '频率', value: freq)); } if (exDays > 0) { fields.add(_ConfirmField(label: '计划天数', value: '${exDays}天')); } final intensity = (meta['intensity'] ?? meta['运动强度'] ?? '').toString(); if (intensity.isNotEmpty) { fields.add(_ConfirmField(label: '运动强度', value: intensity, editable: true)); } final calories = (meta['calories'] ?? meta['calorie'] ?? meta['消耗热量'] ?? '').toString(); if (calories.isNotEmpty) { fields.add(_ConfirmField(label: '消耗热量', value: '$calories kcal', editable: true)); } } else { // 健康指标 — 主展示区已显示指标+数值+单位,详情只列额外信息 title = '健康数据确认'; titleIcon = Icons.monitor_heart_outlined; mainLabel = _getMetricName(backendType); mainValue = meta['value'] as String? ?? ''; mainUnit = meta['unit'] as String? ?? _getMetricUnit(backendType); if (recordTime.isEmpty) recordTime = _formatTime(msg.createdAt); final timeValue = meta['记录时间'] as String? ?? recordTime; fields.add(_ConfirmField(label: '记录时间', value: timeValue, editable: true)); final note = meta['note'] as String? ?? ''; if (note.isNotEmpty) { final noteValue = meta['备注'] as String? ?? note; fields.add(_ConfirmField(label: '备注', value: noteValue, editable: true)); } } return _AnimatedCardEntry(child: Align( alignment: Alignment.centerLeft, child: Container( margin: const EdgeInsets.only(bottom: 16), constraints: BoxConstraints(maxWidth: screenWidth * 0.95), decoration: BoxDecoration( color: AppColors.cardBackground, borderRadius: BorderRadius.circular(28), border: Border.all(color: Color(0xFFE2E8F0), width: 2), boxShadow: AppColors.cardShadow, ), clipBehavior: Clip.antiAlias, child: Column( mainAxisSize: MainAxisSize.min, children: [ // ── 标题区域 ── Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(20, 20, 20, 18), decoration: const BoxDecoration( color: Color(0xFFF8F6FF), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 56, height: 56, decoration: BoxDecoration( color: AppColors.cardInner, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow(color: AppColors.primaryPurple.withAlpha(35), blurRadius: 10, offset: const Offset(0, 4)), ], ), child: Icon(titleIcon, size: 28, color: Colors.white), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: AppColors.textPrimary)), const SizedBox(height: 6), Row( children: [ Icon(Icons.access_time, size: 15, color: AppColors.iconColor), const SizedBox(width: 4), Text(recordTime, style: const TextStyle(fontSize: 15, color: AppColors.textSecondary)), ], ), ], ), ), ], ), ), // ── 主要数据展示区(大卡片) ── Padding( padding: const EdgeInsets.fromLTRB(20, 14, 20, 0), child: Container( width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColors.backgroundSecondary, borderRadius: BorderRadius.circular(18), border: Border.all(color: AppColors.border), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 68, height: 68, decoration: BoxDecoration( gradient: AppColors.lightGradient, borderRadius: BorderRadius.circular(18), ), child: Center( child: isHealth ? Text(_getMetricIcon(backendType), style: const TextStyle(fontSize: 36)) : Icon( isMedication ? Icons.medication_liquid_outlined : Icons.directions_run_outlined, size: 36, color: AppColors.iconColor, ), ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(mainLabel, style: const TextStyle(fontSize: 17, color: AppColors.textSecondary)), if (mainValue.isNotEmpty) ...[ const SizedBox(height: 8), RichText( text: TextSpan( children: [ TextSpan( text: mainValue, style: TextStyle( fontSize: 36, fontWeight: FontWeight.w800, color: abnormal ? AppColors.error : AppColors.textPrimary, letterSpacing: -0.5, ), ), if (mainUnit.isNotEmpty) TextSpan( text: ' $mainUnit', style: TextStyle( fontSize: 19, color: abnormal ? AppColors.error : AppColors.iconColor, fontWeight: FontWeight.w500, ), ), ], ), ), ], ], ), ), ], ), ), ), // ── 异常提示 ── if (abnormal) ...[ const SizedBox(height: 12), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: AppColors.error.withAlpha(10), borderRadius: BorderRadius.circular(14), border: Border.all(color: AppColors.error.withAlpha(30)), ), child: Row( children: [ Container( width: 32, height: 32, decoration: BoxDecoration(color: AppColors.error.withAlpha(15), borderRadius: BorderRadius.circular(10)), child: Icon(Icons.warning_amber_rounded, size: 20, color: AppColors.error), ), const SizedBox(width: 10), Expanded(child: Text('数值偏高,建议关注并咨询医生', style: TextStyle(fontSize: 16, color: AppColors.error, fontWeight: FontWeight.w500))), ], ), ), ), ], // ── 详细字段列表(可点击修改) ── if (fields.isNotEmpty) ...[ const SizedBox(height: 12), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all(color: AppColors.border), ), child: Column( children: [ for (int i = 0; i < fields.length; i++) ...[ if (i > 0) Container(height: 1, color: AppColors.borderLight, margin: const EdgeInsets.symmetric(horizontal: 16)), _buildFieldRow(fields[i], msg, ref), ], ], ), ), ), ], // ── 确认按钮(白色底+渐变边框) ── const SizedBox(height: 18), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Container( width: double.infinity, height: 56, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all(color: AppColors.borderLight, width: 1.5), boxShadow: AppColors.cardShadow, ), child: msg.confirmed ? Container( decoration: BoxDecoration( gradient: AppColors.successButtonGradient, borderRadius: BorderRadius.circular(20), boxShadow: AppColors.buttonShadow, ), child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.check_circle_outline, size: 22, color: Colors.white), const SizedBox(width: 8), const Text('录入成功', style: TextStyle(fontSize: 19, fontWeight: FontWeight.w600, color: Colors.white)), ]), ) : Material( color: Colors.transparent, child: InkWell( onTap: () { ref.read(chatProvider.notifier).confirmMessage(msg.id); }, borderRadius: BorderRadius.circular(20), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 32, height: 32, decoration: BoxDecoration( gradient: AppColors.purpleBlueGradient, borderRadius: BorderRadius.circular(8), ), child: const Icon(Icons.check, size: 18, color: Colors.white), ), const SizedBox(width: 10), Text('确认录入', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: AppColors.textPrimary)), ], ), ), ), ), ), const SizedBox(height: 20), ], ), ), ), ); } Widget _buildFieldRow(_ConfirmField field, ChatMessage msg, WidgetRef ref) { final isEditing = field.label == msg.metadata?['_editingField']; return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox( width: 80, child: Text(field.label, style: const TextStyle(fontSize: 15, color: AppColors.textHint)), ), const SizedBox(width: 12), Expanded( child: !field.editable ? Text(field.value.isNotEmpty ? field.value : '未设置', style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: AppColors.textPrimary)) : isEditing ? _buildEditableTextField(field, msg, ref) : InkWell( onTap: () => ref.read(chatProvider.notifier).startEditingField(msg.id, field.label), child: Row(children: [ Expanded(child: Text(field.value.isNotEmpty ? field.value : '未设置', style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: AppColors.textPrimary))), Icon(Icons.edit_outlined, size: 18, color: AppColors.border), ]), ), ), ], ), ); } Widget _buildEditableTextField(_ConfirmField field, ChatMessage msg, WidgetRef ref) { return StatefulBuilder( builder: (context, setState) { final controller = TextEditingController(text: field.value); return TextField( controller: controller, autofocus: true, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: AppColors.textPrimary), decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), filled: true, fillColor: Colors.white, enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: AppColors.primary.withAlpha(80)), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: AppColors.primary, width: 1.5), ), ), onSubmitted: (value) { ref.read(chatProvider.notifier).finishEditingField(msg.id, field.label, value); }, onTapOutside: (_) { ref.read(chatProvider.notifier).finishEditingField(msg.id, field.label, controller.text); }, ); }, ); } void _editField(_ConfirmField field, ChatMessage msg, WidgetRef ref) { // 字段可点击修改的入口,后续可接入 showDialog 实现 } // ═══════════════════════════════════════════════════════════ // 3. ThinkingBubble — 思考动画 // ═══════════════════════════════════════════════════════════ 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: AppColors.cardBackground, borderRadius: const BorderRadius.only(topLeft: Radius.circular(4), topRight: Radius.circular(20), bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)), border: Border.all(color: AppColors.border, 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: AppColors.textHint)), ], ), ), ); } Widget _buildTextBubble(BuildContext context, ChatMessage msg, ChatState? chatState) { 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 ? Colors.white : AppColors.cardBackground, 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: Border.all(color: isUser ? const Color(0xFFE8E4FF) : AppColors.border, width: isUser ? 1 : 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: AppColors.textPrimary, height: 1.5)) else MarkdownBody( data: _cleanAiText(msg.content), selectable: true, styleSheet: MarkdownStyleSheet( p: const TextStyle(fontSize: 19, color: AppColors.textPrimary, height: 1.5), strong: const TextStyle(fontSize: 19, color: AppColors.textPrimary, height: 1.5, fontWeight: FontWeight.w700), h1: const TextStyle(fontSize: 21, color: AppColors.textPrimary, fontWeight: FontWeight.w700), h2: const TextStyle(fontSize: 20, color: AppColors.textPrimary, fontWeight: FontWeight.w700), ), ), // 图片缩略图(在文字下方) 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: AppColors.backgroundSecondary, borderRadius: BorderRadius.circular(8)), child: const Icon(Icons.image, size: 28, color: AppColors.textHint), )) : 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: AppColors.textHint)), const SizedBox(width: 4), const Text('仅供参考', style: TextStyle(fontSize: 14, color: AppColors.textHint)), ]), ), ], ), ), ); } // ═══════════════════════════════════════════════════════════ // 公共组件:通用按钮 // ═══════════════════════════════════════════════════════════ 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)), )), ]), ), ); } /// 清理 AI 返回文本中的异常占位符(Markdown 格式交给 MarkdownBody 渲染) static String _cleanAiText(String text) { return text .replaceAll('\$1', '') // AI 偶尔输出 $1 占位符(正则残留) .replaceAll(RegExp(r'\n{3,}'), '\n\n') .trim(); } String _freqLabel(String freq) { switch (freq.toLowerCase()) { case 'daily': return '每天'; case 'everyotherday': return '隔天'; case 'weekly': return '每周'; default: return freq; } } 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: AppColors.warning), ); 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(0xFFB0CCF8), const Color(0xFF7DA8F0), const Color(0xFF4A80E8)], bg: const Color(0xFFEEF2FF), border: const Color(0xFFC8D8FA), iconBg: const Color(0xFFDEE6FC), accent: const Color(0xFF4A80E8), ), 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 String _agentTip(ActiveAgent agent) => switch (agent) { ActiveAgent.health => '直接说出您的症状或数据,如"血压130/85,心率72"', ActiveAgent.diet => '拍照或描述您吃了什么,如"早餐吃了两个鸡蛋一杯牛奶"', ActiveAgent.medication => '说出药品名称和用法,如"每天早晚各一片阿司匹林"', ActiveAgent.consultation => '描述您的症状和不适感,如"最近总是头晕胸闷"', ActiveAgent.report => '上传您的检查报告,AI 将自动提取指标并解读', ActiveAgent.exercise => '说出运动计划,如"每天散步30分钟坚持一周"', _ => '直接描述您的需求,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 = []; // ── 解析健康指标 ── 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; // 异常检测 final bpAbnormal = bp is Map && bp['systolic'] is int && (bp['systolic'] as int) >= 140; final hasAbnormal = bpAbnormal; // ── 1. 健康指标 ── const healthIconColor = Color(0xFF3B82F6); const healthIconBg = Color(0xFFDBEAFE); if (allNull) { tasks.add(_taskRow(context, Icons.monitor_heart_outlined, '健康指标', trailing: '今日暂无记录,点击录入', status: 'pending', iconColor: healthIconColor, iconBg: healthIconBg, onTap: () => pushRoute(ref, 'trend'))); } else if (hasAbnormal) { // 有异常时显示异常指标 final abnormalParts = []; if (bpAbnormal) abnormalParts.add('血压 ${bp['systolic']}/${bp['diastolic']} 偏高'); tasks.add(_taskRow(context, Icons.warning_amber_rounded, '健康指标', trailing: abnormalParts.join(' · '), status: 'warning', iconColor: healthIconColor, iconBg: healthIconBg, onTap: () => pushRoute(ref, 'trend'))); } else { // 全部正常 tasks.add(_taskRow(context, Icons.check_circle, '健康指标', trailing: '指标正常', status: 'done', iconColor: healthIconColor, iconBg: healthIconBg, onTap: () => pushRoute(ref, 'trend'))); } // ── 2. 运动 ── const exIconColor = Color(0xFFF59E0B); const exIconBg = Color(0xFFFEF3C7); final exercisePlan = ref.watch(currentExercisePlanProvider); var hasExercise = false; exercisePlan.whenOrNull(data: (plan) { if (plan != null) { final items = (plan['items'] as List?)?.cast>() ?? []; final today = now.weekday % 7; final todayItem = items.cast?>().firstWhere( (i) => i?['dayOfWeek'] == today, orElse: () => null); if (todayItem != null && todayItem['isRestDay'] != true) { hasExercise = 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'), iconColor: exIconColor, iconBg: exIconBg, onTap: () => pushRoute(ref, 'exercisePlan'), )); } } }); if (!hasExercise) { tasks.add(_taskRow(context, Icons.directions_run, '运动', trailing: '暂无今日运动计划', status: 'pending', iconColor: exIconColor, iconBg: exIconBg, onTap: () => pushRoute(ref, 'exercisePlan'))); } // ── 3. 用药打卡 ── const medIconColor = Color(0xFFEC4899); const medIconBg = Color(0xFFFCE7F3); reminders.whenOrNull(data: (meds) { if (meds.isNotEmpty) { final overdueMeds = meds.where((m) => m['status'] == 'overdue').toList(); final upcomingMeds = meds.where((m) => m['status'] == 'upcoming').toList(); final takenMeds = meds.where((m) => m['status'] == 'taken').toList(); // 未服/过期的在前面 for (final m in [...overdueMeds, ...upcomingMeds]) { final name = m['name']?.toString() ?? '药品'; final time = m['scheduledTime']?.toString() ?? ''; final isOverdue = m['status'] == 'overdue'; tasks.add(_taskRow(context, Icons.medication_rounded, name, trailing: '${isOverdue ? "❗过期" : "⏳待服"} $time', status: isOverdue ? 'overdue' : 'pending', iconColor: medIconColor, iconBg: medIconBg, onTap: () => pushRoute(ref, 'medCheckIn'))); } // 已服的合并显示 if (takenMeds.isNotEmpty) { final names = takenMeds.map((m) => m['name']?.toString() ?? '药品').join('、'); tasks.add(_taskRow(context, Icons.check_circle, names, trailing: '已服用 (${takenMeds.length}项)', status: 'done', iconColor: medIconColor, iconBg: medIconBg)); } } }); final hasMeds = reminders.asData?.value != null && (reminders.asData!.value).isNotEmpty; if (!hasMeds) { tasks.add(_taskRow(context, Icons.medication_rounded, '用药打卡', trailing: '暂无用药提醒', status: 'pending', iconColor: medIconColor, iconBg: medIconBg, onTap: () => pushRoute(ref, 'medications'))); } return Container( margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(28), border: Border.all(color: Color(0xFFE2E8F0), width: 2), boxShadow: AppColors.cardShadow, ), clipBehavior: Clip.antiAlias, child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(16, 12, 16, 10), decoration: const BoxDecoration(gradient: AppColors.drawerGradient), child: Row(children: [ Container(width: 28, height: 28, decoration: BoxDecoration(color: Colors.white.withOpacity(0.6), borderRadius: BorderRadius.circular(8)), child: const Icon(Icons.health_and_safety, size: 16, color: AppColors.primary)), const SizedBox(width: 8), const Text('今日健康', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: AppColors.textPrimary)), const Spacer(), Text('${now.month}月${now.day}日', style: const TextStyle(fontSize: 13, color: AppColors.textSecondary)), ]), ), Padding( padding: const EdgeInsets.all(14), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ if (tasks.isEmpty) _taskRow(context, Icons.check_circle, '今日暂无待办', status: 'done') else ...tasks, ]), ), ]), ); } Widget _taskRow(BuildContext context, IconData icon, String label, {String status = 'pending', String? trailing, VoidCallback? onTap, Color? iconColor, Color? iconBg}) { final colors = {'done': AppTheme.success, 'warning': AppColors.warning, 'pending': AppColors.textHint, 'overdue': AppTheme.error}; final icons = {'done': Icons.check_circle, 'warning': Icons.warning, 'pending': Icons.circle_outlined, 'overdue': Icons.error}; final isOverdue = status == 'overdue'; final ic = iconColor ?? AppTheme.primary; final ibg = iconBg ?? AppTheme.primaryLight; 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: ibg, borderRadius: BorderRadius.circular(8)), child: Icon(icon, size: 18, color: ic)), const SizedBox(width: 10), Expanded(child: Text(trailing ?? label, style: const TextStyle(fontSize: 16, color: AppColors.textPrimary))), 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}); } class _AgentFeature { final IconData icon; final String title; final String desc; final String? imageAsset; const _AgentFeature(this.icon, this.title, this.desc, {this.imageAsset}); } class _ConfirmField { final String label; final String value; final IconData icon; final bool editable; const _ConfirmField({ required this.label, required this.value, this.icon = Icons.info_outline, this.editable = false, }); } 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: AppColors.backgroundSecondary, 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: AppColors.textHint), ], ), if (_expanded) ...[ const SizedBox(height: 10), Text(widget.advice, style: const TextStyle(fontSize: 16, color: Color(0xFF555555), height: 1.6)), ], ], ), ), ); } }