You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

760 lines
26 KiB

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 'chat_music_card.dart';
import 'chat_navigation_card.dart';
import 'weather_day_card.dart';
class ChatView extends GetView<ChatController> {
const ChatView({Key? key}) : super(key: key);
@override
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),
);
}
// 构建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: 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: BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
),
),
SizedBox(width: 4.w),
Text(
'online'.tr, // 在线
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
: 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: [
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);
}
}),
),
// 底部输入区域
_buildInputArea(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.data != null) {
if (message.data?['card_weather'] != null) {
box = WeatherDayCard(message.data, isDarkMode);
}
if (message.data?['card_spotify'] != null) {
box = ChatMusicCard(message.data?['card_spotify'], isDarkMode);
}
if (message.data?['card_navigation'] != null) {
box = ChatNavigationCard(message.data?['card_navigation'], isDarkMode);
}
}
return box;
}
// 构建输入区域
Widget _buildInputArea(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(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
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: Row(
children: [
// 语音识别按钮
Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[100],
shape: BoxShape.circle,
),
child: IconButton(
icon: Icon(
Icons.mic_none,
color: isDarkMode ? Colors.white : Colors.grey[700],
size: 20.r,
),
padding: EdgeInsets.zero,
onPressed: controller.startVoiceRecognition,
),
),
SizedBox(width: 8.w),
// 输入框
Expanded(
child: TextField(
controller: textController,
decoration: InputDecoration(
hintText: 'inputMessage'.tr, // 输入消息...
hintStyle: TextStyle(
color: isDarkMode ? Colors.grey[500] : Colors.grey[500]),
filled: true,
fillColor: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[100],
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20.r),
borderSide: BorderSide.none,
),
contentPadding: EdgeInsets.symmetric(
horizontal: 16.w,
vertical: 10.h,
),
),
maxLines: null,
textInputAction: TextInputAction.newline,
style: TextStyle(
fontSize: 15.sp,
color: isDarkMode ? Colors.white : Colors.black87,
),
onChanged: (value) => controller.inputText.value = value,
onSubmitted: (value) {
if (value.trim().isNotEmpty) {
controller.sendMessage(value);
textController.clear();
controller.inputText.value = ''; // 清空输入框
}
},
),
),
SizedBox(width: 8.w),
// 发送按钮
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,
),
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();
controller.inputText.value = ''; // 清空输入框
}
: null,
),
);
}),
],
),
),
);
}
// 构建语音输入面板
Widget _buildVoiceInputPanel(bool isDarkMode) {
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: 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
: 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,
),
)),
],
),
),
// 关闭按钮
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: 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);
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';
}
}
}