All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 48s
506 lines
20 KiB
TypeScript
506 lines
20 KiB
TypeScript
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<string> = 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.Win.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.Win.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);
|
||
}
|
||
}
|