Files
rating/utils/adManager.js
2026-05-07 11:05:30 +08:00

200 lines
6.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();