|
|
|
@ -13,20 +13,26 @@ class ChatView extends GetView<ChatController> { |
|
|
|
|
|
|
|
@override |
|
|
|
Widget build(BuildContext context) { |
|
|
|
final isDarkMode = Theme.of(context).brightness == Brightness.dark; |
|
|
|
|
|
|
|
return Scaffold( |
|
|
|
backgroundColor: Colors.grey[50], // 使用浅灰背景,类似HTML模板 |
|
|
|
appBar: _buildAppBar(), |
|
|
|
body: _buildBody(), |
|
|
|
backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.grey[50], |
|
|
|
appBar: _buildAppBar(isDarkMode), |
|
|
|
body: _buildBody(isDarkMode), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
// 构建AppBar |
|
|
|
PreferredSizeWidget _buildAppBar() { |
|
|
|
PreferredSizeWidget _buildAppBar(bool isDarkMode) { |
|
|
|
return AppBar( |
|
|
|
elevation: 1, |
|
|
|
backgroundColor: Colors.white, |
|
|
|
backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white, |
|
|
|
leading: IconButton( |
|
|
|
icon: Icon(Icons.arrow_back_ios, color: Colors.black87, size: 20.r), |
|
|
|
icon: Icon( |
|
|
|
Icons.arrow_back_ios, |
|
|
|
color: isDarkMode ? Colors.white : Colors.black87, |
|
|
|
size: 20.r |
|
|
|
), |
|
|
|
onPressed: () => Get.back(), |
|
|
|
), |
|
|
|
titleSpacing: 0, |
|
|
|
@ -80,7 +86,7 @@ class ChatView extends GetView<ChatController> { |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 16.sp, |
|
|
|
fontWeight: FontWeight.bold, |
|
|
|
color: Colors.black87, |
|
|
|
color: isDarkMode ? Colors.white : Colors.black87, |
|
|
|
), |
|
|
|
), |
|
|
|
Row( |
|
|
|
@ -117,7 +123,7 @@ class ChatView extends GetView<ChatController> { |
|
|
|
: Icons.volume_off, |
|
|
|
color: controller.isTtsEnabled.value |
|
|
|
? AppColors.primary |
|
|
|
: Colors.grey, |
|
|
|
: isDarkMode ? Colors.grey[400] : Colors.grey, |
|
|
|
), |
|
|
|
onPressed: controller.toggleTts, |
|
|
|
)), |
|
|
|
@ -127,21 +133,27 @@ class ChatView extends GetView<ChatController> { |
|
|
|
height: 34.r, |
|
|
|
margin: EdgeInsets.only(right: 8.w), |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: Colors.grey[100], |
|
|
|
color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], |
|
|
|
shape: BoxShape.circle, |
|
|
|
boxShadow: [ |
|
|
|
BoxShadow( |
|
|
|
color: Colors.black.withOpacity(0.05), |
|
|
|
blurRadius: 2, |
|
|
|
spreadRadius: 0.5, |
|
|
|
), |
|
|
|
if (!isDarkMode) |
|
|
|
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), |
|
|
|
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', |
|
|
|
@ -149,16 +161,23 @@ class ChatView extends GetView<ChatController> { |
|
|
|
children: [ |
|
|
|
Icon(Icons.delete_outline, color: Colors.red), |
|
|
|
SizedBox(width: 8.w), |
|
|
|
Text('清空对话'), |
|
|
|
Text( |
|
|
|
'清空聊天记录', |
|
|
|
style: TextStyle( |
|
|
|
color: isDarkMode ? Colors.white : Colors.black87, |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
onTap: () { |
|
|
|
// 延迟执行,避免在弹出菜单关闭前执行 |
|
|
|
Future.delayed( |
|
|
|
const Duration(milliseconds: 100), |
|
|
|
() => controller.clearChat(), |
|
|
|
); |
|
|
|
}, |
|
|
|
), |
|
|
|
], |
|
|
|
onSelected: (value) { |
|
|
|
if (value == 'clear') { |
|
|
|
controller.clearChat(); |
|
|
|
} |
|
|
|
}, |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
@ -166,59 +185,43 @@ class ChatView extends GetView<ChatController> { |
|
|
|
} |
|
|
|
|
|
|
|
// 构建主体内容 |
|
|
|
Widget _buildBody() { |
|
|
|
Widget _buildBody(bool isDarkMode) { |
|
|
|
return Column( |
|
|
|
children: [ |
|
|
|
// 消息列表 |
|
|
|
// 聊天内容区域 |
|
|
|
Expanded( |
|
|
|
child: Container( |
|
|
|
color: Colors.grey[50], // 使用浅灰背景 |
|
|
|
child: Obx(() => controller.messages.isEmpty |
|
|
|
? _buildEmptyChat() |
|
|
|
: _buildChatList()), |
|
|
|
), |
|
|
|
child: Obx(() { |
|
|
|
if (controller.messages.isEmpty) { |
|
|
|
return _buildEmptyState(isDarkMode); |
|
|
|
} else { |
|
|
|
return _buildChatList(isDarkMode); |
|
|
|
} |
|
|
|
}), |
|
|
|
), |
|
|
|
|
|
|
|
// 输入区域 - 根据当前模式显示不同的输入面板 |
|
|
|
Obx(() => controller.isVoiceRecognitionActive.value |
|
|
|
? _buildVoiceInputPanel() |
|
|
|
: _buildTextInputPanel()), |
|
|
|
// 底部输入区域 |
|
|
|
_buildInputArea(isDarkMode), |
|
|
|
], |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
// 构建空聊天界面 |
|
|
|
Widget _buildEmptyChat() { |
|
|
|
// 构建空状态 |
|
|
|
Widget _buildEmptyState(bool isDarkMode) { |
|
|
|
return Center( |
|
|
|
child: Column( |
|
|
|
mainAxisSize: MainAxisSize.min, |
|
|
|
mainAxisAlignment: MainAxisAlignment.center, |
|
|
|
children: [ |
|
|
|
Container( |
|
|
|
width: 80.r, |
|
|
|
height: 80.r, |
|
|
|
width: 80.w, |
|
|
|
height: 80.w, |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: AppColors.primary.withOpacity(0.1), |
|
|
|
color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], |
|
|
|
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, |
|
|
|
), |
|
|
|
child: Icon( |
|
|
|
Icons.chat_bubble_outline, |
|
|
|
size: 40.w, |
|
|
|
color: isDarkMode ? Colors.white.withOpacity(0.5) : Colors.grey[400], |
|
|
|
), |
|
|
|
), |
|
|
|
SizedBox(height: 24.h), |
|
|
|
@ -227,7 +230,7 @@ class ChatView extends GetView<ChatController> { |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 18.sp, |
|
|
|
fontWeight: FontWeight.bold, |
|
|
|
color: Colors.black87, |
|
|
|
color: isDarkMode ? Colors.white : Colors.black87, |
|
|
|
), |
|
|
|
), |
|
|
|
SizedBox(height: 12.h), |
|
|
|
@ -238,7 +241,7 @@ class ChatView extends GetView<ChatController> { |
|
|
|
textAlign: TextAlign.center, |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 14.sp, |
|
|
|
color: Colors.grey[600], |
|
|
|
color: isDarkMode ? Colors.grey[400] : Colors.grey[600], |
|
|
|
height: 1.5, |
|
|
|
), |
|
|
|
), |
|
|
|
@ -249,7 +252,7 @@ class ChatView extends GetView<ChatController> { |
|
|
|
} |
|
|
|
|
|
|
|
// 构建聊天列表 |
|
|
|
Widget _buildChatList() { |
|
|
|
Widget _buildChatList(bool isDarkMode) { |
|
|
|
return ListView.builder( |
|
|
|
controller: controller.scrollController, |
|
|
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h), |
|
|
|
@ -259,14 +262,14 @@ class ChatView extends GetView<ChatController> { |
|
|
|
cacheExtent: 1000, // 增加缓存范围,减少重建 |
|
|
|
itemBuilder: (context, index) { |
|
|
|
final message = controller.messages[index]; |
|
|
|
return _buildMessageItem(message); |
|
|
|
return _buildMessageItem(message, isDarkMode); |
|
|
|
}, |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
// 构建消息项 |
|
|
|
Widget _buildMessageItem(Message message) { |
|
|
|
final bool isUser = message.role == 'user'; |
|
|
|
Widget _buildMessageItem(Message message, bool isDarkMode) { |
|
|
|
final isUser = message.role == 'user'; |
|
|
|
|
|
|
|
return Padding( |
|
|
|
padding: EdgeInsets.only(bottom: 16.h), |
|
|
|
@ -274,164 +277,102 @@ class ChatView extends GetView<ChatController> { |
|
|
|
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, |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
|
|
|
|
// 消息气泡 |
|
|
|
if (!isUser) _buildAvatar(false, isDarkMode), |
|
|
|
|
|
|
|
SizedBox(width: 8.w), |
|
|
|
|
|
|
|
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: 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: [ |
|
|
|
// 消息内容 |
|
|
|
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<Color>( |
|
|
|
isUser ? Colors.white : AppColors.primary, |
|
|
|
), |
|
|
|
), |
|
|
|
child: Column( |
|
|
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
|
|
children: [ |
|
|
|
// 消息内容 |
|
|
|
if (message.content.isNotEmpty) |
|
|
|
SelectableText( |
|
|
|
message.content, |
|
|
|
style: TextStyle( |
|
|
|
color: isUser |
|
|
|
? Colors.white |
|
|
|
: isDarkMode ? Colors.white : Colors.black, |
|
|
|
fontSize: 16.sp, |
|
|
|
height: 1.5, |
|
|
|
), |
|
|
|
SizedBox(width: 8.w), |
|
|
|
Text( |
|
|
|
'思考中...', |
|
|
|
style: TextStyle( |
|
|
|
color: isUser ? Colors.white : Colors.black54, |
|
|
|
fontSize: 12.sp, |
|
|
|
), |
|
|
|
|
|
|
|
// 加载中动画 |
|
|
|
if (message.isLoading) |
|
|
|
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], |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
|
|
|
|
// 用户头像(用户消息时显示) |
|
|
|
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, |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
|
|
|
|
SizedBox(width: 8.w), |
|
|
|
|
|
|
|
if (isUser) _buildAvatar(true, isDarkMode), |
|
|
|
], |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
// 构建文本输入面板 |
|
|
|
Widget _buildTextInputPanel() { |
|
|
|
// 构建输入区域 |
|
|
|
Widget _buildInputArea(bool isDarkMode) { |
|
|
|
final textController = TextEditingController(text: controller.inputText.value); |
|
|
|
|
|
|
|
textController.addListener(() { |
|
|
|
controller.inputText.value = textController.text; |
|
|
|
// 检查是否处于语音识别模式 |
|
|
|
return Obx(() { |
|
|
|
if (controller.isVoiceRecognitionActive.value) { |
|
|
|
return _buildVoiceInputPanel(isDarkMode); |
|
|
|
} else { |
|
|
|
return _buildTextInputPanel(isDarkMode, textController); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
// 构建文本输入面板 |
|
|
|
Widget _buildTextInputPanel(bool isDarkMode, TextEditingController textController) { |
|
|
|
return Container( |
|
|
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h), |
|
|
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: Colors.white, |
|
|
|
color: isDarkMode ? AppColors.cardBackground : Colors.white, |
|
|
|
boxShadow: [ |
|
|
|
BoxShadow( |
|
|
|
color: Colors.black.withOpacity(0.06), |
|
|
|
blurRadius: 10, |
|
|
|
color: Colors.black.withOpacity(0.05), |
|
|
|
blurRadius: 3, |
|
|
|
offset: Offset(0, -1), |
|
|
|
), |
|
|
|
], |
|
|
|
@ -439,25 +380,18 @@ class ChatView extends GetView<ChatController> { |
|
|
|
child: SafeArea( |
|
|
|
child: Row( |
|
|
|
children: [ |
|
|
|
// 语音输入按钮 |
|
|
|
// 语音识别按钮 |
|
|
|
Container( |
|
|
|
width: 40.r, |
|
|
|
height: 40.r, |
|
|
|
width: 40.w, |
|
|
|
height: 40.w, |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: Colors.grey[100], |
|
|
|
color: isDarkMode ? Colors.white.withOpacity(0.1) : 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], |
|
|
|
color: isDarkMode ? Colors.white : Colors.grey[700], |
|
|
|
size: 20.r, |
|
|
|
), |
|
|
|
padding: EdgeInsets.zero, |
|
|
|
@ -465,115 +399,99 @@ class ChatView extends GetView<ChatController> { |
|
|
|
), |
|
|
|
), |
|
|
|
|
|
|
|
// 文本输入框 |
|
|
|
SizedBox(width: 8.w), |
|
|
|
|
|
|
|
// 输入框 |
|
|
|
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, |
|
|
|
), |
|
|
|
child: TextField( |
|
|
|
controller: textController, |
|
|
|
decoration: InputDecoration( |
|
|
|
hintText: '输入消息...', |
|
|
|
hintStyle: TextStyle( |
|
|
|
color: isDarkMode ? Colors.grey[500] : Colors.grey[500] |
|
|
|
), |
|
|
|
maxLines: null, |
|
|
|
textInputAction: TextInputAction.send, |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 15.sp, |
|
|
|
color: Colors.black87, |
|
|
|
filled: true, |
|
|
|
fillColor: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], |
|
|
|
border: OutlineInputBorder( |
|
|
|
borderRadius: BorderRadius.circular(20.r), |
|
|
|
borderSide: BorderSide.none, |
|
|
|
), |
|
|
|
onSubmitted: (value) { |
|
|
|
if (value.trim().isNotEmpty) { |
|
|
|
controller.sendMessage(value); |
|
|
|
textController.clear(); |
|
|
|
} |
|
|
|
}, |
|
|
|
contentPadding: EdgeInsets.symmetric( |
|
|
|
horizontal: 16.w, |
|
|
|
vertical: 10.h, |
|
|
|
), |
|
|
|
), |
|
|
|
maxLines: null, |
|
|
|
textInputAction: TextInputAction.send, |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 15.sp, |
|
|
|
color: isDarkMode ? Colors.white : Colors.black87, |
|
|
|
), |
|
|
|
onSubmitted: (value) { |
|
|
|
if (value.trim().isNotEmpty) { |
|
|
|
controller.sendMessage(value); |
|
|
|
textController.clear(); |
|
|
|
} |
|
|
|
}, |
|
|
|
), |
|
|
|
), |
|
|
|
|
|
|
|
SizedBox(width: 8.w), |
|
|
|
|
|
|
|
// 发送按钮 |
|
|
|
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, |
|
|
|
Obx(() { |
|
|
|
final hasText = controller.inputText.value.isNotEmpty; |
|
|
|
return Container( |
|
|
|
width: 40.w, |
|
|
|
height: 40.w, |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: hasText |
|
|
|
? AppColors.primary |
|
|
|
: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[200], |
|
|
|
shape: BoxShape.circle, |
|
|
|
), |
|
|
|
padding: EdgeInsets.zero, |
|
|
|
onPressed: controller.inputText.value.trim().isEmpty |
|
|
|
? null |
|
|
|
: () { |
|
|
|
controller.sendMessage(controller.inputText.value); |
|
|
|
textController.clear(); |
|
|
|
}, |
|
|
|
), |
|
|
|
)), |
|
|
|
child: IconButton( |
|
|
|
icon: Icon( |
|
|
|
Icons.send, |
|
|
|
color: hasText |
|
|
|
? Colors.white |
|
|
|
: isDarkMode ? Colors.white.withOpacity(0.5) : Colors.grey[400], |
|
|
|
size: 18.r, |
|
|
|
), |
|
|
|
padding: EdgeInsets.zero, |
|
|
|
onPressed: hasText |
|
|
|
? () { |
|
|
|
controller.sendMessage(controller.inputText.value); |
|
|
|
textController.clear(); |
|
|
|
} |
|
|
|
: null, |
|
|
|
), |
|
|
|
); |
|
|
|
}), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 构建语音输入面板 |
|
|
|
Widget _buildVoiceInputPanel() { |
|
|
|
Widget _buildVoiceInputPanel(bool isDarkMode) { |
|
|
|
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), |
|
|
|
// ), |
|
|
|
// ], |
|
|
|
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, |
|
|
|
height: 120.h, |
|
|
|
child: Padding( |
|
|
|
padding: EdgeInsets.symmetric(horizontal: 30.w), |
|
|
|
padding: EdgeInsets.symmetric(horizontal: 20.w), |
|
|
|
child: Row( |
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|
|
|
crossAxisAlignment: CrossAxisAlignment.center, |
|
|
|
@ -583,7 +501,7 @@ class ChatView extends GetView<ChatController> { |
|
|
|
width: 60.r, |
|
|
|
height: 60.r, |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: Colors.white, |
|
|
|
color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.white, |
|
|
|
shape: BoxShape.circle, |
|
|
|
boxShadow: [ |
|
|
|
BoxShadow( |
|
|
|
@ -639,7 +557,7 @@ class ChatView extends GetView<ChatController> { |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: isSpeaking |
|
|
|
? AppColors.primary |
|
|
|
: Colors.grey.shade400, |
|
|
|
: isDarkMode ? Colors.grey.shade600 : Colors.grey.shade400, |
|
|
|
borderRadius: BorderRadius.circular(2.r), |
|
|
|
), |
|
|
|
), |
|
|
|
@ -659,7 +577,7 @@ class ChatView extends GetView<ChatController> { |
|
|
|
controller.isSpeechDetected.value ? '正在聆听...' : '你可以开始说话', |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 14.sp, |
|
|
|
color: Colors.grey.shade700, |
|
|
|
color: isDarkMode ? Colors.grey[400] : Colors.grey[700], |
|
|
|
fontWeight: FontWeight.w500, |
|
|
|
), |
|
|
|
)), |
|
|
|
@ -672,7 +590,7 @@ class ChatView extends GetView<ChatController> { |
|
|
|
width: 60.r, |
|
|
|
height: 60.r, |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: Colors.white, |
|
|
|
color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.white, |
|
|
|
shape: BoxShape.circle, |
|
|
|
boxShadow: [ |
|
|
|
BoxShadow( |
|
|
|
@ -686,7 +604,7 @@ class ChatView extends GetView<ChatController> { |
|
|
|
child: IconButton( |
|
|
|
icon: Icon( |
|
|
|
Icons.close, |
|
|
|
color: Colors.grey.shade600, |
|
|
|
color: isDarkMode ? Colors.white : Colors.grey.shade600, |
|
|
|
size: 28.r, |
|
|
|
), |
|
|
|
padding: EdgeInsets.zero, |
|
|
|
@ -701,4 +619,87 @@ class ChatView extends GetView<ChatController> { |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
// 构建头像 |
|
|
|
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: 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: 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); |
|
|
|
|
|
|
|
if (messageDate == today) { |
|
|
|
return '今天 ${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}'; |
|
|
|
} else if (messageDate == yesterday) { |
|
|
|
return '昨天 ${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}'; |
|
|
|
} else { |
|
|
|
return '${time.month}月${time.day}日 ${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}'; |
|
|
|
} |
|
|
|
} |
|
|
|
} |