玩转 OpenTiny TinyVue 的 Flowchart 流程图组件:从基础到深度定制的实战指南
注: 在 OpenTiny TinyVue 项目中,该组件的官方命名为
Flowchart(流程图),而非flow-chat。本文将基于本工程的真实源码与规范,为您深入剖析Flowchart组件的设计理念、核心机制与实战用法。
一、 组件概述与核心机制
在前端开发中,绘制流程图通常需要引入如 JointJS、G6、AntV X6 或 Formily 等重型图表库。然而在许多后台业务场景中,我们只需要展示一个轻量、有状态、支持点击交互且能够响应式适配的步骤或审批流图。
TinyVue 的 Flowchart 组件正是为此而生。它采用了一种**"Canvas 连线 + DOM 节点"**的混合渲染架构:
- 节点(Nodes): 使用绝对定位的 Vue DOM 元素渲染。这使得我们能极其方便地利用 Vue 的插槽(Slots)去深度定制节点的外观、交互或嵌入复杂的下拉菜单(Popover/Select)。
- 连线(Links): 使用 Canvas 绘制在背景层。通过高性能的 Canvas API 保证连线的平滑度,同时在 JavaScript 中通过自研的网格相对运动 DSL 解决连线路径的响应式自适应问题。
二、 核心数据模型与 DSL 解析
在使用 Flowchart 组件时,我们需要提供 data 和 config 两个核心对象。
1. 节点的网格布局系统
在 Flowchart 中,节点的定位不是写死 px 像素值,而是基于一个虚拟网格(Grid)系统:
js
const node = createNode(name, status, label, date, items, row, col, other)
row和col: 分别代表节点在虚拟网格中所在的行索引和列索引(从 0 开始)。- 自动计算: 组件会根据配置的
config.width、config.height以及config.cols、config.rows,自动划分出每一网格单元的宽度和高度,并计算出节点的绝对定位(x, y)坐标。 status(状态码): 预设状态包括:1(已完成)、2(进行中)、3(未开始/等待)、4(失败/异常)。
2. 独创的连线相对路径描述语法 (p)
这是 TinyVue Flowchart 组件最亮眼的设计之一。传统的流程图连线需要给出一系列转折点的具体像素坐标,而 TinyVue 允许你使用一个方向和比例组成的相对描述字符串 p 来控制连线轨迹:
js
createLink(from, to, p, status, style)
在参数 p 中(例如 '0 r0.5 t1 c r1.5'),每个指令代表相对于网格宽度/高度的相对运动:
- 方向指令:
r(Right) /l(Left) 指向水平方向移动,数值乘以单列宽cw。b(Bottom) /t(Top) 指向垂直方向移动,数值乘以单行高rh。
- 步长数值: 如
r0.5代表水平向右移动0.5倍列宽;t1代表垂直向上移动1倍行高。 - 圆角指令
c: 代表在拐弯处渲染一个平滑的圆弧转弯(使用 Canvas 的arcTo实现,默认半径为8px)。
源码逻辑透视:
在 renderless/src/flowchart/index.ts 中,解析该语法的核心正则如下:
typescript
const regDir = /^([lrtb])(\d+(\.\d+)?)$/
const regDirC = /^([lrtb])(\d+(\.\d+)?)c$/
若 p 缺失,组件将默认在两点间画一条直连线。若有 p,则会按照指令依次通过 lineTo 或 arcTo 计算路径,最终完美实现响应式布局下的连线对齐。
三、 Vue 3 Composition API 实战样例
以下展示一个最基础的 Flowchart 组件应用。
vue
<template>
<div class="tiny-demo-flowchart">
<tiny-flowchart
:data="chartDataRaw"
:config="chartConfigRaw"
@click-node="onClickNode"
@click-link="onClickLink"
@click-blank="onClickBlank"
/>
</div>
</template>
<script setup>
import { TinyModal, TinyFlowchart } from '@opentiny/vue'
import { hooks } from '@opentiny/vue-common'
// 1. 解构核心工厂函数
const { createNode, createLink, createConfig } = TinyFlowchart
// 2. 定义节点与连线数据
const chartData = {
nodes: [
// createNode(name, status, label, date, items, row, col)
createNode('1', 1, '基础信息提交', '2026.05.01', [], 1, 0),
createNode('2', 1, '部门主管审批', '2026.05.02', [], 0, 2),
createNode('3', 2, 'HR复核审定', '', [], 1, 4),
createNode('4', 3, '归档完毕', '', [], 1, 6)
],
links: [
// 连线 1 -> 2: 起始点,往右偏移0.5列宽,向上偏移1行高,拐弯,往右偏移1.5列宽
createLink('1', '2', '0 r0.5 t1 c r1.5', 1),
// 连线 2 -> 3
createLink('2', '3', '0 r1.5 c b1 r0.5', 3),
// 连线 3 -> 4 (默认虚线示例)
createLink('3', '4', '', 3, 'dash')
]
}
// 3. 初始化全局配置
const chartConfig = createConfig()
chartConfig.width = 800
chartConfig.height = 300
chartConfig.rows = 4
chartConfig.cols = 8
// 4. 性能优化关键:使用 markRaw 阻止 Vue 对海量底层绘图对象做深层 Proxy 劫持
const chartDataRaw = hooks.markRaw(chartData)
const chartConfigRaw = hooks.markRaw(chartConfig)
// 5. 事件监听
const onClickNode = (afterNode, e) => {
TinyModal.message(`点击了节点: ${afterNode.raw.info.label}`)
}
const onClickLink = (afterLink, e) => {
TinyModal.message(`点击了连线: 从 ${afterLink.raw.from} 指向 ${afterLink.raw.to}`)
}
const onClickBlank = (param, e) => {
TinyModal.message('点击了画布空白处')
}
</script>
<style scoped>
.tiny-demo-flowchart {
padding: 20px;
background-color: #fafafa;
border-radius: 8px;
}
</style>
四、 进阶:使用插槽自定义节点
当默认的 Icon 和 Label 满足不了复杂的交互需求时(例如需要在节点下展示处理人列表、悬浮卡片、表单或按钮),可以利用 Flowchart 提供的插槽。
1. 提供的作用域插槽(Scoped Slots)
#icon="params":自定义节点图标#label="params":自定义节点文字展示#content="params":自定义节点底部的内容区(可结合 Popover)
2. 深入实战:在节点底部定制处理人 Popover
vue
<template>
<tiny-flowchart :data="chartDataRaw" :config="chartConfigRaw">
<!-- 替换节点下方的辅助展示区 -->
<template #content="params">
<tiny-popover
placement="bottom-start"
trigger="click"
width="200"
:visible-arrow="false"
:model-value="params.dropdowns[params.node.name]"
@update:model-value="params.dropdowns[params.node.name] = $event"
>
<template #default>
<div class="user-list">
<div v-for="user in params.node.info.items" :key="user.key" class="user-item">
<span>{{ user.name }} ({{ user.role }})</span> -
<span :class="user.status">{{ user.status }}</span>
</div>
</div>
</template>
<template #reference>
<div class="popover-trigger" @click.stop="togglePopover(params)">
<span>处理人({{ params.node.info.items.length }})</span>
</div>
</template>
</tiny-popover>
</template>
</tiny-flowchart>
</template>
<script setup>
import { TinyFlowchart, TinyPopover } from '@opentiny/vue'
import { hooks } from '@opentiny/vue-common'
const { createNode, createItem, createConfig } = TinyFlowchart
// 构造带处理人的节点
const handlers = [
createItem('1', '张三', '主审人', '已同意'),
createItem('2', '李四', '会签人', '处理中')
]
const chartData = {
nodes: [
createNode('1', 1, '基础信息提交', '2026.05.01', [], 1, 0),
// 传入处理人数组 handlers
createNode('2', 2, '部门主管审批', '2026.05.02', handlers, 1, 2)
],
links: []
}
const chartConfig = createConfig()
// 必须调大 listWidth(节点底部自定义容器宽度),默认 62px 会导致文字换行重叠
chartConfig.listWidth = 120
const chartDataRaw = hooks.markRaw(chartData)
const chartConfigRaw = hooks.markRaw(chartConfig)
const togglePopover = (params) => {
const { dropdowns, node } = params
dropdowns[node.name] = !dropdowns[node.name]
}
</script>
<style scoped>
/* 必须覆盖默认的节点底部插槽高度限制 */
:deep(.tiny-flow-chart__node-item) {
min-height: 24px !important;
height: auto !important;
}
.popover-trigger {
padding: 2px 6px;
border: 1px solid #ddd;
border-radius: 4px;
background: #fff;
cursor: pointer;
font-size: 12px;
text-align: center;
}
.user-list {
padding: 8px;
font-size: 12px;
}
.user-item {
padding: 4px 0;
}
.已同意 { color: #52c41a; }
.处理中 { color: #1890ff; }
</style>
五、 实用配置项与常见避坑指南
1. 实用配置项说明
您可以通过修改由 createConfig() 生成的配置对象来自定义图表:
| 配置属性 | 类型 | 默认值 | 作用说明 |
|---|---|---|---|
width / height |
number |
1024 / 420 |
画布容器的宽高 |
rows / cols |
number |
8 / 8 |
虚拟网格划分的行数与列数 |
colors |
object |
{ 1: '#1890ff', ... } |
不同状态码对应的色彩方案 |
listWidth |
number |
62 |
节点底部内容容器的宽度(使用插槽时强烈建议改大) |
adjustPos |
function |
null |
允许对特定节点的具体 (x, y) 像素坐标进行微调的钩子函数 |
drawLink |
function |
null |
自定义 Canvas 连线绘制逻辑的钩子,用于高级定制线条特效 |
2. 常见避坑指南
- 文字溢出省略: 组件内置了文字省略机制。当
label的字符长度超过由config.labelWidth限制的宽度时,会自动截断并添加...。如果想展示全称,可以把labelWidth调大。 - 大数量响应式卡顿(极重要): 不要将整个
chartData或chartConfig直接声明为 Vue 的ref或reactive,因为它们里面有大量的布局计算数据与配置,会被 Vue 转换为深度 Proxy,这会在 Canvas 重绘时引发性能问题。请务必使用markRaw(或者 Vue 的toRaw)将其包裹之后再传给组件。 - 样式穿透覆盖: 若使用插槽如
#content,需要注意组件默认对.tiny-flow-chart__node-item设定了height: 24px。在使用类似下拉框等高度不固定的组件时,需要在外部使用:deep(.tiny-flow-chart__node-item) { height: auto !important; }进行高度重置。
六、 总结
TinyVue 的 Flowchart 组件以极具创意的"网格行列 + 相对方向控制 DSL"设计,解决了多层级多流转流程图在不同分辨率屏幕下"画线难、对准难"的痛点。对于企业级审批流、流水线构建进度等业务场景,它是一款兼具开发效率与定制空间的理想组件。