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
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
<!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>
|
||||
*,*::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}
|
||||
#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}
|
||||
#content{width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#0a0a14,#1a1a30);gap:3vw}
|
||||
.gear{width:min(30vw,140px);height:min(30vw,140px);border-radius:50%;background:rgba(52,130,255,.12);border:3px solid rgba(52,130,255,.45);display:flex;align-items:center;justify-content:center;box-shadow:0 0 40px rgba(52,130,255,.15)}
|
||||
.gear span{font-size:min(14vw,64px);font-weight:900;color:#fff;transition:transform .15s ease}
|
||||
.gear.flash span{color:#FF9F0A;transform:scale(1.1)}
|
||||
.bars{display:flex;flex-direction:column;gap:4px}
|
||||
.bar-item{display:flex;align-items:center;gap:6px}
|
||||
.bar-item .label{font-size:min(2.5vw,13px);color:rgba(255,255,255,.35);width:min(5vw,20px);text-align:right}
|
||||
.bar-item .fill{height:6px;background:rgba(255,255,255,.08);border-radius:3px;width:min(20vw,100px)}
|
||||
.bar-item .fill-inner{height:100%;border-radius:3px;transition:background .3s ease}
|
||||
.bar-item.active .label{color:#fff}
|
||||
.bar-item.active .fill-inner{background:var(--accent,#3482FF)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"><div id="box" class="contain"><div id="content">
|
||||
<div class="gear" id="gear-el"><span id="gear-txt">N</span></div>
|
||||
<div class="bars">
|
||||
<div class="bar-item" data-g="1"><span class="label">1</span><div class="fill"><div class="fill-inner"></div></div></div>
|
||||
<div class="bar-item" data-g="2"><span class="label">2</span><div class="fill"><div class="fill-inner"></div></div></div>
|
||||
<div class="bar-item" data-g="3"><span class="label">3</span><div class="fill"><div class="fill-inner"></div></div></div>
|
||||
<div class="bar-item" data-g="4"><span class="label">4</span><div class="fill"><div class="fill-inner"></div></div></div>
|
||||
<div class="bar-item" data-g="5"><span class="label">5</span><div class="fill"><div class="fill-inner"></div></div></div>
|
||||
<div class="bar-item" data-g="6"><span class="label">6</span><div class="fill"><div class="fill-inner"></div></div></div>
|
||||
<div class="bar-item" data-g="7"><span class="label">7</span><div class="fill"><div class="fill-inner"></div></div></div>
|
||||
<div class="bar-item" data-g="8"><span class="label">8</span><div class="fill"><div class="fill-inner"></div></div></div>
|
||||
</div>
|
||||
</div></div></div>
|
||||
<script>
|
||||
const M={aspect_ratio:"1:1",render_mode:"contain"};
|
||||
(function(){
|
||||
const box=document.getElementById('box'),gearTxt=document.getElementById('gear-txt'),gearEl=document.getElementById('gear-el');
|
||||
const proto=location.protocol==='https:'?'wss:':'ws:',wsUrl=proto+'//'+location.host+'/ws';
|
||||
let ws,rt,lastGear=-1;
|
||||
|
||||
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||'1:1'),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,800);ch=cw/ratio;if(ch>vh){ch=Math.min(vh,800);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 g=m.data.gear||0;if(g!==lastGear){lastGear=g;gearEl.classList.add('flash');setTimeout(()=>gearEl.classList.remove('flash'),200)}
|
||||
gearTxt.textContent=g===0?'N':g<0?'R':String(g);
|
||||
document.querySelectorAll('.bar-item').forEach(el=>{el.classList.toggle('active',parseInt(el.dataset.g)===g)})}}catch(e){}};
|
||||
ws.onclose=()=>{rt=setTimeout(connect,2000)};
|
||||
}
|
||||
|
||||
apply();window.addEventListener('resize',apply);connect();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,35 +0,0 @@
|
||||
<div style="display:flex;align-items:center;justify-content:center;height:100%;font-family:'Segoe UI',system-ui,sans-serif;background:linear-gradient(135deg,#0a0a14,#1a1a30);gap:20px;">
|
||||
<div style="width:100px;height:100px;border-radius:50%;background:rgba(102,126,234,0.15);border:3px solid rgba(102,126,234,0.5);display:flex;align-items:center;justify-content:center;box-shadow:0 0 30px rgba(102,126,234,0.2);">
|
||||
<span style="font-size:56px;font-weight:900;color:#fff;" data-bind="gear">N</span>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:6px;">
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<span style="font-size:12px;color:rgba(255,255,255,0.5);width:24px;">R</span>
|
||||
<div style="width:80px;height:6px;background:rgba(255,255,255,0.1);border-radius:3px;"></div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<span style="font-size:12px;color:rgba(255,255,255,0.5);width:24px;">1</span>
|
||||
<div style="width:80px;height:6px;background:rgba(255,255,255,0.1);border-radius:3px;"></div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<span style="font-size:12px;color:rgba(255,255,255,0.5);width:24px;">2</span>
|
||||
<div style="width:80px;height:6px;background:rgba(255,255,255,0.1);border-radius:3px;"></div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<span style="font-size:12px;color:rgba(255,255,255,0.5);width:24px;">3</span>
|
||||
<div style="width:80px;height:6px;background:rgba(255,255,255,0.1);border-radius:3px;"></div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<span style="font-size:12px;color:rgba(255,255,255,0.5);width:24px;">4</span>
|
||||
<div style="width:80px;height:6px;background:rgba(255,255,255,0.1);border-radius:3px;"></div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<span style="font-size:12px;color:rgba(255,255,255,0.5);width:24px;">5</span>
|
||||
<div style="width:80px;height:6px;background:rgba(255,255,255,0.1);border-radius:3px;"></div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<span style="font-size:12px;color:rgba(255,255,255,0.5);width:24px;">6</span>
|
||||
<div style="width:80px;height:6px;background:rgba(255,255,255,0.1);border-radius:3px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,69 @@
|
||||
<!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>
|
||||
*,*::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;color:#fff}
|
||||
#root{width:100%;height:100%;display:flex;align-items:center;justify-content:center}
|
||||
#box{position:relative;overflow:hidden}
|
||||
#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}
|
||||
#content{width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;background:linear-gradient(135deg,#0a0a14,#1a1a30);gap:min(3vw,16px)}
|
||||
.lap-num{font-size:min(3vw,16px);color:rgba(255,255,255,.3);letter-spacing:.15em;margin-bottom:-8px}
|
||||
.current{font-size:min(12vw,72px);font-weight:900;color:#3482FF;font-variant-numeric:tabular-nums}
|
||||
.below{display:flex;gap:min(8vw,48px)}
|
||||
.col{text-align:center}
|
||||
.col .label{font-size:min(2vw,12px);color:rgba(255,255,255,.35);letter-spacing:.08em;margin-bottom:2px}
|
||||
.col .val{font-size:min(5vw,28px);font-weight:700;font-variant-numeric:tabular-nums}
|
||||
.col:last-child .val{color:#34C759}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"><div id="box" class="contain"><div id="content">
|
||||
<div class="lap-num">LAP <span id="lap-n">0</span></div>
|
||||
<div class="current" id="cur">00:00.000</div>
|
||||
<div class="below">
|
||||
<div class="col"><div class="label">LAST</div><div class="val" id="last" style="color:rgba(255,255,255,.55)">00:00.000</div></div>
|
||||
<div class="col"><div class="label">BEST</div><div class="val" id="best">00:00.000</div></div>
|
||||
</div>
|
||||
</div></div></div>
|
||||
<script>
|
||||
const M={aspect_ratio:"4:3",render_mode:"contain"};
|
||||
(function(){
|
||||
const box=document.getElementById('box'),curEl=document.getElementById('cur'),lastEl=document.getElementById('last'),bestEl=document.getElementById('best'),lapN=document.getElementById('lap-n');
|
||||
const proto=location.protocol==='https:'?'wss:':'ws:',wsUrl=proto+'//'+location.host+'/ws';
|
||||
let ws,rt;
|
||||
|
||||
function fmt(t){if(!t||t<=0)return'00:00.000';const m=Math.floor(t/60),s=(t%60).toFixed(3);return String(m).padStart(2,'0')+':'+(m>0?String(s).padStart(6,'0'):s)}
|
||||
|
||||
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||'4:3'),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,1200);ch=cw/ratio;if(ch>vh){ch=Math.min(vh,900);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;curEl.textContent=fmt(d.lap_time);lastEl.textContent=fmt(d.last_lap);bestEl.textContent=fmt(d.best_lap);lapN.textContent=d.lap_number||0}}catch(e){}};
|
||||
ws.onclose=()=>{rt=setTimeout(connect,2000)};
|
||||
}
|
||||
|
||||
apply();window.addEventListener('resize',apply);connect();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,20 +0,0 @@
|
||||
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;font-family:'Segoe UI',system-ui,sans-serif;background:linear-gradient(135deg,#0a0a14,#1a1a30);gap:16px;">
|
||||
<div style="text-align:center;">
|
||||
<div style="font-size:12px;color:rgba(255,255,255,0.4);letter-spacing:2px;margin-bottom:4px;">LAP</div>
|
||||
<div style="font-size:14px;color:rgba(255,255,255,0.3);" data-bind="lap_number">0</div>
|
||||
</div>
|
||||
<div style="text-align:center;">
|
||||
<div style="font-size:11px;color:rgba(255,255,255,0.4);letter-spacing:2px;margin-bottom:4px;">CURRENT</div>
|
||||
<div style="font-size:48px;font-weight:900;color:#667eea;font-variant-numeric:tabular-nums;" data-bind="lap_time">00:00.000</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:40px;">
|
||||
<div style="text-align:center;">
|
||||
<div style="font-size:11px;color:rgba(255,255,255,0.4);letter-spacing:1px;margin-bottom:4px;">LAST</div>
|
||||
<div style="font-size:24px;font-weight:700;color:rgba(255,255,255,0.6);font-variant-numeric:tabular-nums;" data-bind="last_lap">00:00.000</div>
|
||||
</div>
|
||||
<div style="text-align:center;">
|
||||
<div style="font-size:11px;color:rgba(255,255,255,0.4);letter-spacing:1px;margin-bottom:4px;">BEST</div>
|
||||
<div style="font-size:24px;font-weight:700;color:#2ecc71;font-variant-numeric:tabular-nums;" data-bind="best_lap">00:00.000</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,75 @@
|
||||
<!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:auto;--mode:contain}
|
||||
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
|
||||
html,body{width:100%;height:100%;overflow:hidden;background:#000;color:#fff;font-family:'SF Pro Text',-apple-system,'PingFang SC',sans-serif;-webkit-tap-highlight-color:transparent}
|
||||
#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)}
|
||||
.speed{font-size:min(18vw,160px);font-weight:900;line-height:1;color:#fff;text-shadow:0 0 50px rgba(52,130,255,.5);transition:font-size .1s ease}
|
||||
.unit{font-size:min(3vw,28px);color:rgba(255,255,255,.55);margin-top:8px;letter-spacing:.3em}
|
||||
.bar{width:min(80%,400px);height:6px;background:rgba(255,255,255,.1);border-radius:3px;overflow:hidden;margin-top:24px}
|
||||
.bar-fill{height:100%;background:linear-gradient(90deg,#3482FF,#764ba2);border-radius:3px;transition:width .08s linear}
|
||||
/* aspect-ratio variants */
|
||||
#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">
|
||||
<div class="speed" id="v">0</div>
|
||||
<div class="unit">KM/H</div>
|
||||
<div class="bar"><div class="bar-fill" id="bar"></div></div>
|
||||
</div></div></div>
|
||||
<script>
|
||||
const M={aspect_ratio:"auto",render_mode:"contain"};
|
||||
(function(){
|
||||
const box=document.getElementById('box');
|
||||
const vEl=document.getElementById('v');
|
||||
const bar=document.getElementById('bar');
|
||||
const proto=location.protocol==='https:'?'wss:':'ws:';
|
||||
const 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||'auto');
|
||||
const 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;vEl.textContent=(d.speed_kmh||0).toFixed(0);bar.style.width=Math.min((d.speed_kmh||0)/400,1)*100+'%'}}catch(e){}};
|
||||
ws.onclose=()=>{rt=setTimeout(connect,2000)};
|
||||
}
|
||||
|
||||
apply();
|
||||
window.addEventListener('resize',apply);
|
||||
connect();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,7 +0,0 @@
|
||||
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;font-family:'Segoe UI',system-ui,sans-serif;background:linear-gradient(135deg,#0a0a14 0%,#1a1a30 100%);">
|
||||
<div style="font-size:120px;font-weight:900;line-height:1;color:#fff;text-shadow:0 0 40px rgba(102,126,234,0.5);" data-bind="speed_kmh">0</div>
|
||||
<div style="font-size:24px;color:rgba(255,255,255,0.6);margin-top:8px;letter-spacing:4px;">KM/H</div>
|
||||
<div style="margin-top:20px;width:300px;height:6px;background:rgba(255,255,255,0.1);border-radius:3px;overflow:hidden;">
|
||||
<div data-bind-speed style="height:100%;background:linear-gradient(90deg,#667eea,#764ba2);border-radius:3px;transition:width 0.1s ease;width:calc(var(--speed,0) / 400 * 100%);"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,72 @@
|
||||
<!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>
|
||||
@@ -1,35 +0,0 @@
|
||||
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;font-family:'Segoe UI',system-ui,sans-serif;background:linear-gradient(135deg,#0a0a14,#1a1a30);">
|
||||
<svg viewBox="0 0 300 200" width="90%" style="max-width:500px;">
|
||||
<defs>
|
||||
<linearGradient id="rpmGrad" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#667eea"/>
|
||||
<stop offset="40%" stop-color="#667eea"/>
|
||||
<stop offset="70%" stop-color="#f39c12"/>
|
||||
<stop offset="100%" stop-color="#e74c3c"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M 50,170 A 100,100 0 0,1 250,170" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="18" stroke-linecap="round"/>
|
||||
<path id="rpm-arc" d="M 50,170 A 100,100 0 0,1 250,170" fill="none" stroke="url(#rpmGrad)" stroke-width="18" stroke-linecap="round"
|
||||
stroke-dasharray="314" stroke-dashoffset="314" style="transition:stroke-dashoffset 0.1s ease;"/>
|
||||
<text x="150" y="150" text-anchor="middle" fill="#fff" font-size="48" font-weight="900" data-bind="rpm">0</text>
|
||||
<text x="150" y="180" text-anchor="middle" fill="rgba(255,255,255,0.5)" font-size="14">RPM</text>
|
||||
<text x="42" y="185" text-anchor="middle" fill="rgba(255,255,255,0.3)" font-size="10">0</text>
|
||||
<text x="258" y="185" text-anchor="middle" fill="rgba(255,255,255,0.3)" font-size="10">8k</text>
|
||||
</svg>
|
||||
<div data-bind-rpm style="display:none;">
|
||||
<script>
|
||||
(function() {
|
||||
const arc = document.getElementById('rpm-arc');
|
||||
const observer = new MutationObserver(() => {
|
||||
const rpm = parseFloat(getComputedStyle(document.querySelector('[data-bind-rpm]')).getPropertyValue('--rpm')) || 0;
|
||||
const max = parseFloat(getComputedStyle(document.querySelector('[data-bind-rpm]')).getPropertyValue('--rpm-max')) || 8000;
|
||||
if (!isNaN(rpm) && arc) {
|
||||
const pct = Math.min(rpm / max, 1);
|
||||
arc.setAttribute('stroke-dashoffset', 314 - (314 * pct));
|
||||
}
|
||||
});
|
||||
observer.observe(document.querySelector('[data-bind-rpm]'), { attributes: true, attributeFilter: ['style'] });
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user