jsPlumb设置流程图

1,896 阅读1分钟

image.png


<template>
  <div v-loading="loading" v-if="easyFlowVisible" class="box">
    <div id="flowContainer" class="container">
      <template v-for="node in data.nodeList">
        <flow-node
          v-show="node.show"
          :id="node.id"
          :node="node"
          :key="node.id"
          :nodeData="nodeData"
          @deleteNode="deleteNode"
          @changeNodeSite="changeNodeSite"
          @nodeRightMenu="nodeRightMenu"
          @editNode="editNode"
        ></flow-node>
      </template>
    </div>

    <flow-info v-if="flowInfoVisible" ref="flowInfo" :data="data"></flow-info>
    <flow-node-form v-if="nodeFormVisible" ref="nodeForm"></flow-node-form>

    <div class="mask"></div>
  </div>
</template>

<script>
import draggable from 'vuedraggable'
import { jsPlumb } from 'jsplumb'
import flowNode from '@/components/flow/node'
import flowTool from '@/components/flow/tool'
import FlowInfo from '@/components/flow/info'
import FlowNodeForm from './node_form'
import lodash from 'lodash'
import API from '@/api/build-management/index'
import { SvgTemperature } from '@hui/svg-icon'

export default {
  name: 'easyFlow',
  data () {
    return {
      loading: false,
      jsPlumb: null, // jsPlumb 实例
      easyFlowVisible: true,
      flowInfoVisible: false,
      nodeFormVisible: false,
      index: 1,
      // 默认设置参数
      jsplumbSetting: {
        // 动态锚点、位置自适应
        Anchors: [
          'Top',
          'TopCenter',
          'TopRight',
          'TopLeft',
          'Right',
          'RightMiddle',
          'Bottom',
          'BottomCenter',
          'BottomRight',
          'BottomLeft',
          'Left',
          'LeftMiddle'
        ],
        Container: 'flowContainer',
        // 连线的样式 StateMachine、Flowchart
        Connector: 'Flowchart',
        // 鼠标不能拖动删除线
        ConnectionsDetachable: false,
        // 删除线的时候节点不删除
        DeleteEndpointsOnDetach: false,
        // 连线的端点
        // Endpoint: ["Dot", {radius: 5}],
        Endpoint: ['Rectangle', { height: 10, width: 10 }],
        // 线端点的样式
        EndpointStyle: { fill: 'rgba(255,255,255,0)', outlineWidth: 1 },
        LogEnabled: true, // 是否打开jsPlumb的内部日志记录
        // 绘制线
        PaintStyle: { stroke: 'black', strokeWidth: 3 },
        // 绘制箭头
        Overlays: [['Arrow', { width: 12, length: 12, location: 1 }]],
        RenderMode: 'svg'
      },
      // jsplumb连接参数
      jsplumbConnectOptions: {
        isSource: true,
        isTarget: true
        // 动态锚点、提供了4个方向 Continuous、AutoDefault
        // anchor: 'Continuous'
        // anchor: 'AutoDefault'
      },
      jsplumbSourceOptions: {
        filter:
          '.flow-node-drag' /* "span"表示标签,".className"表示类,"#id"表示元素id */,
        filterExclude: false,
        anchor: 'Continuous',
        allowLoopback: false
      },
      jsplumbTargetOptions: {
        filter:
          '.flow-node-drag' /* "span"表示标签,".className"表示类,"#id"表示元素id */,
        filterExclude: false,
        anchor: 'Continuous',
        allowLoopback: false
      },
      // 是否加载完毕
      loadEasyFlowFinish: false,
      // 数据
      data: {},
      nodeData: {},
      levelTreeData: []
      // exampleGreyEndpointOptions: {
      //   endpoint: 'Rectangle',
      //   paintStyle: { width: 25, height: 21, fillStyle: '#666' },
      //   isSource: true,
      //   connectorStyle: { strokeStyle: '#666' },
      //   isTarget: true
      // }
    }
  },
  props: {
    flowData: Object
  },
  components: {
    draggable,
    flowNode,
    flowTool,
    FlowInfo,
    FlowNodeForm
  },
  watch: {
    flowData: {
      // 图例在展示的时候,改变buildLevel或idAndNameDtos都会更新图例
      handler: async function (val, oldVal) {
        this.loading = true
        const levelTreeRes = await API.levelTree()
        this.levelTreeData = levelTreeRes.data
        this.dataReloadC(this.transformData(val))
        this.nodeData = val
        setTimeout(() => {
          this.loading = false
        }, 500)
      },
      deep: true,
      immediate: true
    }
  },
  created () {
    this.loading = true
  },
  async mounted () {
    const levelTreeRes = await API.levelTree()
    this.levelTreeData = levelTreeRes.data
    this.jsPlumb = jsPlumb.getInstance()
    this.$nextTick(() => {
      // const data = {
      //   name: '流程',
      //   nodeList: [
      //     {
      //       id: '1',
      //       name: '园区',
      //       left: '100px',
      //       top: '15px',
      //       ico: 'h-icon-user',
      //       show: true
      //     },
      //     {
      //       id: '2',
      //       name: '学校',
      //       left: '100px',
      //       top: '140px',
      //       ico: 'h-icon-user',
      //       show: true
      //     },
      //     {
      //       id: '3',
      //       name: '建筑',
      //       left: '100px',
      //       top: '265px',
      //       ico: 'h-icon-user',
      //       show: true
      //     },
      //     {
      //       id: '4',
      //       name: '楼栋',
      //       left: '100px',
      //       top: '390px',
      //       ico: 'h-icon-user',
      //       show: true
      //     },
      //     {
      //       id: '5',
      //       name: '楼层',
      //       left: '100px',
      //       top: '515px',
      //       ico: 'h-icon-user',
      //       show: true
      //     },
      //     {
      //       id: '6',
      //       name: '房间',
      //       left: '100px',
      //       top: '640px',
      //       ico: 'h-icon-user',
      //       show: true
      //     }
      //   ],
      //   lineList: [
      //     {
      //       from: '1',
      //       to: '2'
      //     },
      //     {
      //       from: '2',
      //       to: '3'
      //     },
      //     {
      //       from: '3',
      //       to: '4'
      //     },
      //     {
      //       from: '4',
      //       to: '5'
      //     },
      //     {
      //       from: '5',
      //       to: '6'
      //     },
      //     {
      //       from: '3',
      //       to: '6',
      //       anchor: ['Right', 'Left']
      //     },
      //     {
      //       from: '3',
      //       to: '5',
      //       anchor: ['Right', 'Left']
      //     }
      //   ]
      // }
      this.nodeData = this.flowData
      this.dataReloadC(this.transformData(this.flowData))
      this.loading = false
    })
  },
  methods: {
    transformData (data) {
      // const data = this.flowData
      // const buildLevelListTemp = this.levelTreeData
      const buildLevelListTemp = this.levelTreeData
      const sortFun = (a, b) => a.buildLevel - b.buildLevel
      const buildLevelList = buildLevelListTemp.sort(sortFun)
      // 生成节点
      const res = {
        name: '流程',
        nodeList: [],
        lineList: []
      }
      res.nodeList = buildLevelList.map((item, index) => {
        return {
          id: item.buildTypeId,
          name: item.buildName,
          left: '100px',
          top: `${15 + 125 * (item.buildLevel - 1)}px`,
          ico: 'h-icon-user',
          show: true,
          buildLevel: item.buildLevel
        }
      })
      res.nodeList.forEach((item, index) => {
        if (index + 1 < res.nodeList.length) {
          res.lineList.push({
            from: item.id,
            to: res.nodeList[index + 1].id
          })
        }
      })
      if (
        Array.isArray(data.hierarchyForm.idAndNameDtos) &&
        data.hierarchyForm.idAndNameDtos.length > 0
      ) {
        data.hierarchyForm.idAndNameDtos.forEach((item, index) => {
          if (
            this.levelTreeData[data.hierarchyForm.buildLevel - 1] !== undefined
          ) {
            res.lineList.push({
              from: this.levelTreeData[data.hierarchyForm.buildLevel - 1]
                .buildTypeId,
              to: item,
              anchor: ['Right', 'Left'],
              paintStyle: { stroke: '#E72528', strokeWidth: 2 }
            })
          }
        })
      }
      return res
    },
    jsPlumbInit () {
      const _this = this
      this.jsPlumb.ready(function () {
        // 导入默认配置
        _this.jsPlumb.importDefaults(_this.jsplumbSetting)
        // 会使整个jsPlumb立即重绘。
        _this.jsPlumb.setSuspendDrawing(false, true)
        // 初始化节点
        _this.loadEasyFlow()

        // 单点击了连接线,
        _this.jsPlumb.bind('click', function (conn, originalEvent) {
          console.log('click', conn)

          _this
            .$confirm('确定删除所点击的线吗?', '提示', {
              confirmButtonText: '确定',
              cancelButtonText: '取消',
              type: 'warning'
            })
            .then(() => {
              _this.jsPlumb.deleteConnection(conn)
            })
            .catch(() => {})
        })
        // 连线
        _this.jsPlumb.bind('connection', function (evt) {
          console.log('connection', evt)
          const from = evt.source.id
          const to = evt.target.id
          if (_this.loadEasyFlowFinish) {
            _this.lineList.push({
              from: from,
              to: to
            })
          }
        })

        // 删除连线
        _this.jsPlumb.bind('connectionDetached', function (evt) {
          console.log('connectionDetached', evt)
          _this.deleteLine(evt.sourceId, evt.targetId)
        })

        // 改变线的连接节点
        _this.jsPlumb.bind('connectionMoved', function (evt) {
          console.log('connectionMoved', evt)
          _this.changeLine(evt.originalSourceId, evt.originalTargetId)
        })

        // 单击endpoint
        // jsPlumb.bind("endpointClick", function (evt) {
        //   console.log('endpointClick', evt)
        // })
        //
        // // 双击endpoint
        // jsPlumb.bind("endpointDblClick", function (evt) {
        //   console.log('endpointDblClick', evt)
        // })

        // contextmenu
        _this.jsPlumb.bind('contextmenu', function (evt) {
          console.log('contextmenu', evt)
        })

        // beforeDrop
        _this.jsPlumb.bind('beforeDrop', function (evt) {
          console.log('beforeDrop', evt)
          _this.$message.error('beforeDrop')
          _this.$message({
            message: '恭喜你,这是一条成功消息',
            type: 'success'
          })
          const from = evt.sourceId
          const to = evt.targetId
          if (from === to) {
            _this.$message.error('不能连接自己')
            return false
          }
          if (_this.hasLine(from, to)) {
            _this.$message.error('不能重复连线')
            return false
          }
          if (_this.hashOppositeLine(from, to)) {
            _this.$message.error('不能回环哦')
            return false
          }
          return true
        })

        // beforeDetach
        _this.jsPlumb.bind('beforeDetach', function (evt) {
          console.log('beforeDetach', evt)
        })
      })
    },
    // 加载流程图
    loadEasyFlow () {
      // 初始化节点
      for (var i = 0; i < this.data.nodeList.length; i++) {
        const node = this.data.nodeList[i]
        console.log(node)
        // 设置源点,可以拖出线连接其他节点
        this.jsPlumb.makeSource(node.id, this.jsplumbSourceOptions)
        // // 设置目标点,其他源点拖出的线可以连接该节点
        this.jsPlumb.makeTarget(node.id, this.jsplumbTargetOptions)
        // this.jsPlumb.addEndpoint(node.id, this.exampleGreyEndpointOptions)
        // 设置可拖拽
        // jsPlumb.draggable(node.id, {
        //     containment: 'parent',
        //     grid: [10, 10]
        // })

        this.jsPlumb.draggable(node.id, {
          containment: 'parent'
        })

        // jsPlumb.draggable(node.id)
      }

      // 初始化连线
      for (var i = 0; i < this.data.lineList.length; i++) {
        const line = this.data.lineList[i]
        this.jsPlumb.connect(
          {
            source: line.from,
            target: line.to
          },
          Object.assign({
            ...this.jsplumbConnectOptions,
            anchor: line.anchor,
            paintStyle:
              line.paintStyle === undefined
                ? { stroke: 'lightgray', strokeWidth: 1 }
                : line.paintStyle
            // connector: ['Bezier']
          })
        )
      }
      this.$nextTick(function () {
        this.loadEasyFlowFinish = true
      })
    },
    getNodes () {
      console.log(jsPlumb)
      console.log(jsPlumb.Defaults)
    },
    getLines () {
      console.log('线', jsPlumb.getConnections())
    },
    // 删除线
    deleteLine (from, to) {
      this.data.lineList = this.data.lineList.filter(function (line) {
        return line.from !== from && line.to !== to
      })
    },
    // 改变连线
    changeLine (oldFrom, oldTo) {
      this.deleteLine(oldFrom, oldTo)
    },
    // 改变节点的位置
    changeNodeSite (data) {
      for (var i = 0; i < this.data.nodeList.length; i++) {
        const node = this.data.nodeList[i]
        if (node.id === data.nodeId) {
          node.left = data.left
          node.top = data.top
        }
      }
    },
    // 添加新的节点
    addNode (evt, nodeMenu, mousePosition) {
      console.log('添加节点', evt, nodeMenu)
      const width = this.$refs.flowTool.$el.clientWidth
      const index = this.index++
      const nodeId = 'node' + index
      var left = mousePosition.left
      var top = mousePosition.top
      if (mousePosition.left < 0) {
        left = evt.originalEvent.layerX - width
      }
      if (mousePosition.top < 0) {
        top = evt.originalEvent.clientY - 50
      }
      var node = {
        id: 'node' + index,
        name: '节点' + index,
        left: left + 'px',
        top: top + 'px',
        ico: nodeMenu.ico,
        show: true
      }
      this.data.nodeList.push(node)
      this.$nextTick(function () {
        this.jsPlumb.makeSource(nodeId, this.jsplumbSourceOptions)

        this.jsPlumb.makeTarget(nodeId, this.jsplumbTargetOptions)

        this.jsPlumb.draggable(nodeId, {
          containment: 'parent'
        })
      })
    },
    // 是否具有该线
    hasLine (from, to) {
      for (var i = 0; i < this.data.lineList.length; i++) {
        var line = this.data.lineList[i]
        if (line.from === from && line.to === to) {
          return true
        }
      }
      return false
    },
    // 是否含有相反的线
    hashOppositeLine (from, to) {
      return this.hasLine(to, from)
    },
    nodeRightMenu (nodeId, evt) {
      this.menu.show = true
      this.menu.curNodeId = nodeId
      this.menu.left = evt.x + 'px'
      this.menu.top = evt.y + 'px'
    },
    deleteNode (nodeId) {
      this.$confirm('确定要删除节点' + nodeId + '?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning',
        closeOnClickModal: false
      })
        .then(() => {
          this.data.nodeList = this.data.nodeList.filter(function (node) {
            // return node.id !== nodeId
            if (node.id === nodeId) {
              node.show = false
            }
            return true
          })
          this.$nextTick(function () {
            console.log('删除' + nodeId)
            this.jsPlumb.removeAllEndpoints(nodeId)
          })
        })
        .catch(() => {})
      return true
    },
    editNode (nodeId) {
      console.log('编辑节点', nodeId)
      this.nodeFormVisible = true
      this.$nextTick(function () {
        this.$refs.nodeForm.init(this.data, nodeId)
      })
    },
    // 流程数据信息
    dataInfo () {
      this.flowInfoVisible = true
      this.$nextTick(function () {
        this.$refs.flowInfo.init()
      })
    },
    dataReload (data) {
      this.easyFlowVisible = false
      this.data.nodeList = []
      this.data.lineList = []
      this.$nextTick(() => {
        // 这里模拟后台获取数据、然后加载
        data = lodash.cloneDeep(data)
        this.easyFlowVisible = true
        this.data = data
        this.$nextTick(() => {
          this.jsPlumb = jsPlumb.getInstance()
          this.$nextTick(() => {
            this.jsPlumbInit()
          })
        })
      })
    },
    // 数据重新载入
    dataReloadC (data) {
      this.dataReload(data)
    }
  }
}
</script>

<style lang="scss" scoped>
.box {
  height: calc(100% - 35px);
  position: relative;
  .mask {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    z-index: 1000;
  }
  .container {
    // background-image: linear-gradient(
    //     90deg,
    //     rgba(0, 0, 0, 0.15) 10%,
    //     rgba(0, 0, 0, 0) 10%
    //   ),
    //   linear-gradient(rgba(0, 0, 0, 0.15) 10%, rgba(0, 0, 0, 0) 10%);
    background-size: 10px 10px;
    height: 100%;
    background-color: rgb(251, 251, 251);
    /*background-color: #42b983;*/
    position: relative;
  }
  .labelClass {
    background-color: white;
    padding: 5px;
    opacity: 0.7;
    border: 1px solid #346789;
    /*border-radius: 10px;*/
    cursor: pointer;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
  }
}
</style>

flowNode

<template>
  <div
    ref="node"
    :style="flowNodeContainer"
    @mouseenter="showDelete"
    @mouseleave="hideDelete"
    @mouseup="changeNodeSite"
  >
    <!-- <div class="flow-node-header"> -->
    <!--左上角的那个图标样式-->
    <!-- <i :class="nodeClass"></i> -->
    <!--鼠标移入到节点中时右上角的【编辑】、【删除】 按钮-->
    <!-- <div style="position: absolute;top: 0px;right: 0px;line-height: 25px" v-show="mouseEnter">
        <a href="#" style @click="editNode">
          <img src="@/assets/edit.png" />
        </a>&nbsp;
        <a href="#" style @click="deleteNode">
          <img src="@/assets/delete.png" />
        </a> &nbsp;
    </div>-->
    <!-- </div> -->
    <!--节点的正文部分-->
    <!-- <div class="flow-node-body">{{node.name}}</div> -->
    <div class="box">
      <div :class="['flow-node',activeStyle]">
        <div class="flow-node-left">
          <img src="@/assets/img_层级编号.png" alt />
          <span>{{node.buildLevel}}</span>
        </div>
        <div class="flow-node-right">
          <span>{{node.name}}</span>
        </div>
      </div>
      <div v-if="activeDot" class="dot"></div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    node: Object,
    nodeData: Object
  },
  data () {
    return {
      // 控制节点操作显示
      mouseEnter: false
    }
  },
  computed: {
    // 节点容器样式
    flowNodeContainer: {
      get () {
        return {
          position: 'absolute',
          width: '274px',
          top: this.node.top,
          left: this.node.left,
          boxShadow: this.mouseEnter ? '#66a6e0 0px 0px 12px 0px' : '',
          backgroundColor: 'transparent'
        }
      }
    },
    nodeClass () {
      var nodeclass = {}
      nodeclass[this.node.ico] = true
      nodeclass['flow-node-drag'] = true
      return nodeclass
    },
    activeStyle () {
      return this.node.buildLevel === this.nodeData.hierarchyForm.buildLevel ? 'active' : ''
    },
    activeDot () {
      return (this.nodeData.hierarchyForm.idAndNameDtos.length > 0) && (
        this.node.buildLevel === this.nodeData.hierarchyForm.buildLevel || this.nodeData.hierarchyForm.idAndNameDtos.find(
          id => {
            return id === this.node.id
          }
        )
      )
    }
  },
  watch: {
  },
  mounted () {
    console.log(this.node)
    console.log(this.nodeData)
  },
  methods: {
    // 删除节点
    deleteNode () {
      this.$emit('deleteNode', this.node.id)
    },
    // 编辑节点
    editNode () {
      this.$emit('editNode', this.node.id)
    },
    // 鼠标进入
    showDelete () {
      this.mouseEnter = true
    },
    // 鼠标离开
    hideDelete () {
      this.mouseEnter = false
    },
    // 鼠标移动后抬起
    changeNodeSite () {
      // 避免抖动
      if (this.node.left == this.$refs.node.style.left && this.node.top == this.$refs.node.style.top) {
        return
      }
      this.$emit('changeNodeSite', {
        nodeId: this.node.id,
        left: this.$refs.node.style.left,
        top: this.$refs.node.style.top
      })
    }
  }
}
</script>

<style lang="scss" scoped>
.box {
  width: 100%;
  padding-right: 18px;
  position: relative;
  .dot {
    position: absolute;
    right: 0px;
    top: 18px;
    width: 14px;
    height: 14px;
    border-radius: 7px;
    background: #e72528;
    &:after {
      content: '';
      width: 4px;
      height: 4px;
      border-radius: 2px;
      background: #fff;
      position: absolute;
      right: 5px;
      top: 5px;
    }
  }
  .flow-node {
    width: 100%;
    height: 50px;
    background: #ffffff;
    border: 1px solid rgba(0, 0, 0, 0.12);
    border-radius: 2px;
    overflow: hidden;
    position: relative;
    .flow-node-left {
      width: 48px;
      height: 48px;
      // background: rgba(0, 0, 0, 0.9);
      border-radius: 2px NaNpx 2px;
      float: left;
      position: relative;
      img {
        position: relative;
        // top: 2px;
      }
      span {
        position: absolute;
        left: 18px;
        top: 11px;
        width: 8px;
        height: 26px;
        font-family: PingFangSC-Medium;
        font-size: 18px;
        color: rgba(255, 255, 255, 0.9);
        line-height: 26px;
        font-weight: 500;
      }
    }
    .flow-node-right {
      height: 48px;
      line-height: 50px;
      font-family: PingFangSC-Regular;
      font-size: 14px;
      color: rgba(0, 0, 0, 0.7);
      line-height: 20px;
      font-weight: 400;
      float: left;
      span {
        display: inline-block;
        height: 48px;
        line-height: 48px;
        margin-left: 16px;
        max-width: 188px;
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
      }
    }
  }
  .active {
    background: rgba(231, 37, 40, 0.2);
    border: 2px solid #e72528;
    box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12),
      0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
    border-radius: 2px;
    position: relative;
  }
}

// .flow-node-drag {
//   width: 25px;
//   height: 25px;
// }

// .flow-node-header {
//   background-color: #66a6e0;
//   height: 25px;
//   cursor: pointer;
//   border-top-left-radius: 6px;
//   border-top-right-radius: 6px;
// }

// .flow-node-header a {
//   text-decoration: none;
//   line-height: 25px;
//   vertical-align: middle;
// }

// .flow-node-header a img {
//   line-height: 25px;
//   vertical-align: middle;
//   margin-bottom: 5px;
// }

// .flow-node-body {
//   /*background-color: beige;*/
//   background-color: white;
//   text-align: center;
//   cursor: pointer;
//   height: 25px;
//   line-height: 25px;
//   border-bottom-left-radius: 6px;
//   border-bottom-right-radius: 6px;
// }
</style>