Files
li 4a94fe36cf feat: 重构代码架构 + 新增报表/跟单/输赢统计功能
- 拆分 HomeController → TransactionController, ReportWebController, FollowPlanController
- 新增 Service 层: TransactionService, ReportService, FollowPlanService
- pk10.php JS 抽离为 4 个独立文件 (sound/race/bet/poll)
- 前台新增报表查询页面 (/report) + 跟单计划页面 (/follow-plan)
- 后台新增跟单计划管理 + 用户输赢明细统计
- 封盘状态显示倒计时 (x:xx)
- 音效仅在开奖弹窗打开时播放
- 路由按模块分组整理
- autoload 支持 App\Services 命名空间
2026-03-27 18:41:06 +08:00

180 lines
14 KiB
JavaScript

/**
* PK10 Race Animation System
* Depends on: pk10-sound.js (SoundFX)
* Globals expected: raceAnimState, lastAnimatedResult, idleAnimTimer, carPositions,
* raceFrameTimer, ballRollTimer, FINISH_LINE, START_LINE, trafficLightTimers,
* CAR_COLORS, I18N, raceModalOpen, currentView, cachedRaceResult, pendingRaceHeader
*/
function setCarPosition(n,pct,dur){var c=document.getElementById('carContainer'+n);var b=document.getElementById('bar'+n);if(!c)return;c.style.transition='right '+dur+'s ease';c.style.right=pct+'%';if(b){b.style.transition='width '+dur+'s ease';b.style.width=Math.min(pct+3,92)+'%';}carPositions[n]=pct;}
function setCarMoving(n,moving,sprinting){var c=document.getElementById('carContainer'+n);if(!c)return;c.classList.toggle('moving',moving);c.classList.toggle('sprinting',!!sprinting);c.classList.remove('ready','braking');}
function setCarBraking(n){var c=document.getElementById('carContainer'+n);if(!c)return;c.classList.remove('sprinting','moving');c.classList.add('braking');setTimeout(function(){if(c)c.classList.remove('braking');},800);}
function setRankBadge(n,rank){var badge=document.getElementById('rankBadge'+n);if(!badge)return;var c=document.getElementById('carContainer'+n);if(rank>0){var labels=['1st','2nd','3rd','4th','5th','6th','7th','8th','9th','10th'];badge.textContent=labels[rank-1]||rank;if(c)c.classList.add('show-rank');}else{badge.textContent='';if(c)c.classList.remove('show-rank');}}
// ANIM: idle
function startIdleAnimation(){
if(idleAnimTimer)return;raceAnimState='waiting';hidePodium();
showRaceOverlay();clearTrafficLights();
for(var i=1;i<=10;i++){
setCarPosition(i,START_LINE,0.6);
setCarMoving(i,false,false);
setRankBadge(i,0);
}
idleAnimTimer=true;}
function stopIdleAnimation(){if(idleAnimTimer&&idleAnimTimer!==true){clearInterval(idleAnimTimer);}idleAnimTimer=null;if(raceFrameTimer){cancelAnimationFrame(raceFrameTimer);raceFrameTimer=null;}for(var i=1;i<=10;i++){setCarMoving(i,false,false);var c=document.getElementById('carContainer'+i);if(c)c.classList.remove('ready');}}
function startReadyAnimation(){stopIdleAnimation();raceAnimState='ready';hidePodium();showRaceOverlay();
for(var i=1;i<=10;i++){setCarPosition(i,START_LINE,0.8);setCarMoving(i,true,false);setRankBadge(i,0);var c=document.getElementById('carContainer'+i);if(c)c.classList.add('ready');}
SoundFX.engineIdle();
startTrafficLightSequence();}
// ANIM: race
function triggerFinishBurst(){var fb=document.getElementById('finishBurst');if(!fb)return;fb.classList.remove('active');void fb.offsetWidth;fb.classList.add('active');setTimeout(function(){fb.classList.remove('active');},1000);}
function generateConfetti(){var box=document.getElementById('confettiBox');if(!box)return;box.innerHTML='';var colors=['#ff3333','#3399ff','#ffcc00','#33ff66','#ff66cc','#ff8800','#66ffff','#ffff33','#9b59b6','#1abc9c'];for(var i=0;i<40;i++){var c=document.createElement('div');c.className='confetti';c.style.left=Math.random()*100+'%';c.style.background=colors[i%colors.length];c.style.setProperty('--cf-dur',(2.5+Math.random()*2)+'s');c.style.setProperty('--cf-delay',(Math.random()*1.5)+'s');c.style.width=(4+Math.random()*4)+'px';c.style.height=(6+Math.random()*8)+'px';c.style.borderRadius=Math.random()>.5?'50%':'2px';box.appendChild(c);}}
function startTrackShake(){var t=document.getElementById('raceArea');if(t)t.classList.add('shaking');}
function stopTrackShake(){var t=document.getElementById('raceArea');if(t)t.classList.remove('shaking');}
/* Direct positioning (rAF driven, no CSS transition) */
function setCarPosDirect(n,pct){var c=document.getElementById('carContainer'+n),b=document.getElementById('bar'+n);if(!c)return;c.style.transition='none';c.style.right=pct+'%';if(b){b.style.transition='none';b.style.width=Math.min(pct+3,92)+'%';}carPositions[n]=pct;}
/* Race script generation (high-frequency overtaking) */
function _raceScript(rankMap,carTargets){
var sc={},i,w,c,tmp,f;
var N=25,T=[],PF=[],PG=[],B=[];
for(i=0;i<=N;i++){f=i/N;T.push(f);
PF.push(START_LINE+f*(FINISH_LINE-START_LINE));
PG.push(f<0.82?(1.0+Math.abs(Math.sin(f*Math.PI*6))*0.8):(1.0+(f-0.82)/0.18*6.5));
B.push(f<0.78?f*0.06:0.05+(f-0.78)/0.22*0.95);
}B[0]=0;B[N]=1;PG[0]=0;
var FO=[];for(w=0;w<=N;w++){var a=[];for(c=1;c<=10;c++)a.push(c);for(i=9;i>0;i--){var j=Math.floor(Math.random()*(i+1));tmp=a[i];a[i]=a[j];a[j]=tmp;}FO.push(a);}
var fin=[];for(c=1;c<=10;c++)fin[rankMap[c]]=c;FO[N]=fin.slice();
var W=fin[0],S2=fin[1];
for(w=1;w<Math.floor(N*.65);w++){var wi=FO[w].indexOf(W);if(wi<5){var si=5+Math.floor(Math.random()*4);if(si<10){tmp=FO[w][wi];FO[w][wi]=FO[w][si];FO[w][si]=tmp;}}}
for(w=Math.floor(N*.85);w<N;w++){wi=FO[w].indexOf(W);if(wi>1){si=Math.floor(Math.random()*2);tmp=FO[w][wi];FO[w][wi]=FO[w][si];FO[w][si]=tmp;}}
if(S2)for(w=Math.floor(N*.4);w<N-2;w++){var s2i=FO[w].indexOf(S2);if(s2i>2&&Math.random()>.3){tmp=FO[w][s2i];FO[w][s2i]=FO[w][0];FO[w][0]=tmp;}}
for(c=1;c<=10;c++){sc[c]=[];for(w=0;w<=N;w++){
if(!w){sc[c].push({t:0,p:START_LINE});continue;}
var fk=FO[w].indexOf(c),rk=rankMap[c]!==undefined?rankMap[c]:9;
var br=fk*(1-B[w])+rk*B[w];
var p=PF[w]-br*PG[w]+(Math.random()-.5)*1.5;
sc[c].push({t:T[w],p:Math.max(START_LINE,Math.min(FINISH_LINE+2,p))});
}}return sc;}
/* Smoothstep interpolation */
function _interp(wp,t){if(t<=wp[0].t)return wp[0].p;var L=wp.length-1;if(t>=wp[L].t)return wp[L].p;for(var i=0;i<L;i++){if(t>=wp[i].t&&t<=wp[i+1].t){var s=(t-wp[i].t)/(wp[i+1].t-wp[i].t);s=s*s*(3-2*s);return wp[i].p+(wp[i+1].p-wp[i].p)*s;}}return wp[L].p;}
/* Core race animation (dramatic chase version) */
function animateRace(result,dur){if(!result||result.length<10)return;var rk=result.join(',');if(lastAnimatedResult===rk&&raceAnimState==='finished')return;
lastAnimatedResult=rk;stopIdleAnimation();hideRaceOverlay();hidePodium();raceAnimState='racing';
dur=dur||5500;
var rankMap={};for(var i=0;i<10;i++)rankMap[result[i]]=i;
var carTargets={};for(var i=1;i<=10;i++){var r=rankMap[i]!==undefined?rankMap[i]:9;carTargets[i]=FINISH_LINE-r*7.5;setCarMoving(i,true,false);setRankBadge(i,0);}
var script=_raceScript(rankMap,carTargets);
startBallRoll(dur);
var t0=performance.now(),shook=false,sprinted=false;
function tick(now){if(raceAnimState!=='racing')return;
var el=now-t0,t=Math.min(el/dur,1);
if(!shook&&t>.10){shook=true;startTrackShake();SoundFX.engineStart();}
if(!sprinted&&t>.16){sprinted=true;for(var i=1;i<=10;i++)setCarMoving(i,true,true);}
for(var c=1;c<=10;c++)setCarPosDirect(c,_interp(script[c],t));
if(t>=1){for(var i=1;i<=10;i++){setCarPosDirect(i,carTargets[i]);setCarBraking(i);}
SoundFX.brakeSqueal();triggerFinishBurst();SoundFX.finish();
setTimeout(function(){if(raceAnimState!=='racing')return;raceAnimState='finished';stopTrackShake();stopBallRoll();for(var i=1;i<=10;i++){setCarMoving(i,false,false);var c=document.getElementById('carContainer'+i);if(c)c.classList.remove('braking');var rank=rankMap[i]!==undefined?rankMap[i]+1:0;setRankBadge(i,rank);}
var badge=document.getElementById('statusBadge');if(badge){badge.textContent=I18N.drawing;badge.style.cssText='color:var(--warn)';}
if(pendingRaceHeader){updateSceneHeader(pendingRaceHeader,pendingRaceHeader.period_number||'');pendingRaceHeader=null;}
setTimeout(function(){generateConfetti();showPodium(result);SoundFX.podium();},800);},500);return;}
raceFrameTimer=requestAnimationFrame(tick);}
raceFrameTimer=requestAnimationFrame(tick);}
function resetRace(){stopIdleAnimation();hideRaceOverlay();hidePodium();stopTrackShake();stopBallRoll();raceAnimState='idle';lastAnimatedResult=null;cachedRaceResult=null;pendingRaceHeader=null;if(window._podiumTimer){clearTimeout(window._podiumTimer);window._podiumTimer=null;}var fb=document.getElementById('finishBurst');if(fb)fb.classList.remove('active');var cb=document.getElementById('confettiBox');if(cb)cb.innerHTML='';for(var i=1;i<=10;i++){var c=document.getElementById('carContainer'+i);var b=document.getElementById('bar'+i);if(c){c.style.transition='right 0.5s ease';c.style.right=START_LINE+'%';c.classList.remove('moving','sprinting','ready','show-rank','braking');}if(b){b.style.transition='width 0.5s ease';b.style.width='5%';}carPositions[i]=START_LINE;setRankBadge(i,0);}}
// ===== Scene controls (traffic lights + podium + scene header) =====
function showRaceOverlay(){var ov=document.getElementById('raceOverlay');if(ov)ov.classList.add('show');}
function hideRaceOverlay(){var ov=document.getElementById('raceOverlay');if(ov)ov.classList.remove('show');clearTrafficLights();}
function clearTrafficLights(){trafficLightTimers.forEach(function(t){clearTimeout(t);});trafficLightTimers=[];for(var i=1;i<=5;i++){var l=document.getElementById('tl'+i);if(l)l.className='tl';}}
function startTrafficLightSequence(){clearTrafficLights();
trafficLightTimers.push(setTimeout(function(){document.getElementById('tl1').className='tl red';SoundFX.trafficBeep(4);},300));
trafficLightTimers.push(setTimeout(function(){document.getElementById('tl2').className='tl red';SoundFX.trafficBeep(3);},600));
trafficLightTimers.push(setTimeout(function(){document.getElementById('tl3').className='tl yellow';SoundFX.trafficBeep(2);},900));
trafficLightTimers.push(setTimeout(function(){document.getElementById('tl4').className='tl red';SoundFX.trafficBeep(1);},1200));
trafficLightTimers.push(setTimeout(function(){document.getElementById('tl5').className='tl red';SoundFX.trafficBeep(0);},1500));
trafficLightTimers.push(setTimeout(function(){for(var i=1;i<=5;i++)document.getElementById('tl'+i).className='tl green';SoundFX.trafficBeep(0);setTimeout(clearTrafficLights,500);},2500));}
function updateRaceOverlayTimer(secs){var td=document.getElementById('raceTimerDisplay');if(!td)return;var m=Math.floor(secs/60),s=secs%60;td.innerHTML=(m<10?'0':'')+m+':'+(s<10?'0':'')+s+'<span class="ms">00</span>';}
// ===== Ball roll effect =====
function startBallRoll(dur){
stopBallRoll();
var balls=document.getElementById('raceSceneBalls');
if(!balls)return;
dur=dur||5500;
var speed=100;
function roll(){
var arr=[];for(var i=1;i<=10;i++)arr.push(i);
for(var i=arr.length-1;i>0;i--){var j=Math.floor(Math.random()*(i+1));var t=arr[i];arr[i]=arr[j];arr[j]=t;}
var html='';
arr.forEach(function(v){var cc=CAR_COLORS[v];html+='<span class="rs-ball" style="background:'+(cc?cc[0]:'#666')+'">'+v+'</span>';});
balls.innerHTML=html;
}
roll();
ballRollTimer=setInterval(roll,speed);
var slow1=Math.round(dur*0.4);
var slow2=Math.round(dur*0.75);
setTimeout(function(){if(ballRollTimer){clearInterval(ballRollTimer);ballRollTimer=setInterval(roll,200);}},slow1);
setTimeout(function(){if(ballRollTimer){clearInterval(ballRollTimer);ballRollTimer=setInterval(roll,400);}},slow2);
}
function stopBallRoll(){
if(ballRollTimer){clearInterval(ballRollTimer);ballRollTimer=null;}
}
function updateSceneHeader(result,periodNum){
var balls=document.getElementById('raceSceneBalls');var pn=document.getElementById('raceScenePeriod');
if(pn&&periodNum){var short=periodNum.length>6?periodNum.slice(-6):periodNum;pn.textContent='\u671F\u6570\uFF1A'+short;}
if(balls&&result){var html='';['rank_1','rank_2','rank_3','rank_4','rank_5','rank_6','rank_7','rank_8','rank_9','rank_10'].forEach(function(k){var v=result[k];if(v){var cc=CAR_COLORS[v];html+='<span class="rs-ball" style="background:'+(cc?cc[0]:'#666')+'">'+v+'</span>';}});balls.innerHTML=html||'<span style="color:#666;font-size:10px">---</span>';}
updateSceneFooter(result,periodNum);
}
function updateSceneFooter(result,periodNum){
var ftPn=document.getElementById('ftPeriodNum');
if(ftPn&&periodNum)ftPn.textContent=periodNum;
if(!result)return;
var r1=+result.rank_1,r2=+result.rank_2;
if(r1&&r2){
var sum=r1+r2;
var ftSv=document.getElementById('ftSumVal');if(ftSv)ftSv.textContent=sum;
var ftSb=document.getElementById('ftSumBs');if(ftSb)ftSb.textContent=sum>=12?I18N.big:I18N.small;
var ftSo=document.getElementById('ftSumOe');if(ftSo)ftSo.textContent=sum%2?I18N.odd:I18N.even;
}
var ftDt=document.getElementById('ftDtTags');
if(ftDt){var html='';for(var i=1;i<=5;i++){var a=+result['rank_'+i],b=+result['rank_'+(11-i)];if(a&&b){var isDragon=a>b;html+='<span class="race-ft-tag dt-tag" style="color:'+(isDragon?'#ff6633':'#33aaff')+'">'+(isDragon?I18N.dragon:I18N.tiger)+'</span>';}}ftDt.innerHTML=html||'---';}
}
// ===== Toggle scene collapse =====
function toggleRaceScene(){var scene=document.querySelector('.race-scene');if(!scene)return;scene.classList.toggle('collapsed');localStorage.setItem('raceSceneCollapsed',scene.classList.contains('collapsed')?'1':'0');}
// Init collapsed state + sound icon
(function(){var btn=document.querySelector('.rs-sound');if(btn)btn.textContent=SoundFX.enabled?'🔊':'🔇';if(localStorage.getItem('raceSceneCollapsed')==='1'){var s=document.querySelector('.race-scene');if(s)s.classList.add('collapsed');}})();
// ===== Modal control =====
function openRaceModal(){
document.getElementById('raceModalMask').classList.add('show');
raceModalOpen=true;
document.body.style.overflow='hidden';
}
function closeRaceModal(){
document.getElementById('raceModalMask').classList.remove('show');
raceModalOpen=false;
document.body.style.overflow='';
SoundFX.stopAll();
}
function getPodiumCarSrc(num){return '/Static/img/cars/car'+num+'.png';}
function showPodium(result){if(!result||result.length<3)return;document.getElementById('podiumImg1').src=getPodiumCarSrc(result[0]);document.getElementById('podiumImg2').src=getPodiumCarSrc(result[1]);document.getElementById('podiumImg3').src=getPodiumCarSrc(result[2]);var po=document.getElementById('podiumOverlay');if(po)po.classList.add('show');}
function hidePodium(){var po=document.getElementById('podiumOverlay');if(po)po.classList.remove('show');}
// ===== Video/Anim switch =====
function switchVideoOrAnim(status,changed){
currentView='anim';
var desc=document.getElementById('raceEntryDesc');
if(desc){
var map={pending:'\u7B49\u5F85\u5F00\u5956\u4E2D...',locked:'\u6B63\u5728\u5F00\u5956\uFF01',drawn:'\u5F00\u5956\u7ED3\u675F',settled:'\u5DF2\u7ED3\u7B97'};
desc.textContent=map[status]||'\u70B9\u51FB\u67E5\u770B\u5F00\u5956\u52A8\u753B';
}
}
function tryResumeVideo(){var v=document.querySelector('#pk10VideoPlayer video');if(v){v.muted=true;v.play().catch(function(){});}}