乐趣区

关于前端:用flex布局实现一个流程设计器

最近接到一个需要,要做一个流程设计的性能,大略长上面这个样子:

反对增加、编辑和删除节点,节点只有四种类型:开始节点、一般节点、分支节点、完结节点。

因为每个节点只有一个进和一个出,且节点不须要反对拖拽,连线也是主动连贯,所以整体比较简单,不必开源库,本人做的老本也不会很高。

初看其实比拟麻烦的只有布局和连线,布局因为节点不须要反对拖拽,所以地位都是主动且固定的,更准确点说其实就是垂直居中,说到居中,你可能会想到 flex 布局,那么这里能不能应用 flex 布局呢,显然是能够的,另外连线通常可能会应用 svg,然而其实间接应用div 和伪元素也齐全能够实现。

接下来咱们就从零来实现一下,因为咱们的我的项目起因,所以还是会基于 Vue2 版本来实现。

数据结构

整体数据是一个数组,数组的每一项代表一个节点。

开始节点

{id: 'startEvent', type: 'start', title: '开始'}

id除了开始和完结节点外,其余节点的 id 随机生成即可,type代表节点的类型,title为节点的题目。

完结节点

{id: 'endEvent', type: 'end', title: '完结'}

一般节点

{
    id: '随机 id',
    type: 'normal',
    title: '审批人',
    content: '主管',// 节点内容
    configData: {},// 节点配置数据
    nodeList: []// 后续节点}

默认 titlecontent的内容会在节点上显示,而针对每个节点的配置数据保留在 configData 上,个别状况下,顶层节点会间接作为数组的一项,而当处于条件分支中时,则须要把后续节点保留在 nodeList 上。

分支节点

{
    id: '随机 id',
    type: 'condition',
    title: '条件分支',
    children: [// 分支
        // 一般节点
    ]
}

分支节点的分支保留在 children 属性上,每个分支节点其实就是一个一般节点,一般节点里又能够嵌套分支节点。

布局

入口组件

首先创立一个入口组件:

<template>
  <div class="sfcContainer">
    <div class="sfcContent">
      <Node v-for="node in data" :key="node.id" :data="node"></Node>
    </div>
  </div>
</template>

<script>
export default {
  name: 'SimpleFlowChart',
  props: {
    data: {
      type: Array,
      default() {return []
      }
    }
  }
}
</script>

<style lang="less" scoped>
.sfcContainer {
  width: 100%;
  height: 100%;
  overflow: auto;// 超出显示滚动条
  box-sizing: border-box;
  background: rgba(0, 0, 0, 0.03);

  * {box-sizing: border-box;}

  .sfcContent {
    // 设置垂直居中
    display: flex;
    align-items: center;
    padding: 20px;
    // 最小宽高设为容器宽高,否则无奈居中
    min-width: 100%;
    min-height: 100%;
    // 否则宽高以理论的内容为准
    width: max-content;
    height: max-content;
  }
}
</style>

流程数据通过 props 传入,循环数据渲染 Node 组件,Node组件为所有节点组件的容器。

css中给 sfcContent 元素设置的 display: flex;align-items: center; 很要害,就是这两行款式,使得所有顶层节点能够程度排列并垂直居中。

根底组件 Node

这个组件作为所有节点组件的容器,只有依据类型渲染不同节点组件即可:

<template>
  <div class="sfcNodeContainer">
    <!-- 开始节点 -->
    <StartNode v-if="data.type ==='start'":data="data"></StartNode>
    <!-- 完结节点 -->
    <EndNode v-else-if="data.type ==='end'":data="data"></EndNode>
    <!-- 分支节点 -->
    <ConditionNode v-else-if="data.type ==='condition'":data="data"></ConditionNode>
    <!-- 一般节点 -->
    <NormalNode v-else :data="data"></NormalNode>
  </div>
</template>

<script>
export default {
  name: 'Node',
  props: {
    data: {
      type: Object,
      default: null
    }
  }
}
</script>

开始节点 StartNode、完结节点组件 EndNode

开始节点和完结节点差不多,除了款式略微有点差异外,就是开始节点有根指向下一个节点的箭头线。

开始节点组件:

<template>
  <div class="sfcStartNodeContainer">
    <div class="sfcStartNodeContent">{{data.title}}</div>
  </div>
</template>

<script>
export default {
  name: 'StartNode',
  props: {
    data: {
      type: Object,
      default: null
    }
  }
}
</script>

箭头线下一个大节再看,节点的根底款式因为不影响布局所以也没贴出来。

完结节点组件:

<template>
  <div class="sfcEndNodeContainer">{{data.title}}</div>
</template>

<script>
export default {
  name: 'EndNode',
  props: {
    data: {
      type: Object,
      default: null
    }
  }
}
</script>

<style lang="less" scoped>
.sfcEndNodeContainer {// 省略不影响布局的节点款式}
</style>

一般节点组件 NormalNode

<template>
  <div class="sfcNormalNodeContainer">
    <!-- 节点内容 -->
    <div class="sfcNormalNodeWrap">
      <div class="sfcNormalNodeContent">
        <div class="sfcNormalNodeTitle">
          {{data.title || ''}}
        </div>
        <!-- 省略 -->
      </div>
    </div>
    <!-- 递归渲染后续 Node 组件 -->
    <Node v-for="node in (data.nodeList || [])" :key="node.id" :data="node"></Node>
  </div>
</template>

<script>
export default {
  name: 'NormalNode',
  props: {
    data: {
      type: Object,
      default: null
    }
  }
}
</script>

<style lang="less" scoped>
.sfcNormalNodeContainer {
  position: relative;
  // 使以后节点的内容和后续节点程度排列,并且垂直居中
  display: flex;
  align-items: center;
  flex-shrink: 0;

  // 省略节点根底款式
}
</style>

sfcNormalNodeWrap元素渲染节点本身的内容,如果以后节点的 nodeList 中有后续节点,那么遍历递归渲染 Node 节点。

通过在容器上设置 display: flex 款式,让节点本身内容和后续其余节点程度排列显示,再通过 align-items: center 款式让它们垂直居中对齐。

分支节点组件 ConditionNode

<template>
  <div class="sfcConditionNodeContainer">
    <div class="sfcConditionNodeItemList">
      <div
        class="sfcConditionNodeItem"
        v-for="node in data.children"
        :key="node.id"
      >
        <div class="sfcConditionNodeItemNodeWrap">
          <Node :data="node"></Node>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'ConditionNode',
  props: {
    data: {
      type: Object,
      default: null
    }
  }
}
</script>

分支节点本身其实没有理论内容,只是作为一个容器来渲染 childen 中的具体分支节点,分支节点其实就是一般节点,所以遍历渲染 Node 组件传入数据即可。

到目前为止所有节点组件就曾经创立结束了,传入数据看一下当初的成果:

能够看到大体上曾经成型了,只有连上线就功败垂成了。

连线

箭头组件

箭头线的款式其实是一样的,所以咱们创立一个箭头线的组件ArrowLine

<template>
  <div class="sfcArrowLine"></div>
</template>

<script>
export default {name: 'ArrowLine'}
</script>

<style lang="less" scoped>
.sfcArrowLine {
  position: relative;
  width: 65px;
  user-select: none;

  &:before {
    position: absolute
    top: 0;
    left: 0;
    transform: translateY(-50%);
    height: 2px;
    width: 100%;
    background-color: #dedede;
    content: '';
  }

  &:after {
    position: absolute;
    width: 0;
    height: 0;
    border-left: 10px solid #dedede;
    border-top: 6px solid transparent;
    border-bottom: 6px solid transparent;
    content: '';
    right: 0;
    top: 0;
    transform: translateY(-50%);
  }
}
</style>

线应用 before 元素绘制,箭头三角形应用 after 元素绘制。

开始节点增加箭头

首先在开始节点中引入箭头组件:

<template>
  <div class="sfcStartNodeContainer">
    <div class="sfcStartNodeContent">{{data.title}}</div>
    <ArrowLine></ArrowLine>
  </div>
</template>

成果如下:

箭头应该在左边,很简略,flex大法:

<style lang="less" scoped>
.sfcStartNodeContainer {
  display: flex;
  align-items: center;
}
</style>

一般节点增加箭头

同样先引入箭头组件:

<template>
  <div class="sfcNormalNodeContainer">
    <!-- 节点内容 -->
    <div class="sfcNormalNodeWrap">
      <div class="sfcNormalNodeContent">
        <div class="sfcNormalNodeTitle">
          {{data.title || ''}}
        </div>
        <!-- 省略 -->
      </div>
      <!-- 箭头组件放在这里 -->
      <ArrowLine></ArrowLine>
    </div>
    <!-- 递归渲染后续 Node 组件 -->
    <Node v-for="node in (data.nodeList || [])" :key="node.id" :data="node"></Node>
  </div>
</template>

同样须要设置成 flex 布局:

<style lang="less" scoped>
    .sfcNormalNodeWrap {
        display: flex;
        align-items: center;
    }
</style>

分支节点增加箭头

<template>
  <div class="sfcConditionNodeContainer">
    <div class="sfcConditionNodeItemList">
      <div
        class="sfcConditionNodeItem"
        v-for="node in data.children"
        :key="node.id"
      >
        <div class="sfcConditionNodeItemNodeWrap">
          <Node :data="node"></Node>
        </div>
      </div>
    </div>
    <ArrowLine></ArrowLine>
  </div>
</template>

和分支节点列表并列,同样少不了 flex 款式:

<style lang="less" scoped>
.sfcConditionNodeContainer {
  display: flex;
  align-items: center;
}
</style>

到这里整体的成果如下:

离胜利只有一步之遥了。

欠缺分支节点的连线

首先给分支节点加个间距,当初都挨着一起:

<template>
  <div class="sfcConditionNodeContainer">
    <div class="sfcConditionNodeItemList">
      <div
        class="sfcConditionNodeItem"
        v-for="node in data.children"
        :key="node.id"
      >
        <div class="sfcConditionNodeItemNodeWrap">
          <Node :data="node"></Node>
        </div>
      </div>
    </div>
  </div>
</template>

<style lang="less" scoped>
.sfcConditionNodeItem {padding: 30px;}
</style>

连贯分支整体的竖线

须要增加如下的竖线:

仔细观察能够发现其实就是给分支节点的前后各增加一竖线,其中的间距其实是因为后面咱们给分支节点的每个节点都设置了一个 30pxpadding,然而其实尾部的间距是不须要的:

所以咱们批改一下,把右内边距设为0

<style lang="less" scoped>
.sfcConditionNodeItem {
  padding: 30px;
  padding-right: 0;
}
</style>

你可能会想间接在分支节点的容器元素 sfcConditionNodeContainer 上间接前后绘制两条线,然而问题是这根线不是 100% 和容器元素一样高的,而是延长到最外侧两个分支的高度的一半,通过纯 css 其实很难绘制进去,所以咱们能够换种办法,让每个分支本人来绘制,这样其实就把一根线分成几段:

具体来说,就是最外侧的两个分支画一根一半高度的线,两头的分支画一根和高度一样的线。

要增加的线比拟多,伪元素不够用,所以咱们通过 div 元素来作为连线,而后通过相对定位来显示。

<template>
  <div class="sfcConditionNodeContainer">
    <div class="sfcConditionNodeItemList">
      <div
        class="sfcConditionNodeItem"
        v-for="node in data.children"
        :key="node.id"
      >
        <!-- 左侧的竖线 -->
        <div
          class="sfcConditionNodeItemLine sfcConditionNodeItemFirstLine"
        ></div>
        <!-- 右侧的竖线 -->
        <div
          class="sfcConditionNodeItemLine sfcConditionNodeItemLastLine"
        ></div>
        <div class="sfcConditionNodeItemNodeWrap">
          <Node :data="node"></Node>
        </div>
      </div>
    </div>
  </div>
</template>

<style lang="less" scoped>
    .sfcConditionNodeItem {
        position: relative;// 设置绝对定位

        // 前后竖线
        .sfcConditionNodeItemLine {
            position: absolute;
            height: 100%;// 默认为两头分支的的竖线,高度 100%
            width: 2px;
            left: 0px;
            top: 0;
            background-color: #dedede;

            // 右侧竖线间隔左侧为 100%
            &.sfcConditionNodeItemLastLine {left: 100%;}
        }

        // 最外侧的两个分支的竖线高度为 50%
        &:first-of-type {
            // 最顶部的分支的竖线距顶部 50%
            > .sfcConditionNodeItemLine {
                top: 50%;
                height: 50%;
            }
        }
        &:last-of-type {
            // 最底部的分支的竖线距顶部 0
            > .sfcConditionNodeItemLine {
                top: 0;
                height: 50%;
            }
        }
    }
</style>

成果如下:

连贯分支整体和分支的水平线

画完了竖线,接下来是水平线,如下所示,咱们要连贯分支左侧竖线和分支节点:

这根线的宽度其实就是 padding 的大小,而后 left0top50%,同样应用div 来绘制:

<template>
  <div class="sfcConditionNodeContainer">
    <div class="sfcConditionNodeItemList">
      <div
        class="sfcConditionNodeItem"
        v-for="node in data.children"
        :key="node.id"
      >
        <div
          class="sfcConditionNodeItemLine sfcConditionNodeItemFirstLine"
        ></div>
        <div
          class="sfcConditionNodeItemLine sfcConditionNodeItemLastLine"
        ></div>
        <!-- 连贯竖线和节点的水平线 -->
        <div class="sfcConditionNodeItemLinkLine"></div>
        <div class="sfcConditionNodeItemNodeWrap">
          <Node :data="node"></Node>
        </div>
      </div>
    </div>
  </div>
</template>

<style lang="less" scoped>
    // 连贯竖线和节点的水平线
    .sfcConditionNodeItemLinkLine {
        position: absolute;
        width: 30px;
        height: 2px;
        left: 0px;
        top: 50%;
        transform: translateY(-50%);// 让线段真正居中
        background-color: #dedede;
    }
</style>

连贯较短分支和分支整体右侧的水平线

最初还剩下如下图所示的较短分支和分支整体右侧的水平线:

这个也很简略,在每个分支的节点前面增加一个 div 作为连线,和分支节点作为兄弟节点,父级设置 flex 布局,连线宽度自适应即可:

<template>
  <div class="sfcConditionNodeContainer">
    <div class="sfcConditionNodeItemList">
      <div
        class="sfcConditionNodeItem"
        v-for="node in data.children"
        :key="node.id"
      >
        <div
          class="sfcConditionNodeItemLine sfcConditionNodeItemFirstLine"
        ></div>
        <div
          class="sfcConditionNodeItemLine sfcConditionNodeItemLastLine"
        ></div>
        <div class="sfcConditionNodeItemLinkLine"></div>
        <div class="sfcConditionNodeItemNodeWrap">
          <Node :data="node"></Node>
          <!-- 连贯较短分支和分支整体右侧的水平线 -->
          <div class="sfcConditionNodeItemLinkCrossLine"></div>
        </div>
      </div>
    </div>
  </div>
</template>

<style lang="less" scoped>
    .sfcConditionNodeItemNodeWrap {
        // 父级设置 flex 布局,让连线和节点整体垂直居中
        display: flex;
        align-items: center;

        // 连贯较短分支和分支整体右侧的水平线
        .sfcConditionNodeItemLinkCrossLine {
            height: 2px;
            flex-grow: 1;// 连线宽度自适应,填充残余空间
            background-color: #dedede;
        }
    }
</style>

到这里,节点布局以及连线都已实现,最终成果如下:

是不是很简略。

新增、编辑、删除节点

新增节点

新增节点首先须要在每一个节点前面的连接线上增加一个按钮,点击按钮后抉择要增加的节点的类型,而后进行增加。

除了分支节点外,只能增加一般节点,然而对于流程设计的业务来说,能够细分为很多类型,比方审批人、抄送人、发送短信等等,这个不同的业务可能不一样,所以必定不能写死,须要凋谢进去可自定义。

首先创立一个增加节点的按钮组件:

<template>
  <div class="sfcAddNode">
    <div class="sfcAddNodeBtn">
      <!-- 省略 -->
    </div>
  </div>
</template>

<script>
export default {name: 'AddNode'}
</script>

<style lang="less" scoped>
.sfcAddNode {
  position: absolute;
  right: 0;
  top: 0;
  width: 65px;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;

  .sfcAddNodeBtn {// 疏忽按钮款式}
}
</style>

按钮组件相对定位,宽度和箭头线宽度统一,为 65px,高度100%,和节点统一,相当于笼罩在箭头线上,而后通过flex 布局让真正的按钮居中即可。

先给一般节点加上:

<template>
  <div class="sfcNormalNodeContainer">
    <div class="sfcNormalNodeWrap">
      <div class="sfcNormalNodeContent">
          <!-- 省略 -->
      </div>
      <ArrowLine></ArrowLine>
      <!-- 增加节点组件 -->
      <AddNode></AddNode>
    </div>
    <Node v-for="node in (data.nodeList || [])" :key="node.id" :data="node"></Node>
  </div>
</template>

<style lang="less" scoped>
    .sfcNormalNodeWrap {position: relative;// ++}
</style>

接下来给每个分支也加上:

<template>
  <div class="sfcConditionNodeContainer">
    <div class="sfcConditionNodeItemList">
      <!-- 省略 -->
    </div>
    <ArrowLine></ArrowLine>
    <AddNode></AddNode>
  </div>
</template>

按钮默认都显示可能不太好看,能够暗藏起来,鼠标滑入按钮组件区域再显示。

而后当鼠标移入按钮时显示可增加的节点类型,点击要增加的节点类型后进行增加。增加一个节点其实就是往数组里插入一项,但不同的节点对应的数组是不一样的,如下图所示:

顶层节点增加下一个节点须要把节点插入顶层数组,分支里的节点插入下一个节点须要插入到本人的 nodeList 数组里,所以实现时须要辨别一下。

当然分支也是能够增加条件的:

点击后往分支节点的 children 数组里增加一项即可。

成果如下:

编辑

编辑次要是当点击某个节点当前能够批改节点题目,节点配置,节点显示的内容个别也是来自节点的配置。

所以对于库来说只有抛出一个点击事件即可,具体的编辑界面用户可依据业务自行开发。

删除

当鼠标悬浮到节点内容上显示一个删除按钮,点击后删除掉以后节点即可,对于条件分支来说,如果删除到仅剩一个分支,那么这个条件分支也就没有了意义,间接整个条件分支主动删除。

自定义节点内容

因为组件树层级比拟深,所以通过 slot 自定义节点内容不是很不便,所以我抉择了一个比拟 low 的形式,行将节点内容独自抽成一个组件,而后在注册组件的时候提供选项配置,那么如果想自定义节点内容,很简略,不要应用内置的节点内容组件,自行编写并注册一个即可,应用约定的组件名称就能够了。

const install = function (Vue, { notRegisterNodeContent} = {}) {Vue.component(ConditionNode.name, ConditionNode);
  Vue.component(EndNode.name, EndNode);
  Vue.component(Node.name, Node);
  Vue.component(NormalNode.name, NormalNode);
  Vue.component(StartNode.name, StartNode);
  Vue.component(Index.name, Index);
  // 须要自定义节点内容时通过选项参数指定不要注册内置节点内容组件即可
  if (!notRegisterNodeContent) {Vue.component(NodeContent.name, NodeContent);
  }
};

export default {install};

而后本人编写一个内容节点并注册:

Vue.component(CustomNodeContent.name, CustomNodeContent)
Vue.use(SimpleFlowChart, {notRegisterNodeContent: true})

同样增加节点悬浮面板也能够通过这种形式自定义。

垂直排列

反对垂直排列也很简略,基本上只有在所有设置了 display:flex 的中央加上flex-direction: column;,而后再把连线由竖的改成程度的,地位调一下就能够了:

最初

本文具体的介绍了一下如何应用 flex 布局实现一个简略的流程设计器,demo及残缺的源码如下:

demo:https://wanglin2.github.io/simple-flow-chart。

源码:https://github.com/wanglin2/simple-flow-chart。

退出移动版