From 14de02c822093daaeca07f83a998159a1cf07725 Mon Sep 17 00:00:00 2001 From: roxy Date: Sat, 9 May 2026 15:52:18 +0800 Subject: [PATCH] =?UTF-8?q?bug=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/modules/index/index_page.dart | 69 +++++++++-- lib/modules/user/task/task_page.dart | 19 ++- lib/services/audio/rtm_manager.dart | 114 ++++++++++++++---- .../effect/luck_gift_nomor_anim_widget.dart | 3 + .../room_red_packet_send_panel.dart | 7 ++ 需求进度.md | 1 + 6 files changed, 170 insertions(+), 43 deletions(-) diff --git a/lib/modules/index/index_page.dart b/lib/modules/index/index_page.dart index 5399ada..62d4ef0 100644 --- a/lib/modules/index/index_page.dart +++ b/lib/modules/index/index_page.dart @@ -44,6 +44,7 @@ class _SCIndexPageState extends State final List _pages = []; final Set _builtPageIndexes = {0}; final List _bottomItems = []; + final ValueNotifier _taskClaimableCount = ValueNotifier(0); SCAppGeneralManager? generalProvider; Locale? _lastLocale; bool _hasShownEntryDialogs = false; @@ -63,6 +64,7 @@ class _SCIndexPageState extends State .on() .listen(_onRegisterRewardGranted); _initializePages(); + unawaited(_loadTaskClaimableCount()); generalProvider = Provider.of(context, listen: false); Provider.of( context, @@ -103,6 +105,7 @@ class _SCIndexPageState extends State routeObserver.unsubscribe(this); OverlayManager().setHomeRootTabsVisible(false); _registerRewardSubscription?.cancel(); + _taskClaimableCount.dispose(); WakelockPlus.disable(); super.dispose(); } @@ -115,6 +118,7 @@ class _SCIndexPageState extends State WidgetsBinding.instance.addPostFrameCallback((_) { unawaited(_showEntryPopupIfNeeded()); }); + unawaited(_loadTaskClaimableCount()); } @override @@ -141,6 +145,7 @@ class _SCIndexPageState extends State @override void didPopNext() { _setHomeRootTabsVisible(true); + unawaited(_loadTaskClaimableCount()); } @override @@ -256,7 +261,12 @@ class _SCIndexPageState extends State } _pages.add(SCIndexHomePage()); - _pages.add(const TaskPage(showBackButton: false)); + _pages.add( + TaskPage( + showBackButton: false, + onTaskStatusChanged: () => unawaited(_loadTaskClaimableCount()), + ), + ); _pages.add(SCMessagePage()); _pages.add(MePage2()); } @@ -277,6 +287,9 @@ class _SCIndexPageState extends State } void _switchBottomTab(int index) { + if (index == 1) { + unawaited(_loadTaskClaimableCount()); + } if (index == _currentIndex) { return; } @@ -311,16 +324,8 @@ class _SCIndexPageState extends State ); _bottomItems.add( BottomNavigationBarItem( - icon: _buildBottomTabIcon( - active: false, - svgaPath: "sc_images/index/sc_icon_explore_anim.svga", - fallbackPath: "sc_images/index/sc_icon_explore_no.png", - ), - activeIcon: _buildBottomTabIcon( - active: true, - svgaPath: "sc_images/index/sc_icon_explore_anim.svga", - fallbackPath: "sc_images/index/sc_icon_explore_en.png", - ), + icon: _buildTaskTabIcon(active: false), + activeIcon: _buildTaskTabIcon(active: true), label: SCAppLocalizations.of(context)!.task, ), ); @@ -401,6 +406,18 @@ class _SCIndexPageState extends State ); } + Future _loadTaskClaimableCount() async { + try { + final count = await SCAccountRepository().taskClaimableCount(); + if (!mounted) return; + _taskClaimableCount.value = count; + } catch (error) { + debugPrint('[TaskCenter][TabBadge] load failed error=$error'); + if (!mounted) return; + _taskClaimableCount.value = 0; + } + } + Future _showEntryDialogs() async { if (_hasShownEntryDialogs || !mounted) { return; @@ -586,4 +603,34 @@ class _SCIndexPageState extends State fallback: Image.asset(fallbackPath, width: 35.w, height: 35.w), ); } + + Widget _buildTaskTabIcon({required bool active}) { + return ValueListenableBuilder( + valueListenable: _taskClaimableCount, + builder: (_, claimableCount, __) { + final icon = _buildBottomTabIcon( + active: active, + svgaPath: "sc_images/index/sc_icon_explore_anim.svga", + fallbackPath: + active + ? "sc_images/index/sc_icon_explore_en.png" + : "sc_images/index/sc_icon_explore_no.png", + ); + if (claimableCount <= 0) { + return icon; + } + return Badge( + backgroundColor: Colors.red, + label: text( + claimableCount > 99 ? "99+" : "$claimableCount", + fontSize: 9.sp, + textColor: Colors.white, + fontWeight: FontWeight.w600, + ), + alignment: AlignmentDirectional.topEnd, + child: icon, + ); + }, + ); + } } diff --git a/lib/modules/user/task/task_page.dart b/lib/modules/user/task/task_page.dart index a2ae916..75fb782 100644 --- a/lib/modules/user/task/task_page.dart +++ b/lib/modules/user/task/task_page.dart @@ -12,9 +12,14 @@ import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository import 'package:yumi/shared/tools/sc_entry_popup_coordinator.dart'; class TaskPage extends StatefulWidget { - const TaskPage({super.key, this.showBackButton = true}); + const TaskPage({ + super.key, + this.showBackButton = true, + this.onTaskStatusChanged, + }); final bool showBackButton; + final VoidCallback? onTaskStatusChanged; @override State createState() => _TaskPageState(); @@ -22,10 +27,7 @@ class TaskPage extends StatefulWidget { class _TaskPageState extends State with WidgetsBindingObserver, RouteAware { - static const String _taskMicAsset = 'sc_images/index/sc_icon_task_mic.png'; - static const String _taskGameAsset = 'sc_images/index/sc_icon_task_game.png'; static const String _taskGiftAsset = 'sc_images/index/sc_icon_task_gift.png'; - static const String _taskDefaultAsset = 'sc_images/index/sc_icon_task.png'; static const Duration _taskStatusCacheRefreshDelay = Duration(seconds: 65); final SCAccountRepository _repository = SCAccountRepository(); @@ -108,6 +110,7 @@ class _TaskPageState extends State _tasks = sortedTasks; _loading = false; }); + widget.onTaskStatusChanged?.call(); } catch (error) { debugPrint('[TaskCenter][Page] load failed error=$error'); if (!mounted) return; @@ -558,13 +561,7 @@ class _TaskPageState extends State } String _iconForTask(SCTaskListRes task) { - final text = - '${task.conditionType ?? ''} ${task.taskName ?? ''} ${task.taskDesc ?? ''}' - .toLowerCase(); - if (text.contains('gift')) return _taskGiftAsset; - if (text.contains('game')) return _taskGameAsset; - if (text.contains('mic')) return _taskMicAsset; - return _taskDefaultAsset; + return _taskGiftAsset; } String _taskProgressText( diff --git a/lib/services/audio/rtm_manager.dart b/lib/services/audio/rtm_manager.dart index f77ead7..1d0c599 100644 --- a/lib/services/audio/rtm_manager.dart +++ b/lib/services/audio/rtm_manager.dart @@ -108,10 +108,10 @@ class RealTimeMessagingManager extends ChangeNotifier { static const int _luckyGiftFloatMinMultiple = 5; static const int _luckyGiftBurstMinMultiple = 10; static const int _luckyGiftBurstMinAwardAmount = 5000; - static const int _luckyGiftBurstDisplayDurationMs = 2000; static const int _luckyGiftBurstMergeDelayMs = 600; static const int _luckyGiftBurstMergeWindowMs = 1800; static const int _luckyGiftBurstRecentEventTtlMs = 5000; + static const int _luckyGiftBurstPlaybackWatchdogMs = 8000; BuildContext? context; @@ -222,6 +222,7 @@ class RealTimeMessagingManager extends ChangeNotifier { Timer? _luckGiftPushPlayTimer; int _luckGiftPushPlaybackToken = 0; final Map _recentLuckGiftPushEventTimes = {}; + int get currentLuckGiftBurstPlaybackToken => _luckGiftPushPlaybackToken; String? roomRedPacketBroadcastGroupId; String? roomRedPacketBroadcastRegionCode; Debouncer debouncer = Debouncer(); @@ -648,9 +649,7 @@ class RealTimeMessagingManager extends ChangeNotifier { ///加入房间的im群聊 Future joinRoomGroup(String groupID, String message) async { - _luckGiftPushQueue.clear(); - currentPlayingLuckGift = null; - _currentLuckGiftPushKey = null; + _resetLuckGiftBurstQueue(); var joinResult = await TencentImSDKPlugin.v2TIMManager.joinGroup( groupID: groupID, message: message, @@ -2542,6 +2541,75 @@ class RealTimeMessagingManager extends ChangeNotifier { } } + Future sendRoomRedPacketBroadcast( + Map payload, { + String roomGroupId = '', + }) async { + if (payload.isEmpty) { + return; + } + final targetGroupIds = {}; + final normalizedRoomGroupId = roomGroupId.trim(); + if (normalizedRoomGroupId.isNotEmpty) { + targetGroupIds.add(normalizedRoomGroupId); + } + + var regionGroupId = roomRedPacketBroadcastGroupId?.trim() ?? ''; + if (regionGroupId.isEmpty) { + await syncRoomRedPacketBroadcastGroup(); + regionGroupId = roomRedPacketBroadcastGroupId?.trim() ?? ''; + } + if (regionGroupId.isNotEmpty) { + targetGroupIds.add(regionGroupId); + } + + if (targetGroupIds.isEmpty) { + debugPrint( + '[RoomRedPacket][IM] skip send broadcast because target groups are empty', + ); + return; + } + + final message = { + 'type': SCRoomMsgType.roomRedPacket, + 'data': payload, + }; + final data = jsonEncode(message); + for (final groupId in targetGroupIds) { + await _sendRoomRedPacketCustomMessage(groupId, data, payload); + } + } + + Future _sendRoomRedPacketCustomMessage( + String groupId, + String data, + Map payload, + ) async { + try { + final textMsg = await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .createCustomMessage(data: data); + if (textMsg.code != 0 || textMsg.data?.id == null) { + debugPrint( + '[RoomRedPacket][IM] create message failed ' + 'group=$groupId code=${textMsg.code} desc=${textMsg.desc}', + ); + return; + } + final sendResult = await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .sendMessage(id: textMsg.data!.id!, groupID: groupId, receiver: ''); + debugPrint( + '[RoomRedPacket][IM] send message packetId=${payload['packetId']} ' + 'group=$groupId code=${sendResult.code} desc=${sendResult.desc}', + ); + } catch (error) { + debugPrint( + '[RoomRedPacket][IM] send message failed group=$groupId error=$error', + ); + } + } + Future quitGroup(String groupID) async { if (groupID.isEmpty) { return; @@ -2585,14 +2653,7 @@ class RealTimeMessagingManager extends ChangeNotifier { roomAllMsgList.clear(); roomGiftMsgList.clear(); roomChatMsgList.clear(); - _luckGiftPushQueue.clear(); - currentPlayingLuckGift = null; - _currentLuckGiftPushKey = null; - _currentLuckGiftPushEventKeys.clear(); - _luckGiftPushPlayTimer?.cancel(); - _luckGiftPushPlayTimer = null; - _luckGiftPushPlaybackToken++; - _recentLuckGiftPushEventTimes.clear(); + _resetLuckGiftBurstQueue(); onNewMessageListenerGroupMap.forEach((k, v) { v = null; }); @@ -2654,12 +2715,18 @@ class RealTimeMessagingManager extends ChangeNotifier { } void cleanLuckGiftBackCoins() { + _resetLuckGiftBurstQueue(); + } + + void _resetLuckGiftBurstQueue() { _luckGiftPushQueue.clear(); + currentPlayingLuckGift = null; _currentLuckGiftPushKey = null; _currentLuckGiftPushEventKeys.clear(); _luckGiftPushPlayTimer?.cancel(); _luckGiftPushPlayTimer = null; _luckGiftPushPlaybackToken++; + _recentLuckGiftPushEventTimes.clear(); } void _scheduleLuckGiftBackCoins({bool restart = false}) { @@ -2695,20 +2762,25 @@ class RealTimeMessagingManager extends ChangeNotifier { final playbackToken = ++_luckGiftPushPlaybackToken; notifyListeners(); Future.delayed( - Duration(milliseconds: _luckyGiftBurstDisplayDurationMs), + Duration(milliseconds: _luckyGiftBurstPlaybackWatchdogMs), () { - if (playbackToken != _luckGiftPushPlaybackToken) { - return; - } - currentPlayingLuckGift = null; - _currentLuckGiftPushKey = null; - _currentLuckGiftPushEventKeys.clear(); - notifyListeners(); - _scheduleLuckGiftBackCoins(); + completeLuckGiftBackCoins(playbackToken); }, ); } + void completeLuckGiftBackCoins(int playbackToken) { + if (playbackToken != _luckGiftPushPlaybackToken || + currentPlayingLuckGift == null) { + return; + } + currentPlayingLuckGift = null; + _currentLuckGiftPushKey = null; + _currentLuckGiftPushEventKeys.clear(); + notifyListeners(); + _scheduleLuckGiftBackCoins(); + } + bool _hasSeenLuckGiftPushEvent(String eventKey, int now) { if (eventKey.isEmpty) { return false; diff --git a/lib/ui_kit/widgets/room/effect/luck_gift_nomor_anim_widget.dart b/lib/ui_kit/widgets/room/effect/luck_gift_nomor_anim_widget.dart index c71cff2..c7609a1 100644 --- a/lib/ui_kit/widgets/room/effect/luck_gift_nomor_anim_widget.dart +++ b/lib/ui_kit/widgets/room/effect/luck_gift_nomor_anim_widget.dart @@ -262,6 +262,7 @@ class _LuckGiftNomorAnimWidgetState extends State { '|${rewardData.multiple ?? 0}' '|${rewardData.normalizedMultipleType}', ); + final playbackToken = provider.currentLuckGiftBurstPlaybackToken; return SizedBox( height: 380.w, child: Padding( @@ -278,6 +279,8 @@ class _LuckGiftNomorAnimWidgetState extends State { movieConfigurer: (movieEntity) => _configureRewardBurstSvga(movieEntity, rewardData), + onPlaybackCompleted: + () => provider.completeLuckGiftBackCoins(playbackToken), ), ), ); diff --git a/lib/ui_kit/widgets/room/red_packet/room_red_packet_send_panel.dart b/lib/ui_kit/widgets/room/red_packet/room_red_packet_send_panel.dart index e3c7f0f..2bdb475 100644 --- a/lib/ui_kit/widgets/room/red_packet/room_red_packet_send_panel.dart +++ b/lib/ui_kit/widgets/room/red_packet/room_red_packet_send_panel.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -616,6 +617,12 @@ class _RoomRedPacketSendPanelState extends State { user: currentUser, ), ); + unawaited( + rtmProvider.sendRoomRedPacketBroadcast( + payload, + roomGroupId: roomProfile?.roomAccount ?? '', + ), + ); OverlayManager().addMessage( SCFloatingMessage( type: 4, diff --git a/需求进度.md b/需求进度.md index 9e34ea4..49f2a1e 100644 --- a/需求进度.md +++ b/需求进度.md @@ -558,6 +558,7 @@ - 已收口房间外红包领取层触发:红包飘屏 `GO` 只有在目标房间页面当前可见时才直接打开领取层,否则先进房;同语区或房间 IM 收到即时红包时,也只有当前可见房间匹配目标 `roomId` 才自动弹领取层,避免首页、房间列表、我的页直接弹抢红包。 - 已按最新反馈修正领取记录头像和跨房进房:领取记录空头像不再回退成红包图标,统一走项目默认头像;红包飘窗 `GO` 跨房点击直接进目标房间,不再弹进入房间二次确认。 - 已修正延时红包多入口数据不一致:公屏红包消息会同步写入待领缓存,右侧入口和领取弹层遇到同一 `packetId` 会合并接口列表、缓存和聊天 payload 的倒计时字段,避免飘屏 `GO`/右侧入口丢失倒计时;领取成功后右侧入口会优先按本地已领状态隐藏,避免接口刷新滞后导致残留。 +- 已补齐红包发送后的 IM 广播:发送成功后除本机兜底展示外,会把 `{type: ROOM_RED_PACKET, data: payload}` 发送到当前房间 IM 群和当前语区 IM 群;同房用户通过房间群看到公屏/飘屏,房外或其它房同语区用户通过语区群看到飘屏,并继续依赖 `packetId` 去重避免重复展示。 ## 已知问题 - 已接入语音房表情包素材第一版:桌面 `表情素材` 中的 `fluent_emoji` 与 `monkey` gif 已纳入项目资源,底部聊天入口改为图稿里的短白色胶囊并移除内部输入图标,右侧新增独立表情按钮;同一个房间输入弹层现在可从表情按钮直接打开表情包面板,原 unicode emoji 面板已替换为本地 gif 表情。发送逻辑同步按最新规则分流:用户在麦上发表情时只在自己麦位播放,不进入聊天列表;用户未上麦发表情时进入房间聊天框展示,不触发麦位动画。