From 2de308c1e15aa7fe00da9f445886f8d2d6cefbfe Mon Sep 17 00:00:00 2001 From: bia Date: Mon, 8 Jun 2026 18:25:07 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BC=96=E8=BE=91=E5=99=A8=E4=BD=93=E9=AA=8C?= =?UTF-8?q?=E6=94=B9=E8=BF=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 导入文件后直接关闭弹窗 + toast 提示导入数量 - 自动布局改为按出口顺序的子树居中:选项1/2/3 分支顺序正确且对齐 --- web/static/app.js | 14 ++++++++++++-- web/static/graph.js | 31 +++++++++++++++++++++++-------- web/static/style.css | 7 +++++++ 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/web/static/app.js b/web/static/app.js index f5cc183..9671cb4 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -210,14 +210,16 @@ } $("import-do").onclick = async () => { - $("import-result").classList.add("err"); $("import-result").style.color = ""; + $("import-result").textContent = ""; let events; try { events = await collectImportEvents(); } catch (e) { $("import-result").textContent = e.message; return; } if (!events.length) { $("import-result").textContent = "请先选择文件或粘贴 JSON"; return; } const r = await api("/api/import", { method: "POST", body: JSON.stringify({ events, by: App.by }) }); const d = await r.json(); - $("import-result").textContent = "已导入 " + (d.saved || []).length + " 个" + ((d.errors || []).length ? "," + d.errors.join(";") : ""); importFiles = []; renderImportFiles(); $("import-text").value = ""; + $("import-modal").classList.add("hidden"); // 导入完直接关闭 + const ne = (d.errors || []).length; + toast("已导入 " + (d.saved || []).length + " 个事件" + (ne ? "," + ne + " 个跳过/出错" : "")); await loadList(); }; @@ -241,6 +243,14 @@ // ---------- 工具 ---------- function esc(s) { return String(s == null ? "" : s).replace(/&/g, "&").replace(//g, ">"); } + let toastTimer = null; + function toast(msg) { + let el = $("toast"); + if (!el) { el = document.createElement("div"); el.id = "toast"; document.body.appendChild(el); } + el.textContent = msg; el.classList.add("show"); + clearTimeout(toastTimer); + toastTimer = setTimeout(() => el.classList.remove("show"), 2600); + } // ---------- 画布工具栏 ---------- $("btn-autolayout").onclick = () => { diff --git a/web/static/graph.js b/web/static/graph.js index e24689d..c59b3fe 100644 --- a/web/static/graph.js +++ b/web/static/graph.js @@ -91,21 +91,36 @@ (ir.nodes || []).forEach(n => nodes[n.id] = n); (ir.endings || []).forEach(e => nodes[e.id] = e); const ids = Object.keys(nodes); + // 层级:最长路径(决定横向 x,左→右) const edges = []; - (ir.nodes || []).forEach(n => rawTargets(n).forEach(t => { if (t && nodes[t]) edges.push([n.id, t]); })); + ids.forEach(id => rawTargets(nodes[id]).forEach(t => { if (t && nodes[t]) edges.push([id, t]); })); const layer = {}; ids.forEach(id => layer[id] = 0); let changed = true, guard = 0; while (changed && guard++ < 9999) { changed = false; edges.forEach(([u, v]) => { if (layer[v] < layer[u] + 1) { layer[v] = layer[u] + 1; changed = true; } }); } - const perLayer = {}, pos = {}; - ids.forEach(id => { - const l = layer[id]; perLayer[l] = perLayer[l] || 0; - // 横向分层(左→右),契合 Drawflow 端口左右朝向 - pos[id] = { x: 40 + l * COL, y: 30 + perLayer[l] * ROW }; - perLayer[l]++; - }); + // 有序子表(按出口顺序,去重)+ 入度求根 + const childMap = {}, indeg = {}; ids.forEach(id => { childMap[id] = []; indeg[id] = 0; }); + const seen = new Set(); + ids.forEach(id => rawTargets(nodes[id]).forEach(t => { + if (t && nodes[t]) { const k = id + ">" + t; if (!seen.has(k)) { seen.add(k); childMap[id].push(t); indeg[t]++; } } + })); + let roots = ids.filter(id => indeg[id] === 0); + if (!roots.length && ids.length) roots = [ids[0]]; + // 子树居中:按出口顺序递归分配纵向 y,父节点居子范围中点 → 选项1上/2中/3下且对齐 + const ypos = {}; let nextY = 0; const vis = new Set(); + function assignY(id) { + if (!id || vis.has(id)) return; vis.add(id); + if (!childMap[id].length) { ypos[id] = nextY; nextY += ROW; return; } + childMap[id].forEach(assignY); + const placed = childMap[id].map(c => ypos[c]).filter(v => v !== undefined); + ypos[id] = placed.length ? (Math.min(...placed) + Math.max(...placed)) / 2 : (nextY += ROW, nextY - ROW); + } + roots.forEach(assignY); + ids.forEach(id => { if (ypos[id] === undefined) { ypos[id] = nextY; nextY += ROW; } }); + const pos = {}; + ids.forEach(id => { pos[id] = { x: 40 + layer[id] * COL, y: 30 + ypos[id] }; }); return pos; } function ensureLayout(ir) { diff --git a/web/static/style.css b/web/static/style.css index 524f4dc..9027109 100644 --- a/web/static/style.css +++ b/web/static/style.css @@ -114,6 +114,13 @@ button.mini { padding:2px 8px; font-size:12px; } #drawflow .connection .main-path { stroke:#7a96c8; stroke-width:2.4px; } #drawflow .connection .main-path:hover { stroke:#e6c878; } +/* ---- toast ---- */ +#toast { position:fixed; left:50%; bottom:38px; transform:translateX(-50%) translateY(10px); + background:#2a2316; color:#f3dca0; border:1px solid #6a5630; border-radius:7px; + padding:10px 18px; font-size:13.5px; box-shadow:0 4px 16px rgba(0,0,0,.5); + opacity:0; pointer-events:none; transition:.25s; z-index:9999; } +#toast.show { opacity:1; transform:translateX(-50%) translateY(0); } + /* ---- form ---- */ .fld { margin:9px 0; } .fld > label { display:block; font-size:12px; color:#9a8f7e; margin-bottom:3px; }