import { MiniGamePf } from '../../../mx/tools/MiniGameHostInfo'; import { sdkConfig } from '../sdkConfig'; /** vivo 广告组件(Banner / 原生模板 / 退出浮层) */ interface VivoAdInst { /** 展示 */ show: () => Promise; /** 销毁。实例不能复用 */ destroy?: () => void; /** 加载或展示失败 */ onError?: (cb: (err: unknown) => void) => void; /** 用户关闭 */ onClose?: (cb: () => void) => void; /** 取消错误监听 */ offError?: (cb: (err: unknown) => void) => void; /** 取消关闭监听 */ offClose?: (cb: () => void) => void; } /** * vivo 小游戏广告。仅 chanId 为 vivo 时生效。 * Banner:qg.createBannerAd。原生模板与退出浮层:qg.createCustomAd。参数用 posId。 */ export class VivoAd { /** 大厅 Banner */ private static _banner: VivoAdInst | null = null; /** 结算原生模板 */ private static _native: VivoAdInst | null = null; /** 退出浮层 */ private static _exit: VivoAdInst | null = null; /** 退出浮层关闭或失败后的继续回调 */ private static _exitDone: (() => void) | null = null; /** 当前尝试的退出浮层下标 */ private static _exitIndex = 0; /** 退出浮层已结束,避免 onError 与 show 失败重复回调 */ private static _exitSettled = false; /** 刚展示过退出浮层,下一次原生模板跳过,避免退出后立刻再叠一条 */ private static _blockNative = false; /** 当前配置是否 vivo 渠道 */ static isVivo(): boolean { return sdkConfig.config.chanId === 'vivo'; } /** 进入大厅时展示底部 Banner */ static showBanner(): void { if (!this.isVivo()) return; const posId = this._firstId(sdkConfig.config.platformConf?.banner); if (!posId) { console.warn('[VivoAd] 未配置 banner posId'); return; } const qg = this._qg(); if (!qg) return; if (typeof qg.createBannerAd !== 'function') { console.warn('[VivoAd] 宿主无 createBannerAd'); return; } this._drop(this._banner, this._onBannerError, undefined); this._banner = null; let ad: VivoAdInst | null = null; try { ad = qg.createBannerAd({ posId, style: {} }) ?? null; } catch (e) { console.warn('[VivoAd] createBannerAd 失败', String(e)); return; } if (!ad?.show) { console.warn('[VivoAd] createBannerAd 返回空'); return; } this._banner = ad; ad.onError?.(this._onBannerError); this._callShow(ad, this._onBannerError); } /** 离开大厅时销毁 Banner */ static hideBanner(): void { this._drop(this._banner, this._onBannerError, undefined); this._banner = null; } /** * 结算页展示原生模板(顶部,避免挡住领取按钮)。 * @returns 是否已发起展示 */ static showNative(): boolean { if (!this.isVivo()) return false; if (this._blockNative) { this._blockNative = false; return false; } const posId = this._firstId(sdkConfig.config.platformConf?.nativeAd); if (!posId) { console.warn('[VivoAd] 未配置原生模板 posId'); return false; } const ad = this._createCustom(posId, { gravity: 'top' }, this._onNativeError, undefined); if (!ad) return false; this._drop(this._native, this._onNativeError, undefined); this._native = ad; this._callShow(ad, this._onNativeError); return true; } /** 离开结算页时销毁原生模板 */ static hideNative(): void { this._drop(this._native, this._onNativeError, undefined); this._native = null; } /** * 展示退出浮层。非 vivo、无广告位或宿主不支持时立即回调。 * @param onDone 关闭、失败或未展示 */ static showExit(onDone: () => void): void { const ids = this._ids(sdkConfig.config.platformConf?.exitAd); if (!this.isVivo() || ids.length === 0) { if (this.isVivo() && ids.length === 0) console.warn('[VivoAd] 未配置退出浮层 posId'); onDone(); return; } if (this._exitDone) { console.warn('[VivoAd] 退出浮层展示中'); onDone(); return; } this._exitDone = onDone; this._exitIndex = 0; this._exitSettled = false; this._blockNative = true; this._openExit(); } /** Banner 失败 */ private static _onBannerError = (err: unknown): void => { console.warn('[VivoAd] banner', VivoAd._errText(err)); }; /** 原生模板失败 */ private static _onNativeError = (err: unknown): void => { console.warn('[VivoAd] native', VivoAd._errText(err)); }; /** 退出浮层失败,换下一条 */ private static _onExitFail = (err: unknown): void => { if (VivoAd._exitSettled || !VivoAd._exit) return; console.warn('[VivoAd] 退出浮层', VivoAd._errText(err)); VivoAd._nextExit(); }; /** 用户关闭退出浮层 */ private static _onExitClose = (): void => { VivoAd._finishExit(); }; /** 创建并展示当前下标的退出浮层 */ private static _openExit(): void { const ids = this._ids(sdkConfig.config.platformConf?.exitAd); const posId = ids[this._exitIndex]; if (!posId) { this._finishExit(); return; } const ad = this._createCustom(posId, undefined, this._onExitFail, this._onExitClose); if (!ad) { this._nextExit(); return; } this._drop(this._exit, this._onExitFail, this._onExitClose); this._exit = ad; this._callShow(ad, this._onExitFail); } /** 当前退出浮层失败,尝试下一条 */ private static _nextExit(): void { if (this._exitSettled) return; this._drop(this._exit, this._onExitFail, this._onExitClose); this._exit = null; this._exitIndex++; const ids = this._ids(sdkConfig.config.platformConf?.exitAd); if (this._exitIndex < ids.length) { this._openExit(); return; } this._finishExit(); } /** 退出浮层流程结束 */ private static _finishExit(): void { if (this._exitSettled) return; this._exitSettled = true; const done = this._exitDone; this._exitDone = null; this._drop(this._exit, this._onExitFail, this._onExitClose); this._exit = null; done?.(); } /** * 创建原生模板。style 不传则用广告位后台样式。 * @param posId 广告位 * @param style 位置,结算页用 top * @param onError 错误回调 * @param onClose 关闭回调 */ private static _createCustom(posId: string, style: { gravity?: string } | undefined, onError: (err: unknown) => void, onClose: (() => void) | undefined): VivoAdInst | null { const qg = this._qg(); if (!qg) return null; if (typeof qg.createCustomAd !== 'function') { console.warn('[VivoAd] 宿主无 createCustomAd'); return null; } let ad: VivoAdInst | null = null; try { ad = qg.createCustomAd(style ? { posId, style } : { posId }) ?? null; } catch (e) { console.warn('[VivoAd] createCustomAd 失败', posId, String(e)); return null; } if (!ad?.show) { console.warn('[VivoAd] createCustomAd 返回空', posId); return null; } ad.onError?.(onError); if (onClose) ad.onClose?.(onClose); return ad; } /** 调用 show,Promise 拒绝走 onFail */ private static _callShow(ad: VivoAdInst, onFail: (err: unknown) => void): void { try { const ret = ad.show(); if (ret && typeof ret.catch === 'function') ret.catch(onFail); } catch (e) { onFail(e); } } /** 解绑并销毁 */ private static _drop(ad: VivoAdInst | null, onError: ((err: unknown) => void) | undefined, onClose: (() => void) | undefined): void { if (!ad) return; if (onError) ad.offError?.(onError); if (onClose) ad.offClose?.(onClose); try { ad.destroy?.(); } catch (e) { console.warn('[VivoAd] destroy 失败', String(e)); } } /** vivo 小游戏宿主。非 qg 时警告并返回 null */ private static _qg(): Record | null { if (MiniGamePf.id() !== 'qg') { console.warn('[VivoAd] 当前不是 qg 宿主'); return null; } return MiniGamePf.api(); } /** 取第一条非空广告位 */ private static _firstId(ids: string[] | undefined): string { return this._ids(ids)[0] ?? ''; } /** 去掉空广告位 */ private static _ids(ids: string[] | undefined): string[] { if (!ids) return []; const out: string[] = []; for (let i = 0; i < ids.length; i++) { if (ids[i]) out.push(ids[i]); } return out; } /** 错误对象转日志文本 */ private static _errText(err: unknown): string { if (err && typeof err === 'object') { const o = err as { errCode?: number; errMsg?: string }; if (o.errCode != null || o.errMsg) return String(o.errCode ?? '') + ' ' + String(o.errMsg ?? ''); } return String(err ?? ''); } }