Compare commits

..
2 Commits
Author SHA1 Message Date
hz4th_coder 3cbe3a5996 修复:DAG 画布 tab 切换后节点拖不动(多层根因)
根因链:
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%按钮/删除区正常
2026-08-13 00:25:01 +08:00
hz4th_coder dfdcd78f1a DAG画布:节点拖不动根因修复 + 缩放比例显示/一键100%
- 根因:dagLayout 画布宽(1100-1360px)超出视口可用宽度(约1060px),加上保存的偏移视图可能把节点推到视口外 → 节点不可见/不可点,表现为'拖不动'
- 修复:打开画布时若无保存视图、或节点全部在视口外,自动适配缩放(≤100%)并居中,全部节点始终可见可拖
- 新增右上角缩放控件:实时显示缩放百分比(滚轮/适配/拖动联动) + ⤢适配按钮 + 100%一键回原始视图
- 实测:真实鼠标拖动在 0.82x 缩放下精确跟随;滚轮 112%→100% 显示联动
2026-08-12 23:55:57 +08:00
2 changed files with 67 additions and 6 deletions
+63 -6
View File
@@ -323,6 +323,41 @@ function dagSize() {
return dagLayout(tasks, idMap);
}
function dagFitView() {
// 画布适配视口:基于节点实际包围盒缩放并居中,让全部节点可见
const s = dagState;
const box = $('#dag-box');
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) / cw, (bh - 60) / ch));
s.scale = scale;
// 节点包围盒中心(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();
}
function dagZoomTo(scale) {
// 一键回到指定比例(100% = 原始视图)
const s = dagState;
if (!s) return;
s.scale = scale;
s.tx = 0;
s.ty = 0;
dagApplyView();
saveDagState();
}
function saveDagState() {
if (!dagState) return;
try {
@@ -340,6 +375,8 @@ function dagApplyView() {
if (!svg) return;
svg.style.transform = `translate(${s.tx}px, ${s.ty}px) scale(${s.scale})`;
svg.style.transformOrigin = '0 0';
const pct = $('#dag-zoom-pct');
if (pct) pct.textContent = Math.round(s.scale * 100) + '%';
}
function drawDag() {
@@ -354,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;
@@ -364,10 +402,14 @@ function drawDag() {
<button class="btn success" ${ready.length ? '' : 'disabled'} onclick="runWorkflow()">▶ 执行整个工作流(${ready.length}</button>
</div>
<div class="dag-box" id="dag-box">
<div class="dag-zoom" id="dag-zoom">🔍 <b id="dag-zoom-pct">100%</b>
<button onclick="dagFitView()" title="缩放至适配整个画布">⤢ 适配</button>
<button onclick="dagZoomTo(1)" title="一键回到 100% 原始视图">100%</button>
</div>
<div class="dag-trash" id="dag-trash" onclick="openDagTrash()" title="点击查看回收站">🗑️ 删除区<span class="cnt" id="dag-trash-cnt">0</span></div>
<svg id="dag-svg" width="${width}" height="${Math.max(480, height)}" viewBox="0 0 ${width} ${height}" style="transform-origin:0 0">
<svg id="dag-svg" width="${width + M * 2}" height="${Math.max(480, height) + M * 2}" viewBox="${-M} ${-M} ${width + M * 2} ${height + M * 2}" style="transform-origin:0 0">
<defs><marker id="arrow" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#4a5878"/></marker></defs>
<rect id="dag-bg" x="-4000" y="-4000" width="${width + 8000}" height="${height + 8000}" fill="var(--panel)"/>
<rect id="dag-bg" x="${-M - 1000}" y="${-M - 1000}" width="${width + 2 * M + 2000}" height="${height + 2 * M + 2000}" fill="var(--panel)"/>
${edges.map(e => {
const a = dagState.pos[e.from], b = dagState.pos[e.to];
if (!a || !b) return '';
@@ -394,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;
@@ -477,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);
@@ -495,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;
@@ -627,9 +670,23 @@ function renderDag() {
ty: savedView?.ty || 0,
drag: null, moved: false,
};
const box = $('#tab-body');
box.innerHTML = '';
const body2 = $('#tab-body');
body2.innerHTML = '';
drawDag();
// 节点是否全部在视口内?任一越界则自动适配(防止节点被平移到视口外无法操作)
const box = $('#dag-box');
const bb = box ? box.getBoundingClientRect() : null;
let allVisible = !bb;
if (bb) {
allVisible = tasks.every(t => {
const p = dagState.pos[t.id];
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 (!allVisible || !savedView) dagFitView();
}
async function runWorkflow() {
+4
View File
@@ -143,6 +143,10 @@ tr:hover td{background:var(--panel2)}
.dag-trash{position:absolute;left:14px;bottom:14px;background:rgba(255,107,107,.1);border:1.5px dashed var(--danger);color:var(--danger);border-radius:10px;padding:8px 14px;font-size:12px;cursor:pointer;z-index:5;display:flex;align-items:center;gap:6px;backdrop-filter:blur(2px)}
.dag-trash:hover,.dag-trash.hover{background:rgba(255,107,107,.25);border-style:solid}
.dag-trash .cnt{background:var(--danger);color:#fff;border-radius:8px;padding:0 6px;font-size:11px}
.dag-zoom{position:absolute;right:14px;top:14px;background:rgba(23,30,46,.88);border:1px solid var(--border);border-radius:8px;padding:4px 10px;font-size:12px;color:var(--muted);display:flex;align-items:center;gap:6px;z-index:6}
.dag-zoom b{color:var(--text);font-weight:600;min-width:42px;text-align:center}
.dag-zoom button{background:var(--panel2);border:1px solid var(--border);color:var(--text);border-radius:5px;padding:2px 8px;font-size:11px;cursor:pointer}
.dag-zoom button:hover{border-color:var(--accent);color:var(--accent)}
.alert-row{border-left:3px solid var(--border);padding:12px 14px;margin-bottom:8px;background:var(--panel2);border-radius:0 10px 10px 0;cursor:pointer}
.alert-row.unread{border-left-color:var(--warn)}
.alert-row.critical{border-left-color:var(--danger)}