index.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932
  1. <template>
  2. <splitpanes class="default-theme">
  3. <pane min-size="50" size="100" max-size="100">
  4. <VueFlow
  5. ref="vueFlowRef"
  6. v-model:nodes="nodes"
  7. v-model:edges="edges"
  8. :class="{ dark }"
  9. class="basic-flow"
  10. :default-viewport="{ zoom: 1.5 }"
  11. :min-zoom="0.2"
  12. :max-zoom="2.5"
  13. @drop="customOnDrop"
  14. @node-contextmenu="onNodeContextMenu"
  15. @dragover="onDragOver"
  16. @dragleave="onDragLeave"
  17. @edge-click="onEdgeClick"
  18. @node-double-click="onNodeDoubleClick"
  19. @node-click="onNodeClick"
  20. @edge-double-click="onEdgeDoubleClick"
  21. @node-drag="onNodeDrag"
  22. >
  23. <!-- 自定义节点类型为default的节点 -->
  24. <template #node-default="props">
  25. <defaultnode :node="props" :key="props.data.idCodeser || props.id" />
  26. </template>
  27. <template #node-point-only="props">
  28. <PointOnlyNode
  29. :node="props"
  30. :key="props.data.idCodeser || props.id"
  31. />
  32. </template>
  33. <Background pattern-color="#aaa" :gap="16" />
  34. <!-- <Controls position="top-left">
  35. <ControlButton title="重置" @click="resetTransform">
  36. <changebak name="reset" />
  37. </ControlButton>
  38. <ControlButton title="背景切换" @click="toggleDarkMode">
  39. <changebak v-if="dark" name="sun" />
  40. <changebak v-else name="moon" />
  41. </ControlButton>
  42. <ControlButton title="保存" @click="saveproject">
  43. <el-icon :color="iconcolor"><UploadFilled /></el-icon>
  44. </ControlButton>
  45. <ControlButton title="删除节点" @click="removeNode()">
  46. <el-icon :color="iconcolor"><DocumentDelete /></el-icon>
  47. </ControlButton>
  48. <ControlButton title="删除线" @click="removeEdge()">
  49. <el-icon :color="iconcolor"><Crop /></el-icon>
  50. </ControlButton>
  51. <ControlButton title="清空全部" @click="confirmDelete()">
  52. <el-icon :color="iconcolor"><DeleteFilled /></el-icon>
  53. </ControlButton>
  54. </Controls> -->
  55. </VueFlow>
  56. </pane>
  57. <pane
  58. v-if="showPanel"
  59. min-size="20"
  60. :size="30"
  61. max-size="50"
  62. class="flow-panel"
  63. >
  64. <asideData ref="asideDataref" @close="closePanel" />
  65. </pane>
  66. </splitpanes>
  67. </template>
  68. <script setup>
  69. import { VueFlow, Panel, useVueFlow, MarkerType } from "@vue-flow/core"
  70. import {
  71. ElMessage,
  72. ElButton,
  73. ElDialog,
  74. ElSelect,
  75. ElMessageBox
  76. } from "element-plus"
  77. import {
  78. DocumentDelete,
  79. Delete,
  80. UploadFilled,
  81. Histogram,
  82. DeleteFilled,
  83. Crop,
  84. Close
  85. } from "@element-plus/icons-vue"
  86. import { request, uploadFile, getImageBase64 } from "@/utils/request"
  87. import { Background } from "@vue-flow/background"
  88. import { ControlButton, Controls } from "@vue-flow/controls"
  89. import "./main.css" //重置样式
  90. import defaultnode from "./defaultnode.vue"
  91. import PointOnlyNode from "./pointonlynode.vue"
  92. import useDragAndDrop from "./useDnD"
  93. import changebak from "./changebak.vue"
  94. import { useProjectStore } from "@/store/project"
  95. import emitter from "@/utils/emitter"
  96. import { onMounted } from "vue"
  97. import { Splitpanes, Pane } from "splitpanes"
  98. import "splitpanes/dist/splitpanes.css"
  99. import asideData from "./aside/asideData.vue"
  100. import node from "@/assets/img/node.png"
  101. import nodePoint from "@/assets/img/node.png"
  102. import { debounce } from 'lodash-es'
  103. import tempic from "@/assets/img/temp.png"
  104. // 获取路由实例
  105. const router = useRouter()
  106. const {
  107. onInit,
  108. onNodeDragStop,
  109. onConnect,
  110. addEdges,
  111. setViewport,
  112. toObject,
  113. addNodes,
  114. updateEdgeData,
  115. onConnectStart,
  116. updateNode,
  117. updateNodeInternals,
  118. onNodeDrag
  119. } = useVueFlow()
  120. const { onDragOver, onDrop, onDragLeave, isDragOver } = useDragAndDrop()
  121. const dark = ref(false)
  122. let vueFlowRef = ref()
  123. let iconcolor = ref("#000")
  124. const GRID_SIZE = 2;
  125. const edges = ref([])
  126. const nodes = ref([])
  127. let mergedObj = ref("")
  128. // 选中节点
  129. let noid = ref([])
  130. let Edgeid = ref() //选中线段id
  131. let seledge = ref(null)
  132. let previousEdge = null // 用于保存上一个选中的边缘
  133. // 连线颜色
  134. let linecolor = ref("#2267B1")
  135. // 线宽
  136. let linewidth = ref(1)
  137. let midNodeCounter = 0 //储存中间节点的序号
  138. const projectStore = useProjectStore()
  139. let pid = computed(() => projectStore.pid || "")
  140. const showPanel = ref(false)
  141. const asideDataref = ref()
  142. const props = defineProps({
  143. jobId: {
  144. type: String,
  145. default: ""
  146. }
  147. })
  148. const debouncedSave = debounce(() => {
  149. saveproject()
  150. }, 500) // 500ms 防抖
  151. // 处理节点拖动时的网格吸附
  152. onNodeDrag(({ node }) => {
  153. if (!node) {
  154. console.error('onNodeDrag 未收到节点数据');
  155. return;
  156. }
  157. const step = node.type === 'point-only' ? 1 : GRID_SIZE;
  158. const newPosition = {
  159. x: Math.round(node.position.x / step) * step,
  160. y: Math.round(node.position.y / step) * step,
  161. };
  162. updateNode(node.id, { position: newPosition });
  163. });
  164. // 旧数据存储
  165. let prevNodes = [...nodes.value]
  166. let prevEdges = [...edges.value]
  167. // 监听节点变化
  168. watch(
  169. nodes,
  170. (newNodes) => {
  171. const deletedNodes = prevNodes.filter(
  172. (node) => !newNodes.some((n) => n.id === node.id)
  173. )
  174. const addedNodes = newNodes.filter(
  175. (node) => !prevNodes.some((n) => n.id === node.id)
  176. )
  177. if (deletedNodes.length > 0) {
  178. console.log("Deleted Nodes:", deletedNodes)
  179. deletedNodes.forEach((node) => {
  180. if (node.data?.pcId) {
  181. comdelete(node.data.pcId)
  182. }
  183. })
  184. }
  185. if (addedNodes.length > 0) {
  186. // console.log('Added Nodes:', addedNodes);
  187. }
  188. // 更新快照
  189. prevNodes = [...newNodes]
  190. debouncedSave();
  191. },
  192. { deep: true }
  193. )
  194. // 监听连线变化
  195. watch(
  196. edges,
  197. (newEdges) => {
  198. const deletedEdges = prevEdges.filter(
  199. (edge) => !newEdges.some((e) => e.id === edge.id)
  200. )
  201. const addedEdges = newEdges.filter(
  202. (edge) => !prevEdges.some((e) => e.id === edge.id)
  203. )
  204. if (deletedEdges.length > 0) {
  205. console.log("Deleted Edges:", deletedEdges)
  206. deletedEdges.forEach((edge) => {
  207. const sourceNode = prevNodes.find((n) => n.id === edge.source)
  208. const targetNode = prevNodes.find((n) => n.id === edge.target)
  209. if (!sourceNode || !targetNode) return
  210. let npcId = ""
  211. let pcId = ""
  212. // 判断 source 或 target 是否为中间点 comId === '3'
  213. if (sourceNode.data?.comId === "3") {
  214. npcId = sourceNode.data.pcId
  215. pcId = targetNode.data?.pcId
  216. } else if (targetNode.data?.comId === "3") {
  217. npcId = targetNode.data.pcId
  218. pcId = sourceNode.data?.pcId
  219. }
  220. if (npcId && pcId) {
  221. comlinedelete(npcId, pcId)
  222. }
  223. })
  224. }
  225. if (addedEdges.length > 0) {
  226. }
  227. // 更新快照
  228. prevEdges = [...newEdges]
  229. debouncedSave();
  230. },
  231. { deep: true }
  232. )
  233. const resetTransform = () => {
  234. setViewport({ x: 0, y: 0, zoom: 1 })
  235. }
  236. const toggleDarkMode = () => {
  237. dark.value = !dark.value
  238. if (dark.value) {
  239. iconcolor.value = "#fff"
  240. } else {
  241. iconcolor.value = "#000"
  242. }
  243. }
  244. const saveproject = async () => {
  245. try {
  246. let obj = {
  247. nodes: toObject().nodes,
  248. edges: toObject().edges
  249. }
  250. // 移除每个节点的 image 字段,保留 imageIdentify
  251. obj.nodes = obj.nodes.map(node => ({
  252. ...node,
  253. data: {
  254. ...node.data,
  255. image: undefined // 移除 image 字段
  256. }
  257. }))
  258. mergedObj.value = JSON.stringify(obj)
  259. const params = {
  260. transCode: "ES0002",
  261. pid: pid.value,
  262. flow: mergedObj.value,
  263. name: projectStore.projectInfo.name || "",
  264. remark: projectStore.projectInfo.remark || "",
  265. keywords: projectStore.projectInfo.keywords || ""
  266. }
  267. await request(params)
  268. projectStore.setProjectInfo({
  269. ...projectStore.projectInfo,
  270. flow: mergedObj.value || ""
  271. })
  272. console.log('保存成功')
  273. } catch (error) {
  274. console.error("保存失败:", error)
  275. ElMessage.error("保存失败,请稍后重试")
  276. }
  277. }
  278. const removeNode = () => {
  279. if (!noid.value) {
  280. console.warn("未选中任何节点")
  281. return
  282. }
  283. const nodeIdToRemove = noid.value.id
  284. vueFlowRef.value.removeNodes(noid.value.id)
  285. // 清空当前选中
  286. noid.value = null
  287. saveproject()
  288. }
  289. const removeEdge = () => {
  290. if (!seledge.value) {
  291. ElMessage.warning("请先选择要删除的连线")
  292. return
  293. }
  294. vueFlowRef.value.removeEdges(seledge.value) // 移除选中的边
  295. seledge.value = null // 清空选中状态
  296. saveproject()
  297. }
  298. const confirmDelete = () => {
  299. ElMessageBox.confirm("确定要删除所有节点和连线吗?此操作不可恢复!", "警告", {
  300. confirmButtonText: "确认",
  301. cancelButtonText: "取消",
  302. type: "warning"
  303. })
  304. .then(() => {
  305. vueFlowRef.value.removeNodes(nodes.value)
  306. vueFlowRef.value.removeEdges(edges.value)
  307. ElMessage.success("所有节点和连线已删除")
  308. saveproject()
  309. })
  310. .catch(() => {
  311. ElMessage.info("已取消删除")
  312. })
  313. }
  314. const comdelete = (pcId) => {
  315. const params = {
  316. transCode: "ES0005",
  317. pcId: pcId
  318. }
  319. request(params)
  320. .then((res) => {
  321. console.log("组件删除成功")
  322. })
  323. .catch((err) => {
  324. ElMessage.error(err.returnMsg)
  325. })
  326. }
  327. const comlinedelete = (npcId, pcId) => {
  328. const params = {
  329. transCode: "ES0007",
  330. npcId: npcId,
  331. pcId: pcId,
  332. type: 0
  333. }
  334. request(params)
  335. .then((res) => {
  336. console.log("组件连线删除成功")
  337. })
  338. .catch((err) => {
  339. ElMessage.error(err.returnMsg)
  340. })
  341. }
  342. const customOnDrop = async (event) => {
  343. console.log("自定义拖放事件", event)
  344. await onDrop(event) // 等待 onDrop 执行完成
  345. saveproject() // onDrop 执行完之后再保存项目
  346. }
  347. const onNodeContextMenu = (e) => {
  348. console.log("右键点击", e)
  349. }
  350. const onEdgeClick = (e) => {
  351. console.log("Edge Click", e.edge)
  352. // console.log('所有线段:', edges.value);
  353. // 如果已经有选中的边缘
  354. if (seledge.value) {
  355. // 恢复上一个选中边缘的样式
  356. if (previousEdge) {
  357. previousEdge.style = {
  358. ...previousEdge.style,
  359. stroke: previousEdge.originalColor, // 恢复原始颜色
  360. strokeWidth: previousEdge.originalWidth // 恢复原始宽度
  361. }
  362. }
  363. }
  364. // 保存当前点击的边缘为选中边缘
  365. Edgeid.value = e.edge.id
  366. seledge.value = e.edge
  367. // 暂时更改当前选中边缘的样式
  368. seledge.value.originalColor = seledge.value.style.stroke // 保存当前边缘的原始颜色
  369. seledge.value.originalWidth = seledge.value.style.strokeWidth // 保存当前边缘的原始宽度
  370. const isdefault = e.edge.data.type === "default" // 判断是否为默认线
  371. seledge.value.style = {
  372. ...seledge.value.style,
  373. stroke: isdefault ? "#2267B1" : "rgba(255, 255, 0, 0.3)", // 设置选中边缘的颜色
  374. strokeWidth: isdefault ? 2 : 6 // 设置选中边缘的宽度
  375. }
  376. // 保存当前选中的边缘作为上一个选中边缘
  377. previousEdge = seledge.value
  378. }
  379. const onNodeClick = (event) => {
  380. noid.value = event.node
  381. }
  382. const onNodeDoubleClick = (event) => {
  383. console.log("节点被双击:", event)
  384. showPanel.value = true
  385. const pcId = event.node.data.pcId
  386. nextTick(() => {
  387. console.log("diaoy", pcId)
  388. if (asideDataref.value) {
  389. asideDataref.value.getcomdata(pcId)
  390. if (props.jobId){
  391. asideDataref.value.getresultData(props.jobId)
  392. }
  393. }
  394. })
  395. }
  396. const onEdgeDoubleClick = (event) => {
  397. console.log("连线双击:", event)
  398. noid.value = event.node
  399. }
  400. // 监听连接开始,提前取消选中的线段
  401. onConnectStart(() => {
  402. cleanEdgeselect()
  403. })
  404. // 线的类型 default
  405. let lineType = ref("default")
  406. onConnect(async (connection) => {
  407. console.log("线连接", connection)
  408. if (connection.source === connection.target) {
  409. console.warn("禁止节点自连")
  410. return
  411. }
  412. const sourceNode = vueFlowRef.value.getNode(connection.source)
  413. const targetNode = vueFlowRef.value.getNode(connection.target)
  414. if (sourceNode.data.comId === "3" && targetNode.data.comId === "3") {
  415. console.warn("禁止连接两个中间节点")
  416. return
  417. }
  418. // 验证 Handle ID 格式(source-${point} 或 source-${direction})
  419. const validHandleRegex = /^source-(\d+|[a-z]+)$/
  420. if (!validHandleRegex.test(connection.sourceHandle) || !validHandleRegex.test(connection.targetHandle)) {
  421. console.warn("无效的 Handle ID:", connection.sourceHandle, connection.targetHandle)
  422. return
  423. }
  424. const hasDuplicateEdge = edges.value.some(
  425. (edge) =>
  426. (edge.source === connection.source && edge.target === connection.target) ||
  427. (edge.source === connection.target && edge.target === connection.source)
  428. )
  429. if (hasDuplicateEdge) {
  430. console.warn("禁止重复连接:起点和终点已连接")
  431. return
  432. }
  433. const isDuplicate = edges.value.some((edge1) => {
  434. if (
  435. edge1.source === connection.source ||
  436. edge1.target === connection.source
  437. ) {
  438. const middleNodeId =
  439. edge1.source === connection.source ? edge1.target : edge1.source
  440. const middleNode = vueFlowRef.value.getNode(middleNodeId)
  441. if (!middleNode || middleNode.data.comId !== "3") return false
  442. return edges.value.some(
  443. (edge2) =>
  444. (edge2.source === middleNodeId && edge2.target === connection.target) ||
  445. (edge2.target === middleNodeId && edge2.source === connection.target)
  446. )
  447. }
  448. return false
  449. })
  450. if (isDuplicate) {
  451. console.warn("禁止重复连接:这两个节点已经通过中间组件连接过了")
  452. return
  453. }
  454. const isCom3 = sourceNode.data.comId === "3" || targetNode.data.comId === "3"
  455. if (isCom3) {
  456. const edgeId = `${lineType.value}-${connection.source}-${connection.sourceHandle}-${connection.target}-${connection.targetHandle}`
  457. connection.id = edgeId
  458. connection.type = "smoothstep"
  459. connection.color = linecolor.value
  460. connection.style = { strokeWidth: linewidth.value, stroke: linecolor.value }
  461. const sameEdge = edges.value.find((edge) => edge.id === edgeId)
  462. if (sameEdge) {
  463. deleteflow(sameEdge.data?.wid)
  464. }
  465. addEdges(connection)
  466. seledge.value = null
  467. const pcId1 =
  468. sourceNode.data.comId === "3" ? targetNode.data.pcId : sourceNode.data.pcId
  469. const npcId =
  470. sourceNode.data.comId === "3" ? sourceNode.data.pcId : targetNode.data.pcId
  471. try {
  472. const { pccId1 } = await createEdge(pid.value, 0, npcId, pcId1)
  473. updateEdgeData(connection.id, {
  474. pcid: pccId1,
  475. type: lineType.value,
  476. frompcId: sourceNode.data.pcId,
  477. topcId: targetNode.data.pcId
  478. })
  479. } catch (error) {
  480. console.error("保存流程失败:", error)
  481. }
  482. } else {
  483. const pointNodeId = `point-${connection.source}-${connection.target}`
  484. const sourceCenterX = sourceNode.position.x + 60 / 2
  485. const sourceCenterY = sourceNode.position.y + 58 / 2
  486. const targetCenterX = targetNode.position.x + 60 / 2
  487. const targetCenterY = targetNode.position.y + 58 / 2
  488. const midX = (sourceCenterX + targetCenterX) / 2
  489. const midY = (sourceCenterY + targetCenterY) / 2
  490. // 使用 source 和 target 的点位方向
  491. const sourceHandlePoint = connection.sourceHandle.replace("source-", "")
  492. const targetHandlePoint = connection.targetHandle.replace("source-", "")
  493. const sourceDirection = getDirectionFromPoint(sourceHandlePoint)
  494. const targetDirection = getDirectionFromPoint(targetHandlePoint)
  495. // point-only 节点的 Handle 使用对侧方向(source 到 point)和服务方向(point 到 target)
  496. const pointTargetHandle = `source-${getOppositeDirection(sourceHandlePoint)}`
  497. const pointSourceHandle = `source-${getDirectionFromPoint(targetHandlePoint)}`
  498. nodes.value.push({
  499. id: pointNodeId,
  500. type: "point-only",
  501. position: { x: midX, y: midY },
  502. data: {
  503. comId: "3",
  504. label: "节点",
  505. image: node,
  506. imageIdentify: "66"
  507. }
  508. })
  509. try {
  510. const { pcId: npcId, ser, idCode } = await createCom3(pid.value)
  511. const pointNode = nodes.value.find((n) => n.id === pointNodeId)
  512. updateNode(pointNodeId, (node) => ({
  513. ...node,
  514. data: {
  515. ...node.data,
  516. pcId: npcId,
  517. idCodeser: `${idCode}${ser}`
  518. }
  519. }))
  520. updateNodeInternals(pointNodeId)
  521. const edgeId1 = `${lineType.value}-${connection.source}-${connection.sourceHandle}-${pointNodeId}`
  522. const edge1 = {
  523. id: edgeId1,
  524. source: connection.source,
  525. sourceHandle: connection.sourceHandle,
  526. target: pointNodeId,
  527. targetHandle: pointTargetHandle,
  528. type: "smoothstep",
  529. color: linecolor.value,
  530. style: { strokeWidth: linewidth.value, stroke: linecolor.value }
  531. }
  532. const edgeId2 = `${lineType.value}-${pointNodeId}-${connection.targetHandle}-${connection.target}`
  533. const edge2 = {
  534. id: edgeId2,
  535. source: pointNodeId,
  536. sourceHandle: pointSourceHandle,
  537. target: connection.target,
  538. targetHandle: connection.targetHandle,
  539. type: "smoothstep",
  540. color: linecolor.value,
  541. style: { strokeWidth: linewidth.value, stroke: linecolor.value }
  542. }
  543. console.log("Edge1:", { id: edgeId1, sourceHandle: connection.sourceHandle, targetHandle: pointTargetHandle })
  544. console.log("Edge2:", { id: edgeId2, sourceHandle: pointSourceHandle, targetHandle: connection.targetHandle })
  545. addEdges([edge1, edge2])
  546. const { pccId1, pccId2 } = await createEdge(
  547. pid.value,
  548. 0,
  549. npcId,
  550. sourceNode.data.pcId,
  551. targetNode.data.pcId
  552. )
  553. updateEdgeData(edgeId1, {
  554. pcid: pccId1,
  555. type: lineType.value,
  556. frompcId: sourceNode.data.pcId,
  557. topcId: npcId
  558. })
  559. updateEdgeData(edgeId2, {
  560. pcid: pccId2,
  561. type: lineType.value,
  562. frompcId: npcId,
  563. topcId: targetNode.data.pcId
  564. })
  565. } catch (err) {
  566. console.error("中间点处理失败:", err)
  567. }
  568. }
  569. saveproject()
  570. })
  571. // 将点位转换为方向(与 defaultnode.vue 的角度映射一致)
  572. function getDirectionFromPoint(point) {
  573. if (!isNaN(parseInt(point))) {
  574. const angle = ((parseInt(point) * 30) % 360 + 360) % 360 // 移除 -90° 偏移,与 defaultnode.vue 同步
  575. if (angle >= 315 || angle < 45) return "right" // 315° to 45° -> 3
  576. if (angle >= 45 && angle < 135) return "bottom" // 45° to 135° -> 6
  577. if (angle >= 135 && angle < 225) return "left" // 135° to 225° -> 9
  578. if (angle >= 225 && angle < 315) return "top" // 225° to 315° -> 0
  579. }
  580. return point // 直接使用字符串(如果 point 不是数字)
  581. }
  582. // 计算 point-only 节点的 Handle 方向,匹配 source 和 target 的点位
  583. function getOppositeDirection(point) {
  584. if (!isNaN(parseInt(point))) {
  585. const oppositePoint = (parseInt(point) + 6) % 12 // 对侧点位(180° 偏移)
  586. return getDirectionFromPoint(oppositePoint.toString())
  587. }
  588. // 如果是自定义方向,返回对侧
  589. return point === "top" ? "bottom" :
  590. point === "bottom" ? "top" :
  591. point === "left" ? "right" :
  592. point === "right" ? "left" : point
  593. }
  594. const createEdge = async (pid, type, npcId, pcId1, pcId2) => {
  595. const params = {
  596. transCode: "ES0006",
  597. pid: pid || "",
  598. type: type || 0,
  599. npcId: npcId || "",
  600. pcId1: pcId1 || "",
  601. pcId2: pcId2 || ""
  602. }
  603. try {
  604. const res = await request(params)
  605. return {
  606. pccId1: res.pccId1,
  607. pccId2: res.pccId2
  608. } // 返回包含 pccId1 和 pccId2 的对象
  609. } catch (err) {
  610. // 处理错误
  611. ElMessage.error(err.returnMsg || "保存流程失败")
  612. throw err // 可以选择重新抛出错误以便调用者处理
  613. }
  614. }
  615. // 创建特殊节点
  616. const createCom3 = async (pid) => {
  617. const params = {
  618. transCode: "ES0004",
  619. pid: pid || "",
  620. comId: "3" // 节点ID
  621. }
  622. try {
  623. const res = await request(params)
  624. return {
  625. pcId: res.pcId,
  626. ser: res.ser,
  627. idCode: res.idCode
  628. }
  629. } catch (err) {
  630. ElMessage.error(err.returnMsg)
  631. }
  632. }
  633. const flowInit = async () => {
  634. try {
  635. const flow = JSON.parse(projectStore.projectInfo.flow || '{"nodes":[],"edges":[]}')
  636. if (!flow.nodes || !flow.edges) {
  637. throw new Error("无效的 flow 数据")
  638. }
  639. const updatedNodes = await Promise.all(
  640. flow.nodes.map(async (node) => {
  641. if (node.data?.imageIdentify) {
  642. if (node.data.imageIdentify === "point-only") {
  643. // point-only 节点使用本地图片
  644. return {
  645. ...node,
  646. data: {
  647. ...node.data,
  648. image: nodePoint
  649. }
  650. }
  651. }
  652. // 普通节点调用接口获取图片
  653. try {
  654. const imageData = await getImageBase64(node.data.imageIdentify)
  655. return {
  656. ...node,
  657. data: {
  658. ...node.data,
  659. image: imageData || tempic
  660. }
  661. }
  662. } catch (err) {
  663. console.error(`获取图片 ${node.data.imageIdentify} 失败:`, err.message)
  664. return {
  665. ...node,
  666. data: {
  667. ...node.data,
  668. image: tempic
  669. }
  670. }
  671. }
  672. }
  673. return node // 如果没有 imageIdentify,直接返回
  674. })
  675. )
  676. nodes.value = updatedNodes
  677. edges.value = flow.edges
  678. console.log("加载项目成功")
  679. } catch (error) {
  680. console.error("加载项目失败:", error)
  681. }
  682. }
  683. const cleanEdgeselect = () => {
  684. if (seledge.value) {
  685. // 恢复选中边缘的原始样式
  686. seledge.value.style = {
  687. ...seledge.value.style,
  688. stroke: seledge.value.originalColor,
  689. strokeWidth: previousEdge?.originalWidth || 1 // 恢复原始宽度
  690. }
  691. // 清空选中的边缘
  692. seledge.value = null
  693. Edgeid.value = null
  694. previousEdge = null
  695. }
  696. }
  697. onMounted(() => {
  698. flowInit()
  699. // 点击其他区域取消线段选中
  700. if (vueFlowRef.value) {
  701. vueFlowRef.value.$el.addEventListener("click", (event) => {
  702. // 确保点击的不是边缘
  703. if (seledge.value && !event.target.closest(".vue-flow__edge")) {
  704. cleanEdgeselect()
  705. }
  706. })
  707. }
  708. })
  709. const closePanel = () => {
  710. setTimeout(() => {
  711. showPanel.value = false
  712. }, 100) // 延迟 100ms 销毁组件,避免 Splitpanes 销毁错误
  713. }
  714. // 路由切换前保存
  715. // router.beforeEach(async (to, from, next) => {
  716. // if (to.path !== from.path) {
  717. // try {
  718. // await saveproject() // 直接调用 saveproject
  719. // next()
  720. // } catch (error) {
  721. // console.error("路由切换保存失败:", error)
  722. // next() // 即使保存失败也允许切换路由
  723. // }
  724. // } else {
  725. // next()
  726. // }
  727. // })
  728. // // 组件卸载前保存
  729. // onBeforeUnmount(async () => {
  730. // await saveproject() // 直接调用 saveproject
  731. // })
  732. defineExpose({
  733. resetTransform,
  734. toggleDarkMode,
  735. saveproject,
  736. removeNode,
  737. removeEdge,
  738. confirmDelete,
  739. asideDataref
  740. })
  741. </script>
  742. <style scoped>
  743. /* .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 {
  744. stroke: #555 !important;
  745. } */
  746. .vue-flow__edge:focus .vue-flow__edge-path,
  747. .vue-flow__edge:focus-visible .vue-flow__edge-path {
  748. stroke: #555 !important;
  749. }
  750. .vue-flow__edge {
  751. text-align: left;
  752. /* 设置edges的左对齐 */
  753. }
  754. .vue-flow__edge-text {
  755. transform: translateY(-10px); /* 将 label 向上偏移 */
  756. background: transparent !important;
  757. font-size: 8px;
  758. font-family: "Microsoft YaHei";
  759. color: #333333;
  760. }
  761. .vue-flow__edge-textbg {
  762. fill: transparent !important; /* 将背景设置为透明 */
  763. }
  764. .vue-flow__node-default.selectable:hover,
  765. .vue-flow__node-input.selectable:hover,
  766. .vue-flow__node-output.selectable:hover {
  767. box-shadow: none;
  768. }
  769. .remove {
  770. background: #fff;
  771. color: #666;
  772. margin: 0 10px;
  773. font-size: 12px;
  774. }
  775. .vue-flow__node-default,
  776. .vue-flow__node-input,
  777. .vue-flow__node-output {
  778. /* width: auto !important; */
  779. border: none;
  780. background-color: rgba(0, 0, 0, 0);
  781. }
  782. .node-content {
  783. cursor: move; /* 更改鼠标光标表示可拖动 */
  784. }
  785. .vue-flow__node {
  786. cursor: move;
  787. }
  788. /* 禁用文本选中效果 */
  789. .left_main * {
  790. -webkit-user-select: none; /* Safari */
  791. -moz-user-select: none; /* Firefox */
  792. -ms-user-select: none; /* IE10+/Edge */
  793. user-select: none; /* Standard syntax */
  794. }
  795. .lableaniu {
  796. font-size: 12px;
  797. background-color: #ddd;
  798. padding: 4px 16px;
  799. /* margin-top: -17px; */
  800. margin-left: 5px;
  801. margin-top: 0px;
  802. border-radius: 1px;
  803. }
  804. .vue-flow__controls-button svg {
  805. max-width: 16px;
  806. max-height: 16px;
  807. }
  808. .field {
  809. display: flex;
  810. }
  811. </style>
  812. <!-- 用于侧栏pane -->
  813. <style scoped>
  814. .flow-panel {
  815. font-size: 14px;
  816. }
  817. </style>