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 为例,步骤特简单:
- 建项目:用 vue-cli 建 Vue2 项目(命令行输
vue create my-flowchart-app,选 Vue2 模板)。 - 装依赖:进入项目目录,执行
npm i vue-flowchart-editor。 - 全局注册:打开
src/main.js,加两行:import VueFlowChart from 'vue-flowchart-editor' import 'vue-flowchart-editor/dist/vue-flowchart-editor.css' Vue.use(VueFlowChart)
- 写第一个流程图组件:新建
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(包含 id、label、x、y 这些),模板里用 div 或 svg 元素当容器,通过 :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>,通过计算起点和终点的坐标,动态生成 path 的 d 属性(比如直线: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 遍历 nodes 和 edges,渲染自定义节点和连线:
<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,计算鼠标移动的偏移量,更新节点的 x、y;mouseup 时移除 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.vue,props接收nodeId,点击时触发startConnect事件。 - 父组件里维护
isConnecting状态和tempEdge(存起始节点id)。 - 拖拽时,用临时连线组件(
TempEdge.vue)跟着鼠标画连线,松开后判断目标节点,生成正式edge。
删除连线:给连线加右键菜单或者 hover 时显示删除按钮,点击时从 edges 数组里删掉对应项。
实战!用 Vue2 做一个简易工作流编辑器
需求:做个请假审批流程图,节点有“申请人提交”“部门主管审批”“HR备案”“结束”,支持拖拽节点、创建/删除连线、节点类型区分样式。
步骤1:设计数据结构
nodes 数组每个节点含:id、type(start/approve/record/end)、label、x、y;edges 数组含:id、source、target。
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:处理连线创建逻辑
父组件里加 isConnecting 和 tempSource 状态,拖拽时画临时连线:
<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前端网发表,如需转载,请注明页面地址。
code前端网




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