Widget:可视化流程图编辑器
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@logicflow/core/dist/style/index.css" /> <script src="https://cdn.jsdelivr.net/npm/@logicflow/core/dist/logic-flow.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@logicflow/extension/lib/DndPanel.js"></script> <script src="https://cdn.jsdelivr.net/npm/@logicflow/extension/lib/Menu.js"></script> <script src="https://cdn.jsdelivr.net/npm/@logicflow/extension/lib/Control.js"></script> <script src="https://cdn.jsdelivr.net/npm/@logicflow/extension/lib/MiniMap.js"></script> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@logicflow/extension/lib/style/index.css" />
<link rel="stylesheet" href="/lysk/MediaWiki:CustomNode.css?action=raw&ctype=text/css" /> <script src="/lysk/index.php?title=MediaWiki:CustomNode.js&action=raw&ctype=text/javascript"></script> <body>
</body> <script>
console.log('编辑器 is loading...') LogicFlow.use(DndPanel); // 拖拽面板 LogicFlow.use(Menu); // 右键菜单面板 LogicFlow.use(Control); // 控制面板 LogicFlow.use(MiniMap); // 地图面板 const lf = new LogicFlow({ container: document.querySelector("#container"), //grid: true, idGenerator: (type) => { //重写全局id let edgeTypes =['line','polyline','bezier']; if (!edgeTypes.includes(type)) { let id_num = graphModel.nodes.length + 1; for (let i = 0; i < graphModel.nodes.length; i++) { if (id_num == graphModel.nodes[i].id.split('_')[1]) { id_num++; i--; } } return 'node_' + id_num; } } }); //注册自定义节点 lf.register(PlotNodeBox); lf.register(EndingNodeBox); lf.register(CommonGeneLgNodeBox); lf.register(CommonGeneSmNodeBox);
lf.extension.dndPanel.setPatternItems([ { type: 'plot_node', text: '剧情节点', label: '剧情节点', properties: {}, icon: 'https://patchwiki.biligame.com/images/lysk/4/43/hx0pjs2s9mgh7egmn7bbla00nmv8uvx.png', }, { type: 'ending_node', text: '结局节点', label: '结局节点', properties: {}, icon: 'https://patchwiki.biligame.com/images/lysk/9/92/ck64koptace7w3hxcv6u4iz5qq6dmz2.png', }, { type: 'common-gene-lg', text: '通用基因-大', label: '通用基因-大', properties: { iconURL: , }, icon: 'https://patchwiki.biligame.com/images/lysk/0/07/pkoemk9l9fkw2j9c98m161oiqvqpkuf.png', }, { type: 'common-gene-sm', text: '通用基因-小', label: '通用基因-小', properties: { iconURL: , }, icon: 'https://patchwiki.biligame.com/images/lysk/4/46/2te5815kkbuf7a8opo9pl6zttufa6sv.png', } ]); lf.extension.menu.setMenuConfig({ //edgeMenu: false, // 删除默认的边右键菜单 //graphMenu: [], // 覆盖默认的边右键菜单,与false表现一样 }); lf.extension.control.removeItem('zoom-out'); lf.extension.control.removeItem('zoom-in'); lf.extension.control.removeItem('reset'); lf.extension.control.addItem({ key: 'mini-map', iconClass: "custom-minimap", title: "", text: "导航", onClick: (lf, ev) => { const position = lf.getPointByClient(ev.x, ev.y); lf.extension.miniMap.show( position.domOverlayPosition.x - 120, position.domOverlayPosition.y + 35 ); }, }); lf.extension.control.addItem({ //格式化节点位置 iconClass: 'update-nodes', title: "", text: "格式化(剧情)", onClick: (lf, ev) => { updateNodesPos(); }, }); lf.extension.control.addItem({ //导入json iconClass: 'input-json', title: "", text: "导入json", onClick: (lf, ev) => { $('#input-json')[0].style.display = ; $('#model-json-input')[0].value = ; }, }); lf.extension.control.addItem({ //打印json iconClass: 'log-json', title: "", text: "导出json", onClick: (lf, ev) => { const modelData = graphModel.modelToGraphData(); //图形->数据 $('#show-json')[0].style.display = ; $('.json-content')[0].innerText = JSON.stringify(modelData); }, }); lf.extension.control.addItem({ //保存 iconClass: 'save-json', title: "", text: "保存", onClick: (lf, ev) => { //localStorage.setItem('flowData',JSON.stringify(graphModel.modelToGraphData())); const request = indexedDB.open('myDatabase'); request.onerror = function (event) { console.error('Database error: ', event.target.errorCode); }; request.onsuccess = function (event) { const db = request.result; const newData = { id: 'flowData', data: JSON.stringify(graphModel.modelToGraphData()) }; addData(db, newData); }; }, }); // 复制json到剪贴板 $('.copy-json')[0].addEventListener('click', (event) => { async function copyToClipboard(text) { try { await navigator.clipboard.writeText(text); alert('已复制到剪贴板!'); } catch (err) { console.error('Failed to copy text: ', err); } } copyToClipboard($('.json-content')[0].innerText); }) $('.close-json')[0].addEventListener('click', (event) => { $('#show-json')[0].style.display = 'none'; }) // 确认导入 $('.confirm-input')[0].addEventListener('click',(event)=>{ graphData = JSON.parse($('#model-json-input')[0].value); lf.render(graphData); console.log('流程数据:', graphData) changeSourceNode(); $('#input-json')[0].style.display = 'none'; }) $('.cancle-input')[0].addEventListener('click', (event) => { $('#input-json')[0].style.display = 'none'; $('#model-json-input')[0].value = ; }) // 输入节点属性json $('#node-json-input')[0].addEventListener('blur', (event) => { try { graphModel.getNodeModelById(currentNodeId).properties = JSON.parse(event.target.value); changeSourceNode(); //自动匹配parentNode event.target.value = JSON.stringify(graphModel.getNodeModelById(currentNodeId).properties); checkButtonTrigger(); // 检测按钮组buttonTrigger } catch (error) { $('#buttons-tip')[0].innerText = 'json格式错误,请检查!'; $('#buttons-tip')[0].style.opacity = '1'; setTimeout(() => { $('#buttons-tip')[0].style.opacity = '0'; }, 2000); } }) $('.close-panel')[0].addEventListener('click', (event) => { $('.right-panel')[0].style.display = 'none'; }) function changeSourceNode() { const modelData = graphModel.modelToGraphData(); if(!modelData.edges || modelData.edges.length === 0){return;} for (let i = 0; i < modelData.edges.length; i++) { let edge = modelData.edges[i]; let targetNode = graphModel.getNodeModelById(edge.targetNodeId); if (targetNode.properties.isBranchNode) { //分支节点 targetNode.properties.parentNode = edge.sourceNodeId; } } } function checkButtonTrigger(){ const dialogs = graphModel.getNodeModelById(currentNodeId).properties.dialogContents; if(!dialogs) return; for (let i = 0; i < dialogs.length; i++) { if (dialogs[i].unitType === "按钮组") { let buttons = dialogs[i].buttonsArr; for (let j = 0; j < buttons.length; j++) { if (!buttons[j].isInlineButton && !buttons[j].buttonTrigger) { $('#buttons-tip')[0].innerText = '该节点含有按钮组,请检查是否正确添加buttonTrigger!'; $('#buttons-tip')[0].style.opacity = '1'; setTimeout(() => { $('#buttons-tip')[0].style.opacity = '0'; }, 2000); } } } } } // 更新所有节点位置 function updateNodesPos() { let nodeFlag = {}; // for (let i = 0; i < graphModel.nodes.length; i++) { nodeFlag[graphModel.nodes[i].id] = false; } for (let i = 0; i < graphModel.nodes.length; i++) { const node = graphModel.nodes[i]; if (nodeFlag[node.id] && node.type === "ending_node") { continue; } if (node.properties.isBranchNode) { updateNodePos(findLeftNodeId(node.properties.parentNode), true); const edges = lf.getEdgeModels({ sourceNodeId: node.properties.parentNode, }); for (let j = 0; j < edges.length; j++) { nodeFlag[edges[j].targetNodeId] = true; //分支节点只遍历第一个 } } else { updateNodePos(node.id, false); nodeFlag[node.id] = true; }
} }
// 寻找同级最左端的节点 function findLeftNodeId(parentId) { const edges = lf.getEdgeModels({ sourceNodeId: parentId, }); let leftId = edges[0].targetNodeId; let minX = lf.getNodeDataById(edges[0].targetNodeId).x; for (let i = 1; i < edges.length; i++) { let curNode = lf.getNodeDataById(edges[i].targetNodeId); if (minX > curNode.x) { minX = curNode.x; leftId = curNode.id; } } return leftId; } // 寻找同级最右端的节点 function findRightNodeId(parentId) { const edges = lf.getEdgeModels({ sourceNodeId: parentId, }); let rightId = edges[0].targetNodeId; let maxX = lf.getNodeDataById(edges[0].targetNodeId).x; for (let i = 1; i < edges.length; i++) { let curNode = lf.getNodeDataById(edges[i].targetNodeId); if (maxX < curNode.x) { maxX = curNode.x; rightId = curNode.id; } } return rightId; }
//更新下一节点位置 function updateNodePos(sourceNodeId, isBranch) { const nextEdges = lf.getEdgeModels({ sourceNodeId: sourceNodeId, }); const sourceNode = lf.getNodeDataById(sourceNodeId); const transXY_one = [{ transX: 0, transY: 80 }], transXY_two_left = { transX: -100, transY: 110 }, transXY_two_right = { transX: 100, transY: 110 }, transXY_three = [{ transX: -100, transY: 110 }, { transX: 0, transY: 150 }, { transX: 100, transY: 110 }]; let transXY; // 存放下级节点偏移量 let nextNodesId = []; // 按当前左右顺序存放节点id[] if (nextEdges.length === 1 && !isBranch) { //无分支,直接指向下一节点 transXY = transXY_one; nextNodesId = [nextEdges[0].targetNodeId]; } else if (nextEdges.length === 1 && isBranch) { //分支汇总 transXY = [{ transX: 100, transY: 130 }]; nextNodesId = [nextEdges[0].targetNodeId]; } else if (nextEdges.length === 2) {// 两个分支 nextNodesId.push(findLeftNodeId(sourceNodeId)); nextNodesId.push(findRightNodeId(sourceNodeId)); transXY = [transXY_two_left, transXY_two_right]; // 含有结局节点强制右侧显示 if(lf.getNodeDataById(findLeftNodeId(sourceNodeId)).type === 'ending_node'){ transXY = [transXY_two_right, transXY_two_left]; } } else if (nextEdges.length === 3) {// 三个分支 transXY = transXY_three; let leftId = findLeftNodeId(sourceNodeId), rightId = findRightNodeId(sourceNodeId) nextNodesId.push(leftId); nextNodesId.push( nextEdges.filter(edge => { return edge.targetNodeId !== leftId && edge.targetNodeId !== rightId; })[0].targetNodeId ); nextNodesId.push(rightId); } for (let i = 0; i < nextNodesId.length; i++) { const targetNode = lf.getNodeDataById(nextNodesId[i]); let tragetX = sourceNode.x + transXY[i].transX, tragetY = sourceNode.y + transXY[i].transY; // 修改节点坐标 graphModel.moveNode2Coordinate(nextNodesId[i], tragetX, tragetY, true); } }
// 向indexedDB中写入数据 function addData(db, data) { const transaction = db.transaction(['myObjectStore'], 'readwrite'); const store = transaction.objectStore('myObjectStore'); const request = store.put(data); request.onsuccess = function (event) { alert('数据缓存成功!'); }; request.onerror = function (event) { console.error('Error adding data.', event.target.errorCode); }; } //读取数据 var graphData,graphModel; const request = indexedDB.open('myDatabase'); request.onerror = function (event) { console.error('Database error: ', event.target.errorCode); }; request.onsuccess = function (event) { const db = request.result; const transaction = db.transaction(['myObjectStore'], 'readonly'); const store = transaction.objectStore('myObjectStore'); const result = store.get('flowData'); result.onsuccess = function (event) { const data = result.result; if (data) { graphData = JSON.parse(data.data); } else { graphData = {}; } console.log('流程数据:', graphData); lf.render(graphData); //渲染 graphModel = lf.graphModel; changeSourceNode(); // 初始化分支节点的parentNode属性 };
result.onerror = function (event) { console.error('Error retrieving data.', event.target.errorCode); }; } request.onupgradeneeded = function (event) { const db = event.target.result; const store = db.createObjectStore('myObjectStore', { keyPath: 'id' }); }; // 节点点击事件 var currentNodeId = ; lf.on("node:click", (node) => { console.log('节点数据:', node.data); currentNodeId = node.data.id; $('.right-panel')[0].style.display = ; const currentNodeModel = graphModel.getNodeModelById(currentNodeId); $('#current-node')[0].innerText = `${currentNodeModel.text.value}(${currentNodeModel.id})`; $('#node-json-input')[0].value = JSON.stringify(currentNodeModel.properties); }); lf.on('text:update', (data) => { // 修改通用基因text显示 if (data.type.includes("common-gene")) { $(`#${data.id}>.text-div`)[0].innerText = data.text; } })
</script> <style>
#container { width: 100%; height: 500px } #buttons-tip { position: absolute; width: 500px; background-color: #fdaeae; border-radius: 5px; text-align: center; padding: 10px; top: -40px; left: calc((100vw - 500px) / 2); transition: opacity 0.5s ease-in-out; } .overlay { position: absolute; width: 80%; height: 80%; font-size: 10px; overflow: auto; top: 10%; left: 10%; background: #e1dddd; padding: 20px; } .right-panel { position: absolute; height: 500px; width: 300px; right: 0; top: 0; background-color: #ddd; padding: 10px; } .lf-dndpanel{ top:60px; } .lf-control{ right: unset !important; } .custom-minimap { background: url(data:image/png;base64,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);
}
.lf-dnd-item>.lf-dnd-shape{ background-size: 100%; }
</style>