scense_drag/wwwroot/drag-canvas.js
2026-08-29 21:10:23 +08:00

401 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* drag-canvas.js — 拖拽编程画布核心bricks 前端)
*
* 功能:块面板 → 拖入画布 / 点击添加 / 移动 / 连线 / 删除 / 选中参数面板 / 保存 / 编译 / 校验
* 数据结构与后端 drag 模块契约一致:
* block = {id, type, x, y, params: {}}
* conn = {from, fromPort, to, toPort}
*
* 依赖 bricks 环境bricks_fetch / bricks_toast / eventbus由宿主注入。
*/
(function (global) {
'use strict';
var SVG_NS = 'http://www.w3.org/2000/svg';
var blockSeq = 1000;
function uid(prefix) {
blockSeq += 1;
return (prefix || 'b') + '_' + Date.now().toString(36) + '_' + blockSeq;
}
/**
* DragCanvas 构造:挂载到 container必须含 .drag-stage 画布区)
*/
function DragCanvas(container, opts) {
opts = opts || {};
this.container = container;
this.blocks = opts.blocks || [];
this.connections = opts.connections || [];
this.blockDefs = opts.blockDefs || {categories: [], blocks: {}};
this.api = opts.api || {};
this.onChange = opts.onChange || null;
this.selectedId = null;
this.dragMode = null; // 'block' | 'conn'
this.dragging = null; // {id, startX, startY, origX, origY}
this.connStart = null; // {blockId, port, portType}
this._binds = [];
this._init();
}
DragCanvas.prototype._init = function () {
var self = this;
var stage = this.container.querySelector('.drag-stage');
if (!stage) { return; }
this.stage = stage;
// 连线层SVG 放在画布区底层)
var svg = document.createElementNS(SVG_NS, 'svg');
svg.setAttribute('class', 'drag-lines');
svg.style.position = 'absolute';
svg.style.left = '0';
svg.style.top = '0';
svg.style.width = '100%';
svg.style.height = '100%';
svg.style.pointerEvents = 'none';
svg.style.zIndex = '1';
stage.appendChild(svg);
this.svg = svg;
// 画布区鼠标事件(连线 + 画布空白点击取消选中)
stage.addEventListener('mousedown', function (e) {
if (e.target === stage || e.target === svg) {
self.selectBlock(null);
}
});
stage.addEventListener('mousemove', function (e) { self._onMouseMove(e); });
stage.addEventListener('mouseup', function (e) { self._onMouseUp(e); });
document.addEventListener('mouseup', function () { self._endConn(); });
this.render();
this._emitChange();
};
/* ─────────────── 块渲染 ─────────────── */
DragCanvas.prototype._blockDef = function (type) {
return (this.blockDefs.blocks || {})[type] || null;
};
DragCanvas.prototype.render = function () {
var self = this;
// 清空既有块 DOM连线层保留
var olds = this.container.querySelectorAll('.drag-block');
for (var i = 0; i < olds.length; i++) { olds[i].parentNode.removeChild(olds[i]); }
while (this.svg.lastChild) { this.svg.removeChild(this.svg.lastChild); }
this.blocks.forEach(function (b) {
var def = self._blockDef(b.type);
if (!def) { return; }
var el = document.createElement('div');
el.className = 'drag-block' + (b.id === self.selectedId ? ' selected' : '');
el.dataset.id = b.id;
el.style.left = (b.x || 0) + 'px';
el.style.top = (b.y || 0) + 'px';
var cat = def.category || '';
var color = (self.blockDefs.categories || []).filter(function (c) { return c.key === cat; })[0] || {};
el.style.borderLeftColor = color.color || def.color || '#666';
var head = document.createElement('div');
head.className = 'drag-block-head';
head.innerHTML = '<i class="' + (def.icon || 'fa fa-cube') + '"></i> ' + def.label;
var body = document.createElement('div');
body.className = 'drag-block-body';
var params = b.params || {};
(def.params || []).forEach(function (pd) {
if (pd.uitype === 'entity' || pd.uitype === 'select' || pd.uitype === 'boolean') {
var opt = (pd.options || []).filter(function (o) { return String(o.value) === String(params[pd.name]); })[0];
body.innerHTML += '<div class="drag-param"><span>' + pd.label + '</span><b>' +
(opt ? opt.text : (params[pd.name] !== undefined ? params[pd.name] : '')) + '</b></div>';
} else if (params[pd.name] !== undefined && String(params[pd.name]) !== '') {
body.innerHTML += '<div class="drag-param"><span>' + pd.label + '</span><b>' + params[pd.name] + '</b></div>';
}
});
var ports = document.createElement('div');
ports.className = 'drag-ports';
var inp = document.createElement('div');
inp.className = 'drag-port drag-port-in';
inp.dataset.block = b.id;
inp.dataset.port = 'in';
inp.title = '输入';
var outWrap = document.createElement('div');
outWrap.className = 'drag-out-ports';
// 输出端口(含动态端口)
var outs = def.ports.outputs.map(function (p) { return p.name; });
var dyn = this._dynamicOutputs(def, params);
outs = outs.concat(dyn);
outs.forEach(function (pn) {
var o = document.createElement('div');
o.className = 'drag-port drag-port-out';
o.dataset.block = b.id;
o.dataset.port = pn;
o.title = pn;
outWrap.appendChild(o);
});
ports.appendChild(inp);
ports.appendChild(outWrap);
el.appendChild(head);
el.appendChild(body);
el.appendChild(ports);
// 块交互
el.addEventListener('mousedown', function (e) {
if (e.target.classList.contains('drag-port')) { return; }
self.selectBlock(b.id);
self.dragMode = 'block';
self.dragging = {id: b.id, startX: e.clientX, startY: e.clientY, origX: b.x || 0, origY: b.y || 0};
e.preventDefault();
e.stopPropagation();
});
el.addEventListener('dblclick', function () {
if (self.onEditBlock) { self.onEditBlock(b); }
});
// 端口连线起点
var pm = el.querySelectorAll('.drag-port-out');
for (var k = 0; k < pm.length; k++) {
pm[k].addEventListener('mousedown', function (e) {
e.stopPropagation();
e.preventDefault();
self._startConn(e.currentTarget.dataset.block, e.currentTarget.dataset.port, 'out');
});
}
var pi = el.querySelectorAll('.drag-port-in');
for (var j = 0; j < pi.length; j++) {
pi[j].addEventListener('mousedown', function (e) {
e.stopPropagation();
e.preventDefault();
self._startConn(e.currentTarget.dataset.block, e.currentTarget.dataset.port, 'in');
});
}
stage.appendChild(el);
}, this);
this.renderLines();
};
DragCanvas.prototype._dynamicOutputs = function (def, params) {
var d = def.dynamic_outputs;
if (!d) { return []; }
var n = parseInt(params[d.param], 10);
if (isNaN(n) || n < 2) { n = 2; }
if (n > 8) { n = 8; }
var out = [];
for (var i = 1; i <= n; i++) { out.push(d.prefix + i); }
return out;
};
/* ─────────────── 连线渲染SVG 贝塞尔) ─────────────── */
DragCanvas.prototype._portEl = function (blockId, port, ptype) {
var blocks = this.container.querySelectorAll('.drag-block[data-id="' + blockId + '"]');
if (!blocks.length) { return null; }
var el = blocks[0];
var q = ptype === 'out'
? el.querySelectorAll('.drag-port-out[data-port="' + port + '"]')
: el.querySelectorAll('.drag-port-in[data-port="' + port + '"]');
return q.length ? q[0] : null;
};
DragCanvas.prototype._portPos = function (blockId, port, ptype) {
var el = this._portEl(blockId, port, ptype);
if (!el) { return null; }
var r = el.getBoundingClientRect();
var sr = this.stage.getBoundingClientRect();
return {x: r.left - sr.left + r.width / 2, y: r.top - sr.top + r.height / 2};
};
DragCanvas.prototype._makePath = function (a, b) {
if (!a || !b) { return ''; }
var dx = Math.max(20, Math.abs(b.x - a.x) / 2);
return 'M ' + a.x + ' ' + a.y +
' C ' + (a.x + dx) + ' ' + a.y + ', ' + (b.x - dx) + ' ' + b.y + ', ' + b.x + ' ' + b.y;
};
DragCanvas.prototype.renderLines = function () {
var self = this;
while (this.svg.lastChild) { this.svg.removeChild(this.svg.lastChild); }
this.connections.forEach(function (c) {
var a = self._portPos(c.from, c.fromPort, 'out');
var b = self._portPos(c.to, c.toPort, 'in');
if (!a || !b) { return; }
var path = document.createElementNS(SVG_NS, 'path');
path.setAttribute('d', self._makePath(a, b));
path.setAttribute('stroke', '#7c8aa0');
path.setAttribute('stroke-width', '2');
path.setAttribute('fill', 'none');
self.svg.appendChild(path);
});
// 连线中临时线
if (this.connStart && this.connMouse) {
var tp = document.createElementNS(SVG_NS, 'path');
tp.setAttribute('d', this._makePath(this.connStart.pos, this.connMouse));
tp.setAttribute('stroke', '#4a90d9');
tp.setAttribute('stroke-width', '2');
tp.setAttribute('stroke-dasharray', '6,3');
tp.setAttribute('fill', 'none');
this.svg.appendChild(tp);
}
};
/* ─────────────── 交互:移动 / 连线 / 选中 ─────────────── */
DragCanvas.prototype._onMouseMove = function (e) {
if (this.dragMode === 'block' && this.dragging) {
var d = this.dragging;
var b = this._block(d.id);
if (b) {
b.x = Math.max(0, Math.round(d.origX + (e.clientX - d.startX)));
b.y = Math.max(0, Math.round(d.origY + (e.clientY - d.startY)));
}
var el = this.container.querySelector('.drag-block[data-id="' + d.id + '"]');
if (el) {
el.style.left = (b ? b.x : 0) + 'px';
el.style.top = (b ? b.y : 0) + 'px';
}
this.renderLines();
return;
}
if (this.connStart) {
var sr = this.stage.getBoundingClientRect();
this.connMouse = {x: e.clientX - sr.left, y: e.clientY - sr.top};
this.renderLines();
}
};
DragCanvas.prototype._onMouseUp = function (e) {
if (this.dragMode === 'block' && this.dragging) {
this.dragMode = null;
this.dragging = null;
this._emitChange();
return;
}
if (this.connStart) {
var t = e.target;
if (t && t.classList && t.classList.contains('drag-port')) {
var targetType = t.classList.contains('drag-port-in') ? 'in' : 'out';
this._finishConn(t.dataset.block, t.dataset.port, targetType);
}
this._endConn();
}
};
DragCanvas.prototype._startConn = function (blockId, port, ptype) {
var pos = this._portPos(blockId, port, ptype);
if (!pos) { return; }
this.connStart = {blockId: blockId, port: port, ptype: ptype, pos: pos};
this.connMouse = pos;
};
DragCanvas.prototype._finishConn = function (toBlock, toPort, toType) {
if (!this.connStart) { return; }
var s = this.connStart;
var from, fromPort, to, toPort;
if (s.ptype === 'out' && toType === 'in') {
from = s.blockId; fromPort = s.port; to = toBlock; toPort = toPort;
} else if (s.ptype === 'in' && toType === 'out') {
from = toBlock; fromPort = toPort; to = s.blockId; toPort = s.port;
} else {
return; // 同向端口不允许连线
}
if (from === to) { return; } // 自环拒绝
var key = from + ':' + fromPort + '->' + to + ':' + toPort;
var dup = this.connections.some(function (c) {
return c.from === from && c.fromPort === fromPort && c.to === to && c.toPort === toPort;
});
if (dup) { return; }
this.connections.push({from: from, fromPort: fromPort, to: to, toPort: toPort});
this.renderLines();
this._emitChange();
};
DragCanvas.prototype._endConn = function () {
this.connStart = null;
this.connMouse = null;
this.renderLines();
};
DragCanvas.prototype._block = function (id) {
for (var i = 0; i < this.blocks.length; i++) {
if (this.blocks[i].id === id) { return this.blocks[i]; }
}
return null;
};
DragCanvas.prototype.selectBlock = function (id) {
this.selectedId = id;
var els = this.container.querySelectorAll('.drag-block');
for (var i = 0; i < els.length; i++) {
els[i].classList.toggle('selected', els[i].dataset.id === id);
}
if (this.onSelect) { this.onSelect(id ? this._block(id) : null); }
};
/* ─────────────── 对外操作:加块 / 删块 / 删线 / 取图 ─────────────── */
DragCanvas.prototype.addBlock = function (type, x, y) {
var def = this._blockDef(type);
if (!def) { return null; }
var params = {};
(def.params || []).forEach(function (p) {
if (p.default !== undefined) { params[p.name] = p.default; }
});
var b = {id: uid('b'), type: type, x: x != null ? x : 60 + this.blocks.length * 30,
y: y != null ? y : 60, params: params};
this.blocks.push(b);
this.render();
this.selectBlock(b.id);
this._emitChange();
return b;
};
DragCanvas.prototype.removeBlock = function (id) {
this.blocks = this.blocks.filter(function (b) { return b.id !== id; });
this.connections = this.connections.filter(function (c) { return c.from !== id && c.to !== id; });
if (this.selectedId === id) { this.selectedId = null; }
this.render();
this._emitChange();
};
DragCanvas.prototype.removeConnection = function (from, fromPort, to, toPort) {
this.connections = this.connections.filter(function (c) {
return !(c.from === from && c.fromPort === fromPort && c.to === to && c.toPort === toPort);
});
this.renderLines();
this._emitChange();
};
DragCanvas.prototype.clear = function () {
this.blocks = [];
this.connections = [];
this.selectedId = null;
this.render();
this._emitChange();
};
DragCanvas.prototype.setBlockParams = function (id, params) {
var b = this._block(id);
if (!b) { return; }
b.params = params || {};
this.render();
this._emitChange();
};
DragCanvas.prototype.getGraph = function () {
return {blocks: this.blocks, connections: this.connections};
};
DragCanvas.prototype.setGraph = function (graph) {
this.blocks = (graph && graph.blocks) || [];
this.connections = (graph && graph.connections) || [];
this.selectedId = null;
this.render();
this._emitChange();
};
DragCanvas.prototype._emitChange = function () {
if (this.onChange) { this.onChange(this.getGraph()); }
};
global.DragCanvas = DragCanvas;
})(window);