本WIKI编辑权限开放,正由 恋与深空Evol攻略组 搭建基础框架ing,期待更多猎人加入WIKI建设!
反馈留言收藏方法加入我们

全站通知:

Widget:可视化流程图编辑器

来自恋与深空WIKI_BWIKI_哔哩哔哩
跳到导航 跳到搜索

<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>