// 聊天室赚钱面板,注册 earnPanelData Alpine 组件并集中管理 FluidPlayer 加载。 const FLUID_PLAYER_SCRIPT_URL = "https://cdn.fluidplayer.com/v3/current/fluidplayer.min.js"; const FALLBACK_VIDEO_SOURCE = "https://cdn.fluidplayer.com/videos/valerian-1080p.mkv"; const VAST_TAG_URL = "https://s.magsrv.com/v1/vast.php?idzone=5889208"; let earnPanelControlsBound = false; let earnPanelRegistered = false; let fluidPlayerScriptPromise = null; /** * 读取 CSRF Token。 * * @returns {string} */ function csrf() { return document.querySelector('meta[name="csrf-token"]')?.content || ""; } /** * 显示统一 Toast,避免面板代码直接依赖未初始化对象。 * * @param {object} options * @returns {void} */ function showToast(options) { window.chatToast?.show?.(options); } /** * 按需加载 FluidPlayer CDN 脚本。 * * @returns {Promise} */ function loadFluidPlayerScript() { if (typeof window.fluidPlayer === "function") { return Promise.resolve(); } if (fluidPlayerScriptPromise) { return fluidPlayerScriptPromise; } fluidPlayerScriptPromise = new Promise((resolve, reject) => { const existingScript = document.querySelector(`script[src="${FLUID_PLAYER_SCRIPT_URL}"]`); if (existingScript) { existingScript.addEventListener("load", () => resolve(), { once: true }); existingScript.addEventListener("error", () => reject(new Error("FluidPlayer 加载失败")), { once: true }); return; } const script = document.createElement("script"); script.src = FLUID_PLAYER_SCRIPT_URL; script.async = true; script.addEventListener("load", () => resolve(), { once: true }); script.addEventListener("error", () => reject(new Error("FluidPlayer 加载失败")), { once: true }); document.head.appendChild(script); }); return fluidPlayerScriptPromise; } /** * 销毁播放器实例并吞掉第三方库的重复销毁异常。 * * @param {object|null} player * @returns {void} */ function destroyPlayer(player) { if (!player) { return; } try { player.destroy(); } catch (error) { // FluidPlayer 重复销毁时可能抛错,面板关闭流程应继续执行。 } } /** * 重建原始 video DOM,避免 FluidPlayer 包裹后的节点影响下一次初始化。 * * @returns {void} */ function restoreVideoDOM() { const wrapper = document.getElementById("video-wrapper"); if (!wrapper) { return; } wrapper.innerHTML = ``; } /** * 创建赚钱面板 Alpine 组件。 * * @returns {object} */ export function createEarnPanelData() { return { isOpen: false, status: "idle", countdown: "获取中...", timer: null, player: null, videoFinished: false, /** * 打开面板并提前加载播放器脚本。 * * @returns {void} */ openPanel() { this.isOpen = true; void loadFluidPlayerScript().catch(() => { showToast({ title: "播放器加载失败", message: "广告播放器暂时不可用,请稍后再试。", icon: "⚠️", color: "#ef4444" }); }); if (this.status !== "claimed" && this.status !== "claiming") { this.resetPanelState(); } }, /** * 关闭面板并释放播放器资源。 * * @returns {void} */ closePanel() { this.isOpen = false; this.stopTimer(); if (this.status === "watching") { showToast({ title: "取消提示", message: "已取消观看,没有扣除观影次数。", icon: "ℹ️", color: "#3b82f6" }); } destroyPlayer(this.player); this.player = null; restoreVideoDOM(); this.status = "idle"; }, /** * 重置面板到待观看状态。 * * @returns {void} */ resetPanelState() { this.status = "idle"; this.countdown = "获取中..."; this.videoFinished = false; this.stopTimer(); destroyPlayer(this.player); this.player = null; restoreVideoDOM(); }, /** * 领奖后延迟重置,给用户保留到账反馈。 * * @returns {void} */ resetPanel() { this.closePanel(); window.setTimeout(() => { this.resetPanelState(); }, 300); }, /** * 标记观看完成并清理播放器。 * * @returns {void} */ completeWatch() { this.videoFinished = true; this.stopTimer(); this.status = "completed"; destroyPlayer(this.player); this.player = null; restoreVideoDOM(); window.EffectSounds?.ding?.(); }, /** * 开始观看广告并启动进度轮询。 * * @returns {Promise} */ async startWatching() { this.status = "watching"; this.countdown = "请求广告..."; this.videoFinished = false; this.stopTimer(); try { await loadFluidPlayerScript(); } catch (error) { this.status = "idle"; showToast({ title: "播放器加载失败", message: "广告播放器暂时不可用,请稍后再试。", icon: "⚠️", color: "#ef4444" }); return; } this.initPlayer(); let elapsedTime = 0; this.timer = window.setInterval(() => { if (this.videoFinished) { return; } const video = document.getElementById("exoclick-video"); elapsedTime += 0.5; if (!video) { return; } const duration = video.duration; const currentTime = video.currentTime; if (!Number.isNaN(duration) && duration > 0) { const left = Math.ceil(duration - currentTime); this.countdown = `${left > 0 ? left : 0} 秒`; if (left <= 0 && currentTime > 0) { this.completeWatch(); } return; } // 无广告填充或缓冲过慢时,最多等待 20 秒后放行奖励。 const fallbackLeft = Math.ceil(20 - elapsedTime); if (fallbackLeft > 0) { this.countdown = `缓冲/加载中... ${fallbackLeft}s`; return; } this.completeWatch(); }, 500); }, /** * 初始化 FluidPlayer 实例。 * * @returns {void} */ initPlayer() { const video = document.getElementById("exoclick-video"); const doInit = () => { try { this.player = window.fluidPlayer("exoclick-video", { layoutControls: { primaryColor: "#336699", posterImage: false, playButtonShowing: true, playPauseAnimation: false, fillToContainer: true, autoPlay: true, mute: false, }, vastOptions: { allowVPAID: true, vastTimeout: 8000, adList: [ { roll: "preRoll", vastTag: VAST_TAG_URL, adText: "请观看广告获取金币", }, ], }, }); } catch (error) { // 第三方播放器初始化失败时保留面板,用户可关闭后重试。 console.error("[EarnPanel] 播放器初始化未成功", error); } }; if (video && !video.paused) { video.pause(); video.addEventListener("pause", doInit, { once: true }); return; } doInit(); }, /** * 停止观看进度计时器。 * * @returns {void} */ stopTimer() { if (!this.timer) { return; } window.clearInterval(this.timer); this.timer = null; }, /** * 向后端领取观看奖励。 * * @returns {Promise} */ async claimReward() { if (this.status !== "completed") { return; } this.status = "claiming"; try { const response = await fetch(window.chatContext.earnRewardUrl, { method: "POST", headers: { "Content-Type": "application/json", "X-CSRF-TOKEN": csrf(), "Accept": "application/json", }, body: JSON.stringify({ room_id: window.chatContext?.roomId || 0, }), }); const data = await response.json(); if (response.ok && data.success) { this.status = "claimed"; showToast({ title: "奖励到账", message: data.message || "领取成功!", icon: "🎉", color: "#10b981" }); if (data.new_jjb !== undefined) { window.chatContext.userJjb = data.new_jjb; window.chatContext.myGold = data.new_jjb; window.dispatchEvent(new CustomEvent("update-user-points", { detail: { points: data.new_jjb } })); } window.EffectSounds?.ding?.(); if (data.level_up) { showToast({ title: "等级提升", message: `恭喜!您的等级提升到了 ${data.new_level_name}!`, icon: "🌟", color: "#8b5cf6", duration: 5000, }); window.setTimeout(() => { window.EffectManager?.play?.("fireworks"); }, 500); } return; } this.status = "completed"; showToast({ title: "出错了", message: data.message || "领奖失败,请稍后再试。", icon: "❌", color: "#ef4444", duration: 4000 }); } catch (error) { this.status = "completed"; showToast({ title: "网络错误", message: "网络请求失败,请检查连接。", icon: "⚠️", color: "#ef4444" }); } }, }; } /** * 注册赚钱面板 Alpine 组件。 * * @returns {void} */ function registerEarnPanelData() { if (earnPanelRegistered || !window.Alpine) { return; } earnPanelRegistered = true; window.Alpine.data("earnPanelData", createEarnPanelData); } /** * 绑定赚钱面板入口和 Alpine 注册事件。 * * @returns {void} */ export function bindEarnPanelControls() { if (typeof window === "undefined" || earnPanelControlsBound) { return; } earnPanelControlsBound = true; window.createEarnPanelData = createEarnPanelData; window.openEarnPanel = () => { window.dispatchEvent(new CustomEvent("open-earn-panel")); }; document.addEventListener("alpine:init", registerEarnPanelData, { once: true }); registerEarnPanelData(); }