From 9b13ca0e007af9908e25872dc7314bf4d4a57e14 Mon Sep 17 00:00:00 2001 From: Jolie <412895109@qq.com> Date: Tue, 23 Dec 2025 23:00:54 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0IM=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../message/conversation_controller.dart | 31 +++ lib/im/im_manager.dart | 220 ++++++++++++++++++ lib/pages/main/main_page.dart | 97 ++++++-- .../message/message_notification_dialog.dart | 107 +++++++++ 4 files changed, 441 insertions(+), 14 deletions(-) create mode 100644 lib/widget/message/message_notification_dialog.dart diff --git a/lib/controller/message/conversation_controller.dart b/lib/controller/message/conversation_controller.dart index 9e6cdf3..edee404 100644 --- a/lib/controller/message/conversation_controller.dart +++ b/lib/controller/message/conversation_controller.dart @@ -58,6 +58,9 @@ class ConversationController extends GetxController { // 筛选类型 final filterType = FilterType.none.obs; + // 总未读数 + final totalUnreadCount = 0.obs; + /// 缓存用户信息(公开方法,供 ChatController 调用) void cacheUserInfo(String userId, ExtendedUserInfo userInfo) { _userInfoCache[userId] = userInfo; @@ -141,6 +144,9 @@ class ConversationController extends GetxController { // 更新会话列表(在用户信息提取完成后) conversations.value = convList; + + // 计算总未读数 + await _updateTotalUnreadCount(); // 使用GetX日志系统 if (Get.isLogEnable) { @@ -418,6 +424,31 @@ class ConversationController extends GetxController { } } + /// 更新总未读数(内部方法) + Future _updateTotalUnreadCount() async { + try { + int total = 0; + for (var conversation in conversations) { + final unreadCount = await getUnreadCount(conversation); + total += unreadCount; + } + totalUnreadCount.value = total; + if (Get.isLogEnable) { + Get.log('✅ [ConversationController] 总未读数已更新: $total'); + } + } catch (e) { + if (Get.isLogEnable) { + Get.log('⚠️ [ConversationController] 更新总未读数失败: $e'); + } + totalUnreadCount.value = 0; + } + } + + /// 刷新总未读数(公开方法,供外部调用) + Future refreshTotalUnreadCount() async { + await _updateTotalUnreadCount(); + } + /// 格式化消息时间 String formatMessageTime(int timestamp) { DateTime messageTime = DateTime.fromMillisecondsSinceEpoch(timestamp); diff --git a/lib/im/im_manager.dart b/lib/im/im_manager.dart index a9ac66c..911c089 100644 --- a/lib/im/im_manager.dart +++ b/lib/im/im_manager.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'dart:async'; import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; @@ -12,7 +13,24 @@ import '../controller/message/conversation_controller.dart'; import '../controller/message/chat_controller.dart'; import '../controller/global.dart'; import '../pages/mine/login_page.dart'; +import '../pages/message/chat_page.dart'; import '../network/user_api.dart'; +import '../widget/message/message_notification_dialog.dart'; + +// 消息通知数据结构 +class _NotificationMessage { + final String fromId; + final String nickName; + final String avatarUrl; + final String messageContent; + + _NotificationMessage({ + required this.fromId, + required this.nickName, + required this.avatarUrl, + required this.messageContent, + }); +} // 完整的IM管理器实现,使用实际的SDK类型和方法 class IMManager { @@ -40,6 +58,12 @@ class IMManager { // 存储 Presence 状态变化回调,key 为 userId final Map _presenceCallbacks = {}; + // 消息通知弹框队列 + final List<_NotificationMessage> _notificationQueue = []; + + // 当前是否有弹框正在显示 + bool _isShowingNotification = false; + IMManager._internal() { print('IMManager instance created'); } @@ -186,6 +210,8 @@ class IMManager { for (var message in messages) { if (message.direction == MessageDirection.RECEIVE) { _parseUserInfoFromMessageExt(message); + // 检查发送者是否是当前正在聊天的用户,如果不是则显示弹框 + _checkAndShowNotificationDialog(message); } } // 收到新消息时,更新会话列表 @@ -1353,6 +1379,200 @@ class IMManager { } } + /// 检查并显示消息通知弹框(如果发送者不是当前正在聊天的用户) + void _checkAndShowNotificationDialog(EMMessage message) { + try { + // 获取消息发送者ID + final fromId = message.from; + if (fromId == null || fromId.isEmpty) { + return; + } + + // 检查发送者是否是当前正在聊天的用户 + // 如果 _activeChatControllers 中包含该用户ID,说明当前正在和该用户聊天 + if (_activeChatControllers.containsKey(fromId)) { + // 是当前正在聊天的用户,不显示弹框 + return; + } + + // 获取消息内容 + String messageContent = _getMessageContent(message); + if (messageContent.isEmpty) { + return; + } + + // 从消息扩展字段中获取用户信息(头像、昵称) + Map? attributes; + try { + attributes = message.attributes; + } catch (e) { + if (Get.isLogEnable) { + Get.log('⚠️ [IMManager] 无法访问消息扩展字段: $e'); + } + return; + } + + String? nickName; + String? avatarUrl; + + if (attributes != null) { + // 从扩展字段中获取发送者信息 + nickName = attributes['sender_nickName'] as String?; + avatarUrl = attributes['sender_avatarUrl'] as String?; + } + + // 如果从消息扩展字段中获取不到,尝试从 ConversationController 的缓存中获取 + if ((nickName == null || nickName.isEmpty) || (avatarUrl == null || avatarUrl.isEmpty)) { + try { + if (Get.isRegistered()) { + final conversationController = Get.find(); + final cachedUserInfo = conversationController.getCachedUserInfo(fromId); + if (cachedUserInfo != null) { + nickName = nickName ?? cachedUserInfo.nickName; + avatarUrl = avatarUrl ?? cachedUserInfo.avatarUrl; + } + } + } catch (e) { + if (Get.isLogEnable) { + Get.log('⚠️ [IMManager] 从ConversationController获取用户信息失败: $e'); + } + } + } + + // 使用默认值 + final finalNickName = nickName ?? fromId; + final finalAvatarUrl = avatarUrl ?? ''; + + // 将消息加入队列 + _notificationQueue.add(_NotificationMessage( + fromId: fromId, + nickName: finalNickName, + avatarUrl: finalAvatarUrl, + messageContent: messageContent, + )); + + if (Get.isLogEnable) { + Get.log('✅ [IMManager] 消息已加入通知队列: fromId=$fromId, nickName=$finalNickName, 队列长度=${_notificationQueue.length}'); + } + + // 如果当前没有弹框显示,立即显示队列中的第一条消息 + if (!_isShowingNotification) { + _showNextNotification(); + } + } catch (e) { + if (Get.isLogEnable) { + Get.log('⚠️ [IMManager] 处理消息通知失败: $e'); + } + } + } + + /// 显示队列中的下一条消息通知 + void _showNextNotification() { + // 如果队列为空或正在显示弹框,直接返回 + if (_notificationQueue.isEmpty || _isShowingNotification) { + return; + } + + // 从队列中取出第一条消息 + final notification = _notificationQueue.removeAt(0); + _isShowingNotification = true; + + if (Get.isLogEnable) { + Get.log('✅ [IMManager] 显示消息通知弹框: fromId=${notification.fromId}, nickName=${notification.nickName}, 剩余队列长度=${_notificationQueue.length}'); + } + + // 显示弹框(从上方弹出) + SmartDialog.show( + builder: (context) { + return MessageNotificationDialog( + avatarUrl: notification.avatarUrl, + nickName: notification.nickName, + messageContent: notification.messageContent, + onTap: () { + // 点击弹框时关闭弹框 + SmartDialog.dismiss(); + _onNotificationDismissed(); + // 跳转到聊天页面 + Get.to(() => ChatPage( + userId: notification.fromId, + userData: null, // userData 可选,ChatPage 会自己处理 + )); + }, + ); + }, + alignment: Alignment.topCenter, + animationType: SmartAnimationType.centerFade_otherSlide, + animationTime: Duration(milliseconds: 300), + usePenetrate: false, + clickMaskDismiss: true, + backDismiss: true, + keepSingle: true, // 保持只有一个弹框显示 + maskColor: Colors.transparent, // 透明背景 + ); + + // 3秒后自动关闭弹框 + Future.delayed(Duration(seconds: 3), () { + if (_isShowingNotification) { + SmartDialog.dismiss(); + // 延迟一小段时间后调用,确保弹框关闭动画完成 + Future.delayed(Duration(milliseconds: 300), () { + _onNotificationDismissed(); + }); + } + }); + } + + /// 弹框关闭时的处理 + void _onNotificationDismissed() { + if (!_isShowingNotification) { + return; + } + + _isShowingNotification = false; + + if (Get.isLogEnable) { + Get.log('✅ [IMManager] 消息通知弹框已关闭,剩余队列长度=${_notificationQueue.length}'); + } + + // 延迟一小段时间后显示下一条消息(确保动画完成) + Future.delayed(Duration(milliseconds: 300), () { + if (!_isShowingNotification) { + _showNextNotification(); + } + }); + } + + /// 获取消息内容文本 + String _getMessageContent(EMMessage message) { + try { + if (message.body.type == MessageType.TXT) { + final body = message.body as EMTextMessageBody; + return body.content ?? ''; + } else if (message.body.type == MessageType.IMAGE) { + return '[图片]'; + } else if (message.body.type == MessageType.VOICE) { + return '[语音]'; + } else if (message.body.type == MessageType.VIDEO) { + return '[视频]'; + } else if (message.body.type == MessageType.FILE) { + return '[文件]'; + } else if (message.body.type == MessageType.LOCATION) { + return '[位置]'; + } else if (message.body.type == MessageType.CUSTOM) { + final body = message.body as EMCustomMessageBody; + if (body.event == 'live_room_invite') { + return '[分享房间]'; + } + return '[自定义消息]'; + } + } catch (e) { + if (Get.isLogEnable) { + Get.log('⚠️ [IMManager] 获取消息内容失败: $e'); + } + } + return ''; + } + /// 刷新会话列表 void _refreshConversationList() { try { diff --git a/lib/pages/main/main_page.dart b/lib/pages/main/main_page.dart index 1876b4c..8a60f11 100644 --- a/lib/pages/main/main_page.dart +++ b/lib/pages/main/main_page.dart @@ -3,12 +3,12 @@ import 'package:dating_touchme_app/pages/message/message_page.dart'; import 'package:dating_touchme_app/pages/mine/mine_page.dart'; import 'package:dating_touchme_app/rtc/rtm_manager.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:flutter_svga/flutter_svga.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:dating_touchme_app/controller/mine/user_controller.dart'; +import 'package:dating_touchme_app/controller/message/conversation_controller.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; import '../../extension/router_service.dart'; @@ -85,22 +85,38 @@ class _MainPageState extends State { minePage, ], ), - bottomNavigationBar: TDBottomTabBar( - currentIndex: currentIndex, - TDBottomTabBarBasicType.iconText, - componentType: TDBottomTabBarComponentType.normal, - useVerticalDivider: false, - navigationTabs: [ - tabItem('首页', Assets.imagesHomePre, Assets.imagesHomeNol, 0), - tabItem('找对象', Assets.imagesDiscoverPre, Assets.imagesDiscoverNol, 1), - tabItem('消息', Assets.imagesMessagePre, Assets.imagesMessageNol, 2), - tabItem('我的', Assets.imagesMinePre, Assets.imagesMineNol, 3), - ] - ) + bottomNavigationBar: _buildBottomNavigationBar(), ), ); } + /// 构建底部导航栏 + Widget _buildBottomNavigationBar() { + // 确保 ConversationController 已注册(如果未注册则注册一个,避免 Obx 中的错误) + if (!Get.isRegistered()) { + Get.put(ConversationController(), permanent: false); + } + + return Obx(() { + // 在 Obx 内部直接访问 observable,不使用条件判断,确保 GetX 能够追踪变化 + final controller = Get.find(); + final unreadCount = controller.totalUnreadCount.value; + + return TDBottomTabBar( + currentIndex: currentIndex, + TDBottomTabBarBasicType.iconText, + componentType: TDBottomTabBarComponentType.normal, + useVerticalDivider: false, + navigationTabs: [ + tabItem('首页', Assets.imagesHomePre, Assets.imagesHomeNol, 0), + tabItem('找对象', Assets.imagesDiscoverPre, Assets.imagesDiscoverNol, 1), + tabItemWithBadge('消息', Assets.imagesMessagePre, Assets.imagesMessageNol, 2, unreadCount), + tabItem('我的', Assets.imagesMinePre, Assets.imagesMineNol, 3), + ] + ); + }); + } + /// 底部导航栏item TDBottomTabBarTabConfig tabItem(String title, String selectedIcon, String unselectedIcon, int index) { return TDBottomTabBarTabConfig( @@ -115,4 +131,57 @@ class _MainPageState extends State { }, ); } + + /// 底部导航栏item(带未读数红点) + TDBottomTabBarTabConfig tabItemWithBadge(String title, String selectedIcon, String unselectedIcon, int index, int unreadCount) { + // 构建带未读数红点的图标 + Widget buildIconWithBadge(String iconPath, bool isSelected) { + return Stack( + clipBehavior: Clip.none, + children: [ + Image.asset(iconPath, width: 25, height: 25, fit: BoxFit.cover), + if (unreadCount > 0) + Positioned( + right: -6.w, + top: -6.w, + child: Container( + padding: EdgeInsets.symmetric(horizontal: unreadCount > 99 ? 4.w : 5.w, vertical: 2.w), + decoration: BoxDecoration( + color: Color(0xFFFF3B30), + borderRadius: BorderRadius.circular(10.w), + border: Border.all(color: Colors.white, width: 1.w), + ), + constraints: BoxConstraints( + minWidth: 16.w, + minHeight: 16.w, + ), + child: Center( + child: Text( + unreadCount > 99 ? '99+' : unreadCount.toString(), + style: TextStyle( + color: Colors.white, + fontSize: 10.sp, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + ), + ), + ), + ), + ], + ); + } + + return TDBottomTabBarTabConfig( + tabText: title, + selectedIcon: buildIconWithBadge(selectedIcon, true), + unselectedIcon: buildIconWithBadge(unselectedIcon, false), + selectTabTextStyle: TextStyle(color: Color(0xFFED4AC3)), + unselectTabTextStyle: TextStyle(color: Color(0xFF999999)), + onTap: () { + currentIndex = index; + pageController.jumpToPage(index); + }, + ); + } } diff --git a/lib/widget/message/message_notification_dialog.dart b/lib/widget/message/message_notification_dialog.dart new file mode 100644 index 0000000..cd60e31 --- /dev/null +++ b/lib/widget/message/message_notification_dialog.dart @@ -0,0 +1,107 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:dating_touchme_app/generated/assets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +/// 消息通知弹框 +class MessageNotificationDialog extends StatelessWidget { + final String avatarUrl; + final String nickName; + final String messageContent; + final VoidCallback? onTap; + + const MessageNotificationDialog({ + super.key, + required this.avatarUrl, + required this.nickName, + required this.messageContent, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + margin: EdgeInsets.symmetric(horizontal: 16.w), + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: Offset(0, 2.h), + ), + ], + ), + child: Row( + children: [ + // 头像 + ClipOval( + child: avatarUrl.isNotEmpty + ? CachedNetworkImage( + imageUrl: avatarUrl, + width: 48.w, + height: 48.w, + fit: BoxFit.cover, + placeholder: (context, url) => Image.asset( + Assets.imagesUserAvatar, + width: 48.w, + height: 48.w, + fit: BoxFit.cover, + ), + errorWidget: (context, url, error) => Image.asset( + Assets.imagesUserAvatar, + width: 48.w, + height: 48.w, + fit: BoxFit.cover, + ), + ) + : Image.asset( + Assets.imagesUserAvatar, + width: 48.w, + height: 48.w, + fit: BoxFit.cover, + ), + ), + SizedBox(width: 12.w), + // 昵称和内容 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // 昵称 + Text( + nickName, + style: TextStyle( + fontSize: 15.sp, + fontWeight: FontWeight.w600, + color: Color(0xFF333333), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 4.h), + // 消息内容 + Text( + messageContent, + style: TextStyle( + fontSize: 13.sp, + color: Color(0xFF666666), + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + ); + } +} +