|
<template>
<div class="subflow-logic">
<!-- 逻辑编排节点区域 -->
<div class="logic-l">
<dnd-nodes
:nodes="nodes"
:graph="graph"
:on-drag-start="onDragStart"
></dnd-nodes>
</div>
<!-- 绘画区域 -->
<div class="logic-r">
<tool-bar
:graph="graph"
:tool-list="toolList"
@tool-handel-click="toolHandelClick"
></tool-bar>
<div id="flow-content" class="flow-content">
<div id="flow"></div>
<TeleportContainer />
</div>
</div>
</div>
<!-- 弹窗区域 -->
<right-panel
:computed-components="computedComponents"
:target-node="targetNode"
:show-modal="showModal"
@close-dialog="closeDialog"
></right-panel>
</template>
<script setup>
import {
ref,
getCurrentInstance,
onMounted,
nextTick,
watchEffect,
defineEmits
} from 'vue';
/** x6相关start */
import { Graph, Dom, Point, Shape } from '@antv/x6';
import { Snapline } from '@antv/x6-plugin-snapline';
import { Clipboard } from '@antv/x6-plugin-clipboard';
import { Keyboard } from '@antv/x6-plugin-keyboard';
import { History } from '@antv/x6-plugin-history';
import { Dnd } from '@antv/x6-plugin-dnd';
import { Selection } from '@antv/x6-plugin-selection';
import { register, getTeleport } from '@antv/x6-vue-shape';
/** x6相关end */
import CustomeNode from './node/CustomeNode.vue';
import BranchNode from './node/BranchNode.vue';
import DndNodes from './DndNodes.vue';
import ToolBar from './ToolBar.vue';
import { CreateNodeClass } from './shape.js';
import RightPanel from './RightPanel.vue';
const { proxy } = getCurrentInstance();
const showModal = ref(false);
/**
* 该组件,接收参数: nodes,nodesComponents,toolbar
*/
const props = defineProps({
nodes: {
type: Array,
default: () => []
},
toolList: {
type: Array,
default: () => []
},
flowData: {
type: Object,
default: () => {}
},
computedComponents: {
type: Object,
default: () => {}
}
});
const emits = defineEmits([
'toolHandelClick',
'select-node',
'change:node',
'open:right-panel'
]);
// 画布初始化
const graph = ref(null);
const dnd = ref(null);
const zoom = ref(1);
const targetNode = ref(null);
//
function initGraph() {
const container = document.getElementById('flow');
graph.value = new Graph({
container,
autoResize: true,
background: {
color: '#F8F8F8'
},
panning: {
enabled: true
},
mousewheel: {
enabled: true
},
scaling: {
min: 0.5,
max: 2
},
// @:网格
grid: {
size: 10,
visible: true
},
// 配置全局连线规则
connecting: {
snap: true,
anchor: 'center', // 锚点位置
allowBlank: false, // 是否允许连接空白位置
allowLoop: false, // 是否允许循环创建节点
allowPort: true, // 是否允许边链接到链接桩,
allowNode: false,
highlight: true,
// snap: true, // 自动吸附(50px)
createEdge() {
return new Shape.Edge({
attrs: {
line: {
stroke: '#B1B1B1',
strokeWidth: 1,
targetMarker: {
name: 'block',
width: 5,
height: 9
}
}
},
router: 'custom',
zIndex: 0,
connector: {
name: '',
args: {
radius: 90
}
}
});
},
// 判断节点是否可被连接
validateConnection({ sourcePort, targetPort, sourceCell, targetCell }) {
const data = targetCell.getData();
// @: xx类型节点不被连接 start节点不能被连接
if (data.name === 'start') {
return false;
}
const tartgetPorts = targetCell.getPorts();
const sourcePorts = sourceCell.getPorts();
/**
* 1. 如果源点的group为input时,不允许连接
* 2. 如果target的group为output时,不允许连接
*/
if (
sourcePorts.filter((el) => el.id === sourcePort)?.[0].group ===
'input'
) {
proxy.$message.error('不能从输入节点进行连线!');
return false;
}
if (
tartgetPorts.filter((el) => el.id === targetPort)?.[0].group ===
'output'
) {
proxy.$message.error('不能连接输出节点!');
return false;
}
return true;
},
// 限制链接桩只能链接一条边(需在ports中设置connectionCount)
validateMagnet({ magnet, cell }) {
let count = 0;
const connectionCount = magnet.getAttribute('connection-count');
const max = connectionCount ? parseInt(connectionCount, 10) : 1;
if (max === 0) {
proxy.$message.error('请配置路由分发节点!');
}
const outgoingEdges = graph.value.getOutgoingEdges(cell);
if (outgoingEdges) {
outgoingEdges.forEach((edge) => {
const edgeView = graph.value.findViewByCell(edge);
if (edgeView.sourceMagnet === magnet) {
count += 1;
}
});
}
return count < max;
}
},
// 节点拖拽到另外一个节点中
embedding: {
enabled: true,
findParent({ node }) {
const bbox = node.getBBox();
return this.getNodes().filter((item) => {
// @: 只有data.parent为true的节点才是父节点
const data = item.getData();
if (data && data.parent) {
const targetBBox = item.getBBox();
return bbox.isIntersectWithRect(targetBBox);
}
return false;
});
}
}
});
dnd.value = new Dnd({
target: graph.value,
scaled: false,
validateNode(droppingNode, options) {
return droppingNode.shape === 'html'
? new Promise((resolve) => {
const { draggingNode, draggingGraph } = options;
const view = draggingGraph.findView(draggingNode);
const contentElem = view.findOne('foreignObject > body > div');
Dom.addClass(contentElem, 'validating');
setTimeout(() => {
Dom.removeClass(contentElem, 'validating');
resolve(true);
}, 3000);
})
: true;
}
});
graph.value
.use(
new Selection({
enabled: true,
movable: true,
showNodeSelectionBox: true,
multiple: false
})
)
.use(
new Snapline({
enabled: true
})
)
.use(
new Clipboard({
enabled: true,
useLocalStorage: true
})
)
.use(
new Keyboard({
enabled: true
})
)
.use(
new History({
enabled: true
})
);
// eslint-disable-next-line no-use-before-define
initEvent(graph.value);
}
// 节点注册
const TeleportContainer = getTeleport();
const customNodes = [
{
name: 'flow-start-node',
component: CustomeNode,
w: 100,
h: 100
},
{
name: 'flow-end-node',
component: CustomeNode,
w: 100,
h: 100
},
{
name: 'flow-input-node',
component: CustomeNode,
w: 100,
h: 100
},
{
name: 'flow-route-node',
component: BranchNode,
w: 200,
h: 100
}
];
function registerNode() {
customNodes.forEach((node) => {
register({
shape: node.name,
component: node.component,
width: node.w,
height: node.h
});
});
}
registerNode();
// 开始节点
const dragStartNode = (target, e) => {
if (target.name === 'start') {
const node = new CreateNodeClass(
'flow-start-node',
graph.value,
target
).createStartNode();
dnd.value.start(node, e);
}
};
// 结束节点
const dragEndNode = (target, e) => {
if (target.name === 'end') {
const node = new CreateNodeClass(
'flow-end-node',
graph.value,
target
).createEndNode();
dnd.value.start(node, e);
}
};
// 输入输出节点
const dragInputNode = (target, e) => {
if (
target.type === 'logic' ||
target.type === 'data-processing' ||
target.type === 'route'
) {
const node = new CreateNodeClass(
'flow-input-node',
graph.value,
target
).createNormalNode();
dnd.value.start(node, e);
}
};
// 分支节点
const dragBranchNode = (target, e) => {
if (target.id === 'branch') {
const node = new CreateNodeClass(
'flow-route-node',
graph.value,
target
).createBranchNode();
dnd.value.start(node, e);
}
};
// 拖拽事件: 拖拽节点到画布
function onDragStart(data, event) {
dragStartNode(data, event);
dragEndNode(data, event);
dragInputNode(data, event);
dragBranchNode(data, event);
}
// 注册路由
function registerRouter() {
Graph.registerRouter('custom', (vertices, args, view) => {
const start = view.sourceAnchor;
const points = [];
points.push(new Point(start.x + 48, start.y));
points.push(
new Point(view.targetBBox.leftMiddle.x - 20, view.targetBBox.leftMiddle.y)
);
return points;
});
}
// 初始化事件
function initEvent(graph) {
graph.on('edge:mouseenter', ({ cell }) => {
if (cell) {
let removeBtnCfg = {};
if (cell.isEdge()) {
removeBtnCfg = { distance: '30%' };
}
if (cell.isNode()) {
const cellView = graph.value.findView(cell);
cellView.addClass(`${cell.shape}-selected`);
removeBtnCfg = { x: 0, y: 0, offset: { x: -5, y: -5 } };
}
cell.addTools({
name: 'button-remove', // 工具名称
args: removeBtnCfg // 工具对应的参数
});
}
});
graph.on('edge:mouseleave', ({ cell }) => {
cell.removeTools();
});
graph.on('node:added', () => {
// store.commit('sceneFlow/setCurrentNodes', graph.getNodes());
});
// 双击节点,打开弹窗
graph.on('node:dblclick', (node) => {
// proxy.$eBus.emit('select:node', node.node.store.data);
emits('select-node', node.node.store.data);
targetNode.value = node.node.store.data;
showModal.value = true;
});
graph.on('node:mousedown', () => {});
graph.on('node:selected', (cell) => {
// const data = cell.node.getData()
// proxy.$eBus.emit('select-node', cell)
emits('select-node', cell);
});
graph.on('cell:dblclick', (cell) => {
// proxy.$eBus.emit('select-node', cell);
emits('select-node', cell);
});
graph.on('edge:click', (e, edge) => {
/**
* 判断连线的source cell,如果是route节点,则可以进行label的编辑与新增
*/
console.log(e, edge);
});
graph.on('edge:contextmenu', ({ edge }) => {
const curCell = edge.getSourceCell();
console.log(curCell);
});
graph.on('node:changed', (e) => {
// console.log(node, view);
// proxy.$eBus.emit('change:node', e.cell.store.data);
emits('change:node', e.cell.store.data);
});
// 单机选中节点,右键打开弹窗
graph.on('node:contextmenu', () => {});
// 点击空白处关闭弹窗
graph.on('blank:dblclick', () => {
// proxy.$eBus.emit('open:right-panel');
emits('open:right-panel');
showModal.value = true;
});
graph.on('blank:mousedown', () => {});
// 复制
graph.bindKey(['meta+c', 'ctrl+c'], () => {
const cells = graph.getSelectedCells();
if (cells.length) {
graph.copy(cells);
}
return false;
});
// 剪切
graph.bindKey(['meta+x', 'ctrl+x'], () => {
const cells = graph.getSelectedCells();
if (cells.length) {
graph.cut(cells);
}
return false;
});
// 粘贴
graph.bindKey(['meta+v', 'ctrl+v'], () => {
if (!graph.isClipboardEmpty()) {
const cells = graph.paste({ offset: 100 });
graph.cleanSelection();
graph.select(cells);
}
return false;
});
// 删除
graph.bindKey(['meta+d', 'ctrl+d'], () => {
const cells = graph.getSelectedCells();
if (cells.length) {
graph.cut(cells);
graph.cleanClipboard();
}
return false;
});
// 保存数据
graph.bindKey(['meta+p', 'ctrl+p'], () => {
localStorage.setItem('grphData', JSON.stringify(graph.toJSON()));
return false;
});
// undo redo
graph.bindKey(['meta+z', 'ctrl+z'], () => {
if (graph.history.canUndo()) {
graph.history.undo();
}
return false;
});
graph.bindKey(['meta+shift+z', 'ctrl+shift+z'], () => {
if (graph.history.canRedo()) {
graph.history.redo();
}
return false;
});
// select all
graph.bindKey(['ctrl+a', 'meta+a'], () => {
const nodes = graph.getNodes();
if (nodes) {
graph.select(nodes);
}
});
// delete
graph.bindKey('backspace', () => {
const cells = graph.getSelectedCells();
if (cells.length) {
graph.removeCells(cells);
// proxy.$eBus.emit('close:right-panel');
}
});
// zoom 放大
graph.bindKey(['ctrl+1', 'meta+1'], () => {
zoom.value = graph.zoom();
if (zoom.value < 2) {
graph.zoom(0.1);
}
});
// 缩小
graph.bindKey(['ctrl+2', 'meta+2'], () => {
zoom.value = graph.zoom();
if (zoom.value > 0.5) {
graph.zoom(-0.1);
}
});
// 聚焦
graph.bindKey(['ctrl+3', 'meta+3'], () => {
graph.centerContent();
});
}
function centerContent() {
graph.value.centerContent();
}
// 异步渲染数据
const graphData = ref(JSON.parse(props.flowData?.jsonData || '{}'));
// 初始化形状
function initGraphShape() {
if (graph.value) {
graph.value.fromJSON([]);
graph.value.fromJSON(graphData.value);
centerContent();
}
}
// 初始化
function init() {
Graph.unregisterRouter('custom');
registerRouter();
if (!graph.value) {
initGraph();
}
if (graphData.value) {
initGraphShape();
}
}
function toolHandelClick(name) {
emits('toolHandelClick', { name, graph: graph.value });
}
function closeDialog() {
showModal.value = false;
}
watchEffect(() => {
graphData.value = JSON.parse(props.flowData?.jsonData || '{}');
initGraphShape();
});
onMounted(async () => {
nextTick(() => {
init();
});
});
</script>
<script>
export default { name: 'CommonX6Flow' };
</script>
<style scoped lang="scss">
.subflow-logic {
padding: 0px 26px;
display: flex;
justify-content: space-between;
width: 100%;
height: 100%;
overflow-y: hidden;
.logic-l {
width: 260px;
height: 100%;
background: #fff;
box-sizing: border-box;
// padding: 0 10px;
border-right: 1px solid rgba(0, 0, 0, 0.1);
}
.logic-r {
width: calc(100% - 260px);
.flow-content {
position: relative;
height: calc(100vh - 100px);
width: calc(100vw - 260px);
.tool-control {
position: absolute;
top: 20px;
left: 10px;
z-index: 100;
display: flex;
align-items: center;
.zoom {
width: 50px;
text-align: center;
font-size: 14px;
color: rgba(0, 0, 0, 0.85);
font-weight: 500;
}
.icon {
margin: 0 5px;
cursor: pointer;
}
}
}
}
}
</style>
|