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} 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();