迁移赚钱面板脚本

This commit is contained in:
2026-04-25 14:25:07 +08:00
parent c7b8ba956b
commit 8a690ac40d
3 changed files with 407 additions and 245 deletions
@@ -95,248 +95,4 @@
</div>
<!-- 引入 Fluid Player 用于播放 VAST 广告 -->
<!-- v3 CDN 版本已内嵌样式,不能再引用已下线的 fluidplayer.min.css -->
<script src="https://cdn.fluidplayer.com/v3/current/fluidplayer.min.js"></script>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('earnPanelData', () => ({
isOpen: false,
status: 'idle', // idle, watching, completed, claiming, claimed
countdown: '获取中...',
timer: null,
player: null,
videoFinished: false,
openPanel() {
this.isOpen = true;
if (this.status !== 'claimed' && this.status !== 'claiming') {
this.resetPanelState();
}
},
closePanel() {
this.isOpen = false;
this.stopTimer();
if (this.status === 'watching') {
window.chatToast.show({ title: '取消提示', message: '已取消观看,没有扣除观影次数。', icon: '️', color: '#3b82f6' });
}
if (this.player) {
try { this.player.destroy(); } catch(e) {}
this.player = null;
}
this.restoreVideoDOM();
this.status = 'idle';
},
resetPanelState() {
this.status = 'idle';
this.countdown = '获取中...';
this.videoFinished = false;
this.stopTimer();
if (this.player) {
try { this.player.destroy(); } catch(e) {}
this.player = null;
}
this.restoreVideoDOM();
},
resetPanel() {
this.closePanel();
setTimeout(() => { this.resetPanelState(); }, 300);
},
restoreVideoDOM() {
// FluidPlayer 销毁后会修改/包裹 video 元素,需要重建以便下次干净初始化
// source 必须保留,否则 VAST 失败时 FluidPlayer 的 fallback 无目标可播
const wrapper = document.getElementById('video-wrapper');
if (wrapper) {
wrapper.innerHTML = `<video id="exoclick-video" style="width: 100%; height: 100%;" playsinline preload="none"><source src="https://cdn.fluidplayer.com/videos/valerian-1080p.mkv" type="video/mp4" /></video>`;
}
},
completeWatch() {
this.videoFinished = true;
this.stopTimer();
this.status = 'completed';
if (this.player) {
try { this.player.destroy(); } catch(e) {}
this.player = null;
}
this.restoreVideoDOM();
if (typeof EffectSounds !== 'undefined') {
EffectSounds.ding();
}
},
startWatching() {
this.status = 'watching';
this.countdown = '请求广告...';
this.videoFinished = false;
this.stopTimer();
// 同步初始化播放器,保证满足浏览器的 user gesture 直接挂钩播放
this.initPlayer();
let elapsedTime = 0;
// 基于视频真实时长的倒计时检测与失败降级处理
this.timer = setInterval(() => {
if (this.videoFinished) return;
const v = document.getElementById('exoclick-video');
elapsedTime += 0.5;
if (v) {
const dur = v.duration;
const cur = v.currentTime;
// 当系统取到真实的视频时长时(确保不是NaN)
if (!isNaN(dur) && dur > 0) {
const left = Math.ceil(dur - cur);
this.countdown = (left > 0 ? left : 0) + ' 秒';
// 若倒计时归零并且正在播放中(保证不是误判)
if (left <= 0 && cur > 0) {
this.completeWatch();
}
} else {
// 没有视频时长(可能正在缓冲,也可能广告没有填充)
// 最多让用户干等 20 秒作为无广告情况下的保底奖励
const fallbackLeft = Math.ceil(20 - elapsedTime);
if (fallbackLeft > 0) {
this.countdown = '缓冲/加载中... ' + fallbackLeft + 's';
} else {
// 20秒超时还没获得时长,则当作广告未填充或网速太慢,放行奖励
this.completeWatch();
}
}
}
}, 500);
},
initPlayer() {
// 按照 Chrome 规范:确保 video 当前未在 play() 中,通过 pause() 后再初始化
// 参考:https://developer.chrome.com/blog/play-request-was-interrupted
const v = document.getElementById('exoclick-video');
const doInit = () => {
try {
this.player = 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: 'https://s.magsrv.com/v1/vast.php?idzone=5889208',
adText: '请观看广告获取金币'
}
]
}
}
);
// 注意:FluidPlayer 不支持 player.on('error'),不要注册该事件
// VAST 失败时 FluidPlayer 会自动播放 fallback source
// 我们的 setInterval 轮询 currentTime/duration 会自然触发 completeWatch()
} catch(e) {
console.error('[EarnPanel] 播放器初始化未成功', e);
}
};
if (v && !v.paused) {
// 如果 video 正在播放,先等 pause 后再 init,避免 play() 被 load() 中断
v.pause();
v.addEventListener('pause', doInit, { once: true });
} else {
doInit();
}
},
stopTimer() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
},
async claimReward() {
if (this.status !== 'completed') return;
this.status = 'claiming';
try {
const payload = {
room_id: window.chatContext?.roomId || 0
};
const response = await fetch(window.chatContext.earnRewardUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Accept': 'application/json'
},
body: JSON.stringify(payload)
});
const data = await response.json();
if (response.ok && data.success) {
this.status = 'claimed';
window.chatToast.show({ title: '奖励到账', message: data.message || '领取成功!', icon: '🎉', color: '#10b981' });
// 更新本地上下文
if (data.new_jjb !== undefined) {
window.chatContext.userJjb = data.new_jjb;
window.chatContext.myGold = data.new_jjb;
// 派发更新全局 UI 的事件,类似钓鱼的更新
window.dispatchEvent(new CustomEvent('update-user-points', { detail: { points: data.new_jjb }}));
}
// 播放到账特定金币音效
if (typeof EffectSounds !== 'undefined') {
EffectSounds.ding();
}
if (data.level_up) {
window.chatToast.show({
title: '等级提升',
message: `恭喜!您的等级提升到了 ${data.new_level_name}`,
icon: '🌟',
color: '#8b5cf6',
duration: 5000
});
setTimeout(() => {
// 触发烟花庆贺
if(typeof EffectManager !== 'undefined') {
EffectManager.play('fireworks');
}
}, 500);
}
} else {
// 领奖失败,可能是超出次数或防刷限制
this.status = 'completed';
window.chatToast.show({ title: '出错了', message: data.message || '领奖失败,请稍后再试。', icon: '❌', color: '#ef4444', duration: 4000 });
}
} catch (error) {
this.status = 'completed';
window.chatToast.show({ title: '网络错误', message: '网络请求失败,请检查连接。', icon: '⚠️', color: '#ef4444' });
}
}
}));
});
</script>
{{-- 赚钱面板 Alpine 组件与 FluidPlayer 加载已迁移到 resources/js/chat-room/earn-panel.js --}}