bugfix250107.1
全站通知:

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>

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