//********************* // create by 流云 // time: Fri Aug 14 2026 // desc: 排行榜(省榜 / 总榜 / 每日胜场榜) //********************* import { _decorator, EventHandler, find, Layout, Node, Vec2, Widget } from 'cc'; import { Auto_Rank } from './Auto_Rank'; import List from 'db://assets/mx/components/list/List'; import { RankBoardEntry, RankBoardManager, RankBoardResult, RankBoardScope } from '../../manager/RankBoardManager'; import { RankItemView } from './RankItemView'; import { DailyRankDataManager } from '../../manager/DailyRankDataManager'; import { RankReport } from '../../manager/RankReport'; const { ccclass, property } = _decorator; /** 虚拟列表每帧渲染条数(榜单最多 100 条) */ const LIST_FRAME_RENDER_NUM = 3; @ccclass('UIRank') export class UIRank extends Auto_Rank { /** sv_list 上挂的 List(虚拟列表 + 分帧渲染) */ @property(List) listV: List = null; private _scope: RankBoardScope = RankBoardScope.Total; private _boardList: RankBoardEntry[] = []; /** 滚动复位剩余重试次数 */ private _scrollHeadRetries = 0; /** 加载序号:切换页签时丢弃过期结果 */ private _reloadSeq = 0; /** 可选授权按钮(prefab 未挂时不为 null 才绑定) */ private _btnAuthorize: Node | null = null; onLoad(): void { super.onLoad(); this._btnAuthorize = find('ui_center/btnAuthorize', this.node); this.ui_rankItem.active = false; if (!this.listV) { this.listV = this.sv_list?.node?.getComponent(List) ?? null; } if (this.listV) { this.listV.virtual = true; this.listV.frameByFrameRenderNum = LIST_FRAME_RENDER_NUM; const content = this.listV.scrollView?.content; // 虚拟列表由 List 自算 content 尺寸与 item 坐标,必须关掉 Widget/Layout const widget = content?.getComponent(Widget); if (widget) { widget.enabled = false; } const layout = content?.getComponent(Layout); if (layout) { layout.enabled = false; } } this._bindListRenderEvent(); // 已有预拉取缓存时不闪遮罩 this._setLoadingVisible(!RankBoardManager.getBoardCache(this._scope)); } onEnable(): void { super.onEnable(); this._btnAuthorize?.on(Node.EventType.TOUCH_END, this.click_btnAuthorize, this); if (this._boardList.length > 0) { this._applyListItems(); this._setLoadingVisible(false); } } onDisable(): void { this._btnAuthorize?.off(Node.EventType.TOUCH_END, this.click_btnAuthorize, this); super.onDisable(); this.unschedule(this._tryScrollListToHead); this._scrollHeadRetries = 0; } /** 弹窗入场动画结束后,ScrollView 可视区才稳定,需再次滚到顶部 */ protected onPopOpenRevealEnd(): void { if (this._boardList.length > 0) { this._scrollListToHead(); } } onInit(): void { RankBoardManager.markRankEntered(); RankReport.rankEnter(); const hadProfile = RankBoardManager.hasRankProfile(); const hadProvince = !!RankBoardManager.getSavedProvince(); // 首次弹用户信息授权;随后申请定位(不在进首页申请)。不阻塞打开与列表渲染。 void RankBoardManager.tryPromptRankAuthOnFirstOpen() .then(() => RankBoardManager.ensureProvinceOnRankOpen()) .then((province) => { if (!this.isValid) return; this._refreshAuthorizeButton(); // 仅资料/省份刚拿到时才强刷,避免暖缓存下每次打开都重拉 const profileGot = !hadProfile && RankBoardManager.hasRankProfile(); const provinceGot = !!province && !hadProvince; if (profileGot || provinceGot) { void this._reloadBoard({ force: true }); } }); this._scope = RankBoardScope.Total; this._refreshTabButtons(); this._refreshAuthorizeButton(); void this._reloadBoard(); } click_btnRank1(): void { if (this._scope === RankBoardScope.Province) return; void this._trySwitchProvince(); } click_btnRank2(): void { this._switchScope(RankBoardScope.Total); } click_btnRank3(): void { this._switchScope(RankBoardScope.DailyWins); } /** * 切省榜:缺用户信息或定位时会拉授权/打开设置;均就绪才切 Tab。 */ private async _trySwitchProvince(): Promise { console.warn(`[RankDebug] 点击省榜 hasProfile=${RankBoardManager.hasRankProfile()} province="${RankBoardManager.getSavedProvince()}"`); if (!RankBoardManager.hasRankProfile()) { const ok = await RankBoardManager.ensureRankUserAuth(); console.warn(`[RankDebug] ensureRankUserAuth 返回=${ok}`); if (!this.isValid || !ok) return; } if (!RankBoardManager.getSavedProvince()) { await RankBoardManager.ensureProvinceWithAuth(); console.warn(`[RankDebug] ensureProvinceWithAuth 后 province="${RankBoardManager.getSavedProvince()}"`); if (!this.isValid) return; } const province = RankBoardManager.getSavedProvince(); if (!province) { gg.ui.showToast( gg.lang.get('需要授权定位才能查看省榜') || '需要授权定位才能查看省榜', false, ); return; } this._switchScope(RankBoardScope.Province); } click_btnAuthorize(): void { const authP = RankBoardManager.ensureRankUserAuth(); void authP.then((ok) => { if (!ok || !this.isValid) return; this._refreshAuthorizeButton(); void this._reloadBoard({ force: true }); }); } click_btnClose(): void { this.close(2); } click_bgMask(): void { this.close(2); } onListRender(item: Node, index: number): void { if (!item?.isValid) return; item.active = true; const data = this._boardList[index]; if (!data) return; RankItemView.bind(item, data, this._scope); } private _bindListRenderEvent(): void { const list = this.listV; if (!list?.isValid) return; const eh = new EventHandler(); eh.target = this.node; eh.component = 'UIRank'; eh.handler = 'onListRender'; list.renderEvent = eh; } private _applyListItems(): void { const list = this.listV; if (!list?.isValid) return; const count = this._boardList.length; const apply = () => { if (!list.isValid || !this.isValid) return; if (!list.checkInited(false)) { this.scheduleOnce(apply, 0); return; } list.numItems = count; this._scrollListToHead(); }; apply(); } /** 布局与弹窗动画稳定后滚到榜首(index 0 = 第 1 名) */ private _scrollListToHead(): void { this.unschedule(this._tryScrollListToHead); this._scrollHeadRetries = 5; this.scheduleOnce(this._tryScrollListToHead, 0); } private _tryScrollListToHead = (): void => { const list = this.listV; if (!this.isValid || !list?.isValid) { this._scrollHeadRetries = 0; return; } if (!list.checkInited(false) || list.numItems <= 0) { this._scrollHeadRetries--; if (this._scrollHeadRetries > 0) { this.scheduleOnce(this._tryScrollListToHead, 0); } return; } const sv = list.scrollView; const content = sv?.content; if (!sv?.isValid || !content?.isValid) { this._scrollHeadRetries = 0; return; } // 虚拟列表禁止走 Layout.updateLayout,否则会打乱 List 自算坐标 sv.stopAutoScroll(); sv.scrollToOffset(new Vec2(0, 0), 0); list.scrollTo(0, 0); this._scrollHeadRetries--; if (this._scrollHeadRetries > 0) { this.scheduleOnce(this._tryScrollListToHead, 0); } }; private async _reloadBoard(opts?: { force?: boolean }): Promise { if (!this.isValid) return; const scope = this._scope; const requestId = ++this._reloadSeq; try { const cached = !opts?.force ? RankBoardManager.getBoardCache(scope) : null; if (cached) { if (!this.isValid || requestId !== this._reloadSeq) return; this._applyBoardResult(cached); return; } if (this._boardList.length <= 0) { this._setLoadingVisible(true); } const result = await RankBoardManager.getBoard(scope, { force: !!opts?.force }); if (!this.isValid || requestId !== this._reloadSeq) return; this._applyBoardResult(result); } catch (e) { console.error('[UIRank] reloadBoard', String(e)); if (requestId === this._reloadSeq) { this._setLoadingVisible(false); } } } private _applyBoardResult(result: RankBoardResult): void { this._boardList = result.list ?? []; this._applyListItems(); this._setLoadingVisible(false); if (result.self) { this.ui_selfRank.active = true; RankItemView.bind(this.ui_selfRank, result.self, this._scope, true); if (this._scope === RankBoardScope.Province) { DailyRankDataManager.recordProvinceBoardRank( Math.max(0, Math.floor(Number(result.self.rank) || 0)), ); } } } private _setLoadingVisible(show: boolean): void { const node = this.ui_loading; if (node?.isValid) node.active = show; } private _switchScope(scope: RankBoardScope): void { if (this._scope === scope) return; this._scope = scope; this._refreshTabButtons(); void this._reloadBoard(); } private _refreshTabButtons(): void { this._setTabSelected(this.btnRank1, this._scope === RankBoardScope.Province); this._setTabSelected(this.btnRank2, this._scope === RankBoardScope.Total); this._setTabSelected(this.btnRank3, this._scope === RankBoardScope.DailyWins); } /** 已授权则隐藏授权按钮 */ private _refreshAuthorizeButton(): void { if (!this._btnAuthorize?.isValid) return; this._btnAuthorize.active = !RankBoardManager.hasRankProfile(); } private _setTabSelected(btn: Node, selected: boolean): void { if (!btn?.isValid) return; const bg = btn.find('bg'); const select = btn.find('select'); const font = btn.find('font'); const fontSelect = btn.find('fontSelect'); if (bg?.isValid) bg.active = !selected; if (select?.isValid) select.active = selected; if (font?.isValid) font.active = !selected; if (fontSelect?.isValid) fontSelect.active = selected; } }