feat: TurboSu initial release - racing telemetry dashboard

This commit is contained in:
Yei.J. (AskaEth)
2026-07-25 15:23:40 +08:00
commit d61bb61a83
61 changed files with 5270 additions and 0 deletions
+221
View File
@@ -0,0 +1,221 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>TurboSu - {{ theme.name if theme else 'Dashboard' }}</title>
<style>
:root {
--aspect-ratio: {{ theme.config.aspect_ratio | default('auto') if theme else 'auto' }};
--render-mode: {{ theme.config.render_mode | default('contain') if theme else 'contain' }};
}
* { 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', 'MiSans', system-ui, sans-serif;
-webkit-tap-highlight-color: transparent;
}
#dashboard-root {
width: 100%; height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
#dashboard-letterbox {
position: relative;
overflow: hidden;
}
#dashboard-content {
width: 100%; height: 100%;
}
.loading {
display: flex; align-items: center; justify-content: center;
height: 100%; font-size: 24px; color: #888;
}
/* Aspect ratio constraint: "contain" mode */
#dashboard-letterbox.contain {
max-width: 100vw;
max-height: 100vh;
}
/* Aspect ratio constraint: "cover" mode */
#dashboard-letterbox.cover {
min-width: 100vw;
min-height: 100vh;
}
/* Aspect ratio constraint: "fill" mode */
#dashboard-letterbox.fill {
width: 100vw;
height: 100vh;
}
/* Aspect ratio constraint: "center" mode */
#dashboard-letterbox.center {
width: auto;
height: auto;
}
</style>
</head>
<body>
<div id="dashboard-root">
<div id="dashboard-letterbox" class="{{ theme.config.render_mode | default('contain') if theme else 'contain' }}">
<div id="dashboard-content">
{{ template_html | safe if template_html else '<div class="loading">加载仪表盘中...</div>' }}
</div>
</div>
</div>
<script>
const THEME_ID = "{{ theme.id if theme else '' }}";
const THEME_CONFIG = {{ theme.config | tojson if theme and theme.config else '{}' }};
</script>
<script>
(function() {
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${location.host}/ws`;
const letterbox = document.getElementById('dashboard-letterbox');
const root = document.getElementById('dashboard-root');
let ws;
let reconnectTimer;
function parseAspectRatio(ratio) {
if (!ratio || ratio === 'auto') return null;
const parts = ratio.split(':');
if (parts.length === 2) {
const w = parseFloat(parts[0]);
const h = parseFloat(parts[1]);
if (w > 0 && h > 0) return w / h;
}
return null;
}
function applyAspectRatio() {
const ratio = parseAspectRatio(THEME_CONFIG.aspect_ratio || 'auto');
const mode = THEME_CONFIG.render_mode || 'contain';
if (!ratio) {
letterbox.style.width = '100vw';
letterbox.style.height = '100vh';
letterbox.classList.add('fill');
return;
}
const vw = window.innerWidth;
const vh = window.innerHeight;
const vr = vw / vh;
let cw, ch;
switch (mode) {
case 'contain':
if (vr > ratio) {
ch = vh;
cw = vh * ratio;
} else {
cw = vw;
ch = vw / ratio;
}
letterbox.classList.add('contain');
break;
case 'cover':
if (vr > ratio) {
cw = vw;
ch = vw / ratio;
} else {
ch = vh;
cw = vh * ratio;
}
letterbox.classList.add('cover');
break;
case 'fill':
letterbox.style.width = '100vw';
letterbox.style.height = '100vh';
letterbox.classList.add('fill');
return;
case 'center':
const maxW = THEME_CONFIG.max_width || 1920;
const maxH = THEME_CONFIG.max_height || 1080;
cw = Math.min(vw, maxW);
ch = cw / ratio;
if (ch > vh) {
ch = Math.min(vh, maxH);
cw = ch * ratio;
}
letterbox.classList.add('center');
break;
default:
if (vr > ratio) {
ch = vh;
cw = vh * ratio;
} else {
cw = vw;
ch = vw / ratio;
}
letterbox.classList.add('contain');
}
if (mode !== 'fill') {
letterbox.style.width = cw + 'px';
letterbox.style.height = ch + 'px';
}
}
function connect() {
ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.log('[Dashboard] WS connected');
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
};
ws.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
if (msg.type === 'telemetry') updateDashboard(msg.data);
} catch(e) {}
};
ws.onclose = () => {
console.log('[Dashboard] WS closed, reconnecting...');
reconnectTimer = setTimeout(connect, 2000);
};
ws.onerror = (e) => console.error('[Dashboard] WS error', e);
}
function updateDashboard(data) {
document.querySelectorAll('[data-bind]').forEach(el => {
const bind = el.getAttribute('data-bind');
const value = bind.split('.').reduce((o, k) => o?.[k], data);
if (value !== undefined && value !== null) {
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
el.value = typeof value === 'number' ? value.toFixed(1) : value;
} else {
el.textContent = typeof value === 'number' ? value.toFixed(1) : value;
}
}
});
document.querySelectorAll('[data-bind-rpm]').forEach(el => {
const value = data.rpm || 0;
const max = data.max_rpm || 8000;
const pct = Math.min(value / max, 1);
el.style.setProperty('--rpm-pct', pct);
el.style.setProperty('--rpm', value);
el.style.setProperty('--rpm-max', max);
});
document.querySelectorAll('[data-bind-speed]').forEach(el => {
el.style.setProperty('--speed', data.speed_kmh || 0);
});
document.querySelectorAll('[data-bind-gear]').forEach(el => {
const g = data.gear || 0;
el.style.setProperty('--gear', g);
});
}
applyAspectRatio();
window.addEventListener('resize', applyAspectRatio);
connect();
})();
</script>
</body>
</html>