function renderGraph() {
canvasInner.querySelectorAll('.gcard:not(.company)').forEach(el => el.remove());
let companyEl = canvasInner.querySelector('.gcard.company');
if (!companyEl) {
companyEl = document.createElement('div');
companyEl.className = 'gcard company';
companyEl.innerHTML = '
' + esc(custName()) + '
客户主体 · 公司节点固定不可编辑
';
canvasInner.insertBefore(companyEl, $('unrecZone'));
}
companyEl.style.left = COMPANY_POS.x + 'px'; companyEl.style.top = COMPANY_POS.y + 'px';
G.contacts.forEach(c => {
const el = document.createElement('div');
el.className = 'gcard'; el.dataset.id = c.id;
const p = G.cardPos[c.id] || { x: 0, y: 0 };
const show = revealed[c.id] || C.revealAll;
el.innerHTML =
(c.level === 10 ? '顶层
' : '') +
'' + esc((c.name || '?')[0] || '?') + '
' +
'' + esc(c.name) + '
' +
'' + esc(c.job) + '
' +
(c.level != null ? '' + c.level + '
' : '') +
'' + (show ? esc(c.phone) : maskPhone(c.phone)) + '
' +
'';
el.style.left = p.x + 'px'; el.style.top = p.y + 'px';
canvasInner.insertBefore(el, $('unrecZone'));
});
renderEdgeSvg();
$('unsavedChip').className = 'unsaved-chip' + (G.editing && (G.pendingAdds.length || G.pendingDeletes.length) ? ' show' : '');
}
function posOf(id) {
if (id === COMPANY_ID) return { x: COMPANY_POS.x, y: COMPANY_POS.y };
return G.cardPos[id] || null;
}
function renderEdgesOnly() { renderEdgeSvg(); }
/* SVG 边渲染(手动边带 × 删除钮;公司边 = 派生视图:虚线浅蓝、无 ×、hover 不变红) */
function renderEdgeSvg() {
edgeSvg.innerHTML = '';
const list = effectiveEdges().concat(derivedCompanyEdges());
const cycPairs = new Set();
if (G.cycleHighlight) for (let i = 0; i < G.cycleHighlight.length - 1; i++) cycPairs.add(G.cycleHighlight[i] + '>' + G.cycleHighlight[i + 1]);
list.forEach(e => {
const p = posOf(e.parent), c = posOf(e.child);
if (!p || !c) return;
const x1 = p.x + CARD_W / 2, y1 = p.y + CARD_H, x2 = c.x + CARD_W / 2, y2 = c.y;
const my = (y1 + y2) / 2, d = 'M ' + x1 + ' ' + y1 + ' C ' + x1 + ' ' + my + ', ' + x2 + ' ' + my + ', ' + x2 + ' ' + y2;
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
g.setAttribute('class', 'edge-g' + (e.company ? ' edge-company' : ''));
g.dataset.eid = e.id; g.dataset.pair = e.parent + '>' + e.child;
const isCyc = !e.company && cycPairs.has(e.parent + '>' + e.child);
const cls = 'edge-line' + (e.pending ? ' edge-pending' : '') + (isCyc ? ' edge-cycle' : '');
g.innerHTML = '' +
(e.company ? '' :
'' +
'×');
edgeSvg.appendChild(g);
});
}