scense_drag/wwwroot/drag-editor.js

323 lines
12 KiB
JavaScript
Raw 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-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('/scense_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('/scense_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('/scense_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('/scense_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('/scense_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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
/* ─────────────── 初始化 ─────────────── */
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('/scense_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('/scense_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();
}
})();