AIoT前端公共UI

Index.vue 15KB

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