编辑器体验改进
- 导入文件后直接关闭弹窗 + toast 提示导入数量 - 自动布局改为按出口顺序的子树居中:选项1/2/3 分支顺序正确且对齐
This commit is contained in:
@ -210,14 +210,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$("import-do").onclick = async () => {
|
$("import-do").onclick = async () => {
|
||||||
$("import-result").classList.add("err"); $("import-result").style.color = "";
|
$("import-result").textContent = "";
|
||||||
let events;
|
let events;
|
||||||
try { events = await collectImportEvents(); } catch (e) { $("import-result").textContent = e.message; return; }
|
try { events = await collectImportEvents(); } catch (e) { $("import-result").textContent = e.message; return; }
|
||||||
if (!events.length) { $("import-result").textContent = "请先选择文件或粘贴 JSON"; 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 r = await api("/api/import", { method: "POST", body: JSON.stringify({ events, by: App.by }) });
|
||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
$("import-result").textContent = "已导入 " + (d.saved || []).length + " 个" + ((d.errors || []).length ? "," + d.errors.join(";") : "");
|
|
||||||
importFiles = []; renderImportFiles(); $("import-text").value = "";
|
importFiles = []; renderImportFiles(); $("import-text").value = "";
|
||||||
|
$("import-modal").classList.add("hidden"); // 导入完直接关闭
|
||||||
|
const ne = (d.errors || []).length;
|
||||||
|
toast("已导入 " + (d.saved || []).length + " 个事件" + (ne ? "," + ne + " 个跳过/出错" : ""));
|
||||||
await loadList();
|
await loadList();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -241,6 +243,14 @@
|
|||||||
|
|
||||||
// ---------- 工具 ----------
|
// ---------- 工具 ----------
|
||||||
function esc(s) { return String(s == null ? "" : s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); }
|
function esc(s) { return String(s == null ? "" : s).replace(/&/g, "&").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 = () => {
|
$("btn-autolayout").onclick = () => {
|
||||||
|
|||||||
@ -91,21 +91,36 @@
|
|||||||
(ir.nodes || []).forEach(n => nodes[n.id] = n);
|
(ir.nodes || []).forEach(n => nodes[n.id] = n);
|
||||||
(ir.endings || []).forEach(e => nodes[e.id] = e);
|
(ir.endings || []).forEach(e => nodes[e.id] = e);
|
||||||
const ids = Object.keys(nodes);
|
const ids = Object.keys(nodes);
|
||||||
|
// 层级:最长路径(决定横向 x,左→右)
|
||||||
const edges = [];
|
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);
|
const layer = {}; ids.forEach(id => layer[id] = 0);
|
||||||
let changed = true, guard = 0;
|
let changed = true, guard = 0;
|
||||||
while (changed && guard++ < 9999) {
|
while (changed && guard++ < 9999) {
|
||||||
changed = false;
|
changed = false;
|
||||||
edges.forEach(([u, v]) => { if (layer[v] < layer[u] + 1) { layer[v] = layer[u] + 1; changed = true; } });
|
edges.forEach(([u, v]) => { if (layer[v] < layer[u] + 1) { layer[v] = layer[u] + 1; changed = true; } });
|
||||||
}
|
}
|
||||||
const perLayer = {}, pos = {};
|
// 有序子表(按出口顺序,去重)+ 入度求根
|
||||||
ids.forEach(id => {
|
const childMap = {}, indeg = {}; ids.forEach(id => { childMap[id] = []; indeg[id] = 0; });
|
||||||
const l = layer[id]; perLayer[l] = perLayer[l] || 0;
|
const seen = new Set();
|
||||||
// 横向分层(左→右),契合 Drawflow 端口左右朝向
|
ids.forEach(id => rawTargets(nodes[id]).forEach(t => {
|
||||||
pos[id] = { x: 40 + l * COL, y: 30 + perLayer[l] * ROW };
|
if (t && nodes[t]) { const k = id + ">" + t; if (!seen.has(k)) { seen.add(k); childMap[id].push(t); indeg[t]++; } }
|
||||||
perLayer[l]++;
|
}));
|
||||||
});
|
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;
|
return pos;
|
||||||
}
|
}
|
||||||
function ensureLayout(ir) {
|
function ensureLayout(ir) {
|
||||||
|
|||||||
@ -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 { stroke:#7a96c8; stroke-width:2.4px; }
|
||||||
#drawflow .connection .main-path:hover { stroke:#e6c878; }
|
#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 ---- */
|
/* ---- form ---- */
|
||||||
.fld { margin:9px 0; }
|
.fld { margin:9px 0; }
|
||||||
.fld > label { display:block; font-size:12px; color:#9a8f7e; margin-bottom:3px; }
|
.fld > label { display:block; font-size:12px; color:#9a8f7e; margin-bottom:3px; }
|
||||||
|
|||||||
Reference in New Issue
Block a user