最近做了一个在线考试系统,其中有个题型是流程图类型的,考试端大概效果就是把答案选项拖拽到流程图框中,要求能拖动左侧选项,但是不能编辑右侧流程框和连接线。

需要满足简单流程图编辑,最主要是要满足在后台中流程图题型动态配置性,在考试端流程图的展示和答案选项的拖拽。

最开始的想法是把流程图当作背景,然后通过 CSS 相对定位一个个调整元素位置,使用拖拽工具给元素绑定拖拽事件,效果也还能凑合,但是如果流程图类型太多,难以维护,并且不支持后台动态配置。

如果是流程图的拖拽、编辑,基本大多数流程图工具都支持,但是这个拖拽选项到流程图框上填充内容的小小需求没有找到解决方案,那就只能自己解决了,选择的是 vue-super-flow 这个工具,因为只需要简单的流程图编辑能力,并且这个工具可以在引用时灵活可配置。

觉得挺有意思,记录一下考试端实现过程
效果图

vue-super-flow
vue-super-flow的使用

主要讲一下这个 flowChart 组件,上代码

// flowChart.vue
<template>
  <div class="f-super-flow">
    <div class="f-node-container">
        class="f-node-item"
        :class="!item.label ? 'f-node-item-empty' : ''"
        v-for="(item, index) in nodeItemList"
        :key="index"
        @mousedown="evt => nodeItemMouseDown(evt, item.value, index)"
        {{ item.label }}
      </div>
    </div>
    <div class="f-flow-container" ref="flowContainer">
      <super-flow
        ref="superFlow"
        :draggable="false"
        :linkAddable="false"
        :linkEditable="false"
        :hasMarkLine="false"
        :link-desc="linkDesc"
        :node-list="nodeList"
        :link-list="linkList"
        <template v-slot:node="{ meta }">
            v-if="meta.name" 
            class="f-node-del" 
            :style="meta.type == 'judge' ? 'top: 10%;' : ''" 
            @mouseup="evt => nodeMouseUp(evt, meta)"
          >x</div>
            class="f-flow-node"
            :class="meta.type? `f-flow-node-${meta.type}`: ''"
            <div class="f-node-content" :title="meta.name">{{ meta.name }}</div>
          </div>
        </template>
      </super-flow>
    </div>
  </div>
</template>
<script>
 * 流程图题
import SuperFlow from "vue-super-flow";
import "vue-super-flow/lib/index.css";
export default {
  name: "flow-chart",
  components: {
    SuperFlow,
  props: {
    optionList: {
      type: Array,
      default: () => [],
    flowChartNode: {
      type: Array,
      default: () => [],
    flowChartLink: {
      type: Array,
      default: () => [],
  data() {
    return {
      // flowChart 节点
      nodeList: [],
      // 线条
      linkList: [],
      // 左侧列表
      nodeItemList: [],
      dragConf: {
        isDown: false,
        isMove: false,
        offsetTop: 0,
        offsetLeft: 0,
        clientX: 0,
        clientY: 0,
        ele: null,
        info: null,
      resetNodeItem: {
        label: '',
        value: {
          meta: {
            label: '',
            name: '',
            type: ''
  computed: {
  mounted() {
	// 测试数据
	this.nodeItemList = [
        label: 1,
        value: {
          meta: {
            label: 1,
            name: 1
        label: 3,
        value: {
          meta: {
            label: 3,
            name: 3
        label: 2,
        value: {
          meta: {
            label: 2,
            name: 2
        label: 5,
        value: {
          meta: {
            label: 5,
            name: 5
        label: 4,
        value: {
          meta: {
            label: 4,
            name: 4
    this.nodeList = [
        id: "N1",
        width: 180,
        height: 50,
        coordinate: [100, 32],
        meta: {
          label: "",
          name: "",
          type: "startAndEnd",
        id: "N2",
        width: 180,
        height: 50,
        coordinate: [100, 139],
        meta: {
          label: "",
          name: "",
          type: "process",
        id: "N3",
        width: 180,
        height: 180,
        coordinate: [100, 236],
        meta: {
          label: "",
          name: "",
          type: "judge",
        id: "N4",
        width: 180,
        height: 50,
        coordinate: [100, 500],
        meta: {
          label: "",
          name: "",
          type: "process",
        id: "N5",
        width: 180,
        height: 50,
        coordinate: [360, 300],
        meta: {
          label: "",
          name: "",
          type: "process",
        id: "N6",
        width: 180,
        height: 50,
        coordinate: [100, 596],
        meta: {
          label: "",
          name: "",
          type: "startAndEnd",
    this.linkList = [
        id: "linkUkwVhocoTp08AbLQ",
        startId: "N1",
        endId: "N2",
        startAt: [60, 40],
        endAt: [100, 0],
        meta: null,
        id: "linkS27wPzJ1Z7plttsR",
        startId: "N3",
        endId: "N5",
        startAt: [168, 84],
        endAt: [0, 20],
        meta: { desc: "NO" },
        id: "linka8cGGQAPQTtXuYID",
        startId: "N5",
        endId: "N2",
        startAt: [100, 0],
        endAt: [200, 20],
        meta: null,
        id: "linkdNLEL6EcIVijSQx4",
        startId: "N2",
        endId: "N3",
        startAt: [100, 40],
        endAt: [84, 0],
        meta: null,
        id: "link3heD5DMOJbmxcLHu",
        startId: "N3",
        endId: "N4",
        startAt: [84, 168],
        endAt: [100, 0],
        meta: { desc: "YES" },
        id: "linkVpTa6NUNNcG2NKeY",
        startId: "N4",
        endId: "N6",
        startAt: [100, 40],
        endAt: [60, 0],
        meta: null,
    document.addEventListener("mousemove", this.docMousemove);
    document.addEventListener("mouseup", this.docMouseup);
    this.$once("hook:beforeDestroy", () => {
      document.removeEventListener("mousemove", this.docMousemove);
      document.removeEventListener("mouseup", this.docMouseup);
    });
  methods: {
    linkDesc(link) {
      return link.meta ? link.meta.desc : "";
    docMousemove({ clientX, clientY }) {
      const conf = this.dragConf;
      if (conf.isMove) {
        conf.ele.style.top = clientY - conf.offsetTop + "px";
        conf.ele.style.left = clientX - conf.offsetLeft + "px";
      } else if (conf.isDown) {
        // 鼠标移动量大于 5 时 移动状态生效
        conf.isMove =
          Math.abs(clientX - conf.clientX) > 5 ||
          Math.abs(clientY - conf.clientY) > 5;
    docMouseup({ clientX, clientY }) {
      const conf = this.dragConf;
      conf.isDown = false;
      if (conf.isMove) {
        const { top, right, bottom, left } = this.$refs.flowContainer.getBoundingClientRect();
        // 判断鼠标是否进入 flow container
        if (
          clientX > left &&
          clientX < right &&
          clientY > top &&
          clientY < bottom
          // 获取拖动元素左上角相对 super flow 区域原点坐标
          const coordinate = this.$refs.superFlow.getMouseCoordinate( clientX, clientY );
          for(let i = 0;i<this.nodeList.length;i++) {
            let nodeData = this.nodeList[i]
            let xStartPostion = nodeData.coordinate[0]
            let yStartPostion = nodeData.coordinate[1] * 0.5
            let xEndPostion = +nodeData.width + nodeData.coordinate[0]
            let yEndPostion = +nodeData.height + nodeData.coordinate[1]
            if((coordinate[0] >= xStartPostion  && coordinate[0] <= xEndPostion) && (coordinate[1] >= yStartPostion  && coordinate[1] <= yEndPostion)) {
              nodeData.meta.label = conf.info.meta.label
              nodeData.meta.name = conf.info.meta.name
              nodeData.meta.index = conf.info.meta.index
              this.$set(this.nodeList,i,nodeData)
              this.nodeItemList[conf.info.meta.index] = this.resetNodeItem
              break; 
        conf.isMove = false;
      if (conf.ele) {
        conf.ele.remove();
        conf.ele = null;
    nodeItemMouseDown(evt, infoFun, index) {
      const { clientX, clientY, currentTarget } = evt;
      const { top, left } = evt.currentTarget.getBoundingClientRect();
      const conf = this.dragConf;
      const ele = currentTarget.cloneNode(true);
      infoFun.meta.index = index
      Object.assign(this.dragConf, {
        offsetLeft: clientX - left,
        offsetTop: clientY - top,
        clientX: clientX,
        clientY: clientY,
        info: infoFun,
        ele,
        isDown: true,
      });
      ele.style.position = "fixed";
      ele.style.margin = "0";
      ele.style.top = clientY - conf.offsetTop + "px";
      ele.style.left = clientX - conf.offsetLeft + "px";
      this.$el.appendChild(this.dragConf.ele);
    nodeMouseUp(evt, meta) {
      evt.preventDefault()
      const metaData = { ...meta }
      let selectIndex = this.nodeList.findIndex(item => item.meta.name === meta.name)
      let nodeItem = this.nodeList[selectIndex]
      nodeItem.meta.label = ""
      nodeItem.meta.name = ""
      this.$set(this.nodeList, selectIndex, nodeItem)
      this.$set(this.nodeItemList, metaData.index, {
        label: metaData.label,
        value: {
          meta: {
            label: metaData.label,
            name: metaData.name,
    getNodeList() {
      let list = this.$refs.superFlow.toJSON().nodeList
      let arr = []
      list.map(item => {
        arr.push({
          id: item.id,
          name: item.meta.name
      // console.log(arr);
      return arr
</script>
<style lang="scss" scoped>
.f-super-flow {
  position: relative;
  display: flex;
  background: #f5f5f5;
  height: 100%;
  .f-node-container {
    width: 20%;
    text-align: center;
    background-color: #ffffff;
    overflow: auto;
    .f-node-item {
      margin: 20px 20%;
      padding: 6px 0;
      border: 2px solid #333;
      border-radius: 6px;
      min-height: 40px;
      font-size: 18px;
      color: #333;
      box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.3);
      cursor: pointer;
      user-select: none; // 防止鼠标左键拖动选中页面的文字
      &:hover {
        box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.4);
      &.f-node-item-empty {
        border: 1px solid rgba(118, 118, 118, 0.3);
        pointer-events: none;
        background: rgba(239, 239, 239, 0.3);
        cursor: no-drop;
  .f-flow-container {
    flex: 1;
    // padding: 10px;
    .super-flow {
      overflow: auto;
  /deep/.super-flow__node {
    display: table;
    border: none;
    background: none;
    box-shadow: none;
    &:hover {
      .f-node-del {
        display: block;
    .f-node-del {
      display: none;
      position: absolute;
      right: -10px;
      top: -10px;
      width: 26px;
      height: 26px;
      line-height: 26px;
      font-size: 20px;
      text-align: center;
      color: #666;
      border-radius: 50%;
      background: rgba(150,150,150,.5);
      z-index: 1;
    .f-flow-node {
      position: relative;
      display: table-cell;
      width: 100%;
      height: 100%;
      vertical-align: middle;
      font-size: 16px;
      color: #333;
      font-weight: bold;
      box-sizing: border-box;
      background: #fff;
      .f-node-content {
        text-align: center;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: pre-wrap;
    .f-flow-node-startAndEnd {
      border: 2px solid rgba(39, 107, 232, .6);
      border-radius: 22px;
    .f-flow-node-process {
      border: 2px solid rgba(39, 107, 232, .6);
    .f-flow-node-judge {
      padding: 0 40px;
      border: 2px solid rgba(39, 107, 232, .6);
      transform: rotate(45deg) scale(.7);
      .f-node-content {
        transform: rotate(-45deg) scale(1.4);
    .f-flow-node-quote {
      padding: 0 40px;
      border: 2px solid rgba(39, 107, 232, .6);
      border-radius: 50%;
</style>

因为不需要对流程图进行操作,先流程图的编辑功能相关配置设置为falsevue-super-flow Demo 里有绑定鼠标事件,不过不支持拖拽到流程图框上面替换,利用已有的鼠标事件做修改。

自定义流程图框型样式

<template v-slot:node="{ meta }">
    v-if="meta.name" 
    class="f-node-del" 
    :style="meta.type == 'judge' ? 'top: 10%;' : ''" 
    @mouseup="evt => nodeMouseUp(evt, meta)"
  >x</div>
    class="f-flow-node"
    :class="meta.type? `f-flow-node-${meta.type}`: ''"
    <div class="f-node-content" :title="meta.name">{{ meta.name }}</div>
  </div>
</template>

左侧选项拖拽到流程图上填充

鼠标是否进入 flow container 之后,循环当前节点列表,判断当前鼠标位置是否在流程图框的范围内,在范围内时设置nodeList节点内容,并重置nodeItemList选项禁止选中拖拽

// 获取当前鼠标在 graph 坐标系的坐标
const coordinate = this.$refs.superFlow.getMouseCoordinate( clientX, clientY );
for(let i = 0;i<this.nodeList.length;i++) {
  let nodeData = this.nodeList[i]
  let xStartPostion = nodeData.coordinate[0]
  let yStartPostion = nodeData.coordinate[1] * 0.5
  let xEndPostion = +nodeData.width + nodeData.coordinate[0]
  let yEndPostion = +nodeData.height + nodeData.coordinate[1]
  if((coordinate[0] >= xStartPostion  && coordinate[0] <= xEndPostion) && (coordinate[1] >= yStartPostion  && coordinate[1] <= yEndPostion)) {
    nodeData.meta.label = conf.info.meta.label
    nodeData.meta.name = conf.info.meta.name
    nodeData.meta.index = conf.info.meta.index
    this.$set(this.nodeList,i,nodeData)
    this.nodeItemList[conf.info.meta.index] = this.resetNodeItem
    break; 

删除流程图框中内容

拖拽到流程图框中的内容支持删除,删除操作绑定mouseup事件上,尝试过绑定click事件无效。

@mouseup="evt => nodeMouseUp(evt, meta)"
// 删除事件
nodeMouseUp(evt, meta) {
  evt.preventDefault()
  const metaData = { ...meta }
  let selectIndex = this.nodeList.findIndex(item => item.meta.name === meta.name)
  let nodeItem = this.nodeList[selectIndex]
  nodeItem.meta.label = ""
  nodeItem.meta.name = ""
  this.$set(this.nodeList, selectIndex, nodeItem)
  this.$set(this.nodeItemList, metaData.index, {
    label: metaData.label,
    value: {
      meta: {
        label: metaData.label,
        name: metaData.name,
npm install vue-super-flow
yarn add vue-super-flow
import SuperFlow from 'vue-super-flow'
import 'vue-super-flow/lib/index.css'
Vue.use(SuperFlow)
Attributes
draggable
Boolean
是否开启节点拖拽
linkAddable
Boolean
是否开启快捷创建 link
linkEditable
Boolean
link 是否可编辑
hasMarkLine
Boolean
2.1.第一种是全局引用,在main.js中引入
import SuperFlow from 'vue-super-flow'
import 'vue-super-flow/lib/index.css'
Vue.use(SuperFlow)
2.2.第二种是局部引用,在你
				
主要功能介绍 最近需要开发一款营销流程图项目,需要各种节点流程任务的拖拽跑出数据结果,需要用到节点的增删改查,插销删除流程测试执行各种操作,于是发现了mxgraph这款插件。 mxGraph是一款基于web的绘制流程图javascript库工具 英文官方网站:http://jgraph.github.io/mxgraph/docs/js-api/files/index-txt.html gitDemo地址:https://github.com/jgraph/mxgraph2 npm可以直接安装
mxgraph项目实战中遇到的问题 mxgraph是一款比较复杂的插件,毕竟都是英文的官方文档,在所难免会遇到一些问题,下面跟大家分享一下我的问题及解决方法: 1. 新建节点必须配合try…finally方法 insertVertexHandler(id, x, y) { //获取元素 const element = getOptionsElement(this.asideOptions, id); //获取图片 const src = element.icon; //获取标题 import VueAwesomeSwiper from 'vue-awesome-swiper' // or require var Vue = require('vue') var VueAwesomeSwiper = re
Vue-super-flow是一个开源的Vue.js库,用于构建带有流程控制能力的动态表单和工作流程应用。通过使用Vue-super-flow,可以利用Vue.js熟悉和强大的组件架构和响应式特性,并在其基础上增加了可配置性、可扩展性和可重用性,使得开发复杂的流程控制应用变得简单和高效。 Vue-super-flow提供了一系列组件,包括:节点、连线、流程区域等组件,这些组件可以无缝地集成到Vue.js应用中,并支持与其他Vue.js组件进行协同工作。用户可以自定义每个组件的外观和行为,如颜色、大小、字体、边框等。此外,Vue-super-flow还提供了丰富的API,支持根据业务需求来控制流程图的各种交互行为,例如新增、修改、删除、拖动和连线节点等操作。 总之,Vue-super-flow是一个灵活、高效和可定制的流程控制库,深度集成了Vue.js的基础框架和生态系统,并提供了丰富的功能和API,使得开发者能够快速构建出复杂的流程控制应用。