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 '../../core/api_client.dart'; import '../../core/navigation_provider.dart'; import '../../providers/auth_provider.dart'; import '../../providers/chat_provider.dart'; import '../../providers/data_providers.dart'; import '../../widgets/agent_bar.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; bool _showExpandButton = false; @override void initState() { super.initState(); _scrollCtrl.addListener(_onScroll); } @override void dispose() { _textCtrl.dispose(); _scrollCtrl.dispose(); super.dispose(); } void _onScroll() { if (_scrollCtrl.offset > 50 && !_showExpandButton) { setState(() => _showExpandButton = true); } else if (_scrollCtrl.offset <= 50 && _showExpandButton) { setState(() => _showExpandButton = false); } } void _sendMessage() { final text = _textCtrl.text.trim(); if (text.isEmpty) return; _textCtrl.clear(); ref.read(chatProvider.notifier).sendMessage(text); } @override Widget build(BuildContext context) { final chatState = ref.watch(chatProvider); final selectedAgent = ref.watch(selectedAgentProvider); return Scaffold( drawer: const HealthDrawer(), body: SafeArea( child: Stack(children: [ Column(children: [ _buildHeader(context), if (_taskCardsExpanded) _buildTaskCards(), Expanded(child: ChatMessagesView(scrollCtrl: _scrollCtrl, messages: chatState.messages)), _buildAgentPanel(context, selectedAgent), const AgentBar(), _buildInputBar(), ]), _buildExpandButton(), ]), ), ); } Widget _buildExpandButton() { if (!_showExpandButton || _taskCardsExpanded) return const SizedBox.shrink(); return Positioned( top: 60, right: 16, child: AnimatedOpacity( opacity: _showExpandButton ? 1.0 : 0.0, duration: const Duration(milliseconds: 300), child: FloatingActionButton( onPressed: () => setState(() => _taskCardsExpanded = true), mini: true, backgroundColor: const Color(0xFF635BFF), child: const Icon(Icons.keyboard_arrow_down, size: 20), ), ), ); } Widget _buildHeader(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row(children: [ Builder(builder: (ctx) => IconButton( icon: const Icon(Icons.menu, size: 24), onPressed: () => Scaffold.of(ctx).openDrawer(), )), const Spacer(), Text('健康管家', style: Theme.of(context).textTheme.titleLarge), const Spacer(), const SizedBox(width: 48), ]), ); } Widget _buildTaskCards() { final latestHealth = ref.watch(latestHealthProvider); return latestHealth.when( data: (data) { final tasks = _getTaskCards(data); if (tasks.isEmpty) return const SizedBox.shrink(); return Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFFFEFEFF), borderRadius: BorderRadius.circular(24), boxShadow: [BoxShadow(color: const Color(0xFF635BFF).withAlpha(10), blurRadius: 8, offset: const Offset(0, 2))], ), child: Column(children: [ Row(children: [ const Icon(Icons.wb_sunny, size: 20, color: Color(0xFFFFB800)), const SizedBox(width: 8), Text(_getGreeting(), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const Spacer(), GestureDetector( onTap: () => setState(() => _taskCardsExpanded = false), child: const Icon(Icons.keyboard_arrow_down, size: 22, color: Color(0xFF999999)), ), ]), const SizedBox(height: 12), Column(children: tasks), ]), ); }, loading: () => const SizedBox.shrink(), error: (_, __) { final tasks = _getTaskCards(const {}); if (tasks.isEmpty) return const SizedBox.shrink(); return Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFFFEFEFF), borderRadius: BorderRadius.circular(24), boxShadow: [BoxShadow(color: const Color(0xFF635BFF).withAlpha(10), blurRadius: 8, offset: const Offset(0, 2))], ), child: Column(children: [ Row(children: [ const Icon(Icons.wb_sunny, size: 20, color: Color(0xFFFFB800)), const SizedBox(width: 8), Text(_getGreeting(), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const Spacer(), GestureDetector( onTap: () => setState(() => _taskCardsExpanded = false), child: const Icon(Icons.keyboard_arrow_down, size: 22, color: Color(0xFF999999)), ), ]), const SizedBox(height: 12), Column(children: tasks), ]), ); }, ); } String _getGreeting() { final hour = DateTime.now().hour; if (hour < 6) return '夜深了'; if (hour < 9) return '早上好'; if (hour < 12) return '上午好'; if (hour < 14) return '中午好'; if (hour < 18) return '下午好'; if (hour < 22) return '晚上好'; return '夜深了'; } List _getTaskCards(Map healthData) { final cards = []; cards.add(_buildMedicationCard()); cards.add(_buildExerciseCard()); cards.add(_buildMeasurementCard()); final abnormalCards = _buildAbnormalCards(healthData); cards.addAll(abnormalCards); final summaryCard = _buildSummaryCard(healthData); if (summaryCard != null) cards.add(summaryCard); return cards; } Widget _buildMedicationCard() { return _buildTaskCard( '💊', '计划 8:00 吃 阿司匹林 100mg', Icons.check_circle_outline, () => _handleMedicationCheck(), type: 'medication', ); } Widget _buildExerciseCard() { return _buildTaskCard( '🏃', '今日待运动:散步 30 分钟', Icons.check_circle_outline, () => _handleExerciseCheck(), type: 'exercise', ); } Widget _buildMeasurementCard() { return _buildTaskCard( '🩺', '今日待测量:血压', Icons.arrow_forward_ios, () => _textCtrl.text = '血压 ', type: 'measurement', ); } List _buildAbnormalCards(Map healthData) { final cards = []; final bp = healthData['BloodPressure']; if (bp != null && bp is Map) { final systolic = bp['systolic']; final diastolic = bp['diastolic']; if (systolic != null && systolic >= 140) { cards.add(_buildTaskCard( '⚠️', '昨日血压 ${systolic}/${diastolic ?? '--'},偏高', Icons.arrow_forward_ios, () => pushRoute(ref, 'trend', params: {'type': 'blood_pressure'}), type: 'warning', highlight: true, )); } } final hr = healthData['HeartRate']; if (hr != null && hr is Map) { final value = hr['value']; if (value != null && (value > 100 || value < 60)) { cards.add(_buildTaskCard( '⚠️', '昨日心率 $value,${value > 100 ? '偏高' : '偏低'}', Icons.arrow_forward_ios, () => pushRoute(ref, 'trend', params: {'type': 'heart_rate'}), type: 'warning', highlight: true, )); } } return cards; } Widget? _buildSummaryCard(Map healthData) { final values = []; final bp = healthData['BloodPressure']; if (bp != null && bp is Map) { final sys = bp['systolic']; final dia = bp['diastolic']; if (sys != null && dia != null) values.add('血压 $sys/$dia'); } final hr = healthData['HeartRate']; if (hr != null && hr is Map && hr['value'] != null) { values.add('心率 ${hr['value']}'); } final glucose = healthData['Glucose']; if (glucose != null && glucose is Map && glucose['value'] != null) { values.add('血糖 ${glucose['value']}'); } if (values.isEmpty) return null; return _buildTaskCard( '📊', '今日已记录:${values.join('、')}', Icons.arrow_forward_ios, () => pushRoute(ref, 'trend', params: {'type': 'blood_pressure'}), type: 'summary', ); } Widget _buildTaskCard(String icon, String text, IconData actionIcon, VoidCallback onTap, {String type = '', bool highlight = false}) { return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Container( padding: const EdgeInsets.symmetric(vertical: 8), decoration: highlight ? BoxDecoration( color: const Color(0xFFFDF2F2), borderRadius: BorderRadius.circular(12), ) : null, child: Row(children: [ Text(icon, style: const TextStyle(fontSize: 20)), const SizedBox(width: 10), Expanded(child: Text(text, style: TextStyle( fontSize: 14, color: highlight ? const Color(0xFFDC2626) : const Color(0xFF333333), ))), GestureDetector( onTap: onTap, child: Icon(actionIcon, size: 20, color: highlight ? const Color(0xFFDC2626) : const Color(0xFF635BFF)), ), ]), ), ); } void _handleMedicationCheck() async { await ref.read(medicationServiceProvider).confirm(''); ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('已记录服药 ✅'), backgroundColor: Color(0xFF635BFF), duration: Duration(seconds: 2), )); } void _handleExerciseCheck() async { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('已完成运动 ✅'), backgroundColor: Color(0xFF635BFF), duration: Duration(seconds: 2), )); } Widget _buildAgentPanel(BuildContext context, ActiveAgent? agent) { if (agent == null) return const SizedBox.shrink(); return AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFFFEFEFF), borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), boxShadow: [BoxShadow(color: const Color(0xFF635BFF).withAlpha(20), blurRadius: 12, offset: const Offset(0, -4))], ), child: Column(mainAxisSize: MainAxisSize.min, children: [ _buildAgentPanelHeader(agent), const SizedBox(height: 12), ..._getAgentButtons(agent), ]), ); } Widget _buildAgentPanelHeader(ActiveAgent agent) { final titles = { ActiveAgent.consultation: '🩺 AI 问诊', ActiveAgent.health: '📊 记数据', ActiveAgent.diet: '📸 拍饮食', ActiveAgent.medication: '💊 药管家', ActiveAgent.report: '📋 看报告', ActiveAgent.exercise: '🏃 运动计划', }; final tips = { ActiveAgent.consultation: '或直接对我说你的症状', ActiveAgent.health: '或直接对我说:"血压 135/85"', ActiveAgent.diet: '或直接对我说:"中午吃了牛肉面"', ActiveAgent.medication: '或直接对我说:"医生让我吃阿托伐他汀 20mg"', ActiveAgent.report: '或直接上传报告图片', ActiveAgent.exercise: '或直接对我说:"每周一三五散步 30 分钟"', }; return Column(children: [ Text(titles[agent] ?? '', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const SizedBox(height: 4), Text(tips[agent] ?? '', style: const TextStyle(fontSize: 12, color: Color(0xFF999999))), ]); } List _getAgentButtons(ActiveAgent agent) { final buttons = []; if (agent == ActiveAgent.health) { buttons.add(_panelBtn('手动录入血压', Icons.favorite)); buttons.add(_panelBtn('手动录入血糖', Icons.bloodtype)); buttons.add(_panelBtn('手动录入心率', Icons.monitor_heart)); buttons.add(_panelBtn('手动录入血氧', Icons.air)); buttons.add(_panelBtn('手动录入体重', Icons.monitor_weight)); } else if (agent == ActiveAgent.diet) { buttons.add(_panelBtn('拍照', Icons.camera_alt)); buttons.add(_panelBtn('上传照片', Icons.photo_library)); } else if (agent == ActiveAgent.medication) { buttons.add(_panelBtn('用药管理', Icons.medication)); buttons.add(_panelBtn('用药提醒', Icons.alarm)); } else if (agent == ActiveAgent.consultation) { buttons.add(_panelBtn('找医生', Icons.person_search)); } else if (agent == ActiveAgent.exercise) { buttons.add(_panelBtn('查看本周计划', Icons.calendar_view_week)); buttons.add(_panelBtn('创建新计划', Icons.add_circle_outline)); } return buttons; } Widget _panelBtn(String label, IconData icon) { return Padding( padding: const EdgeInsets.only(bottom: 8), child: SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: () => _onAgentAction(label), icon: Icon(icon, size: 18), label: Text(label, style: const TextStyle(fontSize: 14)), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFF5F3FF), foregroundColor: const Color(0xFF635BFF), elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), ), ), ), ); } void _onAgentAction(String label) { switch (label) { case '拍照': pushRoute(ref, 'dietCapture'); break; case '上传照片': pushRoute(ref, 'dietCapture'); break; case '手动录入血压': _textCtrl.text = '血压 '; break; case '手动录入血糖': _textCtrl.text = '血糖 '; break; case '手动录入心率': _textCtrl.text = '心率 '; break; case '手动录入血氧': _textCtrl.text = '血氧 '; break; case '手动录入体重': _textCtrl.text = '体重 '; break; case '用药管理': pushRoute(ref, 'medications'); break; case '找医生': pushRoute(ref, 'doctors'); break; case '查看本周计划': pushRoute(ref, 'exercisePlan'); break; case '创建新计划': pushRoute(ref, 'exercisePlan'); break; } } 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; _textCtrl.text = '[图片已上传] $baseUrl/api/files/${picked.path.split('/').last}'; setState(() {}); } } 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}'; setState(() {}); } }, ), ], ), ), ); } Widget _buildInputBar() { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: Colors.white, border: Border(top: BorderSide(color: Colors.grey.shade200)), ), child: Row(children: [ IconButton(icon: const Icon(Icons.attach_file, size: 24, color: Color(0xFF666666)), onPressed: () => _showAttachmentPicker(context)), Expanded( child: TextField( controller: _textCtrl, decoration: const InputDecoration(hintText: '输入你想说的...', contentPadding: EdgeInsets.symmetric(horizontal: 12), border: InputBorder.none), onSubmitted: (_) => _sendMessage(), ), ), IconButton(icon: const Icon(Icons.send, size: 24, color: Color(0xFF635BFF)), onPressed: _sendMessage), ]), ); } }