Files
TurboSu/dashboards/tachometer/index.html
T
AskaEth 63e3c8d607 refactor: standalone dashboard folders with index.html, zip-based import/export (.tsd/.tss/.tsp)
- Each dashboard is now a self-contained folder: manifest.json + index.html
- Auto-scan dashboards/ and data/dashboards/ on startup
- Export: .tsd (dashboards), .tss (scenes), .tsp (game plugins) - all zip format
- Dashboard index.html is complete standalone page (WS + data binding)
- Aspect ratio constraints handled in each dashboard's own JS
- Removed template-based dashboard rendering in favor of static serve
- Import via file upload endpoints, export via direct file download
2026-07-25 15:35:08 +08:00

73 lines
3.8 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,viewport-fit=cover">
<title>TurboSu - 转速表</title>
<style>
:root{--ratio:16/9;--mode:contain}
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
html,body{width:100%;height:100%;overflow:hidden;background:#000;font-family:'SF Pro Text',-apple-system,'PingFang SC',sans-serif}
#root{width:100%;height:100%;display:flex;align-items:center;justify-content:center}
#box{position:relative;overflow:hidden}
#content{width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;background:linear-gradient(135deg,#0a0a14,#1a1a30)}
svg{width:min(90%,500px)}
.rpm-text{font-size:min(8vw,48px);font-weight:900;fill:#fff}
.rpm-label{font-size:min(2vw,14px);fill:rgba(255,255,255,.45)}
.gauge-bg{fill:none;stroke:rgba(255,255,255,.08);stroke-width:20;stroke-linecap:round}
.gauge-fg{fill:none;stroke:url(#g);stroke-width:20;stroke-linecap:round;stroke-dasharray:314;transition:stroke-dashoffset .1s ease}
.labels text{font-size:10px;fill:rgba(255,255,255,.25);text-anchor:middle}
/* aspect-ratio */
#box.contain{max-width:100vw;max-height:100vh}
#box.cover{min-width:100vw;min-height:100vh}
#box.fill{width:100vw;height:100vh}
#box.center{width:auto;height:auto}
</style>
</head>
<body>
<div id="root"><div id="box" class="contain"><div id="content">
<svg viewBox="0 0 320 200">
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="#3482FF"/><stop offset="40%" stop-color="#3482FF"/><stop offset="70%" stop-color="#FF9F0A"/><stop offset="100%" stop-color="#E94634"/></linearGradient></defs>
<path d="M 30,170 A 130,130 0 0,1 290,170" class="gauge-bg"/>
<path id="arc" d="M 30,170 A 130,130 0 0,1 290,170" class="gauge-fg" stroke-dashoffset="314"/>
<text x="160" y="148" text-anchor="middle" class="rpm-text" id="rpm-v">0</text>
<text x="160" y="172" text-anchor="middle" class="rpm-label">RPM</text>
<g class="labels">
<text x="28" y="188">0</text><text x="80" y="72">2k</text><text x="160" y="44">4k</text><text x="240" y="72">6k</text><text x="292" y="188">8k</text>
</g>
</svg>
</div></div></div>
<script>
const M={aspect_ratio:"16:9",render_mode:"contain"};
(function(){
const box=document.getElementById('box'),arc=document.getElementById('arc'),rpmV=document.getElementById('rpm-v');
const proto=location.protocol==='https:'?'wss:':'ws:',wsUrl=proto+'//'+location.host+'/ws';
let ws,rt;
function parseRatio(r){if(!r||r==='auto')return null;const p=r.split(':');if(p.length===2){const w=+p[0],h=+p[1];if(w>0&&h>0)return w/h;}return null;}
function apply(){
const ratio=parseRatio(M.aspect_ratio||'16:9'),mode=M.render_mode||'contain';
if(!ratio){box.style.width='100vw';box.style.height='100vh';box.className='fill';return;}
const vw=window.innerWidth,vh=window.innerHeight,vr=vw/vh;let cw,ch;
switch(mode){
case'cover':if(vr>ratio){cw=vw;ch=vw/ratio}else{ch=vh;cw=vh*ratio};box.className='cover';break;
case'fill':box.style.width='100vw';box.style.height='100vh';box.className='fill';return;
case'center':cw=Math.min(vw,1920);ch=cw/ratio;if(ch>vh){ch=Math.min(vh,1080);cw=ch*ratio};box.className='center';break;
default:if(vr>ratio){ch=vh;cw=vh*ratio}else{cw=vw;ch=vw/ratio};box.className='contain';
}
if(mode!=='fill'){box.style.width=cw+'px';box.style.height=ch+'px';}
}
function connect(){
ws=new WebSocket(wsUrl);ws.onopen=()=>{if(rt){clearTimeout(rt);rt=null}};
ws.onmessage=(e)=>{try{const m=JSON.parse(e.data);if(m.type==='telemetry'){const d=m.data,rpm=d.rpm||0,max=d.max_rpm||8000,pct=Math.min(rpm/max,1);rpmV.textContent=rpm.toFixed(0);arc.setAttribute('stroke-dashoffset',314-314*pct)}}catch(e){}};
ws.onclose=()=>{rt=setTimeout(connect,2000)};
}
apply();window.addEventListener('resize',apply);connect();
})();
</script>
</body>
</html>