index.vue 31 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058
  1. <template>
  2. <splitpanes class="default-theme">
  3. <pane min-size="50" size="100" max-size="100">
  4. <!-- :nodes-draggable="isSelectMode"
  5. :nodes-connectable="isSelectMode ? false : true" -->
  6. <VueFlow
  7. ref="vueFlowRef"
  8. v-model:nodes="nodes"
  9. v-model:edges="edges"
  10. :class="{ dark }"
  11. class="basic-flow"
  12. :default-viewport="{ zoom: 1.5 }"
  13. :min-zoom="0.2"
  14. :max-zoom="2.5"
  15. :multi-selection-key-code="'Control'"
  16. :selection-key-code="'Shift'"
  17. :nodes-draggable="!props.isSelectMode"
  18. :nodes-connectable="props.isSelectMode ? false : true"
  19. @drop="customOnDrop"
  20. @node-contextmenu="onNodeContextMenu"
  21. @dragover="onDragOver"
  22. @dragleave="onDragLeave"
  23. @edge-click="onEdgeClick"
  24. @node-double-click="onNodeDoubleClick"
  25. @node-click="onNodeClick"
  26. @edge-double-click="onEdgeDoubleClick"
  27. @node-drag="onNodeDrag"
  28. @nodes-selection-change="onNodesSelectionChange"
  29. @edges-selection-change="onEdgesSelectionChange"
  30. >
  31. <!-- 自定义节点类型为default的节点 -->
  32. <template #node-default="props">
  33. <defaultnode :node="props" :key="props.data.idCodeser || props.id" />
  34. </template>
  35. <template #node-point-only="props">
  36. <PointOnlyNode
  37. :node="props"
  38. :key="props.data.idCodeser || props.id"
  39. />
  40. </template>
  41. <Background
  42. :variant="showGrid ? 'dots' : 'none'"
  43. pattern-color="#aaa"
  44. :gap="20"
  45. />
  46. </VueFlow>
  47. </pane>
  48. <pane
  49. v-if="showPanel"
  50. min-size="20"
  51. :size="30"
  52. max-size="50"
  53. class="flow-panel"
  54. >
  55. <asideData ref="asideDataref" @close="closePanel" />
  56. </pane>
  57. </splitpanes>
  58. </template>
  59. <script setup>
  60. import { VueFlow, useVueFlow, MarkerType } from "@vue-flow/core"
  61. import {
  62. ElMessage,
  63. ElMessageBox
  64. } from "element-plus"
  65. import { DeleteFilled } from "@element-plus/icons-vue"
  66. import { onBeforeRouteLeave } from "vue-router"
  67. import { request, uploadFile, getImageBase64 } from "@/utils/request"
  68. import { Background } from "@vue-flow/background"
  69. import "./main.css" //重置样式
  70. import defaultnode from "./defaultnode.vue"
  71. import PointOnlyNode from "./pointonlynode.vue"
  72. import useDragAndDrop from "./useDnD"
  73. import { useProjectStore } from "@/store/project"
  74. import { useClipboardStore } from '@/store/clipboard'
  75. import emitter from "@/utils/emitter"
  76. import { onMounted, onUnmounted, onBeforeUnmount, nextTick } from "vue"
  77. import { Splitpanes, Pane } from "splitpanes"
  78. import "splitpanes/dist/splitpanes.css"
  79. import asideData from "./aside/asideData.vue"
  80. import { copyNodes, cutNodes, pasteNodes } from "./utils/clipboardUtils";
  81. import node from "@/assets/img/node.png"
  82. import nodePoint from "@/assets/img/node.png"
  83. import tempic from "@/assets/img/temp.png"
  84. import { debounce } from 'lodash-es'
  85. // 常量定义
  86. const GRID_SIZE = 10
  87. const DEBOUNCE_DELAY = 1000
  88. const DEFAULT_LINE_COLOR = "#2267B1"
  89. const DEFAULT_LINE_WIDTH = 1
  90. // 定义一个新的 ref 用于存储边的选择状态
  91. const edgeSelectionState = ref({}); // 格式: { [edgeId]: { class: string, style: object } }
  92. // 获取路由实例
  93. const router = useRouter()
  94. const {
  95. onInit,
  96. onNodeDragStop,
  97. onConnect,
  98. addEdges,
  99. setViewport,
  100. toObject,
  101. addNodes,
  102. updateEdgeData,
  103. onConnectStart,
  104. updateNode,
  105. updateNodeInternals,
  106. onNodeDrag,
  107. snapToGrid,
  108. snapGrid,
  109. setInteractive,
  110. removeNodes,
  111. removeEdges,
  112. selectedNodes: vueFlowSelectedNodes,
  113. selectedEdges: vueFlowSelectedEdges,
  114. } = useVueFlow()
  115. const { onDragOver, onDrop, onDragLeave, isDragOver } = useDragAndDrop()
  116. const dark = ref(false)
  117. const vueFlowRef = ref()
  118. const iconcolor = ref("#000")
  119. const edges = ref([])
  120. const nodes = ref([])
  121. const mergedObj = ref("")
  122. const selectedNodes = ref([]); // 选中节点
  123. const selectedEdges = ref([]); // 选中边
  124. // 选中状态
  125. const selectedNode = ref(null)
  126. const selectedEdge = ref(null)
  127. const previousEdge = ref(null)
  128. const lineColor = ref(DEFAULT_LINE_COLOR)
  129. const lineWidth = ref(DEFAULT_LINE_WIDTH)
  130. const midNodeCounter = 0
  131. const projectStore = useProjectStore()
  132. const clipboardStore = useClipboardStore();
  133. const pid = computed(() => projectStore.pid || "")
  134. // 删除组件和边的列表
  135. const pendingDelete = ref({ nodes: [], edges: [] });
  136. const isCutting = ref(false);
  137. const showPanel = ref(false)
  138. const asideDataref = ref()
  139. const props = defineProps({
  140. jobId: { type: String, default: "" },
  141. showGrid: { type: Boolean, default: false },
  142. fixedGrid: { type: Boolean, default: false },
  143. isSelectMode: { type: Boolean, default: true }
  144. })
  145. // 保存项目
  146. const saveproject = async () => {
  147. try {
  148. const activeProject = projectStore.getActiveProject();
  149. if (!activeProject) {
  150. throw new Error("当前没有激活的项目");
  151. }
  152. const obj = {
  153. nodes: toObject().nodes.map((node) => ({
  154. ...node,
  155. data: { ...node.data, image: undefined },
  156. })),
  157. edges: toObject().edges,
  158. };
  159. mergedObj.value = JSON.stringify(obj);
  160. const params = {
  161. transCode: "ES0002",
  162. pid: pid.value,
  163. flow: mergedObj.value,
  164. name: activeProject.projectName || "",
  165. remark: activeProject.remark || "",
  166. keywords: activeProject.keywords || "",
  167. };
  168. // 保存前处理待删除的组件和边
  169. for (const { pcId } of pendingDelete.value.nodes) {
  170. await comdelete(pcId);
  171. }
  172. for (const { npcId, pcId } of pendingDelete.value.edges) {
  173. await comlinedelete(npcId, pcId);
  174. }
  175. pendingDelete.value = { nodes: [], edges: [] }; // 清空待删除列表
  176. await request(params);
  177. projectStore.updateProjectInfo(projectStore.activeProjectId, {
  178. ...activeProject,
  179. flow: mergedObj.value,
  180. });
  181. ElMessage.success("保存成功");
  182. } catch (error) {
  183. console.error("保存失败:", error);
  184. ElMessage.error("保存失败,请稍后重试");
  185. }
  186. };
  187. const debouncedSave = debounce(saveproject, DEBOUNCE_DELAY)
  188. let prevNodes = []
  189. let prevEdges = []
  190. let isInitializing = ref(true)
  191. // 合并的 watch:监听节点和边变化
  192. watch(
  193. [nodes, edges],
  194. ([newNodes, newEdges]) => {
  195. if (isInitializing.value) {
  196. prevNodes = [...newNodes];
  197. prevEdges = [...newEdges];
  198. isInitializing.value = false;
  199. return;
  200. }
  201. if (isCutting.value) {
  202. // 剪切时不触发保存,仅记录删除
  203. const deletedNodes = prevNodes.filter((n) => !newNodes.some((nn) => nn.id === n.id));
  204. if (deletedNodes.length > 0) {
  205. pendingDelete.value.nodes.push(...deletedNodes.map((node) => ({ pcId: node.data.pcId })));
  206. }
  207. const deletedEdges = prevEdges.filter((e) => !newEdges.some((ne) => ne.id === e.id));
  208. if (deletedEdges.length > 0) {
  209. deletedEdges.forEach((edge) => {
  210. const sourceNode = prevNodes.find((n) => n.id === edge.source);
  211. const targetNode = prevNodes.find((n) => n.id === edge.target);
  212. if (!sourceNode || !targetNode) return;
  213. let npcId = "",
  214. pcId = "";
  215. if (sourceNode.data?.comId === "3") {
  216. npcId = sourceNode.data.pcId;
  217. pcId = targetNode.data?.pcId;
  218. } else if (targetNode.data?.comId === "3") {
  219. npcId = targetNode.data.pcId;
  220. pcId = sourceNode.data?.pcId;
  221. }
  222. if (npcId && pcId) {
  223. pendingDelete.value.edges.push({ npcId, pcId });
  224. }
  225. });
  226. }
  227. prevNodes = [...newNodes];
  228. prevEdges = [...newEdges];
  229. debouncedSave.cancel();
  230. return; // 阻止 debouncedSave
  231. }
  232. // 其他操作(如添加、删除)触发保存
  233. const deletedNodes = prevNodes.filter((n) => !newNodes.some((nn) => nn.id === n.id));
  234. if (deletedNodes.length > 0) {
  235. pendingDelete.value.nodes.push(...deletedNodes.map((node) => ({ pcId: node.data.pcId })));
  236. }
  237. const deletedEdges = prevEdges.filter((e) => !newEdges.some((ne) => ne.id === e.id));
  238. if (deletedEdges.length > 0) {
  239. deletedEdges.forEach((edge) => {
  240. const sourceNode = prevNodes.find((n) => n.id === edge.source);
  241. const targetNode = prevNodes.find((n) => n.id === edge.target);
  242. if (!sourceNode || !targetNode) return;
  243. let npcId = "",
  244. pcId = "";
  245. if (sourceNode.data?.comId === "3") {
  246. npcId = sourceNode.data.pcId;
  247. pcId = targetNode.data?.pcId;
  248. } else if (targetNode.data?.comId === "3") {
  249. npcId = targetNode.data.pcId;
  250. pcId = sourceNode.data?.pcId;
  251. }
  252. if (npcId && pcId) {
  253. pendingDelete.value.edges.push({ npcId, pcId });
  254. }
  255. });
  256. }
  257. prevNodes = [...newNodes];
  258. prevEdges = [...newEdges];
  259. debouncedSave();
  260. },
  261. { deep: false }
  262. );
  263. // 网格固定
  264. watch(
  265. () => props.fixedGrid,
  266. (newValue) => {
  267. snapToGrid.value = newValue
  268. snapGrid.value = newValue ? [GRID_SIZE, GRID_SIZE] : [1, 1] // 禁用吸附时用 [1, 1]
  269. },
  270. { immediate: true }
  271. )
  272. onMounted(() => {
  273. flowInit()
  274. // 点击画布取消边选中
  275. if (vueFlowRef.value) {
  276. vueFlowRef.value.$el.addEventListener("click", (event) => {
  277. if (selectedEdge.value && !event.target.closest(".vue-flow__edge")) {
  278. cleanEdgeSelect();
  279. selectedNodes.value = [];
  280. selectedEdges.value = [];
  281. }
  282. })
  283. }
  284. console.log('Binding keydown event listener');
  285. window.addEventListener('keydown', handleKeyDown)
  286. window.addEventListener('beforeunload', handleBeforeUnload) // 绑定页面关闭事件
  287. })
  288. onBeforeUnmount(() =>{
  289. saveproject();
  290. })
  291. onUnmounted(() => {
  292. window.removeEventListener('keydown', handleKeyDown)
  293. window.removeEventListener('beforeunload', handleBeforeUnload) // 移除页面关闭事件
  294. })
  295. // 在路由切换前保存
  296. onBeforeRouteLeave((to, from, next) => {
  297. console.log('onBeforeRouteLeave triggered, saving project')
  298. debouncedSave.flush() // 立即执行任何待保存的操作
  299. saveproject().then(() => {
  300. console.log('Save completed before route leave')
  301. next()
  302. }).catch((error) => {
  303. console.error('Save failed before route leave:', error)
  304. ElMessage.error('保存失败,请检查网络后重试')
  305. next() // 即使保存失败,也允许路由切换
  306. })
  307. })
  308. const handleBeforeUnload = (event) => {
  309. console.log('beforeunload triggered, saving project')
  310. debouncedSave.flush() // 立即执行待保存操作
  311. // 注意:beforeunload 中无法等待异步 saveproject 完成
  312. event.preventDefault()
  313. event.returnValue = '项目尚未保存,确定要离开吗?' // 提示用户
  314. }
  315. // 新增:监听节点选择变化(包括 Ctrl+点击和框选)
  316. const onNodesSelectionChange = (event) => {
  317. selectedNodes.value = Array.from(vueFlowSelectedNodes.value);
  318. };
  319. // 新增:监听边选择变化
  320. const onEdgesSelectionChange = (event) => {
  321. selectedEdges.value = Array.from(vueFlowSelectedEdges.value);
  322. };
  323. // 网格吸附逻辑
  324. onNodeDrag(({ node }) => {
  325. if (!props.fixedGrid || node.type !== 'point-only') return
  326. // 仅对 point-only 节点禁用吸附效果
  327. const newPosition = {
  328. x: node.position.x, // 保留原始位置
  329. y: node.position.y,
  330. }
  331. updateNode(node.id, { position: newPosition })
  332. })
  333. // 键盘事件:Backspace 删除选中节点
  334. const handleKeyDown = (event) => {
  335. if (event.ctrlKey) {
  336. if (event.key === 'c') {
  337. event.preventDefault(); // 阻止默认行为
  338. event.stopPropagation(); // 阻止事件冒泡
  339. copyNodes(selectedNodes.value, selectedEdges.value, edges.value, nodes.value, pid.value);
  340. } else if (event.key === 'x') {
  341. event.preventDefault(); // 阻止默认行为
  342. event.stopPropagation(); // 阻止事件冒泡
  343. isCutting.value = true;
  344. cutNodes(selectedNodes.value, selectedEdges.value, nodes.value, edges.value, saveproject, removeNodes, removeEdges, clearSelection )
  345. .finally(() => {
  346. isCutting.value = false; // 重置标志
  347. });
  348. } else if (event.key === 'v') {
  349. event.preventDefault(); // 阻止默认行为
  350. event.stopPropagation(); // 阻止事件冒泡
  351. pasteNodes(pid.value, nodes.value, edges.value, saveproject, createEdge, addNodes, addEdges, updateNodeInternals);
  352. }
  353. } else if (event.key === 'Backspace' && selectedNodes.value.length > 0) {
  354. event.preventDefault(); // 阻止默认行为
  355. event.stopPropagation(); // 阻止事件冒泡
  356. removeNodes(selectedNodes.value.map((n) => n.id));
  357. selectedNodes.value = [];
  358. }
  359. };
  360. const clearSelection = () => {
  361. selectedNodes.value = [];
  362. selectedEdges.value = [];
  363. };
  364. const resetTransform = () => {
  365. setViewport({ x: 0, y: 0, zoom: 1 })
  366. }
  367. const toggleDarkMode = () => {
  368. dark.value = !dark.value
  369. iconcolor.value = dark.value ? "#fff" : "#000"
  370. }
  371. const removeNode = () => {
  372. if (!selectedNode.value) {
  373. ElMessage.warning("未选中任何节点")
  374. return
  375. }
  376. vueFlowRef.value.removeNodes(selectedNode.value.id)
  377. selectedNode.value = null
  378. debouncedSave();
  379. }
  380. const removeEdge = () => {
  381. if (!selectedEdge.value) {
  382. ElMessage.warning("请先选择要删除的连线")
  383. return
  384. }
  385. vueFlowRef.value.removeEdges(selectedEdge.value.id)
  386. selectedEdge.value = null
  387. debouncedSave();
  388. }
  389. const confirmDelete = () => {
  390. ElMessageBox.confirm("确定要删除所有节点和连线吗?此操作不可恢复!", "警告", {
  391. confirmButtonText: "确认",
  392. cancelButtonText: "取消",
  393. type: "warning"
  394. })
  395. .then(() => {
  396. vueFlowRef.value.removeNodes(nodes.value)
  397. vueFlowRef.value.removeEdges(edges.value)
  398. ElMessage.success("所有节点和连线已删除")
  399. debouncedSave();
  400. })
  401. .catch(() => {
  402. ElMessage.info("已取消删除")
  403. })
  404. }
  405. const comdelete = async (pcId) => {
  406. try {
  407. const params = { transCode: "ES0005", pcId }
  408. await request(params)
  409. } catch (err) {
  410. ElMessage.error(err.returnMsg || "组件删除失败")
  411. }
  412. }
  413. const comlinedelete = async (npcId, pcId) => {
  414. try {
  415. const params = { transCode: "ES0007", npcId, pcId, type: 0 }
  416. await request(params)
  417. } catch (err) {
  418. ElMessage.error(err.returnMsg || "连线删除失败")
  419. }
  420. }
  421. const customOnDrop = async (event) => {
  422. await onDrop(event)
  423. debouncedSave();
  424. }
  425. const onNodeContextMenu = (e) => {
  426. console.log("右键点击", e)
  427. }
  428. const getChainFromEdge = (edge) => {
  429. const chainNodes = new Set(); // 用 Set 避免重复
  430. const chainEdges = new Set(); // 用 Set 避免重复
  431. // 添加当前边
  432. chainEdges.add(edge);
  433. // 获取源和目标节点
  434. let currentSource = vueFlowRef.value.getNode(edge.source);
  435. let currentTarget = vueFlowRef.value.getNode(edge.target);
  436. chainNodes.add(currentSource);
  437. chainNodes.add(currentTarget);
  438. // 如果源是中间节点,向前扩展(找出源的输入边)
  439. if (currentSource.data?.comId === '3') {
  440. const inputEdges = edges.value.filter(e => e.target === currentSource.id);
  441. inputEdges.forEach(inputEdge => {
  442. chainEdges.add(inputEdge);
  443. const prevNode = vueFlowRef.value.getNode(inputEdge.source);
  444. chainNodes.add(prevNode);
  445. });
  446. }
  447. // 如果目标是中间节点,向后扩展(找出目标的输出边)
  448. if (currentTarget.data?.comId === '3') {
  449. const outputEdges = edges.value.filter(e => e.source === currentTarget.id);
  450. outputEdges.forEach(outputEdge => {
  451. chainEdges.add(outputEdge);
  452. const nextNode = vueFlowRef.value.getNode(outputEdge.target);
  453. chainNodes.add(nextNode);
  454. });
  455. }
  456. // 如果链条两端不是大节点,继续递归扩展(但假设单中间节点,此处简化)
  457. // 如果有多个中间节点,可递归调用 getChainFromEdge 于新边
  458. return {
  459. nodes: Array.from(chainNodes),
  460. edges: Array.from(chainEdges)
  461. };
  462. };
  463. // 边点击事件
  464. const onEdgeClick = (e) => {
  465. // 恢复上一个选中边样式(原有逻辑)
  466. if (previousEdge.value) {
  467. edges.value = edges.value.map(edge => {
  468. if (edge.id === previousEdge.value.id) {
  469. return {
  470. ...edge,
  471. class: edge.class ? edge.class.replace("selected", "").trim() : "",
  472. style: {
  473. ...edge.style,
  474. stroke: previousEdge.value.originalColor || DEFAULT_LINE_COLOR,
  475. strokeWidth: previousEdge.value.originalWidth || DEFAULT_LINE_WIDTH
  476. }
  477. };
  478. }
  479. return edge;
  480. });
  481. }
  482. // 设置当前选中边(原有逻辑)
  483. selectedEdge.value = { ...e.edge };
  484. selectedEdge.value.originalColor = e.edge.style?.stroke || DEFAULT_LINE_COLOR;
  485. selectedEdge.value.originalWidth = e.edge.style?.strokeWidth || DEFAULT_LINE_WIDTH;
  486. // 更新 selectedEdges.value(原有逻辑,支持 Ctrl 多选)
  487. if (e.event.ctrlKey) {
  488. const index = selectedEdges.value.findIndex((edge) => edge.id === e.edge.id);
  489. if (index === -1) {
  490. selectedEdges.value.push(e.edge);
  491. } else {
  492. selectedEdges.value.splice(index, 1); // 取消选择
  493. }
  494. } else {
  495. selectedEdges.value = [e.edge];
  496. selectedNodes.value = []; // 清空节点选择
  497. }
  498. // 新增:获取完整链条,并自动更新 selectedNodes 和 selectedEdges
  499. const chain = getChainFromEdge(e.edge);
  500. selectedNodes.value = chain.nodes; // 自动选中相关节点
  501. selectedEdges.value = [...selectedEdges.value, ...chain.edges.filter(ce => ce.id !== e.edge.id)]; // 添加其他相关边
  502. ElMessage.success('已自动选中边的相关节点、边和链条');
  503. // 可选:如果需要立即保存项目
  504. // saveproject();
  505. // 更新边样式(原有逻辑,稍作调整以支持多边选中)
  506. selectedEdges.value.forEach(selEdge => {
  507. const isDefault = selEdge.data?.type === "default";
  508. const newStyle = {
  509. ...selEdge.style,
  510. stroke: isDefault ? "#2267B1" : "rgba(255, 255, 0, 0.3)",
  511. strokeWidth: isDefault ? 2 : 6
  512. };
  513. edges.value = edges.value.map(edge => {
  514. if (edge.id === selEdge.id) {
  515. return {
  516. ...edge,
  517. class: edge.class ? `${edge.class} selected` : "selected",
  518. style: newStyle
  519. };
  520. }
  521. return edge;
  522. });
  523. });
  524. previousEdge.value = { ...selectedEdge.value, style: { ...selectedEdge.value.style } };
  525. };
  526. // 点击节点
  527. const onNodeClick = (event) => {
  528. if (event.event.ctrlKey) {
  529. // 支持多选节点
  530. const index = selectedNodes.value.findIndex((n) => n.id === event.node.id);
  531. if (index === -1) {
  532. selectedNodes.value.push(event.node);
  533. } else {
  534. selectedNodes.value.splice(index, 1); // 取消选择
  535. }
  536. } else {
  537. // 单选节点,清空边选择
  538. selectedNodes.value = [event.node];
  539. selectedEdges.value = [];
  540. }
  541. };
  542. const onNodeDoubleClick = (event) => {
  543. showPanel.value = true
  544. const pcId = event.node.data.pcId
  545. nextTick(() => {
  546. if (asideDataref.value) {
  547. asideDataref.value.getcomdata(pcId)
  548. if (props.jobId) {
  549. asideDataref.value.getresultData(props.jobId)
  550. }
  551. }
  552. })
  553. }
  554. const onEdgeDoubleClick = (event) => {
  555. selectedNode.value = event.node
  556. }
  557. // 连接开始:取消边选中
  558. onConnectStart(() => cleanEdgeSelect())
  559. // 方向计算工具函数
  560. const getDirectionFromPoint = (point) => {
  561. if (!isNaN(parseInt(point))) {
  562. const angle = ((parseInt(point) * 30) % 360 + 360) % 360
  563. if (angle >= 315 || angle < 45) return "right"
  564. if (angle >= 45 && angle < 135) return "bottom"
  565. if (angle >= 135 && angle < 225) return "left"
  566. if (angle >= 225 && angle < 315) return "top"
  567. }
  568. return point
  569. }
  570. const getOppositeDirection = (point) => {
  571. if (!isNaN(parseInt(point))) {
  572. const oppositePoint = (parseInt(point) + 6) % 12
  573. return getDirectionFromPoint(oppositePoint.toString())
  574. }
  575. return point === "top" ? "bottom" :
  576. point === "bottom" ? "top" :
  577. point === "left" ? "right" :
  578. point === "right" ? "left" : point
  579. }
  580. // 连接验证工具函数
  581. const validateConnection = (connection, sourceNode, targetNode) => {
  582. if (connection.source === connection.target) {
  583. ElMessage.warning("禁止节点自连")
  584. return false
  585. }
  586. if (sourceNode.data.comId === "3" && targetNode.data.comId === "3") {
  587. ElMessage.warning("禁止连接两个中间节点")
  588. return false
  589. }
  590. const hasDuplicate = edges.value.some(edge =>
  591. (edge.source === connection.source && edge.target === connection.target) ||
  592. (edge.source === connection.target && edge.target === connection.source)
  593. )
  594. if (hasDuplicate) {
  595. ElMessage.warning("禁止重复连接:起点和终点已连接")
  596. return false
  597. }
  598. const isDuplicateViaMiddle = edges.value.some(edge1 => {
  599. if (edge1.source === connection.source || edge1.target === connection.source) {
  600. const middleId = edge1.source === connection.source ? edge1.target : edge1.source
  601. const middleNode = vueFlowRef.value.getNode(middleId)
  602. if (!middleNode || middleNode.data.comId !== "3") return false
  603. return edges.value.some(edge2 =>
  604. (edge2.source === middleId && edge2.target === connection.target) ||
  605. (edge2.target === middleId && edge2.source === connection.target)
  606. )
  607. }
  608. return false
  609. })
  610. if (isDuplicateViaMiddle) {
  611. ElMessage.warning("禁止重复连接:这两个节点已经通过中间组件连接过了")
  612. return false
  613. }
  614. return true
  615. }
  616. const lineType = ref("default")
  617. onConnect(async (connection) => {
  618. const sourceNode = vueFlowRef.value.getNode(connection.source)
  619. const targetNode = vueFlowRef.value.getNode(connection.target)
  620. if (!validateConnection(connection, sourceNode, targetNode)) return
  621. const isCom3 = sourceNode.data.comId === "3" || targetNode.data.comId === "3"
  622. if (isCom3) {
  623. await handleDirectConnection(connection, sourceNode, targetNode)
  624. } else {
  625. await handleIndirectConnection(connection, sourceNode, targetNode)
  626. }
  627. debouncedSave();
  628. })
  629. const handleDirectConnection = async (connection, sourceNode, targetNode) => {
  630. const edgeId = `${lineType.value}-${connection.source}-${connection.sourceHandle}-${connection.target}-${connection.targetHandle}`
  631. connection.id = edgeId
  632. connection.type = "smoothstep"
  633. connection.color = lineColor.value
  634. connection.style = { strokeWidth: lineWidth.value, stroke: lineColor.value }
  635. const sameEdge = edges.value.find(e => e.id === edgeId)
  636. if (sameEdge) {
  637. await deleteflow(sameEdge.data?.wid)
  638. }
  639. addEdges(connection)
  640. selectedEdge.value = null
  641. const pcId1 = sourceNode.data.comId === "3" ? targetNode.data.pcId : sourceNode.data.pcId
  642. const npcId = sourceNode.data.comId === "3" ? sourceNode.data.pcId : targetNode.data.pcId
  643. try {
  644. const { pccId1 } = await createEdge(pid.value, 0, npcId, pcId1)
  645. updateEdgeData(connection.id, {
  646. pcid: pccId1,
  647. type: lineType.value,
  648. frompcId: sourceNode.data.pcId,
  649. topcId: targetNode.data.pcId
  650. })
  651. } catch (error) {
  652. console.error("保存流程失败:", error)
  653. }
  654. }
  655. const handleIndirectConnection = async (connection, sourceNode, targetNode) => {
  656. const pointNodeId = `point-${connection.source}-${connection.target}`
  657. const sourceCenterX = sourceNode.position.x + 60 / 2
  658. const sourceCenterY = sourceNode.position.y + 58 / 2
  659. const targetCenterX = targetNode.position.x + 60 / 2
  660. const targetCenterY = targetNode.position.y + 58 / 2
  661. const midX = (sourceCenterX + targetCenterX) / 2
  662. const midY = (sourceCenterY + targetCenterY) / 2
  663. const dxSource = sourceNode.position.x - midX
  664. const dySource = sourceNode.position.y - midY
  665. const dxTarget = targetNode.position.x - midX
  666. const dyTarget = targetNode.position.y - midY
  667. const isSourceHorizontal = Math.abs(dxSource) > Math.abs(dySource)
  668. const sourceDirection = isSourceHorizontal
  669. ? dxSource < 0 ? "left" : "right"
  670. : dySource < 0 ? "top" : "bottom"
  671. const isTargetHorizontal = Math.abs(dxTarget) > Math.abs(dyTarget)
  672. const targetDirection = isTargetHorizontal
  673. ? dxTarget < 0 ? "left" : "right"
  674. : dyTarget < 0 ? "top" : "bottom"
  675. // 添加 point-only 节点
  676. nodes.value.push({
  677. id: pointNodeId,
  678. type: "point-only",
  679. position: { x: midX, y: midY },
  680. data: {
  681. comId: "3",
  682. label: "节点",
  683. image: node,
  684. imageIdentify: "point-only"
  685. }
  686. })
  687. try {
  688. const { pcId: npcId, ser, idCode } = await createCom3(pid.value)
  689. updateNode(pointNodeId, node => ({
  690. ...node,
  691. data: {
  692. ...node.data,
  693. pcId: npcId,
  694. idCodeser: `${idCode}${ser}`
  695. }
  696. }))
  697. updateNodeInternals(pointNodeId)
  698. // edge1: source -> pointNode
  699. const edgeId1 = `${lineType.value}-${connection.source}-${connection.sourceHandle}-${pointNodeId}`
  700. const edge1 = {
  701. id: edgeId1,
  702. source: connection.source,
  703. sourceHandle: connection.sourceHandle,
  704. target: pointNodeId,
  705. targetHandle: `source-${sourceDirection}`,
  706. type: "smoothstep",
  707. color: lineColor.value,
  708. style: { strokeWidth: lineWidth.value, stroke: lineColor.value }
  709. }
  710. // edge2: pointNode -> target
  711. const edgeId2 = `${lineType.value}-${pointNodeId}-${connection.targetHandle}-${connection.target}`
  712. const edge2 = {
  713. id: edgeId2,
  714. source: pointNodeId,
  715. sourceHandle: `source-${targetDirection}`,
  716. target: connection.target,
  717. targetHandle: connection.targetHandle,
  718. type: "smoothstep",
  719. color: lineColor.value,
  720. style: { strokeWidth: lineWidth.value, stroke: lineColor.value }
  721. }
  722. addEdges([edge1, edge2])
  723. const { pccId1, pccId2 } = await createEdge(pid.value, 0, npcId, sourceNode.data.pcId, targetNode.data.pcId)
  724. updateEdgeData(edgeId1, {
  725. pcid: pccId1,
  726. type: lineType.value,
  727. frompcId: sourceNode.data.pcId,
  728. topcId: npcId
  729. })
  730. updateEdgeData(edgeId2, {
  731. pcid: pccId2,
  732. type: lineType.value,
  733. frompcId: npcId,
  734. topcId: targetNode.data.pcId
  735. })
  736. } catch (err) {
  737. console.error("中间点处理失败:", err)
  738. // 回滚:移除添加的节点
  739. nodes.value = nodes.value.filter(n => n.id !== pointNodeId)
  740. }
  741. }
  742. const createEdge = async (pid, type, npcId, pcId1, pcId2 = "") => {
  743. const params = {
  744. transCode: "ES0006",
  745. pid: pid || "",
  746. type: type || 0,
  747. npcId: npcId || "",
  748. pcId1: pcId1 || "",
  749. pcId2: pcId2 || ""
  750. }
  751. try {
  752. const res = await request(params)
  753. return { pccId1: res.pccId1, pccId2: res.pccId2 }
  754. } catch (err) {
  755. ElMessage.error(err.returnMsg || "保存流程失败")
  756. throw err
  757. }
  758. }
  759. const createCom3 = async (pid) => {
  760. try {
  761. const params = { transCode: "ES0004", pid: pid || "", comId: "3" }
  762. const res = await request(params)
  763. return { pcId: res.pcId, ser: res.ser, idCode: res.idCode }
  764. } catch (err) {
  765. ElMessage.error(err.returnMsg || "创建中间组件失败")
  766. throw err
  767. }
  768. }
  769. const flowInit = async () => {
  770. try {
  771. const activeProject = projectStore.getActiveProject()
  772. if (!activeProject) {
  773. throw new Error('当前没有激活的项目')
  774. }
  775. let flow
  776. // 检查 activeProject.flow 的类型
  777. if (typeof activeProject.flow === 'string') {
  778. // 如果是字符串,尝试解析
  779. try {
  780. flow = JSON.parse(activeProject.flow || '{"nodes":[], "edges":[] }')
  781. } catch (error) {
  782. console.error('解析 flow 数据失败:', activeProject.flow, error)
  783. flow = { nodes: [], edges: [] }
  784. }
  785. } else if (typeof activeProject.flow === 'object' && activeProject.flow !== null) {
  786. // 如果已经是对象,直接使用
  787. flow = activeProject.flow
  788. } else {
  789. // 其他情况使用默认值
  790. flow = { nodes: [], edges: [] }
  791. }
  792. const updatedNodes = await Promise.all(
  793. flow.nodes.map(async (node) => {
  794. if (node.data?.imageIdentify) {
  795. if (node.data.imageIdentify === 'point-only') {
  796. return { ...node, data: { ...node.data, image: nodePoint } }
  797. }
  798. try {
  799. const imageData = await getImageBase64(node.data.imageIdentify)
  800. return { ...node, data: { ...node.data, image: imageData || tempic } }
  801. } catch (err) {
  802. console.error(`获取图片 ${node.data.imageIdentify} 失败:`, err.message)
  803. return { ...node, data: { ...node.data, image: tempic } }
  804. }
  805. }
  806. return node
  807. })
  808. )
  809. nodes.value = updatedNodes
  810. edges.value = flow.edges
  811. prevNodes = [...updatedNodes]
  812. prevEdges = [...flow.edges]
  813. isInitializing.value = true
  814. } catch (error) {
  815. console.error('加载项目失败:', error)
  816. ElMessage.error('加载项目失败,请检查项目数据')
  817. }
  818. }
  819. const cleanEdgeSelect = () => {
  820. if (selectedEdge.value) {
  821. edges.value = edges.value.map(edge => {
  822. if (edge.id === selectedEdge.value.id) {
  823. return {
  824. ...edge,
  825. class: edge.class ? edge.class.replace("selected", "").trim() : "",
  826. style: {
  827. ...edge.style,
  828. stroke: selectedEdge.value.originalColor || DEFAULT_LINE_COLOR,
  829. strokeWidth: selectedEdge.value.originalWidth || DEFAULT_LINE_WIDTH
  830. }
  831. };
  832. }
  833. return edge;
  834. });
  835. selectedEdge.value = null;
  836. previousEdge.value = null;
  837. }
  838. };
  839. const closePanel = () => {
  840. nextTick(() => {
  841. showPanel.value = false
  842. })
  843. }
  844. // 暴露方法
  845. defineExpose({
  846. resetTransform,
  847. toggleDarkMode,
  848. saveproject,
  849. removeNode,
  850. removeEdge,
  851. confirmDelete,
  852. asideDataref,
  853. copyNodes: () => copyNodes(selectedNodes.value, selectedEdges.value, edges.value, nodes.value, pid.value),
  854. cutNodes: () => cutNodes(selectedNodes.value, selectedEdges.value, nodes.value, edges.value, saveproject, removeNodes, removeEdges, clearSelection)
  855. .finally(() => {
  856. isCutting.value = false;
  857. }),
  858. pasteNodes: () => pasteNodes(pid.value, nodes.value, edges.value, saveproject, createEdge, addNodes, addEdges, updateNodeInternals),
  859. flowInit,
  860. refreshFlow: flowInit
  861. })
  862. </script>
  863. <style scoped>
  864. .vue-flow__edge:focus .vue-flow__edge-path,
  865. .vue-flow__edge:focus-visible .vue-flow__edge-path {
  866. stroke: #555 !important;
  867. }
  868. .vue-flow__edge {
  869. text-align: left;
  870. }
  871. .vue-flow__edge-text {
  872. transform: translateY(-10px);
  873. background: transparent !important;
  874. font-size: 8px;
  875. font-family: "Microsoft YaHei";
  876. color: #333333;
  877. }
  878. .vue-flow__edge-textbg {
  879. fill: transparent !important;
  880. }
  881. .vue-flow__node-default.selectable:hover,
  882. .vue-flow__node-input.selectable:hover,
  883. .vue-flow__node-output.selectable:hover {
  884. box-shadow: none;
  885. }
  886. .remove {
  887. background: #fff;
  888. color: #666;
  889. margin: 0 10px;
  890. font-size: 12px;
  891. }
  892. .vue-flow__node-default,
  893. .vue-flow__node-input,
  894. .vue-flow__node-output {
  895. border: none;
  896. background-color: rgba(0, 0, 0, 0);
  897. }
  898. .node-content {
  899. cursor: move;
  900. }
  901. .vue-flow__node {
  902. cursor: move;
  903. }
  904. /* 禁用文本选中效果 */
  905. .left_main * {
  906. -webkit-user-select: none;
  907. -moz-user-select: none;
  908. -ms-user-select: none;
  909. user-select: none;
  910. }
  911. .lableaniu {
  912. font-size: 12px;
  913. background-color: #ddd;
  914. padding: 4px 16px;
  915. margin-left: 5px;
  916. margin-top: 0px;
  917. border-radius: 1px;
  918. }
  919. .vue-flow__controls-button svg {
  920. max-width: 16px;
  921. max-height: 16px;
  922. }
  923. .field {
  924. display: flex;
  925. }
  926. </style>
  927. <!-- 用于侧栏pane -->
  928. <style scoped>
  929. .flow-panel {
  930. font-size: 14px;
  931. }
  932. </style>