bug修复

This commit is contained in:
roxy 2026-05-09 15:52:18 +08:00
parent 058a4d9c4e
commit 14de02c822
6 changed files with 170 additions and 43 deletions

View File

@ -44,6 +44,7 @@ class _SCIndexPageState extends State<SCIndexPage>
final List<Widget> _pages = [];
final Set<int> _builtPageIndexes = <int>{0};
final List<BottomNavigationBarItem> _bottomItems = [];
final ValueNotifier<int> _taskClaimableCount = ValueNotifier<int>(0);
SCAppGeneralManager? generalProvider;
Locale? _lastLocale;
bool _hasShownEntryDialogs = false;
@ -63,6 +64,7 @@ class _SCIndexPageState extends State<SCIndexPage>
.on<RegisterRewardGrantedEvent>()
.listen(_onRegisterRewardGranted);
_initializePages();
unawaited(_loadTaskClaimableCount());
generalProvider = Provider.of<SCAppGeneralManager>(context, listen: false);
Provider.of<RtcProvider>(
context,
@ -103,6 +105,7 @@ class _SCIndexPageState extends State<SCIndexPage>
routeObserver.unsubscribe(this);
OverlayManager().setHomeRootTabsVisible(false);
_registerRewardSubscription?.cancel();
_taskClaimableCount.dispose();
WakelockPlus.disable();
super.dispose();
}
@ -115,6 +118,7 @@ class _SCIndexPageState extends State<SCIndexPage>
WidgetsBinding.instance.addPostFrameCallback((_) {
unawaited(_showEntryPopupIfNeeded());
});
unawaited(_loadTaskClaimableCount());
}
@override
@ -141,6 +145,7 @@ class _SCIndexPageState extends State<SCIndexPage>
@override
void didPopNext() {
_setHomeRootTabsVisible(true);
unawaited(_loadTaskClaimableCount());
}
@override
@ -256,7 +261,12 @@ class _SCIndexPageState extends State<SCIndexPage>
}
_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<SCIndexPage>
}
void _switchBottomTab(int index) {
if (index == 1) {
unawaited(_loadTaskClaimableCount());
}
if (index == _currentIndex) {
return;
}
@ -311,16 +324,8 @@ class _SCIndexPageState extends State<SCIndexPage>
);
_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<SCIndexPage>
);
}
Future<void> _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<void> _showEntryDialogs() async {
if (_hasShownEntryDialogs || !mounted) {
return;
@ -586,4 +603,34 @@ class _SCIndexPageState extends State<SCIndexPage>
fallback: Image.asset(fallbackPath, width: 35.w, height: 35.w),
);
}
Widget _buildTaskTabIcon({required bool active}) {
return ValueListenableBuilder<int>(
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,
);
},
);
}
}

View File

@ -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<TaskPage> createState() => _TaskPageState();
@ -22,10 +27,7 @@ class TaskPage extends StatefulWidget {
class _TaskPageState extends State<TaskPage>
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<TaskPage>
_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<TaskPage>
}
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(

View File

@ -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<String, int> _recentLuckGiftPushEventTimes = <String, int>{};
int get currentLuckGiftBurstPlaybackToken => _luckGiftPushPlaybackToken;
String? roomRedPacketBroadcastGroupId;
String? roomRedPacketBroadcastRegionCode;
Debouncer debouncer = Debouncer();
@ -648,9 +649,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
///im群聊
Future<V2TimCallback> 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<void> sendRoomRedPacketBroadcast(
Map<String, dynamic> payload, {
String roomGroupId = '',
}) async {
if (payload.isEmpty) {
return;
}
final targetGroupIds = <String>{};
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 = <String, dynamic>{
'type': SCRoomMsgType.roomRedPacket,
'data': payload,
};
final data = jsonEncode(message);
for (final groupId in targetGroupIds) {
await _sendRoomRedPacketCustomMessage(groupId, data, payload);
}
}
Future<void> _sendRoomRedPacketCustomMessage(
String groupId,
String data,
Map<String, dynamic> 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;

View File

@ -262,6 +262,7 @@ class _LuckGiftNomorAnimWidgetState extends State<LuckGiftNomorAnimWidget> {
'|${rewardData.multiple ?? 0}'
'|${rewardData.normalizedMultipleType}',
);
final playbackToken = provider.currentLuckGiftBurstPlaybackToken;
return SizedBox(
height: 380.w,
child: Padding(
@ -278,6 +279,8 @@ class _LuckGiftNomorAnimWidgetState extends State<LuckGiftNomorAnimWidget> {
movieConfigurer:
(movieEntity) =>
_configureRewardBurstSvga(movieEntity, rewardData),
onPlaybackCompleted:
() => provider.completeLuckGiftBackCoins(playbackToken),
),
),
);

View File

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
@ -616,6 +617,12 @@ class _RoomRedPacketSendPanelState extends State<RoomRedPacketSendPanel> {
user: currentUser,
),
);
unawaited(
rtmProvider.sendRoomRedPacketBroadcast(
payload,
roomGroupId: roomProfile?.roomAccount ?? '',
),
);
OverlayManager().addMessage(
SCFloatingMessage(
type: 4,

View File

@ -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 表情。发送逻辑同步按最新规则分流:用户在麦上发表情时只在自己麦位播放,不进入聊天列表;用户未上麦发表情时进入房间聊天框展示,不触发麦位动画。