import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../controllers/chat_controller.dart'; import '../models/message_model.dart'; import '../../../data/services/volcano_tts_api_service.dart'; import '../widgets/voice_input_panel.dart'; class ChatView extends GetView { const ChatView({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return WillPopScope( onWillPop: () async { // 停止TTS服务 final tts = Get.find(); await tts.endSession(); // 停止语音输入 if (controller.isVoiceInputVisible.value) { controller.stopVoiceInput(); } return true; }, child: Scaffold( backgroundColor: Colors.white, appBar: _buildAppBar(), body: SafeArea( child: Stack( children: [ // 代理头像作为背景 if (controller.agentAvatar.isNotEmpty) Positioned.fill( child: Opacity( opacity: 0.1, // 设置透明度,使其不影响内容阅读 child: Image.asset( controller.agentAvatar, fit: BoxFit.contain, ), ), ), // 主要内容 Column( children: [ Expanded( child: _buildMessageList(), ), _buildInputSection(), ], ), ], ), ), ), ); } PreferredSizeWidget _buildAppBar() { return AppBar( backgroundColor: Colors.white, elevation: 0, leading: IconButton( icon: Icon( Icons.arrow_back_ios, color: Colors.black, size: 20.sp, ), onPressed: () async { // 停止TTS服务 final tts = Get.find(); await tts.endSession(); // 停止语音输入 if (controller.isVoiceInputVisible.value) { controller.stopVoiceInput(); } Get.back(); }, ), title: Column( children: [ Text( controller.agentName, style: TextStyle( fontSize: 17.sp, fontWeight: FontWeight.w600, color: Colors.black, ), ), if (controller.agentSubtitle != null) Text( controller.agentSubtitle!, style: TextStyle( fontSize: 12.sp, color: Colors.grey[600], ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), centerTitle: true, actions: [ IconButton( icon: Obx(() => Icon( controller.isTtsEnabled.value ? Icons.volume_up : Icons.volume_off, color: Colors.black, size: 24.sp, )), onPressed: controller.toggleTTS, ), IconButton( icon: Icon( Icons.more_horiz, color: Colors.black, size: 24.sp, ), onPressed: () => _showMoreOptions(), ), ], ); } Widget _buildMessageList() { return Stack( children: [ Obx(() => ListView.builder( controller: controller.scrollController, padding: EdgeInsets.only( top: 16.h, bottom: controller.isVoiceInputVisible.value ? 160.h : (controller.isInputCollapsed.value ? 70.h : 90.h), ), itemCount: controller.messages.length, itemBuilder: (context, index) => _buildMessage(controller.messages[index]), physics: const AlwaysScrollableScrollPhysics(), clipBehavior: Clip.none, keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, )), Obx(() { if (!controller.isLoading.value) return const SizedBox.shrink(); return Positioned( bottom: controller.isVoiceInputVisible.value ? 170.h : (controller.isInputCollapsed.value ? 80.h : 100.h), left: 0, right: 0, child: Container( padding: EdgeInsets.symmetric(vertical: 8.h), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: 16.w, height: 16.w, child: const CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Color(0xFF4A6FE5)), ), ), ], ), ), ); }), ], ); } Widget _buildInputSection() { return Obx(() { if (controller.isVoiceInputVisible.value) { return Column( mainAxisSize: MainAxisSize.min, children: [ VoiceInputPanel( onRecordingResult: controller.handleVoiceResult, onClose: controller.stopVoiceInput, onRecognizing: controller.handleRecognizing, ), ], ); } return _buildTextInput(); }); } Widget _buildTextInput() { return Container( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), decoration: BoxDecoration( color: Colors.white, border: Border( top: BorderSide( color: Colors.grey[200]!, width: 1, ), ), ), child: Obx(() { if (controller.isRecordingVoice.value) { return _buildRecordingInput(); } else if (!controller.isVoiceMode.value) { return _buildExpandedInput(); } else { return _buildDefaultInput(); } }), ); } Widget _buildDefaultInput() { return Row( children: [ Container( width: 40.w, height: 40.w, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.grey[100], border: Border.all( color: Colors.grey[300]!, width: 0.5, ), ), child: IconButton( icon: Icon( Icons.camera_alt, color: Colors.grey[600], size: 20.sp, ), onPressed: () { // 拍照功能 }, padding: EdgeInsets.zero, ), ), SizedBox(width: 8.w), Expanded( child: GestureDetector( onTap: () { // 点击打开VoiceInputPanel controller.startVoiceInput(); }, child: Container( height: 48.h, decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(24.r), border: Border.all( color: Colors.grey[300]!, width: 0.5, ), ), child: Center( child: Text( '点击开始连续语音对话', style: TextStyle( color: Colors.grey[600], fontSize: 16.sp, ), ), ), ), ), ), SizedBox(width: 8.w), Container( width: 40.w, height: 40.w, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.grey[100], border: Border.all( color: Colors.grey[300]!, width: 0.5, ), ), child: IconButton( icon: Icon( Icons.keyboard, color: Colors.grey[600], size: 20.sp, ), onPressed: controller.toggleInputMode, padding: EdgeInsets.zero, ), ), ], ); } Widget _buildExpandedInput() { return Row( children: [ Container( width: 40.w, height: 40.w, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.grey[100], border: Border.all( color: Colors.grey[300]!, width: 0.5, ), ), child: IconButton( icon: Icon( Icons.mic, color: const Color(0xFF4A6FE5), size: 20.sp, ), onPressed: controller.toggleInputMode, padding: EdgeInsets.zero, ), ), SizedBox(width: 8.w), Expanded( child: Container( padding: EdgeInsets.symmetric(horizontal: 12.w), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(24.r), border: Border.all( color: Colors.grey[300]!, width: 0.5, ), ), child: TextField( controller: controller.messageController, decoration: InputDecoration( hintText: '输入消息...', hintStyle: TextStyle( color: Colors.grey[400], fontSize: 14.sp, ), border: InputBorder.none, contentPadding: EdgeInsets.symmetric(vertical: 12.h), ), maxLines: 5, minLines: 1, style: TextStyle( fontSize: 14.sp, color: Colors.black87, ), textInputAction: TextInputAction.send, onSubmitted: (_) => controller.sendMessage(), ), ), ), SizedBox(width: 8.w), Container( width: 40.w, height: 40.w, decoration: const BoxDecoration( shape: BoxShape.circle, color: Color(0xFF4A6FE5), ), child: IconButton( icon: Icon( Icons.send, color: Colors.white, size: 20.sp, ), onPressed: controller.sendMessage, padding: EdgeInsets.zero, ), ), ], ); } Widget _buildRecordingInput() { return Container( height: 48.h, decoration: BoxDecoration( color: const Color(0xFF4A6FE5), borderRadius: BorderRadius.circular(24.r), ), child: Center( child: Row( mainAxisSize: MainAxisSize.min, children: List.generate( 40, (index) => Container( width: 4.w, height: 4.h, margin: EdgeInsets.symmetric(horizontal: 1.w), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(2.r), ), ), ), ), ), ); } void _showMoreOptions() { Get.bottomSheet( Container( padding: EdgeInsets.all(20.w), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.only( topLeft: Radius.circular(20.r), topRight: Radius.circular(20.r), ), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: const Icon(Icons.delete_outline, color: Colors.red), title: const Text('清空对话'), onTap: () { Get.back(); controller.clearChat(); }, ), ], ), ), ); } Widget _buildMessage(Message message) { final isUser = message.role == 'user'; final isSystem = message.role == 'system'; final isRecognizing = isUser && message.content.startsWith('🎤 '); if (isSystem) { // 系统消息使用特殊样式 return Padding( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), child: Container( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(12.r), border: Border.all( color: Colors.grey[300]!, width: 0.5, ), ), child: Row( children: [ Icon( Icons.info_outline, color: Colors.grey[600], size: 20.sp, ), SizedBox(width: 8.w), Expanded( child: Text( message.content, style: TextStyle( fontSize: 14.sp, color: Colors.grey[700], height: 1.5, ), ), ), ], ), ), ); } return Padding( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), child: Row( mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!isUser) ...[ CircleAvatar( radius: 20.r, backgroundImage: AssetImage(controller.agentAvatar), backgroundColor: Colors.transparent, ), SizedBox(width: 12.w), ], Flexible( child: Container( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), decoration: BoxDecoration( color: isUser ? (isRecognizing ? Colors.purple.withOpacity(0.7) : const Color(0xFF4A6FE5)) : const Color(0xFFF5F5F5), borderRadius: BorderRadius.only( topLeft: Radius.circular(isUser ? 18.r : 4.r), topRight: Radius.circular(isUser ? 4.r : 18.r), bottomLeft: Radius.circular(18.r), bottomRight: Radius.circular(18.r), ), // 为正在识别的消息添加脉动效果 boxShadow: isRecognizing ? [ BoxShadow( color: Colors.purple.withOpacity(0.3), blurRadius: 8, spreadRadius: 2, ), ] : [], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (isRecognizing) ...[ Icon( Icons.mic, color: Colors.white, size: 16.sp, ), SizedBox(width: 8.w), ], Flexible( child: Text( // 如果是正在识别的消息,移除麦克风图标前缀 isRecognizing ? message.content.substring(2) : message.content, style: TextStyle( fontSize: 16.sp, color: isUser ? Colors.white : Colors.black87, height: 1.5, ), ), ), if (isRecognizing) ...[ SizedBox(width: 8.w), _buildPulsatingDot(), ], ], ), ), ), if (isUser) ...[ SizedBox(width: 12.w), CircleAvatar( radius: 20.r, backgroundColor: const Color(0xFFEEF4FF), child: Icon( Icons.person, color: const Color(0xFF4A6FE5), size: 24.sp, ), ), ], ], ), ); } // 创建脉动点动画 Widget _buildPulsatingDot() { return Obx(() { // 使用controller.isRecognizing来控制动画是否显示 if (!controller.isRecognizing.value) return const SizedBox.shrink(); return _PulsatingDot(); }); } } // 脉动点动画组件 class _PulsatingDot extends StatefulWidget { @override _PulsatingDotState createState() => _PulsatingDotState(); } class _PulsatingDotState extends State<_PulsatingDot> with SingleTickerProviderStateMixin { late AnimationController _animationController; late Animation _animation; @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 1000), )..repeat(reverse: true); _animation = Tween(begin: 0.5, end: 1.0).animate(_animationController); } @override void dispose() { _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return SizedBox( width: 16.w, height: 16.w, child: AnimatedBuilder( animation: _animation, builder: (context, child) { return Stack( alignment: Alignment.center, children: [ Opacity( opacity: 1.0 - _animation.value, child: Container( width: 16.w * _animation.value, height: 16.w * _animation.value, decoration: BoxDecoration( color: Colors.white.withOpacity(0.5), shape: BoxShape.circle, ), ), ), Container( width: 6.w, height: 6.w, decoration: const BoxDecoration( color: Colors.white, shape: BoxShape.circle, ), ), ], ); }, ), ); } }