You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
117 lines
5.8 KiB
117 lines
5.8 KiB
/* ---- 拖线 / 拖卡 / 平移(mousedown 分流 + document 跟随;bindContactsUI 统一绑定) ---- */
|
|
let dragLine = null, dragCard = null, panning = null;
|
|
function startDragLine(e, fromId, dir) {
|
|
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
path.setAttribute('class', 'edge-line edge-pending'); path.setAttribute('fill', 'none');
|
|
edgeSvg.appendChild(path);
|
|
dragLine = { from: fromId, dir: dir || 'down', path: path };
|
|
updateDragLine(e);
|
|
}
|
|
function updateDragLine(e) {
|
|
const p = posOf(dragLine.from);
|
|
if (!p) return;
|
|
const sy = dragLine.dir === 'up' ? p.y : p.y + CARD_H;
|
|
const pt = toCanvas(e.clientX, e.clientY);
|
|
dragLine.path.setAttribute('d', 'M ' + (p.x + CARD_W / 2) + ' ' + sy + ' L ' + pt.x + ' ' + pt.y);
|
|
canvasInner.querySelectorAll('.gcard.link-target').forEach(el => el.classList.remove('link-target'));
|
|
const t = document.elementFromPoint(e.clientX, e.clientY);
|
|
const card = t && t.closest ? t.closest('.gcard') : null;
|
|
if (card && !card.classList.contains('company') && card.dataset.id && card.dataset.id !== dragLine.from) card.classList.add('link-target');
|
|
}
|
|
function tryConnect(aId, bId) {
|
|
if (aId === bId) { toast('不允许连接自身(' + cname(aId) + ')', 'err'); return; }
|
|
if (aId === COMPANY_ID || bId === COMPANY_ID) { toast('不允许为公司节点新增或删除关系', 'err'); return; }
|
|
if (hasPair(effectiveEdges(), aId, bId) || hasPair(effectiveEdges(), bId, aId)) {
|
|
toast('已存在连线:' + cname(aId) + ' ↔ ' + cname(bId) + '(蓝色实线即是),换一对联系人试试', 'err'); return;
|
|
}
|
|
const a = byId(aId), b = byId(bId);
|
|
if (!a || !b) return;
|
|
if (a.level == null || b.level == null) {
|
|
toast('「' + cname(a.level == null ? aId : bId) + '」未设置等级,请先将其拖入等级格子完成定级再连线'); return;
|
|
}
|
|
commitDirectionalEdge(aId, bId);
|
|
}
|
|
/* 两端层级已齐:按层级自动定方向(高者为上级;同级不连)+ 单父约束 → 落 pending */
|
|
function commitDirectionalEdge(aId, bId) {
|
|
const a = byId(aId), b = byId(bId);
|
|
if (!a || !b) return false;
|
|
if (a.level === b.level) { toast('同级(层级相同)之间不建立上下级连线', 'err'); return false; }
|
|
const parentId = a.level > b.level ? aId : bId, childId = a.level > b.level ? bId : aId;
|
|
const exParent = effectiveEdges().find(e => e.child === childId);
|
|
if (exParent) { toast(cname(childId) + ' 已有上级(' + cname(exParent.parent) + '),一个节点最多一个上级,请先删除原连线', 'err'); return false; }
|
|
commitPendingEdge(parentId, childId);
|
|
return true;
|
|
}
|
|
function commitPendingEdge(parentId, childId) {
|
|
G.pendingAdds.push({ parent: parentId, child: childId });
|
|
G.cycleHighlight = null;
|
|
addLog('关系', '新增连线(未保存):' + cname(parentId) + ' → ' + cname(childId));
|
|
renderGraph(); renderGraphState();
|
|
}
|
|
function graphMouseDown(e) {
|
|
if (e.button !== 0) return;
|
|
const dot = e.target.closest('.dot');
|
|
if (dot && G.editing) {
|
|
const gc = dot.closest('.gcard');
|
|
if (!gc) return;
|
|
if (dot.classList.contains('dot-bottom')) { e.preventDefault(); startDragLine(e, gc.dataset.id, 'down'); return; }
|
|
if (dot.classList.contains('dot-top')) { e.preventDefault(); startDragLine(e, gc.dataset.id, 'up'); return; }
|
|
}
|
|
const card = e.target.closest('.gcard');
|
|
if (card && G.editing && !card.classList.contains('company') && card.dataset.id) {
|
|
const pt = toCanvas(e.clientX, e.clientY), p = G.cardPos[card.dataset.id];
|
|
if (!p) return;
|
|
dragCard = { id: card.dataset.id, dx: pt.x - p.x, dy: pt.y - p.y };
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
if (!card) {
|
|
panning = { x: e.clientX, y: e.clientY, px: G.view.panX, py: G.view.panY };
|
|
wrap.classList.add('panning');
|
|
}
|
|
}
|
|
function graphMouseMove(e) {
|
|
if (dragLine) { updateDragLine(e); return; }
|
|
if (dragCard) {
|
|
const pt = toCanvas(e.clientX, e.clientY);
|
|
const p = G.cardPos[dragCard.id];
|
|
if (!p) return;
|
|
p.x = Math.max(0, Math.min(2400 - CARD_W, pt.x - dragCard.dx));
|
|
p.y = Math.max(0, Math.min(1400 - CARD_H, pt.y - dragCard.dy));
|
|
const el = canvasInner.querySelector('.gcard[data-id="' + dragCard.id + '"]');
|
|
if (el) { el.style.left = p.x + 'px'; el.style.top = p.y + 'px'; }
|
|
canvasInner.querySelectorAll('.lv-row.drop-target').forEach(x => x.classList.remove('drop-target'));
|
|
if (G.editing) {
|
|
const t = rowAt(pt.x, pt.y);
|
|
if (t.kind === 'lv') { const row = canvasInner.querySelector('.lv-row[data-lv="' + t.lv + '"]'); if (row) row.classList.add('drop-target'); }
|
|
}
|
|
renderEdgesOnly();
|
|
return;
|
|
}
|
|
if (panning) {
|
|
G.view.panX = panning.px + (e.clientX - panning.x);
|
|
G.view.panY = panning.py + (e.clientY - panning.y);
|
|
applyTransform();
|
|
}
|
|
}
|
|
function graphMouseUp(e) {
|
|
if (dragLine) {
|
|
const t = document.elementFromPoint(e.clientX, e.clientY);
|
|
const card = t && t.closest ? t.closest('.gcard') : null;
|
|
if (card && card.classList.contains('company')) toast('不允许为公司节点新增或删除关系', 'err');
|
|
else if (card && card.dataset.id && card.dataset.id !== dragLine.from) tryConnect(dragLine.from, card.dataset.id);
|
|
dragLine.path.remove();
|
|
dragLine = null;
|
|
canvasInner.querySelectorAll('.gcard.link-target').forEach(el => el.classList.remove('link-target'));
|
|
}
|
|
if (dragCard) {
|
|
const pt = toCanvas(e.clientX, e.clientY);
|
|
const t = rowAt(pt.x, pt.y);
|
|
if (t.kind === 'lv' || t.kind === 'unrec') tryLevelDrop(dragCard.id, t);
|
|
else if (t.kind === 'company') toast('公司行不可放置联系人卡片');
|
|
canvasInner.querySelectorAll('.lv-row.drop-target').forEach(x => x.classList.remove('drop-target'));
|
|
}
|
|
dragCard = null;
|
|
if (panning) { panning = null; wrap.classList.remove('panning'); }
|
|
}
|
|
function graphWheel(e) { e.preventDefault(); zoomAt(e.deltaY < 0 ? 1.12 : 0.9, e.clientX, e.clientY); }
|