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>

沪公网安备 31011002002714 号