const PageScene = { _el: null, _currentGameId: null, _currentGameName: '', _editorSceneId: null, _editorCanvasIdx: 0, _placements: [], _dashboards: [], _dragItem: null, _dragOffsetX: 0, _dragOffsetY: 0, _resizeItem: null, _resizeDir: '', init() { this._el = document.getElementById('page-scene'); }, async render() { const cfg = await API.getConfig(); this._currentGameId = cfg.selected_game_id; if (this._currentGameId) { const games = await API.getGames(); const game = games.find(g => g.id === this._currentGameId); this._currentGameName = game ? game.name : this._currentGameId; this._dashboards = await API.getDashboards('all'); } this._el.innerHTML = this._template(); this._showSceneList(); this._bindEvents(); }, async _showSceneList() { document.getElementById('scene-list-container').style.display = 'block'; document.getElementById('scene-editor-container').style.display = 'none'; const grid = document.getElementById('scene-grid'); if (!grid) return; try { const scenes = await API.getScenes(this._currentGameId); if (!scenes || scenes.length === 0) { grid.innerHTML = `

暂无场景

点击"新建场景"创建你的场景

`; return; } grid.innerHTML = scenes.map(s => this._sceneCardHtml(s)).join(''); } catch (e) { grid.innerHTML = '

加载失败

'; } }, async _openEditor(sceneId) { this._editorSceneId = sceneId; this._editorCanvasIdx = 0; this._placements = []; let scene = null; if (sceneId) { scene = await API.getScene(sceneId); if (scene && scene.canvases && scene.canvases[0]) { this._placements = scene.canvases[0].placements || []; } } const listC = document.getElementById('scene-list-container'); const editorC = document.getElementById('scene-editor-container'); listC.style.display = 'none'; editorC.style.display = 'flex'; this._renderCanvasEditor(scene); }, _renderCanvasEditor(scene) { const nameInput = document.getElementById('edit-scene-name'); if (nameInput && scene) nameInput.value = scene.name || ''; this._renderDashboardList(); this._renderCanvas(); }, _renderDashboardList() { const list = document.getElementById('editor-dash-list'); if (!list) return; const filtered = this._dashboards.filter(d => d.supported_games === 'all' || (d.supported_games||'').split(',').includes(this._currentGameId)); list.innerHTML = filtered.map(d => `
${d.icon||'📊'} ${d.name}
` ).join('') || '

无兼容仪表盘

'; list.querySelectorAll('.editor-dash-item').forEach(el => { el.addEventListener('dragstart', (e) => { this._dragItem = { dashId: el.dataset.dashId, isNew: true }; }); }); }, _renderCanvas() { const canvas = document.getElementById('editor-canvas'); if (!canvas) return; const cw = 1920, ch = 1080; const scale = Math.min(canvas.parentElement.clientWidth / cw, 1); canvas.style.width = (cw * scale) + 'px'; canvas.style.height = (ch * scale) + 'px'; canvas.innerHTML = this._placements.map((p, idx) => { const dash = this._dashboards.find(d => d.id === p.dashboard_id); const name = dash ? dash.name : p.dashboard_id; const icon = dash ? (dash.icon || '📊') : '❓'; return `
${icon}
${name}
`; }).join(''); canvas.querySelectorAll('.editor-placement').forEach(el => { const idx = parseInt(el.dataset.idx); el.addEventListener('mousedown', (e) => this._startDragPlace(e, idx, el)); el.addEventListener('click', (e) => { if (e.target.closest('[data-action="remove-place"]')) { this._placements.splice(idx, 1); this._renderCanvas(); } }); }); canvas.addEventListener('dragover', (e) => e.preventDefault()); canvas.addEventListener('drop', (e) => { e.preventDefault(); const rect = canvas.getBoundingClientRect(); const x = (e.clientX - rect.left) / scale; const y = (e.clientY - rect.top) / scale; if (this._dragItem && this._dragItem.isNew) { this._placements.push({ dashboard_id: this._dragItem.dashId, x, y, width: 300, height: 200, z_index: this._placements.length }); this._renderCanvas(); } }); }, _startDragPlace(e, idx, el) { if (e.target.closest('button')) return; const canvasEl = document.getElementById('editor-canvas'); const rect = canvasEl.getBoundingClientRect(); const scale = parseFloat(canvasEl.style.width) / 1920; const startX = e.clientX, startY = e.clientY; const origP = { ...this._placements[idx] }; const onMove = (ev) => { const dx = (ev.clientX - startX) / scale; const dy = (ev.clientY - startY) / scale; this._placements[idx].x = origP.x + dx; this._placements[idx].y = origP.y + dy; el.style.left = (this._placements[idx].x * scale) + 'px'; el.style.top = (this._placements[idx].y * scale) + 'px'; }; const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }, async _saveScene() { const name = document.getElementById('edit-scene-name')?.value || 'New Scene'; const data = { name, game_id: this._currentGameId || '', canvases: [{ width: 1920, height: 1080, label: '16:9', placements: this._placements }] }; try { if (this._editorSceneId) { await API.updateScene(this._editorSceneId, data); } else { const created = await API.createScene(data); this._editorSceneId = created.id; } Toast.show('场景保存成功', 'success'); this._showSceneList(); } catch (e) { Toast.show('保存失败: ' + e.message, 'error'); } }, _bindEvents() { document.getElementById('btn-new-scene')?.addEventListener('click', () => this._openEditor(null)); document.getElementById('btn-import-scene')?.addEventListener('click', () => { const input = document.createElement('input'); input.type = 'file'; input.accept = '.tss'; input.onchange = async (e) => { const file = e.target.files[0]; if (!file) return; const fd = new FormData(); fd.append('file', file); const res = await fetch('/api/scenes/import', { method: 'POST', body: fd }); const data = await res.json(); if (data.ok) { Toast.show('场景导入成功', 'success'); this._showSceneList(); } else Toast.show('导入失败', 'error'); }; input.click(); }); document.getElementById('btn-back-to-list')?.addEventListener('click', () => this._showSceneList()); document.getElementById('btn-save-scene')?.addEventListener('click', () => this._saveScene()); document.getElementById('scene-grid')?.addEventListener('click', (e) => { const card = e.target.closest('[data-scene-id]'); if (!card) return; const action = card.dataset.action, sceneId = card.dataset.sceneId; if (action === 'render') this._renderScene(sceneId); else if (action === 'edit') this._openEditor(sceneId); else if (action === 'delete') { if (confirm('确定删除?')) { API.deleteScene(sceneId); this._showSceneList(); } } else if (action === 'export') { const a = document.createElement('a'); a.href = '/api/scenes/' + sceneId + '/export'; a.download = sceneId + '.tss'; a.click(); } }); }, _renderScene(sceneId) { const url = `${location.origin}/scene/${sceneId}`; window.open(url, '_blank'); navigator.clipboard.writeText(url).then(() => Toast.show('链接已复制', 'success')).catch(() => {}); }, _sceneCardHtml(scene) { const canvasCount = (scene.canvases || []).length; const preview = scene.preview_ext ? `/scene/${scene.id}/preview.${scene.preview_ext}` : ''; return `
${preview ? `
` : ''}
${scene.name}
${this._currentGameName || '未关联游戏'}
${canvasCount} 画布${scene.updated_at ? new Date(scene.updated_at).toLocaleDateString() : ''}
${(scene.canvases||[]).map(c => `${c.label||c.width+'x'+c.height}`).join('')}
`; }, _template() { return `

场景管理

${this._currentGameId ? `当前游戏: ${this._currentGameName}` : '请先在侧边栏选择一个游戏'}

`; } }; window.PageScene = PageScene;