Code前端首页关于Code前端联系我们

Vue2 做流程图,该选啥技术方案?

terry 8小时前 阅读数 14 #Vue
文章标签 Vue2 流程图

前端项目里要做流程图(Flowchart),Vue2 技术栈该从哪入手?选啥库、咋处理节点连线、交互功能咋实现……这些问题是不是让你头大?今天咱就围绕 Vue2 流程图开发,从技术方案到实战案例,把关键环节拆碎了讲明白,新手也能跟着一步步搞懂~

先明确需求:要是项目赶时间、功能常规(比如节点拖拽、连线、基础样式),直接用现成 Vue 生态的流程图库最省心,像 vue-flowchart-editor,专门为 Vue 设计,开箱即用,数据驱动渲染,节点和连线的增删改查都有封装好的逻辑;还有 jsPlumb,它不是 Vue 专属,但跨框架通用,灵活性高,适合需要高度定制连线逻辑(比如曲线样式、锚点位置)的场景。

要是产品需求特别“奇葩”——比如要做流程图 + 时序图结合,或者节点里嵌套复杂表单、动画效果,那得自己基于 SVG 或 Canvas 封装,SVG 优势是 DOM 化,节点和连线能当组件写,样式用 CSS 搞;Canvas 则是在 canvas 标签里用 JavaScript 绘图,性能好但开发复杂度高,适合节点数量爆炸(比如上百个节点)的场景。

总结下:快速落地选现成库,追求独特体验自己造轮子,选库时先看 GitHub 星数、维护频率,再跑官方示例,确认 API 符合需求~

从零开始,Vue2 项目咋搭流程图基础环境?

vue-flowchart-editor 为例,步骤特简单:

  1. 建项目:用 vue-cli 建 Vue2 项目(命令行输 vue create my-flowchart-app,选 Vue2 模板)。
  2. 装依赖:进入项目目录,执行 npm i vue-flowchart-editor
  3. 全局注册:打开 src/main.js,加两行:
    import VueFlowChart from 'vue-flowchart-editor'
    import 'vue-flowchart-editor/dist/vue-flowchart-editor.css'
    Vue.use(VueFlowChart)
  4. 写第一个流程图组件:新建 src/components/FlowChartDemo.vue,模板里放:
    <template>
      <div class="flow-demo">
        <flow-chart :nodes="nodes" :edges="edges" />
      </div>
    </template>
    <script>
    export default {
      data() {
        return {
          nodes: [
            { id: 'node1', label: '起始节点', x: 100, y: 100 },
            { id: 'node2', label: '结束节点', x: 300, y: 100 }
          ],
          edges: [
            { source: 'node1', target: 'node2' }
          ]
        }
      }
    }
    </script>
    <style scoped>
    .flow-demo { width: 800px; height: 600px; }
    </style>

    运行项目,浏览器里就能看到两个节点连了条线~这就是最基础的流程图架子,数据里的 nodes 存节点信息(位置、内容),edges 存连线关系(谁连谁)。

流程图核心的“节点”和“连线”,Vue2 里咋渲染?

不管用库还是自己写,核心逻辑都是数据驱动视图

节点渲染:

每个节点是独立组件(Node.vue),通过 props 接收 nodeData(包含 idlabelxy 这些),模板里用 divsvg 元素当容器,通过 :style="{ left: nodeData.x + 'px', top: nodeData.y + 'px' }" 定位,复杂节点还能加插槽,让用户自定义内容(比如节点里放按钮、输入框)。

举个自定义节点的简化例子:

<template>
  <div 
    class="custom-node" 
    :style="{ left: nodeData.x + 'px', top: nodeData.y + 'px' }"
    @mousedown="handleDragStart"
  >
    <div class="node-label">{{ nodeData.label }}</div>
    <button @click="handleDelete">删除</button>
  </div>
</template>
<script>
export default {
  props: ['nodeData'],
  methods: {
    handleDelete() {
      this.$emit('delete-node', this.nodeData.id)
    },
    handleDragStart() {
      // 拖拽逻辑后面讲,这里先抛事件
      this.$emit('drag-start', this.nodeData)
    }
  }
}
</script>
<style scoped>
.custom-node { 
  border: 1px solid #333; 
  padding: 8px; 
  position: absolute; 
  cursor: move; 
}
</style>

连线渲染:

连线本质是“两个节点之间的视觉连接”,用 SVG 的话,写个 Edge.vue 组件,props 接收 sourceNode(起点节点数据)和 targetNode(终点节点数据),模板里用 <svg><path /></svg>,通过计算起点和终点的坐标,动态生成 pathd 属性(比如直线:M {sourceX} {sourceY} L {targetX} {targetY})。

简化的连线组件:

<template>
  <svg class="edge-svg" :style="{ position: 'absolute' }">
    <path 
      :d="getEdgePath()" 
      stroke="black" 
      stroke-width="2" 
      fill="none"
    />
  </svg>
</template>
<script>
export default {
  props: ['sourceNode', 'targetNode'],
  methods: {
    getEdgePath() {
      const { x: sx, y: sy } = this.sourceNode
      const { x: tx, y: ty } = this.targetNode
      return `M ${sx} ${sy} L ${tx} ${ty}`
    }
  }
}
</script>
<style scoped>
.edge-svg { pointer-events: none; } /* 避免遮挡节点交互 */
</style>

然后在父组件里,用 v-for 遍历 nodesedges,渲染自定义节点和连线:

<template>
  <div class="flow-container">
    <custom-node 
      v-for="node in nodes" 
      :key="node.id" 
      :nodeData="node"
      @delete-node="handleDeleteNode"
      @drag-start="handleDragStart"
    />
    <edge 
      v-for="edge in edges" 
      :key="edge.id" 
      :sourceNode="getNodeById(edge.source)"
      :targetNode="getNodeById(edge.target)"
    />
  </div>
</template>
<script>
import CustomNode from './Node.vue'
import Edge from './Edge.vue'
export default {
  components: { CustomNode, Edge },
  data() {
    return {
      nodes: [...],
      edges: [...]
    }
  },
  methods: {
    getNodeById(id) {
      return this.nodes.find(n => n.id === id)
    },
    handleDeleteNode(id) {
      // 删除节点逻辑:从nodes数组里删,同时删关联的edges
      this.nodes = this.nodes.filter(n => n.id !== id)
      this.edges = this.edges.filter(e => e.source !== id && e.target !== id)
    },
    handleDragStart(node) {
      // 后续讲拖拽时更新位置
    }
  }
}
</script>

流程图的交互功能,Vue2 咋实现拖拽、连线编辑?

这部分是流程图“活起来”的关键,分两块讲:

节点拖拽:

思路是监听鼠标事件,实时更新节点坐标,给节点加 mousedown 事件,记录初始鼠标位置和节点初始位置;然后在 document 上监听 mousemove,计算鼠标移动的偏移量,更新节点的 xymouseup 时移除 mousemove 监听。

在之前的 CustomNode.vue 里完善拖拽逻辑:

<script>
export default {
  // ...其他代码
  data() {
    return {
      startX: 0,
      startY: 0,
      startNodeX: 0,
      startNodeY: 0
    }
  },
  methods: {
    handleDragStart(e) {
      // 记录初始位置
      this.startX = e.clientX
      this.startY = e.clientY
      this.startNodeX = this.nodeData.x
      this.startNodeY = this.nodeData.y
      document.addEventListener('mousemove', this.handleDrag)
      document.addEventListener('mouseup', this.handleDragEnd)
    },
    handleDrag(e) {
      // 计算偏移量
      const dx = e.clientX - this.startX
      const dy = e.clientY - this.startY
      // 更新节点数据(注意:要触发Vue响应式,直接改对象属性可能不行,要用$set或者替换对象)
      this.$set(this.nodeData, 'x', this.startNodeX + dx)
      this.$set(this.nodeData, 'y', this.startNodeY + dy)
    },
    handleDragEnd() {
      document.removeEventListener('mousemove', this.handleDrag)
      document.removeEventListener('mouseup', this.handleDragEnd)
    }
  }
}
</script>

这样节点就能拖着满屏跑,而且位置变化会同步到 data 里的 nodes 数组~

连线编辑(创建/删除连线):

创建连线:常见逻辑是“点击节点 A 的锚点 → 拖拽到节点 B 的锚点 → 生成新连线”,实现时,给节点加锚点元素(比如节点右下角的小圆圈),点击锚点时记录“起始节点”,然后监听鼠标移动,拖拽过程中画临时连线;松开鼠标时,判断是否落在另一个节点的锚点上,若在则生成 edges 数据。

简化步骤:

  • 给节点加锚点组件 Anchor.vueprops 接收 nodeId,点击时触发 startConnect 事件。
  • 父组件里维护 isConnecting 状态和 tempEdge(存起始节点 id)。
  • 拖拽时,用临时连线组件(TempEdge.vue)跟着鼠标画连线,松开后判断目标节点,生成正式 edge

删除连线:给连线加右键菜单或者 hover 时显示删除按钮,点击时从 edges 数组里删掉对应项。

实战!用 Vue2 做一个简易工作流编辑器

需求:做个请假审批流程图,节点有“申请人提交”“部门主管审批”“HR备案”“结束”,支持拖拽节点、创建/删除连线、节点类型区分样式。

步骤1:设计数据结构

nodes 数组每个节点含:idtypestart/approve/record/end)、labelxyedges 数组含:idsourcetarget

data() {
  return {
    nodes: [
      { id: 'n1', type: 'start', label: '申请人提交', x: 150, y: 100 },
      { id: 'n2', type: 'approve', label: '部门主管审批', x: 350, y: 100 },
      { id: 'n3', type: 'record', label: 'HR备案', x: 350, y: 250 },
      { id: 'n4', type: 'end', label: '流程结束', x: 550, y: 175 }
    ],
    edges: [
      { id: 'e1', source: 'n1', target: 'n2' },
      { id: 'e2', source: 'n2', target: 'n3' },
      { id: 'e3', source: 'n3', target: 'n4' }
    ]
  }
}

步骤2:写带类型样式的节点组件

根据 type 显示不同颜色、图标:

<template>
  <div 
    class="node-wrap" 
    :class="nodeData.type"
    :style="{ left: nodeData.x + 'px', top: nodeData.y + 'px' }"
    @mousedown="handleDragStart"
  >
    <div class="node-label">{{ nodeData.label }}</div>
    <div class="anchors">
      <span class="anchor" @mousedown="startConnect">+</span>
    </div>
  </div>
</template>
<script>
export default {
  props: ['nodeData'],
  methods: {
    handleDragStart() { /* 同之前拖拽逻辑 */ },
    startConnect(e) {
      e.stopPropagation() // 避免触发节点拖拽
      this.$emit('start-connect', this.nodeData.id)
    }
  }
}
</script>
<style scoped>
.node-wrap { 
  position: absolute; 
  padding: 10px; 
  border-radius: 4px; 
  cursor: move; 
  display: flex; 
  justify-content: space-between;
}
.start { background: #67c23a; }
.approve { background: #409eff; }
.record { background: #f56c6c; }
.end { background: #909399; }
.anchors { margin-left: 8px; }
.anchor { 
  display: inline-block; 
  width: 16px; 
  height: 16px; 
  background: #fff; 
  border: 1px solid #999; 
  border-radius: 50%; 
  text-align: center; 
  line-height: 16px; 
  cursor: crosshair; 
}
</style>

步骤3:处理连线创建逻辑

父组件里加 isConnectingtempSource 状态,拖拽时画临时连线:

<template>
  <div class="flow-wrapper">
    <custom-node 
      v-for="node in nodes" 
      :key="node.id" 
      :nodeData="node"
      @start-connect="handleStartConnect"
    />
    <edge 
      v-for="edge in edges" 
      :key="edge.id" 
      :sourceNode="getNodeById(edge.source)"
      :targetNode="getNodeById(edge.target)"
    />
    <temp-edge 
      v-if="isConnecting" 
      :startX="tempStartX" 
      :startY="tempStartY" 
      :endX="tempEndX" 
      :endY="tempEndY"
    />
  </div>
</template>
<script>
import CustomNode from './CustomNode.vue'
import Edge from './Edge.vue'
import TempEdge from './TempEdge.vue'
export default {
  components: { CustomNode, Edge, TempEdge },
  data() {
    return {
      isConnecting: false,
      tempSource: '',
      tempStartX: 0,
      tempStartY: 0,
      tempEndX: 0,
      tempEndY: 0
    }
  },
  methods: {
    handleStartConnect(sourceId) {
      this.isConnecting = true
      this.tempSource = sourceId
      // 记录起始节点位置(简化:取节点中心)
      const sourceNode = this.getNodeById(sourceId)
      this.tempStartX = sourceNode.x + 50 // 假设节点宽100,取中心x
      this.tempStartY = sourceNode.y + 20 // 假设节点高40,取中心y
      // 监听鼠标移动画临时连线
      document.addEventListener('mousemove', this.handleConnectMove)
      document.addEventListener('mouseup', this.handleConnectEnd)
    },
    handleConnectMove(e) {
      this.tempEndX = e.clientX
      this.tempEndY = e.clientY
    },
    handleConnectEnd(e) {
      document.removeEventListener('mousemove', this.handleConnectMove)
      document.removeEventListener('mouseup', this.handleConnectEnd)
      this.isConnecting = false
      // 这里简化:假设点击处有节点,找到targetId
      // 实际要判断鼠标位置下的节点,这里省略逻辑,直接模拟
      const targetId = 'n2' // 实际开发要动态获取
      if (targetId && targetId !== this.tempSource) {
        this.edges.push({ 
          id: `e${Date.now()}`, 
          source: this.tempSource, 
          target: targetId 
        })
      }
    }
  }
}
</script>

临时连线组件 TempEdge.vue 负责画随鼠标动的线:

<template>
  <svg class="temp-edge-svg" :style

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门