vueFlow.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748
  1. <template >
  2. <div class="vueFlow1">
  3. <div id="container"></div>
  4. <div id="stencil"></div>
  5. <div id="graph-container"></div>
  6. <div id="minimap"></div>
  7. </div>
  8. <el-dialog title="节点信息配置" v-model="dialogVisible" width="500" v-if="dialogVisible">
  9. <byForm :formConfig="formConfig" :formOption="formOption" v-model="formData.data" v-loading="loading" :rules="rules" ref="byform">
  10. </byForm>
  11. <template #footer>
  12. <div>
  13. <el-button @click="dialogVisible = false" size="default">取 消</el-button>
  14. <el-button type="danger" @click="deleteFlowDefinitionNodeObj()" size="default">
  15. 删 除
  16. </el-button>
  17. <el-button type="primary" @click="submitForm" size="default">
  18. 确 定
  19. </el-button>
  20. </div>
  21. </template>
  22. </el-dialog>
  23. </template>
  24. <script lang="ts" setup>
  25. import {
  26. defineComponent,
  27. ref,
  28. onMounted,
  29. onUnmounted,
  30. watch,
  31. reactive,
  32. toRefs,
  33. computed,
  34. nextTick,
  35. getCurrentInstance,
  36. onDeactivated,
  37. onActivated,
  38. } from "vue";
  39. import byForm from "@/components/byForm/index";
  40. import { Graph, Shape } from "@antv/x6";
  41. import { Stencil } from "@antv/x6-plugin-stencil";
  42. import { Transform } from "@antv/x6-plugin-transform";
  43. import { Selection } from "@antv/x6-plugin-selection";
  44. import { Snapline } from "@antv/x6-plugin-snapline";
  45. import { Keyboard } from "@antv/x6-plugin-keyboard";
  46. import { Clipboard } from "@antv/x6-plugin-clipboard";
  47. import { register } from "@antv/x6-vue-shape";
  48. import { History } from "@antv/x6-plugin-history";
  49. import Cookies from "js-cookie";
  50. import { ElMessage, ElMessageBox } from "element-plus";
  51. import startBtn from "./startBtn.vue";
  52. import endBtn from "./endBtn.vue";
  53. import handleBtn from "./handleBtn.vue";
  54. import branchBtn from "./branchBtn.vue";
  55. import { MiniMap } from "@antv/x6-plugin-minimap";
  56. import useTagsViewStore from "@/store/modules/tagsView";
  57. import { rectToBox } from "@vue-flow/core/dist/utils/graph";
  58. import { async } from "@antv/x6/lib/registry/marker/main";
  59. defineProps({
  60. title: {
  61. type: Object,
  62. default: "",
  63. },
  64. nodeObject: {
  65. type: String,
  66. default: "",
  67. },
  68. });
  69. const { proxy } = getCurrentInstance();
  70. const flowNodeObj = ref({});
  71. const dialogVisible = ref(false);
  72. const loading = ref(false);
  73. const byform = ref(null);
  74. const formData = reactive({
  75. data: {},
  76. });
  77. const formOption = reactive({
  78. inline: true,
  79. labelWidth: 100,
  80. itemWidth: 100,
  81. });
  82. const formConfig = computed(() => {
  83. return [
  84. {
  85. type: "input",
  86. prop: "nodeName",
  87. label: "工序名称",
  88. required: true,
  89. itemType: "text",
  90. },
  91. ];
  92. });
  93. const rules = reactive({
  94. nodeName: [
  95. {
  96. required: true,
  97. message: "请输入工序名称",
  98. trigger: "blur",
  99. },
  100. ],
  101. });
  102. const flowDefinitionNodeObj = ref({});
  103. let graph;
  104. const submitFormData = {
  105. flowInfoId: null,
  106. titleTemplate: null,
  107. tenantId: Cookies.get("tenantId"),
  108. nodeObject: "",
  109. lineObject: "",
  110. flowDefinitionNodeList: [],
  111. };
  112. const deleteFlowDefinitionNodeObj = () => {
  113. graph.removeNode(formData.data.id);
  114. delete flowNodeObj.value[formData.data.id];
  115. dialogVisible.value = false;
  116. // let productionId = formData.data.cell.store.data.productionId;
  117. // type 1为自定义工序 0为固定工序
  118. // proxy.$emit("removeRow", { id: productionId });
  119. };
  120. const submitForm = () => {
  121. byform.value.handleSubmit((valid) => {
  122. console.log(formData.data.nodeName, "sada");
  123. formData.data.cell.setData({
  124. title: formData.data.nodeName,
  125. });
  126. flowNodeObj.value[formData.data.id].nodeName = formData.data.nodeName;
  127. dialogVisible.value = false;
  128. // let cell = formData.data.cell;
  129. // let productionId = cell.store.data.productionId;
  130. // let name = cell.store.data.data.title;
  131. // proxy.$emit("changeName", { productionId, name });
  132. });
  133. };
  134. const submitAll = () => {
  135. const nodeList = graph.toJSON().cells;
  136. submitFormData.nodeObject = JSON.stringify(nodeList);
  137. // submitFormData.lineObject = JSON.stringify(flowDefinitionNodeObj.value);
  138. console.log(nodeList, "asda");
  139. const arr = nodeList
  140. .filter((x) => x.shape != "edge")
  141. .filter((y) => y.productionId);
  142. console.log(arr, "bbb");
  143. const newArr = arr.map((x) => x.productionId);
  144. if (new Set(newArr).size != newArr.length) {
  145. ElMessage({
  146. message: "工序不可重复",
  147. type: "info",
  148. });
  149. return false;
  150. }
  151. return {
  152. nodeObject: submitFormData.nodeObject,
  153. };
  154. };
  155. //选取一个随机不重复的正整数id
  156. const randomId = () => {
  157. const id = Math.floor(Math.random() * 100000000000000000);
  158. if (flowDefinitionNodeObj.value[id]) {
  159. randomId();
  160. } else {
  161. return id;
  162. }
  163. };
  164. const addVersion = () => {
  165. const idObg = {};
  166. for (let i = 0; i < submitFormData.flowDefinitionNodeList.length; i++) {
  167. const element = submitFormData.flowDefinitionNodeList[i];
  168. if (element.parentId == null && element.nodeName == "结束") {
  169. ElMessage({
  170. message: "有结束节点未连线,请配置",
  171. type: "warning",
  172. });
  173. return;
  174. }
  175. if (isNaN(element.id)) {
  176. if (idObg[element.id]) {
  177. element.id = idObg[element.id];
  178. } else {
  179. const id = randomId();
  180. idObg[element.id] = id;
  181. element.id = id;
  182. }
  183. }
  184. if (isNaN(element.parentId) && element.nodeName != "开始") {
  185. if (idObg[element.parentId]) {
  186. element.parentId = idObg[element.parentId];
  187. } else {
  188. const id = randomId();
  189. idObg[element.parentId] = id;
  190. element.parentId = id;
  191. }
  192. }
  193. //nodeButtonSet转成字符串类型,用逗号隔开
  194. if (element.nodeButtonSet) {
  195. element.nodeButtonSet = element.nodeButtonSet.join(",");
  196. }
  197. }
  198. proxy.post("/flowDefinition/addVersion", submitFormData).then((res) => {
  199. ElMessage({
  200. message: "保存成功",
  201. type: "success",
  202. });
  203. useTagsViewStore().delView(router.currentRoute.value);
  204. history.go(-1);
  205. });
  206. };
  207. const pushRoom = (port: any) => {
  208. // if (port.node.shape == "end-btn") {
  209. // flowDefinitionNodeObj.value[port.node.id] = {
  210. // nodeName: "结束",
  211. // nodeType: 99,
  212. // id: port.id,
  213. // nodeButtonSet: "",
  214. // parentId: null,
  215. // };
  216. // }
  217. if (port.node.shape == "handle-btn") {
  218. let nodeName = "";
  219. if (
  220. port.node.store &&
  221. port.node.store.data &&
  222. port.node.store.data.data &&
  223. port.node.store.data.data.title
  224. ) {
  225. nodeName = port.node.store.data.data.title;
  226. }
  227. flowNodeObj.value[port.node.id] = {
  228. nodeName: nodeName,
  229. id: port.node.id,
  230. };
  231. }
  232. };
  233. //用于存储流程定义节点数据
  234. const antvInit = async (data) => {
  235. graph = new Graph({
  236. height: 600,
  237. container: document.getElementById("graph-container")!,
  238. grid: true,
  239. panning: {
  240. enabled: true,
  241. eventTypes: "rightMouseDown",
  242. },
  243. onPortRendered: pushRoom,
  244. mousewheel: {
  245. enabled: true,
  246. zoomAtMousePosition: true,
  247. modifiers: "ctrl",
  248. minScale: 0.5,
  249. maxScale: 3,
  250. },
  251. connecting: {
  252. allowLoop: false,
  253. // router: 'manhattan',
  254. connector: {
  255. name: "rounded",
  256. args: {
  257. radius: 8,
  258. },
  259. },
  260. anchor: "center",
  261. connectionPoint: "anchor",
  262. allowBlank: false,
  263. snap: {
  264. radius: 20,
  265. },
  266. createEdge() {
  267. return new Shape.Edge({
  268. attrs: {
  269. line: {
  270. stroke: "#A2B1C3",
  271. strokeWidth: 2,
  272. targetMarker: {
  273. name: "block",
  274. width: 12,
  275. height: 8,
  276. },
  277. },
  278. },
  279. zIndex: 0,
  280. });
  281. },
  282. validateConnection({ targetMagnet }) {
  283. return !!targetMagnet;
  284. },
  285. },
  286. highlighting: {
  287. magnetAdsorbed: {
  288. name: "stroke",
  289. args: {
  290. attrs: {
  291. fill: "#5F95FF",
  292. stroke: "#5F95FF",
  293. },
  294. },
  295. },
  296. },
  297. });
  298. // nextTick(() => {
  299. // graph.use(
  300. // new MiniMap({
  301. // container: document.getElementById("minimap"),
  302. // width: 200,
  303. // height: 160,
  304. // scalable: false,
  305. // })
  306. // );
  307. // });
  308. const stencil = new Stencil({
  309. title: "流程图",
  310. target: graph,
  311. stencilGraphWidth: 360,
  312. stencilGraphHeight: 280,
  313. collapsable: true,
  314. groups: [
  315. {
  316. title: "基础流程图",
  317. name: "group1",
  318. },
  319. ],
  320. layoutOptions: {
  321. columns: 1,
  322. columnWidth: 170,
  323. rowHeight: 100,
  324. },
  325. });
  326. document.getElementById("stencil")!.appendChild(stencil.container);
  327. // #region 使用插件
  328. graph
  329. .use(
  330. new Transform({
  331. resizing: true,
  332. rotating: true,
  333. })
  334. )
  335. .use(
  336. new Selection({
  337. enabled: true,
  338. rubberband: true,
  339. showNodeSelectionBox: true,
  340. })
  341. )
  342. .use(
  343. new Snapline({
  344. enabled: true,
  345. })
  346. )
  347. .use(
  348. new Keyboard({
  349. enabled: true,
  350. })
  351. )
  352. .use(
  353. new Clipboard({
  354. enabled: true,
  355. })
  356. )
  357. .use(
  358. new History({
  359. enabled: true,
  360. })
  361. );
  362. // 控制连接桩显示/隐藏
  363. const showPorts = (ports: NodeListOf<SVGElement>, show: boolean) => {
  364. for (let i = 0, len = ports.length; i < len; i += 1) {
  365. ports[i].style.visibility = show ? "visible" : "hidden";
  366. }
  367. };
  368. // 监听添加节点
  369. graph.on("node:added", ({ node }) => {});
  370. graph.on("node:mouseenter", () => {
  371. const container = document.getElementById("graph-container")!;
  372. const ports = container.querySelectorAll(
  373. ".x6-port-body"
  374. ) as NodeListOf<SVGElement>;
  375. showPorts(ports, true);
  376. });
  377. graph.on("node:mouseleave", () => {
  378. const container = document.getElementById("graph-container")!;
  379. const ports = container.querySelectorAll(
  380. ".x6-port-body"
  381. ) as NodeListOf<SVGElement>;
  382. showPorts(ports, false);
  383. });
  384. // #endregion
  385. graph.on("cell:click", ({ e, x, y, cell, view }) => {
  386. if (cell.shape === "start-btn") {
  387. return;
  388. }
  389. if (cell.shape === "end-btn") {
  390. return;
  391. }
  392. if (cell.shape === "handle-btn" || cell.shape === "edge") {
  393. // ElMessageBox.confirm("是否删除", "提示", {
  394. // confirmButtonText: "确定",
  395. // cancelButtonText: "取消",
  396. // type: "warning",
  397. // }).then(() => {
  398. // graph.removeNode(cell.id);
  399. // });
  400. // return;
  401. formData.data.id = cell.id;
  402. formData.data.cell = cell;
  403. console.log(cell, "sss");
  404. if (cell.store.data.data) {
  405. formData.data.nodeName = cell.store.data.data.title;
  406. } else if (cell.store.data) {
  407. formData.data.nodeName = cell.store.data.label;
  408. } else {
  409. formData.data.nodeName = "";
  410. }
  411. dialogVisible.value = true;
  412. }
  413. });
  414. // #region 初始化图形
  415. const ports = {
  416. groups: {
  417. top: {
  418. position: "top",
  419. attrs: {
  420. circle: {
  421. r: 4,
  422. magnet: true,
  423. stroke: "#5F95FF",
  424. strokeWidth: 1,
  425. fill: "#fff",
  426. style: {
  427. visibility: "hidden",
  428. },
  429. },
  430. },
  431. },
  432. right: {
  433. position: "right",
  434. attrs: {
  435. circle: {
  436. r: 4,
  437. magnet: true,
  438. stroke: "#5F95FF",
  439. strokeWidth: 1,
  440. fill: "#fff",
  441. style: {
  442. visibility: "hidden",
  443. },
  444. },
  445. },
  446. },
  447. bottom: {
  448. position: "bottom",
  449. attrs: {
  450. circle: {
  451. r: 4,
  452. magnet: true,
  453. stroke: "#5F95FF",
  454. strokeWidth: 1,
  455. fill: "#fff",
  456. style: {
  457. visibility: "hidden",
  458. },
  459. },
  460. },
  461. },
  462. left: {
  463. position: "left",
  464. attrs: {
  465. circle: {
  466. r: 4,
  467. magnet: true,
  468. stroke: "#5F95FF",
  469. strokeWidth: 1,
  470. fill: "#fff",
  471. style: {
  472. visibility: "hidden",
  473. },
  474. },
  475. },
  476. },
  477. },
  478. items: [
  479. {
  480. group: "top",
  481. },
  482. {
  483. group: "right",
  484. },
  485. {
  486. group: "bottom",
  487. },
  488. {
  489. group: "left",
  490. },
  491. ],
  492. };
  493. Graph.registerNode(
  494. "custom-rect",
  495. {
  496. inherit: "rect",
  497. width: 66,
  498. height: 36,
  499. attrs: {
  500. body: {
  501. strokeWidth: 1,
  502. stroke: "#5F95FF",
  503. fill: "#EFF4FF",
  504. },
  505. text: {
  506. fontSize: 12,
  507. fill: "#262626",
  508. },
  509. },
  510. ports: { ...ports },
  511. },
  512. true
  513. );
  514. register({
  515. shape: "start-btn",
  516. width: 150,
  517. height: 90,
  518. component: startBtn,
  519. effect: ["title"],
  520. ports: { ...ports },
  521. data: {
  522. title: 80,
  523. },
  524. });
  525. register({
  526. shape: "handle-btn",
  527. width: 150,
  528. height: 90,
  529. effect: ["title"],
  530. component: handleBtn,
  531. ports: { ...ports },
  532. });
  533. register({
  534. shape: "branch-btn",
  535. width: 150,
  536. height: 90,
  537. effect: ["title"],
  538. component: branchBtn,
  539. ports: { ...ports },
  540. });
  541. register({
  542. shape: "end-btn",
  543. width: 150,
  544. height: 90,
  545. effect: ["title"],
  546. component: endBtn,
  547. ports: { ...ports },
  548. });
  549. // const r1 = graph.createNode({
  550. // shape: 'start-btn',
  551. // label: '开始',
  552. // zIndex: 100,
  553. // attrs: {
  554. // body: {
  555. // rx: 20,
  556. // ry: 26,
  557. // },
  558. // },
  559. // data: {
  560. // title: 80,
  561. // },
  562. // })
  563. const r2 = graph.createNode({
  564. shape: "handle-btn",
  565. label: "办理",
  566. zIndex: 100,
  567. attrs: {
  568. body: {
  569. rx: 40,
  570. ry: 46,
  571. },
  572. },
  573. });
  574. const r3 = graph.createNode({
  575. shape: "branch-btn",
  576. label: "分支",
  577. zIndex: 100,
  578. attrs: {
  579. body: {
  580. rx: 40,
  581. ry: 46,
  582. },
  583. },
  584. });
  585. const r4 = graph.createNode({
  586. shape: "end-btn",
  587. label: "结束",
  588. zIndex: 100,
  589. attrs: {
  590. body: {
  591. rx: 20,
  592. ry: 26,
  593. },
  594. },
  595. });
  596. let arr = [];
  597. const resList = await proxy.post("/productionProcesses/page", {
  598. pageNum: 1,
  599. pageSize: 9999,
  600. });
  601. arr = resList.rows.map((x) => {
  602. return graph.createNode({
  603. shape: "handle-btn",
  604. label: x.name,
  605. productionId: x.id,
  606. zIndex: 100,
  607. attrs: {
  608. body: {
  609. rx: 20,
  610. ry: 26,
  611. },
  612. },
  613. });
  614. });
  615. stencil.load([...arr], "group1");
  616. if (data) {
  617. graph.fromJSON(data);
  618. } else {
  619. graph.addNode({
  620. shape: "start-btn",
  621. x: 300,
  622. y: 20,
  623. label: "开始",
  624. id: 1,
  625. attrs: {},
  626. });
  627. graph.addNode({
  628. shape: "end-btn",
  629. x: 300,
  630. y: 300,
  631. label: "结束",
  632. id: 99,
  633. attrs: {},
  634. });
  635. }
  636. };
  637. const getFlowInfo = (data) => {
  638. if (data) {
  639. antvInit(data);
  640. } else {
  641. antvInit();
  642. }
  643. };
  644. onActivated(() => {});
  645. onDeactivated(() => {
  646. console.log(window.document.getElementById("minimap").children);
  647. if (window.document.getElementById("minimap").children.length > 1) {
  648. window.document.getElementById("minimap").children[0].remove();
  649. }
  650. });
  651. onMounted(() => {
  652. if (proxy.nodeObject) {
  653. let data = JSON.parse(proxy.nodeObject);
  654. getFlowInfo(data);
  655. } else {
  656. getFlowInfo();
  657. }
  658. setTimeout(() => {
  659. if (window.document.getElementById("minimap").children.length > 1) {
  660. window.document.getElementById("minimap").children[0].remove();
  661. }
  662. }, 500);
  663. });
  664. defineExpose({
  665. submitAll,
  666. });
  667. </script>
  668. <style lang="scss" scope>
  669. #minimap .x6-widget-minimap {
  670. border: 1px solid #dcdcdc;
  671. }
  672. .x6-widget-stencil-group-title {
  673. display: none !important;
  674. }
  675. .x6-widget-stencil-title {
  676. display: none;
  677. }
  678. .x6-widget-stencil-content {
  679. top: 0 !important;
  680. &::-webkit-scrollbar {
  681. width: 2px !important;
  682. height: 2px !important;
  683. }
  684. }
  685. .vueFlow1 {
  686. // position: relative;
  687. display: flex;
  688. justify-content: space-between;
  689. overflow: hidden;
  690. height: 600px;
  691. .x6-graph {
  692. width: 100% !important;
  693. }
  694. #stencil {
  695. // position: fixed;
  696. position: absolute;
  697. top: 50px;
  698. left: 30px;
  699. z-index: 100;
  700. width: 190px;
  701. height: 600px;
  702. background: #fff;
  703. overflow: hidden;
  704. // background: #fff;
  705. border-radius: 10px;
  706. // border: 1px solid #eee;
  707. }
  708. #container {
  709. }
  710. #graph-container {
  711. width: 100%;
  712. // width: 100%;
  713. // position: absolute;
  714. // right: 0;
  715. // top: 0;
  716. }
  717. }
  718. #stencil .x6-widget-stencil-content .x6-widget-stencil-group-content .x6-graph {
  719. height: 900px !important;
  720. }
  721. </style>