import 'dart:io'; 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 'package:shadcn_ui/shadcn_ui.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'; import '../../widgets/health_drawer.dart'; import '../diet/diet_capture_page.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(); String? _pickedImagePath; final Set _welcomedAgents = {}; final _focusNode = FocusNode(); @override void initState() { super.initState(); } @override void dispose() { _textCtrl.dispose(); _scrollCtrl.dispose(); _focusNode.dispose(); super.dispose(); } void _sendMessage() { final text = _textCtrl.text.trim(); final imagePath = _pickedImagePath; if (text.isEmpty && imagePath == null) return; _textCtrl.clear(); setState(() => _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); final theme = ShadTheme.of(context); 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(); } }); ref.listen(dietActionProvider, (prev, next) { if (next == 'pickFoodCamera') { _pickFoodImage(ImageSource.camera); ref.read(dietActionProvider.notifier).clear(); } else if (next == 'pickFoodGallery') { _pickFoodImage(ImageSource.gallery); ref.read(dietActionProvider.notifier).clear(); } }); return Scaffold( drawer: const HealthDrawer(), backgroundColor: theme.colorScheme.background, body: SafeArea( child: Column(children: [ _buildHeader(user, theme), Expanded(child: ChatMessagesView(scrollCtrl: _scrollCtrl, messages: chatState.messages)), _buildBottomBar(context, selectedAgent, theme), ]), ), ); } // ═══════ 顶部栏 ═══════ Widget _buildHeader(dynamic user, ShadThemeData theme) { return Container( padding: const EdgeInsets.symmetric(horizontal: AppTheme.sLg, vertical: 10), decoration: BoxDecoration( color: theme.colorScheme.card, border: Border(bottom: BorderSide(color: theme.colorScheme.border, width: 0.5)), ), child: Row(children: [ Builder(builder: (ctx) => GestureDetector( onTap: () => Scaffold.of(ctx).openDrawer(), child: CircleAvatar( radius: 20, backgroundColor: AppTheme.primaryLight, backgroundImage: user?.avatarUrl != null ? NetworkImage(user!.avatarUrl!) : null, child: user?.avatarUrl == null ? const Icon(Icons.person, size: 28, color: AppTheme.primary) : null, ), )), const SizedBox(width: 10), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(mainAxisSize: MainAxisSize.min, children: [ Icon(LucideIcons.bot, size: 19, color: AppTheme.primary), const SizedBox(width: 4), Text('AI 健康管家', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: theme.colorScheme.mutedForeground)), ]), const SizedBox(height: 2), Text('${_getGreeting()},${(user?.name != null && user!.name!.isNotEmpty) ? user.name : '用户'}!', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: theme.colorScheme.foreground)), ])), Icon(LucideIcons.bell, size: 25, color: theme.colorScheme.mutedForeground), ]), ); } String _getGreeting() { final hour = DateTime.now().hour; if (hour < 9) return '早上好'; if (hour < 12) return '上午好'; if (hour < 18) return '下午好'; return '晚上好'; } // ═══════ 智能体选择条 ═══════ static final _agentDefs = [ (ActiveAgent.consultation, '问诊', LucideIcons.messageCircle), (ActiveAgent.health, '记数据', LucideIcons.heart), (ActiveAgent.diet, '拍饮食', LucideIcons.utensils), (ActiveAgent.medication, '药管家', LucideIcons.pill), (ActiveAgent.report, '看报告', LucideIcons.fileText), (ActiveAgent.exercise, '运动', LucideIcons.trendingUp), ]; Widget _buildAgentBar(ActiveAgent? selected, ShadThemeData theme) { return Container( height: 40, padding: const EdgeInsets.symmetric(horizontal: AppTheme.sMd), child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: _agentDefs.length, separatorBuilder: (_, i) => 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: AnimatedContainer( duration: const Duration(milliseconds: 200), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: isActive ? AppTheme.primary : theme.colorScheme.card, borderRadius: BorderRadius.circular(AppTheme.rPill), boxShadow: isActive ? [AppTheme.shadowLight] : null, ), child: Row(mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 17, color: isActive ? Colors.white : theme.colorScheme.mutedForeground), const SizedBox(width: 4), Text(label, style: TextStyle(fontSize: 15, fontWeight: isActive ? FontWeight.w600 : FontWeight.w500, color: isActive ? Colors.white : theme.colorScheme.mutedForeground)), ]), ), ); }, ), ); } // ═══════ 底部区域 ═══════ Widget _buildBottomBar(BuildContext context, ActiveAgent? selectedAgent, ShadThemeData theme) { return Column(mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.only(top: AppTheme.sSm, bottom: 6), child: _buildAgentBar(selectedAgent, theme), ), if (_pickedImagePath != null) _buildImagePreview(theme), _buildCompactInputBar(theme), ]); } Widget _buildImagePreview(ShadThemeData theme) { return Container( padding: const EdgeInsets.fromLTRB(12, 8, 12, 4), decoration: BoxDecoration( color: theme.colorScheme.card, border: Border(top: BorderSide(color: theme.colorScheme.border)), ), child: Row(children: [ Stack(children: [ ClipRRect( borderRadius: BorderRadius.circular(AppTheme.rXs), 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: AppTheme.text, shape: BoxShape.circle), child: const Icon(Icons.close, size: 17, color: Colors.white), ), )), ]), const Spacer(), Text('点击发送上传图片', style: TextStyle(fontSize: 15, color: theme.colorScheme.mutedForeground)), ]), ); } Widget _buildCompactInputBar(ShadThemeData theme) { return Container( padding: const EdgeInsets.symmetric(horizontal: AppTheme.sSm, vertical: AppTheme.sSm), decoration: BoxDecoration( color: theme.colorScheme.card, border: Border(top: BorderSide(color: theme.colorScheme.border)), ), child: Row(crossAxisAlignment: CrossAxisAlignment.end, children: [ ShadButton.ghost( size: ShadButtonSize.sm, leading: Icon(LucideIcons.paperclip, size: 25, color: theme.colorScheme.mutedForeground), onPressed: () => _showAttachmentPicker(context, theme), ), Expanded( child: TextField( controller: _textCtrl, focusNode: _focusNode, style: TextStyle(fontSize: 18, color: theme.colorScheme.foreground), maxLines: null, textInputAction: TextInputAction.newline, decoration: InputDecoration( hintText: '输入你想说的...', hintStyle: TextStyle(fontSize: 18, color: theme.colorScheme.mutedForeground), filled: true, fillColor: theme.colorScheme.muted, contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), border: OutlineInputBorder( borderRadius: BorderRadius.circular(AppTheme.rXl), borderSide: BorderSide.none, ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppTheme.rXl), borderSide: BorderSide.none, ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppTheme.rXl), borderSide: BorderSide.none, ), ), onSubmitted: (_) => _sendMessage(), ), ), const SizedBox(width: 4), ShadButton( size: ShadButtonSize.sm, leading: Icon(LucideIcons.send, size: 21, color: theme.colorScheme.primaryForeground), onPressed: _sendMessage, ), ]), ); } void _showAttachmentPicker(BuildContext context, ShadThemeData theme) { showModalBottomSheet( context: context, backgroundColor: theme.colorScheme.card, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(AppTheme.rXl)), ), builder: (ctx) => SafeArea( child: Padding( padding: const EdgeInsets.symmetric(vertical: AppTheme.sMd), child: Wrap(children: [ ListTile( leading: Icon(LucideIcons.camera, color: theme.colorScheme.foreground), title: Text('拍照', style: TextStyle(color: theme.colorScheme.foreground)), onTap: () { Navigator.pop(ctx); _pickImage(ImageSource.camera); }, ), ListTile( leading: Icon(LucideIcons.image, color: theme.colorScheme.foreground), title: Text('从相册选', style: TextStyle(color: theme.colorScheme.foreground)), onTap: () { Navigator.pop(ctx); _pickImage(ImageSource.gallery); }, ), ListTile( leading: Icon(LucideIcons.file, color: theme.colorScheme.foreground), title: Text('传文件', style: TextStyle(color: theme.colorScheme.foreground)), 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(() {}); } }, ), ]), ), ), ); } 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); } } Future _pickFoodImage(ImageSource source) async { final picker = ImagePicker(); final picked = await picker.pickImage(source: source, imageQuality: 80, maxWidth: 1024, maxHeight: 1024); if (picked != null && mounted) { ref.read(dietProvider.notifier).reset(); ref.read(dietProvider.notifier).setImage(picked.path); ref.read(dietProvider.notifier).analyzeImage(); pushRoute(ref, 'dietCapture'); } } }