import { _decorator, Component, Node, Layout, sp, Tween, tween, UIOpacity, UIRenderer, Vec3 } from 'cc'; import { UIInfo } from './UIManager'; import { ResPakage } from '../res/ResourcesManager'; import { IPopAnimConfig, IPopAnimSession, IPopRevealConfig, PopCenterPreset, PopMaskPreset, popStaggerNode, PopStaggerPreset, preparePopReveal, runPopReveal, stopPopStagger, POP_REVEAL_START_DELAY, } from './popAnim'; const { ccclass } = _decorator; @ccclass('UIBase') export class UIBase extends Component { getResPkg(): ResPakage { return null; } onLoad(): void { } uiInfo: UIInfo = null; prema: any; private _onOpenAnimEnd: Function; private _onClose: Function; private _showLoading: number = 0; private _popSession: IPopAnimSession = { mask: 'none', center: 'none' }; private _popOpenOnceDone = false; /** onInit 里动态创建的待分帧节点(center 入场完后播放) */ private _pendingRevealNodes: Node[] = []; private _pendingRevealPreset: PopStaggerPreset = 'scale'; /** 内容区入场完成,动态项可立即错峰 */ private _popRevealReady = false; private _revealSeq = 0; /** popRevealEnqueue 登记过的节点,用于跳过动效时一次性显示 */ private _revealTrackNodes: Node[] = []; /** 道具/列表项错峰动效是否已全部播完(含跳过) */ private _itemRevealComplete = true; /** 内部 reveal 是否已启动(与 center 弹入重叠,不等 center 结束) */ private _revealBegun = false; private _pendingRevealCfg: IPopAnimConfig | null = null; private _markItemRevealComplete = (): void => { this._itemRevealComplete = true; }; private _beginItemRevealIfNeeded = (): void => { if (this._revealBegun) return; const cfg = this._pendingRevealCfg; if (!cfg) return; this._revealBegun = true; this._runRevealFromConfig(cfg); }; private _scheduleBeginItemReveal(cfg: IPopAnimConfig): void { this._pendingRevealCfg = cfg; this.unschedule(this._beginItemRevealIfNeeded); const center = cfg.center ?? 'none'; const delay = center === 'none' ? 0.02 : POP_REVEAL_START_DELAY; this.scheduleOnce(this._beginItemRevealIfNeeded, delay); } private _resolveRevealConfig(cfg: IPopAnimConfig): IPopRevealConfig | null { if (cfg.reveal) return cfg.reveal; if (cfg.stagger && cfg.staggerNode) { return { itemsNode: cfg.staggerNode, itemsPreset: cfg.stagger }; } return null; } /** 上屏前同步初始姿态(遮罩透明、内容区 t=0、列表项隐藏) */ preparePopOpenPose(): void { const cfg = this.uiInfo?.uiconf?.popAnim as IPopAnimConfig | undefined; if (!cfg) return; if (cfg.once && this._popOpenOnceDone) return; this._popRevealReady = false; this._revealSeq = 0; this._revealTrackNodes = []; this._itemRevealComplete = true; this._revealBegun = false; this._pendingRevealCfg = null; this.unschedule(this._markItemRevealComplete); this.unschedule(this._beginItemRevealIfNeeded); gg.ui.popAnim.prepareOpen( this.node, cfg.mask ?? 'none', cfg.center ?? 'none', ); const reveal = this._resolveRevealConfig(cfg); if (reveal) { const center = gg.ui.popAnim.resolveCenterNode(this.node); preparePopReveal(this.node, center, reveal); } } /** UIManager 在 onInit 前调用(含复开) */ beforePanelInit(): void { this.preparePopOpenPose(); this.playPopFromConfig(); } /** * 写入本次打开参数(含已打开再开)。 * @param args 打开参数 */ applyOpenParam(args?: OpenParam): void { this.prema = args?.data; this._onOpenAnimEnd = args?.onOpenAnimEnd; this._onClose = args?.onClose; if (args?.showLoading !== undefined && args?.showLoading !== null) { this._showLoading = args.showLoading; } } /** * 通过UI管理器打开面板时才会触发onAdded * @param args */ onAdded(args: OpenParam): void | Promise { this.applyOpenParam(args); let resPkg = this.getResPkg(); if (resPkg) { return new Promise((resolve) => { let settled = false; const settle = (): void => { if (settled) return; settled = true; gg.game.hideLoading(); resolve(); }; // 资源包挂起/onInit 抛错都会让 openPanel inflight 永不结束 → 同 id 再也打不开 const guard = setTimeout(() => { console.error('[UIBase] loadResPakg/onAdded timeout', this.node?.name); if (this.isValid && this.node?.isValid) { try { this.beforePanelInit(); void Promise.resolve(this.onInit()).catch((e) => { console.error('[UIBase] onInit after pkg timeout', this.node?.name, String(e)); }); } catch (e) { console.error('[UIBase] onInit after pkg timeout', this.node?.name, String(e)); } } settle(); }, 30000); try { gg.res.loadResPakg(resPkg, () => { }, () => { clearTimeout(guard); if (!this.isValid || !this.node?.isValid) { settle(); return; } void (async () => { try { this.beforePanelInit(); await Promise.resolve(this.onInit()); } catch (e) { console.error('[UIBase] onInit failed', this.node?.name, String(e)); } finally { settle(); } })(); }); } catch (e) { console.error('[UIBase] loadResPakg throw', this.node?.name, String(e)); clearTimeout(guard); settle(); } }); } this.beforePanelInit(); return Promise.resolve(this.onInit()); } onInit(): void | Promise { } // ─── 弹窗动效库(按需组合,无 options 对象) ─── /** 遮罩入场 */ popMask(preset: PopMaskPreset = 'dim'): void { if (gg.ui.popAnim.openMask(this.node, preset)) { this._popSession.mask = preset; } } /** 自定义入场动效结束后通知 UIManager(子类勿直接访问 _onOpenAnimEnd) */ protected notifyOpenAnimEnd(): void { this._onOpenAnimEnd?.(); } /** 面板关闭回调(子类 destroyPanel 复写时调用) */ protected notifyPanelClose(): void { this._onClose?.(); } /** 内容区入场 */ popCenter(preset: PopCenterPreset = 'scale', callback?: () => void): void { const center = gg.ui.popAnim.resolveCenterNode(this.node); const done = () => { callback?.(); this._onOpenAnimEnd?.(); }; if (center) { gg.ui.popAnim.prepareOpen(this.node, 'none', preset); } if (center && gg.ui.popAnim.openCenter(center, preset, done)) { this._popSession.center = preset; return; } done(); } /** 读取 ConfigRes.popAnim 自动播放入场 */ playPopFromConfig(): void { const cfg = this.uiInfo?.uiconf?.popAnim as IPopAnimConfig | undefined; if (!cfg) return; if (cfg.once && this._popOpenOnceDone) return; const mask = cfg.mask ?? 'none'; const center = cfg.center ?? 'none'; if (mask !== 'none') this.popMask(mask); const onCenterDone = () => { if (cfg.once) this._popOpenOnceDone = true; this._onOpenAnimEnd?.(); }; this._scheduleBeginItemReveal(cfg); if (center !== 'none') { this.popCenter(center, onCenterDone); } else { onCenterDone(); } } private _runRevealFromConfig(cfg: IPopAnimConfig): void { const reveal = this._resolveRevealConfig(cfg); const center = gg.ui.popAnim.resolveCenterNode(this.node); if (reveal) { runPopReveal(this.node, center, reveal); } this._flushPendingReveal(); this._popRevealReady = true; this.onPopOpenRevealEnd(); } /** 面板入场 + reveal 配置播完后回调(虚拟列表在此后停止入场动效) */ protected onPopOpenRevealEnd(): void { } /** 道具/列表项错峰动效是否仍在播放 */ protected isItemRevealPlaying(): boolean { return !this._itemRevealComplete; } /** * 跳过面板入场与道具错峰动效,全部立即显示(不关闭面板) * 用于获得页:点遮罩先跳过动画,再点才关闭 */ protected skipPopReveal(): void { this.unschedule(this._markItemRevealComplete); this.unschedule(this._beginItemRevealIfNeeded); this._revealBegun = true; gg.ui.popAnim.snapCenterOpen(this.node); const reveal = this._resolveRevealConfig( (this.uiInfo?.uiconf?.popAnim as IPopAnimConfig | undefined) ?? {}, ); if (reveal) { const center = gg.ui.popAnim.resolveCenterNode(this.node); const names = [ ...(reveal.itemNodes ?? []), ...(reveal.lastNodes ?? []), ]; if (reveal.itemsNode) { const container = center?.getChildByName(reveal.itemsNode) ?? this.node.getChildByName(reveal.itemsNode); container?.children.forEach((c) => { if (c.active) stopPopStagger(c); }); } for (const name of names) { const n = center?.getChildByName(name) ?? this.node.getChildByName(name); if (n?.isValid) stopPopStagger(n); } } for (const n of this._revealTrackNodes) { if (n?.isValid) stopPopStagger(n); } for (const n of this._pendingRevealNodes) { if (n?.isValid) stopPopStagger(n); } this._pendingRevealNodes = []; this._popRevealReady = true; this._itemRevealComplete = true; this.onPopOpenRevealEnd(); } private _armItemRevealCompleteTimer(): void { this.unschedule(this._markItemRevealComplete); const total = Math.max(this._revealTrackNodes.length, this._revealSeq); if (total === 0) { this._itemRevealComplete = true; return; } this._itemRevealComplete = false; const delay = (total - 1) * 0.04 + 0.22; this.scheduleOnce(this._markItemRevealComplete, delay); } private _flushPendingReveal(): void { const nodes = this._pendingRevealNodes; if (!nodes.length) return; this._pendingRevealNodes = []; const preset = this._pendingRevealPreset; nodes.forEach((n, i) => { if (n?.isValid) popStaggerNode(n, preset, i * 0.04); }); this._revealSeq = nodes.length; this._armItemRevealCompleteTimer(); } /** 动态列表项:先隐藏,与 center 弹入重叠错峰出现(在 onInit instantiate 后调用) */ popRevealEnqueue(node: Node, preset: PopStaggerPreset = 'scale'): void { if (!node?.isValid) return; if (!this._revealTrackNodes.includes(node)) { this._revealTrackNodes.push(node); } this._itemRevealComplete = false; if (preset === 'scale' || preset === 'celebrate') { node.setScale(Vec3.ZERO); } else { node.setScale(Vec3.ONE); const op = node.getComponent(UIOpacity) || node.addComponent(UIOpacity); op.opacity = 0; } if (this._popRevealReady) { popStaggerNode(node, preset, this._revealSeq * 0.04); this._revealSeq++; this._armItemRevealCompleteTimer(); return; } this._pendingRevealNodes.push(node); this._pendingRevealPreset = preset; } /** 容器子节点错峰 */ popStagger(container: Node, preset: PopStaggerPreset = 'scale'): void { const children = container.children.filter(c => c.active); children.forEach((child, i) => { popStaggerNode(child, preset, i * 0.04); }); } /** 单个节点错峰(立即播放,不等待 center) */ popStaggerItem(node: Node, index: number, preset: PopStaggerPreset = 'scale'): void { popStaggerNode(node, preset, index * 0.04); } // ─── 兼容旧 API(内部委托 popAnim 库) ─── showOpenCenterEff(callback?: () => void) { this.popCenter('scale', callback); } showOpenCenterEffCute(callback?: () => void) { this.popCenter('bounce', callback); } showCloseCenterEff(type = 1, callback?: () => void) { const center = gg.ui.popAnim.resolveCenterNode(this.node); if (!center) { callback?.(); return false; } if (type === 1) { return gg.ui.popAnim.closeCenter(center, 'scale', callback); } if (type === 2) { Tween.stopAllByTarget(center); tween(center).to(0.2, { opacity: 0 }).set({ opacity: 255 }).call(() => callback?.()).start(); return true; } callback?.(); return false; } showCloseCenterEffCute(callback?: () => void) { const center = gg.ui.popAnim.resolveCenterNode(this.node); if (!center) { callback?.(); return false; } return gg.ui.popAnim.closeCenter(center, 'bounce', callback); } showOpenMaskEff(type = 1) { if (type === 1) this.popMask('root'); else this.popMask('dim'); } showOpenMaskEffCute() { this.popMask('dimFast'); } closeMaskEff(callback?: () => void) { gg.ui.popAnim.closeMask(this.node, this._popSession.mask, callback); return this._popSession.mask !== 'none'; } async close(tweenType = 1) { await gg.ui.closePanel(this.uiInfo.id, tweenType); } /** 关页/切场景前由 UIManager 或 GameController 调用:中止仍依赖有效资源的子表现 */ teardownBeforeDestroy(): void { } /** 销毁前断开渲染与 Spine,避免 bundle 释放后 SkeletonSystem 仍 update */ protected prepareDestroyForRender(): void { if (!this.node?.isValid) return; // 先停渲染,再 active=false(否则 SpriteLoad.onDisable 会 decRef,脏列表仍 updateUVs) const renderers = this.node.getComponentsInChildren(UIRenderer); for (const r of renderers) { if (r?.isValid) r.enabled = false; } this.node.active = false; const stopTweenDeep = (n: Node) => { if (!n?.isValid) return; Tween.stopAllByTarget(n); for (const c of n.children) { stopTweenDeep(c); } }; stopTweenDeep(this.node); const layouts = this.node.getComponentsInChildren(Layout); for (const layout of layouts) { if (layout?.isValid && layout.enabled) { layout.enabled = false; } } const sks = this.node.getComponentsInChildren(sp.Skeleton); for (const sk of sks) { if (sk?.isValid) { sk.enabled = false; try { const skAny = sk as sp.Skeleton & { isAnimationCached?: () => boolean }; if (typeof skAny.isAnimationCached !== 'function' || !skAny.isAnimationCached()) { sk.clearTracks(); } sk.setCompleteListener(null); } catch (_) { /* wasm 已释放等 */ } } } } destroyPanel(tweenType = 1) { return new Promise((resolve) => { const finish = () => { this.prepareDestroyForRender(); if (this.node.isValid) { this.node.destroy(); } resolve(true); this.notifyPanelClose(); }; if (tweenType === 2 || tweenType === 3) { gg.ui.popAnim.playClose(this.node, this._popSession, tweenType, finish); return; } finish(); }); } protected onDestroy(): void { let resPkg = this.getResPkg(); if (resPkg) { gg.res.clearResPkg(resPkg.name); } this.prema = null; this._onOpenAnimEnd = null; this._onClose = null; this.uiInfo = null; this._popSession = { mask: 'none', center: 'none' }; this._popOpenOnceDone = false; this._popRevealReady = false; this._revealSeq = 0; this._pendingRevealNodes = []; this._revealTrackNodes = []; this.unschedule(this._markItemRevealComplete); this.unschedule(this._beginItemRevealIfNeeded); this._revealBegun = false; this._pendingRevealCfg = null; } } /** * 打开面板参数 */ export interface OpenParam { onOpenAnimEnd?: Function; onClose?: Function; data?: any; showLoading?: number; }