import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import 'package:file_picker/file_picker.dart'; import 'dart:io'; import '../../core/navigation_provider.dart'; import '../../providers/auth_provider.dart'; import '../../providers/chat_provider.dart'; import '../../providers/data_providers.dart'; import '../../widgets/health_drawer.dart'; import 'widgets/chat_messages_view.dart'; class HomePage extends ConsumerStatefulWidget { const HomePage({super.key}); @override ConsumerState createState() => _HomePageState(); } class _HomePageState extends ConsumerState { final _textCtrl = TextEditingController(); final _scrollCtrl = ScrollController(); bool _taskCardsExpanded = true; String? _pickedImagePath; double _lastScrollOffset = 0; DateTime? _lastCollapseTime; bool _exerciseDone = false; final Set _welcomedAgents = {}; static final _mockFollowUps = [ {'hospital': '协和医院', 'department': '心内科', 'date': DateTime.now().add(const Duration(days: 2)), 'type': '复查'}, {'hospital': '人民医院', 'department': '心内科', 'date': DateTime.now().add(const Duration(days: 3)), 'type': '复诊'}, ]; @override void initState() { super.initState(); _scrollCtrl.addListener(_onScroll); _textCtrl.addListener(_onTextChange); } @override void dispose() { _textCtrl.dispose(); _scrollCtrl.dispose(); super.dispose(); } void _onTextChange() { if (_textCtrl.text.isNotEmpty && _taskCardsExpanded) { setState(() => _taskCardsExpanded = false); } } void _onScroll() { if (!_scrollCtrl.hasClients) return; final offset = _scrollCtrl.offset; if (offset < _lastScrollOffset && _taskCardsExpanded) { final delta = _lastScrollOffset - offset; if (delta > 50) { final now = DateTime.now(); if (_lastCollapseTime == null || now.difference(_lastCollapseTime!) > const Duration(seconds: 2)) { _lastCollapseTime = now; setState(() => _taskCardsExpanded = false); } } } _lastScrollOffset = offset; } void _sendMessage() { final text = _textCtrl.text.trim(); final imagePath = _pickedImagePath; if (text.isEmpty && imagePath == null) return; _textCtrl.clear(); setState(() { _taskCardsExpanded = false; _pickedImagePath = null; }); if (imagePath != null) { ref.read(chatProvider.notifier).sendImage(imagePath, text); } else { ref.read(chatProvider.notifier).sendMessage(text); } } @override Widget build(BuildContext context) { final chatState = ref.watch(chatProvider); final auth = ref.watch(authProvider); final user = auth.user; final selectedAgent = ref.watch(selectedAgentProvider); ref.listen(cameraActionProvider, (prev, next) { if (next == 'camera') { _pickImage(ImageSource.camera); ref.read(cameraActionProvider.notifier).clear(); } else if (next == 'gallery') { _pickImage(ImageSource.gallery); ref.read(cameraActionProvider.notifier).clear(); } }); return Scaffold( drawer: const HealthDrawer(), backgroundColor: const Color(0xFFF8F9FC), body: SafeArea( child: Column(children: [ // ── 顶部栏 ── _buildHeader(user), // ── 聊天区域(今日任务已移入对话流第一条消息) ── Expanded(child: ChatMessagesView(scrollCtrl: _scrollCtrl, messages: chatState.messages)), // ── 底部合并区:智能体栏 + 操作面板 + 输入框(固定高度) ── _buildBottomBar(context, selectedAgent), ]), ), ); } // ═════════════════════ 顶部栏 ═════════════════════ Widget _buildHeader(dynamic user) { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), child: Row(children: [ Builder(builder: (ctx) => GestureDetector( onTap: () => Scaffold.of(ctx).openDrawer(), child: CircleAvatar(radius: 20, backgroundColor: const Color(0xFFF0F2FF), backgroundImage: user?.avatarUrl != null ? NetworkImage(user!.avatarUrl!) : null, child: user?.avatarUrl == null ? const Icon(Icons.person, size: 24, color: Color(0xFF8B9CF7)) : null), )), const SizedBox(width: 10), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(mainAxisSize: MainAxisSize.min, children: [Icon(Icons.smart_toy_outlined, size: 16, color: const Color(0xFF8B9CF7)), const SizedBox(width: 4), Text('AI 健康管家', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: Colors.grey[600]))]), const SizedBox(height: 2), Text('${_getGreeting()},${user?.name ?? '张三'}!', style: const TextStyle(fontSize: 17, fontWeight: FontWeight.bold, color: Color(0xFF1A1A1A))), ])), Icon(Icons.notifications_none, size: 22, color: Colors.grey[600]), ]), ); } String _getGreeting() { final hour = DateTime.now().hour; if (hour < 9) return '早上好'; if (hour < 12) return '上午好'; if (hour < 18) return '下午好'; return '晚上好'; } // ═════════════════════ 今日任务(可折叠/展开) ═════════════════════ Widget _buildTaskCardsArea() { final latestHealth = ref.watch(latestHealthProvider); if (_taskCardsExpanded) { return latestHealth.when( data: (data) => _taskCardContent(data), loading: () => _taskCardContent({}), error: (_, __) => _taskCardContent({}), ); } // 折叠状态:与展开态容器完全相同,只保留标题行 return GestureDetector( onTap: () => setState(() => _taskCardsExpanded = true), behavior: HitTestBehavior.opaque, child: Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), padding: const EdgeInsets.all(14), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: const Color(0xFF8B9CF7).withAlpha(8), blurRadius: 6, offset: const Offset(0, 1))]), child: Row(children: [ const Text('今日任务', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const Spacer(), const Text('展开 ▾', style: TextStyle(fontSize: 12, color: Color(0xFF8B9CF7))), ]), ), ); } Widget _taskCardContent(Map healthData) { return Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), padding: const EdgeInsets.all(14), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: const Color(0xFF8B9CF7).withAlpha(8), blurRadius: 6, offset: const Offset(0, 1))]), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ const Text('今日任务', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const Spacer(), GestureDetector(onTap: () => setState(() => _taskCardsExpanded = false), child: Row(mainAxisSize: MainAxisSize.min, children: [ const Text('收起', style: TextStyle(fontSize: 12, color: Color(0xFF999999))), const Text('∧', style: TextStyle(fontSize: 12, color: Color(0xFF999999))), ])), ]), const SizedBox(height: 10), ..._getTodayTasks(healthData), ]), ); } List _getTodayTasks(Map healthData) { final now = DateTime.now(); final tasks = []; // 1. 数据摘要卡片(有今日指标数据时显示) final summaryParts = []; final bp = healthData['BloodPressure']; if (bp is Map) { final s = bp['systolic']; final d = bp['diastolic']; if (s != null && d != null) summaryParts.add('血压 $s/$d'); } final hr = healthData['HeartRate']; if (hr is int) summaryParts.add('心率 $hr'); final bs = healthData['BloodSugar']; if (bs is num) summaryParts.add('血糖 $bs'); final bo = healthData['BloodOxygen']; if (bo is num) summaryParts.add('血氧 $bo'); final wt = healthData['Weight']; if (wt is num) summaryParts.add('体重 $wt'); if (summaryParts.isNotEmpty) { tasks.add(_summaryCard(summaryParts)); } // 2. 用药提醒(从后端拉取真实数据) final reminders = ref.watch(medicationReminderProvider); reminders.whenData((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( icon: Icons.medication_rounded, label: '$name $dosage ($times)', status: 'pending', isOverdue: medOverdue, onTap: () => _handleMedicationCheck, )); } }); // 无提醒时显示默认用药卡片 if (tasks.length <= (summaryParts.isNotEmpty ? 1 : 0)) { tasks.add(_taskRow( icon: Icons.medication_rounded, label: '暂无用药提醒', status: 'pending', onTap: null, )); } // 3. 运动卡片(超时变红) final exOverdue = now.hour >= 18 && !_exerciseDone; tasks.add(_taskRow( icon: Icons.directions_run, label: '今日待运动:散步 30 分钟', status: _exerciseDone ? 'done' : 'pending', isOverdue: exOverdue, onTap: _exerciseDone ? null : () => setState(() => _exerciseDone = true), )); // 4. 测量卡片 tasks.add(_taskRow( icon: Icons.today, label: '今日测量:血压', status: 'pending', onTap: () => _textCtrl.text = '血压 ', )); // 5. 异常指标 tasks.addAll(_buildAbnormalRows(healthData)); // 6. 复查提醒(未来3天内有复查安排时显示) final upcomingFollowUps = _mockFollowUps.where((f) { final date = f['date'] as DateTime; return date.difference(now).inDays <= 3 && date.isAfter(now); }).toList(); if (upcomingFollowUps.isNotEmpty) { tasks.add(_followUpCard(upcomingFollowUps.first)); } return tasks; } List _buildAbnormalRows(Map healthData) { final rows = []; final bp = healthData['BloodPressure']; if (bp is Map) { final s = bp['systolic']; if (s is int && s >= 140) rows.add(_taskRow(icon: Icons.warning_amber_rounded, label: '血压 $s/${bp['diastolic'] ?? '--'} 偏高', status: 'warning', onTap: () => pushRoute(ref, 'trend', params: {'type': 'blood_pressure'}))); } return rows; } Widget _summaryCard(List parts) { return Padding( padding: const EdgeInsets.only(bottom: 10), child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => pushRoute(ref, 'trend'), child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: const Color(0xFFF1F8E9), borderRadius: BorderRadius.circular(10), ), child: Row(children: [ const Icon(Icons.check_circle, size: 18, color: Color(0xFF43A047)), const SizedBox(width: 10), Expanded(child: Text( '今日已记录:${parts.join('、')}', style: const TextStyle(fontSize: 13, color: Color(0xFF333333)), )), ]), ), ), ); } Widget _followUpCard(Map followUp) { final date = followUp['date'] as DateTime; final now = DateTime.now(); final diff = date.difference(now).inDays; String dateLabel; if (diff == 0) { dateLabel = '今天'; } else if (diff == 1) { dateLabel = '明天'; } else if (diff == 2) { dateLabel = '后天'; } else { dateLabel = '$diff天后'; } return _taskRow( icon: Icons.event_available, label: '📋 $dateLabel ${followUp['hospital']} ${followUp['department']} ${followUp['type']}', status: 'pending', onTap: () => pushRoute(ref, 'followups'), ); } Widget _taskRow({required IconData icon, required String label, required String status, VoidCallback? onTap, bool isOverdue = false}) { final colors = {'done': const Color(0xFF43A047), 'warning': const Color(0xFFFF9800), 'pending': const Color(0xFF9E9E9E)}; final icons = {'done': Icons.check_circle, 'warning': Icons.warning, 'pending': Icons.circle_outlined}; final effectiveStatus = isOverdue ? 'warning' : status; return Padding( padding: const EdgeInsets.only(bottom: 10), 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(label, style: const TextStyle(fontSize: 13, color: Color(0xFF333333)))), Icon(icons[effectiveStatus], size: 18, color: colors[effectiveStatus] ?? Colors.grey), ]), ), ), ); } void _handleMedicationCheck() async { await ref.read(medicationServiceProvider).confirm(''); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已记录服药 ✅'), backgroundColor: Color(0xFF8B9CF7))); } // ═════════════════════ 智能体选择条(常驻) ═════════════════════ static final _agentDefs = [ (ActiveAgent.consultation, '问诊', Icons.chat_bubble_outline), (ActiveAgent.health, '记数据', Icons.favorite_border), (ActiveAgent.diet, '拍饮食', Icons.restaurant_outlined), (ActiveAgent.medication, '药管家', Icons.medication_outlined), (ActiveAgent.report, '看报告', Icons.description_outlined), (ActiveAgent.exercise, '运动', Icons.directions_run_outlined), ]; Widget _buildAgentBar(ActiveAgent? selected) { return Container( height: 36, padding: const EdgeInsets.symmetric(horizontal: 12), child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: _agentDefs.length, separatorBuilder: (_, __) => const SizedBox(width: 6), itemBuilder: (_, i) { final (agent, label, icon) = _agentDefs[i]; final isActive = selected == agent; return GestureDetector( onTap: () { final notifier = ref.read(selectedAgentProvider.notifier); if (isActive) { notifier.select(null); } else { notifier.select(agent); ref.read(chatProvider.notifier).setAgent(agent); if (_welcomedAgents.add(agent)) { ref.read(chatProvider.notifier).insertAgentWelcome(agent); } } }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: isActive ? const Color(0xFF8B9CF7) : Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: isActive ? const Color(0xFF8B9CF7) : const Color(0xFFE0E0E0)), ), child: Row(mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 13, color: isActive ? Colors.white : const Color(0xFF666666)), const SizedBox(width: 3), Text(label, style: TextStyle(fontSize: 11, fontWeight: isActive ? FontWeight.w600 : FontWeight.w500, color: isActive ? Colors.white : const Color(0xFF666666))), ]), ), ); }, ), ); } // ═════════════════════ 底部合并区:智能体栏 + 操作面板 + 输入框 ═════════════════════ Widget _buildBottomBar(BuildContext context, ActiveAgent? selectedAgent) { return Column(mainAxisSize: MainAxisSize.min, children: [ // 智能体胶囊栏(常驻,高度36) _buildAgentBar(selectedAgent), // 图片预览(有选中图片时显示) if (_pickedImagePath != null) _buildImagePreview(), // 输入框 _buildCompactInputBar(context), ]); } Widget _buildImagePreview() { return Container( padding: const EdgeInsets.fromLTRB(12, 8, 12, 4), decoration: const BoxDecoration(color: Colors.white, border: Border(top: BorderSide(color: Color(0xFFEEEEEE)))), child: Row(children: [ Stack(children: [ ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.file(File(_pickedImagePath!), width: 60, height: 60, fit: BoxFit.cover), ), Positioned(top: -4, right: -4, child: GestureDetector( onTap: () => setState(() => _pickedImagePath = null), child: Container(width: 20, height: 20, decoration: const BoxDecoration(color: Color(0xFF333333), shape: BoxShape.circle), child: const Icon(Icons.close, size: 14, color: Colors.white)), )), ]), const Spacer(), Text('点击发送上传图片', style: TextStyle(fontSize: 12, color: Colors.grey[500])), ]), ); } Widget _buildCompactInputBar(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), decoration: BoxDecoration(color: Colors.white, border: Border(top: BorderSide(color: const Color(0xFFEEEEEE)))), child: Row(children: [ IconButton(icon: const Icon(Icons.attach_file, size: 24, color: Color(0xFF666666)), onPressed: () => _showAttachmentPicker(context)), Expanded(child: TextField( controller: _textCtrl, style: const TextStyle(fontSize: 15), decoration: const InputDecoration(hintText: '输入你想说的...', hintStyle: TextStyle(fontSize: 15, color: Color(0xFFBBBBBB)), contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 10), border: InputBorder.none), onSubmitted: (_) => _sendMessage(), )), IconButton(icon: const Icon(Icons.send, size: 24, color: Color(0xFF8B9CF7)), onPressed: _sendMessage), ]), ); } Future _pickImage(ImageSource source) async { final picker = ImagePicker(); final picked = await picker.pickImage(source: source, imageQuality: 85); if (picked != null) { final token = await ref.read(apiClientProvider).accessToken; if (token == null) return; setState(() => _pickedImagePath = picked.path); } } void _showAttachmentPicker(BuildContext context) { showModalBottomSheet(context: context, builder: (ctx) => SafeArea(child: Wrap(children: [ ListTile(leading: const Icon(Icons.camera_alt), title: const Text('拍照'), onTap: () { Navigator.pop(ctx); _pickImage(ImageSource.camera); }), ListTile(leading: const Icon(Icons.photo_library), title: const Text('从相册选'), onTap: () { Navigator.pop(ctx); _pickImage(ImageSource.gallery); }), ListTile(leading: const Icon(Icons.attach_file), title: const Text('传文件'), onTap: () async { Navigator.pop(ctx); final result = await FilePicker.platform.pickFiles(); if (result != null && result.files.isNotEmpty) { _textCtrl.text = '[文件已选择] ${result.files.first.name}'; if (mounted) setState(() {}); }}), ]))); } }