200 lines
6.5 KiB
JavaScript
200 lines
6.5 KiB
JavaScript
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();
|