diff --git a/web/app.py b/web/app.py index f4f8afa..f7b37f1 100644 --- a/web/app.py +++ b/web/app.py @@ -219,6 +219,21 @@ async def pointsets(): return out +@app.get("/api/sceneshots") +async def sceneshots(): + """列出 SceneShots 目录里所有可用俯视底图(venue 特写等):name -> {url,bounds,w,h}。 + 供演出配置「场景底图」选择器:事件可挑一张当 Timeline 底图(写进 ir.stage.backdrop)。""" + out = {} + if os.path.isdir(_SCENESHOTS_DIR): + for fn in os.listdir(_SCENESHOTS_DIR): + if fn.endswith(".shot.json"): + name = fn[: -len(".shot.json")] + shot = _load_shot(name) + if shot: + out[name] = shot + return out + + @app.get("/sceneshot/{name}.png") async def sceneshot(name: str): # 防目录穿越:只认纯文件名 diff --git a/web/static/app.js b/web/static/app.js index 02309ad..04e28b5 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -3,6 +3,7 @@ const App = { dict: { conditions: {}, grants: {} }, pointsets: {}, // name -> {mapId, points:[]} + sceneshots: {}, // name -> {url,bounds,w,h}(演出配置「场景底图」可选项) events: [], current: null, // 当前 group ir: null, // 工作副本 @@ -42,6 +43,7 @@ try { App.dict = await (await api("/api/dictionary")).json(); App.pointsets = await (await api("/api/pointsets")).json(); + try { App.sceneshots = await (await api("/api/sceneshots")).json(); } catch (_) { App.sceneshots = {}; } } catch (e) { return; } await loadList(); } @@ -252,12 +254,27 @@ host.appendChild(d); }); } + let performIr = null; // 演出配置当前事件 IR(底图选择写这里并持久化) async function performSelect(group) { let d; try { d = await (await api("/api/events/" + encodeURIComponent(group))).json(); } catch (e) { return; } performCurrent = group; + performIr = d.ir; performLoadList(); - Timeline.show($("perform-main"), d.ir, App.dict, App.pointsets); + Timeline.show($("perform-main"), d.ir, App.dict, App.pointsets, { + sceneshots: App.sceneshots, + onPickBackdrop: name => saveBackdrop(group, name), + }); + } + // 演出配置选底图 → 写 ir.stage.backdrop(编辑器元数据,validate/compile 都不读)并 PUT 持久化。 + async function saveBackdrop(group, name) { + if (!performIr || group !== performCurrent) return; + performIr.stage = performIr.stage || {}; + if (name) performIr.stage.backdrop = name; else delete performIr.stage.backdrop; + try { + await api("/api/events/" + encodeURIComponent(group), { method: "PUT", body: JSON.stringify({ ir: performIr, by: App.by }) }); + toast(name ? ("底图已设为 " + name) : "已恢复默认底图"); + } catch (e) { toast("底图保存失败"); } } // ---------- 导入 ---------- diff --git a/web/static/style.css b/web/static/style.css index 67226e5..8e83670 100644 --- a/web/static/style.css +++ b/web/static/style.css @@ -323,7 +323,9 @@ body.points-mode .mode-switch { border-color:#4a52a0; } /* ---- 演出预览:上=舞台面板(自适应放大) / 下=时间轴面板(可拖高度) ---- */ .tl-stagepanel { flex:1 1 auto; min-height:150px; display:flex; flex-direction:column; } -.tl-mapinfo { flex:none; font-size:12px; color:#9a8f7e; margin-bottom:6px; } +.tl-mapinfo { flex:none; font-size:12px; color:#9a8f7e; margin-bottom:6px; display:flex; align-items:center; flex-wrap:wrap; gap:4px; } +.tl-bd-label { margin-left:10px; color:#7fae9c; } +.tl-bd-select { background:#1d1a13; color:#d8cda0; border:1px solid #4a4534; border-radius:4px; padding:1px 4px; font-size:12px; max-width:200px; } .tl-stagewrap { position:relative; flex:1; min-height:0; background:#15130d; border:1px solid #3a322a; border-radius:6px; overflow:hidden; } .tl-stage { width:100%; height:100%; object-fit:contain; display:block; } diff --git a/web/static/timeline.js b/web/static/timeline.js index 1f2b175..2b1f8f3 100644 --- a/web/static/timeline.js +++ b/web/static/timeline.js @@ -305,6 +305,9 @@ // ===================== 状态 & 挂载 ===================== let S, model, playT = 0, playing = false, rafId = 0, lastTs = 0, stageCv, stageCtx, els = {}, pending = null, selNode = null, PX = 80, fitMode = false, lastDecisionId = null; let shotBg = null, shotReady = false, shotBounds = null; // 真实场景正交俯视底图 + // 场景底图选择器:SHOTS=所有可选 venue 特写({name:{url,bounds}});curBackdrop=当前选中底图名(""=点位集默认); + // onPick=切换底图回调(app.js 写 ir.stage.backdrop 并持久化);baseBounds=无底图时回退的投影范围。 + let SHOTS = {}, curPsName = "", curPs = {}, curBackdrop = "", onPick = null, baseBounds = null; const TEMPLATE = '