背景
本文是对 基于vue和jsplumb的工作流编辑器开发 的扩展
业务实现
- 撤销
- 初始化数据
- 自动排列
- 清空数据
撤销
对于撤销的实现,主要是需要一个缓冲内存,存储每次操作之后的数据结构,方便再点击撤销
按钮的时候,从缓冲内存中拿出数据结构来渲染页面。
利用数组来存储操作之后的数据结构。
let MEMORY_LIST = [];
这里只缓存10次操作。
$_updateMemoryList() {
<!--格式化数据结构-->
const tempItem = this.formatData();
// max store is 10
if (MEMORY_LIST.length > 10) {
MEMORY_LIST.shift();
}
MEMORY_LIST.push(tempItem);
// 更新按钮可操作状态。
this.$_updateCanUndoBtn();
}
然后在各种操作,比如新增节点,拖拽节点的时候,调用$_updateMemoryList
方法
当点击撤销
按钮的时候,
handleUndo() {
if (!this.canUndo) {
return;
}
if (MEMORY_LIST.length > 0) {
const tempItem = MEMORY_LIST.pop();
this.$_doClear();
this.$options.jsPlumb.reset();
this.$nextTick(() => {
this.updateFlow(tempItem, this.$_plumbRepaintEverything);
})
}
this.$_updateCanUndoBtn();
},
直接从缓冲内存里面读取数据结构,重新渲染流程图。更新按钮状态。
初始化数据
对于初始化渲染的数据结构
{
positions:{
"key":{
left:'xx',
top:'xx'
}
},
steps:[
{
elementId:'startNode',
stepId:'uuid',
nextStep:'uuid'
},
{
"elementId": "switchNode",
"stepId": "c89829f5-8595-458c-b040-4ff84d27befc",
"nextSteps": [
{
"nextStep": "d611c32f-b6c0-4b97-80d9-47b783bd93ad",
},
{
"nextStep": "7bd4fc3d-c3b9-4b19-81dc-e49cd1e7b5c5",
},
{
"nextStep": "85c30556-75fa-441c-9a1d-0dced21755a5",
}
],
},
{
"elementId": "stopNode",
"stepId": "d611c32f-b6c0-4b97-80d9-47b783bd93ad",
"nextStep": null
},
]
}
通过这样的数据结构,然后执行渲染方法updateFlow
updateFlow(editItem, callback) {
let positions = JSON.parse(editItem.positions);
let steps = editItem.steps;
let flowList = [];
steps.forEach((step) => {
let flowItem = this.getFlowItemById(step.elementId);
if (!flowItem) {
return;
}
flowItem.next = [];
flowItem.prev = [];
flowItem.uuid = step.stepId;
let position = positions[step.stepId];
if (position) {
flowItem.left = position.left;
flowItem.top = position.top;
}
if (step.nextStep) {
flowItem.next = [step.nextStep];
} else if (step.nextSteps) {
flowItem.next = step.nextSteps.map((nextStep) => {
return nextStep.nextStep;
});
}
if (flowItem.type !== FLOW_ITEM_TYPE.endNode) {
//
if (this.isIfFlowItem(flowItem.type)) {
let formData = clone(step.nextSteps[0]);
formData.stepName = step.stepName;
flowItem.formData = this.getFlowItemFormData(formData);
// else
if (formData.isDefault) {
flowItem.nextElseId = formData.nextStep;
flowItem.nextIfId = step.nextSteps[1].nextStep;
} else {
flowItem.nextIfId = formData.nextStep;
flowItem.nextElseId = step.nextSteps[1].nextStep;
}
if (step.stepJson) {
let stepOtherObj = JSON.parse(step.stepJson);
flowItem.formData.ifNodeTitle = stepOtherObj.ifNodeTitle;
}
} else if (this.isExpandFlowItem(flowItem.type)) {
let ruleGroupList = step.nextSteps;
let formData = {};
formData.stepName = step.stepName;
formData.ruleGroupList = ruleGroupList;
flowItem.formData = formData;
} else {
flowItem.formData = this.getFlowItemFormData(step);
}
}
flowList.push(flowItem);
});
// update
flowList.forEach((item) => {
if (item.next.length > 0) {
item.next.forEach((id) => {
let nextItem = _.find(flowList, (tempItem) => {
return tempItem.uuid === id;
});
if (nextItem) {
if (nextItem.prev.indexOf(item.uuid) === -1) {
nextItem.prev.push(item.uuid);
}
}
});
}
});
this.flowList = flowList;
this.$nextTick(() => {
//
flowList.forEach((item) => {
this.$options.jsPlumb.draggable(item.uuid, {});
});
this.$nextTick(() => {
flowList.forEach((item) => {
item.next.forEach((id, index) => {
let nextFlowItem = this.getFlow(id);
if (!this.isTempFlowItem(nextFlowItem)) {
this.addFlowItemConnect(item.uuid, id);
} else {
this.draggableFlowConnect(item.uuid, id, true);
}
//
if (this.isIfFlowItem(item.type)) {
let isIf = item.nextIfId === nextFlowItem.uuid;
this.createFlowItemLabel(item.uuid, id, isIf ? '是' : '否');
} else if (this.isExpandFlowItem(item.type)) {
let name = this.getExpandFlowItemName(item, id);
this.createFlowItemLabel(item.uuid, id, name);
}
});
});
this.$nextTick(() => {
callback && callback();
})
});
});
},
自动排列
对于自动排列的实现,主要是借鉴了 一种紧凑树形布局算法的实现的实现,写的非常棒,谢谢这位巨人,让我站在了他的肩膀上。
大体的思路就是:
- 我们从上往下,先根据相邻节点间地最小间距
FLOW_LEFT_STEP_LENGTH
对树进行第一次布局。先初始化根节点的位置 。从根节点开始。人为设定好根节点的坐标 ,然后将根节点的子节点挂在根节点下,且子节点分布在根节点的FLOW_STEP_LENGTH
高度下方,子节点彼此间距为FLOW_LEFT_STEP_LENGTH
且相对于根节点对称分布。递归进行此步骤,直到所有的节点都布局好。 - 然后,我们需要一个hashTree,用作将树保存到一个按层次分别的线性表中。我们将树转换到hashTree。效果图中的树对应hashTree如下:
/**
* layer [
* 0 [ node(0) ],
* 1 [ node(1), node(2), node(3), node(4), node(5) ],
* 2 [ node(6), node(7), node(8), node(9), node(10), node(11), node(12), node(13), node(14), node(15), node(16), node(17), node(18), node(19), node(20) ],
* 3 [ node(21), node(22), node(23), node(24), node(25), node(26), node(27), node(28) ]
* ]
*/
- 从最低层开始从下往上按层遍历hashTree,检测相邻的节点。假设n1,n2为相邻的一对节点,n1的在线性表的下标小于n2。检测n1,n2是否重叠。如果发生重叠,则左边不动,整体往右进行调整。但调整的不是n2节点,而是“与n1的某个祖先节点为兄弟节点的n2的祖先节点”。
- 每移动完一个节点,其父节点都会失去对称性,所以要进行调整。但我们不动父节点,只通过往左移动子节点来恢复对称性。
- 每次恢复对称性后,有某些子节点又会发生重叠现象,所以这时要回到底层重新开始扫描。
- 重复3,4,5步骤,直到所有重叠都被消除,布局完成。
先初始化 开始节点
的位置。
this.tempLayerMap = [];
const startNode = _.find(this.flowList, (flowItem) => {
return this.isStartFlowItem(flowItem);
});
// init start flow item
startNode.top = FLOW_START_STEP_TOP;
startNode.left = this.getFlowItemInitLeft();
初始化第一层的数据。
this.tempLayerMap[0] = [startNode];
对子树进行递归下降布局
this.$_layoutChild(startNode, 1);
在这个方法里面,遍历所有的子节点,初始化top
和left
的位置。
$_layoutChild 方法
$_layoutChild(pre, layer) {
const nextList = pre.next;
const nextListLength = nextList.length;
if (this.tempLayerMap[layer] === undefined) {
this.tempLayerMap[layer] = [];
}
nextList.forEach((nextFlowUUid, index) => {
const flowItem = this.getFlow(nextFlowUUid);
// 初始化 top
flowItem.top = pre.top + FLOW_STEP_LENGTH;
const startLeft = pre.left - (FLOW_LEFT_STEP_LENGTH * (nextListLength - 1)) / 2
// 初始化 left
flowItem.left = startLeft + FLOW_LEFT_STEP_LENGTH * index;
this.tempLayerMap[layer].push(flowItem);
if (flowItem.next && flowItem.next.length > 0) {
this.$_layoutChild(flowItem, layer + 1);
}
})
},
然后再 对所有子节点进行上升重叠调整
$_adjustChild()
$_adjustChild() {
let flowList = null;
<!--从最底层开始-->
for (let i = this.tempLayerMap.length - 1; i >= 0; i--) {
flowList = this.tempLayerMap[i];
flowList.forEach((flowItem, index) => {
const leftFlowItem = flowList[index - 1];
<!--发生重叠-->
if (leftFlowItem && flowItem.left - leftFlowItem.left < FLOW_LEFT_STEP_LENGTH) {
<!---->
const parentFlowItem = this.$_findCommonParentNode(leftFlowItem, flowItem);
const leftOffset = Math.abs(flowItem.left - leftFlowItem.left) + FLOW_LEFT_STEP_LENGTH;
<!--更改每个节点的left值-->
this.$_translateXTree(parentFlowItem, leftOffset);
const prevFlowItem = this.getFlow(parentFlowItem.prev[0]);
<!--居中所有子节点-->
this.$_centerChild(prevFlowItem);
<!--移动后下层节点有可能再次发生重叠,所以重新从底层扫描-->
i = this.tempLayerMap.length;
}
})
}
},
清空数据(重做)
因为涉及到清除掉每个节点信息,以及节点绑定的事件,以及jsPlumb
上面绑定的事件,所以不能单纯的清除vue 绑定的 list
数据、
所以我的做法是,找到开始节点,然后一层层的遍历下去,把节点信息以及绑定的jsPlumb
信息删除掉。
$_doClear(needInit) {
const startNode = _.find(this.flowList, (flowItem) => {
return this.isStartFlowItem(flowItem);
});
if (startNode) {
this.deleteNextFlowItem(startNode.uuid);
if (needInit) {
this.$nextTick(() => {
this.initFlow();
})
}
}
},
项目地址
github: github.com/bosscheng/v…