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_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'; 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(); int _lastMsgCount = 0; @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(); _focusNode.unfocus(); 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; 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(); } }); final currentCount = chatState.messages.length; if (currentCount > _lastMsgCount) { WidgetsBinding.instance.addPostFrameCallback((_) { if (_scrollCtrl.hasClients) { _scrollCtrl.jumpTo(_scrollCtrl.position.maxScrollExtent); } }); } _lastMsgCount = currentCount; return Scaffold( drawer: const HealthDrawer(), body: Container( decoration: const BoxDecoration(gradient: AppColors.bgGradient), child: SafeArea( child: Column(children: [ _buildHeader(user), Expanded(child: ChatMessagesView(scrollCtrl: _scrollCtrl, messages: chatState.messages)), _buildBottomBar(context), ]), ), ), ); } // ═══════ 顶部栏 — 透明背景,与页面渐变无缝衔接 ═══════ 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: const Padding( padding: EdgeInsets.all(8), child: Icon(LucideIcons.menu, size: 22, color: AppColors.primary), ), )), const SizedBox(width: 12), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('AI 健康管家', style: TextStyle(fontSize: 13, color: AppColors.textHint)), Text('${_getGreeting()},${(user?.name != null && user!.name!.isNotEmpty) ? user.name : '用户'}!', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: AppColors.textPrimary)), ])), GestureDetector( onTap: () => pushRoute(ref, 'notificationPrefs'), child: const Icon(LucideIcons.bell, size: 22, color: AppColors.textSecondary), ), ]), ); } 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() { return SizedBox( height: 32, child: ListView.separated( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: _agentDefs.length, separatorBuilder: (_, i) => const SizedBox(width: 8), itemBuilder: (_, i) { final (agent, label, icon) = _agentDefs[i]; return GestureDetector( onTap: () => ref.read(chatProvider.notifier).triggerAgent(agent, label), child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(18), boxShadow: AppColors.cardShadowLight, ), child: Row(mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 17, color: AppColors.textPrimary), const SizedBox(width: 5), Text(label, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: AppColors.textPrimary)), ]), ), ); }, ), ); } // ═══════ 底部输入区 ═══════ Widget _buildBottomBar(BuildContext context) { return Column(mainAxisSize: MainAxisSize.min, children: [ Container(padding: const EdgeInsets.only(top: 4, bottom: 4), child: _buildAgentBar()), if (_pickedImagePath != null) _buildImagePreview(), _buildInputBar(), ]); } Widget _buildImagePreview() { return Container( padding: const EdgeInsets.fromLTRB(12, 8, 12, 4), color: AppColors.cardInner, child: Row(children: [ ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.file(File(_pickedImagePath!), width: 48, height: 48, fit: BoxFit.cover), ), const SizedBox(width: 10), const Text('点击发送图片', style: TextStyle(fontSize: 14, color: AppColors.textSecondary)), const Spacer(), GestureDetector( onTap: () => setState(() => _pickedImagePath = null), child: const Icon(Icons.close, size: 20, color: AppColors.textHint), ), ]), ); } Widget _buildInputBar() { return Container( padding: const EdgeInsets.fromLTRB(12, 8, 12, 10), child: Row(crossAxisAlignment: CrossAxisAlignment.end, children: [ GestureDetector( onTap: () => _showAttachmentPicker(context), child: const Padding( padding: EdgeInsets.only(bottom: 10), child: Icon(LucideIcons.plus, size: 24, color: AppColors.textSecondary), ), ), const SizedBox(width: 8), Expanded( child: Container( constraints: const BoxConstraints(maxHeight: 120), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), ), child: TextField( controller: _textCtrl, focusNode: _focusNode, style: const TextStyle(fontSize: 15, color: AppColors.textPrimary), maxLines: null, textInputAction: TextInputAction.newline, decoration: const InputDecoration( hintText: '输入你想说的...', hintStyle: TextStyle(fontSize: 15, color: AppColors.textHint), filled: true, fillColor: Colors.white, contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 10), border: OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(20)), borderSide: BorderSide.none), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(20)), borderSide: BorderSide.none), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(20)), borderSide: BorderSide.none), ), onSubmitted: (_) => _sendMessage(), ), ), ), const SizedBox(width: 8), GestureDetector( onTap: _sendMessage, child: Container( width: 40, height: 40, margin: EdgeInsets.only(bottom: _pickedImagePath != null ? 0 : 4), decoration: BoxDecoration( gradient: const LinearGradient( colors: [AppColors.blueMeasure, AppColors.primary], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(20), ), child: const Icon(LucideIcons.send, size: 20, color: Colors.white), ), ), ]), ); } void _showAttachmentPicker(BuildContext context) { showModalBottomSheet( context: context, backgroundColor: Colors.white, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (ctx) => SafeArea( child: Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Wrap(children: [ ListTile( leading: const Icon(Icons.camera_alt_outlined, color: AppColors.primary), title: const Text('拍照'), onTap: () { Navigator.pop(ctx); _pickImage(ImageSource.camera); }, ), ListTile( leading: const Icon(Icons.photo_library_outlined, color: AppColors.primary), title: const Text('从相册选'), onTap: () { Navigator.pop(ctx); _pickImage(ImageSource.gallery); }, ), ListTile( leading: const Icon(Icons.file_open_outlined, color: AppColors.primary), 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(() {}); } }, ), ]), ), ), ); } Future _pickImage(ImageSource source) async { final picked = await ImagePicker().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 picked = await ImagePicker().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'); } } }