feat(web): 海选按场景分组 + 删场景点位页签 + 演出真实底图 + 破缓存

- 海选审核左侧改两列:场景列(按新字段 ir.scene 手动归类聚合,含全部/未分类) + 该场景事件列
- 删独立「场景/点位」页签(pointview.js 保留未引用)
- 演出配置 Timeline 接真实场景俯视底图(setupShot 覆盖投影范围 + drawStage 叠图,复用 /api/pointsets 的 shot)
- 事件 meta 加「所属场景」归类输入框(datalist 提示已有场景名)
- db: events 加 scene 列 + 旧库 ALTER 迁移;upsert 镜像 ir.scene;list 返回
- app.py: 首页按文件 mtime 给 js/css 注入 ?v= 破浏览器缓存(根治新html配旧缓存js崩溃→弹口令)
This commit is contained in:
2026-06-15 11:46:59 +08:00
parent 603f78b77f
commit 65424a4dfb
7 changed files with 137 additions and 71 deletions

View File

@ -304,6 +304,7 @@
// ===================== 状态 & 挂载 =====================
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; // 真实场景正交俯视底图
const TEMPLATE =
'<div class="tl-stagepanel">' +
@ -326,6 +327,7 @@
const psName = (IR.stage || {}).point_set || IR.id;
const ps = (POINTSETS || {})[psName] || {};
S = prepare(IR, ps.anchors || []); model = S;
setupShot(ps.shot); // 有真实场景俯视底图 → 覆盖投影范围并异步加载drawStage 当舞台底
host.innerHTML = TEMPLATE;
els = {
@ -344,7 +346,8 @@
playhead: null,
};
els.mapinfo.textContent = "点位集:" + psName + (ps.mapId ? "(地图 " + ps.mapId + "" : "") +
(S.synthetic ? " · ⚠ 未取到真实坐标,按示意布局自动铺开(走位仍可预览)" : " · 真实坐标");
(S.synthetic ? " · ⚠ 未取到真实坐标,按示意布局自动铺开(走位仍可预览)"
: (shotBounds ? " · 真实坐标 · 已叠场景俯视底图" : " · 真实坐标"));
stageCv = els.stage; stageCtx = stageCv.getContext("2d");
els.play.onclick = () => playing ? stopPlay() : play();
@ -405,6 +408,20 @@
}
function clear() { stopPlay(); if (els.host) els.host.innerHTML = ""; els = {}; model = S = null; }
// 真实场景俯视底图shot={url,bounds:[minX,maxX,minZ,maxZ]}Unity 抓拍产出)。
// 有底图时把投影范围覆盖成底图覆盖的 map-local 范围 → actor/锚点像素级落在真实场景上。
function setupShot(shot) {
shotBg = null; shotReady = false; shotBounds = null;
if (!shot || !Array.isArray(shot.bounds) || shot.bounds.length !== 4) return;
const b = shot.bounds;
shotBounds = { minX: b[0], maxX: b[1], minZ: b[2], maxZ: b[3] };
S.bounds = shotBounds; // 与底图对齐(口径同 pointview屏幕右=+X、上=+Z
shotBg = new Image();
shotBg.onload = () => { shotReady = true; if (stageCv) renderFrame(); };
shotBg.onerror = () => { shotReady = false; };
shotBg.src = shot.url;
}
function restart() { startFrom(firstNode(S.IR)); }
function refreshTimeline() { orderRows(S); buildTracks(); }
@ -472,6 +489,12 @@
function drawStage(tau) {
const ctx = stageCtx, w = stageCv.width, h = stageCv.height;
ctx.clearRect(0, 0, w, h); ctx.fillStyle = "#15130d"; ctx.fillRect(0, 0, w, h);
// 真实场景俯视底图:画进 bounds 投影出的矩形(纵横比=bounds=图,故像素级贴合)
if (shotBg && shotReady && shotBounds) {
const tl = worldToStage({ x: shotBounds.minX, z: shotBounds.maxZ });
const br = worldToStage({ x: shotBounds.maxX, z: shotBounds.minZ });
ctx.drawImage(shotBg, tl.x, tl.y, br.x - tl.x, br.y - tl.y);
}
(model.anchors || []).forEach(a => {
const p = worldToStage({ x: a.pos[0], z: a.pos[2] });
ctx.strokeStyle = "rgba(180,170,140,.35)"; ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(p.x, p.y, 4, 0, Math.PI * 2); ctx.stroke();