后端别再手绘了!TinyVue 流程图组件 Flowchart 跨端定制指南

玩转 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 组件时,我们需要提供 dataconfig 两个核心对象。

1. 节点的网格布局系统

在 Flowchart 中,节点的定位不是写死 px 像素值,而是基于一个虚拟网格(Grid)系统

js 复制代码
const node = createNode(name, status, label, date, items, row, col, other)
  • rowcol 分别代表节点在虚拟网格中所在的行索引和列索引(从 0 开始)。
  • 自动计算: 组件会根据配置的 config.widthconfig.height 以及 config.colsconfig.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,则会按照指令依次通过 lineToarcTo 计算路径,最终完美实现响应式布局下的连线对齐。


三、 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>

四、 进阶:使用插槽自定义节点

当默认的 IconLabel 满足不了复杂的交互需求时(例如需要在节点下展示处理人列表、悬浮卡片、表单或按钮),可以利用 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. 常见避坑指南

  1. 文字溢出省略: 组件内置了文字省略机制。当 label 的字符长度超过由 config.labelWidth 限制的宽度时,会自动截断并添加 ...。如果想展示全称,可以把 labelWidth 调大。
  2. 大数量响应式卡顿(极重要): 不要将整个 chartDatachartConfig 直接声明为 Vue 的 refreactive,因为它们里面有大量的布局计算数据与配置,会被 Vue 转换为深度 Proxy,这会在 Canvas 重绘时引发性能问题。请务必使用 markRaw(或者 Vue 的 toRaw)将其包裹之后再传给组件。
  3. 样式穿透覆盖: 若使用插槽如 #content,需要注意组件默认对 .tiny-flow-chart__node-item 设定了 height: 24px。在使用类似下拉框等高度不固定的组件时,需要在外部使用 :deep(.tiny-flow-chart__node-item) { height: auto !important; } 进行高度重置。

六、 总结

TinyVue 的 Flowchart 组件以极具创意的"网格行列 + 相对方向控制 DSL"设计,解决了多层级多流转流程图在不同分辨率屏幕下"画线难、对准难"的痛点。对于企业级审批流、流水线构建进度等业务场景,它是一款兼具开发效率与定制空间的理想组件。

相关推荐
cc.ChenLy9 小时前
大文件断点续传原理总结和Demo示例详解
javascript·vue.js·文件上传·大文件断点续传
程序员祥云9 小时前
VUE2_TO_VITE_VUE3
javascript·vue.js·ecmascript
苏瞳儿9 小时前
vue3+pinia+mqtt实时响应连接
前端·javascript·vue.js
i220818 Faiz Ul10 小时前
理财系统|基于java+vue的家庭理财系统小程序(源码+数据库+文档)
java·vue.js·spring boot·小程序·论文·毕设·理财系统
暗冰ཏོ10 小时前
《2026 Vue2 + Vue3 完整学习指南:基础语法、路由缓存、登录拦截、项目实战与面试题》
前端·vue.js·vue·vue3·vue2
幸运小圣11 小时前
动态表格在 Vue 3 中的实现指南【前端】
前端·javascript·vue.js
SwJieJie11 小时前
Day 3|表格表单分页范式与 vue-request 最佳实践:从配置驱动到业务落地
前端·javascript·vue.js
沙漠11 小时前
Mock Server 中间件
vue.js·webpack
MacroZheng12 小时前
平替Cursor!Claude Code + VSCode = 王炸!
前端·vue.js·人工智能