import { _decorator, Button, Color, Component, Label, Node, Sprite, Tween, tween, UITransform, v3 } from 'cc'; import { VirtualScrollView } from './VScrollView'; import { Palette } from './Palette'; import { SlotRankingDataManager } from './SlotRankingDataManager'; import { callGameApiForRank, getGameId, truncateString } from 'db://assets/Loading/scripts/comm'; import { I18nManager } from 'db://assets/Loading/scripts/manager/I18nManager'; import { AudioManager } from 'db://assets/Loading/scripts/manager/AudioManager'; const { ccclass, property } = _decorator; @ccclass('RankList') export class RankList extends Component { // ==================== 当前排行榜 ==================== msg_1: Node = null // 跑马灯信息 currentMarqueeIndex: number = 0; // 当前跑马灯文本索引 marqueeTexts: string[] = []; // 跑马灯文本数组 rankList: Node = null; // 当前排行榜节点 rankingEndTime: Node = null; // 排行榜结束时间节点 rankLoadingNode: Node = null; // 加载动画节点 rankingListVScroll: VirtualScrollView = null; // 排行榜虚拟列表 selfInfo: Node = null; // 自己的信息节点 // 当前排行榜 - 日周月切换 rankRadioDWM: Node = null; // 日周月单选按钮父节点 rankRadioDayBtn: Node = null; // 日榜单选按钮 rankRadioWeekBtn: Node = null; // 周榜单选按钮 rankRadioMonthBtn: Node = null; // 月榜单选按钮 rankingHistoryBtn: Node = null; // 历史记录按钮 rankingRewardBtn: Node = null; // 奖励按钮 // 活动未开启提示 activityNotOpenTip: Node = null; // 活动未开启提示 label currentRankType: string = ''; countdownTimer: any = null; // 存储定时器 availableTypes: Set = new Set(); // 可用的类型 // 回调函数 onShowHistory: (type: string) => void = null; // 跳转到历史记录 onShowReward: (type: string) => void = null; // 跳转到奖励列表 onClose: () => void = null; // 关闭主弹窗 init(rankListNode: Node) { this.rankList = rankListNode; this.msg_1 = this.rankList.getChildByName('msg').getChildByName('Mask').getChildByName('msg_1'); this.rankLoadingNode = this.rankList.getChildByName('list').getChildByName('loading'); this.rankingEndTime = this.rankList.getChildByName('endTime'); this.rankingListVScroll = this.rankList.getChildByName('list').getChildByName('vScroll').getComponent(VirtualScrollView); this.selfInfo = this.rankList.getChildByName('list').getChildByName('self'); this.rankRadioDWM = this.rankList.getChildByName('rankRadioDWM'); this.rankRadioDayBtn = this.rankRadioDWM.getChildByName('dayBtn'); this.rankRadioWeekBtn = this.rankRadioDWM.getChildByName('weekBtn'); this.rankRadioMonthBtn = this.rankRadioDWM.getChildByName('monthBtn'); this.rankingHistoryBtn = this.rankList.getChildByName('history'); let radioTitle = this.rankList.getChildByName('radioTitle'); this.rankingRewardBtn = radioTitle.getChildByName('rewardBtn'); this.activityNotOpenTip = this.rankList.getChildByName('list').getChildByName('activityNotOpenTip'); this.rankList.active = false; this.rankLoadingNode.active = false; this.activityNotOpenTip.active = false; this.rankingHistoryBtn.on(Node.EventType.TOUCH_END, this.onClickRankHistoryBtn, this); this.rankingRewardBtn.on(Node.EventType.TOUCH_END, this.onClickRewardBtn, this); this.rankRadioDayBtn.on(Node.EventType.TOUCH_END, this.onClickRankRadioDayBtn, this); this.rankRadioWeekBtn.on(Node.EventType.TOUCH_END, this.onClickRankRadioWeekBtn, this); this.rankRadioMonthBtn.on(Node.EventType.TOUCH_END, this.onClickRankRadioMonthBtn, this); } // ==================== 对外接口 ==================== async show(defaultType: string = 'day') { this.rankList.active = true; this.currentRankType = ''; this.updateRankButtonsAvailability(); this.refreshSelfRankingInfo(null); await this.switchRankTab(defaultType); } hide() { this.rankList.active = false; if (this.countdownTimer) { clearInterval(this.countdownTimer); this.countdownTimer = null; } // 停止跑马灯 if (this.msg_1 && this.msg_1.isValid) { Tween.stopAllByTarget(this.msg_1); this.msg_1.active = false; } this.marqueeTexts = []; this.currentMarqueeIndex = 0; } getCurrentType(): string { return this.currentRankType; } // ==================== 排行榜逻辑 ==================== isTypeAvailable(type: string): boolean { return this.availableTypes.has(type); } showActivityNotOpenTip() { this.activityNotOpenTip.active = true; this.rankingListVScroll.setTotalCount(0); this.rankingEndTime.getComponent(Label).string = "--:--:--"; } hideActivityNotOpenTip() { this.activityNotOpenTip.active = false; } async switchRankTab(type: string) { if (this.currentRankType === type) { return; } this.currentRankType = type; this.setRankRadioBtn(type); this.refreshSelfRankingInfo(null); if (!this.isTypeAvailable(type)) { console.log(`${type} 榜单活动未开启`); this.rankLoadingNode.active = false; if (this.msg_1 && this.msg_1.isValid) { this.msg_1.active = false; } this.marqueeTexts = []; this.currentMarqueeIndex = 0; this.showActivityNotOpenTip(); if (this.countdownTimer) { clearInterval(this.countdownTimer); this.countdownTimer = null; } return; } this.hideActivityNotOpenTip(); this.rankingListVScroll.setTotalCount(0); Tween.stopAllByTarget(this.rankLoadingNode); this.rankLoadingNode.active = true; tween(this.rankLoadingNode) .by(0.3, { angle: 360 }) .repeatForever() .start(); let currentRankInfo = SlotRankingDataManager.instance.getRankInfoByType(type); if (this.msg_1 && this.msg_1.isValid) { Tween.stopAllByTarget(this.msg_1); } this.setupMarquee(currentRankInfo); this.setRankingEndTime(type); let data = { Type: type, GameId: getGameId(), Date: this.getCurrentDateString(), } try { let rankInfos = await callGameApiForRank('getRankInfos', data); Tween.stopAllByTarget(this.rankLoadingNode); this.rankLoadingNode.active = false; this.openRankingPopup(rankInfos); } catch (error) { console.error('获取排行榜数据失败:', error); this.showActivityNotOpenTip(); Tween.stopAllByTarget(this.rankLoadingNode); if (this.countdownTimer) { clearInterval(this.countdownTimer); this.countdownTimer = null; } this.rankingEndTime.getComponent(Label).string = "--:--:--"; this.rankLoadingNode.active = false; // 停止跑马灯 if (this.msg_1 && this.msg_1.isValid) { Tween.stopAllByTarget(this.msg_1); this.msg_1.active = false; } this.marqueeTexts = []; this.currentMarqueeIndex = 0; } } // ==================== 跑马灯逻辑 ==================== setupMarquee(currentRankInfo: any) { if (!this.msg_1 || !this.msg_1.isValid) return; // 获取活动数据 let rewardsCount = currentRankInfo.Rewards?.length || 0; let spinLimit = currentRankInfo.SpinLimit || 0; let betLimit = currentRankInfo.BetLimit || 0; // 获取翻译文本并替换XX let text1 = I18nManager.instance.t('During the event, participate in designated games and rank in the top XX by winnings to receive generous rewards!'); let text2 = I18nManager.instance.t('Accumulate XX bets or a total wager of XX to participate in the event'); // 替换第一段文本中的XX为Rewards的length text1 = text1.replace('XX', rewardsCount.toString()); // 替换第二段文本中的两个XX(SpinLimit和BetLimit) text2 = text2.replace(/XX/, spinLimit.toString()).replace(/XX/, (betLimit / 10000).toString()); // 保存两段文本 this.marqueeTexts = [text1, text2]; this.currentMarqueeIndex = 0; // 显示跑马灯并播放第一段文本 this.msg_1.active = true; this.playMarquee(); } playMarquee() { if (!this.msg_1 || !this.msg_1.isValid || this.marqueeTexts.length === 0) return; let labelComp = this.msg_1.getComponent(Label); if (!labelComp) return; // 获取当前要显示的文本 let currentText = this.marqueeTexts[this.currentMarqueeIndex]; // 确保Label的overflow设置为NONE,以便完整显示文本 labelComp.overflow = Label.Overflow.NONE; labelComp.string = currentText; // 强制更新Label的渲染数据,确保宽度正确计算 labelComp.updateRenderData(true); // 等待下一帧更新UITransform this.scheduleOnce(() => { if (!this.msg_1 || !this.msg_1.isValid) return; // 再次强制更新以确保宽度准确 labelComp.updateRenderData(true); let labelLength = labelComp.node.getComponent(UITransform).width; // 设置初始位置 this.msg_1.setPosition(-450, this.msg_1.position.y, this.msg_1.position.z); let endX = -labelLength; let duration = Math.max(3, (-450 + labelLength) / 80); // 启动跑马灯动画 Tween.stopAllByTarget(this.msg_1); tween(this.msg_1) .delay(1) .to(duration, { position: v3(endX, this.msg_1.position.y, this.msg_1.position.z) }) .call(() => { // 切换到下一段文本 this.currentMarqueeIndex = (this.currentMarqueeIndex + 1) % this.marqueeTexts.length; // 播放下一段文本 this.playMarquee(); }) .start(); }, 0); } // ==================== 排行榜列表逻辑 ==================== openRankingPopup(rankInfos: any) { this.rankingListVScroll.setTotalCount(rankInfos.List.length); this.rankingListVScroll.renderItemFn = (node: Node, idx: number) => { let itemData = rankInfos.List[idx]; if (!itemData) { return; } node.getChildByName('bg').active = idx % 2 === 0; node.getChildByName('bg2').active = idx % 2 !== 0; node.getChildByName('Rank').getChildByName('sp_1').active = idx === 0; node.getChildByName('Rank').getChildByName('sp_2').active = idx === 1; node.getChildByName('Rank').getChildByName('sp_3').active = idx === 2; node.getChildByName('Rank').getChildByName('rankLab').active = idx >= 3; node.getChildByName('Rank').getChildByName('rankLab').getComponent(Label).string = itemData.Rank.toString(); node.getChildByName('Uid').getComponent(Label).string = truncateString(itemData.Uid); node.getChildByName('Shop').getComponent(Label).string = SlotRankingDataManager.instance.getRankListServerId() || '-'; node.getChildByName('Win').getComponent(Label).string = itemData.Bet.toString(); } this.rankingListVScroll.refreshList(rankInfos.List); this.refreshSelfRankingInfo(rankInfos.Self); } refreshSelfRankingInfo(selfInfo: any) { if (!selfInfo) { this.selfInfo.getChildByName('Rank').getChildByName('sp_1').active = false; this.selfInfo.getChildByName('Rank').getChildByName('sp_2').active = false; this.selfInfo.getChildByName('Rank').getChildByName('sp_3').active = false; this.selfInfo.getChildByName('Rank').getChildByName('rankLab').active = true; this.selfInfo.getChildByName('Rank').getChildByName('rankLab').getComponent(Label).string = '-'; this.selfInfo.getChildByName('Uid').getComponent(Label).string = '-'; this.selfInfo.getChildByName('Shop').getComponent(Label).string = '-'; this.selfInfo.getChildByName('Win').getComponent(Label).string = '-'; return; } this.selfInfo.getChildByName('Rank').getChildByName('sp_1').active = selfInfo.Rank === 1; this.selfInfo.getChildByName('Rank').getChildByName('sp_2').active = selfInfo.Rank === 2; this.selfInfo.getChildByName('Rank').getChildByName('sp_3').active = selfInfo.Rank === 3; this.selfInfo.getChildByName('Rank').getChildByName('rankLab').active = selfInfo.Rank >= 4; this.selfInfo.getChildByName('Rank').getChildByName('rankLab').getComponent(Label).string = selfInfo.Rank.toString(); if (selfInfo.Rank === -1) { this.selfInfo.getChildByName('Rank').getChildByName('rankLab').active = true; this.selfInfo.getChildByName('Rank').getChildByName('rankLab').getComponent(Label).string = '-'; } this.selfInfo.getChildByName('Uid').getComponent(Label).string = truncateString(selfInfo.Uid); this.selfInfo.getChildByName('Shop').getComponent(Label).string = SlotRankingDataManager.instance.getRankListServerId() || '-'; this.selfInfo.getChildByName('Win').getComponent(Label).string = selfInfo.Bet.toString(); } // ==================== 按钮状态管理 ==================== updateRankButtonsAvailability() { let rankList = SlotRankingDataManager.instance.rankList; this.availableTypes.clear(); if (rankList && rankList.List) { for (let item of rankList.List) { this.availableTypes.add(item.Type); } } this.updateButtonAvailability(this.rankRadioDayBtn); this.updateButtonAvailability(this.rankRadioWeekBtn); this.updateButtonAvailability(this.rankRadioMonthBtn); } updateButtonAvailability(btn: Node) { btn.active = true; let button = btn.getComponent(Button); if (button) { button.interactable = true; } let check = btn.getChildByName('check'); let unCheck = btn.getChildByName('unCheck'); let whiteColor = Color.WHITE; if (check) { let checkSprite = check.getComponent(Sprite); if (checkSprite) { checkSprite.color = whiteColor; } } if (unCheck) { let unCheckSprite = unCheck.getComponent(Sprite); if (unCheckSprite) { unCheckSprite.color = whiteColor; } } } setRankRadioBtn(type: string) { let selectedColor = new Color(219, 180, 180, 255); let unselectedColor = new Color(197, 255, 175, 255); this.setRadioBtnState(this.rankRadioDayBtn, type === 'day', selectedColor, unselectedColor); this.setRadioBtnState(this.rankRadioWeekBtn, type === 'week', selectedColor, unselectedColor); this.setRadioBtnState(this.rankRadioMonthBtn, type === 'month', selectedColor, unselectedColor); } setRadioBtnState(btn: Node, isSelected: boolean, selectedColor: Color, unselectedColor: Color) { let check = btn.getChildByName('check'); let unCheck = btn.getChildByName('unCheck'); let label = btn.getChildByName('label'); let palette = label?.getComponent(Palette); if (isSelected) { check.active = true; unCheck.active = false; if (palette) { palette.colorLB = selectedColor; palette.colorRB = selectedColor; } } else { check.active = false; unCheck.active = true; if (palette) { palette.colorLB = unselectedColor; palette.colorRB = unselectedColor; } } } // ==================== 倒计时功能 ==================== setRankingEndTime(type: string) { if (this.countdownTimer) { clearInterval(this.countdownTimer); this.countdownTimer = null; } let endTime = SlotRankingDataManager.instance.getRankEndTimeByType(type); if (!endTime) { this.rankingEndTime.getComponent(Label).string = "--:--:--"; return; } this.updateCountdown(endTime); this.countdownTimer = setInterval(() => { this.updateCountdown(endTime); }, 1000); } // 1. 客户端可定时通过HTTP请求轮询服务器,定期拉取活动状态或结束时间(如每分钟或每次切换排行榜时),如果发现状态变为停止,则立即处理。 // 2. 在每次需要用到活动信息前(如展示排行榜、下注等),都重新请求活动状态信息,避免缓存已过期的信息。 // 3. 可以在服务器接口响应中增加“活动状态”字段,每次请求都返回当前的活动状态(如active、stopped),客户端据此判定。 // 4. 如有WebSocket等实时通道,可让服务器主动推送“活动已结束”通知,客户端收到后立刻更新活动状态并做相应提示。 // 5. 若客户端检测到接口数据异常(如活动相关接口返回错误代码、空列表或特定标记等),也可判定活动可能已停止并刷新状态。 updateCountdown(endTime: number) { let now = Math.floor(Date.now() / 1000); let remainingSeconds = endTime - now; if (remainingSeconds <= 0) { this.rankingEndTime.getComponent(Label).string = "--:--:--"; if (this.countdownTimer) { clearInterval(this.countdownTimer); this.countdownTimer = null; } return; } let timeStr = this.formatCountdown(remainingSeconds); this.rankingEndTime.getComponent(Label).string = I18nManager.instance.t('Countdown to end') + ' : ' + timeStr; } formatCountdown(seconds: number): string { let days = Math.floor(seconds / 86400); let hours = Math.floor((seconds % 86400) / 3600); let minutes = Math.floor((seconds % 3600) / 60); let secs = seconds % 60; if (days >= 1) { return `${days}d ${hours}h ${minutes}min ${secs}s`; } else { return `${hours}h ${minutes}min ${secs}s`; } } // ==================== 按钮事件 ==================== onClickRankRadioDayBtn() { AudioManager.instance.playSFX("Common_Button_Click"); this.switchRankTab('day'); } onClickRankRadioWeekBtn() { AudioManager.instance.playSFX("Common_Button_Click"); this.switchRankTab('week'); } onClickRankRadioMonthBtn() { AudioManager.instance.playSFX("Common_Button_Click"); this.switchRankTab('month'); } onClickRankHistoryBtn() { AudioManager.instance.playSFX("Common_Button_Click"); this.onShowHistory?.call(null, this.currentRankType || 'day'); } onClickRewardBtn() { AudioManager.instance.playSFX("Common_Button_Click"); this.onShowReward?.call(null, this.currentRankType || 'day'); } closeRankingPopup() { this.onClose?.call(null); } // ==================== 工具方法 ==================== getCurrentDateString(): string { let now = new Date(); let year = now.getFullYear(); let month = (now.getMonth() + 1).toString(); let day = now.getDate().toString(); if (month.length < 2) month = '0' + month; if (day.length < 2) day = '0' + day; return `${year}${month}${day}`; } onDestroy() { clearInterval(this.countdownTimer); this.countdownTimer = null; this.rankingHistoryBtn.off(Node.EventType.TOUCH_END, this.onClickRankHistoryBtn, this); this.rankingRewardBtn.off(Node.EventType.TOUCH_END, this.onClickRewardBtn, this); this.rankRadioDayBtn.off(Node.EventType.TOUCH_END, this.onClickRankRadioDayBtn, this); this.rankRadioWeekBtn.off(Node.EventType.TOUCH_END, this.onClickRankRadioWeekBtn, this); this.rankRadioMonthBtn.off(Node.EventType.TOUCH_END, this.onClickRankRadioMonthBtn, this); } }