Files
rating/utils/adManager.js

200 lines
6.5 KiB
JavaScript
Raw Normal View History

2026-05-07 11:05:30 +08:00
import { watchAdStart, watchAdReward } from "@/api/system.js";
import { useUserStore } from "@/stores/user";
class AdManager {
constructor() {
this.videoAd = null;
this.rewardToken = "";
this.isLoaded = false;
this.isAdShowing = false; // 新增:标记广告是否正在展示
this.adUnitId = "adunit-d7a28e0357d98947"; // Default ID from RewardAd.vue
// 彻底废除预初始化。
// 在微信小程序中,如果在页面未完全就绪时调用 createRewardedVideoAd
// 会导致底层寻找挂载点失败,抛出 `insertTextView:fail` 等严重渲染错误。
// 因此改为:只在用户真正点击“看广告”时才进行初始化。
}
/**
* Initializes the ad instance synchronously when needed.
* @param {string} adUnitId
*/
_initSync(adUnitId) {
if (this.videoAd && this.adUnitId === adUnitId) {
return true; // Already initialized for this ID
}
this.adUnitId = adUnitId || this.adUnitId;
// #ifdef MP-WEIXIN || MP-ALIPAY || MP-TOUTIAO || MP-QQ
if (uni.createRewardedVideoAd) {
try {
// Destroy old instance if ID changed
if (this.videoAd) {
if (this.videoAd.destroy) {
try {
this.videoAd.destroy();
} catch (destroyErr) {
console.error("Ad Manager: destroy old ad failed", destroyErr);
}
}
this.videoAd = null;
}
// 核心修复:微信小程序官方建议使用单例,且多次调用 createRewardedVideoAd 传入相同的 adUnitId 会返回同一个实例。
// 但如果在短时间内销毁又立即创建,底层可能会发生不可预知的渲染报错(如找不到 parent 节点)。
// 因此,我们只需安全地获取实例,并重新绑定事件。
this.videoAd = uni.createRewardedVideoAd({
adUnitId: this.adUnitId
});
if (this.videoAd) {
// 清理之前可能存在的残留事件,防止重复触发和内存泄漏导致底层崩溃
if (this.videoAd.offLoad) this.videoAd.offLoad();
if (this.videoAd.offError) this.videoAd.offError();
if (this.videoAd.offClose) this.videoAd.offClose();
this.videoAd.onLoad(() => {
console.log("Ad Manager: Ad Loaded");
this.isLoaded = true;
});
this.videoAd.onError((err) => {
console.error("Ad Manager: Ad Load Error", err);
this.isLoaded = false;
});
return true;
}
} catch (e) {
console.error("Ad Manager Init Error", e);
return false;
}
}
// #endif
return false;
}
/**
* Show the rewarded video ad.
* Handles the full flow: start session -> show ad -> verify completion -> claim reward -> refresh assets.
* @param {string} [adUnitId] - Optional adUnitId to override the default.
* @param {string} [providedToken] - Optional token if watchAdStart was already called externally.
* @returns {Promise<boolean>} Resolves with true if reward was claimed, false otherwise.
*/
async showVideoAd(adUnitId, providedToken) {
if (this.isAdShowing || this.isStarting) {
console.log("Ad Manager: Ad is already starting or showing");
return false;
}
this.isStarting = true;
// 延迟到这一步才初始化,避开页面渲染期
const canInit = this._initSync(adUnitId);
if (!canInit || !this.videoAd) {
uni.showToast({ title: "当前环境不支持广告", icon: "none" });
this.isStarting = false;
return false;
}
try {
// Step 1: Start Ad Session (if not provided)
if (providedToken) {
this.rewardToken = providedToken;
} else {
const startRes = await watchAdStart();
if (!startRes || !startRes.rewardToken) {
uni.showToast({ title: "广告启动失败,请稍后再试", icon: "none" });
this.isStarting = false;
return false;
}
this.rewardToken = startRes.rewardToken;
}
// Step 2: Show Ad
try {
this.isAdShowing = true;
this.isStarting = false;
await this.videoAd.show();
} catch (e) {
// Retry load and show
try {
await this.videoAd.load();
await this.videoAd.show();
} catch (loadErr) {
this.isAdShowing = false;
console.error("Ad Manager: Retry load failed", loadErr);
throw loadErr;
}
}
// Step 3: Wait for Close
return new Promise((resolve) => {
const onCloseHandler = async (res) => {
// 延迟 500ms 释放状态防止从广告返回页面时onShow立即触发 DOM 大量更新,导致底层 insertTextView:fail 崩溃
setTimeout(() => {
this.isAdShowing = false;
}, 500);
// 安全地解绑事件
try {
if (this.videoAd && this.videoAd.offClose) {
this.videoAd.offClose(onCloseHandler); // Clean up listener
}
} catch (offErr) {
console.error("Ad Manager: offClose error", offErr);
}
if (res && res.isEnded) {
// Step 4: Claim Reward
// Only claim if token wasn't provided externally (if provided, external component claims it)
if (!providedToken) {
await this._claimReward(this.rewardToken);
}
resolve(true);
} else {
uni.showToast({ title: "观看完整广告才能获取奖励哦", icon: "none" });
resolve(false);
}
};
try {
this.videoAd.onClose(onCloseHandler);
} catch (onErr) {
console.error("Ad Manager: onClose error", onErr);
resolve(false);
}
});
} catch (e) {
console.error("Ad Manager: Show failed", e);
uni.showToast({ title: "广告加载失败,请稍后再试", icon: "none" });
return false;
}
}
async _claimReward(token) {
try {
uni.showLoading({ title: "发放奖励中..." });
const res = await watchAdReward(token);
if (res) {
uni.showToast({
title: "获得50积分",
icon: "success",
});
// Refresh user assets
const userStore = useUserStore();
await userStore.fetchUserAssets();
}
} catch (e) {
console.error("Ad Manager: Reward claim failed", e);
uni.showToast({ title: "奖励发放失败", icon: "none" });
} finally {
uni.hideLoading();
}
}
}
export default new AdManager();