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前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。