编辑器体验改进

- 导入文件后直接关闭弹窗 + toast 提示导入数量
- 自动布局改为按出口顺序的子树居中:选项1/2/3 分支顺序正确且对齐
This commit is contained in:
bia
2026-06-08 18:25:07 +08:00
parent 4a681dfe91
commit 2de308c1e1
3 changed files with 42 additions and 10 deletions

View File

@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); }
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 = () => {

View File

@ -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) {

View File

@ -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; }