Files
TurboSu/static/js/pages/scene.js
T

269 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = `<div class="empty-state" style="grid-column:1/-1"><h4>暂无场景</h4><p>点击"新建场景"创建你的场景</p></div>`;
return;
}
grid.innerHTML = scenes.map(s => this._sceneCardHtml(s)).join('');
} catch (e) { grid.innerHTML = '<div class="empty-state"><p>加载失败</p></div>'; }
},
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 =>
`<div class="glass-card editor-dash-item" draggable="true" data-dash-id="${d.id}" style="padding:10px;margin-bottom:6px;cursor:grab;font-size:13px;display:flex;align-items:center;gap:8px">
<span>${d.icon||'📊'}</span>
<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${d.name}</span>
</div>`
).join('') || '<p style="color:var(--text-tertiary);font-size:12px">无兼容仪表盘</p>';
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 `<div class="editor-placement" data-idx="${idx}" style="left:${p.x*scale}px;top:${p.y*scale}px;width:${p.width*scale}px;height:${p.height*scale}px;z-index:${p.z_index||0}">
<div class="ep-icon">${icon}</div>
<div class="ep-name">${name}</div>
<div class="ep-actions">
<button class="btn btn-sm btn-danger" data-action="remove-place" data-idx="${idx}">×</button>
</div>
</div>`;
}).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 `<div class="glass-card scene-card" data-scene-id="${scene.id}" data-action="render">
${preview ? `<div style="height:120px;background:url(${preview}) center/cover;border-radius:8px 8px 0 0"></div>` : ''}
<div class="scene-card-name" style="${preview?'':'padding-top:18px'}">${scene.name}</div>
<div class="scene-card-game" style="margin-bottom:6px">${this._currentGameName || '未关联游戏'}</div>
<div class="scene-card-meta"><span>${canvasCount} 画布</span><span>${scene.updated_at ? new Date(scene.updated_at).toLocaleDateString() : ''}</span></div>
<div class="scene-canvas-list">${(scene.canvases||[]).map(c => `<span class="scene-canvas-badge">${c.label||c.width+'x'+c.height}</span>`).join('')}</div>
<div style="margin-top:12px;display:flex;gap:6px;flex-wrap:wrap">
<button class="btn btn-sm btn-primary" data-scene-id="${scene.id}" data-action="render">渲染</button>
<button class="btn btn-sm btn-secondary" data-scene-id="${scene.id}" data-action="edit">编辑</button>
<button class="btn btn-sm btn-secondary" data-scene-id="${scene.id}" data-action="export">导出</button>
<button class="btn btn-sm btn-danger" data-scene-id="${scene.id}" data-action="delete">删除</button>
</div></div>`;
},
_template() {
return `
<div class="scene-header animated" id="scene-list-container">
<div>
<h2>场景管理</h2>
<p style="color:var(--text-secondary);font-size:14px;margin-top:4px">${this._currentGameId ? `当前游戏: ${this._currentGameName}` : '请先在侧边栏选择一个游戏'}</p>
</div>
<div style="display:flex;gap:8px">
<button id="btn-import-scene" class="btn btn-secondary btn-sm">导入 .tss</button>
<button id="btn-new-scene" class="btn btn-primary btn-sm" ${!this._currentGameId ? 'disabled' : ''}>+ 新建场景</button>
</div>
</div>
<div id="scene-grid" class="scene-grid animated"></div>
<div id="scene-editor-container" style="display:none;flex-direction:column;height:100%">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">
<div style="display:flex;align-items:center;gap:12px">
<button id="btn-back-to-list" class="btn btn-sm btn-secondary">← 返回</button>
<input id="edit-scene-name" placeholder="场景名称" style="font-size:18px;font-weight:600;width:250px" value="New Scene">
</div>
<button id="btn-save-scene" class="btn btn-primary btn-sm">保存场景</button>
</div>
<div style="display:flex;gap:12px;flex:1;min-height:0">
<div style="width:180px;flex-shrink:0;overflow-y:auto;border-right:1px solid var(--border-subtle);padding-right:10px">
<h4 style="font-size:13px;color:var(--text-secondary);margin-bottom:10px">仪表盘列表 (拖到画布)</h4>
<div id="editor-dash-list"></div>
</div>
<div style="flex:1;overflow:auto;background:var(--bg-tertiary);border-radius:8px;position:relative">
<div id="editor-canvas" style="position:relative;background:rgba(255,255,255,.02);margin:0 auto;overflow:hidden;transition:width 0.2s,height 0.2s"></div>
</div>
</div>
</div>`;
}
};
window.PageScene = PageScene;