You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
315 lines
11 KiB
315 lines
11 KiB
|
3 weeks ago
|
|
||
|
|
//*********************
|
||
|
|
// 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<void> {
|
||
|
|
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<void> {
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
}
|