index.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846
  1. <template>
  2. <div class="chart">
  3. <div class="chart-block">
  4. <div class="chart-ref" ref="ref_chart"></div>
  5. </div>
  6. <div class="tools">
  7. <div class="mini-map" ref="ref_miniMap" />
  8. <div class="operations">
  9. <div class="zoom">
  10. <el-tooltip
  11. content="缩小"
  12. effect="light"
  13. placement="top"
  14. :show-arrow="false"
  15. >
  16. <div
  17. class="__hover-bg"
  18. :class="{ __disabled: state.zoom <= 0.25 }"
  19. @click="graphZoom(state.zoom - 0.1)"
  20. >
  21. <SvgIcon name="zoom-" />
  22. </div>
  23. </el-tooltip>
  24. <el-dropdown
  25. :teleported="false"
  26. placement="top"
  27. :popper-options="{
  28. modifiers: [
  29. {
  30. name: 'offset',
  31. options: {
  32. offset: [0, 20],
  33. },
  34. },
  35. {
  36. name: 'arrow',
  37. },
  38. ],
  39. }"
  40. >
  41. <div class="__hover-bg px-0.5">
  42. {{ (state.zoom * 100).toFixed(0) }}%
  43. </div>
  44. <template #dropdown>
  45. <el-dropdown-menu>
  46. <el-dropdown-item @click="graphZoom(2)">200%</el-dropdown-item>
  47. <el-dropdown-item @click="graphZoom(1)">100%</el-dropdown-item>
  48. <el-dropdown-item @click="graphZoom(0.75)"
  49. >75%</el-dropdown-item
  50. >
  51. <el-dropdown-item @click="graphZoom(0.5)">50%</el-dropdown-item>
  52. <el-dropdown-item divided @click="graphZoom(0)"
  53. >自适应视图</el-dropdown-item
  54. >
  55. </el-dropdown-menu>
  56. </template>
  57. </el-dropdown>
  58. <el-tooltip
  59. content="放大"
  60. effect="light"
  61. placement="top"
  62. :show-arrow="false"
  63. >
  64. <div
  65. class="__hover-bg"
  66. :class="{ __disabled: state.zoom >= 2 }"
  67. @click="graphZoom(state.zoom + 0.1)"
  68. >
  69. <SvgIcon name="zoom+" />
  70. </div>
  71. </el-tooltip>
  72. </div>
  73. <div>
  74. <el-tooltip
  75. content="撤销"
  76. effect="light"
  77. placement="top"
  78. :show-arrow="false"
  79. >
  80. <div
  81. class="__hover-bg"
  82. :class="{ __disabled: !state.history.canUndo }"
  83. @click="onHistoryDo(-1)"
  84. >
  85. <SvgIcon name="back" />
  86. </div>
  87. </el-tooltip>
  88. <el-tooltip
  89. content="重做"
  90. effect="light"
  91. placement="top"
  92. :show-arrow="false"
  93. >
  94. <div
  95. class="__hover-bg"
  96. :class="{ __disabled: !state.history.canRedo }"
  97. @click="onHistoryDo(1)"
  98. >
  99. <SvgIcon name="back" rotate="180" />
  100. </div>
  101. </el-tooltip>
  102. <el-popover
  103. :show-arrow="false"
  104. :width="240"
  105. :popper-style="{
  106. padding: 0,
  107. }"
  108. >
  109. <template #reference>
  110. <div class="__hover-bg">
  111. <SvgIcon name="history" />
  112. </div>
  113. </template>
  114. <div class="">
  115. <div class="p-3 pb-1 text-[18px]">变更历史</div>
  116. <div class="px-1">
  117. <template v-if="state.history.steps.length < 2">
  118. <div
  119. class="mt-2 flex h-[120px] flex-col items-center justify-center"
  120. >
  121. <SvgIcon name="history" size="30" class="mb-3" />
  122. 尚未更改任何内容
  123. </div>
  124. </template>
  125. <template v-else>
  126. <div
  127. class="mt-2 flex max-h-[300px] flex-col gap-1 overflow-y-auto"
  128. >
  129. <template v-for="(item, index) in state.history.steps">
  130. <div
  131. :class="`__hover-bg rounded-md px-2 py-1.5 text-[13px] ${state.history.current === index && 'bg-[#c8ceda33]'}`"
  132. @click="onHistoryDo(state.history.current - index)"
  133. >
  134. {{ MapGraphHistoryStep.get(item) }}
  135. <template v-if="state.history.current < index"
  136. >{{ index - state.history.current }} 步后退</template
  137. >
  138. <template v-else-if="state.history.current > index"
  139. >{{ state.history.current - index }} 步前进</template
  140. >
  141. <template v-else>当前状态</template>
  142. )
  143. </div>
  144. </template>
  145. </div>
  146. <div class="my-2 h-[1px] w-full bg-[#eaecf0]" />
  147. <div
  148. class="__hover-bg mb-2 rounded-md px-2 py-1.5 text-[13px]"
  149. @click="onHistoryClear(state.history.steps[0])"
  150. >
  151. 清空历史记录
  152. </div>
  153. </template>
  154. </div>
  155. <div class="p-3 text-[12px] text-[#676f83]">
  156. 提示<br />
  157. 您的编辑操作将被跟踪并存储在您的设备上,直到您离开编辑器。此历史记录将在您离开编辑器时被清除。
  158. </div>
  159. </div>
  160. </el-popover>
  161. </div>
  162. <div>
  163. <el-popover
  164. :show-arrow="false"
  165. :width="100"
  166. :popper-style="{
  167. padding: 0,
  168. }"
  169. >
  170. <template #reference>
  171. <div class="__hover-bg">
  172. <SvgIcon name="add" />
  173. </div>
  174. </template>
  175. <div class="">
  176. <div class="p-3 pb-1 text-[18px]">新增节点</div>
  177. <div class="px-1">
  178. <div class="mt-2">
  179. <nodeAdd @onAddNode="onAddNode" ref="ref_dndContainer" />
  180. </div>
  181. <div class="my-2 h-[1px] w-full bg-[#eaecf0]" />
  182. </div>
  183. <div class="p-3 text-[12px] text-[#676f83]">
  184. 提示<br />
  185. 点击后拖拽至指定位置后,松开鼠标即可放置到画布上。
  186. </div>
  187. </div>
  188. </el-popover>
  189. <el-tooltip
  190. content="自动布局"
  191. effect="light"
  192. placement="top"
  193. :show-arrow="false"
  194. >
  195. <div class="__hover-bg" @click="onAutoFix">
  196. <SvgIcon name="mind-map" />
  197. </div>
  198. </el-tooltip>
  199. </div>
  200. </div>
  201. </div>
  202. </div>
  203. </template>
  204. <script setup lang="ts">
  205. import {
  206. createVNode,
  207. getCurrentInstance,
  208. nextTick,
  209. onMounted,
  210. reactive,
  211. ref,
  212. render,
  213. watch,
  214. } from 'vue'
  215. import { Graph, Path, Shape } from '@antv/x6'
  216. import {
  217. getNodeDefault,
  218. lineActiveStyle,
  219. lineStyle,
  220. } from '@/views/workflow/config'
  221. import nodePort from './node-port.vue'
  222. import { register } from '@antv/x6-vue-shape'
  223. import WorkflowNode from './node-index.vue'
  224. import { handleEdge, handleNode } from '@/views/workflow/handle'
  225. import { Snapline } from '@antv/x6-plugin-snapline'
  226. import { ContextMenuTool } from './context-menu-tool'
  227. import { useWorkflowStore } from '@/stores/modules/workflow'
  228. import {
  229. GraphHistoryStep,
  230. MapGraphHistoryStep,
  231. NodeType,
  232. } from '@/views/workflow/types'
  233. import { MiniMap } from '@antv/x6-plugin-minimap'
  234. import { History } from '@antv/x6-plugin-history'
  235. import { v4 } from 'uuid'
  236. import { Dnd } from '@antv/x6-plugin-dnd'
  237. import nodeAdd from './node-add.vue'
  238. import dagre from 'dagre'
  239. register({
  240. shape: 'workflow-node',
  241. component: WorkflowNode,
  242. })
  243. let edgeTimer: any = null
  244. Graph.registerPortLayout('start', (portsPositionArgs, elemBBox) => {
  245. return portsPositionArgs.map((_, index) => {
  246. return {
  247. position: {
  248. x: 0,
  249. y: 25,
  250. },
  251. }
  252. })
  253. })
  254. Graph.registerPortLayout('end', (portsPositionArgs, elemBBox) => {
  255. if (edgeTimer) {
  256. clearTimeout(edgeTimer)
  257. }
  258. edgeTimer = setTimeout(() => {
  259. initEdges()
  260. }, 100)
  261. return portsPositionArgs.map((_, index) => {
  262. return {
  263. position: {
  264. x: elemBBox.width,
  265. y: 25,
  266. },
  267. }
  268. })
  269. })
  270. Graph.registerPortLayout('more', (portsPositionArgs, elemBBox) => {
  271. return portsPositionArgs.map((_, index) => {
  272. nextTick(() => {
  273. WorkflowStore.layoutPort(_.nodeId, _.portId)
  274. if (edgeTimer) {
  275. clearTimeout(edgeTimer)
  276. }
  277. edgeTimer = setTimeout(() => {
  278. initEdges()
  279. }, 100)
  280. })
  281. return {
  282. position: {
  283. x: elemBBox.width,
  284. y: _.dy,
  285. },
  286. }
  287. })
  288. })
  289. Graph.registerConnector(
  290. 'algo-connector',
  291. (s, e) => {
  292. const offset = 6
  293. const deltaX = Math.abs(e.x - s.x)
  294. const control = Math.floor((deltaX / 3) * 2)
  295. const v1 = { x: s.x + offset + control, y: s.y }
  296. const v2 = { x: e.x - offset - control, y: e.y }
  297. return Path.normalize(
  298. `M ${s.x} ${s.y}
  299. L ${s.x} ${s.y}
  300. C ${v1.x} ${v1.y} ${v2.x} ${v2.y} ${e.x} ${e.y}
  301. L ${e.x} ${e.y}
  302. `,
  303. )
  304. },
  305. true,
  306. )
  307. Graph.registerNodeTool('contextmenu', ContextMenuTool, true)
  308. Graph.registerEdgeTool('contextmenu', ContextMenuTool, true)
  309. Graph.registerHighlighter(
  310. 'port-highlight',
  311. {
  312. highlight(cellView, magnet) {
  313. const dom = magnet.getElementsByClassName('node-port')[0]
  314. dom?.classList.add('highlight')
  315. },
  316. unhighlight(cellView, magnet) {
  317. const dom = magnet.getElementsByClassName('node-port')[0]
  318. dom?.classList.remove('highlight')
  319. },
  320. },
  321. true,
  322. )
  323. Graph.registerHighlighter(
  324. 'port-select',
  325. {
  326. highlight(cellView, magnet) {
  327. const dom = magnet.getElementsByClassName('node-port')[0]
  328. dom?.classList.add('select')
  329. },
  330. unhighlight(cellView, magnet) {
  331. const dom = magnet.getElementsByClassName('node-port')[0]
  332. dom?.classList.remove('select')
  333. },
  334. },
  335. true,
  336. )
  337. const WorkflowStore = useWorkflowStore()
  338. const emit = defineEmits(['save'])
  339. const props = defineProps({
  340. data: <any>{},
  341. ID: <any>{},
  342. })
  343. const { proxy }: any = getCurrentInstance()
  344. const state: any = reactive({
  345. graph: null,
  346. isPort: false,
  347. isInitEdges: false,
  348. zoom: 1,
  349. history: {
  350. canUndo: false,
  351. canRedo: false,
  352. steps: [],
  353. current: 0,
  354. autoSaveInit: false,
  355. },
  356. dnd: null,
  357. dndNode: null,
  358. })
  359. const ref_chart = ref()
  360. const ref_miniMap = ref()
  361. const ref_dndContainer = ref()
  362. const initChart = async () => {
  363. state.graph = new Graph({
  364. container: ref_chart.value,
  365. autoResize: true,
  366. panning: true,
  367. mousewheel: {
  368. enabled: true,
  369. factor: 1.05,
  370. },
  371. scaling: {
  372. min: 0.25,
  373. max: 2,
  374. },
  375. grid: {
  376. visible: true,
  377. type: 'doubleMesh',
  378. args: [
  379. {
  380. color: 'rgba(238,238,238,0.3)', // 主网格线颜色
  381. thickness: 1, // 主网格线宽度
  382. },
  383. {
  384. color: 'rgba(221,221,221,0.3)', // 次网格线颜色
  385. thickness: 1, // 次网格线宽度
  386. factor: 2, // 主次网格线间隔
  387. },
  388. ],
  389. },
  390. highlighting: {
  391. // 连接桩可以被连接时在连接桩外围围渲染一个包围框
  392. magnetAvailable: {
  393. name: 'port-highlight',
  394. },
  395. // 连接桩吸附连线时在连接桩外围围渲染一个包围框
  396. magnetAdsorbed: {
  397. name: 'port-select',
  398. },
  399. },
  400. connecting: {
  401. snap: {
  402. // 连线过程中自动吸附
  403. radius: 20,
  404. },
  405. allowBlank: false, // 是否允许连接到画布空白位置的点
  406. allowLoop: false, // 是否允许创建循环连线,即边的起始节点和终止节点为同一节点
  407. allowNode: false, // 是否允许边连接到节点
  408. allowEdge: false, // 是否允许边链接到另一个边
  409. allowPort: (args: any) => {
  410. // 是否允许边链接到连接桩
  411. // console.log(args)
  412. if (args.sourcePort?.includes('start')) {
  413. return false
  414. }
  415. if (!args.targetPort?.includes('start')) {
  416. return false
  417. }
  418. // if (state.graph.isSuccessor(args.sourceCell, args.targetCell)) {
  419. // return false
  420. // }
  421. // if (state.graph.getPredecessors(args.sourceCell)[0] && state.graph.getPredecessors(args.targetCell)[0] && state.graph.getPredecessors(args.sourceCell)[0] === state.graph.getPredecessors(args.targetCell)[0]) {
  422. // return false
  423. // }
  424. return true
  425. },
  426. allowMulti: 'withPort', // 当设置为 'withPort' 时,在起始和终止节点的相同连接桩之间只允许创建一条边(即,起始和终止节点之间可以创建多条边,但必须要要链接在不同的连接桩上)
  427. highlight: true,
  428. // router: {
  429. // name: 'manhattan',
  430. // args: {
  431. // startDirections: ['right'],
  432. // endDirections: ['left'],
  433. // },
  434. // },
  435. connector: 'algo-connector',
  436. connectionPoint: 'anchor',
  437. anchor: 'center',
  438. // connector: {
  439. // name: 'rounded',
  440. // args: {
  441. // radius: 20,
  442. // },
  443. // },
  444. createEdge: (args) => {
  445. const id = v4()
  446. return new Shape.Edge({
  447. id,
  448. attrs: {
  449. select: false,
  450. hover: false,
  451. line: lineStyle,
  452. },
  453. zIndex: -1,
  454. tools: [
  455. {
  456. name: 'contextmenu',
  457. args: {
  458. delFlag: GraphHistoryStep.EdgeDel,
  459. data: { id },
  460. },
  461. },
  462. ],
  463. })
  464. },
  465. },
  466. onPortRendered: (args: any) => {
  467. const selectors = args.contentSelectors
  468. const container = selectors && selectors.foContent
  469. if (container) {
  470. render(
  471. createVNode(nodePort, {
  472. port: args.port,
  473. node: args.node,
  474. graph: state.graph,
  475. }),
  476. container,
  477. )
  478. }
  479. },
  480. })
  481. await WorkflowStore.init(state.graph)
  482. initPlug()
  483. initWatch()
  484. initNodes()
  485. if (props.data.viewport?.zoom) {
  486. graphZoom(props.data.viewport.zoom)
  487. state.graph.translate(props.data.viewport.x, props.data.viewport.y)
  488. } else {
  489. graphZoom(0)
  490. emit('save')
  491. }
  492. }
  493. const initNodes = () => {
  494. props.data.nodes.forEach((v) => {
  495. state.graph.addNode(handleNode(v))
  496. })
  497. if (props.data.nodes.length === 0) {
  498. const node = getNodeDefault(NodeType.Root)
  499. state.graph.addNode(handleNode(node))
  500. }
  501. }
  502. const initEdges = () => {
  503. if (!state.isInitEdges) {
  504. props.data.edges.forEach((v) => {
  505. const targetNode = state.graph.getCellById(v.target)
  506. targetNode.setData(
  507. {
  508. edgeSource: v.port || v.source,
  509. },
  510. { deep: false },
  511. )
  512. state.graph.addEdge(handleEdge(v))
  513. })
  514. state.isInitEdges = true
  515. onHistoryClear()
  516. }
  517. }
  518. const initWatch = () => {
  519. state.graph.on('blank:click', () => {
  520. state.graph.getCells().forEach((v) => {
  521. v.attr('select', false)
  522. if (v.shape === 'edge') {
  523. setEdgeStyle(v)
  524. }
  525. })
  526. })
  527. state.graph.on('cell:click', () => {
  528. state.graph.getCells().forEach((v) => {
  529. v.attr('select', false)
  530. if (v.shape === 'edge') {
  531. setEdgeStyle(v)
  532. }
  533. })
  534. })
  535. state.graph.on('node:click', ({ node }) => {
  536. setTimeout(() => {
  537. if (!state.isPort) {
  538. WorkflowStore.nodePanelShow(node)
  539. // state.graph.getCells().forEach(v => v.attr('select', false))
  540. node.attr('select', true)
  541. const connectEdges = state.graph.getConnectedEdges(node.id)
  542. connectEdges.forEach((edge) => {
  543. edge.attr('select', true)
  544. setEdgeStyle(edge)
  545. })
  546. }
  547. }, 50)
  548. })
  549. state.graph.on('edge:click', ({ edge }) => {
  550. edge.attr('select', true)
  551. const connectNodes = state.graph.getNeighbors(edge)
  552. connectNodes.forEach((node) => {
  553. node.attr('select', true)
  554. })
  555. })
  556. state.graph.on('node:port:click', () => {
  557. state.isPort = true
  558. setTimeout(() => {
  559. state.isPort = false
  560. }, 100)
  561. })
  562. state.graph.on('node:mouseenter', ({ node }) => {
  563. node.attr('hover', true)
  564. const connectEdges = state.graph.getConnectedEdges(node.id)
  565. connectEdges.forEach((edge) => {
  566. edge.attr('hover', true)
  567. setEdgeStyle(edge)
  568. })
  569. })
  570. state.graph.on('node:mouseleave', ({ node }) => {
  571. node.attr('hover', false)
  572. const connectEdges = state.graph.getConnectedEdges(node.id)
  573. connectEdges.forEach((edge) => {
  574. edge.attr('hover', false)
  575. setEdgeStyle(edge)
  576. })
  577. })
  578. state.graph.on('edge:mouseenter', ({ edge }) => {
  579. edge.attr('hover', true)
  580. setEdgeStyle(edge)
  581. const connectNodes = state.graph.getNeighbors(edge)
  582. connectNodes.forEach((node) => {
  583. node.attr('hover', true)
  584. })
  585. })
  586. state.graph.on('edge:mouseleave', ({ edge }) => {
  587. edge.attr('hover', false)
  588. setEdgeStyle(edge)
  589. const connectNodes = state.graph.getNeighbors(edge)
  590. connectNodes.forEach((node) => {
  591. node.attr('hover', false)
  592. })
  593. })
  594. state.graph.on('scale', ({ sx, sy, ox, oy }) => {
  595. state.zoom = sx
  596. })
  597. state.graph.on('translate', ({ sx, sy, ox, oy }) => {
  598. if (state.autoSaveInit) {
  599. WorkflowStore.autoSave()
  600. }
  601. })
  602. state.graph.on('history:change', ({ cmds, options }) => {
  603. if (state.isInitEdges) {
  604. state.history.canRedo = state.graph.canRedo()
  605. state.history.canUndo = state.graph.canUndo()
  606. if (options.name) {
  607. const arr = [options.name]
  608. state.history.steps.forEach((v, i) => {
  609. if (i >= state.history.current) {
  610. arr.push(v)
  611. }
  612. })
  613. state.history.steps = arr
  614. state.history.current = 0
  615. }
  616. if (options.name === GraphHistoryStep.Dnd) {
  617. const nodeId = cmds[0].data.id
  618. state.graph
  619. .getCellById(nodeId)
  620. .data.workflowData.ports?.forEach((p) => {
  621. WorkflowStore.layoutPort(nodeId, p.id)
  622. })
  623. }
  624. if (state.autoSaveInit) {
  625. WorkflowStore.autoSave()
  626. } else {
  627. state.autoSaveInit = true
  628. }
  629. }
  630. })
  631. }
  632. const initPlug = () => {
  633. state.graph.use(
  634. new Snapline({
  635. enabled: true,
  636. clean: false,
  637. }),
  638. )
  639. state.graph.use(
  640. new MiniMap({
  641. container: ref_miniMap.value,
  642. width: 100,
  643. height: 70,
  644. padding: 6,
  645. }),
  646. )
  647. state.graph.use(
  648. new History({
  649. enabled: true,
  650. beforeAddCommand: (event, args: any) => {
  651. if (
  652. (args.key === 'ports' &&
  653. args.options.propertyPath.includes('ports/items')) ||
  654. args.key === 'attrs'
  655. ) {
  656. return false
  657. }
  658. },
  659. }),
  660. )
  661. state.dnd = new Dnd({
  662. target: state.graph,
  663. getDragNode: (node: any) => {
  664. node.size(WorkflowStore.nodeSize[node.data.workflowData.type])
  665. return node
  666. },
  667. getDropNode: (node) => node.clone({ keepId: true }),
  668. })
  669. }
  670. const setEdgeStyle = (edge) =>
  671. edge.attr(
  672. 'line',
  673. edge.getAttrByPath('hover') || edge.getAttrByPath('select')
  674. ? lineActiveStyle
  675. : lineStyle,
  676. )
  677. const graphZoom = (z) => {
  678. if (z) {
  679. state.graph.zoomTo(z)
  680. } else {
  681. state.graph.zoomToFit({ maxScale: 1 })
  682. }
  683. }
  684. const onHistoryDo = (step) => {
  685. if (step > 0) {
  686. for (let i = 1; i <= step; i++) {
  687. if (state.history.canRedo) {
  688. state.graph.redo()
  689. state.history.current -= 1
  690. }
  691. }
  692. } else {
  693. for (let i = -1; i >= step; i--) {
  694. if (state.history.canUndo) {
  695. state.graph.undo()
  696. state.history.current += 1
  697. }
  698. }
  699. }
  700. }
  701. const onHistoryClear = (first = '') => {
  702. state.graph.cleanHistory() // 初始化完成后清空历史队列
  703. state.history.steps = [first || GraphHistoryStep.Root]
  704. state.history.current = 0
  705. }
  706. const onAddNode = ({ type, e }) => {
  707. const node = state.graph.createNode(handleNode(getNodeDefault(type)))
  708. state.dnd.start(node, e)
  709. }
  710. const onAutoFix = () => {
  711. state.graph.startBatch(GraphHistoryStep.AutoFix)
  712. const xLevelsMap = new Map()
  713. const nodeSep = 20
  714. // 布局方向
  715. const dir = 'LR' // LR RL TB BT
  716. const nodes = state.graph.getNodes()
  717. const edges = state.graph.getEdges()
  718. const g = new dagre.graphlib.Graph()
  719. g.setGraph({
  720. rankdir: dir,
  721. nodesep: nodeSep,
  722. ranksep: 100,
  723. })
  724. g.setDefaultEdgeLabel(() => ({}))
  725. nodes.forEach((node) => {
  726. g.setNode(node.id, node.size())
  727. })
  728. edges.forEach((edge) => {
  729. const source = edge.getSource()
  730. const target = edge.getTarget()
  731. g.setEdge(source.cell, target.cell)
  732. })
  733. dagre.layout(g)
  734. g.nodes().forEach((id) => {
  735. const node = state.graph.getCellById(id) as Node
  736. if (node) {
  737. const pos = g.node(id)
  738. node.position(pos.x, pos.y)
  739. // 自动布局后针对某些节点高度过大,导致挡住其他节点的二次手动排序
  740. if (xLevelsMap.has(pos.x)) {
  741. xLevelsMap.set(pos.x, [...xLevelsMap.get(pos.x), { ...pos, id }])
  742. } else {
  743. xLevelsMap.set(pos.x, [{ ...pos, id }])
  744. }
  745. }
  746. })
  747. xLevelsMap.forEach((value, key, map) => {
  748. const arr = value.sort((a, b) => a.y - b.y)
  749. arr.forEach((n, i) => {
  750. if (i > 0) {
  751. const last = arr[i - 1]
  752. if (n.y < last.y + last.height) {
  753. n.y = nodeSep + last.y + last.height
  754. const node = state.graph.getCellById(n.id) as Node
  755. node.position(n.x, n.y)
  756. }
  757. }
  758. })
  759. })
  760. setTimeout(() => {
  761. state.graph.stopBatch(GraphHistoryStep.AutoFix)
  762. }, 100)
  763. graphZoom(0)
  764. }
  765. watch(
  766. () => props.data,
  767. (n) => {
  768. if (n) {
  769. initChart()
  770. }
  771. },
  772. { immediate: true },
  773. )
  774. onMounted(() => {})
  775. defineExpose({
  776. toJSON: () => state.graph.toJSON(),
  777. })
  778. </script>
  779. <style lang="scss" scoped>
  780. .chart {
  781. width: 100%;
  782. height: 100%;
  783. background-color: #f2f4f7;
  784. position: relative;
  785. .chart-block {
  786. width: 100%;
  787. height: 100%;
  788. .chart-ref {
  789. width: 100%;
  790. height: 100%;
  791. }
  792. }
  793. .tools {
  794. position: absolute;
  795. left: 20px;
  796. bottom: 20px;
  797. z-index: 2;
  798. .mini-map {
  799. position: absolute;
  800. bottom: calc(100% + 10px);
  801. }
  802. :deep(.operations) {
  803. display: flex;
  804. align-items: center;
  805. font-size: 14px;
  806. color: #676f83;
  807. gap: 10px;
  808. .el-popper__arrow {
  809. display: none;
  810. }
  811. > div {
  812. display: flex;
  813. align-items: center;
  814. background-color: rgba(255, 255, 255, 0.95);
  815. box-shadow: 0 0px 8px rgba(0, 0, 0, 0.1);
  816. border-radius: 6px;
  817. padding: 4px;
  818. > div {
  819. min-width: 32px;
  820. height: 32px;
  821. display: flex;
  822. align-items: center;
  823. justify-content: center;
  824. }
  825. }
  826. }
  827. }
  828. }
  829. :deep(.port-start-high) {
  830. background-color: red;
  831. width: 10px;
  832. height: 10px;
  833. .port-start {
  834. background-color: red !important;
  835. }
  836. }
  837. </style>