import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'dart:math'; import '../controllers/chat_controller.dart'; import '../models/message_model.dart'; import '../../../core/theme/app_colors.dart'; import 'animated_gradient_background.dart'; import 'chat_music_card.dart'; import 'chat_navigation_card.dart'; import 'weather_day_card.dart'; class ChatView extends GetView { const ChatView({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; return Obx( () => Scaffold( backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.grey[50], appBar: controller.isVoiceRecognitionActive.value ? null : _buildAppBar(isDarkMode), body: controller.isVoiceRecognitionActive.value ? _buildVoiceInputPanel(isDarkMode) : _buildBody(isDarkMode), ), ); } // 构建AppBar PreferredSizeWidget _buildAppBar(bool isDarkMode) { return AppBar( elevation: 1, backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white, leading: IconButton( icon: Icon(Icons.arrow_back_ios, color: isDarkMode ? Colors.white : Colors.black87, size: 20.r), onPressed: () => Get.back(), ), titleSpacing: 0, title: Row( children: [ // AI头像 Container( width: 40.r, height: 40.r, decoration: BoxDecoration( color: AppColors.primary, borderRadius: BorderRadius.circular(20.r), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 3, offset: const Offset(0, 1), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(20.r), child: controller.agent.avatarUrl.isNotEmpty ? Image.asset( controller.agent.avatarUrl, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Icon( Icons.smart_toy_rounded, color: Colors.white, size: 22.r, ); }, ) : Icon( Icons.smart_toy_rounded, color: Colors.white, size: 22.r, ), ), ), SizedBox(width: 12.w), // AI名称和状态 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( '${controller.agent.name}'.tr, style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.bold, color: isDarkMode ? Colors.white : Colors.black87, ), ), Row( children: [ Container( width: 6.r, height: 6.r, decoration: const BoxDecoration( color: Colors.green, shape: BoxShape.circle, ), ), SizedBox(width: 4.w), Text( 'online'.tr, // 在线 style: TextStyle( fontSize: 12.sp, color: Colors.green, ), ), ], ), ], ), ), ], ), actions: [ IconButton( icon: Icon( Icons.phone, color: isDarkMode ? Colors.white : Colors.black87, ), onPressed: controller.startVoiceRecognition, ), // TTS开关 Obx(() => IconButton( icon: Icon( controller.isTtsEnabled.value ? Icons.volume_up : Icons.volume_off, color: controller.isTtsEnabled.value ? AppColors.primary : isDarkMode ? Colors.grey[400] : Colors.grey, ), onPressed: controller.toggleTts, )), // 更多选项 Container( width: 34.r, height: 34.r, margin: EdgeInsets.only(right: 8.w), decoration: BoxDecoration( color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], shape: BoxShape.circle, boxShadow: [ if (!isDarkMode) BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 2, spreadRadius: 0.5, ), ], ), child: PopupMenuButton( icon: Icon(Icons.more_vert, color: isDarkMode ? Colors.white : Colors.black87, size: 20.r), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12.r), ), color: isDarkMode ? AppColors.cardBackground : Colors.white, itemBuilder: (context) => [ PopupMenuItem( value: 'clear', child: Row( children: [ const Icon(Icons.delete_outline, color: Colors.red), SizedBox(width: 8.w), Text( 'clearChatHistory'.tr, // 清空聊天记录 style: TextStyle( color: isDarkMode ? Colors.white : Colors.black87, ), ), ], ), onTap: () { // 延迟执行,避免在弹出菜单关闭前执行 Future.delayed( const Duration(milliseconds: 100), () => controller.clearChat(), ); }, ), ], ), ), ], ); } // 构建主体内容 Widget _buildBody(bool isDarkMode) { return Column( children: [ // 聊天内容区域 Expanded( child: Obx(() { if (controller.messages.isEmpty) { return _buildEmptyState(isDarkMode); } else { return _buildChatList(isDarkMode); } }), ), // 底部输入区域 _buildTextInputPanel(isDarkMode), ], ); } // 构建空状态 Widget _buildEmptyState(bool isDarkMode) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 80.w, height: 80.w, decoration: BoxDecoration( color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], shape: BoxShape.circle, ), child: Icon( Icons.chat_bubble_outline, size: 40.w, color: isDarkMode ? Colors.white.withOpacity(0.5) : Colors.grey[400], ), ), SizedBox(height: 24.h), Text( '你好!我是${controller.agent.name}助手', style: TextStyle( fontSize: 18.sp, fontWeight: FontWeight.bold, color: isDarkMode ? Colors.white : Colors.black87, ), ), SizedBox(height: 12.h), Padding( padding: EdgeInsets.symmetric(horizontal: 40.w), child: Text( '我能帮你回答问题、实时语音翻译、推荐音乐和播客等。有什么我可以帮你的吗?', textAlign: TextAlign.center, style: TextStyle( fontSize: 14.sp, color: isDarkMode ? Colors.grey[400] : Colors.grey[600], height: 1.5, ), ), ), ], ), ); } // 构建聊天列表 Widget _buildChatList(bool isDarkMode) { return ListView.builder( controller: controller.scrollController, padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h), itemCount: controller.messages.length, reverse: false, physics: const AlwaysScrollableScrollPhysics(), cacheExtent: 1000, // 增加缓存范围,减少重建 itemBuilder: (context, index) { final message = controller.messages[index]; return _buildMessageItem(message, isDarkMode); }, ); } // 构建消息项 Widget _buildMessageItem(Message message, bool isDarkMode) { final isUser = message.role == 'user'; return Padding( padding: EdgeInsets.only(bottom: 16.h), child: Row( mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!isUser) _buildAvatar(false, isDarkMode), SizedBox(width: 8.w), Flexible( child: Column( crossAxisAlignment: isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ // 消息气泡 Container( padding: EdgeInsets.all(12.r), decoration: BoxDecoration( color: isUser ? AppColors.primary : isDarkMode ? AppColors.cardBackground : Colors.grey[200], borderRadius: BorderRadius.circular(16.r), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 消息内容 _buildMessageContent(message, isDarkMode, isUser), // 加载中动画 if (message.isIntermediate) Padding( padding: EdgeInsets.only(top: 8.h), child: Row( mainAxisSize: MainAxisSize.min, children: List.generate( 3, (index) => _buildLoadingDot(index, isDarkMode), ), ), ), ], ), ), // 时间戳 Padding( padding: EdgeInsets.only(top: 4.h, left: 4.w, right: 4.w), child: Text( _formatTime(message.timestamp), style: TextStyle( fontSize: 10.sp, color: isDarkMode ? Colors.grey[500] : Colors.grey[500], ), ), ), ], ), ), SizedBox(width: 8.w), if (isUser) _buildAvatar(true, isDarkMode), ], ), ); } Widget _buildMessageContent(Message message, bool isDarkMode, bool isUser) { Widget box = const SizedBox(); if (message.content.isNotEmpty) { box = SelectableText( message.content.tr, style: TextStyle( color: isUser ? Colors.white : isDarkMode ? Colors.white : Colors.black, fontSize: 16.sp, height: 1.5, ), ); } if (message.meta != null) { if (message.meta?['card_weather'] != null) { box = WeatherDayCard(message.meta?['card_weather'], isDarkMode); } if (message.meta?['card_music'] != null) { box = ChatMusicCard(0, message.meta?['card_music'], isDarkMode); } if (message.meta?['card_spotify'] != null) { box = ChatMusicCard(1, message.meta?['card_spotify'], isDarkMode); } if (message.meta?['card_navigation'] != null) { box = ChatNavigationCard( message.meta?['card_navigation'], message.content, isDarkMode); } } return box; } // 构建文本输入面板 Widget _buildTextInputPanel(bool isDarkMode) { final textController = TextEditingController(text: controller.inputText.value); return Container( height: 48.w, margin: EdgeInsets.only(left: 16.w, right: 16.w, top: 10.w, bottom: 20.w), clipBehavior: Clip.hardEdge, decoration: BoxDecoration( color: controller.isHoldSpeak.value ? AppColors.primary : isDarkMode ? AppColors.cardBackground : Colors.white, borderRadius: BorderRadius.circular(12.w), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 3, offset: const Offset(0, -1), ), ], ), child: Row( children: [ if (!controller.isHoldSpeak.value) 6.horizontalSpace, if (!controller.isHoldSpeak.value) Padding( padding: EdgeInsets.all(6.w), child: Icon( Icons.camera_alt_outlined, size: 28.w, ), ), Expanded( child: Obx( () => controller.isTextInput.value ? Center( child: TextField( controller: textController, style: TextStyle( fontSize: 15.sp, color: isDarkMode ? Colors.white : Colors.black87, ), textInputAction: TextInputAction.send, decoration: InputDecoration( hintText: 'inputMessage'.tr, // 输入消息... hintStyle: TextStyle(color: Colors.grey[500]), filled: false, border: InputBorder.none, focusedBorder: InputBorder.none, contentPadding: EdgeInsets.only(left: 10.w), ), onSubmitted: (value) { if (value.trim().isNotEmpty) { controller.sendMessage(value); textController.clear(); controller.inputText.value = ''; // 清空输入框 } }, ), ) : Listener( onPointerDown: (event) { controller.talkLongPress(); }, onPointerUp: (event) { controller.talkLongPressUp(); }, child: Container( padding: EdgeInsets.only( left: controller.isHoldSpeak.value ? 0 : 40.w), color: Colors.transparent, child: controller.isHoldSpeak.value ? Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate(40, (int index) { return Container( width: 3.w, height: 10.w, margin: EdgeInsets.symmetric(horizontal: 2.w), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(3.r), ), ); }), ) : Center( child: Text( '按住说话', style: TextStyle( fontSize: 16.sp, color: isDarkMode ? Colors.white : Colors.black87, ), ), ), ), ), ), ), if (!controller.isHoldSpeak.value) GestureDetector( onTap: () { controller.isTextInput.value = !controller.isTextInput.value; }, child: Container( width: 28.w, height: 28.w, margin: EdgeInsets.all(6.w), alignment: Alignment.center, decoration: BoxDecoration( borderRadius: BorderRadius.circular(28.r), border: Border.all( color: isDarkMode ? Colors.white : Colors.black87, width: 2, ), ), child: Obx( () => Icon( controller.isTextInput.value ? Icons.mic : Icons.keyboard, size: 20.w, ), ), ), ), if (!controller.isHoldSpeak.value) Container( width: 28.w, height: 28.w, margin: EdgeInsets.all(6.w), alignment: Alignment.center, decoration: BoxDecoration( borderRadius: BorderRadius.circular(28.r), border: Border.all( color: isDarkMode ? Colors.white : Colors.black87, width: 2, ), ), child: Icon( Icons.add, size: 20.w, ), ), if (!controller.isHoldSpeak.value) 6.horizontalSpace, ], ), ); } // 构建语音输入面板 Widget _buildVoiceInputPanel(bool isDarkMode) { return AnimatedGradientBackground( SafeArea( child: Column( children: [ Row( children: [], ), Expanded( child: Container( width: 200.w, height: 200.w, decoration: BoxDecoration( color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.white, shape: BoxShape.circle, ), ), ), SizedBox( height: 80.h, child: Padding( padding: EdgeInsets.symmetric(horizontal: 20.w), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ // 语音录制按钮 Container( width: 60.r, height: 60.r, decoration: BoxDecoration( color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.white, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 8, spreadRadius: 1, ), ], ), child: Center( child: Obx(() => Icon( controller.isListening.value ? Icons.mic : Icons.mic_none, color: controller.isListening.value ? AppColors.primary : Colors.grey.shade400, size: 28.r, )), ), ), // 中间音频波形和状态文本 Expanded( child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ // 音频波形区域 Container( height: 50.h, child: Obx(() { final isSpeaking = controller.isSpeechDetected.value; return Center( child: Container( width: 220.w, height: 30.h, child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: List.generate( 23, (index) { // 随机高度模拟波形 double height = isSpeaking ? (Random().nextDouble() * 20.0 + 5.0) .h : 2.h; return SizedBox( width: 6.w, child: Center( child: AnimatedContainer( duration: const Duration( milliseconds: 500), width: 2.5.w, height: height, margin: EdgeInsets.symmetric( horizontal: 1.5.w), decoration: BoxDecoration( color: isSpeaking ? AppColors.primary : isDarkMode ? Colors.grey.shade600 : Colors.grey.shade400, borderRadius: BorderRadius.circular(2.r), ), ), ), ); }, ), ), ), ); }), ), // 语音状态文本 Obx(() => Text( controller.isSpeechDetected.value ? 'imListening'.tr // 正在聆听... : 'canStartTalking'.tr, // 你可以开始说话 style: TextStyle( fontSize: 14.sp, color: isDarkMode ? Colors.grey[400] : Colors.grey[700], fontWeight: FontWeight.w500, ), )), ], ), ), // 关闭按钮 Container( width: 60.r, height: 60.r, decoration: BoxDecoration( color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.white, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 8, spreadRadius: 1, ), ], ), child: Center( child: IconButton( icon: Icon( Icons.close, color: isDarkMode ? Colors.white : Colors.grey.shade600, size: 28.r, ), padding: EdgeInsets.zero, onPressed: controller.stopVoiceRecognition, ), ), ), ], ), ), ), ], ), ), ); } // 构建头像 Widget _buildAvatar(bool isUser, bool isDarkMode) { return Container( width: 36.r, height: 36.r, decoration: BoxDecoration( color: isUser ? (isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[200]) : AppColors.primary, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 2, offset: const Offset(0, 1), ), ], ), child: isUser ? Center( child: Icon( Icons.person, color: isDarkMode ? Colors.white : Colors.grey[700], size: 20.r, ), ) : ClipRRect( borderRadius: BorderRadius.circular(18.r), child: controller.agent.avatarUrl.isNotEmpty ? Image.asset( controller.agent.avatarUrl, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Icon( Icons.smart_toy_rounded, color: Colors.white, size: 20.r, ); }, ) : Icon( Icons.smart_toy_rounded, color: Colors.white, size: 20.r, ), ), ); } // 构建加载动画点 Widget _buildLoadingDot(int index, bool isDarkMode) { return AnimatedBuilder( animation: const AlwaysStoppedAnimation(0), builder: (context, child) { return Container( margin: EdgeInsets.symmetric(horizontal: 2.w), width: 8.r, height: 8.r, decoration: BoxDecoration( color: isDarkMode ? Colors.white.withOpacity(0.5) : Colors.grey[400], shape: BoxShape.circle, ), ); }, ); } // 格式化时间 String _formatTime(DateTime time) { final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); //final yesterday = today.subtract(const Duration(days: 1)); final messageDate = DateTime(time.year, time.month, time.day); String timeStr = '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}'; if (messageDate == today) { return timeStr; // 只显示时间,如 "14:30" } // else if (messageDate == yesterday) { // return 'Yesterday $timeStr'; // 如 "Yesterday 14:30" // } else { // 国际通用日期格式 "MM-dd HH:mm" return '${time.month.toString().padLeft(2, '0')}-${time.day.toString().padLeft(2, '0')} $timeStr'; } } }