diff --git a/server/api.py b/server/api.py index b9e2d2b..a334ac3 100644 --- a/server/api.py +++ b/server/api.py @@ -481,11 +481,9 @@ async def api_export_recording(filename: str): return FileResponse(path, media_type="application/octet-stream", filename=filename) -@router.post("/recording/import") -async def api_import_recording(file: UploadFile = File(...)): +@router.delete("/recording/{filename}/delete") +async def api_delete_recording(filename: str): from server.recorder import RECORDINGS_DIR - if not file.filename or not file.filename.endswith('.tsr'): - raise HTTPException(400, "Only .tsr files are accepted") - dest = RECORDINGS_DIR / file.filename - dest.write_bytes(await file.read()) + path = RECORDINGS_DIR / filename + if path.exists(): path.unlink() return {"ok": True} diff --git a/static/js/pages/record.js b/static/js/pages/record.js new file mode 100644 index 0000000..6ec099a --- /dev/null +++ b/static/js/pages/record.js @@ -0,0 +1,89 @@ +const PageRecord = { + _el: null, + + init() { this._el = document.getElementById('page-record'); }, + + async render() { + this._el.innerHTML = this._template(); + this._bindEvents(); + this._loadRecordings(); + }, + + async _loadRecordings() { + try { + const res = await fetch('/api/recordings'); + const list = await res.json(); + const el = document.getElementById('rec-file-list'); + if (!el) return; + if (!list.length) { el.innerHTML = '

暂无录制文件

'; return; } + el.innerHTML = list.map(f => { + const size = f.size > 1048576 ? (f.size/1048576).toFixed(1)+'MB' : (f.size/1024).toFixed(1)+'KB'; + return '
' + f.name + '' + size + '导出
'; + }).join(''); + } catch (e) {} + }, + + _bindEvents() { + document.getElementById('btn-rec-start')?.addEventListener('click', async () => { + await fetch('/api/record/start', { method: 'POST' }); + this._updateRecUI(true); + }); + document.getElementById('btn-rec-stop')?.addEventListener('click', async () => { + await fetch('/api/record/stop', { method: 'POST' }); + this._updateRecUI(false); + this._loadRecordings(); + }); + document.getElementById('rec-auto-mode')?.addEventListener('change', async (e) => { + await fetch('/api/record/auto', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: e.target.checked }) }); + }); + document.getElementById('btn-import-rec')?.addEventListener('click', () => { + const input = document.createElement('input'); input.type = 'file'; input.accept = '.tsr'; + input.onchange = async (e) => { + const file = e.target.files[0]; if (!file) return; + const fd = new FormData(); fd.append('file', file); + await fetch('/api/recording/import', { method: 'POST', body: fd }); + Toast.show('导入成功', 'success'); this._loadRecordings(); + }; input.click(); + }); + document.getElementById('rec-file-list')?.addEventListener('click', async (e) => { + const playBtn = e.target.closest('.btn-play-rec'); + const delBtn = e.target.closest('.btn-del-rec'); + if (playBtn) this._playRecording(playBtn.dataset.file); + if (delBtn && confirm('删除?')) { + await fetch('/api/recording/' + delBtn.dataset.file + '/delete', { method: 'DELETE' }); + this._loadRecordings(); + } + }); + document.getElementById('btn-playback-stop')?.addEventListener('click', async () => { + await fetch('/api/playback/stop', { method: 'POST' }); + document.getElementById('playback-bar').style.display = 'none'; + }); + }, + + _updateRecUI(recording) { + document.getElementById('btn-rec-start').style.display = recording ? 'none' : ''; + document.getElementById('btn-rec-stop').style.display = recording ? '' : 'none'; + document.getElementById('rec-status').textContent = recording ? '录制中...' : '就绪'; + }, + + async _playRecording(filename) { + document.getElementById('playback-file').textContent = filename; + document.getElementById('playback-bar').style.display = 'flex'; + await fetch('/api/playback/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ filename }) }); + Toast.show('回放已开始,打开仪表盘查看数据', 'info'); + }, + + _template() { + return '

录制与回放

录制原始 UDP 流到 .tsr 文件,回放调试仪表盘

' + + + '

录制控制

就绪
' + + + '' + + + '

录制文件

' + + + '
'; + } +}; + +window.PageRecord = PageRecord; diff --git a/static/js/pages/settings.js b/static/js/pages/settings.js index 1bb8fbc..18b54be 100644 --- a/static/js/pages/settings.js +++ b/static/js/pages/settings.js @@ -242,7 +242,6 @@ const PageSettings = { { id: 'sec-plugins', label: '游戏插件' }, { id: 'sec-backup', label: '数据备份' }, { id: 'sec-dev', label: '开发工具' }, - { id: 'sec-record', label: '录制回放' }, { id: 'sec-system', label: '系统' }, ]; @@ -260,7 +259,8 @@ const PageSettings = { '

数据备份

导出全量备份
打包仪表盘+场景+配置为 .tsb
恢复备份
从 .tsb 文件恢复
' + - '

录制与回放

录制原始 UDP 流到 .tsr 文件,离线回放调试仪表盘

录制状态
未启动

回放文件

' + + + '

开发工具

UI 测试模式
本地随机模拟遥测数据
' + '

系统

重启 TurboSu
重新加载所有代码和配置
' + diff --git a/static/js/router.js b/static/js/router.js index 3eb1520..942e38e 100644 --- a/static/js/router.js +++ b/static/js/router.js @@ -4,6 +4,7 @@ const Router = { dashboard: PageDashboard, scene: PageScene, debug: PageDebug, + record: PageRecord, settings: PageSettings, }, _currentPage: 'home', diff --git a/templates/index.html b/templates/index.html index 197068a..d86e78d 100644 --- a/templates/index.html +++ b/templates/index.html @@ -57,6 +57,10 @@ 数据测试 + + + 录制回放 + 设置 @@ -97,6 +101,8 @@
+
+
@@ -114,6 +120,7 @@ +