From 676df30c67179699122e449dd54bbd9788d49288 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=93=E9=9B=A8=E9=B9=8F?= <846149189@qq.com> Date: Mon, 15 Jun 2026 12:01:14 +0800 Subject: [PATCH] =?UTF-8?q?@=20feat(web):=20=E6=BC=94=E5=87=BA=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=20Timeline=20=E5=8A=A0=E3=80=8C=E5=9C=BA=E6=99=AF?= =?UTF-8?q?=E5=BA=95=E5=9B=BE=E3=80=8D=E9=80=89=E6=8B=A9=E5=99=A8(venue=20?= =?UTF-8?q?=E7=89=B9=E5=86=99)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端 /api/sceneshots:列 SceneShots 全部俯视底图(venue 特写) name->{url,bounds} - timeline.js:底图优先级 ir.stage.backdrop(venue) > 点位集默认 shot; 顶栏加底图下拉 renderMapInfo + applyBackdrop(换底+改投影范围+重画+回调) - app.js:拉 /api/sceneshots;performSelect 传入;saveBackdrop 写 ir.stage.backdrop 并 PUT - venue 特写与点位集同 map-local → 换底图后锚点自动落对位(无头实拍擂台验证) - ir.stage.backdrop 是编辑器元数据:validate 不读、compile 不碰 @ --- web/app.py | 15 ++++++++++ web/static/app.js | 19 +++++++++++- web/static/style.css | 4 ++- web/static/timeline.js | 65 ++++++++++++++++++++++++++++++++++++++---- 4 files changed, 96 insertions(+), 7 deletions(-) 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 = '