/* ---- 图谱编辑模式(进入=快照基准;保存成功后基准刷新为已保存状态) ---- */ function snapshotGraph() { return { edges: JSON.parse(JSON.stringify(G.edges)), pos: JSON.parse(JSON.stringify(G.cardPos)), version: G.version, levels: Object.fromEntries(G.contacts.map(c => [c.id, c.level])) }; } function paintGraphEditButtons() { $('btnGraphEdit').textContent = G.editing ? '退出编辑' : '编辑图谱'; $('btnGraphSave').style.display = G.editing ? '' : 'none'; $('btnGraphReset').style.display = G.editing ? '' : 'none'; $('hintBar').classList.toggle('show', G.editing); wrap.classList.toggle('editing', G.editing); } function enterEdit() { G.editing = true; G.pendingAdds = []; G.pendingDeletes = []; G.cycleHighlight = null; G.editSnapshot = snapshotGraph(); document.body.classList.add('graph-edit'); paintGraphEditButtons(); renderGraph(); renderGraphState(); toast('已进入图谱编辑:拖卡片定级/改级,拖圆点连线,悬停连线删边'); } function doExitEdit() { G.editing = false; G.pendingAdds = []; G.pendingDeletes = []; G.cycleHighlight = null; document.body.classList.remove('graph-edit'); paintGraphEditButtons(); hideBubble(); renderGraph(); renderGraphState(); } function restoreLevels() { const lv = (G.editSnapshot && G.editSnapshot.levels) || {}; G.contacts.forEach(c => { if (c.id in lv) c.level = lv[c.id]; }); } function discardGraphEdit() { if (G.editSnapshot) { G.edges = JSON.parse(JSON.stringify(G.editSnapshot.edges)); G.cardPos = JSON.parse(JSON.stringify(G.editSnapshot.pos)); G.version = G.editSnapshot.version; restoreLevels(); } doExitEdit(); toast('已放弃本次修改,恢复到进入编辑时的状态'); } function graphEditToggle() { if (G.editing) exitEditAttempt(); else enterEdit(); } function exitEditAttempt(after) { const dirty = G.pendingAdds.length || G.pendingDeletes.length; if (!dirty) { doExitEdit(); if (after) after(); return; } gUnsavedDlg({ discard: () => { discardGraphEdit(); if (after) after(); }, save: () => { saveGraph().then(ok => { if (ok) { doExitEdit(); if (after) after(); } }); } }); } /* 全量快照 POST graph/save(版本 CAS):nodes 全员等级终态(含未定级空值=撤级)+ 手动边终态集; 公司边派生不入快照(D8)。67017 冲突 → 退出编辑态并重拉图谱(比原型更稳,原型保留编辑态易叠脏改)。 */ async function saveGraph() { const list = effectiveEdges(); const cyc = findCycle(list); if (cyc) { G.cycleHighlight = cyc; renderGraph(); toast('检测到循环关系(' + cyc.slice(0, -1).map(cname).join(' → ') + '),已阻止保存,请修改环边(红色高亮)', 'err'); return false; } const adds = G.pendingAdds.slice(); const delEdges = G.edges.filter(e => G.pendingDeletes.includes(e.id)); const nextEdges = G.edges.filter(e => !G.pendingDeletes.includes(e.id)) .concat(adds.map(a => ({ id: a.parent + '>' + a.child, parent: a.parent, child: a.child }))); const form = new URLSearchParams(); form.set('customerId', D.id); form.set('version', String(G.version)); G.contacts.forEach((c, i) => { form.set('nodes[' + i + '].contactId', c.id); form.set('nodes[' + i + '].level', c.level == null ? '' : String(c.level)); }); nextEdges.forEach((e, i) => { form.set('edges[' + i + '].parentId', e.parent); form.set('edges[' + i + '].childId', e.child); }); try { const newVer = await api('/api/customer/contact/graph/save', { method: 'POST', body: form.toString() }); G.version = Number(newVer) || G.version + 1; G.edges = nextEdges; adds.forEach(a => addLog('关系', '新增手动连线:' + cname(a.parent) + ' → ' + cname(a.child))); delEdges.forEach(e => addLog('关系', '删除连线:' + cname(e.parent) + ' → ' + cname(e.child) + '(手动边)')); G.pendingAdds = []; G.pendingDeletes = []; G.cycleHighlight = null; G.editSnapshot = snapshotGraph(); toast('保存成功(版本 ' + G.version + '),已记录关系变更日志', 'ok'); renderContactsList(); renderGraph(); renderGraphState(); return true; } catch (err) { toast('保存失败(' + err.code + '):' + err.message + (err.code === 67017 ? ',版本已被他人推进,已退出编辑并重拉图谱' : ''), 'err'); if (err.code === 67017) { doExitEdit(); loadGraph().then(() => { layoutGraph(); renderContactsList(); renderGraph(); renderGraphState(); }); } return false; } } function resetGraphEdit() { const dirty = G.pendingAdds.length || G.pendingDeletes.length; const doReset = () => { if (G.editSnapshot) { G.edges = JSON.parse(JSON.stringify(G.editSnapshot.edges)); G.cardPos = JSON.parse(JSON.stringify(G.editSnapshot.pos)); G.version = G.editSnapshot.version; restoreLevels(); } G.pendingAdds = []; G.pendingDeletes = []; G.cycleHighlight = null; toast('已重置:恢复到进入编辑时的初始状态'); renderGraph(); renderGraphState(); }; if (dirty) gConfirm('重置', '确定重置图谱布局吗?所有未保存的调整将恢复到初始状态。', doReset); else doReset(); }