323 lines
12 KiB
JavaScript
323 lines
12 KiB
JavaScript
/**
|
||
* drag-editor.js — 拖拽编程画布编辑器页面逻辑
|
||
*
|
||
* 组装:左侧块面板(分类 + 块拖入/点击添加) + 中间画布(DragCanvas) + 右侧参数面板。
|
||
* 工具栏:保存 / 校验 / 编译 / 发布。
|
||
*/
|
||
(function () {
|
||
'use strict';
|
||
|
||
var state = {
|
||
graphId: null,
|
||
graphName: '',
|
||
blockDefs: {categories: [], blocks: {}},
|
||
canvas: null,
|
||
editingBlock: null
|
||
};
|
||
|
||
function $(sel, root) { return (root || document).querySelector(sel); }
|
||
|
||
function toast(msg, type) {
|
||
if (window.bricks_toast) { window.bricks_toast(msg, type || 'info'); return; }
|
||
alert(msg);
|
||
}
|
||
|
||
function apiFetch(url, data) {
|
||
var opts = {method: 'POST', headers: {'Content-Type': 'application/json'}};
|
||
if (data !== undefined) { opts.body = JSON.stringify(data); }
|
||
return fetch(url, opts).then(function (r) { return r.json(); });
|
||
}
|
||
|
||
function entireUrl(path) {
|
||
// 宿主注入 entire_url 模板函数时使用;否则原样
|
||
if (window.entire_url) { return window.entire_url(path); }
|
||
return path;
|
||
}
|
||
|
||
/* ─────────────── 块面板 ─────────────── */
|
||
|
||
function renderBlockPanel() {
|
||
var panel = $('#block-panel');
|
||
if (!panel) { return; }
|
||
panel.innerHTML = '';
|
||
(state.blockDefs.categories || []).forEach(function (cat) {
|
||
var sec = document.createElement('div');
|
||
sec.className = 'drag-cat';
|
||
var title = document.createElement('div');
|
||
title.className = 'drag-cat-title';
|
||
title.style.borderLeftColor = cat.color;
|
||
title.textContent = cat.label;
|
||
sec.appendChild(title);
|
||
var list = document.createElement('div');
|
||
list.className = 'drag-cat-blocks';
|
||
Object.keys(state.blockDefs.blocks || {}).forEach(function (type) {
|
||
var def = state.blockDefs.blocks[type];
|
||
if (def.category !== cat.key) { return; }
|
||
var item = document.createElement('div');
|
||
item.className = 'drag-block-item';
|
||
item.draggable = true;
|
||
item.dataset.type = type;
|
||
item.innerHTML = '<i class="' + (def.icon || 'fa fa-cube') + '"></i><span>' + def.label + '</span>';
|
||
item.title = def.description || def.label;
|
||
// 拖入画布
|
||
item.addEventListener('dragstart', function (e) {
|
||
e.dataTransfer.setData('text/plain', type);
|
||
});
|
||
// 点击添加
|
||
item.addEventListener('click', function () {
|
||
if (state.canvas) { state.canvas.addBlock(type); }
|
||
});
|
||
list.appendChild(item);
|
||
});
|
||
sec.appendChild(list);
|
||
panel.appendChild(sec);
|
||
});
|
||
}
|
||
|
||
/* ─────────────── 参数面板 ─────────────── */
|
||
|
||
function renderParamPanel(block) {
|
||
state.editingBlock = block;
|
||
var panel = $('#param-panel');
|
||
if (!panel) { return; }
|
||
panel.innerHTML = '';
|
||
if (!block) {
|
||
panel.innerHTML = '<div class="drag-param-empty">选中块后在此编辑参数</div>';
|
||
return;
|
||
}
|
||
var def = (state.blockDefs.blocks || {})[block.type];
|
||
if (!def) { return; }
|
||
var title = document.createElement('div');
|
||
title.className = 'drag-param-title';
|
||
title.innerHTML = '<i class="' + (def.icon || '') + '"></i> ' + def.label +
|
||
' <span class="drag-param-desc">' + (def.description || '') + '</span>';
|
||
panel.appendChild(title);
|
||
|
||
var delBtn = document.createElement('button');
|
||
delBtn.className = 'drag-btn drag-btn-danger';
|
||
delBtn.textContent = '删除该块';
|
||
delBtn.addEventListener('click', function () {
|
||
if (state.canvas) { state.canvas.removeBlock(block.id); }
|
||
renderParamPanel(null);
|
||
});
|
||
panel.appendChild(delBtn);
|
||
|
||
var form = document.createElement('div');
|
||
form.className = 'drag-param-form';
|
||
(def.params || []).forEach(function (pd) {
|
||
var row = document.createElement('div');
|
||
row.className = 'drag-param-row';
|
||
var label = document.createElement('label');
|
||
label.textContent = pd.label + (pd.required ? ' *' : '');
|
||
var input = null;
|
||
var val = (block.params || {})[pd.name] !== undefined ? (block.params || {})[pd.name] : (pd.default !== undefined ? pd.default : '');
|
||
if (pd.uitype === 'select' || pd.uitype === 'boolean') {
|
||
input = document.createElement('select');
|
||
(pd.options || []).forEach(function (o) {
|
||
var op = document.createElement('option');
|
||
op.value = o.value;
|
||
op.textContent = o.text;
|
||
if (String(o.value) === String(val)) { op.selected = true; }
|
||
input.appendChild(op);
|
||
});
|
||
} else if (pd.uitype === 'entity') {
|
||
input = document.createElement('select');
|
||
input.innerHTML = '<option value="">请选择实体…</option>';
|
||
apiFetch(entireUrl('/drag/api/get_search_entity.dspy'), {}).then(function (r) {
|
||
var items = (r && r.data) || [];
|
||
items.forEach(function (it) {
|
||
var op = document.createElement('option');
|
||
op.value = it.value;
|
||
op.textContent = it.text;
|
||
if (String(it.value) === String(val)) { op.selected = true; }
|
||
input.appendChild(op);
|
||
});
|
||
});
|
||
} else if (pd.uitype === 'number') {
|
||
input = document.createElement('input');
|
||
input.type = 'number';
|
||
if (pd.min !== undefined) { input.min = pd.min; }
|
||
if (pd.max !== undefined) { input.max = pd.max; }
|
||
input.value = val;
|
||
} else if (pd.uitype === 'expression' || pd.uitype === 'variable') {
|
||
input = document.createElement('input');
|
||
input.type = 'text';
|
||
input.placeholder = pd.placeholder || '';
|
||
input.value = val;
|
||
} else {
|
||
input = document.createElement('input');
|
||
input.type = 'text';
|
||
input.placeholder = pd.placeholder || '';
|
||
input.value = val;
|
||
}
|
||
input.dataset.field = pd.name;
|
||
input.addEventListener('change', function () {
|
||
if (!state.canvas || !state.editingBlock) { return; }
|
||
var p = state.editingBlock.params || {};
|
||
p[pd.name] = input.value;
|
||
state.canvas.setBlockParams(state.editingBlock.id, p);
|
||
// 动态端口(分叉/并行)变化后重渲染
|
||
if (def.dynamic_outputs && def.dynamic_outputs.param === pd.name) {
|
||
state.canvas.render();
|
||
}
|
||
});
|
||
row.appendChild(label);
|
||
row.appendChild(input);
|
||
form.appendChild(row);
|
||
});
|
||
panel.appendChild(form);
|
||
}
|
||
|
||
/* ─────────────── 工具栏动作 ─────────────── */
|
||
|
||
function doSave() {
|
||
if (!state.canvas) { return; }
|
||
var g = state.canvas.getGraph();
|
||
var payload = {
|
||
id: state.graphId || undefined,
|
||
name: state.graphName || '未命名画布',
|
||
blocks: g.blocks,
|
||
connections: g.connections
|
||
};
|
||
apiFetch(entireUrl('/drag/api/graph_save.dspy'), payload).then(function (r) {
|
||
if (r.status === 200 && r.data && r.data.success) {
|
||
state.graphId = r.data.id;
|
||
toast('保存成功' + (r.data.valid ? '' : '(存在校验错误,见校验结果)'), 'success');
|
||
if (!r.data.valid) { showValidate(r.data.errors); }
|
||
} else {
|
||
toast((r.message) || '保存失败', 'error');
|
||
}
|
||
});
|
||
}
|
||
|
||
function doValidate() {
|
||
if (!state.canvas) { return; }
|
||
var g = state.canvas.getGraph();
|
||
apiFetch(entireUrl('/drag/api/graph_validate.dspy'),
|
||
{blocks: g.blocks, connections: g.connections}).then(function (r) {
|
||
var d = r.data || {};
|
||
showValidate(d.errors || []);
|
||
if (d.valid) { toast('校验通过 ✓', 'success'); }
|
||
});
|
||
}
|
||
|
||
function showValidate(errors) {
|
||
var box = $('#validate-result');
|
||
if (!box) { return; }
|
||
if (!errors || !errors.length) {
|
||
box.innerHTML = '<div class="drag-ok">校验通过:无错误</div>';
|
||
return;
|
||
}
|
||
var html = '<div class="drag-err">校验发现 ' + errors.length + ' 个问题:</div><ul>';
|
||
errors.forEach(function (e) {
|
||
html += '<li>[' + (e.code || '') + '] ' + (e.message || '') +
|
||
(e.block_id ? ' (块 ' + e.block_id + ')' : '') + '</li>';
|
||
});
|
||
html += '</ul>';
|
||
box.innerHTML = html;
|
||
}
|
||
|
||
function doCompile() {
|
||
if (!state.canvas) { return; }
|
||
var g = state.canvas.getGraph();
|
||
apiFetch(entireUrl('/drag/api/graph_compile.dspy'),
|
||
{name: state.graphName, blocks: g.blocks, connections: g.connections}).then(function (r) {
|
||
var d = r.data || {};
|
||
var out = $('#compile-output');
|
||
if (!out) { return; }
|
||
if (d.success) {
|
||
out.innerHTML = '<pre class="drag-code">' + escapeHtml(d.content || '') + '</pre>';
|
||
showValidate([]);
|
||
} else {
|
||
out.innerHTML = '';
|
||
showValidate(d.errors || []);
|
||
}
|
||
});
|
||
}
|
||
|
||
function doPublish() {
|
||
if (!state.graphId) { toast('请先保存画布再发布', 'error'); return; }
|
||
apiFetch(entireUrl('/drag/api/graph_publish.dspy'), {id: state.graphId}).then(function (r) {
|
||
if (r.status === 200 && r.data && r.data.success) {
|
||
toast('已发布到脚本引擎:' + (r.data.script_name || ''), 'success');
|
||
} else {
|
||
toast((r.data && r.data.message) || '发布失败', 'error');
|
||
}
|
||
});
|
||
}
|
||
|
||
function escapeHtml(s) {
|
||
return String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||
}
|
||
|
||
/* ─────────────── 初始化 ─────────────── */
|
||
|
||
function init() {
|
||
// 画布名称
|
||
var nameInput = $('#graph-name');
|
||
if (nameInput) {
|
||
nameInput.addEventListener('change', function () { state.graphName = nameInput.value; });
|
||
}
|
||
// 工具栏按钮
|
||
var saveBtn = $('#btn-save');
|
||
var valBtn = $('#btn-validate');
|
||
var cmpBtn = $('#btn-compile');
|
||
var pubBtn = $('#btn-publish');
|
||
if (saveBtn) { saveBtn.addEventListener('click', doSave); }
|
||
if (valBtn) { valBtn.addEventListener('click', doValidate); }
|
||
if (cmpBtn) { cmpBtn.addEventListener('click', doCompile); }
|
||
if (pubBtn) { pubBtn.addEventListener('click', doPublish); }
|
||
|
||
// 画布区拖放(从块面板拖入)
|
||
var stage = $('#canvas-area');
|
||
if (stage) {
|
||
stage.addEventListener('dragover', function (e) { e.preventDefault(); });
|
||
stage.addEventListener('drop', function (e) {
|
||
e.preventDefault();
|
||
var type = e.dataTransfer.getData('text/plain');
|
||
if (!type || !state.canvas) { return; }
|
||
var rect = stage.getBoundingClientRect();
|
||
state.canvas.addBlock(type, e.clientX - rect.left - 60, e.clientY - rect.top - 20);
|
||
});
|
||
}
|
||
|
||
// 加载块定义 → 初始化画布
|
||
apiFetch(entireUrl('/drag/api/block_defs.dspy'), {}).then(function (r) {
|
||
state.blockDefs = (r && r.data) || {categories: [], blocks: {}};
|
||
renderBlockPanel();
|
||
var container = $('#canvas-area');
|
||
if (!container) { return; }
|
||
state.canvas = new DragCanvas(container, {
|
||
blocks: [],
|
||
connections: [],
|
||
blockDefs: state.blockDefs,
|
||
onSelect: function (b) { renderParamPanel(b); },
|
||
onChange: function () { /* 可做脏标记 */ }
|
||
});
|
||
// 载入已保存画布
|
||
loadGraphIfAny();
|
||
});
|
||
}
|
||
|
||
function loadGraphIfAny() {
|
||
// 通过查询参数 ?graph_id=xxx 载入画布
|
||
var m = location.search.match(/[?&]graph_id=([^&]+)/);
|
||
if (!m || !state.canvas) { return; }
|
||
apiFetch(entireUrl('/drag/api/graph_get.dspy'), {id: decodeURIComponent(m[1])}).then(function (r) {
|
||
if (r.status === 200 && r.data) {
|
||
state.graphId = r.data.id;
|
||
state.graphName = r.data.name || '';
|
||
var nameInput = $('#graph-name');
|
||
if (nameInput) { nameInput.value = state.graphName; }
|
||
state.canvas.setGraph({blocks: r.data.blocks || [], connections: r.data.connections || []});
|
||
}
|
||
});
|
||
}
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
} else {
|
||
init();
|
||
}
|
||
})();
|