| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058 |
- <template>
- <splitpanes class="default-theme">
- <pane min-size="50" size="100" max-size="100">
- <!-- :nodes-draggable="isSelectMode"
- :nodes-connectable="isSelectMode ? false : true" -->
- <VueFlow
- ref="vueFlowRef"
- v-model:nodes="nodes"
- v-model:edges="edges"
- :class="{ dark }"
- class="basic-flow"
- :default-viewport="{ zoom: 1.5 }"
- :min-zoom="0.2"
- :max-zoom="2.5"
- :multi-selection-key-code="'Control'"
- :selection-key-code="'Shift'"
- :nodes-draggable="!props.isSelectMode"
- :nodes-connectable="props.isSelectMode ? false : true"
- @drop="customOnDrop"
- @node-contextmenu="onNodeContextMenu"
- @dragover="onDragOver"
- @dragleave="onDragLeave"
- @edge-click="onEdgeClick"
- @node-double-click="onNodeDoubleClick"
- @node-click="onNodeClick"
- @edge-double-click="onEdgeDoubleClick"
- @node-drag="onNodeDrag"
- @nodes-selection-change="onNodesSelectionChange"
- @edges-selection-change="onEdgesSelectionChange"
- >
- <!-- 自定义节点类型为default的节点 -->
- <template #node-default="props">
- <defaultnode :node="props" :key="props.data.idCodeser || props.id" />
- </template>
- <template #node-point-only="props">
- <PointOnlyNode
- :node="props"
- :key="props.data.idCodeser || props.id"
- />
- </template>
- <Background
- :variant="showGrid ? 'dots' : 'none'"
- pattern-color="#aaa"
- :gap="20"
- />
- </VueFlow>
- </pane>
- <pane
- v-if="showPanel"
- min-size="20"
- :size="30"
- max-size="50"
- class="flow-panel"
- >
- <asideData ref="asideDataref" @close="closePanel" />
- </pane>
- </splitpanes>
- </template>
- <script setup>
- import { VueFlow, useVueFlow, MarkerType } from "@vue-flow/core"
- import {
- ElMessage,
- ElMessageBox
- } from "element-plus"
- import { DeleteFilled } from "@element-plus/icons-vue"
- import { onBeforeRouteLeave } from "vue-router"
- import { request, uploadFile, getImageBase64 } from "@/utils/request"
- import { Background } from "@vue-flow/background"
- import "./main.css" //重置样式
- import defaultnode from "./defaultnode.vue"
- import PointOnlyNode from "./pointonlynode.vue"
- import useDragAndDrop from "./useDnD"
- import { useProjectStore } from "@/store/project"
- import { useClipboardStore } from '@/store/clipboard'
- import emitter from "@/utils/emitter"
- import { onMounted, onUnmounted, onBeforeUnmount, nextTick } from "vue"
- import { Splitpanes, Pane } from "splitpanes"
- import "splitpanes/dist/splitpanes.css"
- import asideData from "./aside/asideData.vue"
- import { copyNodes, cutNodes, pasteNodes } from "./utils/clipboardUtils";
- import node from "@/assets/img/node.png"
- import nodePoint from "@/assets/img/node.png"
- import tempic from "@/assets/img/temp.png"
- import { debounce } from 'lodash-es'
- // 常量定义
- const GRID_SIZE = 10
- const DEBOUNCE_DELAY = 1000
- const DEFAULT_LINE_COLOR = "#2267B1"
- const DEFAULT_LINE_WIDTH = 1
- // 定义一个新的 ref 用于存储边的选择状态
- const edgeSelectionState = ref({}); // 格式: { [edgeId]: { class: string, style: object } }
- // 获取路由实例
- const router = useRouter()
- const {
- onInit,
- onNodeDragStop,
- onConnect,
- addEdges,
- setViewport,
- toObject,
- addNodes,
- updateEdgeData,
- onConnectStart,
- updateNode,
- updateNodeInternals,
- onNodeDrag,
- snapToGrid,
- snapGrid,
- setInteractive,
- removeNodes,
- removeEdges,
- selectedNodes: vueFlowSelectedNodes,
- selectedEdges: vueFlowSelectedEdges,
- } = useVueFlow()
- const { onDragOver, onDrop, onDragLeave, isDragOver } = useDragAndDrop()
- const dark = ref(false)
- const vueFlowRef = ref()
- const iconcolor = ref("#000")
- const edges = ref([])
- const nodes = ref([])
- const mergedObj = ref("")
- const selectedNodes = ref([]); // 选中节点
- const selectedEdges = ref([]); // 选中边
- // 选中状态
- const selectedNode = ref(null)
- const selectedEdge = ref(null)
- const previousEdge = ref(null)
- const lineColor = ref(DEFAULT_LINE_COLOR)
- const lineWidth = ref(DEFAULT_LINE_WIDTH)
- const midNodeCounter = 0
- const projectStore = useProjectStore()
- const clipboardStore = useClipboardStore();
- const pid = computed(() => projectStore.pid || "")
- // 删除组件和边的列表
- const pendingDelete = ref({ nodes: [], edges: [] });
- const isCutting = ref(false);
- const showPanel = ref(false)
- const asideDataref = ref()
- const props = defineProps({
- jobId: { type: String, default: "" },
- showGrid: { type: Boolean, default: false },
- fixedGrid: { type: Boolean, default: false },
- isSelectMode: { type: Boolean, default: true }
- })
- // 保存项目
- const saveproject = async () => {
- try {
- const activeProject = projectStore.getActiveProject();
- if (!activeProject) {
- throw new Error("当前没有激活的项目");
- }
- const obj = {
- nodes: toObject().nodes.map((node) => ({
- ...node,
- data: { ...node.data, image: undefined },
- })),
- edges: toObject().edges,
- };
- mergedObj.value = JSON.stringify(obj);
- const params = {
- transCode: "ES0002",
- pid: pid.value,
- flow: mergedObj.value,
- name: activeProject.projectName || "",
- remark: activeProject.remark || "",
- keywords: activeProject.keywords || "",
- };
- // 保存前处理待删除的组件和边
- for (const { pcId } of pendingDelete.value.nodes) {
- await comdelete(pcId);
- }
- for (const { npcId, pcId } of pendingDelete.value.edges) {
- await comlinedelete(npcId, pcId);
- }
- pendingDelete.value = { nodes: [], edges: [] }; // 清空待删除列表
- await request(params);
- projectStore.updateProjectInfo(projectStore.activeProjectId, {
- ...activeProject,
- flow: mergedObj.value,
- });
- ElMessage.success("保存成功");
- } catch (error) {
- console.error("保存失败:", error);
- ElMessage.error("保存失败,请稍后重试");
- }
- };
- const debouncedSave = debounce(saveproject, DEBOUNCE_DELAY)
- let prevNodes = []
- let prevEdges = []
- let isInitializing = ref(true)
- // 合并的 watch:监听节点和边变化
- watch(
- [nodes, edges],
- ([newNodes, newEdges]) => {
- if (isInitializing.value) {
- prevNodes = [...newNodes];
- prevEdges = [...newEdges];
- isInitializing.value = false;
- return;
- }
- if (isCutting.value) {
- // 剪切时不触发保存,仅记录删除
- const deletedNodes = prevNodes.filter((n) => !newNodes.some((nn) => nn.id === n.id));
- if (deletedNodes.length > 0) {
- pendingDelete.value.nodes.push(...deletedNodes.map((node) => ({ pcId: node.data.pcId })));
- }
- const deletedEdges = prevEdges.filter((e) => !newEdges.some((ne) => ne.id === e.id));
- if (deletedEdges.length > 0) {
- deletedEdges.forEach((edge) => {
- const sourceNode = prevNodes.find((n) => n.id === edge.source);
- const targetNode = prevNodes.find((n) => n.id === edge.target);
- if (!sourceNode || !targetNode) return;
- let npcId = "",
- pcId = "";
- if (sourceNode.data?.comId === "3") {
- npcId = sourceNode.data.pcId;
- pcId = targetNode.data?.pcId;
- } else if (targetNode.data?.comId === "3") {
- npcId = targetNode.data.pcId;
- pcId = sourceNode.data?.pcId;
- }
- if (npcId && pcId) {
- pendingDelete.value.edges.push({ npcId, pcId });
- }
- });
- }
- prevNodes = [...newNodes];
- prevEdges = [...newEdges];
- debouncedSave.cancel();
- return; // 阻止 debouncedSave
- }
- // 其他操作(如添加、删除)触发保存
- const deletedNodes = prevNodes.filter((n) => !newNodes.some((nn) => nn.id === n.id));
- if (deletedNodes.length > 0) {
- pendingDelete.value.nodes.push(...deletedNodes.map((node) => ({ pcId: node.data.pcId })));
- }
- const deletedEdges = prevEdges.filter((e) => !newEdges.some((ne) => ne.id === e.id));
- if (deletedEdges.length > 0) {
- deletedEdges.forEach((edge) => {
- const sourceNode = prevNodes.find((n) => n.id === edge.source);
- const targetNode = prevNodes.find((n) => n.id === edge.target);
- if (!sourceNode || !targetNode) return;
- let npcId = "",
- pcId = "";
- if (sourceNode.data?.comId === "3") {
- npcId = sourceNode.data.pcId;
- pcId = targetNode.data?.pcId;
- } else if (targetNode.data?.comId === "3") {
- npcId = targetNode.data.pcId;
- pcId = sourceNode.data?.pcId;
- }
- if (npcId && pcId) {
- pendingDelete.value.edges.push({ npcId, pcId });
- }
- });
- }
- prevNodes = [...newNodes];
- prevEdges = [...newEdges];
- debouncedSave();
- },
- { deep: false }
- );
- // 网格固定
- watch(
- () => props.fixedGrid,
- (newValue) => {
- snapToGrid.value = newValue
- snapGrid.value = newValue ? [GRID_SIZE, GRID_SIZE] : [1, 1] // 禁用吸附时用 [1, 1]
- },
- { immediate: true }
- )
- onMounted(() => {
- flowInit()
- // 点击画布取消边选中
- if (vueFlowRef.value) {
- vueFlowRef.value.$el.addEventListener("click", (event) => {
- if (selectedEdge.value && !event.target.closest(".vue-flow__edge")) {
- cleanEdgeSelect();
- selectedNodes.value = [];
- selectedEdges.value = [];
- }
- })
- }
- console.log('Binding keydown event listener');
- window.addEventListener('keydown', handleKeyDown)
- window.addEventListener('beforeunload', handleBeforeUnload) // 绑定页面关闭事件
- })
- onBeforeUnmount(() =>{
- saveproject();
-
- })
- onUnmounted(() => {
- window.removeEventListener('keydown', handleKeyDown)
- window.removeEventListener('beforeunload', handleBeforeUnload) // 移除页面关闭事件
- })
- // 在路由切换前保存
- onBeforeRouteLeave((to, from, next) => {
- console.log('onBeforeRouteLeave triggered, saving project')
- debouncedSave.flush() // 立即执行任何待保存的操作
- saveproject().then(() => {
- console.log('Save completed before route leave')
- next()
- }).catch((error) => {
- console.error('Save failed before route leave:', error)
- ElMessage.error('保存失败,请检查网络后重试')
- next() // 即使保存失败,也允许路由切换
- })
- })
- const handleBeforeUnload = (event) => {
- console.log('beforeunload triggered, saving project')
- debouncedSave.flush() // 立即执行待保存操作
- // 注意:beforeunload 中无法等待异步 saveproject 完成
- event.preventDefault()
- event.returnValue = '项目尚未保存,确定要离开吗?' // 提示用户
- }
- // 新增:监听节点选择变化(包括 Ctrl+点击和框选)
- const onNodesSelectionChange = (event) => {
- selectedNodes.value = Array.from(vueFlowSelectedNodes.value);
- };
- // 新增:监听边选择变化
- const onEdgesSelectionChange = (event) => {
- selectedEdges.value = Array.from(vueFlowSelectedEdges.value);
- };
- // 网格吸附逻辑
- onNodeDrag(({ node }) => {
- if (!props.fixedGrid || node.type !== 'point-only') return
- // 仅对 point-only 节点禁用吸附效果
- const newPosition = {
- x: node.position.x, // 保留原始位置
- y: node.position.y,
- }
- updateNode(node.id, { position: newPosition })
- })
- // 键盘事件:Backspace 删除选中节点
- const handleKeyDown = (event) => {
- if (event.ctrlKey) {
- if (event.key === 'c') {
- event.preventDefault(); // 阻止默认行为
- event.stopPropagation(); // 阻止事件冒泡
- copyNodes(selectedNodes.value, selectedEdges.value, edges.value, nodes.value, pid.value);
- } else if (event.key === 'x') {
- event.preventDefault(); // 阻止默认行为
- event.stopPropagation(); // 阻止事件冒泡
- isCutting.value = true;
- cutNodes(selectedNodes.value, selectedEdges.value, nodes.value, edges.value, saveproject, removeNodes, removeEdges, clearSelection )
- .finally(() => {
- isCutting.value = false; // 重置标志
- });
- } else if (event.key === 'v') {
- event.preventDefault(); // 阻止默认行为
- event.stopPropagation(); // 阻止事件冒泡
- pasteNodes(pid.value, nodes.value, edges.value, saveproject, createEdge, addNodes, addEdges, updateNodeInternals);
- }
- } else if (event.key === 'Backspace' && selectedNodes.value.length > 0) {
- event.preventDefault(); // 阻止默认行为
- event.stopPropagation(); // 阻止事件冒泡
- removeNodes(selectedNodes.value.map((n) => n.id));
- selectedNodes.value = [];
- }
- };
- const clearSelection = () => {
- selectedNodes.value = [];
- selectedEdges.value = [];
- };
- const resetTransform = () => {
- setViewport({ x: 0, y: 0, zoom: 1 })
- }
- const toggleDarkMode = () => {
- dark.value = !dark.value
- iconcolor.value = dark.value ? "#fff" : "#000"
- }
- const removeNode = () => {
- if (!selectedNode.value) {
- ElMessage.warning("未选中任何节点")
- return
- }
- vueFlowRef.value.removeNodes(selectedNode.value.id)
- selectedNode.value = null
- debouncedSave();
- }
- const removeEdge = () => {
- if (!selectedEdge.value) {
- ElMessage.warning("请先选择要删除的连线")
- return
- }
- vueFlowRef.value.removeEdges(selectedEdge.value.id)
- selectedEdge.value = null
- debouncedSave();
- }
- const confirmDelete = () => {
- ElMessageBox.confirm("确定要删除所有节点和连线吗?此操作不可恢复!", "警告", {
- confirmButtonText: "确认",
- cancelButtonText: "取消",
- type: "warning"
- })
- .then(() => {
- vueFlowRef.value.removeNodes(nodes.value)
- vueFlowRef.value.removeEdges(edges.value)
- ElMessage.success("所有节点和连线已删除")
- debouncedSave();
- })
- .catch(() => {
- ElMessage.info("已取消删除")
- })
- }
- const comdelete = async (pcId) => {
- try {
- const params = { transCode: "ES0005", pcId }
- await request(params)
- } catch (err) {
- ElMessage.error(err.returnMsg || "组件删除失败")
- }
- }
- const comlinedelete = async (npcId, pcId) => {
- try {
- const params = { transCode: "ES0007", npcId, pcId, type: 0 }
- await request(params)
- } catch (err) {
- ElMessage.error(err.returnMsg || "连线删除失败")
- }
- }
- const customOnDrop = async (event) => {
- await onDrop(event)
- debouncedSave();
- }
- const onNodeContextMenu = (e) => {
- console.log("右键点击", e)
- }
- const getChainFromEdge = (edge) => {
- const chainNodes = new Set(); // 用 Set 避免重复
- const chainEdges = new Set(); // 用 Set 避免重复
- // 添加当前边
- chainEdges.add(edge);
- // 获取源和目标节点
- let currentSource = vueFlowRef.value.getNode(edge.source);
- let currentTarget = vueFlowRef.value.getNode(edge.target);
- chainNodes.add(currentSource);
- chainNodes.add(currentTarget);
- // 如果源是中间节点,向前扩展(找出源的输入边)
- if (currentSource.data?.comId === '3') {
- const inputEdges = edges.value.filter(e => e.target === currentSource.id);
- inputEdges.forEach(inputEdge => {
- chainEdges.add(inputEdge);
- const prevNode = vueFlowRef.value.getNode(inputEdge.source);
- chainNodes.add(prevNode);
- });
- }
- // 如果目标是中间节点,向后扩展(找出目标的输出边)
- if (currentTarget.data?.comId === '3') {
- const outputEdges = edges.value.filter(e => e.source === currentTarget.id);
- outputEdges.forEach(outputEdge => {
- chainEdges.add(outputEdge);
- const nextNode = vueFlowRef.value.getNode(outputEdge.target);
- chainNodes.add(nextNode);
- });
- }
- // 如果链条两端不是大节点,继续递归扩展(但假设单中间节点,此处简化)
- // 如果有多个中间节点,可递归调用 getChainFromEdge 于新边
- return {
- nodes: Array.from(chainNodes),
- edges: Array.from(chainEdges)
- };
- };
- // 边点击事件
- const onEdgeClick = (e) => {
- // 恢复上一个选中边样式(原有逻辑)
- if (previousEdge.value) {
- edges.value = edges.value.map(edge => {
- if (edge.id === previousEdge.value.id) {
- return {
- ...edge,
- class: edge.class ? edge.class.replace("selected", "").trim() : "",
- style: {
- ...edge.style,
- stroke: previousEdge.value.originalColor || DEFAULT_LINE_COLOR,
- strokeWidth: previousEdge.value.originalWidth || DEFAULT_LINE_WIDTH
- }
- };
- }
- return edge;
- });
- }
- // 设置当前选中边(原有逻辑)
- selectedEdge.value = { ...e.edge };
- selectedEdge.value.originalColor = e.edge.style?.stroke || DEFAULT_LINE_COLOR;
- selectedEdge.value.originalWidth = e.edge.style?.strokeWidth || DEFAULT_LINE_WIDTH;
- // 更新 selectedEdges.value(原有逻辑,支持 Ctrl 多选)
- if (e.event.ctrlKey) {
- const index = selectedEdges.value.findIndex((edge) => edge.id === e.edge.id);
- if (index === -1) {
- selectedEdges.value.push(e.edge);
- } else {
- selectedEdges.value.splice(index, 1); // 取消选择
- }
- } else {
- selectedEdges.value = [e.edge];
- selectedNodes.value = []; // 清空节点选择
- }
- // 新增:获取完整链条,并自动更新 selectedNodes 和 selectedEdges
- const chain = getChainFromEdge(e.edge);
- selectedNodes.value = chain.nodes; // 自动选中相关节点
- selectedEdges.value = [...selectedEdges.value, ...chain.edges.filter(ce => ce.id !== e.edge.id)]; // 添加其他相关边
- ElMessage.success('已自动选中边的相关节点、边和链条');
- // 可选:如果需要立即保存项目
- // saveproject();
- // 更新边样式(原有逻辑,稍作调整以支持多边选中)
- selectedEdges.value.forEach(selEdge => {
- const isDefault = selEdge.data?.type === "default";
- const newStyle = {
- ...selEdge.style,
- stroke: isDefault ? "#2267B1" : "rgba(255, 255, 0, 0.3)",
- strokeWidth: isDefault ? 2 : 6
- };
- edges.value = edges.value.map(edge => {
- if (edge.id === selEdge.id) {
- return {
- ...edge,
- class: edge.class ? `${edge.class} selected` : "selected",
- style: newStyle
- };
- }
- return edge;
- });
- });
- previousEdge.value = { ...selectedEdge.value, style: { ...selectedEdge.value.style } };
- };
- // 点击节点
- const onNodeClick = (event) => {
- if (event.event.ctrlKey) {
- // 支持多选节点
- const index = selectedNodes.value.findIndex((n) => n.id === event.node.id);
- if (index === -1) {
- selectedNodes.value.push(event.node);
- } else {
- selectedNodes.value.splice(index, 1); // 取消选择
- }
- } else {
- // 单选节点,清空边选择
- selectedNodes.value = [event.node];
- selectedEdges.value = [];
- }
- };
- const onNodeDoubleClick = (event) => {
- showPanel.value = true
- const pcId = event.node.data.pcId
- nextTick(() => {
- if (asideDataref.value) {
- asideDataref.value.getcomdata(pcId)
- if (props.jobId) {
- asideDataref.value.getresultData(props.jobId)
- }
- }
- })
- }
- const onEdgeDoubleClick = (event) => {
- selectedNode.value = event.node
- }
- // 连接开始:取消边选中
- onConnectStart(() => cleanEdgeSelect())
- // 方向计算工具函数
- const getDirectionFromPoint = (point) => {
- if (!isNaN(parseInt(point))) {
- const angle = ((parseInt(point) * 30) % 360 + 360) % 360
- if (angle >= 315 || angle < 45) return "right"
- if (angle >= 45 && angle < 135) return "bottom"
- if (angle >= 135 && angle < 225) return "left"
- if (angle >= 225 && angle < 315) return "top"
- }
- return point
- }
- const getOppositeDirection = (point) => {
- if (!isNaN(parseInt(point))) {
- const oppositePoint = (parseInt(point) + 6) % 12
- return getDirectionFromPoint(oppositePoint.toString())
- }
- return point === "top" ? "bottom" :
- point === "bottom" ? "top" :
- point === "left" ? "right" :
- point === "right" ? "left" : point
- }
- // 连接验证工具函数
- const validateConnection = (connection, sourceNode, targetNode) => {
- if (connection.source === connection.target) {
- ElMessage.warning("禁止节点自连")
- return false
- }
- if (sourceNode.data.comId === "3" && targetNode.data.comId === "3") {
- ElMessage.warning("禁止连接两个中间节点")
- return false
- }
- const hasDuplicate = edges.value.some(edge =>
- (edge.source === connection.source && edge.target === connection.target) ||
- (edge.source === connection.target && edge.target === connection.source)
- )
- if (hasDuplicate) {
- ElMessage.warning("禁止重复连接:起点和终点已连接")
- return false
- }
- const isDuplicateViaMiddle = edges.value.some(edge1 => {
- if (edge1.source === connection.source || edge1.target === connection.source) {
- const middleId = edge1.source === connection.source ? edge1.target : edge1.source
- const middleNode = vueFlowRef.value.getNode(middleId)
- if (!middleNode || middleNode.data.comId !== "3") return false
- return edges.value.some(edge2 =>
- (edge2.source === middleId && edge2.target === connection.target) ||
- (edge2.target === middleId && edge2.source === connection.target)
- )
- }
- return false
- })
- if (isDuplicateViaMiddle) {
- ElMessage.warning("禁止重复连接:这两个节点已经通过中间组件连接过了")
- return false
- }
- return true
- }
- const lineType = ref("default")
- onConnect(async (connection) => {
- const sourceNode = vueFlowRef.value.getNode(connection.source)
- const targetNode = vueFlowRef.value.getNode(connection.target)
- if (!validateConnection(connection, sourceNode, targetNode)) return
- const isCom3 = sourceNode.data.comId === "3" || targetNode.data.comId === "3"
- if (isCom3) {
- await handleDirectConnection(connection, sourceNode, targetNode)
- } else {
- await handleIndirectConnection(connection, sourceNode, targetNode)
- }
- debouncedSave();
- })
- const handleDirectConnection = async (connection, sourceNode, targetNode) => {
- const edgeId = `${lineType.value}-${connection.source}-${connection.sourceHandle}-${connection.target}-${connection.targetHandle}`
- connection.id = edgeId
- connection.type = "smoothstep"
- connection.color = lineColor.value
- connection.style = { strokeWidth: lineWidth.value, stroke: lineColor.value }
- const sameEdge = edges.value.find(e => e.id === edgeId)
- if (sameEdge) {
- await deleteflow(sameEdge.data?.wid)
- }
- addEdges(connection)
- selectedEdge.value = null
- const pcId1 = sourceNode.data.comId === "3" ? targetNode.data.pcId : sourceNode.data.pcId
- const npcId = sourceNode.data.comId === "3" ? sourceNode.data.pcId : targetNode.data.pcId
- try {
- const { pccId1 } = await createEdge(pid.value, 0, npcId, pcId1)
- updateEdgeData(connection.id, {
- pcid: pccId1,
- type: lineType.value,
- frompcId: sourceNode.data.pcId,
- topcId: targetNode.data.pcId
- })
- } catch (error) {
- console.error("保存流程失败:", error)
- }
- }
- const handleIndirectConnection = async (connection, sourceNode, targetNode) => {
- const pointNodeId = `point-${connection.source}-${connection.target}`
- const sourceCenterX = sourceNode.position.x + 60 / 2
- const sourceCenterY = sourceNode.position.y + 58 / 2
- const targetCenterX = targetNode.position.x + 60 / 2
- const targetCenterY = targetNode.position.y + 58 / 2
- const midX = (sourceCenterX + targetCenterX) / 2
- const midY = (sourceCenterY + targetCenterY) / 2
- const dxSource = sourceNode.position.x - midX
- const dySource = sourceNode.position.y - midY
- const dxTarget = targetNode.position.x - midX
- const dyTarget = targetNode.position.y - midY
- const isSourceHorizontal = Math.abs(dxSource) > Math.abs(dySource)
- const sourceDirection = isSourceHorizontal
- ? dxSource < 0 ? "left" : "right"
- : dySource < 0 ? "top" : "bottom"
- const isTargetHorizontal = Math.abs(dxTarget) > Math.abs(dyTarget)
- const targetDirection = isTargetHorizontal
- ? dxTarget < 0 ? "left" : "right"
- : dyTarget < 0 ? "top" : "bottom"
- // 添加 point-only 节点
- nodes.value.push({
- id: pointNodeId,
- type: "point-only",
- position: { x: midX, y: midY },
- data: {
- comId: "3",
- label: "节点",
- image: node,
- imageIdentify: "point-only"
- }
- })
- try {
- const { pcId: npcId, ser, idCode } = await createCom3(pid.value)
- updateNode(pointNodeId, node => ({
- ...node,
- data: {
- ...node.data,
- pcId: npcId,
- idCodeser: `${idCode}${ser}`
- }
- }))
- updateNodeInternals(pointNodeId)
- // edge1: source -> pointNode
- const edgeId1 = `${lineType.value}-${connection.source}-${connection.sourceHandle}-${pointNodeId}`
- const edge1 = {
- id: edgeId1,
- source: connection.source,
- sourceHandle: connection.sourceHandle,
- target: pointNodeId,
- targetHandle: `source-${sourceDirection}`,
- type: "smoothstep",
- color: lineColor.value,
- style: { strokeWidth: lineWidth.value, stroke: lineColor.value }
- }
- // edge2: pointNode -> target
- const edgeId2 = `${lineType.value}-${pointNodeId}-${connection.targetHandle}-${connection.target}`
- const edge2 = {
- id: edgeId2,
- source: pointNodeId,
- sourceHandle: `source-${targetDirection}`,
- target: connection.target,
- targetHandle: connection.targetHandle,
- type: "smoothstep",
- color: lineColor.value,
- style: { strokeWidth: lineWidth.value, stroke: lineColor.value }
- }
- addEdges([edge1, edge2])
- const { pccId1, pccId2 } = await createEdge(pid.value, 0, npcId, sourceNode.data.pcId, targetNode.data.pcId)
- updateEdgeData(edgeId1, {
- pcid: pccId1,
- type: lineType.value,
- frompcId: sourceNode.data.pcId,
- topcId: npcId
- })
- updateEdgeData(edgeId2, {
- pcid: pccId2,
- type: lineType.value,
- frompcId: npcId,
- topcId: targetNode.data.pcId
- })
- } catch (err) {
- console.error("中间点处理失败:", err)
- // 回滚:移除添加的节点
- nodes.value = nodes.value.filter(n => n.id !== pointNodeId)
- }
- }
- const createEdge = async (pid, type, npcId, pcId1, pcId2 = "") => {
- const params = {
- transCode: "ES0006",
- pid: pid || "",
- type: type || 0,
- npcId: npcId || "",
- pcId1: pcId1 || "",
- pcId2: pcId2 || ""
- }
- try {
- const res = await request(params)
- return { pccId1: res.pccId1, pccId2: res.pccId2 }
- } catch (err) {
- ElMessage.error(err.returnMsg || "保存流程失败")
- throw err
- }
- }
- const createCom3 = async (pid) => {
- try {
- const params = { transCode: "ES0004", pid: pid || "", comId: "3" }
- const res = await request(params)
- return { pcId: res.pcId, ser: res.ser, idCode: res.idCode }
- } catch (err) {
- ElMessage.error(err.returnMsg || "创建中间组件失败")
- throw err
- }
- }
- const flowInit = async () => {
- try {
- const activeProject = projectStore.getActiveProject()
- if (!activeProject) {
- throw new Error('当前没有激活的项目')
- }
- let flow
- // 检查 activeProject.flow 的类型
- if (typeof activeProject.flow === 'string') {
- // 如果是字符串,尝试解析
- try {
- flow = JSON.parse(activeProject.flow || '{"nodes":[], "edges":[] }')
- } catch (error) {
- console.error('解析 flow 数据失败:', activeProject.flow, error)
- flow = { nodes: [], edges: [] }
- }
- } else if (typeof activeProject.flow === 'object' && activeProject.flow !== null) {
- // 如果已经是对象,直接使用
- flow = activeProject.flow
- } else {
- // 其他情况使用默认值
- flow = { nodes: [], edges: [] }
- }
- const updatedNodes = await Promise.all(
- flow.nodes.map(async (node) => {
- if (node.data?.imageIdentify) {
- if (node.data.imageIdentify === 'point-only') {
- return { ...node, data: { ...node.data, image: nodePoint } }
- }
- try {
- const imageData = await getImageBase64(node.data.imageIdentify)
- return { ...node, data: { ...node.data, image: imageData || tempic } }
- } catch (err) {
- console.error(`获取图片 ${node.data.imageIdentify} 失败:`, err.message)
- return { ...node, data: { ...node.data, image: tempic } }
- }
- }
- return node
- })
- )
- nodes.value = updatedNodes
- edges.value = flow.edges
- prevNodes = [...updatedNodes]
- prevEdges = [...flow.edges]
- isInitializing.value = true
- } catch (error) {
- console.error('加载项目失败:', error)
- ElMessage.error('加载项目失败,请检查项目数据')
- }
- }
- const cleanEdgeSelect = () => {
- if (selectedEdge.value) {
- edges.value = edges.value.map(edge => {
- if (edge.id === selectedEdge.value.id) {
- return {
- ...edge,
- class: edge.class ? edge.class.replace("selected", "").trim() : "",
- style: {
- ...edge.style,
- stroke: selectedEdge.value.originalColor || DEFAULT_LINE_COLOR,
- strokeWidth: selectedEdge.value.originalWidth || DEFAULT_LINE_WIDTH
- }
- };
- }
- return edge;
- });
- selectedEdge.value = null;
- previousEdge.value = null;
- }
- };
- const closePanel = () => {
- nextTick(() => {
- showPanel.value = false
- })
- }
- // 暴露方法
- defineExpose({
- resetTransform,
- toggleDarkMode,
- saveproject,
- removeNode,
- removeEdge,
- confirmDelete,
- asideDataref,
- copyNodes: () => copyNodes(selectedNodes.value, selectedEdges.value, edges.value, nodes.value, pid.value),
- cutNodes: () => cutNodes(selectedNodes.value, selectedEdges.value, nodes.value, edges.value, saveproject, removeNodes, removeEdges, clearSelection)
- .finally(() => {
- isCutting.value = false;
- }),
- pasteNodes: () => pasteNodes(pid.value, nodes.value, edges.value, saveproject, createEdge, addNodes, addEdges, updateNodeInternals),
- flowInit,
- refreshFlow: flowInit
- })
- </script>
- <style scoped>
- .vue-flow__edge:focus .vue-flow__edge-path,
- .vue-flow__edge:focus-visible .vue-flow__edge-path {
- stroke: #555 !important;
- }
- .vue-flow__edge {
- text-align: left;
- }
- .vue-flow__edge-text {
- transform: translateY(-10px);
- background: transparent !important;
- font-size: 8px;
- font-family: "Microsoft YaHei";
- color: #333333;
- }
- .vue-flow__edge-textbg {
- fill: transparent !important;
- }
- .vue-flow__node-default.selectable:hover,
- .vue-flow__node-input.selectable:hover,
- .vue-flow__node-output.selectable:hover {
- box-shadow: none;
- }
- .remove {
- background: #fff;
- color: #666;
- margin: 0 10px;
- font-size: 12px;
- }
- .vue-flow__node-default,
- .vue-flow__node-input,
- .vue-flow__node-output {
- border: none;
- background-color: rgba(0, 0, 0, 0);
- }
- .node-content {
- cursor: move;
- }
- .vue-flow__node {
- cursor: move;
- }
- /* 禁用文本选中效果 */
- .left_main * {
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- }
- .lableaniu {
- font-size: 12px;
- background-color: #ddd;
- padding: 4px 16px;
- margin-left: 5px;
- margin-top: 0px;
- border-radius: 1px;
- }
- .vue-flow__controls-button svg {
- max-width: 16px;
- max-height: 16px;
- }
- .field {
- display: flex;
- }
- </style>
- <!-- 用于侧栏pane -->
- <style scoped>
- .flow-panel {
- font-size: 14px;
- }
- </style>
|