/** * 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 = '' + def.label + ''; 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 = '
选中块后在此编辑参数
'; return; } var def = (state.blockDefs.blocks || {})[block.type]; if (!def) { return; } var title = document.createElement('div'); title.className = 'drag-param-title'; title.innerHTML = ' ' + def.label + ' ' + (def.description || '') + ''; 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 = ''; 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 = '
校验通过:无错误
'; return; } var html = '
校验发现 ' + errors.length + ' 个问题:
'; 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 = '
' + escapeHtml(d.content || '') + '
'; 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, '>'); } /* ─────────────── 初始化 ─────────────── */ 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(); } })();