Browse Source

优化聊天板块

ios
Jolie 4 months ago
parent
commit
0d3ef4cc56
8 changed files with 540 additions and 533 deletions
  1. 80
      lib/controller/message/chat_controller.dart
  2. 23
      lib/controller/message/conversation_controller.dart
  3. 20
      lib/im/im_manager.dart
  4. 58
      lib/pages/home/user_information_page.dart
  5. 292
      lib/pages/message/chat_page.dart
  6. 148
      lib/pages/message/conversation_tab.dart
  7. 18
      lib/widget/message/message_item.dart
  8. 434
      pubspec.lock

80
lib/controller/message/chat_controller.dart

@ -3,6 +3,7 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'dart:convert';
import '../../im/im_manager.dart';
import '../../model/home/marriage_data.dart';
import 'package:im_flutter_sdk/im_flutter_sdk.dart';
import 'conversation_controller.dart';
@ -12,6 +13,13 @@ class ChatController extends GetxController {
//
final Rx<EMUserInfo?> userInfo = Rx<EMUserInfo?>(null);
// 使
MarriageData? _userData;
// 使
String? _externalNickName;
String? _externalAvatarUrl;
//
final RxList<EMMessage> messages = RxList<EMMessage>([]);
@ -22,7 +30,20 @@ class ChatController extends GetxController {
final RxBool isSendingVideo = RxBool(false);
final RxString sendingStatus = RxString('');
ChatController({required this.userId});
ChatController({
required this.userId,
MarriageData? userData,
}) {
// 使
if (userData != null) {
_userData = userData;
_externalNickName = userData.nickName;
_externalAvatarUrl = userData.profilePhoto;
}
}
///
MarriageData? get userData => _userData;
@override
void onInit() {
@ -32,6 +53,10 @@ class ChatController extends GetxController {
//
fetchUserInfo();
fetchMessages();
// UI
if (_externalNickName != null || _externalAvatarUrl != null) {
update();
}
}
@override
@ -52,17 +77,64 @@ class ChatController extends GetxController {
Get.log('获取用户信息成功: ${info.nickName}');
}
} else {
if (Get.isLogEnable) {
Get.log('未找到用户信息: $userId');
// userInfo
if (userInfo.value == null) {
if (Get.isLogEnable) {
Get.log('未找到用户信息: $userId,使用外部传入的信息');
}
// 使 UI
update();
}
}
} catch (e) {
if (Get.isLogEnable) {
Get.log('获取用户信息失败: $e');
Get.log('获取用户信息失败: $e,使用外部传入的信息');
}
// 使 UI
update();
}
}
///
void setUserInfoFromExternal({String? nickName, String? avatarUrl}) {
try {
//
_externalNickName = nickName;
_externalAvatarUrl = avatarUrl;
//
if (userInfo.value == null) {
fetchUserInfo().then((_) {
// UI 使
if (userInfo.value == null) {
update();
}
});
}
// UI 使
update();
if (Get.isLogEnable) {
Get.log('设置外部用户信息: nickName=$nickName, avatarUrl=$avatarUrl');
}
} catch (e) {
if (Get.isLogEnable) {
Get.log('设置用户信息失败: $e');
}
}
}
/// 使使
String? get userNickName {
return userInfo.value?.nickName ?? _externalNickName;
}
/// 使使
String? get userAvatarUrl {
return userInfo.value?.avatarUrl ?? _externalAvatarUrl;
}
///
Future<bool> sendMessage(String content) async {
EMMessage? tempMessage;

23
lib/controller/message/conversation_controller.dart

@ -1,5 +1,4 @@
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:im_flutter_sdk/im_flutter_sdk.dart';
import '../../im/im_manager.dart';
@ -109,9 +108,25 @@ class ConversationController extends GetxController {
return '${messageTime.month.toString().padLeft(2, '0')}-${messageTime.day.toString().padLeft(2, '0')}';
}
Future<EMUserInfo> loadContact(String userId) async{
var data = await IMManager.instance.getContacts(userId);
return data[userId]!;
Future<EMUserInfo?> loadContact(String userId) async{
try {
var data = await IMManager.instance.getContacts(userId);
final userInfo = data[userId];
// null
if (userInfo == null) {
if (Get.isLogEnable) {
Get.log('⚠️ [ConversationController] 未获取到用户信息: $userId');
}
}
return userInfo;
} catch (e) {
if (Get.isLogEnable) {
Get.log('❌ [ConversationController] 获取用户信息失败: $userId, 错误: $e');
}
return null;
}
}
Future<EMMessage?> lastMessage(EMConversation conversation) async{

20
lib/im/im_manager.dart

@ -171,7 +171,6 @@ class IMManager {
String content,
String toChatUsername,
) async {
print('Text message sent');
try {
//
final message = EMMessage.createTxtSendMessage(
@ -179,10 +178,23 @@ class IMManager {
content: content,
);
print('Text message sent successfully');
return await EMClient.getInstance.chatManager.sendMessage(message);
//
final sentMessage = await EMClient.getInstance.chatManager.sendMessage(message);
if (Get.isLogEnable) {
Get.log('✅ [IMManager] 文本消息发送成功: to=$toChatUsername, content=$content');
} else {
print('✅ [IMManager] 文本消息发送成功: to=$toChatUsername, content=$content');
}
return sentMessage;
} catch (e) {
print('Failed to send text message: $e');
//
if (Get.isLogEnable) {
Get.log('❌ [IMManager] 发送文本消息失败: to=$toChatUsername, 错误: $e');
} else {
print('❌ [IMManager] 发送文本消息失败: to=$toChatUsername, 错误: $e');
}
return null;
}
}

58
lib/pages/home/user_information_page.dart

@ -1,6 +1,7 @@
import 'package:dating_touchme_app/generated/assets.dart';
import 'package:dating_touchme_app/model/home/marriage_data.dart';
import 'package:dating_touchme_app/pages/home/report_page.dart';
import 'package:dating_touchme_app/pages/message/chat_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
@ -252,31 +253,40 @@ class _UserInformationPageState extends State<UserInformationPage> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
width: 246.w,
height: 38.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(38.w)),
color: const Color.fromRGBO(51, 51, 51, 1)
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
Assets.imagesChatBtn,
width: 13.w,
height: 12.w,
),
SizedBox(width: 4.w,),
Text(
"发消息",
style: TextStyle(
fontSize: 15.w,
color: Colors.white,
fontWeight: FontWeight.w500
InkWell(
onTap: () {
// ID和用户数据模型
Get.to(() => ChatPage(
userId: widget.userData.userId,
userData: widget.userData,
));
},
child: Container(
width: 246.w,
height: 38.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(38.w)),
color: const Color.fromRGBO(51, 51, 51, 1)
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
Assets.imagesChatBtn,
width: 13.w,
height: 12.w,
),
)
],
SizedBox(width: 4.w,),
Text(
"发消息",
style: TextStyle(
fontSize: 15.w,
color: Colors.white,
fontWeight: FontWeight.w500
),
)
],
),
),
),
Container(

292
lib/pages/message/chat_page.dart

@ -8,14 +8,20 @@ import '../../controller/message/chat_controller.dart';
import '../../controller/message/voice_player_manager.dart';
// import '../../controller/message/call_manager.dart'; //
import '../../generated/assets.dart';
import '../../model/home/marriage_data.dart';
import '../../../widget/message/chat_input_bar.dart';
import '../../../widget/message/message_item.dart';
import 'chat_settings_page.dart';
class ChatPage extends StatefulWidget {
final String userId;
final String userId; // MarriageData.userId
final MarriageData? userData; //
const ChatPage({required this.userId, super.key});
const ChatPage({
required this.userId,
this.userData,
super.key,
});
@override
State<ChatPage> createState() => _ChatPageState();
@ -29,8 +35,19 @@ class _ChatPageState extends State<ChatPage> {
@override
void initState() {
super.initState();
// controller
_controller = Get.put(ChatController(userId: widget.userId));
// controller
// userId controller
final tag = 'chat_${widget.userId}';
if (Get.isRegistered<ChatController>(tag: tag)) {
Get.delete<ChatController>(tag: tag);
}
_controller = Get.put(
ChatController(
userId: widget.userId,
userData: widget.userData,
),
tag: tag,
);
//
_scrollController.addListener(() {
@ -79,7 +96,7 @@ class _ChatPageState extends State<ChatPage> {
child: Scaffold(
backgroundColor: Color(0xffF5F5F5),
appBar: AppBar(
title: Text(controller.userInfo.value?.nickName ?? ''),
title: Text(controller.userNickName ?? widget.userData?.nickName ?? ''),
centerTitle: true,
actions: [
Container(
@ -124,7 +141,7 @@ class _ChatPageState extends State<ChatPage> {
itemBuilder: (context, index) {
//
if (index == 0) {
return _buildUserInfoCard(controller);
return _buildUserInfoCard(controller, widget.userData);
}
final message = controller.messages[index - 1];
@ -141,6 +158,7 @@ class _ChatPageState extends State<ChatPage> {
message: message,
isSentByMe: isSentByMe,
previousMessage: previousMessage,
chatController: controller, // controller 使 Get.find
);
},
),
@ -204,55 +222,44 @@ class _ChatPageState extends State<ChatPage> {
}
//
Widget _buildUserInfoCard(ChatController controller) {
final userInfo = controller.userInfo.value;
Widget _buildUserInfoCard(ChatController controller, MarriageData? userData) {
// 使 MarriageData使
final marriageData = userData ?? _getMarriageDataFromController(controller);
//
if (userInfo == null) {
if (marriageData == null) {
return SizedBox.shrink();
}
return Container(
margin: EdgeInsets.only(bottom: 16.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.w),
),
child: LayoutBuilder(
builder: (context, constraints) {
// 710:351
// ListView padding16.w
final screenWidth = MediaQuery.of(context).size.width;
final availableWidth = screenWidth - 32.w; // padding
final width = availableWidth;
final height = width * (351 / 710);
return Container(
height: height,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.w),
image: DecorationImage(
image: AssetImage(Assets.imagesChatUserBg),
fit: BoxFit.cover,
return IntrinsicHeight(
child: Container(
margin: EdgeInsets.only(bottom: 16.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.w),
image: DecorationImage(
image: AssetImage(Assets.imagesChatUserBg),
fit: BoxFit.cover,
),
),
child: Stack(
children: [
// chat_user_bg_bottom
Positioned.fill(
child: ClipRRect(
borderRadius: BorderRadius.circular(16.w),
child: Image.asset(
Assets.imagesChatUserBgBottom,
fit: BoxFit.cover, //
),
),
),
child: Stack(
children: [
// chat_user_bg_bottom710x351比例覆盖整个卡片
Positioned.fill(
child: ClipRRect(
borderRadius: BorderRadius.circular(16.w),
child: Image.asset(
Assets.imagesChatUserBgBottom,
fit: BoxFit.cover,
),
),
),
//
Padding(
padding: EdgeInsets.all(16.w),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//
Padding(
padding: EdgeInsets.all(16.w),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//
Row(
crossAxisAlignment: CrossAxisAlignment.start,
@ -268,7 +275,7 @@ class _ChatPageState extends State<ChatPage> {
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Text(
userInfo.nickName ?? '用户',
marriageData.nickName,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
@ -276,47 +283,48 @@ class _ChatPageState extends State<ChatPage> {
),
),
//
Container(
padding: EdgeInsets.symmetric(
horizontal: 8.w,
vertical: 2.h,
),
decoration: BoxDecoration(
color: Color(0xFFF3E9FF),
borderRadius: BorderRadius.circular(12.w),
border: Border.all(
width: 1,
color: Color.fromRGBO(117, 98, 249, 0.32),
if (marriageData.isRealNameCertified)
Container(
padding: EdgeInsets.symmetric(
horizontal: 8.w,
vertical: 4.h,
),
),
constraints: BoxConstraints(
minWidth: 40.w,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
Assets.imagesVerifiedIcon,
width: 14.w,
height: 12.w,
decoration: BoxDecoration(
color: Color(0xFFF3E9FF),
borderRadius: BorderRadius.circular(12.w),
border: Border.all(
width: 1,
color: Color.fromRGBO(117, 98, 249, 0.32),
),
SizedBox(width: 4.w),
Text(
'实名',
style: TextStyle(
fontSize: 9.sp,
color: Color.fromRGBO(160, 92, 255, 1),
fontWeight: FontWeight.w500,
),
constraints: BoxConstraints(
minWidth: 40.w,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
Assets.imagesVerifiedIcon,
width: 14.w,
height: 12.w,
),
),
],
SizedBox(width: 4.w),
Text(
'实名',
style: TextStyle(
fontSize: 9.sp,
color: Color.fromRGBO(160, 92, 255, 1),
fontWeight: FontWeight.w500,
),
),
],
),
),
),
// 线
Container(
padding: EdgeInsets.symmetric(
horizontal: 8.w,
vertical: 2.h,
vertical: 4.h,
),
decoration: BoxDecoration(
color: Color.fromRGBO(234, 255, 219, 1),
@ -338,10 +346,10 @@ class _ChatPageState extends State<ChatPage> {
],
),
SizedBox(height: 8.h),
//
// MarriageData
Row(
children: [
//
//
Image.asset(
Assets.imagesFemale,
width: 14.w,
@ -349,28 +357,22 @@ class _ChatPageState extends State<ChatPage> {
),
SizedBox(width: 4.w),
Text(
'19',
'${marriageData.age}',
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey[700],
),
),
SizedBox(width: 12.w),
Text(
'北京',
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey[700],
),
),
SizedBox(width: 12.w),
Text(
'160cm',
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey[700],
if (marriageData.cityName.isNotEmpty) ...[
SizedBox(width: 12.w),
Text(
marriageData.cityName,
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey[700],
),
),
),
],
],
),
],
@ -394,48 +396,70 @@ class _ChatPageState extends State<ChatPage> {
),
SizedBox(height: 8.h), //
//
Text(
'喜欢在广州的早茶里抢最后一个虾饺,也能在深夜的猎德大桥...',
style: TextStyle(
fontSize: 13.sp,
color: Colors.grey[800],
height: 1.4,
if (marriageData.describeInfo.isNotEmpty)
Text(
marriageData.describeInfo,
style: TextStyle(
fontSize: 13.sp,
color: Colors.grey[800],
height: 1.4,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 8.h), //
//
Row(
children: List.generate(4, (index) {
return Expanded(
child: Container(
margin: EdgeInsets.only(
right: index < 3 ? 8.w : 0,
),
height: 50.w, //
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.w),
image: DecorationImage(
image: AssetImage(Assets.imagesAvatarsExample),
fit: BoxFit.cover,
if (marriageData.photoList.isNotEmpty) ...[
SizedBox(height: 8.h), //
//
Row(
children: marriageData.photoList.take(4).toList().asMap().entries.map((entry) {
final index = entry.key;
final photo = entry.value;
final photoUrl = photo.photoUrl.trim().replaceAll('`', ''); // URL中的反引号
return Expanded(
child: AspectRatio(
aspectRatio: 1, //
child: Container(
margin: EdgeInsets.only(
right: index < 3 ? 8.w : 0,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.w),
image: DecorationImage(
image: NetworkImage(photoUrl),
fit: BoxFit.cover,
onError: (exception, stackTrace) {
//
return;
},
),
),
),
),
),
);
}),
),
],
),
),
],
);
}).toList(),
),
SizedBox(height: 4.h), //
],
],
),
),
);
},
],
),
),
);
}
// Controller MarriageData
MarriageData? _getMarriageDataFromController(ChatController controller) {
// controller userData
if (controller.userData != null) {
return controller.userData;
}
// controller null
return null;
}
//
Future<void> _loadMoreMessages() async {
if (_isLoadingMore) return;

148
lib/pages/message/conversation_tab.dart

@ -44,16 +44,11 @@ class _ConversationTabState extends State<ConversationTab>
);
}
// 使
return ListView.builder(
padding: const EdgeInsets.only(top: 8),
itemCount: controller.conversations.length + 1, //
itemCount: controller.conversations.length,
itemBuilder: (context, index) {
//
if (index == 0) {
return _buildTestConversationItem();
}
final conversation = controller.conversations[index - 1];
final conversation = controller.conversations[index];
return _buildConversationItem(conversation);
},
);
@ -63,142 +58,9 @@ class _ConversationTabState extends State<ConversationTab>
);
}
//
Widget _buildTestConversationItem() {
final double screenWidth = MediaQuery.of(context).size.width;
final int testUnreadCount = 3;
final String testTime = controller.formatMessageTime(
DateTime.now().millisecondsSinceEpoch,
);
final Widget cellContent = Builder(
builder: (cellContext) => GestureDetector(
onTap: () {
TDSwipeCellInherited.of(cellContext)?.cellClick();
// 使ID
Get.to(ChatPage(userId: 'test_user_001'));
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(16)),
),
margin: const EdgeInsets.only(
bottom: 8,
left: 16,
right: 16,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(28),
image: const DecorationImage(
image: AssetImage(Assets.imagesAvatarsExample),
fit: BoxFit.cover,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Expanded(
child: Text(
'测试用户',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.black,
),
),
),
Text(
testTime,
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
],
),
const SizedBox(height: 6),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Expanded(
child: Text(
'这是一条测试消息,用于调试',
style: TextStyle(
fontSize: 14,
color: Colors.grey,
),
overflow: TextOverflow.ellipsis,
),
),
Container(
margin: const EdgeInsets.only(left: 8),
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(10),
),
child: Text(
testUnreadCount.toString(),
style: const TextStyle(
fontSize: 12,
color: Colors.white,
),
),
),
],
),
],
),
),
],
),
),
),
);
return TDSwipeCell(
slidableKey: const ValueKey('test_conversation'),
groupTag: 'conversation_swipe_group',
right: TDSwipeCellPanel(
extentRatio: 72 / screenWidth,
children: [
TDSwipeCellAction(
backgroundColor: TDTheme.of(context).errorColor6,
label: '删除',
onPressed: (actionContext) {
Get.snackbar('调试', '删除测试会话');
},
),
],
),
cell: cellContent,
);
}
//
Widget _buildConversationItem(EMConversation conversation) {
return FutureBuilder<EMUserInfo>(
return FutureBuilder<EMUserInfo?>(
future: controller.loadContact(conversation.id),
builder: (context, userSnapshot) {
final EMUserInfo? userInfo = userSnapshot.data;
@ -263,7 +125,9 @@ class _ConversationTabState extends State<ConversationTab>
children: [
Expanded(
child: Text(
userInfo?.nickName ?? '联系人',
(userInfo?.nickName ?? '').isNotEmpty
? userInfo!.nickName!
: conversation.id, // ID
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,

18
lib/widget/message/message_item.dart

@ -14,11 +14,13 @@ class MessageItem extends StatelessWidget {
final EMMessage message;
final bool isSentByMe;
final EMMessage? previousMessage;
final ChatController? chatController; // controller
const MessageItem({
required this.message,
required this.isSentByMe,
this.previousMessage,
this.chatController,
super.key,
});
@ -51,9 +53,9 @@ class MessageItem extends StatelessWidget {
showTime: shouldShowTime(),
formattedTime: formatMessageTime(message.serverTime),
onResend: () {
// Get找到ChatController并调用重发方法
// controller Get ChatController
try {
final controller = Get.find<ChatController>();
final controller = chatController ?? Get.find<ChatController>();
controller.resendMessage(message);
} catch (e) {
print('重发消息失败: $e');
@ -72,9 +74,9 @@ class MessageItem extends StatelessWidget {
formattedTime: formatMessageTime(message.serverTime),
message: message,
onResend: () {
// Get找到ChatController并调用重发方法
// controller Get ChatController
try {
final controller = Get.find<ChatController>();
final controller = chatController ?? Get.find<ChatController>();
controller.resendMessage(message);
} catch (e) {
print('重发消息失败: $e');
@ -92,9 +94,9 @@ class MessageItem extends StatelessWidget {
formattedTime: formatMessageTime(message.serverTime),
message: message,
onResend: () {
// Get找到ChatController并调用重发方法
// controller Get ChatController
try {
final controller = Get.find<ChatController>();
final controller = chatController ?? Get.find<ChatController>();
controller.resendMessage(message);
} catch (e) {
print('重发消息失败: $e');
@ -124,9 +126,9 @@ class MessageItem extends StatelessWidget {
formattedTime: formatMessageTime(message.serverTime),
message: message,
onResend: () {
// Get找到ChatController并调用重发方法
// controller Get ChatController
try {
final controller = Get.find<ChatController>();
final controller = chatController ?? Get.find<ChatController>();
controller.resendMessage(message);
} catch (e) {
print('重发消息失败: $e');

434
pubspec.lock
File diff suppressed because it is too large
View File

Loading…
Cancel
Save