index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689
  1. <template>
  2. <VueFlow ref="vueFlowRef" v-model:nodes="nodes" v-model:edges="edges" :class="{ dark }"
  3. class="basic-flow"
  4. :default-viewport="{ zoom: 1.5 }" :min-zoom="0.2" :max-zoom="2.5" @drop="customOnDrop"
  5. @node-contextmenu="onNodeContextMenu"
  6. @dragover="onDragOver" @dragleave="onDragLeave" @edge-click="onEdgeClick" @node-double-click="onNodeDoubleClick"
  7. @node-click="onNodeClick" @edge-double-click="onEdgeDoubleClick">
  8. <!-- 自定义节点类型为default的节点 -->
  9. <template #node-default="props">
  10. <defaultnode :node="props" />
  11. </template>
  12. <template #node-point-only="props">
  13. <PointOnlyNode :node="props" />
  14. </template>
  15. <Background pattern-color="#aaa" :gap="16"
  16. />
  17. <Controls position="top-left">
  18. <ControlButton title="重置" @click="resetTransform">
  19. <changebak name="reset" />
  20. </ControlButton>
  21. <ControlButton title="背景切换" @click="toggleDarkMode">
  22. <changebak v-if="dark" name="sun" />
  23. <changebak v-else name="moon" />
  24. </ControlButton>
  25. <ControlButton title="保存" @click="saveproject">
  26. <!-- <Icon name="log" /> -->
  27. <el-icon :color="iconcolor"><UploadFilled /></el-icon>
  28. </ControlButton>
  29. <ControlButton title="删除节点" @click="removeNode()">
  30. <el-icon :color="iconcolor"><DocumentDelete /></el-icon>
  31. </ControlButton>
  32. <ControlButton title="删除线" @click="removeEdge()">
  33. <el-icon :color="iconcolor"><Crop /></el-icon>
  34. </ControlButton>
  35. <ControlButton title="清空全部" @click="confirmDelete()">
  36. <el-icon :color="iconcolor"><DeleteFilled /></el-icon>
  37. </ControlButton>
  38. </Controls>
  39. </VueFlow>
  40. </template>
  41. <script setup>
  42. import { VueFlow,Panel, useVueFlow, MarkerType} from '@vue-flow/core'
  43. import { ElMessage, ElButton, ElDialog, ElSelect, ElMessageBox} from 'element-plus'
  44. import {
  45. DocumentDelete,
  46. Delete,
  47. UploadFilled,
  48. Histogram,
  49. DeleteFilled,
  50. Crop,
  51. } from '@element-plus/icons-vue'
  52. import { request, uploadFile } from "@/utils/request";
  53. import { Background } from '@vue-flow/background'
  54. import { ControlButton, Controls } from '@vue-flow/controls'
  55. import "./main.css";//重置样式
  56. import defaultnode from './defaultnode.vue'
  57. import PointOnlyNode from './pointonlynode.vue'
  58. import useDragAndDrop from './useDnD';
  59. import {getNodeCount, setNodeCount} from './useDnD';
  60. import changebak from './changebak.vue'
  61. import { useProjectStore } from '@/store/project'
  62. import emitter from "@/utils/emitter";
  63. import { onMounted } from 'vue';
  64. const { onInit, onNodeDragStop, onConnect, addEdges, setViewport, toObject,addNodes,updateEdgeData,onConnectStart} = useVueFlow()
  65. const { onDragOver, onDrop, onDragLeave, isDragOver } = useDragAndDrop();
  66. const dark = ref(false)
  67. let vueFlowRef = ref();
  68. let iconcolor=ref('#000')
  69. const edges = ref([]);
  70. const nodes = ref([]);
  71. let mergedObj = ref('');
  72. // 选中节点
  73. let noid = ref([]);
  74. let Edgeid = ref();//选中线段id
  75. let seledge=ref(null);
  76. let previousEdge = null; // 用于保存上一个选中的边缘
  77. // 连线颜色
  78. let linecolor=ref('#2267B1')
  79. // 线宽
  80. let linewidth=ref(1);
  81. let midNodeCounter = 0;//储存中间节点的序号
  82. const projectStore = useProjectStore()
  83. let pid = computed(() => projectStore.pid || '')
  84. // 旧数据存储
  85. let prevNodes = [...nodes.value];
  86. let prevEdges = [...edges.value];
  87. // 监听节点变化
  88. watch(nodes, (newNodes) => {
  89. const deletedNodes = prevNodes.filter((node) => !newNodes.some((n) => n.id === node.id));
  90. const addedNodes = newNodes.filter((node) => !prevNodes.some((n) => n.id === node.id));
  91. if (deletedNodes.length > 0) {
  92. console.log('Deleted Nodes:', deletedNodes);
  93. deletedNodes.forEach((node) => {
  94. if (node.data?.pcId) {
  95. comdelete(node.data.pcId);
  96. }
  97. });
  98. }
  99. if (addedNodes.length > 0) {
  100. // console.log('Added Nodes:', addedNodes);
  101. }
  102. // 更新快照
  103. prevNodes = [...newNodes];
  104. }, { deep: true });
  105. // 监听连线变化
  106. watch(edges, (newEdges) => {
  107. const deletedEdges = prevEdges.filter((edge) => !newEdges.some((e) => e.id === edge.id));
  108. const addedEdges = newEdges.filter((edge) => !prevEdges.some((e) => e.id === edge.id));
  109. if (deletedEdges.length > 0) {
  110. console.log('Deleted Edges:', deletedEdges);
  111. deletedEdges.forEach((edge) => {
  112. const sourceNode = prevNodes.find(n => n.id === edge.source);
  113. const targetNode = prevNodes.find(n => n.id === edge.target);
  114. if (!sourceNode || !targetNode) return;
  115. let npcId = '';
  116. let pcId = '';
  117. // 判断 source 或 target 是否为中间点 comId === '3'
  118. if (sourceNode.data?.comId === '3') {
  119. npcId = sourceNode.data.npcId;
  120. pcId = targetNode.data?.pcId;
  121. } else if (targetNode.data?.comId === '3') {
  122. npcId = targetNode.data.npcId;
  123. pcId = sourceNode.data?.pcId;
  124. }
  125. if (npcId && pcId) {
  126. comlinedelete(npcId, pcId);
  127. }
  128. });
  129. }
  130. if (addedEdges.length > 0) {
  131. console.log('Added Edges:', addedEdges);
  132. }
  133. // 更新快照
  134. prevEdges = [...newEdges];
  135. }, { deep: true });
  136. const resetTransform = () => {
  137. setViewport({ x: 0, y: 0, zoom: 1 })
  138. }
  139. const toggleDarkMode = () => {
  140. dark.value = !dark.value;
  141. if(dark.value){
  142. iconcolor.value='#fff'
  143. }else{
  144. iconcolor.value='#000'
  145. }
  146. }
  147. const saveproject = () => {
  148. let obj = {
  149. nodes: toObject().nodes,
  150. edges: toObject().edges,
  151. midNodeCounter: midNodeCounter, // 添加中间节点计数器
  152. nodeCount: getNodeCount(), // 获取当前节点计数
  153. };
  154. mergedObj.value=JSON.stringify(obj);
  155. const params = {
  156. transCode: 'ES0002',
  157. pid: pid.value,
  158. flow: mergedObj.value,
  159. name: projectStore.projectInfo.name || '',
  160. remark: projectStore.projectInfo.remark || '',
  161. keywords: projectStore.projectInfo.keywords || '',
  162. };
  163. request(params)
  164. .then((res) => {
  165. projectStore.setProjectInfo({
  166. ...projectStore.projectInfo,
  167. flow: mergedObj.value || '',
  168. })
  169. })
  170. .catch((error) => {
  171. console.error('保存失败:', error);
  172. });
  173. }
  174. const removeNode = () => {
  175. if (!noid.value) {
  176. console.warn('未选中任何节点');
  177. return;
  178. }
  179. const nodeIdToRemove = noid.value.id;
  180. vueFlowRef.value.removeNodes(noid.value.id);
  181. // 清空当前选中
  182. noid.value = null;
  183. saveproject();
  184. }
  185. const removeEdge = () => {
  186. if (!seledge.value) {
  187. ElMessage.warning('请先选择要删除的连线');
  188. return;
  189. }
  190. vueFlowRef.value.removeEdges(seledge.value); // 移除选中的边
  191. seledge.value = null; // 清空选中状态
  192. saveproject();
  193. }
  194. const confirmDelete = () => {
  195. ElMessageBox.confirm(
  196. '确定要删除所有节点和连线吗?此操作不可恢复!',
  197. '警告',
  198. {
  199. confirmButtonText: '确认',
  200. cancelButtonText: '取消',
  201. type: 'warning',
  202. }
  203. )
  204. .then(() => {
  205. vueFlowRef.value.removeNodes(nodes.value)
  206. vueFlowRef.value.removeEdges(edges.value)
  207. ElMessage.success('所有节点和连线已删除')
  208. saveproject();
  209. })
  210. .catch(() => {
  211. ElMessage.info('已取消删除')
  212. })
  213. }
  214. const comdelete = (pcId) => {
  215. const params = {
  216. transCode:'ES0005',
  217. pcId: pcId
  218. }
  219. request(params)
  220. .then((res) => {
  221. // console.log('组件删除成功')
  222. })
  223. .catch((err) => {
  224. ElMessage.error(err.returnMsg);
  225. });
  226. }
  227. const comlinedelete = (npcId,pcId) => {
  228. const params = {
  229. transCode:'ES0005',
  230. npcId: npcId,
  231. pcId: pcId,
  232. type: 0
  233. }
  234. request(params)
  235. .then((res) => {
  236. // console.log('组件连线删除成功')
  237. })
  238. .catch((err) => {
  239. ElMessage.error(err.returnMsg);
  240. });
  241. }
  242. const customOnDrop = (event) => {
  243. console.log('自定义拖放事件', event);
  244. onDrop(event);
  245. saveproject();
  246. }
  247. const onNodeContextMenu = ( e) => {
  248. console.log('右键点击', e);
  249. }
  250. const onEdgeClick = (e) => {
  251. console.log('Edge Click', e.edge);
  252. // console.log('所有线段:', edges.value);
  253. // 如果已经有选中的边缘
  254. if (seledge.value) {
  255. // 恢复上一个选中边缘的样式
  256. if (previousEdge) {
  257. previousEdge.style = {
  258. ...previousEdge.style,
  259. stroke: previousEdge.originalColor, // 恢复原始颜色
  260. strokeWidth: previousEdge.originalWidth,// 恢复原始宽度
  261. };
  262. }
  263. }
  264. // 保存当前点击的边缘为选中边缘
  265. Edgeid.value = e.edge.id;
  266. seledge.value = e.edge;
  267. // 暂时更改当前选中边缘的样式
  268. seledge.value.originalColor = seledge.value.style.stroke; // 保存当前边缘的原始颜色
  269. seledge.value.originalWidth = seledge.value.style.strokeWidth; // 保存当前边缘的原始宽度
  270. const isdefault = e.edge.data.type === 'default'; // 判断是否为默认线
  271. seledge.value.style = {
  272. ...seledge.value.style,
  273. stroke: isdefault ? '#2267B1' : 'rgba(255, 255, 0, 0.3)',// 设置选中边缘的颜色
  274. strokeWidth: isdefault ? 2 : 6,// 设置选中边缘的宽度
  275. };
  276. // 保存当前选中的边缘作为上一个选中边缘
  277. previousEdge = seledge.value;
  278. }
  279. const onNodeClick = (event) => {
  280. console.log('节点被单击:', event);
  281. noid.value = event.node;
  282. };
  283. const onNodeDoubleClick = (event) => {
  284. console.log('节点被双击:', event);
  285. };
  286. const onEdgeDoubleClick = (event) => {
  287. console.log("连线双击:", event);
  288. noid.value = event.node;
  289. }
  290. // 监听连接开始,提前取消选中的线段
  291. onConnectStart(() => {
  292. cleanEdgeselect();
  293. });
  294. // 线的类型 default
  295. let lineType = ref('default');
  296. // 线序号
  297. let linecount = ref(0);
  298. onConnect(async (connection) => {
  299. console.log('线连接', connection);
  300. if (connection.source === connection.target) {
  301. console.warn('禁止节点自连');
  302. return;
  303. }
  304. const sourceNode = vueFlowRef.value.getNode(connection.source);
  305. const targetNode = vueFlowRef.value.getNode(connection.target);
  306. if(sourceNode.data.comId === '3' && targetNode.data.comId === '3'){
  307. console.warn('禁止连接两个中间节点');
  308. return;
  309. }
  310. // 是否需要中间节点
  311. const isCom3 = sourceNode.data.comId === '3' || targetNode.data.comId === '3';
  312. if (isCom3) {
  313. //(comID == 3)
  314. const edgeId = `${lineType.value}-${connection.source}-${connection.sourceHandle}-${connection.target}-${connection.targetHandle}`;
  315. connection.id = edgeId;
  316. connection.type = 'smoothstep';
  317. connection.color = linecolor.value;
  318. connection.style = { strokeWidth: linewidth.value, stroke: linecolor.value };
  319. // connection.markerEnd = lineType.value === 'data'
  320. // ? { type: MarkerType.ArrowClosed, width: 6, height: 6, color: linecolor.value }
  321. // : MarkerType.ArrowClosed;
  322. const sameEdge = edges.value.find(edge => edge.id === edgeId);
  323. if (sameEdge) {
  324. deleteflow(sameEdge.data?.wid);
  325. }
  326. addEdges(connection);
  327. seledge.value = null;
  328. linecount.value++;
  329. const newName = `Seg${linecount.value}`;
  330. const pcId1 = sourceNode.data.comId === '3' ? targetNode.data.pcId : sourceNode.data.pcId;
  331. const npcId = sourceNode.data.comId === '3' ? sourceNode.data.pcId : targetNode.data.pcId;
  332. try {
  333. const { pccId1 } = await createEdge(pid.value, 0, npcId, pcId1);
  334. updateEdgeData(connection.id, {
  335. pcid: pccId1,
  336. uid: newName,
  337. type: lineType.value,
  338. frompcId: sourceNode.data.pcId,
  339. topcId: targetNode.data.pcId,
  340. });
  341. } catch (error) {
  342. console.error('保存流程失败:', error);
  343. }
  344. } else {
  345. // 添加 point-only 中间节点
  346. const pointNodeId = `point-${connection.source}-${connection.target}}`;
  347. const sourceCenterX = sourceNode.position.x + (60) / 2;
  348. const sourceCenterY = sourceNode.position.y + (58) / 2;
  349. const targetCenterX = targetNode.position.x + (60) / 2;
  350. const targetCenterY = targetNode.position.y + (58) / 2;
  351. const midX = (sourceCenterX + targetCenterX) / 2;
  352. const midY = (sourceCenterY + targetCenterY) / 2;
  353. // 计算 sourceNode 和 targetNode 相对于 pointNode 的位置
  354. const dxSource = sourceNode.position.x - midX;
  355. const dySource = sourceNode.position.y - midY;
  356. const dxTarget = targetNode.position.x - midX;
  357. const dyTarget = targetNode.position.y - midY;
  358. // 判断 sourceNode 的主要方向(水平 or 垂直)
  359. const isSourceHorizontal = Math.abs(dxSource) > Math.abs(dySource);
  360. const sourceDirection = isSourceHorizontal
  361. ? (dxSource < 0 ? 'left' : 'right')
  362. : (dySource < 0 ? 'top' : 'bottom');
  363. // 判断 targetNode 的主要方向
  364. const isTargetHorizontal = Math.abs(dxTarget) > Math.abs(dyTarget);
  365. const targetDirection = isTargetHorizontal
  366. ? (dxTarget < 0 ? 'left' : 'right')
  367. : (dyTarget < 0 ? 'top' : 'bottom');
  368. midNodeCounter++;
  369. const uid = `N${midNodeCounter}`;
  370. nodes.value.push({
  371. id: pointNodeId,
  372. type: 'point-only',
  373. position: { x: midX, y: midY },
  374. data: {
  375. comId:'3',
  376. label: '节点',
  377. uid: uid,
  378. },
  379. });
  380. try {
  381. // 创建 comID 为 3 的中间组件
  382. const npcId = await createCom3(pid.value);
  383. const pointNode = nodes.value.find(n => n.id === pointNodeId);
  384. if (pointNode) {
  385. pointNode.data.pcId = npcId; // 写入 data
  386. }
  387. // edge1: source -> pointNode
  388. const edgeId1 = `${lineType.value}-${connection.source}-${connection.sourceHandle}-${pointNodeId}`;
  389. const edge1 = {
  390. id: edgeId1,
  391. source: connection.source,
  392. sourceHandle: connection.sourceHandle,
  393. target: pointNodeId,
  394. targetHandle: `source-${sourceDirection}`,
  395. type: 'smoothstep',
  396. color: linecolor.value,
  397. style: { strokeWidth: linewidth.value, stroke: linecolor.value }
  398. };
  399. // edge2: pointNode -> target
  400. const edgeId2 = `${lineType.value}-${pointNodeId}-${connection.targetHandle}-${connection.target}`;
  401. const edge2 = {
  402. id: edgeId2,
  403. source: pointNodeId,
  404. sourceHandle: `source-${targetDirection}`, // 使用计算的方向
  405. target: connection.target,
  406. targetHandle: connection.targetHandle,
  407. type: 'smoothstep',
  408. color: linecolor.value,
  409. style: { strokeWidth: linewidth.value, stroke: linecolor.value }
  410. };
  411. addEdges([edge1, edge2]);
  412. linecount.value++;
  413. const newName = `Seg${linecount.value}`;
  414. // 保存后端 edge 数据(分别对应两条边)
  415. const { pccId1, pccId2 } = await createEdge(pid.value, 0, npcId, sourceNode.data.pcId, targetNode.data.pcId);
  416. updateEdgeData(edgeId1, {
  417. pcid: pccId1,
  418. uid: newName,
  419. type: lineType.value,
  420. frompcId: sourceNode.data.pcId,
  421. topcId: npcId
  422. });
  423. updateEdgeData(edgeId2, {
  424. pcid: pccId2,
  425. uid: newName,
  426. type: lineType.value,
  427. frompcId: npcId,
  428. topcId: targetNode.data.pcId
  429. });
  430. } catch (err) {
  431. console.error('中间点处理失败:', err);
  432. }
  433. }
  434. saveproject();
  435. });
  436. const createEdge = async (pid, type, npcId, pcId1, pcId2) => {
  437. const params = {
  438. transCode: 'ES0006',
  439. pid: pid || '',
  440. type: type || 0,
  441. npcId: npcId || '',
  442. pcId1: pcId1 || '',
  443. pcId2: pcId2 || '',
  444. };
  445. try {
  446. const res = await request(params);
  447. return {
  448. pccId1: res.pccId1,
  449. pccId2: res.pccId2
  450. }; // 返回包含 pccId1 和 pccId2 的对象
  451. } catch (err) {
  452. // 处理错误
  453. ElMessage.error(err.returnMsg || '保存流程失败');
  454. throw err; // 可以选择重新抛出错误以便调用者处理
  455. }
  456. };
  457. // 创建特殊节点
  458. const createCom3 = async (pid) => {
  459. const params = {
  460. transCode: "ES0004",
  461. pid: pid || "",
  462. comId:"3", // 节点ID
  463. }
  464. try {
  465. const res = await request(params)
  466. return res.pcId // 返回新创建的组件在项目中的ID
  467. } catch (err) {
  468. ElMessage.error(err.returnMsg)
  469. }
  470. }
  471. const flowInit = () => {
  472. let nodesflow = JSON.parse(projectStore.projectInfo.flow || '{"nodes":[],"edges":[]}');
  473. console.log('初始化流程数据:', nodesflow);
  474. nodes.value = nodesflow.nodes;
  475. edges.value = nodesflow.edges;
  476. // 提取中间节点计数器
  477. midNodeCounter = nodesflow.midNodeCounter || 0;
  478. // 提取节点计数
  479. setNodeCount(nodesflow.nodeCount || 0);
  480. }
  481. const cleanEdgeselect = () => {
  482. if(seledge.value) {
  483. // 恢复选中边缘的原始样式
  484. seledge.value.style = {
  485. ...seledge.value.style,
  486. stroke: seledge.value.originalColor,
  487. strokeWidth: previousEdge?.originalWidth || 1, // 恢复原始宽度
  488. };
  489. // 清空选中的边缘
  490. seledge.value = null;
  491. Edgeid.value = null;
  492. previousEdge = null;
  493. }
  494. }
  495. onMounted(() => {
  496. flowInit();
  497. // 点击其他区域取消线段选中
  498. if (vueFlowRef.value) {
  499. vueFlowRef.value.$el.addEventListener('click', (event) => {
  500. // 确保点击的不是边缘
  501. if (seledge.value && !event.target.closest('.vue-flow__edge')) {
  502. cleanEdgeselect();
  503. }
  504. });
  505. }
  506. });
  507. </script>
  508. <style scoped>
  509. /* .vue-flow__edge.selected .vue-flow__edge-path, .vue-flow__edge:focus .vue-flow__edge-path, .vue-flow__edge:focus-visible .vue-flow__edge-path {
  510. stroke: #555 !important;
  511. } */
  512. .vue-flow__edge:focus .vue-flow__edge-path, .vue-flow__edge:focus-visible .vue-flow__edge-path {
  513. stroke: #555 !important;
  514. }
  515. .vue-flow__edge {
  516. text-align: left;
  517. /* 设置edges的左对齐 */
  518. }
  519. .vue-flow__edge-text {
  520. transform: translateY(-10px); /* 将 label 向上偏移 */
  521. background: transparent !important;
  522. font-size: 8px;
  523. font-family: 'Microsoft YaHei';
  524. color: #333333;
  525. }
  526. .vue-flow__edge-textbg {
  527. fill: transparent !important; /* 将背景设置为透明 */
  528. }
  529. .vue-flow__node-default.selectable:hover,
  530. .vue-flow__node-input.selectable:hover,
  531. .vue-flow__node-output.selectable:hover {
  532. box-shadow: none;
  533. }
  534. .remove {
  535. background: #fff;
  536. color: #666;
  537. margin: 0 10px;
  538. font-size: 12px;
  539. }
  540. .vue-flow__node-default, .vue-flow__node-input, .vue-flow__node-output{
  541. /* width: auto !important; */
  542. border: none;
  543. background-color: rgba(0,0,0,0);
  544. }
  545. .node-content {
  546. cursor: move; /* 更改鼠标光标表示可拖动 */
  547. }
  548. .vue-flow__node {
  549. cursor: move;
  550. }
  551. /* 禁用文本选中效果 */
  552. .left_main * {
  553. -webkit-user-select: none; /* Safari */
  554. -moz-user-select: none; /* Firefox */
  555. -ms-user-select: none; /* IE10+/Edge */
  556. user-select: none; /* Standard syntax */
  557. }
  558. .lableaniu{
  559. font-size: 12px;
  560. background-color: #ddd;
  561. padding: 4px 16px;
  562. /* margin-top: -17px; */
  563. margin-left: 5px;
  564. margin-top: 0px;
  565. border-radius: 1px;
  566. }
  567. .vue-flow__controls-button svg{
  568. max-width: 16px;
  569. max-height: 16px;
  570. }
  571. .field{
  572. display: flex;
  573. }
  574. </style>