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:
+64
-64
@@ -18,17 +18,31 @@ const PageSettings = {
|
||||
Toast.show('遥测端口已更新,重启监听后生效', 'info');
|
||||
});
|
||||
|
||||
document.getElementById('settings-server-port')?.addEventListener('change', async (e) => {
|
||||
Toast.show('服务器端口修改后需要重启程序', 'warning');
|
||||
});
|
||||
|
||||
document.getElementById('settings-restart-telemetry')?.addEventListener('click', async () => {
|
||||
await API.stopTelemetry();
|
||||
await API.startTelemetry();
|
||||
Toast.show('遥测监听已重启', 'success');
|
||||
});
|
||||
|
||||
document.getElementById('btn-import-plugin')?.addEventListener('click', () => this._importPlugin());
|
||||
document.getElementById('btn-import-plugin')?.addEventListener('click', () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.tsp';
|
||||
input.onchange = async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
try {
|
||||
const res = await fetch('/api/games/install', { method: 'POST', body: formData });
|
||||
const data = await res.json();
|
||||
if (data.ok) { Toast.show('游戏插件安装成功', 'success'); this.render(); }
|
||||
else Toast.show('安装失败', 'error');
|
||||
} catch (err) { Toast.show('安装失败', 'error'); }
|
||||
};
|
||||
input.click();
|
||||
});
|
||||
|
||||
document.getElementById('btn-reload-plugins')?.addEventListener('click', async () => {
|
||||
await API.reloadGamePlugins();
|
||||
Toast.show('插件已重新加载', 'success');
|
||||
@@ -40,11 +54,11 @@ const PageSettings = {
|
||||
const removeBtn = e.target.closest('.btn-remove-plugin');
|
||||
if (exportBtn) {
|
||||
const pluginId = exportBtn.dataset.pluginId;
|
||||
const data = await API.exportGamePlugin(pluginId);
|
||||
if (data) {
|
||||
this._downloadJson(`plugin_${pluginId}.json`, data);
|
||||
Toast.show('插件已导出', 'success');
|
||||
}
|
||||
const a = document.createElement('a');
|
||||
a.href = `/api/games/${pluginId}/export`;
|
||||
a.download = `${pluginId}.tsp`;
|
||||
a.click();
|
||||
Toast.show('正在下载 .tsp 文件...', 'info');
|
||||
}
|
||||
if (removeBtn) {
|
||||
const pluginId = removeBtn.dataset.pluginId;
|
||||
@@ -55,40 +69,25 @@ const PageSettings = {
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async _importPlugin() {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json';
|
||||
input.onchange = async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
const text = await file.text();
|
||||
const data = JSON.parse(text);
|
||||
if (data.type === 'game_plugin') {
|
||||
await API.installGamePlugin(data);
|
||||
Toast.show('插件安装成功', 'success');
|
||||
this.render();
|
||||
} else {
|
||||
Toast.show('无效的插件文件格式', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
Toast.show('文件解析失败: ' + err.message, 'error');
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
},
|
||||
|
||||
_downloadJson(filename, data) {
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
document.getElementById('btn-import-dashboard')?.addEventListener('click', () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.tsd';
|
||||
input.onchange = async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
try {
|
||||
const res = await fetch('/api/dashboards/import', { method: 'POST', body: formData });
|
||||
const data = await res.json();
|
||||
if (data.ok) Toast.show('仪表盘导入成功', 'success');
|
||||
else Toast.show('导入失败', 'error');
|
||||
} catch (err) { Toast.show('导入失败', 'error'); }
|
||||
};
|
||||
input.click();
|
||||
});
|
||||
},
|
||||
|
||||
_gamePluginListHtml(games) {
|
||||
@@ -107,7 +106,7 @@ const PageSettings = {
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;">
|
||||
<button class="btn btn-sm btn-secondary btn-export-plugin" data-plugin-id="${g.id}">导出</button>
|
||||
<button class="btn btn-sm btn-secondary btn-export-plugin" data-plugin-id="${g.id}">导出 .tsp</button>
|
||||
${!g.is_builtin ? `<button class="btn btn-sm btn-danger btn-remove-plugin" data-plugin-id="${g.id}">移除</button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
@@ -124,25 +123,16 @@ const PageSettings = {
|
||||
<div class="settings-row">
|
||||
<div>
|
||||
<div class="settings-label">遥测监听端口</div>
|
||||
<div class="settings-desc">游戏内设置的数据输出端口</div>
|
||||
<div class="settings-desc">游戏内设置的数据输出端口 (默认 20777)</div>
|
||||
</div>
|
||||
<div class="settings-control">
|
||||
<input type="number" id="settings-telemetry-port" value="${cfg.telemetry_port || 20777}" min="1024" max="65535">
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<div>
|
||||
<div class="settings-label">Web 服务器端口</div>
|
||||
<div class="settings-desc">Web UI 服务的端口号</div>
|
||||
</div>
|
||||
<div class="settings-control">
|
||||
<input type="number" id="settings-server-port" value="${cfg.server_port || 9527}" min="80" max="65535" disabled>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<div>
|
||||
<div class="settings-label">重启遥测监听</div>
|
||||
<div class="settings-desc">修改端口或切换游戏后需要重启监听</div>
|
||||
<div class="settings-desc">修改端口或切换游戏后需要重启</div>
|
||||
</div>
|
||||
<div class="settings-control">
|
||||
<button id="settings-restart-telemetry" class="btn btn-secondary btn-sm">重启监听</button>
|
||||
@@ -150,22 +140,32 @@ const PageSettings = {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3>仪表盘管理</h3>
|
||||
<div style="display:flex;gap:8px;margin-bottom:16px;">
|
||||
<button id="btn-import-dashboard" class="btn btn-secondary btn-sm">导入 .tsd</button>
|
||||
<span style="font-size:12px;color:var(--text-tertiary);display:flex;align-items:center;">
|
||||
右键点击仪表盘卡片即可导出
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3>游戏插件管理</h3>
|
||||
<div style="display:flex;gap:8px;margin-bottom:16px;">
|
||||
<button id="btn-import-plugin" class="btn btn-secondary btn-sm">📥 导入插件</button>
|
||||
<button id="btn-reload-plugins" class="btn btn-secondary btn-sm">🔄 重新加载</button>
|
||||
<button id="btn-import-plugin" class="btn btn-secondary btn-sm">导入 .tsp</button>
|
||||
<button id="btn-reload-plugins" class="btn btn-secondary btn-sm">重新加载</button>
|
||||
</div>
|
||||
<div id="settings-game-list">
|
||||
${this._gamePluginListHtml(games)}
|
||||
</div>
|
||||
<div class="glass-card" style="padding:16px;margin-top:12px;font-size:12px;color:var(--text-tertiary);line-height:1.6;">
|
||||
<strong style="color:var(--text-secondary);">社区开发指南:</strong><br>
|
||||
1. 创建一个包含 <code>manifest.json</code> 和 <code>parser.py</code> 的文件夹<br>
|
||||
2. <code>manifest.json</code> 定义游戏元信息,<code>parser.py</code> 实现 <code>get_parser()</code> 函数<br>
|
||||
3. <code>get_parser()</code> 返回对象需实现 <code>game_id()</code> 和 <code>parse(data, addr)</code> 方法<br>
|
||||
4. 通过"导入插件"或放入 <code>games/user/</code> 目录安装<br>
|
||||
5. 导出你的插件分享给社区!
|
||||
<strong style="color:var(--text-secondary);">社区插件开发指南:</strong><br>
|
||||
1. 创建包含 <code>manifest.json</code> + <code>parser.py</code> 的文件夹<br>
|
||||
2. <code>manifest.json</code> 定义元信息,<code>parser.py</code> 需有 <code>get_parser()</code> 函数<br>
|
||||
3. 返回对象实现 <code>game_id()</code> 和 <code>parse(data, addr)</code> 方法<br>
|
||||
4. 打包时只需要 zip 这两个文件,重命名后缀为 <code>.tsp</code> 即可导入<br>
|
||||
5. 完整的 <code>TelemetryData</code> 字段参考见 <code>server/telemetry/data.py</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -174,7 +174,7 @@ const PageSettings = {
|
||||
<div class="settings-row">
|
||||
<div>
|
||||
<div class="settings-label">TurboSu</div>
|
||||
<div class="settings-desc">赛车遥测仪表盘 v1.0.0</div>
|
||||
<div class="settings-desc">赛车遥测仪表盘 v1.0.0 · Yei.J. (AskaEth)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user