From 3cbe3a59964336b1bc4b95a5fd0b49fcc98de3cb Mon Sep 17 00:00:00 2001 From: hz4th_coder Date: Thu, 13 Aug 2026 00:25:01 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9ADAG=20=E7=94=BB?= =?UTF-8?q?=E5=B8=83=20tab=20=E5=88=87=E6=8D=A2=E5=90=8E=E8=8A=82=E7=82=B9?= =?UTF-8?q?=E6=8B=96=E4=B8=8D=E5=8A=A8=EF=BC=88=E5=A4=9A=E5=B1=82=E6=A0=B9?= =?UTF-8?q?=E5=9B=A0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因链: 1. mousemove handler 更新 rect 用闭包 s.pos(旧 dagState 对象),切回 tab 后 renderDag 重建 dagState,s 指向旧对象 → 节点被设回旧布局值 → 视觉上拖不动 → 改为 handler 内现查的 st.pos 2. overflow:visible 让节点绘制到 svg 元素盒外,盒外内容看得见但命中测试不到 → svg 盒加 ±3000 边距(DAG_M)+viewBox 同步扩大,节点永远可命中 3. 保存的越界节点位置(y=620>画布560)使 fit 看不到 → dagFitView 改为基于节点实际包围盒计算 4. 打开时仅'任一节点可见'就保留旧视图 → 改为'全部节点可见',任一越界自动适配 实测:刷新进入/SPA切看板切回/连续切换3次后,真实鼠标拖动均精确跟随;缩放显示/100%按钮/删除区正常 --- static/app.js | 45 ++++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/static/app.js b/static/app.js index f485c5c..bcd2aa6 100644 --- a/static/app.js +++ b/static/app.js @@ -324,16 +324,25 @@ function dagSize() { } function dagFitView() { - // 画布适配视口:缩放并居中,让全部节点可见 + // 画布适配视口:基于节点实际包围盒缩放并居中,让全部节点可见 const s = dagState; const box = $('#dag-box'); - const {width, height} = dagSize(); if (!box || !s) return; + const poss = Object.values(s.pos || {}); + if (!poss.length) return; + const minX = Math.min(...poss.map(p => p.x)) - 120; + const maxX = Math.max(...poss.map(p => p.x)) + 120; + const minY = Math.min(...poss.map(p => p.y)) - 120; + const maxY = Math.max(...poss.map(p => p.y)) + 120; + const cw = Math.max(200, maxX - minX), ch = Math.max(200, maxY - minY); const bw = box.clientWidth, bh = box.clientHeight; - const scale = Math.max(0.25, Math.min(1, (bw - 60) / width, (bh - 60) / height)); + const scale = Math.max(0.25, Math.min(1, (bw - 60) / cw, (bh - 60) / ch)); s.scale = scale; - s.tx = (bw - width * scale) / 2; - s.ty = (bh - height * scale) / 2; + // 节点包围盒中心(viewBox 坐标)对准视口中心 + const cx = DAG_M + (minX + maxX) / 2; + const cy = DAG_M + (minY + maxY) / 2; + s.tx = bw / 2 - cx * scale; + s.ty = bh / 2 - cy * scale; dagApplyView(); saveDagState(); } @@ -382,6 +391,7 @@ function drawDag() { const body = $('#tab-body'); if (!body) return; const NW = 140, NHW = 70; // 节点宽 / 半宽 + const M = DAG_M; // svg 盒边距:节点平移/缩放后仍在盒内可命中 const dagText = t => { const raw = `#${t.id} ${t.title}`; return raw.length > 13 ? raw.slice(0, 13) + '…' : raw; @@ -397,9 +407,9 @@ function drawDag() {
🗑️ 删除区0
- + - + ${edges.map(e => { const a = dagState.pos[e.from], b = dagState.pos[e.to]; if (!a || !b) return ''; @@ -426,6 +436,7 @@ function drawDag() { let dagBound = false; // window 级监听只绑定一次 let dagDrag = null; // 模块级拖拽状态(box 重绑后 window handler 仍可访问) +const DAG_M = 3000; // svg 盒边距:节点平移/缩放后仍在盒内可命中 function bindDagEvents() { const box = $('#dag-box'); if (!box) return; @@ -509,7 +520,7 @@ function bindDagEvents() { if (!svgEl2) return; svgEl2.querySelectorAll('.dag-node').forEach(g => { const nid = Number(g.dataset.id); - const pp = s.pos[nid]; + const pp = st.pos[nid]; if (!pp) return; g.querySelector('rect').setAttribute('x', pp.x - 70); g.querySelector('rect').setAttribute('y', pp.y - 24); @@ -527,7 +538,7 @@ function bindDagEvents() { projCtx.tasks.forEach(t => { if (t.id !== tid) return; (t.depends_on || []).forEach(d => { - const b = s.pos[d]; + const b = st.pos[d]; if (!b) return; const path = svgEl2.querySelector(`path[data-e="${d}-${tid}"]`); if (!path) return; @@ -662,20 +673,20 @@ function renderDag() { const body2 = $('#tab-body'); body2.innerHTML = ''; drawDag(); - // 节点是否在视口内?不在则自动适配(防止节点被平移到视口外无法操作) + // 节点是否全部在视口内?任一越界则自动适配(防止节点被平移到视口外无法操作) const box = $('#dag-box'); const bb = box ? box.getBoundingClientRect() : null; - let anyVisible = false; + let allVisible = !bb; if (bb) { - tasks.forEach(t => { + allVisible = tasks.every(t => { const p = dagState.pos[t.id]; - if (!p) return; - const vx = bb.left + dagState.tx + p.x * dagState.scale; - const vy = bb.top + dagState.ty + p.y * dagState.scale; - if (vx < bb.right && vx > bb.left && vy < bb.bottom && vy > bb.top) anyVisible = true; + if (!p) return true; + const vx = bb.left + dagState.tx + (DAG_M + p.x) * dagState.scale; + const vy = bb.top + dagState.ty + (DAG_M + p.y) * dagState.scale; + return vx < bb.right && vx > bb.left && vy < bb.bottom && vy > bb.top; }); } - if (!anyVisible || !savedView) dagFitView(); + if (!allVisible || !savedView) dagFitView(); } async function runWorkflow() {