本WIKI编辑权限开放,正由 恋与深空Evol攻略组 搭建基础框架ing,期待更多猎人加入WIKI建设!
反馈留言 • 收藏方法 • 加入我们
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>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@logicflow/extension/lib/style/index.css" /> <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="/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>
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: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAABGdBTUEAALGPC/xhBQAAAnBJREFUOBGdVL1rU1EcPfdGBddmaZLiEhdx1MHZQXApraCzQ7GKLgoRBxMfcRELuihWKcXFRcEWF8HBf0DdDCKYRZpnl7p0svLe9Zzbd29eQhTbC8nv+9zf130AT63jvooOGS8Vf9Nt5zxba7sXQwODfkWpkbjTQfCGUd9gIp3uuPP8bZ946g56dYQvnBg+b1HB8VIQmMFrazKcKSvFW2dQTxJnJdQ77urmXWOMBCmXM2Rke4S7UAW+/8ywwFoewmBps2tu7mbTdp8VMOkIRAkKfrVawalJTtIliclFbaOBqa0M2xImHeVIfd/nKAfVq/LGnPss5Kh00VEdSzfwnBXPUpmykNss4lUI9C1ga+8PNrBD5YeqRY2Zz8PhjooIbfJXjowvQJBqkmEkVnktWhwu2SM7SMx7Cj0N9IC0oQXRo8xwAGzQms+xrB/nNSUWVveI48ayrFGyC2+E2C+aWrZHXvOuz+CiV6iycWe1Rd1Q6+QUG07nb5SbPrL4426d+9E1axKjY3AoRrlEeSQo2Eu0T6BWAAr6COhTcWjRaYfKG5csnvytvUr/WY4rrPMB53Uo7jZRjXaG6/CFfNMaXEu75nG47X+oepU7PKJvvzGDY1YLSKHJrK7vFUwXKkaxwhCW3u+sDFMVrIju54RYYbFKpALZAo7sB6wcKyyrd+aBMryMT2gPyD6GsQoRFkGHr14TthZni9ck0z+Pnmee460mHXbRAypKNy3nuMdrWgVKj8YVV8E7PSzp1BZ9SJnJAsXdryw/h5ctboUVi4AFiCd+lQaYMw5z3LGTBKjLQOeUF35k89f58Vv/tGh+l+PE/wG0rgfIUbZK5AAAAABJRU5ErkJggg==', }, { type: 'ending_node', text: '结局节点', label: '结局节点', properties: {}, icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAABGdBTUEAALGPC/xhBQAAAnBJREFUOBGdVL1rU1EcPfdGBddmaZLiEhdx1MHZQXApraCzQ7GKLgoRBxMfcRELuihWKcXFRcEWF8HBf0DdDCKYRZpnl7p0svLe9Zzbd29eQhTbC8nv+9zf130AT63jvooOGS8Vf9Nt5zxba7sXQwODfkWpkbjTQfCGUd9gIp3uuPP8bZ946g56dYQvnBg+b1HB8VIQmMFrazKcKSvFW2dQTxJnJdQ77urmXWOMBCmXM2Rke4S7UAW+/8ywwFoewmBps2tu7mbTdp8VMOkIRAkKfrVawalJTtIliclFbaOBqa0M2xImHeVIfd/nKAfVq/LGnPss5Kh00VEdSzfwnBXPUpmykNss4lUI9C1ga+8PNrBD5YeqRY2Zz8PhjooIbfJXjowvQJBqkmEkVnktWhwu2SM7SMx7Cj0N9IC0oQXRo8xwAGzQms+xrB/nNSUWVveI48ayrFGyC2+E2C+aWrZHXvOuz+CiV6iycWe1Rd1Q6+QUG07nb5SbPrL4426d+9E1axKjY3AoRrlEeSQo2Eu0T6BWAAr6COhTcWjRaYfKG5csnvytvUr/WY4rrPMB53Uo7jZRjXaG6/CFfNMaXEu75nG47X+oepU7PKJvvzGDY1YLSKHJrK7vFUwXKkaxwhCW3u+sDFMVrIju54RYYbFKpALZAo7sB6wcKyyrd+aBMryMT2gPyD6GsQoRFkGHr14TthZni9ck0z+Pnmee460mHXbRAypKNy3nuMdrWgVKj8YVV8E7PSzp1BZ9SJnJAsXdryw/h5ctboUVi4AFiCd+lQaYMw5z3LGTBKjLQOeUF35k89f58Vv/tGh+l+PE/wG0rgfIUbZK5AAAAABJRU5ErkJggg==', }, { type: 'common-gene-lg', text: '通用基因-大', label: '通用基因-大', properties: { iconURL: , }, icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAABGdBTUEAALGPC/xhBQAAAnBJREFUOBGdVL1rU1EcPfdGBddmaZLiEhdx1MHZQXApraCzQ7GKLgoRBxMfcRELuihWKcXFRcEWF8HBf0DdDCKYRZpnl7p0svLe9Zzbd29eQhTbC8nv+9zf130AT63jvooOGS8Vf9Nt5zxba7sXQwODfkWpkbjTQfCGUd9gIp3uuPP8bZ946g56dYQvnBg+b1HB8VIQmMFrazKcKSvFW2dQTxJnJdQ77urmXWOMBCmXM2Rke4S7UAW+/8ywwFoewmBps2tu7mbTdp8VMOkIRAkKfrVawalJTtIliclFbaOBqa0M2xImHeVIfd/nKAfVq/LGnPss5Kh00VEdSzfwnBXPUpmykNss4lUI9C1ga+8PNrBD5YeqRY2Zz8PhjooIbfJXjowvQJBqkmEkVnktWhwu2SM7SMx7Cj0N9IC0oQXRo8xwAGzQms+xrB/nNSUWVveI48ayrFGyC2+E2C+aWrZHXvOuz+CiV6iycWe1Rd1Q6+QUG07nb5SbPrL4426d+9E1axKjY3AoRrlEeSQo2Eu0T6BWAAr6COhTcWjRaYfKG5csnvytvUr/WY4rrPMB53Uo7jZRjXaG6/CFfNMaXEu75nG47X+oepU7PKJvvzGDY1YLSKHJrK7vFUwXKkaxwhCW3u+sDFMVrIju54RYYbFKpALZAo7sB6wcKyyrd+aBMryMT2gPyD6GsQoRFkGHr14TthZni9ck0z+Pnmee460mHXbRAypKNy3nuMdrWgVKj8YVV8E7PSzp1BZ9SJnJAsXdryw/h5ctboUVi4AFiCd+lQaYMw5z3LGTBKjLQOeUF35k89f58Vv/tGh+l+PE/wG0rgfIUbZK5AAAAABJRU5ErkJggg==', }, { type: 'common-gene-sm', text: '通用基因-小', label: '通用基因-小', properties: { iconURL: , }, icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAABGdBTUEAALGPC/xhBQAAAnBJREFUOBGdVL1rU1EcPfdGBddmaZLiEhdx1MHZQXApraCzQ7GKLgoRBxMfcRELuihWKcXFRcEWF8HBf0DdDCKYRZpnl7p0svLe9Zzbd29eQhTbC8nv+9zf130AT63jvooOGS8Vf9Nt5zxba7sXQwODfkWpkbjTQfCGUd9gIp3uuPP8bZ946g56dYQvnBg+b1HB8VIQmMFrazKcKSvFW2dQTxJnJdQ77urmXWOMBCmXM2Rke4S7UAW+/8ywwFoewmBps2tu7mbTdp8VMOkIRAkKfrVawalJTtIliclFbaOBqa0M2xImHeVIfd/nKAfVq/LGnPss5Kh00VEdSzfwnBXPUpmykNss4lUI9C1ga+8PNrBD5YeqRY2Zz8PhjooIbfJXjowvQJBqkmEkVnktWhwu2SM7SMx7Cj0N9IC0oQXRo8xwAGzQms+xrB/nNSUWVveI48ayrFGyC2+E2C+aWrZHXvOuz+CiV6iycWe1Rd1Q6+QUG07nb5SbPrL4426d+9E1axKjY3AoRrlEeSQo2Eu0T6BWAAr6COhTcWjRaYfKG5csnvytvUr/WY4rrPMB53Uo7jZRjXaG6/CFfNMaXEu75nG47X+oepU7PKJvvzGDY1YLSKHJrK7vFUwXKkaxwhCW3u+sDFMVrIju54RYYbFKpALZAo7sB6wcKyyrd+aBMryMT2gPyD6GsQoRFkGHr14TthZni9ck0z+Pnmee460mHXbRAypKNy3nuMdrWgVKj8YVV8E7PSzp1BZ9SJnJAsXdryw/h5ctboUVi4AFiCd+lQaYMw5z3LGTBKjLQOeUF35k89f58Vv/tGh+l+PE/wG0rgfIUbZK5AAAAABJRU5ErkJggg==', } ]); 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: "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({ //格式化节点位置 key: 'update-nodes', title: "", text: "格式化(剧情)", onClick: (lf, ev) => { updateNodesPos(); }, }); lf.extension.control.addItem({ //导入json key: 'input-json', title: "", text: "导入json", onClick: (lf, ev) => { $('#input-json')[0].style.display = ; $('#model-json-input')[0].value = ; }, }); lf.extension.control.addItem({ //打印json key: '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({ //保存 key: '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; }
</style>