diff --git a/lib/modules/chat/controllers/chat_controller.dart b/lib/modules/chat/controllers/chat_controller.dart index 50b3e89ba..4ce3bf80b 100644 --- a/lib/modules/chat/controllers/chat_controller.dart +++ b/lib/modules/chat/controllers/chat_controller.dart @@ -96,6 +96,9 @@ class ChatController extends GetxController { final RxBool isHoldSpeak = false.obs; String finalResult = ''; + // 按住说话防抖 + Timer? _talkDebounceTimer; + @override Future onInit() async { super.onInit(); @@ -289,6 +292,7 @@ class ChatController extends GetxController { _voiceInteractionSubscription?.cancel(); _autoStopTimer?.cancel(); _scrollDebounceTimer?.cancel(); // 清理防抖计时器 + _talkDebounceTimer?.cancel(); _amapFlutter.dispose(); _locationStream?.cancel(); @@ -578,12 +582,15 @@ class ChatController extends GetxController { // 按下开始说话 void talkLongPress() async { + _talkDebounceTimer?.cancel(); if (isHoldSpeak.value) return; _voiceInteractionService.pauseVoiceInteraction(); try { final recognitionStream = await _asrService.startContinuousRecognition(); + await Future.delayed(const Duration(milliseconds: 150)); + isHoldSpeak.value = true; recognizedText.value = ''; finalResult = ''; @@ -601,26 +608,29 @@ class ChatController extends GetxController { // 松开停止说话 void talkLongPressUp() async { - if (!isHoldSpeak.value) return; - try { - await _asrService.stopContinuousRecognition(); - _recognitionSubscription?.cancel(); - _recognitionSubscription = null; + _talkDebounceTimer?.cancel(); + _talkDebounceTimer = Timer(const Duration(milliseconds: 200), () async { + if (!isHoldSpeak.value) return; + try { + await _asrService.stopContinuousRecognition(); + _recognitionSubscription?.cancel(); + _recognitionSubscription = null; - final intermediateIndex = - messages.indexWhere((m) => m.isIntermediate && m.role == 'user'); - if (intermediateIndex >= 0) { - messages.removeAt(intermediateIndex); - } + final intermediateIndex = + messages.indexWhere((m) => m.isIntermediate && m.role == 'user'); + if (intermediateIndex >= 0) { + messages.removeAt(intermediateIndex); + } - sendMessage(finalResult); + sendMessage(finalResult); - recognizedText.value = ''; - finalResult = ''; - isHoldSpeak.value = false; - } catch (e) { - Logger.error('停止语音识别失败', e); - } + recognizedText.value = ''; + finalResult = ''; + isHoldSpeak.value = false; + } catch (e) { + Logger.error('停止语音识别失败', e); + } + }); } // 开始语音识别 diff --git a/lib/modules/chat/views/animated_gradient_background.dart b/lib/modules/chat/views/animated_gradient_background.dart new file mode 100644 index 000000000..181295cea --- /dev/null +++ b/lib/modules/chat/views/animated_gradient_background.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; + +class AnimatedGradientBackground extends StatefulWidget { + const AnimatedGradientBackground({super.key}); + + @override + State createState() => + _AnimatedGradientBackgroundState(); +} + +class _AnimatedGradientBackgroundState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _beginAnimation; + late Animation _endAnimation; + + final Color color1 = const Color(0xFFfbcfdd); + final Color color2 = const Color(0xFFe2e9fb); + + @override + void initState() { + super.initState(); + + _controller = AnimationController( + vsync: this, + duration: const Duration(seconds: 10), + )..repeat(reverse: true); + + // 创建起始点动画 + _beginAnimation = AlignmentTween( + begin: Alignment.topCenter, + end: Alignment.bottomLeft, + ).animate(_controller); + + // 创建结束点动画 + _endAnimation = AlignmentTween( + begin: Alignment.bottomCenter, + end: Alignment.topRight, + ).animate(_controller); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [color1, color2], + begin: _beginAnimation.value!, + end: _endAnimation.value!, + ), + ), + ); + }, + ); + } +} diff --git a/lib/modules/chat/views/chat_view.dart b/lib/modules/chat/views/chat_view.dart index 39020c0b0..ed904d4e6 100644 --- a/lib/modules/chat/views/chat_view.dart +++ b/lib/modules/chat/views/chat_view.dart @@ -5,6 +5,7 @@ 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'; @@ -16,10 +17,17 @@ class ChatView extends GetView { Widget build(BuildContext context) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; - return Scaffold( - backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.grey[50], - appBar: _buildAppBar(isDarkMode), - body: _buildBody(isDarkMode), + 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), + ), ); } @@ -47,7 +55,7 @@ class ChatView extends GetView { BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 3, - offset: Offset(0, 1), + offset: const Offset(0, 1), ), ], ), @@ -92,7 +100,7 @@ class ChatView extends GetView { Container( width: 6.r, height: 6.r, - decoration: BoxDecoration( + decoration: const BoxDecoration( color: Colors.green, shape: BoxShape.circle, ), @@ -164,7 +172,7 @@ class ChatView extends GetView { value: 'clear', child: Row( children: [ - Icon(Icons.delete_outline, color: Colors.red), + const Icon(Icons.delete_outline, color: Colors.red), SizedBox(width: 8.w), Text( 'clearChatHistory'.tr, // 清空聊天记录 @@ -205,7 +213,7 @@ class ChatView extends GetView { ), // 底部输入区域 - _buildInputArea(isDarkMode), + _buildTextInputPanel(isDarkMode), ], ); } @@ -377,24 +385,11 @@ class ChatView extends GetView { return box; } - // 构建输入区域 - Widget _buildInputArea(bool isDarkMode) { + // 构建文本输入面板 + Widget _buildTextInputPanel(bool isDarkMode) { final textController = TextEditingController(text: controller.inputText.value); - // 检查是否处于语音识别模式 - return Obx(() { - if (controller.isVoiceRecognitionActive.value) { - return _buildVoiceInputPanel(isDarkMode); - } else { - return _buildTextInputPanel(isDarkMode, textController); - } - }); - } - - // 构建文本输入面板 - Widget _buildTextInputPanel( - bool isDarkMode, TextEditingController textController) { return Container( height: 48.w, margin: EdgeInsets.only(left: 16.w, right: 16.w, top: 10.w, bottom: 20.w), @@ -521,158 +516,166 @@ class ChatView extends GetView { return Container( decoration: BoxDecoration( color: isDarkMode ? AppColors.cardBackground : Colors.white, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 3, - offset: Offset(0, -1), - ), - ], ), child: SafeArea( - child: 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: Column( + children: [ + Row( + children: [], + ), + Expanded( + child: AnimatedGradientBackground(), + ), + 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, - )), - ), - ), + 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; + // 中间音频波形和状态文本 + 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: 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), + 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), + ), + ), ), - ), - ), - ); - }, + ); + }, + ), + ), ), - ), - ), - ); - }), - ), - SizedBox(height: 8.h), + ); + }), + ), - // 语音状态文本 - 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, - ), - )), - ], - ), - ), + // 语音状态文本 + 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, + // 关闭按钮 + 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, + child: Center( + child: IconButton( + icon: Icon( + Icons.close, + color: isDarkMode + ? Colors.white + : Colors.grey.shade600, + size: 28.r, + ), + padding: EdgeInsets.zero, + onPressed: controller.stopVoiceRecognition, + ), ), - padding: EdgeInsets.zero, - onPressed: controller.stopVoiceRecognition, ), - ), + ], ), - ], + ), ), - ), + ], ), ), ); @@ -692,7 +695,7 @@ class ChatView extends GetView { BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 2, - offset: Offset(0, 1), + offset: const Offset(0, 1), ), ], ), @@ -730,7 +733,7 @@ class ChatView extends GetView { // 构建加载动画点 Widget _buildLoadingDot(int index, bool isDarkMode) { return AnimatedBuilder( - animation: AlwaysStoppedAnimation(0), + animation: const AlwaysStoppedAnimation(0), builder: (context, child) { return Container( margin: EdgeInsets.symmetric(horizontal: 2.w),