vue实现连线效果

前端实现连线效果

效果展示

借鉴地址
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

父组件内容


<template>
  <div>
  //使用jsplumb的容器
    <div class="content step_2-content" id="efContainer">
      <ul>
      //渲染点位必须要绑定id用于连线
      //左侧盒子
        <template v-for="node in dataContent.nodeList">
          <flowNode :node="node" v-if="node.data === '1'" :id="node.domId" :ids="node.id" nodeTypes="left"
            :value="node.value" />
        </template>
      </ul>
      <ul>
      // 右侧盒子
        <template v-for="node in dataContent.nodeList">
          <flowNode :node="node" v-if="node.data === '2'" :id="node.domId" :ids="node.id" nodeTypes="right"
            :value="node.value" />
        </template>
      </ul>
    </div>
    <!-- 连线的数据 -->
    {{ dataContent.lineList}}
  </div>
</template>
<script setup lang="ts">
import 'jsplumb'
import lodash from 'lodash'
import flowNode from './component/node.vue'
import { ref, toRefs, nextTick, onMounted } from 'vue'
import { jsplumbSetting, jsplumbConnectOptions, jsplumbSourceOptions, jsplumbTargetOptions } from './mixins.js'
import { accessauthorization } from '@/store/index'
type TypeDataArr = {
  name: string
  nodeList: any[]
  lineList: any[]
}
type Typeleft={
  id:string
  name:string
}
type TypeData = {
  drawingContent: any
  dataContent: {
    nodeList: any[]
    lineList: any[]
  }
  DataArr: TypeDataArr
  leftData: Typeleft[]
  rightData: Typeleft[]
}
const AccessAuthorization = accessauthorization()
const data = ref<TypeData>({
  // 连线内容数据
  drawingContent: {},
  // 生成连接块的内容
  dataContent: {
    // 生成连接的盒子
    nodeList: [],
    // 生成连接的线
    lineList: []
  },
  // 模拟左侧放假数据
  leftData: [{
    id: '10',
    name: '国庆放假',
  }, {
    id: '11',
    name: '元旦放假',
  }, {
    id: '12',
    name: '除夕放假',
  }, {
    id: '13',
    name: '劳动放假',
  }],
  // 模拟右侧城市数据
  rightData: [{
    id: '2',
    name: '成都',
  }, {
    id: '3',
    name: '武汉',
  }, {
    id: '4',
    name: '青岛',
  }, {
    id: '5',
    name: '重庆',
  }, {
    id: '6',
    name: '西藏',
  }, {
    id: '7',
    name: '上海',
  }, {
    id: '8',
    name: '北京',
  }],
  // 处理需要展示的数据集
  DataArr: {
    name: 'processB',
    nodeList: [],
    lineList: []
  },
})
const { drawingContent, DataArr, dataContent, leftData, rightData } = toRefs(data.value)

// 加载流程图
const dataReload = (data: TypeDataArr) => {
  dataContent.value.nodeList = []
  dataContent.value.lineList = []
  nextTick(() => {
    data = lodash.cloneDeep(data)
    dataContent.value = data
    nextTick(() => {
      drawingContent.value = jsPlumb.getInstance()
      nextTick(() => {
        jsPlumbInit()
      });
    });
  });
}

const jsPlumbInit = () => {
  drawingContent.value.ready(() => {
    // 导入默认配置
    drawingContent.value.importDefaults(jsplumbSetting)
    // 会使整个jsPlumb立即重绘。
    drawingContent.value.setSuspendDrawing(false, true);
    // 初始化节点
    loadEasyFlow()
    // 点击线
    // drawingContent.value.bind('click', (conn) => {
    //   activeElement.value.type = 'line'
    //   activeElement.value.sourceId = conn.sourceId
    //   activeElement.value.targetId = conn.targetId
    // })
    // 双击线
    drawingContent.value.bind('dblclick', (conn: any) => {
      var conn = drawingContent.value.getConnections({
        source: conn.sourceId,
        target: conn.targetId
      })[0]
      drawingContent.value.deleteConnection(conn)
    })
    // 成功连线
    drawingContent.value.bind("connection", (evt: any) => {
      let from = evt.source.id
      let to = evt.target.id
      // nodeTypes
      let sourceType = evt.source.getAttribute('nodeTypes') as string
      let timeId = evt.source.getAttribute('ids') as string
      let areaIds = evt.target.getAttribute('ids') as string
      // 添加线的内容
      if (sourceType === 'left') {
        dataContent.value.lineList.push({ timeId, areaIds, from, to })
      } else {
        dataContent.value.lineList.push({ timeId: areaIds, areaIds: timeId, from, to })
      }
    })

    // 删除连线回调
    drawingContent.value.bind("connectionDetached", (evt: any) => {
      deleteLine(evt.sourceId, evt.targetId)
    })

    // 改变线的连接节点
    drawingContent.value.bind("connectionMoved", (evt: any) => {
      changeLine(evt.originalSourceId, evt.originalTargetId)
    })

    // 连线右击
    drawingContent.value.bind("contextmenu", (evt: any) => {
      console.log('contextmenu', evt)
    })
    // 连线
    drawingContent.value.bind("beforeDrop", (evt: any) => {
      let leftType: HTMLElement = document.getElementById(evt.sourceId) as HTMLElement;
      let leftids = leftType.getAttribute('nodeTypes')
      var rightType: HTMLElement = document.getElementById(evt.targetId) as HTMLElement;
      let rightids = rightType.getAttribute('nodeTypes')
      //  let rightType =evt.source.getAttribute('nodeTypes') as string
      let from = evt.sourceId
      let to = evt.targetId
      if (from === to) {
        console.log('节点不支持连接自己');
        // this.$message.error('节点不支持连接自己')
        return false
      }
      if (leftids === rightids) {
        console.log('节点同节点不能链接');
        // this.$message.error('节点不支持连接自己')
        return false
      }
      if (hasLine(from, to)) {
        console.log('该关系已存在,不允许重复创建');
        // this.$message.error('该关系已存在,不允许重复创建')
        return false
      }
      if (hashOppositeLine(from, to)) {
        console.log('不支持两个节点之间连线回环');
        // this.$message.error('不支持两个节点之间连线回环');
        return false
      }
      console.log('连接成功');
      // this.$message.success('连接成功')
      return true
    })
    // beforeDetach
    drawingContent.value.bind("beforeDetach", (evt: any) => {
      console.log('beforeDetach', evt)
    })
    // drawingContent.value.setContainer(this.$refs.efContainer)
  })
}
// 是否含有相反的线
const hashOppositeLine = (from: string, to: string) => {
  return hasLine(to, from)
}

// 是否具有该线
const hasLine = (from: string, to: string) => {
  for (var i = 0; i < dataContent.value.lineList.length; i++) {
    var line = dataContent.value.lineList[i]
    if (line.from === from && line.to === to) {
      return true
    }
  }
  return false
}
// 加载流程图
const loadEasyFlow = () => {
  // 初始化节点
  for (var i = 0; i < dataContent.value.nodeList.length; i++) {
    let node = dataContent.value.nodeList[i]
    // 设置源点,可以拖出线连接其他节点
    drawingContent.value.makeSource(node.domId, lodash.merge(jsplumbSourceOptions, {}))
    // drawingContent.value.makeSource(node.id, lodash.merge(jsplumbSourceOptions, {}))
    // // 设置目标点,其他源点拖出的线可以连接该节点
    drawingContent.value.makeTarget(node.domId, jsplumbTargetOptions)
  }
  // 初始化连线
  for (var i = 0; i < dataContent.value.lineList.length; i++) {
    let line: any = dataContent.value.lineList[i]
    var connParam = {
      source: line.from,
      target: line.to,
      label: line.label ? line.label : '',
      connector: line.connector ? line.connector : '',
      anchors: line.anchors ? line.anchors : undefined,
      paintStyle: line.paintStyle ? line.paintStyle : undefined,
    }
    drawingContent.value.connect(connParam, jsplumbConnectOptions)
  }
  // this.$nextTick(function () {
  //   this.loadEasyFlowFinish = true
  // })
}

// 删除线
const deleteLine = (from: string, to: string) => {
  dataContent.value.lineList = dataContent.value.lineList.filter(function (line) {
    if (line.from == from && line.to == to) {
      return false
    }
    return true
  })
}
// 改变连线
const changeLine = (oldFrom: string, oldTo: string) => {
  deleteLine(oldFrom, oldTo)
}
onMounted(() => {
  drawingContent.value = jsPlumb.getInstance() as any
  // 处理数据
  let left = leftData.value.map((item: any, index: number) => {
    return {
      ...item,
      value: index,
      domId: 'leftDom' + index,
      data: "1",
      type: 'task',
    }
  })
  let right = rightData.value.map((item: any, index: number) => {
    return {
      ...item,
      value: index,
      domId: 'rightDom' + index,
      data: "2",
      type: 'task',
    }
  })
  DataArr.value.nodeList = [...left, ...right]
  dataReload(DataArr.value)
  AccessAuthorization.locationEmpty()

})
</script>
<style lang="less" scoped>
.content {
  height: 500px;
  overflow: auto;
  margin-left: 40px;
  position: relative;
  display: flex;
  padding: 16px 0 16px 16px;
  border-radius: 4px;
  border: 1px solid rgba(220, 224, 231, 1);
}
</style>

mixins.js文件

export const jsplumbSetting= {
  // 动态锚点、位置自适应
  Anchors: ['Top', 'TopCenter', 'TopRight', 'TopLeft', 'Right', 'RightMiddle', 'Bottom', 'BottomCenter', 'BottomRight', 'BottomLeft', 'Left', 'LeftMiddle'],
  // 容器ID
  Container: 'efContainer',
  // 连线的样式,直线或者曲线等,可选值:  StateMachine,Flowchart,Bezier,Straight
  Connector: ['Bezier', {curviness: 50}],
  // 鼠标不能拖动删除线
  ConnectionsDetachable: false,
  // 删除线的时候节点不删除
  DeleteEndpointsOnDetach: false,

  /**
   * 空白端点
   */
  Endpoint: ['Blank', {Overlays: ''}],
  // Endpoints: [['Dot', {radius: 5, cssClass: 'ef-dot', hoverClass: 'ef-dot-hover'}], ['Rectangle', {height: 20, width: 20, cssClass: 'ef-rectangle', hoverClass: 'ef-rectangle-hover'}]],
  /**
   * 连线的两端端点样式
   * fill: 颜色值,如:#12aabb,为空不显示
   * outlineWidth: 外边线宽度
   */
  EndpointStyle: {fill: '#1879ffa1', outlineWidth: 1},
  // 是否打开jsPlumb的内部日志记录
  LogEnabled: true,
  /**
   * 连线的样式
   */
  PaintStyle: {
      // 线的颜色
      stroke: '#1E93FF',
      // 线的粗细,值越大线越粗
      strokeWidth: 2,
      // 设置外边线的颜色,默认设置透明,这样别人就看不见了,点击线的时候可以不用精确点击,参考 https://blog.csdn.net/roymno2/article/details/72717101
      outlineStroke: 'transparent',
      // 线外边的宽,值越大,线的点击范围越大
      outlineWidth: 10
  },
  DragOptions: {cursor: 'pointer', zIndex: 2000},
  /**
   *  叠加 参考: https://www.jianshu.com/p/d9e9918fd928
   */
  Overlays: [
    
      // 箭头叠加
      ['Arrow', {
          width: 1, // 箭头尾部的宽度
          length: 8, // 从箭头的尾部到头部的距离
          location: 1, // 位置,建议使用0~1之间
          direction: 1, // 方向,默认值为1(表示向前),可选-1(表示向后)
          foldback: 0.623 // 折回,也就是尾翼的角度,默认0.623,当为1时,为正三角
      }],
      ['Label', {
          label: '',
          location: 0.1,
          cssClass: 'aLabel'
      }]
  ],
  // 绘制图的模式 svg、canvas
  RenderMode: 'svg',
  // 鼠标滑过线的样式
  HoverPaintStyle: {stroke: 'red', strokeWidth: 3},
  // 滑过锚点效果
  // EndpointHoverStyle: {fill: 'red'}
  Scope: 'jsPlumb_DefaultScope' // 范围,具有相同scope的点才可连接
}
/**
* 连线参数
*/
export const  jsplumbConnectOptions={
  isSource: true,
  isTarget: true,
  // 动态锚点、提供了4个方向 Continuous、AutoDefault
  anchor: 'AutoDefault',
  // 设置连线上面的label样式
  labelStyle: {
      cssClass: 'flowLabel'
  },
  // 修改了jsplumb 源码,支持label 为空传入自定义style
  emptyLabelStyle: {
      cssClass: 'emptyFlowLabel'
  }
}
/**
* 源点配置参数
*/
export const  jsplumbSourceOptions= {
  // 设置可以拖拽的类名,只要鼠标移动到该类名上的DOM,就可以拖拽连线
  filter: '.flow-node-drag',
  filterExclude: false,
  anchor: 'Continuous',
  // 是否允许自己连接自己
  allowLoopback: true,
  maxConnections: -1,
  onMaxConnections: function (info, e) {
      console.log(`超过了最大值连线: ${info.maxConnections}`)
  }
}
export const  jsplumbTargetOptions= {
  // 设置可以拖拽的类名,只要鼠标移动到该类名上的DOM,就可以拖拽连线
  filter: '.flow-node-drag',
  filterExclude: false,
  // 是否允许自己连接自己
  anchor: 'Continuous',
  allowLoopback: true,
  dropOptions: {hoverClass: 'ef-drop-hover'}
}

flowNode组件

<template>
    <li class="content-left node-data ef-node-container" v-if="node.data === '1'" ref="location">
        <!-- 左侧box内容展示 -->
        <div>
            <span>{{ node.name }}</span>
        </div>
        <div class="ef-node-left-ico flow-node-drag left-dot"></div>
    </li>
    <li class="content-right node-data ef-node-container" v-else ref="location">
        <!-- 右侧box内容展示 -->
        <div class="ef-node-left-ico flow-node-drag right-dot"></div>
        {{ node.name }}
    </li>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
// accessauthorization 是用pinia存储的数据,也可以用vuex代替
import { accessauthorization } from '@/store/index'
// AccessAuthorization内容为左侧容器的高度
const AccessAuthorization = accessauthorization()
const location = ref<HTMLElement | string>('')
type TypeProps = {
    node: any,
}
const props = withDefaults(defineProps<TypeProps>(), {
    node: {},
})
const { node } = props
onMounted(() => {
    if (location.value) {
        let data = location.value as any
        // 获取元素 nodeTypes属性 nodeTypes内容为 left或right
        let azimuth = data.getAttribute('nodeTypes')
        // 判断AccessAuthorization是否数组有值 进行计算总和
        let sum: number = AccessAuthorization.location.length > 0 ? AccessAuthorization.location.reduce((total: number, current: number) => total + current) : 0
        // 判断是左侧进行定位top赋值
        if (azimuth === 'left') {
            data && (data.style.top = sum + (25 * (data.value + 1)) + 'px')
            // 存储左侧box高度 因为左侧盒子是未知的高度,右侧盒子是已知高度
            AccessAuthorization.LocationChange(data.clientHeight)
        }
        // 判断右侧进行定位top赋值
        else {
            data && (data.style.top = (data.value * data.clientHeight) + (33 * (data.value + 1)) + 'px')
        }
    }
})
</script>
<style lang="less" scoped>
.content {
    display: flex;
    padding: 16px 0 20px 16px;
    border-radius: 4px;
    border: 1px solid rgba(220, 224, 231, 1);
    .left-dot,
    .right-dot {
        position: absolute;
        width: 10px;
        height: 10px;
        border-radius: 50%;
        right: -3px;
        top: 50%;
        transform: translateY(-50%);
        background-color: rgba(30, 147, 255, 1);
    }

    .right-dot {
        right: 0;
        left: -3px;
    }

    &-left {
        padding: 19px 21px 22px 25px;
        width: 517px;
        background-color: #F5F5F5;
        font: 14px Arial-regular;
        color: rgba(81, 90, 110, 1);

        >div:nth-child(1) {
            >span:first-child {
                font: 16px Arial-regular;
                margin-right: 6px;
            }
            >span:last-child {
                color: rgba(145, 145, 145, 1);
            }
        }
    }
    &-right {
        left: 605px;
        margin: 0 0 33px 73px;
        padding-left: 29px;
        width: 517px;
        height: 74px;
        font: 14px / 74px Arial-regular;
        background-color: #F5F5F5;
        color: rgba(81, 90, 110, 1);
    }
    .ef-node-container {
        position: absolute;
    }
}
</style>

store内容

// 记得要在main.ts里面进行注册哦
import { createPinia } from 'pinia'
import { accessauthorization } from './modules/accessauthorization'
const pinia = createPinia()
export { accessauthorization }
export default pinia

//-------accessauthorization需要再次创建文件
import { defineStore } from 'pinia';
type accessauthorizationType = {
  location: number[]
}
export const accessauthorization = defineStore({
  id: 'accessauthorization',
  state: (): accessauthorizationType => ({
    // 
    location: [],
  }),
  actions: {
    locationEmpty(){
      this.location=[]
    },
    LocationChange(num:number) {
      this.location.push(num)
    }
  }
})

  • 1
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值