Browse Source

1. 修改首页分页逻辑。

2. 修改好友列表。
3. 修改IM整体逻辑
ios
Jolie 4 months ago
parent
commit
7b5cd32e18
9 changed files with 1418 additions and 361 deletions
  1. 31
      lib/controller/home/home_controller.dart
  2. 320
      lib/controller/message/chat_controller.dart
  3. 9
      lib/controller/message/conversation_controller.dart
  4. 477
      lib/im/im_manager.dart
  5. 3
      lib/pages/message/chat_page.dart
  6. 453
      lib/pages/message/friend_tab.dart
  7. 12
      lib/widget/message/message_item.dart
  8. 54
      lib/widget/message/text_item.dart
  9. 420
      pubspec.lock

31
lib/controller/home/home_controller.dart

@ -125,23 +125,27 @@ class HomeController extends GetxController {
try {
recommendIsLoading.value = true;
recommendPage.value++;
final nextPage = recommendPage.value + 1;
print('推荐列表加载更多 - 当前页: ${recommendPage.value}, 下一页: $nextPage');
// (type=0)
final result = await _fetchMarriageData(
pageNum: recommendPage.value,
pageNum: nextPage,
type: 0,
);
//
recommendPage.value = nextPage;
//
recommendFeed.addAll(result['records']);
//
final int currentPage = result['current'] ?? recommendPage.value;
final int totalPages = result['pages'] ?? recommendPage.value;
final int currentPage = result['current'] as int;
final int totalPages = result['pages'] as int;
recommendHasMore.value = currentPage < totalPages;
print('推荐列表加载更多完成 - 当前页: $currentPage, 总页数: $totalPages, 还有更多: ${recommendHasMore.value}');
} catch (e) {
recommendPage.value--; // 退
_handleError('加载推荐更多异常', e, '加载更多失败');
} finally {
recommendIsLoading.value = false;
@ -154,23 +158,27 @@ class HomeController extends GetxController {
try {
nearbyIsLoading.value = true;
nearbyPage.value++;
final nextPage = nearbyPage.value + 1;
print('同城列表加载更多 - 当前页: ${nearbyPage.value}, 下一页: $nextPage');
// (type=1)
final result = await _fetchMarriageData(
pageNum: nearbyPage.value,
pageNum: nextPage,
type: 1,
);
//
nearbyPage.value = nextPage;
//
nearbyFeed.addAll(result['records']);
//
final int currentPage = result['current'] ?? nearbyPage.value;
final int totalPages = result['pages'] ?? nearbyPage.value;
final int currentPage = result['current'] as int;
final int totalPages = result['pages'] as int;
nearbyHasMore.value = currentPage < totalPages;
print('同城列表加载更多完成 - 当前页: $currentPage, 总页数: $totalPages, 还有更多: ${nearbyHasMore.value}');
} catch (e) {
nearbyPage.value--; // 退
_handleError('加载同城更多异常', e, '加载更多失败');
} finally {
nearbyIsLoading.value = false;
@ -270,6 +278,7 @@ class HomeController extends GetxController {
required int type,
}) async {
try {
print('_fetchMarriageData - pageNum: $pageNum, pageSize: $pageSize, type: $type');
// API获取数据
var response = await _homeApi.getMarriageList(
pageNum: pageNum,
@ -297,6 +306,8 @@ class HomeController extends GetxController {
.map((item) => MarriageData.fromJson(item as Map<String, dynamic>))
.toList();
print('_fetchMarriageData 返回 - 请求页码: $pageNum, 返回当前页: ${paginatedData.current}, 总页数: ${paginatedData.pages}, 记录数: ${records.length}');
return {
'records': records,
'current': paginatedData.current,

320
lib/controller/message/chat_controller.dart

@ -64,20 +64,39 @@ class ChatController extends GetxController {
///
Future<bool> sendMessage(String content) async {
try {
// 使
final tempMessage = EMMessage.createTxtSendMessage(
targetId: userId,
content: content,
);
//
messages.insert(0, tempMessage);
update();
//
final message = await IMManager.instance.sendTextMessage(content, userId);
if (message != null) {
//
messages.insert(0, message);
//
final index = messages.indexWhere((msg) => msg.msgId == tempMessage.msgId);
if (index != -1) {
messages[index] = message;
}
update();
//
_refreshConversationList();
return true;
} else {
// FAIL
update();
SmartDialog.showToast('消息发送失败,请点击重发');
return false;
}
return false;
} catch (e) {
if (Get.isLogEnable) {
Get.log('发送消息失败: $e');
}
SmartDialog.showToast('消息发送失败: $e');
return false;
}
}
@ -85,23 +104,43 @@ class ChatController extends GetxController {
///
Future<bool> sendImageMessage(String imagePath) async {
try {
// 使
final tempMessage = EMMessage.createImageSendMessage(
targetId: userId,
filePath: imagePath,
sendOriginalImage: false,
);
//
messages.insert(0, tempMessage);
update();
//
final message = await IMManager.instance.sendImageMessage(
imagePath,
userId,
);
if (message != null) {
//
messages.insert(0, message);
//
final index = messages.indexWhere((msg) => msg.msgId == tempMessage.msgId);
if (index != -1) {
messages[index] = message;
}
update();
//
_refreshConversationList();
return true;
} else {
// FAIL
update();
SmartDialog.showToast('图片发送失败,请点击重发');
return false;
}
return false;
} catch (e) {
if (Get.isLogEnable) {
Get.log('发送图片消息失败: $e');
}
SmartDialog.showToast('图片发送失败: $e');
return false;
}
}
@ -109,24 +148,44 @@ class ChatController extends GetxController {
///
Future<bool> sendVoiceMessage(String filePath, int seconds) async {
try {
// 使
final tempMessage = EMMessage.createVoiceSendMessage(
targetId: userId,
filePath: filePath,
duration: seconds,
);
//
messages.insert(0, tempMessage);
update();
//
final message = await IMManager.instance.sendVoiceMessage(
filePath,
userId,
seconds,
);
if (message != null) {
//
messages.insert(0, message);
//
final index = messages.indexWhere((msg) => msg.msgId == tempMessage.msgId);
if (index != -1) {
messages[index] = message;
}
update();
//
_refreshConversationList();
return true;
} else {
// FAIL
update();
SmartDialog.showToast('语音发送失败,请点击重发');
return false;
}
return false;
} catch (e) {
if (Get.isLogEnable) {
Get.log('发送语音消息失败: $e');
}
SmartDialog.showToast('语音发送失败: $e');
return false;
}
}
@ -209,6 +268,14 @@ class ChatController extends GetxController {
///
Future<void> fetchMessages({bool loadMore = false}) async {
try {
//
if (loadMore && _cursor == null) {
if (Get.isLogEnable) {
Get.log('没有更多消息了');
}
return;
}
final List<EMMessage?> fetchedMessages = await IMManager.instance
.getMessages(
userId,
@ -222,24 +289,43 @@ class ChatController extends GetxController {
.toList();
if (loadMore) {
//
messages.addAll(validMessages);
//
// ListView是reverse的
final existingMsgIds = messages.map((msg) => msg.msgId).toSet();
final newMessages = validMessages
.where((msg) => !existingMsgIds.contains(msg.msgId))
.toList();
if (newMessages.isNotEmpty) {
messages.addAll(newMessages);
// ID
_cursor = newMessages.last.msgId;
if (Get.isLogEnable) {
Get.log('加载更多消息成功,新增: ${newMessages.length} 条,总数量: ${messages.length}');
}
} else {
//
_cursor = null;
if (Get.isLogEnable) {
Get.log('没有更多消息了');
}
}
} else {
//
messages.assignAll(validMessages);
// ID
if (validMessages.isNotEmpty) {
_cursor = validMessages.last.msgId;
} else {
_cursor = null;
}
if (Get.isLogEnable) {
Get.log('刷新消息成功,数量: ${validMessages.length}');
}
}
// UI更新
update();
//
if (validMessages.isNotEmpty) {
_cursor = validMessages.last.msgId;
}
if (Get.isLogEnable) {
Get.log('获取消息成功,数量: ${validMessages.length}');
}
} catch (e) {
if (Get.isLogEnable) {
Get.log('获取消息失败: $e');
@ -254,6 +340,11 @@ class ChatController extends GetxController {
}
}
///
bool hasMoreMessages() {
return _cursor != null;
}
///
void addReceivedMessage(EMMessage message) {
//
@ -270,6 +361,195 @@ class ChatController extends GetxController {
}
}
///
Future<bool> recallMessage(EMMessage message) async {
try {
final success = await IMManager.instance.recallMessage(message);
if (success) {
//
final index = messages.indexWhere((msg) => msg.msgId == message.msgId);
if (index != -1) {
//
//
messages.removeAt(index);
update();
}
//
_refreshConversationList();
SmartDialog.showToast('消息已撤回');
return true;
}
return false;
} catch (e) {
if (Get.isLogEnable) {
Get.log('撤回消息失败: $e');
}
SmartDialog.showToast('撤回消息失败');
return false;
}
}
///
Future<bool> deleteMessage(EMMessage message, {bool deleteRemote = false}) async {
try {
final conversationId = message.conversationId ?? userId;
final messageId = message.msgId;
if (messageId.isEmpty) {
if (Get.isLogEnable) {
Get.log('消息ID为空,无法删除');
}
return false;
}
final success = await IMManager.instance.deleteMessage(conversationId, messageId, deleteRemote: deleteRemote);
if (success) {
//
final index = messages.indexWhere((msg) => msg.msgId == message.msgId);
if (index != -1) {
messages.removeAt(index);
update();
}
//
_refreshConversationList();
SmartDialog.showToast('消息已删除');
return true;
}
return false;
} catch (e) {
if (Get.isLogEnable) {
Get.log('删除消息失败: $e');
}
SmartDialog.showToast('删除消息失败');
return false;
}
}
///
Future<bool> markMessageAsRead(String messageId) async {
try {
final success = await IMManager.instance.markMessageAsRead(userId, messageId);
if (success) {
//
final index = messages.indexWhere((msg) => msg.msgId == messageId);
if (index != -1) {
update();
}
//
_refreshConversationList();
return true;
}
return false;
} catch (e) {
if (Get.isLogEnable) {
Get.log('标记消息为已读失败: $e');
}
return false;
}
}
///
Future<bool> markAllMessagesAsRead() async {
try {
final success = await IMManager.instance.markAllMessagesAsRead(userId);
if (success) {
//
update();
//
_refreshConversationList();
return true;
}
return false;
} catch (e) {
if (Get.isLogEnable) {
Get.log('标记所有消息为已读失败: $e');
}
return false;
}
}
///
void handleMessageRecalled(EMMessage message) {
try {
//
final index = messages.indexWhere((msg) => msg.msgId == message.msgId);
if (index != -1) {
messages.removeAt(index);
update();
}
//
_refreshConversationList();
if (Get.isLogEnable) {
Get.log('处理消息撤回: ${message.msgId}');
}
} catch (e) {
if (Get.isLogEnable) {
Get.log('处理消息撤回失败: $e');
}
}
}
///
void updateMessageStatus(EMMessage message, MessageStatus status) {
try {
final index = messages.indexWhere((msg) => msg.msgId == message.msgId);
if (index != -1) {
//
update();
if (Get.isLogEnable) {
Get.log('更新消息状态: ${message.msgId}, status=$status');
}
}
} catch (e) {
if (Get.isLogEnable) {
Get.log('更新消息状态失败: $e');
}
}
}
///
bool isMessageFailed(EMMessage message) {
return message.status == MessageStatus.FAIL;
}
///
Future<bool> resendMessage(EMMessage failedMessage) async {
try {
if (Get.isLogEnable) {
Get.log('🔄 [ChatController] 开始重发消息: ${failedMessage.msgId}');
}
// IMManager的重发方法
final newMessage = await IMManager.instance.resendMessage(failedMessage);
if (newMessage != null) {
//
final index = messages.indexWhere((msg) => msg.msgId == failedMessage.msgId);
if (index != -1) {
messages[index] = newMessage;
update();
if (Get.isLogEnable) {
Get.log('✅ [ChatController] 消息重发成功');
}
//
_refreshConversationList();
SmartDialog.showToast('消息已重发');
return true;
}
} else {
if (Get.isLogEnable) {
Get.log('❌ [ChatController] 消息重发失败');
}
SmartDialog.showToast('消息重发失败,请重试');
return false;
}
return false;
} catch (e) {
if (Get.isLogEnable) {
Get.log('❌ [ChatController] 重发消息异常: $e');
}
SmartDialog.showToast('重发消息失败: $e');
return false;
}
}
///
void _refreshConversationList() {
try {

9
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';
@ -30,7 +29,6 @@ class ConversationController extends GetxController {
final List<EMConversation> convList = await IMManager.instance.getConversations();
//
conversations.value = convList;
await IMManager.instance.sendTextMessage('你好啊啊', '1114267797208305664');
// 使GetX日志系统
if (Get.isLogEnable) {
@ -55,22 +53,17 @@ class ConversationController extends GetxController {
///
String getLastMessageContent(EMMessage? message) {
if(message?.body.type == MessageType.TXT){
EMTextMessageBody body = message?.body as EMTextMessageBody;
final body = message?.body as EMTextMessageBody;
return body.content;
}else if(message?.body.type == MessageType.IMAGE){
EMImageMessageBody body = message?.body as EMImageMessageBody;
return '[图片]';
}else if(message?.body.type == MessageType.VOICE){
EMVoiceMessageBody body = message?.body as EMVoiceMessageBody;
return '[语音]';
}else if(message?.body.type == MessageType.VIDEO){
EMVideoMessageBody body = message?.body as EMVideoMessageBody;
return '[视频]';
}else if(message?.body.type == MessageType.FILE){
EMFileMessageBody body = message?.body as EMFileMessageBody;
return '[文件]';
}else if(message?.body.type == MessageType.LOCATION){
EMLocationMessageBody body = message?.body as EMLocationMessageBody;
return '[位置]';
}
return '暂无消息';

477
lib/im/im_manager.dart

@ -17,6 +17,11 @@ class IMManager {
static IMManager get instance => _instance;
bool _isInitialized = false;
bool _listenersRegistered = false;
//
static const String _connectionHandlerKey = 'im_manager_connection_handler';
static const String _chatHandlerKey = 'im_manager_chat_handler';
// ChatController key userId
final Map<String, ChatController> _activeChatControllers = {};
@ -55,75 +60,77 @@ class IMManager {
//
void _registerListeners() {
try {
//
//
if (_listenersRegistered) {
if (Get.isLogEnable) {
Get.log('监听器已注册,跳过重复注册');
}
return;
}
//
EMClient.getInstance.addConnectionEventHandler(
'',
_connectionHandlerKey,
EMConnectionEventHandler(
onConnected: () {
print('Connected to IM server');
if (Get.isLogEnable) {
Get.log('✅ [IMManager] 已连接到IM服务器');
}
},
onDisconnected: () {
print('Disconnected from IM server:');
if (Get.isLogEnable) {
Get.log('❌ [IMManager] 与IM服务器断开连接');
}
// TODO:
},
onTokenDidExpire: () {
print('IM token about to expire');
if (Get.isLogEnable) {
Get.log('⚠️ [IMManager] IM token即将过期');
}
// TODO: token的逻辑
},
onUserKickedByOtherDevice: () {
print('User kicked out of IM server');
if (Get.isLogEnable) {
Get.log('⚠️ [IMManager] 用户被其他设备踢出');
}
// TODO:
},
),
);
//
EMClient.getInstance.chatManager.addEventHandler(
// EMChatEventHandler key
"",
_chatHandlerKey,
EMChatEventHandler(
onMessagesReceived: (messages) {
if (Get.isLogEnable) {
Get.log('📨 [IMManager] 收到 ${messages.length} 条新消息');
}
//
_refreshConversationList();
// ChatController
_notifyChatControllers(messages);
for (var msg in messages) {
switch (msg.body.type) {
case MessageType.TXT:
{}
break;
case MessageType.IMAGE:
{}
break;
case MessageType.VIDEO:
{}
break;
case MessageType.LOCATION:
{}
break;
case MessageType.VOICE:
{}
break;
case MessageType.FILE:
{}
break;
case MessageType.CUSTOM:
{}
break;
case MessageType.COMBINE:
{}
break;
case MessageType.CMD:
{
// CMD CMD `EMChatEventHandler#onCmdMessagesReceived`
}
break;
}
},
onCmdMessagesReceived: (cmdMessages) {
if (Get.isLogEnable) {
Get.log('📨 [IMManager] 收到 ${cmdMessages.length} 条CMD消息');
}
// CMD消息
for (var msg in cmdMessages) {
_handleCmdMessage(msg);
}
},
),
);
_listenersRegistered = true;
if (Get.isLogEnable) {
Get.log('✅ [IMManager] 监听器注册成功');
}
} catch (e) {
print('Failed to register listeners: $e');
if (Get.isLogEnable) {
Get.log('❌ [IMManager] 注册监听器失败: $e');
}
}
}
@ -187,7 +194,7 @@ class IMManager {
int duration,
) async {
try {
//
//
final message = EMMessage.createVoiceSendMessage(
targetId: toChatUsername,
filePath: filePath,
@ -196,10 +203,14 @@ class IMManager {
//
await EMClient.getInstance.chatManager.sendMessage(message);
print('Image message sent successfully');
if (Get.isLogEnable) {
Get.log('✅ [IMManager] 语音消息发送成功');
}
return message;
} catch (e) {
print('Failed to send image message: $e');
if (Get.isLogEnable) {
Get.log('❌ [IMManager] 发送语音消息失败: $e');
}
return null;
}
}
@ -310,7 +321,7 @@ class IMManager {
return EMClient.getInstance.chatManager.loadAllConversations();
}
///
///
Future<Map<String, EMUserInfo>> getContacts(String userId) async {
return await EMClient.getInstance.userInfoManager.fetchUserInfoById([
userId,
@ -323,14 +334,44 @@ class IMManager {
int pageSize = 20,
String? startMsgId,
}) async {
EMConversationType convType = EMConversationType.Chat;
EMCursorResult<EMMessage?> cursor = await EMClient.getInstance.chatManager
.fetchHistoryMessagesByOption(
conversationId,
convType,
pageSize: pageSize,
try {
EMConversationType convType = EMConversationType.Chat;
//
final conversation = await EMClient.getInstance.chatManager.getConversation(
conversationId,
type: convType,
createIfNeed: false,
);
if (conversation == null) {
if (Get.isLogEnable) {
Get.log('❌ [IMManager] 会话不存在: $conversationId');
}
return [];
}
// startMsgId
if (startMsgId != null && startMsgId.isNotEmpty) {
// ID开始加载更旧的消息
final messages = await conversation.loadMessages(
startMsgId: startMsgId,
loadCount: pageSize,
);
return messages.map((msg) => msg as EMMessage?).toList();
} else {
//
final messages = await conversation.loadMessages(
loadCount: pageSize,
);
return cursor.data;
return messages.map((msg) => msg as EMMessage?).toList();
}
} catch (e) {
if (Get.isLogEnable) {
Get.log('❌ [IMManager] 获取消息失败: $e');
}
return [];
}
}
///
@ -436,12 +477,336 @@ class IMManager {
}
}
/// CMD消息
void _handleCmdMessage(EMMessage cmdMessage) {
try {
// CMD消息
if (Get.isLogEnable) {
Get.log('处理CMD消息: ${cmdMessage.msgId}');
}
} catch (e) {
if (Get.isLogEnable) {
Get.log('处理CMD消息失败: $e');
}
}
}
// SDK支持更多回调时可以使用
// SDK的EMChatEventHandler可能不支持这些回调
/*
///
void _notifyMessageRead(List<EMMessage> messages, String from, String to) {
try {
// ChatController
final controller = _activeChatControllers[from];
if (controller != null) {
// TODO: ChatController
if (Get.isLogEnable) {
Get.log('通知消息已读: $from');
}
}
} catch (e) {
if (Get.isLogEnable) {
Get.log('通知消息已读失败: $e');
}
}
}
///
void _notifyMessageDelivered(List<EMMessage> messages, String to) {
try {
// ChatController
for (var message in messages) {
final targetId = message.to;
if (targetId != null) {
final controller = _activeChatControllers[targetId];
if (controller != null) {
// TODO: ChatController
if (Get.isLogEnable) {
Get.log('通知消息已送达: $targetId');
}
}
}
}
} catch (e) {
if (Get.isLogEnable) {
Get.log('通知消息已送达失败: $e');
}
}
}
///
void _notifyMessageRecalled(List<EMMessage> messages) {
try {
// ChatController
for (var message in messages) {
final fromId = message.from;
final toId = message.to;
// ChatController
if (fromId != null) {
final controller = _activeChatControllers[fromId];
if (controller != null) {
// TODO: ChatController
if (Get.isLogEnable) {
Get.log('通知消息撤回(发送方): $fromId');
}
}
}
// ChatController
if (toId != null) {
final controller = _activeChatControllers[toId];
if (controller != null) {
// TODO: ChatController
if (Get.isLogEnable) {
Get.log('通知消息撤回(接收方): $toId');
}
}
}
}
//
_refreshConversationList();
} catch (e) {
if (Get.isLogEnable) {
Get.log('通知消息撤回失败: $e');
}
}
}
///
void _notifyMessageStatusChanged(EMMessage message, MessageStatus status, EMError? error) {
try {
final targetId = message.to;
if (targetId != null) {
final controller = _activeChatControllers[targetId];
if (controller != null) {
// TODO: ChatController
if (Get.isLogEnable) {
Get.log('通知消息状态变更: $targetId, status=$status');
}
}
}
} catch (e) {
if (Get.isLogEnable) {
Get.log('通知消息状态变更失败: $e');
}
}
}
*/
///
Future<bool> recallMessage(EMMessage message) async {
try {
final messageId = message.msgId;
if (messageId.isEmpty) {
if (Get.isLogEnable) {
Get.log('❌ [IMManager] 消息ID为空,无法撤回');
}
return false;
}
await EMClient.getInstance.chatManager.recallMessage(messageId);
if (Get.isLogEnable) {
Get.log('✅ [IMManager] 消息撤回成功: $messageId');
}
//
_refreshConversationList();
return true;
} catch (e) {
if (Get.isLogEnable) {
Get.log('❌ [IMManager] 消息撤回失败: $e');
}
return false;
}
}
///
Future<bool> deleteMessage(String conversationId, String messageId, {bool deleteRemote = false}) async {
try {
final conversation = await EMClient.getInstance.chatManager.getConversation(
conversationId,
type: EMConversationType.Chat,
createIfNeed: false,
);
if (conversation != null) {
await conversation.deleteMessage(messageId);
if (Get.isLogEnable) {
Get.log('✅ [IMManager] 消息删除成功: $messageId');
}
//
_refreshConversationList();
return true;
}
return false;
} catch (e) {
if (Get.isLogEnable) {
Get.log('❌ [IMManager] 消息删除失败: $e');
}
return false;
}
}
///
Future<bool> markMessageAsRead(String conversationId, String messageId) async {
try {
final conversation = await EMClient.getInstance.chatManager.getConversation(
conversationId,
type: EMConversationType.Chat,
createIfNeed: false,
);
if (conversation != null) {
await conversation.markMessageAsRead(messageId);
if (Get.isLogEnable) {
Get.log('✅ [IMManager] 消息标记为已读: $messageId');
}
//
_refreshConversationList();
return true;
}
return false;
} catch (e) {
if (Get.isLogEnable) {
Get.log('❌ [IMManager] 标记消息为已读失败: $e');
}
return false;
}
}
///
Future<bool> markAllMessagesAsRead(String conversationId) async {
try {
final conversation = await EMClient.getInstance.chatManager.getConversation(
conversationId,
type: EMConversationType.Chat,
createIfNeed: false,
);
if (conversation != null) {
await conversation.markAllMessagesAsRead();
if (Get.isLogEnable) {
Get.log('✅ [IMManager] 会话所有消息标记为已读: $conversationId');
}
//
_refreshConversationList();
return true;
}
return false;
} catch (e) {
if (Get.isLogEnable) {
Get.log('❌ [IMManager] 标记会话所有消息为已读失败: $e');
}
return false;
}
}
///
Future<EMMessage?> resendMessage(EMMessage failedMessage) async {
try {
final targetId = failedMessage.to ?? failedMessage.conversationId;
if (targetId == null || targetId.isEmpty) {
if (Get.isLogEnable) {
Get.log('❌ [IMManager] 重发消息失败:目标ID为空');
}
return null;
}
EMMessage? newMessage;
//
switch (failedMessage.body.type) {
case MessageType.TXT:
final textBody = failedMessage.body as EMTextMessageBody;
newMessage = EMMessage.createTxtSendMessage(
targetId: targetId,
content: textBody.content,
);
break;
case MessageType.IMAGE:
final imageBody = failedMessage.body as EMImageMessageBody;
final localPath = imageBody.localPath;
if (localPath.isEmpty) {
if (Get.isLogEnable) {
Get.log('❌ [IMManager] 重发图片消息失败:本地路径为空');
}
return null;
}
newMessage = EMMessage.createImageSendMessage(
targetId: targetId,
filePath: localPath,
sendOriginalImage: false,
);
break;
case MessageType.VOICE:
final voiceBody = failedMessage.body as EMVoiceMessageBody;
final localPath = voiceBody.localPath;
if (localPath.isEmpty) {
if (Get.isLogEnable) {
Get.log('❌ [IMManager] 重发语音消息失败:本地路径为空');
}
return null;
}
newMessage = EMMessage.createVoiceSendMessage(
targetId: targetId,
filePath: localPath,
duration: voiceBody.duration,
);
break;
case MessageType.VIDEO:
final videoBody = failedMessage.body as EMVideoMessageBody;
final localPath = videoBody.localPath;
if (localPath.isEmpty) {
if (Get.isLogEnable) {
Get.log('❌ [IMManager] 重发视频消息失败:本地路径为空');
}
return null;
}
newMessage = EMMessage.createVideoSendMessage(
targetId: targetId,
filePath: localPath,
duration: videoBody.duration ?? 0,
thumbnailLocalPath: videoBody.thumbnailLocalPath,
);
break;
default:
if (Get.isLogEnable) {
Get.log('❌ [IMManager] 不支持重发该类型的消息: ${failedMessage.body.type}');
}
return null;
}
//
final result = await EMClient.getInstance.chatManager.sendMessage(newMessage);
if (Get.isLogEnable) {
Get.log('✅ [IMManager] 消息重发成功: ${newMessage.msgId}');
}
return result;
} catch (e) {
if (Get.isLogEnable) {
Get.log('❌ [IMManager] 重发消息失败: $e');
}
return null;
}
}
///
void dispose() {
try {
EMClient.getInstance.removeConnectionEventHandler("");
if (_listenersRegistered) {
EMClient.getInstance.removeConnectionEventHandler(_connectionHandlerKey);
EMClient.getInstance.chatManager.removeEventHandler(_chatHandlerKey);
_listenersRegistered = false;
if (Get.isLogEnable) {
Get.log('✅ [IMManager] 资源清理完成');
}
}
} catch (e) {
print('Failed to dispose resources: $e');
if (Get.isLogEnable) {
Get.log('❌ [IMManager] 清理资源失败: $e');
}
}
}
}

3
lib/pages/message/chat_page.dart

@ -37,7 +37,8 @@ class _ChatPageState extends State<ChatPage> {
_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 100 &&
!_isLoadingMore &&
_controller.messages.isNotEmpty) {
_controller.messages.isNotEmpty &&
_controller.hasMoreMessages()) {
_loadMoreMessages();
}
});

453
lib/pages/message/friend_tab.dart

@ -8,7 +8,43 @@ class FriendTab extends StatefulWidget {
State<FriendTab> createState() => _FriendTabState();
}
class _FriendTabState extends State<FriendTab> {
class _FriendTabState extends State<FriendTab> with TickerProviderStateMixin {
late TabController _tabController;
//
static const Color _primaryPurple = Color(0xff8E7BF6);
// -
final List<Map<String, dynamic>> _followList = [
{
"id": 1,
"name": "叫我大王",
"avatar": Assets.imagesAvatarsExample,
"age": 30,
"location": "广州",
"gender": "female",
"isFollowed": true,
},
{
"id": 2,
"name": "林园园",
"avatar": Assets.imagesAvatarsExample,
"age": 28,
"location": "深圳",
"gender": "female",
"isFollowed": true,
},
{
"id": 3,
"name": "张雪",
"avatar": Assets.imagesAvatarsExample,
"age": 25,
"location": "北京",
"gender": "female",
"isFollowed": true,
},
];
// -
final List<Map<String, dynamic>> _friendList = [
{
@ -49,87 +85,392 @@ class _FriendTabState extends State<FriendTab> {
},
];
// -
final List<Map<String, dynamic>> _fansList = [
{
"id": 1,
"name": "李晖",
"avatar": Assets.imagesAvatarsExample,
"age": 32,
"location": "上海",
"gender": "male",
},
{
"id": 2,
"name": "王强",
"avatar": Assets.imagesAvatarsExample,
"age": 29,
"location": "杭州",
"gender": "male",
},
{
"id": 3,
"name": "李哲",
"avatar": Assets.imagesAvatarsExample,
"age": 27,
"location": "成都",
"gender": "male",
},
];
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this, initialIndex: 0);
_tabController.addListener(() {
setState(() {});
});
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
//
_buildTabBar(),
//
Expanded(
child: TabBarView(
controller: _tabController,
physics: const NeverScrollableScrollPhysics(), //
children: [
_buildFollowList(), //
_buildFriendList(), //
_buildFansList(), //
],
),
),
],
);
}
//
Widget _buildTabBar() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
_buildTabButton("我的关注", 0),
const SizedBox(width: 16),
_buildTabButton("好友", 1),
const SizedBox(width: 16),
_buildTabButton("粉丝", 2),
],
),
);
}
//
Widget _buildTabButton(String title, int index) {
final bool isSelected = _tabController.index == index;
return Expanded(
child: GestureDetector(
onTap: () {
if (_tabController.index != index) {
_tabController.animateTo(index);
}
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: isSelected ? _primaryPurple : Colors.white,
borderRadius: BorderRadius.circular(20),
),
alignment: Alignment.center,
child: Text(
title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: isSelected ? Colors.white : Colors.black87,
),
),
),
),
);
}
//
Widget _buildFollowList() {
return ListView.builder(
padding: const EdgeInsets.only(top: 8),
itemCount: _followList.length,
itemBuilder: (context, index) {
final item = _followList[index];
return _buildFollowItem(item);
},
);
}
//
Widget _buildFollowItem(Map<String, dynamic> item) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
//
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(25),
child: Image.asset(item["avatar"], fit: BoxFit.cover),
),
),
//
Expanded(
child: Container(
margin: const EdgeInsets.only(left: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
children: [
Text(
item["name"],
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(width: 4),
//
Icon(
item["gender"] == "female" ? Icons.female : Icons.male,
size: 16,
color: item["gender"] == "female" ? Colors.pink : Colors.blue,
),
],
),
const SizedBox(height: 4),
Text(
"${item["age"]}岁·${item["location"]}",
style: const TextStyle(
color: Colors.grey,
fontSize: 12,
),
),
],
),
),
),
//
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(16),
),
child: const Text(
"已关注",
style: TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
),
],
),
);
}
//
Widget _buildFriendList() {
return ListView.builder(
padding: const EdgeInsets.only(top: 8),
itemCount: _friendList.length,
itemBuilder: (context, index) {
final friend = _friendList[index];
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: Row(
return _buildFriendItem(friend);
},
);
}
//
Widget _buildFriendItem(Map<String, dynamic> friend) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
//
Stack(
children: [
//
Stack(
children: [
Container(
width: 50,
height: 50,
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(25),
child: Image.asset(friend["avatar"], fit: BoxFit.cover),
),
),
if (friend["isOnline"] == true)
Positioned(
bottom: 0,
right: 0,
child: Container(
width: 12,
height: 12,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25),
color: Colors.green,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: Colors.white, width: 2),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(25),
child: Image.asset(friend["avatar"], fit: BoxFit.cover),
),
),
],
),
//
Expanded(
child: Container(
margin: const EdgeInsets.only(left: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
friend["name"],
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
if (friend["isOnline"] == true)
Positioned(
bottom: 0,
right: 0,
child: Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: Colors.white, width: 2),
),
),
const SizedBox(height: 4),
Text(
friend["isOnline"] == true ? "在线" : "离线",
style: TextStyle(
color: friend["isOnline"] == true ? Colors.green : Colors.grey,
fontSize: 12,
),
),
],
),
//
Expanded(
child: Container(
margin: const EdgeInsets.only(left: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
),
),
//
const Icon(Icons.arrow_forward_ios, size: 16, color: Colors.grey),
],
),
);
}
//
Widget _buildFansList() {
return ListView.builder(
padding: const EdgeInsets.only(top: 8),
itemCount: _fansList.length,
itemBuilder: (context, index) {
final fan = _fansList[index];
return _buildFanItem(fan);
},
);
}
//
Widget _buildFanItem(Map<String, dynamic> fan) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
//
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(25),
child: Image.asset(fan["avatar"], fit: BoxFit.cover),
),
),
//
Expanded(
child: Container(
margin: const EdgeInsets.only(left: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
children: [
Text(
friend["name"],
fan["name"],
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
friend["isOnline"] == true ? "在线" : "离线",
style: TextStyle(
color: friend["isOnline"] == true ? Colors.green : Colors.grey,
fontSize: 12,
),
const SizedBox(width: 4),
//
Icon(
fan["gender"] == "female" ? Icons.female : Icons.male,
size: 16,
color: fan["gender"] == "female" ? Colors.pink : Colors.blue,
),
],
),
),
const SizedBox(height: 4),
Text(
"${fan["age"]}岁·${fan["location"]}",
style: const TextStyle(
color: Colors.grey,
fontSize: 12,
),
),
],
),
//
const Icon(Icons.arrow_forward_ios, size: 16, color: Colors.grey),
],
),
),
);
},
//
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
decoration: BoxDecoration(
color: _primaryPurple,
borderRadius: BorderRadius.circular(16),
),
child: const Text(
"关注",
style: TextStyle(
fontSize: 12,
color: Colors.white,
),
),
),
],
),
);
}
}

12
lib/widget/message/message_item.dart

@ -1,11 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:im_flutter_sdk/im_flutter_sdk.dart';
import 'package:get/get.dart';
import 'text_item.dart';
import 'image_item.dart';
import 'voice_item.dart';
import 'video_item.dart';
import '../../controller/message/chat_controller.dart';
class MessageItem extends StatelessWidget {
final EMMessage message;
@ -31,6 +33,16 @@ class MessageItem extends StatelessWidget {
isSentByMe: isSentByMe,
showTime: shouldShowTime(),
formattedTime: formatMessageTime(message.serverTime),
message: message,
onResend: () {
// Get找到ChatController并调用重发方法
try {
final controller = Get.find<ChatController>();
controller.resendMessage(message);
} catch (e) {
print('重发消息失败: $e');
}
},
);
}
//

54
lib/widget/message/text_item.dart

@ -10,12 +10,16 @@ class TextItem extends StatelessWidget {
final bool isSentByMe;
final bool showTime;
final String formattedTime;
final EMMessage message; //
final VoidCallback? onResend; //
const TextItem({
required this.textBody,
required this.isSentByMe,
required this.showTime,
required this.formattedTime,
required this.message,
this.onResend,
super.key,
});
@ -61,6 +65,8 @@ class TextItem extends StatelessWidget {
emojiSize: 24.w,
),
),
//
if (isSentByMe) _buildMessageStatus(),
if (isSentByMe) SizedBox(width: 8.w),
if (isSentByMe) _buildAvatar(),
],
@ -106,4 +112,52 @@ class TextItem extends StatelessWidget {
),
);
}
//
Widget _buildMessageStatus() {
//
if (!isSentByMe) {
return SizedBox.shrink();
}
//
final status = message.status;
if (status == MessageStatus.FAIL) {
//
return GestureDetector(
onTap: onResend,
child: Container(
width: 20.w,
height: 20.w,
margin: EdgeInsets.only(right: 4.w),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.refresh,
size: 14.w,
color: Colors.red,
),
),
);
} else if (status == MessageStatus.PROGRESS) {
//
return Container(
width: 16.w,
height: 16.w,
margin: EdgeInsets.only(right: 4.w),
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
isSentByMe ? Colors.white70 : Colors.grey,
),
),
);
} else {
//
return SizedBox(width: 4.w);
}
}
}

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

Loading…
Cancel
Save