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.
AdLike2048/assets/script/ui/Rank/UIRank.ts

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;
}
}