63e3c8d607
- 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
221 lines
8.7 KiB
JavaScript
221 lines
8.7 KiB
JavaScript
const PageScene = {
|
|
_el: null,
|
|
_currentGameId: null,
|
|
_currentGameName: '',
|
|
_editorSceneId: null,
|
|
_editorData: null,
|
|
|
|
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._el.innerHTML = this._template();
|
|
await this._loadScenes();
|
|
this._bindEvents();
|
|
},
|
|
|
|
async _loadScenes() {
|
|
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;">
|
|
<svg viewBox="0 0 24 24" width="64" height="64"><path d="M3 3h8v8H3zm10 0h8v8h-8zM3 13h8v8H3zm10 0h8v8h-8z" fill="currentColor"/></svg>
|
|
<h4>暂无场景</h4>
|
|
<p>点击"新建场景"创建你的第一个场景布局</p>
|
|
</div>`;
|
|
return;
|
|
}
|
|
|
|
grid.innerHTML = scenes.map(s => this._sceneCardHtml(s)).join('');
|
|
} catch (e) {
|
|
console.error('Failed to load scenes:', e);
|
|
}
|
|
},
|
|
|
|
_bindEvents() {
|
|
document.getElementById('btn-new-scene')?.addEventListener('click', () => this._showEditor());
|
|
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 formData = new FormData();
|
|
formData.append('file', file);
|
|
try {
|
|
const res = await fetch('/api/scenes/import', { method: 'POST', body: formData });
|
|
const data = await res.json();
|
|
if (data.ok) { Toast.show('场景导入成功', 'success'); this._loadScenes(); }
|
|
else Toast.show('导入失败', 'error');
|
|
} catch (err) { Toast.show('导入失败', 'error'); }
|
|
};
|
|
input.click();
|
|
});
|
|
|
|
const grid = document.getElementById('scene-grid');
|
|
if (grid) {
|
|
grid.addEventListener('click', (e) => {
|
|
const card = e.target.closest('[data-scene-id]');
|
|
if (!card) return;
|
|
const action = card.dataset.action;
|
|
const sceneId = card.dataset.sceneId;
|
|
|
|
if (action === 'render') {
|
|
this._renderScene(sceneId);
|
|
} else if (action === 'edit') {
|
|
e.stopPropagation();
|
|
this._showEditor(sceneId);
|
|
} else if (action === 'delete') {
|
|
e.stopPropagation();
|
|
this._deleteScene(sceneId);
|
|
} else if (action === 'export') {
|
|
e.stopPropagation();
|
|
this._exportScene(sceneId);
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
async _showEditor(sceneId) {
|
|
let scene = null;
|
|
if (sceneId) {
|
|
scene = await API.getScene(sceneId);
|
|
if (!scene) return;
|
|
}
|
|
|
|
this._editorSceneId = sceneId;
|
|
const modal = document.createElement('div');
|
|
modal.className = 'modal-overlay';
|
|
modal.innerHTML = this._editorTemplate(scene);
|
|
document.body.appendChild(modal);
|
|
|
|
modal.querySelector('.modal-overlay, .modal-actions .btn-secondary')
|
|
?.addEventListener('click', (e) => {
|
|
if (e.target === modal || e.target.matches('.btn-secondary')) {
|
|
modal.remove();
|
|
}
|
|
});
|
|
|
|
modal.querySelector('.btn-primary')?.addEventListener('click', async () => {
|
|
const name = modal.querySelector('#edit-scene-name').value || 'New Scene';
|
|
const desc = modal.querySelector('#edit-scene-desc').value || '';
|
|
const gameId = this._currentGameId || '';
|
|
|
|
const data = { name, description: desc, game_id: gameId };
|
|
if (this._editorSceneId) {
|
|
await API.updateScene(this._editorSceneId, data);
|
|
} else {
|
|
await API.createScene(data);
|
|
}
|
|
modal.remove();
|
|
Toast.show('场景保存成功', 'success');
|
|
this.render();
|
|
});
|
|
},
|
|
|
|
async _renderScene(sceneId) {
|
|
const url = `${location.origin}/scene/${sceneId}`;
|
|
window.open(url, '_blank');
|
|
navigator.clipboard.writeText(url).then(() => {
|
|
Toast.show('场景链接已复制,可在局域网设备访问', 'success');
|
|
}).catch(() => {});
|
|
},
|
|
|
|
async _deleteScene(sceneId) {
|
|
if (!confirm('确定删除此场景?')) return;
|
|
await API.deleteScene(sceneId);
|
|
Toast.show('场景已删除', 'info');
|
|
this._loadScenes();
|
|
},
|
|
|
|
_exportScene(sceneId) {
|
|
const a = document.createElement('a');
|
|
a.href = `/api/scenes/${sceneId}/export`;
|
|
a.download = `${sceneId}.tss`;
|
|
a.click();
|
|
Toast.show('正在下载 .tss 文件...', 'info');
|
|
},
|
|
|
|
_sceneCardHtml(scene) {
|
|
const canvasCount = (scene.canvases || []).length;
|
|
return `
|
|
<div class="glass-card scene-card" data-scene-id="${scene.id}" data-action="render">
|
|
<div class="scene-card-name">${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;">
|
|
<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>`;
|
|
},
|
|
|
|
_editorTemplate(scene) {
|
|
const name = scene ? scene.name : '';
|
|
const desc = scene ? scene.description : '';
|
|
return `
|
|
<div class="modal">
|
|
<h3>${scene ? '编辑场景' : '新建场景'}</h3>
|
|
<div class="form-group">
|
|
<label>场景名称</label>
|
|
<input id="edit-scene-name" type="text" value="${name}" placeholder="输入场景名称">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>描述</label>
|
|
<textarea id="edit-scene-desc" placeholder="场景描述(可选)">${desc}</textarea>
|
|
</div>
|
|
<p style="font-size:12px;color:var(--text-tertiary);margin-top:8px;">
|
|
提示:保存后可在场景编辑器中添加仪表盘、调整布局和画布比例。
|
|
</p>
|
|
<div class="modal-actions">
|
|
<button class="btn btn-secondary">取消</button>
|
|
<button class="btn btn-primary">保存</button>
|
|
</div>
|
|
</div>`;
|
|
},
|
|
|
|
_template() {
|
|
return `
|
|
<div class="scene-header animated">
|
|
<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>`;
|
|
}
|
|
};
|
|
|
|
window.PageScene = PageScene;
|