import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'dart:math'; import 'package:audio_waveforms/audio_waveforms.dart'; import '../controllers/chat_controller.dart'; import '../models/message_model.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/widgets/message_bubble.dart'; class ChatView extends GetView { const ChatView({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.grey[50], // 使用浅灰背景,类似HTML模板 appBar: _buildAppBar(), body: _buildBody(), ); } // 构建AppBar PreferredSizeWidget _buildAppBar() { return AppBar( elevation: 1, backgroundColor: Colors.white, leading: IconButton( icon: Icon(Icons.arrow_back_ios, color: 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: 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, style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.bold, color: Colors.black87, ), ), Row( children: [ Container( width: 6.r, height: 6.r, decoration: BoxDecoration( color: Colors.green, shape: BoxShape.circle, ), ), SizedBox(width: 4.w), Text( '在线', style: TextStyle( fontSize: 12.sp, color: Colors.green, ), ), ], ), ], ), ), ], ), actions: [ // TTS开关 Obx(() => IconButton( icon: Icon( controller.isTtsEnabled.value ? Icons.volume_up : Icons.volume_off, color: controller.isTtsEnabled.value ? AppColors.primary : Colors.grey, ), onPressed: controller.toggleTts, )), // 更多选项 Container( width: 34.r, height: 34.r, margin: EdgeInsets.only(right: 8.w), decoration: BoxDecoration( color: Colors.grey[100], shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 2, spreadRadius: 0.5, ), ], ), child: PopupMenuButton( icon: Icon(Icons.more_vert, color: Colors.black87, size: 20.r), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12.r), ), itemBuilder: (context) => [ PopupMenuItem( value: 'clear', child: Row( children: [ Icon(Icons.delete_outline, color: Colors.red), SizedBox(width: 8.w), Text('清空对话'), ], ), ), ], onSelected: (value) { if (value == 'clear') { controller.clearChat(); } }, ), ), ], ); } // 构建主体内容 Widget _buildBody() { return Column( children: [ // 消息列表 Expanded( child: Container( color: Colors.grey[50], // 使用浅灰背景 child: Obx(() => controller.messages.isEmpty ? _buildEmptyChat() : _buildChatList()), ), ), // 输入区域 - 根据当前模式显示不同的输入面板 Obx(() => controller.isVoiceRecognitionActive.value ? _buildVoiceInputPanel() : _buildTextInputPanel()), ], ); } // 构建空聊天界面 Widget _buildEmptyChat() { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 80.r, height: 80.r, decoration: BoxDecoration( color: AppColors.primary.withOpacity(0.1), shape: BoxShape.circle, ), child: ClipRRect( borderRadius: BorderRadius.circular(40.r), child: controller.agent.avatarUrl.isNotEmpty ? Image.asset( controller.agent.avatarUrl, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Icon( Icons.smart_toy_rounded, size: 40.r, color: AppColors.primary, ); }, ) : Icon( Icons.smart_toy_rounded, size: 40.r, color: AppColors.primary, ), ), ), SizedBox(height: 24.h), Text( '你好!我是${controller.agent.name}助手', style: TextStyle( fontSize: 18.sp, fontWeight: FontWeight.bold, color: Colors.black87, ), ), SizedBox(height: 12.h), Padding( padding: EdgeInsets.symmetric(horizontal: 40.w), child: Text( '我能帮你回答问题、实时语音翻译、推荐音乐和播客等。有什么我可以帮你的吗?', textAlign: TextAlign.center, style: TextStyle( fontSize: 14.sp, color: Colors.grey[600], height: 1.5, ), ), ), ], ), ); } // 构建聊天列表 Widget _buildChatList() { 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); }, ); } // 构建消息项 Widget _buildMessageItem(Message message) { final bool isUser = message.role == 'user'; return Padding( padding: EdgeInsets.only(bottom: 16.h), child: Row( mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ // AI头像(非用户消息时显示) if (!isUser) Container( width: 36.r, height: 36.r, margin: EdgeInsets.only(right: 8.w, top: 2.h), decoration: BoxDecoration( color: AppColors.primary, borderRadius: BorderRadius.circular(18.r), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 2, offset: Offset(0, 1), ), ], ), child: 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, ), ), ), // 消息气泡 Flexible( child: Container( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), decoration: BoxDecoration( color: isUser ? AppColors.primary : Colors.white, borderRadius: BorderRadius.only( topLeft: Radius.circular(isUser ? 16.r : 0), topRight: Radius.circular(isUser ? 0 : 16.r), bottomLeft: Radius.circular(16.r), bottomRight: Radius.circular(16.r), ), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 3, spreadRadius: 0.5, offset: Offset(0, 1), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 消息内容 if (message.content.isNotEmpty) SelectableText( message.content, style: TextStyle( color: isUser ? Colors.white : Colors.black87, fontSize: 15.sp, height: 1.5, letterSpacing: 0.2, ), textAlign: TextAlign.left, maxLines: null, enableInteractiveSelection: true, textWidthBasis: TextWidthBasis.longestLine, ), // 加载指示器 if (message.isLoading) Padding( padding: EdgeInsets.only(top: message.content.isNotEmpty ? 8.h : 0), child: Row( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: 12.w, height: 12.h, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( isUser ? Colors.white : AppColors.primary, ), ), ), SizedBox(width: 8.w), Text( '思考中...', style: TextStyle( color: isUser ? Colors.white : Colors.black54, fontSize: 12.sp, ), ), ], ), ), ], ), ), ), // 用户头像(用户消息时显示) if (isUser) Container( width: 36.r, height: 36.r, margin: EdgeInsets.only(left: 8.w, top: 2.h), decoration: BoxDecoration( color: Colors.grey[200], borderRadius: BorderRadius.circular(18.r), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 2, offset: Offset(0, 1), ), ], ), child: Center( child: Icon( Icons.person, color: Colors.grey[600], size: 20.r, ), ), ), ], ), ); } // 构建文本输入面板 Widget _buildTextInputPanel() { final textController = TextEditingController(text: controller.inputText.value); textController.addListener(() { controller.inputText.value = textController.text; }); return Container( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h), decoration: BoxDecoration( color: Colors.white, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.06), blurRadius: 10, offset: Offset(0, -1), ), ], ), child: SafeArea( child: Row( children: [ // 语音输入按钮 Container( width: 40.r, height: 40.r, decoration: BoxDecoration( color: Colors.grey[100], shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 3, spreadRadius: 0.5, ), ], ), child: IconButton( icon: Icon( Icons.mic_none, color: Colors.grey[600], size: 20.r, ), padding: EdgeInsets.zero, onPressed: controller.startVoiceRecognition, ), ), // 文本输入框 Expanded( child: Container( margin: EdgeInsets.symmetric(horizontal: 8.w), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(20.r), ), child: TextField( controller: textController, decoration: InputDecoration( hintText: '输入消息...', hintStyle: TextStyle(color: Colors.grey[500]), border: InputBorder.none, contentPadding: EdgeInsets.symmetric( horizontal: 16.w, vertical: 10.h, ), ), maxLines: null, textInputAction: TextInputAction.send, style: TextStyle( fontSize: 15.sp, color: Colors.black87, ), onSubmitted: (value) { if (value.trim().isNotEmpty) { controller.sendMessage(value); textController.clear(); } }, ), ), ), // 发送按钮 Obx(() => Container( width: 40.r, height: 40.r, decoration: BoxDecoration( color: controller.inputText.value.trim().isEmpty ? Colors.grey[100] : AppColors.primary, shape: BoxShape.circle, boxShadow: controller.inputText.value.trim().isEmpty ? [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 3, spreadRadius: 0.5, ), ] : [ BoxShadow( color: AppColors.primary.withOpacity(0.3), blurRadius: 5, spreadRadius: 0.5, ), ], ), child: IconButton( icon: Icon( Icons.send, size: 18.r, color: controller.inputText.value.trim().isEmpty ? Colors.grey[500] : Colors.white, ), padding: EdgeInsets.zero, onPressed: controller.inputText.value.trim().isEmpty ? null : () { controller.sendMessage(controller.inputText.value); textController.clear(); }, ), )), ], ), ), ); } // 构建语音输入面板 Widget _buildVoiceInputPanel() { return Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.white, Color(0xFFE8D5F5), ], stops: [0.3, 1.0], ), // boxShadow: [ // BoxShadow( // color: Colors.black.withOpacity(0.06), // blurRadius: 10, // offset: Offset(0, -1), // ), // ], ), child: SafeArea( child: SizedBox( height: 80.h, child: Padding( padding: EdgeInsets.symmetric(horizontal: 30.w), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ // 语音录制按钮 Container( width: 60.r, height: 60.r, decoration: BoxDecoration( color: 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: Duration(milliseconds: 500), width: 2.5.w, height: height, margin: EdgeInsets.symmetric(horizontal: 1.5.w), decoration: BoxDecoration( color: isSpeaking ? AppColors.primary : Colors.grey.shade400, borderRadius: BorderRadius.circular(2.r), ), ), ), ); }, ), ), ), ); }), ), SizedBox(height: 8.h), // 语音状态文本 Obx(() => Text( controller.isSpeechDetected.value ? '正在聆听...' : '你可以开始说话', style: TextStyle( fontSize: 14.sp, color: Colors.grey.shade700, fontWeight: FontWeight.w500, ), )), ], ), ), // 关闭按钮 Container( width: 60.r, height: 60.r, decoration: BoxDecoration( color: 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: Colors.grey.shade600, size: 28.r, ), padding: EdgeInsets.zero, onPressed: controller.stopVoiceRecognition, ), ), ), ], ), ), ), ), ); } }