vueflow

news2025/6/2 5:21:04

自定义节点,自定义线,具体细节还未完善,实现效果:
在这里插入图片描述
1.安装vueflow
2.目录如下在这里插入图片描述
3.

index.vue

<script setup>
import { ref } from 'vue'
import { VueFlow, useVueFlow } from '@vue-flow/core'
import { Background } from '@vue-flow/background'
import { ControlButton, Controls } from '@vue-flow/controls'
import { MiniMap } from '@vue-flow/minimap'
import { MarkerType } from '@vue-flow/core'
import useDragAndDrop from './components/useDnD'
import Sidebar from './components/Sidebar.vue'
const { onInit, onNodeDragStop, onConnect, addEdges, setViewport, toObject, addNodes, project } = useVueFlow()
const { onDragStart, onDragOver, onDrop, onDragLeave, isDragOver } = useDragAndDrop()
import EdgeWithButton from './components/EdgeWithButton.vue'
import { toPng, toJpeg, toBlob } from 'html-to-image'
// const flowContainer = ref(null)
// 导入自定义节点
import DataSetNode from './components/DataSetNode.vue'//数据集
import ConditionNode from './components/ConditionNode.vue'//条件
import AlgorithmsLibraryNode from './components/AlgorithmsLibraryNode.vue'//算法
// 节点
const nodes = ref([])
// 线
const edges = ref([])
// var drawer = ref(false)
// 线的默认颜色
const edgesStyle = {
  style: {
    // stroke: '#6366f1',
    strokeWidth: 1, // 设置线宽 
  },
  markerEnd: {
    type: MarkerType.ArrowClosed,
    // color: '#6366f1',
    // width: 6,   // 箭头宽度
    // height: 12,  // 箭头高度
  }

}
// 初始化
onInit((vueFlowInstance) => {
  vueFlowInstance.fitView()
})
// 链接线
onConnect((connection) => {
  addEdges({
    ...connection, // 保留原始连接属性
    type: 'button',
    ...edgesStyle
  })
})
// 双击事件
// const handleNodeDoubleClick = (event, node) => {
//   drawer.value = true
// }
// 阻止右键事件
const showContextMenu = (e) => {
  // e.preventDefault()
}
// 保存按钮
const saveNodes = () => {
  console.log(nodes.value)
  console.log(edges.value)
  edges.value.map(val => {
    val.type = null
  })
  console.log("保存")
}
</script>

<template>
  <div class="dndflow" @drop="onDrop" @click.right.native="showContextMenu($event)">
    <!-- 顶部的按钮 -->
    <div class="top-title-button">
      <div class="top-title">算法流程编辑</div>
      <el-button type="primary" class="ybutton">运行</el-button>
      <el-button type="success" class="ybutton" @click="saveNodes">保存</el-button>
    </div>
    <div ref="flowContainer" class="flow-container">
      <!-- @node-double-click="handleNodeDoubleClick" -->
      <VueFlow v-model:nodes="nodes" v-model:edges="edges" class="basic-flow" :default-viewport="{ zoom: 1.5 }"
        :min-zoom="0.2" :max-zoom="4" @dragover="onDragOver" @dragleave="onDragLeave">
        <template #edge-button="buttonEdgeProps">
          <!-- 删除线的删除按钮 -->
          <EdgeWithButton :id="buttonEdgeProps.id" :source-x="buttonEdgeProps.sourceX"
            :source-y="buttonEdgeProps.sourceY" :target-x="buttonEdgeProps.targetX" :target-y="buttonEdgeProps.targetY"
            :source-position="buttonEdgeProps.sourcePosition" :target-position="buttonEdgeProps.targetPosition"
            :marker-end="buttonEdgeProps.markerEnd" :style="buttonEdgeProps.style" />
        </template>
        <template #node-data-set="props">
          <!-- 数据集节点 -->
          <DataSetNode :id="props.id" :data="props.data"></DataSetNode>
        </template>
        <template #node-algorithms-library="props">
          <!-- 算法库节点 -->
          <AlgorithmsLibraryNode :id="props.id" :data="props.data"></AlgorithmsLibraryNode>
        </template>
        <template #node-condition="props">
          <!-- 条件节点 -->
          <ConditionNode :id="props.id" :data="props.data"></ConditionNode>
        </template>
        
        <!-- 背景 -->
        <Background :gap="16" />
        <!-- 小地图 -->
        <MiniMap />
        <!-- 小按钮 -->
        <Controls position="bottom-center" />
      </VueFlow>
    </div>
    <!-- 左侧拖动面板 -->
    <Sidebar />
  </div>
</template>
<style>
@import './main.css';
</style>

main.css

/* import the necessary styles for Vue Flow to work */
@import "@vue-flow/core/dist/style.css";

/* import the default theme, this is optional but generally recommended */
@import "@vue-flow/core/dist/theme-default.css";


html,
body,
#app {
    margin: 0;
    height: 100%;
}

#app {
    text-transform: uppercase;
    font-family: 'JetBrains Mono', monospace;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-align: center;
    color: #2c3e50;
}

.clearfix:after {
    content: "";
    display: block;
    clear: both;
}

/* 最外层div样式 */
.dndflow {
    flex-direction: column;
    display: flex;
    height: 100%;
    width: calc(100% - 200px);
    position: absolute;
    left: 200px;
}

.flow-container {
    width: 100%;
    height: calc(100% - 60px);
    background: white;
    border: 1px solid #ddd;
}

/* 小地图 */
.vue-flow__minimap {
    transform: scale(75%);
    transform-origin: bottom right;
}

/* 顶部标题及运行和保存按钮 */
.top-title-button {
    height: 60px;
    text-align: left;
    line-height: 60px;
}

.top-title {
    display: inline-block;
    font-size: 30px;
    font-weight: 800;
    padding-left: 20px;
    font-weight: bold;
    /* color: #0f6cd6; */
    text-shadow:
        -2px -2px 0 #000;
    /* 1px -1px 0 #000,
        -1px 1px 0 #000,
        1px 1px 0 #000; */
    background-image: -webkit-linear-gradient(bottom, red, #fd8403, yellow);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
}

.ybutton {
    margin: 20px 10px 0;
    float: right;
}

/* 工具行样式 */
.basic-flow .vue-flow__controls .vue-flow__controls-button svg {
    height: 16px;
    width: 16px;
    padding: 2px;
}

/* 在 handle 内部添加 + 号 */
.vue-flow__handle {
    height: 12px;
    width: 12px;
    border-radius: 50%;
}

.vue-flow__handle::after {
    content: "+";
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    font-size: 14px;
    color: #fff;
    pointer-events: none;
    /* 避免干扰拖拽事件 */
}

/* 左侧面板 */
.left-panal {
    position: fixed;
    bottom: 0;
    left: 0;
    top: 0;
    margin: 0;
    background: linear-gradient(to left, #ba8beb, #c1e9e9);
    z-index: 5;
    width: 200px;
}

.left-panal>div {
    margin: 10px auto;
    cursor: grab;
}

/*左侧按钮 */
.vue-flow__node-default {
    /* border-width: 3px; */
    padding: 0;
    border: 1px solid #ca9fed;
    padding: 5px 10px;
    font-size: 16px;
    display: flex;
    align-items: center;
    justify-content: center;
}

.vue-flow__node-default .el-icon {
    margin-right: 5px;
}

/* 删除按钮 */
.edgebutton {
    width:15px;
    height:15px;
    line-height:15px;
    font-size: 12px;
    border: 1px solid #b0dee7;
    background: #ffffff;
    border-radius: 50%;
    cursor: pointer;
    color: #aaa;
}

.edgebutton:hover {
    transform: scale(1.1);
    transition: all ease .5s;
    box-shadow: 0 0 0 1px #a8ddcb80, 0 0 0 2px #c0e4e4
}

/* 节点样式 */
.custom-node {
    width: 180px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
    position: relative;
    text-align: left;
    border: 1px solid #ddd;
    background: #fff;
    border-radius: 5px;
    padding:10px;
}

.node-header {
    font-weight: bold;
    /* border-bottom: 1px solid #eee; */
    padding-bottom: 4px;
}

.vue-flow__node.selected .custom-node {
    box-shadow: 0 1px 3px #6366f1 !important;
    border: 1px solid #6366f1 !important;
}

.deletebtn {
    position: absolute;
    right: 5px;
    top: 0;
    cursor: pointer;
}

.deletebtn .el-icon {
    margin: 5px 5px;
    width: 12px;
    height: 12px;
}

.del-icon {
    color: #f00;
}

.copy-icon {
    color: rgb(13, 67, 227);
}

.edit-icon {
    color: rgb(10, 236, 232);
}

.yxjgbtn {
    float: right;
    color: #6366f1;
    font-size: 14px;
    cursor: pointer;
}

/* .btnList{
    cursor: pointer;
  }
  .btnList>p{
    cursor: pointer;
    text-align: center;
    font-size: 16px;
    border-bottom:1px solid #eee;
    margin: 0;
    padding: 5px 0;
  }
  .btnList>p:last-child{
    border: 0;
  } */

Sidebar.vue

<!-- 左侧拖动节点栏 -->
<script setup>
import useDragAndDrop from './useDnD'
const { onDragStart } = useDragAndDrop()
</script>

<template>
    <aside class="left-panal">
        <!-- <div class="vue-flow__node-input" :draggable="true"
            @dragstart="(event) => onDragStart(event, { type: 'input', label: '开始' })">
            开始
        </div>
        <div class="vue-flow__node-output" :draggable="true"
            @dragstart="(event) => onDragStart(event, { type: 'output', label: '结束' })">
            结束
        </div> -->
        <div class="vue-flow__node-default" :draggable="true"
            @dragstart="(event) => onDragStart(event, 'algorithms-library')">
            <el-icon style="color: #532ff3;">
                <Memo />
            </el-icon>算法
        </div>
        <div class="vue-flow__node-default" :draggable="true" @dragstart="(event) => onDragStart(event, 'data-set')">
            <el-icon style="color: #f34033;">
                <Files />
            </el-icon>数据集
        </div>
        <div class="vue-flow__node-default" :draggable="true" @dragstart="(event) => onDragStart(event, 'condition')">
            <el-icon  style="color: #077215;"><Connection /></el-icon>条件
        </div>
    </aside>
</template>

useDnD.js

import { useVueFlow } from '@vue-flow/core'
import { ref, watch } from 'vue'

/**
 * @returns {string} - A unique id.
 */
function getId() {
  let id = Date.now();
  return `dndnode_${id}`
}

/**
 * In a real world scenario you'd want to avoid creating refs in a global scope like this as they might not be cleaned up properly.
 * @type {{draggedType: Ref<string|null>, isDragOver: Ref<boolean>, isDragging: Ref<boolean>}}
 */
const state = {
  /**
   * The type of the node being dragged.
   */
  draggedType: ref(null),
  isDragOver: ref(false),
  isDragging: ref(false),
}

export default function useDragAndDrop() {
  const { draggedType, isDragOver, isDragging } = state

  const { addNodes, screenToFlowCoordinate, onNodesInitialized, updateNode } = useVueFlow()

  watch(isDragging, (dragging) => {
    document.body.style.userSelect = dragging ? 'none' : ''
  })

  function onDragStart(event, type) {
    if (event.dataTransfer) {
      event.dataTransfer.setData('application/vueflow', type)
      event.dataTransfer.effectAllowed = 'move'
    }

    draggedType.value = type
    isDragging.value = true

    document.addEventListener('drop', onDragEnd)
  }

  /**
   * Handles the drag over event.
   *
   * @param {DragEvent} event
   */
  function onDragOver(event) {
    event.preventDefault()

    if (draggedType.value) {
      isDragOver.value = true

      if (event.dataTransfer) {
        event.dataTransfer.dropEffect = 'move'
      }
    }
  }

  function onDragLeave() {
    isDragOver.value = false
  }

  function onDragEnd() {
    isDragging.value = false
    isDragOver.value = false
    draggedType.value = null
    document.removeEventListener('drop', onDragEnd)
  }

  /**
   * Handles the drop event.
   *
   * @param {DragEvent} event
   */
  function onDrop(event) {
    const position = screenToFlowCoordinate({
      x: event.clientX,
      y: event.clientY,
    })

    const nodeId = getId()

    const newNode = {
      id: nodeId,
      type: draggedType.value,
      position,
      data: { label: nodeId },
    }

    /**
     * Align node position after drop, so it's centered to the mouse
     *
     * We can hook into events even in a callback, and we can remove the event listener after it's been called.
     */
    const { off } = onNodesInitialized(() => {
      updateNode(nodeId, (node) => ({
        position: { x: node.position.x - node.dimensions.width / 2, y: node.position.y - node.dimensions.height / 2 },
      }))

      off()
    })

    addNodes(newNode)
  }

  return {
    draggedType,
    isDragOver,
    isDragging,
    onDragStart,
    onDragLeave,
    onDragOver,
    onDrop,
  }
}

AlgorithmsLibraryNode.vue

<!-- CustomNode.vue -->
<template>
    <div class="custom-node clearfix">
      <div class="deletebtn">
        <el-popconfirm class="box-item" title="确定删除该节点吗?" placement="top-start" @confirm="deleteNode(id)">
          <template #reference>
            <el-icon class="del-icon">
              <Delete />
            </el-icon>
          </template>
        </el-popconfirm>
        <el-icon class="copy-icon" @click="duplicateNode(id)">
          <DocumentCopy />
        </el-icon>
        <el-icon class="edit-icon" @click="xgjd(id)">
          <EditPen />
        </el-icon>
        <!-- <el-popover class="box-item"   placement="top-start">
          <template #reference>
            <el-icon>
              <MoreFilled />
            </el-icon>
          </template>
  <div class="btnList">
    <p @click="deleteNode">删除</p>
    <p>复制</p>
  </div>
  </el-popover> -->
      </div>
      <div class="node-header">
        算法
      </div>
      <div @click="yxjg()" class="yxjgbtn">运行结果</div>
      <Handle type="source" position="right" />
      <Handle type="target" position="left" />
    </div>
    <!-- 运行结果 -->
    <el-drawer v-model="draweryx" :with-header="false" size="20%" append-to-body>
      <span>运行结果</span>
    </el-drawer>
    <!-- 点击节点弹出的弹出框 -->
    <el-drawer v-model="drawerjd" :with-header="false" size="20%" append-to-body>
      <span>修改节点</span>
    </el-drawer>
  </template>
  
  <script setup>
  import { Handle } from '@vue-flow/core'
  import { useVueFlow } from '@vue-flow/core'
  const { removeNodes, getNodes, addNodes } = useVueFlow()
  var draweryx = ref(false)
  var drawerjd = ref(false)
  
  const props = defineProps({
    id: String,
    data: Object,
    selected: Boolean
  })
  // 运行结果事件
  const yxjg = (id) => {
    draweryx.value = true
  }
  // 修改节点事件
  const xgjd = (id) => {
    drawerjd.value = true
  }
  // 删除单个节点
  const deleteNode = (nodeId) => {
    removeNodes(nodeId)
  }
  // 复制指定节点
  const duplicateNode = (nodeId) => {
    const originalNode = getNodes.value.find(n => n.id === nodeId)
    if (!originalNode) return
  
    // 创建新节点(修改ID和位置)
    const newNode = {
      ...originalNode,
      id: `${originalNode.id}-copy-${Date.now()}`, // 确保ID唯一
      position: {
        x: originalNode.position.x + 50, // 偏移位置
        y: originalNode.position.y + 50
      },
      selected: false // 取消选中状态
    }
    addNodes(newNode)
  }
  
  </script>
  

ConditionNode.vue

<!-- CustomNode.vue -->
<template>
    <div class="custom-node clearfix" >
        <div class="deletebtn">
            <el-popconfirm class="box-item" title="确定删除该节点吗?" placement="top-start" @confirm="deleteNode(id)">
                <template #reference>
                    <el-icon class="del-icon">
                        <Delete />
                    </el-icon>
                </template>
            </el-popconfirm>
            <el-icon class="copy-icon" @click="duplicateNode(id)">
                <DocumentCopy />
            </el-icon>
            <el-icon class="edit-icon" @click="xgjd(id)">
                <EditPen />
            </el-icon>
        </div>
        <div class="node-header">
            条件
        </div>
        <div v-for="(item, index) in data.conditions" v-if="data.conditions" class="conditionsNode">
            <p v-if="index == 0"><span>Case{{ index + 1 }}</span> <span class="caseif">If</span></p>
            <p v-if="index != 0 && index != data.conditions.length - 1"><span>Case{{ index + 1 }}</span><span
                    class="caseif">Else
                    If</span></p>
            <p v-if="index == data.conditions.length - 1"><span class="caseif">Else</span></p>
            <div class="paramList" v-if="index != data.conditions.length - 1">
                <div v-for="(d, num) in item.rules">
                    <p class="param"> {{ d.param }}{{ d.operator }}{{ d.value }}</p>
                    <p v-if="item.rules.length > 1 && item.rules.length - 1 != num" class="operator">{{ item.operator }}
                    </p>
                </div>
            </div>

            <Handle :position="Position.Right" type="source" :id="item.id + 'right_' + index"
                class="conditionsHandleNode">
            </Handle>
        </div>
        <Handle type="target" position="left" />
        <!-- <Handle v-for="(item, index) in conditions" :position="Position.Right" type="source" :id="'right_' + index"
            :style="getDynamicHandlePos(item, index)">
        </Handle> -->
    </div>
    <!-- 点击节点弹出的弹出框 -->
    <el-drawer v-model="drawerjd" size="20%" append-to-body :with-header="false">
        <div class="drawerTitle"><el-icon style="color: blueviolet;margin-right: 5px;">
                <Edit />
            </el-icon>条件节点</div>
        <p class="nodedescribe">该组件用于根据前面的组件输出相应的引导执行流程,通过定义各种情况并指定操作,或不满足条件时采取默认操作,实现复杂的分支逻辑</p>
        <div v-for="(item, index) in data.conditions" class="drawerCase" v-show="index!=data.conditions.length-1">
            <el-select v-model="item.operator" placeholder="选择" size="large">
                <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
            </el-select>
            <div></div>
        </div>
        <el-button type="success">Add Condition</el-button>
        <!-- <div class="addcondition">Add Condition</div> -->
        <el-button type="primary">Add Case</el-button>
        <!-- <div class="addcase">Add Case</div> -->

    </el-drawer>
</template>

<script setup>
import { Position, Handle } from '@vue-flow/core'
import { useVueFlow } from '@vue-flow/core'
import { onMounted } from 'vue'
const { removeNodes, getNodes, addNodes, updateNode } = useVueFlow()
var draweryx = ref(false)
var drawerjd = ref(false)
const options = [
  {
    value: 'AND',
    label: '与',
  },
  {
    value: 'OR',
    label: '或',
  },
]


const props = defineProps({
    id: String,
    data: Object,
    selected: Boolean
})
const initconditions = () => {
    if (props.data.conditions) return
    props.data.conditions = [
        {
            operator: 'AND',
            rules: [
                {
                    param: 'ceshi',
                    operator: '>',
                    value: '13',
                }, {
                    param: 'ceshi',
                    operator: '>',
                    value: '13',
                }, {
                    param: null,
                    operator: null,
                    value: null,
                }
            ]
        }, {
            operator: null,
            rules: null
        }
    ]
}
// 修改节点事件
const xgjd = (id) => {
    drawerjd.value = true
}
// 删除单个节点
const deleteNode = (nodeId) => {
    removeNodes(nodeId)
}
// 复制指定节点
const duplicateNode = (nodeId) => {
    const originalNode = getNodes.value.find(n => n.id === nodeId)
    if (!originalNode) return

    // 创建新节点(修改ID和位置)
    const newNode = {
        ...originalNode,
        id: `${originalNode.id}-copy-${Date.now()}`, // 确保ID唯一
        position: {
            x: originalNode.position.x + 50, // 偏移位置
            y: originalNode.position.y + 50
        },
        selected: false // 取消选中状态
    }
    addNodes(newNode)
}
onMounted(() => {
    initconditions()
    const originalNode = getNodes.value.find(n => n.id === props.id)
    console.log(originalNode)
})
</script>
<style scoped>
.conditionsNode {
    width: 100%;
    position: relative;
    /* text-align: right; */
}

.conditionsNode p {
    font-size: 14px;
    margin: 5px 0;
}

.conditionsHandleNode {
    position: absolute;
    top: 10px;
    right: -10px;
}

.caseif {
    float: right;
}

.paramList {
    padding: 5px;
    background: #f8f6fe;
}

.paramList .param {
    padding: 5px;
    background: #e2d6ff;
}

.paramList .operator {
    text-align: center;
    font-size: 12px;
    font-weight: 800;
}

.drawerTitle {
    font-size: 16px;
    font-weight: 800;
    display: flex;
    align-items: center;
}

.nodedescribe {
    color: #666;
    font-size: 12px;
}
.drawerCase{
    background: #f8f6fe;
    padding: 5px;
}
</style>

DataSetNode.vue

<!-- CustomNode.vue -->
<template>
  <div class="custom-node clearfix" >
    <div class="deletebtn">
      <el-popconfirm class="box-item" title="确定删除该节点吗?" placement="top-start" @confirm="deleteNode(id)">
        <template #reference>
          <el-icon class="del-icon">
            <Delete />
          </el-icon>
        </template>
      </el-popconfirm>
      <el-icon class="copy-icon" @click="duplicateNode(id)">
        <DocumentCopy />
      </el-icon>
      <el-icon class="edit-icon" @click="xgjd(id)">
        <EditPen />
      </el-icon>
      <!-- <el-popover class="box-item"   placement="top-start">
        <template #reference>
          <el-icon>
            <MoreFilled />
          </el-icon>
        </template>
<div class="btnList">
  <p @click="deleteNode">删除</p>
  <p>复制</p>
</div>
</el-popover> -->
    </div>
    <div class="node-header">
     数据集
    </div>
    <div @click="yxjg()" class="yxjgbtn">运行结果</div>
    <Handle type="source" position="right" />
    <Handle type="target" position="left" />
  </div>
  <!-- 运行结果 -->
  <el-drawer v-model="draweryx" :with-header="false" size="20%" append-to-body>
    <span>运行结果</span>
  </el-drawer>
  <!-- 点击节点弹出的弹出框 -->
  <el-drawer v-model="drawerjd" :with-header="false" size="20%" append-to-body>
    <span>修改节点</span>
  </el-drawer>
</template>

<script setup>
import { Handle } from '@vue-flow/core'
import { useVueFlow } from '@vue-flow/core'
const { removeNodes, getNodes, addNodes } = useVueFlow()
var draweryx = ref(false)
var drawerjd = ref(false)

const props = defineProps({
  id: String,
  data: Object,
  selected: Boolean
})

// 运行结果事件
const yxjg = (id) => {
  draweryx.value = true
}
// 修改节点事件
const xgjd = (id) => {
  drawerjd.value = true
}
// 删除单个节点
const deleteNode = (nodeId) => {
  removeNodes(nodeId)
}
// 复制指定节点
const duplicateNode = (nodeId) => {
  const originalNode = getNodes.value.find(n => n.id === nodeId)
  if (!originalNode) return

  // 创建新节点(修改ID和位置)
  const newNode = {
    ...originalNode,
    id: `${originalNode.id}-copy-${Date.now()}`, // 确保ID唯一
    position: {
      x: originalNode.position.x + 50, // 偏移位置
      y: originalNode.position.y + 50
    },
    selected: false // 取消选中状态
  }
  addNodes(newNode)
}

</script>

EdgeWithButton.vue

<script setup>
import { BaseEdge, EdgeLabelRenderer, getBezierPath, useVueFlow } from '@vue-flow/core'
import { computed } from 'vue'

const props = defineProps({
  id: {
    type: String,
    required: true,
  },
  sourceX: {
    type: Number,
    required: true,
  },
  sourceY: {
    type: Number,
    required: true,
  },
  targetX: {
    type: Number,
    required: true,
  },
  targetY: {
    type: Number,
    required: true,
  },
  sourcePosition: {
    type: String,
    required: true,
  },
  targetPosition: {
    type: String,
    required: true,
  },
  markerEnd: {
    type: String,
    required: false,
  },
  style: {
    type: Object,
    required: false,
  },
})

const { removeEdges } = useVueFlow()

const path = computed(() => getBezierPath(props))
</script>

<script>
export default {
  inheritAttrs: false,
}
</script>

<template>
  <!-- You can use the `BaseEdge` component to create your own custom edge more easily -->
  <BaseEdge :id="id" :style="style" :path="path[0]" :marker-end="markerEnd" />

  <!-- Use the `EdgeLabelRenderer` to escape the SVG world of edges and render your own custom label in a `<div>` ctx -->
  <EdgeLabelRenderer>
    <div :style="{
      pointerEvents: 'all',
      position: 'absolute',
      transform: `translate(-50%, -50%) translate(${path[1]}px,${path[2]}px)`,
    }" class="nodrag nopan">
      <div class="edgebutton" @click="removeEdges(id)">×</div>
    </div>
  </EdgeLabelRenderer>
</template>
<style scoped>

</style>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2393910.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LearnOpenGL-笔记-其十一

Normal Mapping 又到了介绍法线贴图的地方&#xff0c;我感觉我已经写了很多遍了... 法线贴图用最简单的话来介绍的话&#xff0c;就是通过修改贴图对应物体表面的法线来修改光照效果&#xff0c;从而在不修改物体实际几何形状的前提下实现不同于物体几何形状的视觉效果。 因…

openppp2 -- 1.0.0.25225 优化多线接入运营商路由调配

本文涉及到的内容&#xff0c;涉及到上个发行版本相关内容&#xff0c;人们在阅读本文之前&#xff0c;建议应当详细阅读上个版本之中的VBGP技术相关的介绍。 openppp2 -- 1.0.0.25196 版本新增的VBGP技术-CSDN博客 我们知道在现代大型的 Internet 网络服务商&#xff0c;很多…

详细到用手撕transformer下半部分

之前我们讨论了如何实现 Transformer 的核心多头注意力机制&#xff0c;那么这期我们来完整地实现整个 Transformer 的编码器和解码器。 Transformer 架构最初由 Vaswani 等人在 2017 年的论文《Attention Is All You Need》中提出&#xff0c;专为序列到序列&#xff08;seq2s…

【Sqoop基础】Sqoop生态集成:与HDFS、Hive、HBase等组件的协同关系深度解析

目录 1 Sqoop概述与大数据生态定位 2 Sqoop与HDFS的深度集成 2.1 技术实现原理 2.2 详细工作流程 2.3 性能优化实践 3 Sqoop与Hive的高效协同 3.1 集成架构设计 3.2 数据类型映射处理 3.3 案例演示 4 Sqoop与HBase的实时集成 4.1 数据模型转换挑战 4.2 详细集成流程…

MySQL + CloudCanal + Iceberg + StarRocks 构建全栈数据服务

简述 在业务数据快速膨胀的今天&#xff0c;企业对 低成本存储 与 实时查询分析能力 的需求愈发迫切。 本文将带你实战构建一条 MySQL 到 Iceberg 的数据链路&#xff0c;借助 CloudCanal 快速完成数据迁移与同步&#xff0c;并使用 StarRocks 完成数据查询等操作&#xff0c…

截屏精灵:轻松截屏,高效编辑

在移动互联网时代&#xff0c;截图已经成为我们日常使用手机时的一项基本操作。无论是记录重要信息、分享有趣内容&#xff0c;还是进行学习和工作&#xff0c;一款好用的截图工具都能极大地提升我们的效率。截屏精灵就是这样一款功能强大、操作简单的截图工具&#xff0c;它不…

【JavaWeb】基本概念、web服务器、Tomcat、HTTP协议

目录 1. 基本概念1.1 基本概念1.2 web应用程序1.3 静态web1.4 动态web 2. web服务器3. tomcat详解3.1 安装3.2 启动3.3 配置3.3.1 配置启动的端口号3.3.2 配置主机的名称3.3.3 其他常用配置项日志配置数据源配置安全配置 3.4 发布一个网站 4. Http协议4.1 什么是http4.2 http的…

云计算Linux Rocky day02(安装Linux系统、设备表示方式、Linux基本操作)

云计算Linux Rocky day02&#xff08;安装Linux系统、设备表示方式、Linux基本操作&#xff09; 目录 云计算Linux Rocky day02&#xff08;安装Linux系统、设备表示方式、Linux基本操作&#xff09;1、虚拟机VMware安装Rocky2、Linux命令行3、Linux Rocky修改字体大小和背景颜…

在 ODROID-H3+ 上安装 Win11 系统

在 ODROID-H3 上安装 Windows 11 系统。 以下是完整的步骤&#xff0c;包括 BIOS 设置、U 盘制作、安装和驱动处理&#xff0c;全程不保留之前的系统数据。 ✅ 准备工作 1. 准备一个 ≥8GB 的 USB 启动盘 用另一台电脑制作 Windows 11 安装盘。 &#x1f449; 推荐工具&…

使用el-input数字校验,输入汉字之后校验取消不掉

先说说复现方式 本来input是只能输入数字的&#xff0c;然后你不小心输入了汉字&#xff0c;触发校验了&#xff0c;然后这时候&#xff0c;你发现校验取消不掉了 就这样了 咋办啊&#xff0c;你一看校验没错啊&#xff0c;各种number啥的也写了,发现没问题啊 <el-inputv…

Docker容器启动失败的常见原因分析

我们在开发部署的时候&#xff0c;用 Docker 打包环境&#xff0c;理论上是“我装好了你就能跑”。但理想很丰满&#xff0c;现实往往一 docker run 下去就翻车了。 今天来盘点一下我实际工作中经常遇到的 Docker 容器启动失败的常见原因&#xff0c;顺便给点 debug 的小技巧&a…

立志成为一名优秀测试开发工程师(第七天)——unittest框架的学习

目录 unittest框架的学习 一、测试类的编写 创建相关测试类cal.py、CountTest.py 二、常见断言方法 使用unittest单元测试框架编写测试用例CountTest.py 注意&#xff1a;执行的时候光标一定要放在括号后面&#xff0c;鼠标右键运行 三、对测试环境的初始化和清除模块…

论坛系统(4)

用户详情 获取用户信息 实现逻辑 ⽤⼾提交请求&#xff0c;服务器根据是否传⼊Id参数决定返回哪个⽤⼾的详情 1. 不传⽤⼾Id&#xff0c;返回当前登录⽤⼾的详情(从session获取) 2. 传⼊⽤⼾Id&#xff0c;返回指定Id的⽤⼾详情(根据用户id去查) 俩种方式获得用户信息 参…

力扣面试150题--二叉树的层平均值

Day 54 题目描述 思路 初次做法&#xff08;笨&#xff09;&#xff1a;使用两个队列&#xff0c;一个队列存放树的节点&#xff0c;一个队列存放对应节点的高度&#xff0c;使用x存放上一个节点&#xff0c;highb存放上一个节点的高度&#xff0c;sum存放当前层的节点值之和…

【Doris入门】Doris初识:分布式分析型数据库的核心价值与架构解析

目录 1 Doris简介与核心价值 2 Doris架构深度解析 2.1 Frontend&#xff08;FE&#xff09;架构 2.2 Backend&#xff08;BE&#xff09;架构 3 Doris核心概念详解 3.1 数据分布模型 3.2 Tablet与Replica 3.3 数据模型 4 Doris关键技术解析 4.1 存储引擎 4.2 查询执…

数据结构与算法学习笔记(Acwing 提高课)----动态规划·区间DP

数据结构与算法学习笔记----动态规划区间DP author: 明月清了个风 first publish time: 2025.5.26 ps⭐️区间DP的特征在于子结构一般是一个子区间上的问题&#xff0c;涉及到的问题也非常多&#xff0c;如环形区间&#xff0c;记录方案数&#xff0c;高精度&#xff0c;二维…

从0到1搭建AI绘画模型:Stable Diffusion微调全流程避坑指南

从0到1搭建AI绘画模型&#xff1a;Stable Diffusion微调全流程避坑指南 系统化学习人工智能网站&#xff08;收藏&#xff09;&#xff1a;https://www.captainbed.cn/flu 文章目录 从0到1搭建AI绘画模型&#xff1a;Stable Diffusion微调全流程避坑指南摘要引言一、数据集构…

从一到无穷大 #46:探讨时序数据库Deduplicate与Compaction的设计权衡

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。 本作品 (李兆龙 博文, 由 李兆龙 创作)&#xff0c;由 李兆龙 确认&#xff0c;转载请注明版权。 文章目录 引言Compaction AlgorithmsCompact Execution Flow Based On VeloxLocalMergeSource的…

vue3 导出excel

需求&#xff1a;导出自带格式的excel表格 1.自定义二维数组格式 导出 全部代码&#xff1a; <el-button click"exportExcel">导出</el-button> const exportExcel () > {const data [[商品, 单价, 数量, 总价],[A, 100, 1.55, { t: n, f: B2*C2…

day024-网络基础-TCP与UDP、DNS

文章目录 1. 李导推荐书籍2. OSI七层模型2.1 传输层2.2 网络层2.2.1 问&#xff1a;两端处于不同局域网的设备怎么网络通信&#xff1f; 2.3 数据链路层2.4 物理层2.5 图解OSI七层模型 3. 数据传输模式3.1 全双工3.2 半双工3.3 单工 4. TCP 3次握手4.1 抓包 5. TCP 4次挥手5.1 …