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.

285 lines
9.6 KiB

3 weeks ago
import { MiniGamePf } from '../../../mx/tools/MiniGameHostInfo';
import { sdkConfig } from '../sdkConfig';
/** vivo 广告组件(Banner / 原生模板 / 退出浮层) */
interface VivoAdInst {
/** 展示 */
show: () => Promise<unknown>;
/** 销毁。实例不能复用 */
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<string, any> | 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 ?? '');
}
}