首頁 > web前端 > Vue.js > 手把手帶你利用vue3.x繪製流程圖

手把手帶你利用vue3.x繪製流程圖

青灯夜游
發布: 2022-06-08 11:57:44
轉載
8988 人瀏覽過

利用vue3.x怎麼繪製流程圖?以下這篇文章給大家分享基於 vue3.x 的流程圖繪製方法,希望對大家有幫助!

手把手帶你利用vue3.x繪製流程圖

GitHub-workflow

#https://github.com/554246839/component -test/tree/dev/src/components/workflow

這裡主要是針對於工作流程場景的流程圖繪製及實作方式。 (學習影片分享:vuejs影片教學

#以下是效果圖:

手把手帶你利用vue3.x繪製流程圖

##整體結構版面:

手把手帶你利用vue3.x繪製流程圖

需要實現的功能清單:

    節點與連接線的可配置
  • 節點的拖曳與渲染及連接線的繪製
  • 節點與連接線的選擇
  • 節點的樣式調整
  • 節點移動時的吸附
  • 撤銷與還原

節點與連接線的可設定

    節點設定資訊
  • [
      {
        'id': '', // 每次渲染会生成一个新的id
        'name': 'start', // 节点名称,也就是类型
        'label': '开始', // 左侧列表节点的名称
        'displayName': '开始', // 渲染节点的显示名称(可修改)
        'className': 'icon-circle start', // 节点在渲染时候的class,可用于自定义节点的样式
        'attr': { // 节点的属性
          'x': 0, // 节点相对于画布的 x 位置
          'y': 0, // 节点相对于画布的 y 位置
          'w': 70, // 节点的初始宽度
          'h': 70  // 节点的初始高度
        },
        'next': [], // 节点出度的线
        'props': [] // 节点可配置的业务属性
      },
      // ...
    ]
    登入後複製
    連接線設定資訊
  • // next
    [
      {
        // 连接线的id
        'id': 'ee1c5fa3-f822-40f1-98a1-f76db6a2362b',
        // 连接线的结束节点id
        'targetComponentId': 'fa7fbbfa-fc43-4ac8-8911-451d0098d0cb',
        // 连接线在起始节点的方向
        'directionStart': 'right',
        // 连接线在结束节点的方向
        'directionEnd': 'left',
        // 线的类型(直线、折线、曲线)
        'lineType': 'straight',
        // 显示在连接线中点的标识信息
        'extra': '',
        // 连接线在起始节点的id
        'componentId': 'fde2a040-3795-4443-a57b-af412d06c023'
      },
      // ...
    ]
    登入後複製
    節點的屬性配置結構
  • // props
    [
      {
        // 表单的字段
        name: 'displayName',
        // 表单的标签
        label: '显示名称',
        // 字段的值
        value: '旅客运输',
        // 编辑的类型
        type: 'input',
        // 属性的必填字段
        required: true,
        // 表单组件的其它属性
        props: {
            placeholder: 'xxx'
        }
      },
      // ...
    ]
    登入後複製
對於下拉選擇的數據,如果下拉的數據非常多,那麼配置保存的數據量也會很大,所以可以把所有的下拉資料統一管理,在取得左側的配置節點的資訊時,將所有的下拉資料提取出來,以props 的name 值為key 保存起來,在用的時候用props.name 來取對應的下拉資料。

另外還需要配置連接線的屬性,相對於節點的屬性,每一個節點的屬性都有可能不一樣,但是連接線在沒有節點的時候是沒有的,所以我們要先準備好連接線的屬性,在連接線產生的時候,在加到連接線的屬性裡去。當然我們可以把連接線的屬性設定為一樣的,也可以根據節點的不同來設定不同連接線的屬性。

最後使用的方式:

<template>
  <workflow
    ref="workflowRef"
    @component-change="getActiveComponent"
    @line-change="getActiveLine"
    main-height="calc(100vh - 160px)">
  </workflow>
</template>


<script setup>
import { ref } from &#39;vue&#39;
import Workflow from &#39;@/components/workflow&#39;
import { commonRequest } from &#39;@/utils/common&#39;
import { ElMessage, ElMessageBox } from &#39;element-plus&#39;
import { useRoute } from &#39;vue-router&#39;

const route = useRoute()

const processId = route.query.processId // || &#39;testca08c433c34046e4bb2a8d3ce3ebc&#39;
const processType = route.query.processType

// 切换的当前节点
const getActiveComponent = (component: Record<string, any>) => {
  console.log(&#39;active component&#39;, component)
}

// 切换的当前连接线
const getActiveLine = (line: Record<string, any>) => {
  console.log(&#39;active line&#39;, line)
}

const workflowRef = ref<InstanceType<typeof Workflow>>()

// 获取配置的节点列表
const getConfig = () => {
  commonRequest(`/workflow/getWorkflowConfig?processType=${processType}`).then((res: Record<string, any>) => {
    // 需要把所有的属性根据name转换成 key - value 形式
    const props: Record<string, any> = {}
    transferOptions(res.result.nodes, props)
    // 设置左侧配置的节点数据
    workflowRef.value?.setConfig(res.result)
    getData(props)
  })
}
// 获取之前已经配置好的数据
const getData = (props: Record<string, any>) => {
  commonRequest(`/workflow/getWfProcess/${processId}`).then((res: Record<string, any>) => {
    // 调整属性,这里是为了当配置列表的节点或者属性有更新,从而更新已配置的节点的属性
    adjustProps(props, res.result.processJson)
    // 设置已配置好的数据,并渲染
    workflowRef.value?.setData(res.result.processJson, res.result.type || &#39;add&#39;)
  })
}

const init = () => {
  if (!processId) {
    ElMessageBox.alert(&#39;当前没有流程id&#39;)
    return
  }
  getConfig()
}
init()

const transferOptions = (nodes: Record<string, any>[], props: Record<string, any>) => {
  nodes?.forEach((node: Record<string, any>) => {
    props[node.name] = node.props
  })
}

const adjustProps = (props: Record<string, any>, nodes: Record<string, any>[]) => {
  nodes.forEach((node: Record<string, any>) => {
    const oldProp: Record<string, any>[] = node.props
    const res = transferKV(oldProp)
    node.props = JSON.parse(JSON.stringify(props[node.name]))
    node.props.forEach((prop: Record<string, any>) => {
      prop.value = res[prop.name]
    })
  })
}

const transferKV = (props: Record<string, any>[]) => {
  const res: Record<string, any> = {}
  props.forEach((prop: Record<string, any>) => {
    res[prop.name] = prop.value
  })
  return res
}
</script>
登入後複製

節點的拖曳與渲染及連接線的繪製

關於節點的拖曳就不多說了,就是drag 相關的用法,主要是渲染區域的節點和連接線的設計。

這裡的渲染區域的思路是:以canvas 元素作為畫布背景,節點是以div 的方式渲染拖曳進去的節點,拖曳的位置將是以canvas 的相對位置來移動,大概的結構如下:

<template>
    <!-- 渲染区域的祖先元素 -->
    <div>
        <!-- canvas 画布,绝对于父级元素定位, inset: 0; -->
        <canvas></canvas>
        <!-- 节点列表渲染的父级元素,绝对于父级元素定位, inset: 0; -->
        <div>
            <!-- 节点1,绝对于父级元素定位 -->
            <div></div>
            <!-- 节点2,绝对于父级元素定位 -->
            <div></div>
            <!-- 节点3,绝对于父级元素定位 -->
            <div></div>
            <!-- 节点4,绝对于父级元素定位 -->
            <div></div>
        </div>
    </div>
</template>
登入後複製

而連接線的繪製是根據next 字段的信息,查找到targetComponentId 組件的位置,然後在canvas上做兩點間的線條繪製。

連結的類型分為3種: 直線,折線,曲線

    直線
直線的繪製最為簡單,取兩個點連接就行。

// 绘制直线
const drawStraightLine = (
  ctx: CanvasRenderingContext2D, 
  points: [number, number][], 
  highlight?: boolean
) => {
  ctx.beginPath()
  ctx.moveTo(points[0][0], points[0][1])
  ctx.lineTo(points[1][0], points[1][1])
  // 是否是当前选中的连接线,当前连接线高亮
  shadowLine(ctx, highlight)
  ctx.stroke()
  ctx.restore()
  ctx.closePath()
}
登入後複製

手把手帶你利用vue3.x繪製流程圖

    折線
#折線的方式比較複雜,因為折線需要盡可能的不要把連接線和節點重合,所以它要判斷每一種連接線的場景,還有兩個節點的寬度和高度也要考慮計算。如下:

手把手帶你利用vue3.x繪製流程圖

起始節點有四個方向,目標節點也有四個方向,還有目標節點相對於起始節點有四個像限,所以嚴格來說,總共有4 * 4 * 4 = 64 種場景。這些場景中的折線點也不一樣,最多的有 4 次, 最少的折 0 次,單求出這 64 種座標點就用了 700 行程式碼。

手把手帶你利用vue3.x繪製流程圖

最後的繪製方法與直線一樣:

// 绘制折线
const drawBrokenLine = ({ ctx, points }: WF.DrawLineType, highlight?: boolean) => {
  ctx.beginPath()
  ctx.moveTo(points[0][0], points[0][1])
  for (let i = 1; i < points.length; i++) {
    ctx.lineTo(points[i][0], points[i][1])
  }
  shadowLine(ctx, highlight)
  ctx.stroke()
  ctx.restore()
  ctx.closePath()
}
登入後複製

    曲線
曲線相對於折線來說,想法會簡單很多,不需要考慮折線這麼多場景。

6 (1).gif

這裡的折線是用三階的貝塞爾曲線來繪製的,固定的取四個點,兩個起止點,兩個控制點,其中兩個起止點是固定的,我們只需要求出兩個控制點的座標即可。這裡程式碼不多,可以直接貼出來:

/**
 * Description: 计算三阶贝塞尔曲线的坐标
 */
import WF from &#39;../type&#39;

const coeff = 0.5
export default function calcBezierPoints({ startDire, startx, starty, destDire, destx, desty }: WF.CalcBezierType,
  points: [number, number][]) {

  const p = Math.max(Math.abs(destx - startx), Math.abs(desty - starty)) * coeff
  switch (startDire) {
    case &#39;down&#39;:
      points.push([startx, starty + p])
      break
    case &#39;up&#39;:
      points.push([startx, starty - p])
      break
    case &#39;left&#39;:
      points.push([startx - p, starty])
      break
    case &#39;right&#39;:
      points.push([startx + p, starty])
      break
    // no default
  }
  switch (destDire) {
    case &#39;down&#39;:
      points.push([destx, desty + p])
      break
    case &#39;up&#39;:
      points.push([destx, desty - p])
      break
    case &#39;left&#39;:
      points.push([destx - p, desty])
      break
    case &#39;right&#39;:
      points.push([destx + p, desty])
      break
    // no default
  }
}
登入後複製

簡單一點來說,第一個控制點是根據起始點來算的,第二個控制點是跟根據結束點來算的。算的方式是根據當前點相對於節點的方向,繼續往前算一段距離,而這段距離是根據起止兩個點的最大相對距離的一半(可能有點繞...)。

繪製方法:

// 绘制贝塞尔曲线
const drawBezier = ({ ctx, points }: WF.DrawLineType, highlight?: boolean) => {
  ctx.beginPath()
  ctx.moveTo(points[0][0], points[0][1])
  ctx.bezierCurveTo(
    points[1][0], points[1][1], points[2][0], points[2][1], points[3][0], points[3][1]
  )
  shadowLine(ctx, highlight)
  ctx.stroke()
  ctx.restore()
  ctx.globalCompositeOperation = &#39;source-over&#39;    //目标图像上显示源图像
}
登入後複製

节点与连接线的选择

节点是用 div 来渲染的,所以节点的选择可以忽略,然后就是连接点的选择,首先第一点是鼠标在移动的时候都要判断鼠标的当前位置下面是否有连接线,所以这里就有 3 种判断方法,呃... 严格来说是两种,因为折线是多条直线,所以是按直线的判断方法来。

// 判断当前鼠标位置是否有线
export const isAboveLine = (offsetX: number, offsetY: number, points: WF.LineInfo[]) => {
  for (let i = points.length - 1; i >= 0; --i) {
    const innerPonints = points[i].points
    let pre: [number, number], cur: [number, number]
    // 非曲线判断方法
    if (points[i].type !== &#39;bezier&#39;) {
      for (let j = 1; j < innerPonints.length; j++) {
        pre = innerPonints[j - 1]
        cur = innerPonints[j]
        if (getDistance([offsetX, offsetY], pre, cur) < 20) {
          return points[i]
        }
      }
    } else {
      // 先用 x 求出对应的 t,用 t 求相应位置的 y,再比较得出的 y 与 offsetY 之间的差值
      const tsx = getBezierT(innerPonints[0][0], innerPonints[1][0], innerPonints[2][0], innerPonints[3][0], offsetX)
      for (let x = 0; x < 3; x++) {
        if (tsx[x] <= 1 && tsx[x] >= 0) {
          const ny = getThreeBezierPoint(tsx[x], innerPonints[0], innerPonints[1], innerPonints[2], innerPonints[3])
          if (Math.abs(ny[1] - offsetY) < 8) {
            return points[i]
          }
        }
      }
      // 如果上述没有结果,则用 y 求出对应的 t,再用 t 求出对应的 x,与 offsetX 进行匹配
      const tsy = getBezierT(innerPonints[0][1], innerPonints[1][1], innerPonints[2][1], innerPonints[3][1], offsetY)
      for (let y = 0; y < 3; y++) {
        if (tsy[y] <= 1 && tsy[y] >= 0) {
          const nx = getThreeBezierPoint(tsy[y], innerPonints[0], innerPonints[1], innerPonints[2], innerPonints[3])
          if (Math.abs(nx[0] - offsetX) < 8) {
            return points[i]
          }
        }
      }
    }
  }

  return false
}
登入後複製

直线的判断方法是点到线段的距离:

/**
 * 求点到线段的距离
 * @param {number} pt 直线外的点
 * @param {number} p 直线内的点1
 * @param {number} q 直线内的点2
 * @returns {number} 距离
 */
function getDistance(pt: [number, number], p: [number, number], q: [number, number]) {
  const pqx = q[0] - p[0]
  const pqy = q[1] - p[1]
  let dx = pt[0] - p[0]
  let dy = pt[1] - p[1]
  const d = pqx * pqx + pqy * pqy   // qp线段长度的平方
  let t = pqx * dx + pqy * dy     // p pt向量 点积 pq 向量(p相当于A点,q相当于B点,pt相当于P点)
  if (d > 0) {  // 除数不能为0; 如果为零 t应该也为零。下面计算结果仍然成立。                   
    t /= d      // 此时t 相当于 上述推导中的 r。
  }
  if (t < 0) {  // 当t(r)< 0时,最短距离即为 pt点 和 p点(A点和P点)之间的距离。
    t = 0
  } else if (t > 1) { // 当t(r)> 1时,最短距离即为 pt点 和 q点(B点和P点)之间的距离。
    t = 1
  }

  // t = 0,计算 pt点 和 p点的距离; t = 1, 计算 pt点 和 q点 的距离; 否则计算 pt点 和 投影点 的距离。
  dx = p[0] + t * pqx - pt[0]
  dy = p[1] + t * pqy - pt[1]

  return dx * dx + dy * dy
}
登入後複製

关于曲线的判断方法比较复杂,这里就不多介绍, 想了解的可以去看这篇:如何判断一个坐标点是否在三阶贝塞尔曲线附近

连接线还有一个功能就是双击连接线后可以编辑这条连接线的备注信息。这个备注信息的位置是在当前连接线的中心点位置。所以我们需要求出中心点,这个相对简单。

// 获取一条直线的中点坐标
const getStraightLineCenterPoint = ([[x1, y1], [x2, y2]]: [number, number][]): [number, number] => {
  return [(x1 + x2) / 2, (y1 + y2) / 2]
}

// 获取一条折线的中点坐标
const getBrokenCenterPoint = (points: [number, number][]): [number, number] => {
  const lineDistancehalf = getLineDistance(points) >> 1

  let distanceSum = 0, pre = 0, tp: [number, number][] = [], distance = 0

  for (let i = 1; i < points.length; i++) {
    pre = getTwoPointDistance(points[i - 1], points[i])
    if (distanceSum + pre > lineDistancehalf) {
      tp = [points[i - 1], points[i]]
      distance = lineDistancehalf - distanceSum
      break
    }
    distanceSum += pre
  }

  if (!tp.length) {
    return [0, 0]
  }

  let x = tp[0][0], y = tp[0][1]

  if (tp[0][0] === tp[1][0]) {
    if (tp[0][1] > tp[1][1]) {
      y -= distance
    } else {
      y += distance
    }
  } else {
    if (tp[0][0] > tp[1][0]) {
      x -= distance
    } else {
      x += distance
    }
  }

  return [x, y]
}
登入後複製

曲线的中心点位置,可以直接拿三阶贝塞尔曲线公式求出

// 获取三阶贝塞尔曲线的中点坐标
const getBezierCenterPoint = (points: [number, number][]) => {
  return getThreeBezierPoint(
    0.5, points[0], points[1], points[2], points[3]
  )
}

/**
 * @desc 获取三阶贝塞尔曲线的线上坐标
 * @param {number} t 当前百分比
 * @param {Array} p1 起点坐标
 * @param {Array} p2 终点坐标
 * @param {Array} cp1 控制点1
 * @param {Array} cp2 控制点2
 */
export const getThreeBezierPoint = (
  t: number,
  p1: [number, number],
  cp1: [number, number],
  cp2: [number, number],
  p2: [number, number]
): [number, number] => {
  const [x1, y1] = p1
  const [x2, y2] = p2
  const [cx1, cy1] = cp1
  const [cx2, cy2] = cp2
  const x =
    x1 * (1 - t) * (1 - t) * (1 - t) +
    3 * cx1 * t * (1 - t) * (1 - t) +
    3 * cx2 * t * t * (1 - t) +
    x2 * t * t * t
  const y =
    y1 * (1 - t) * (1 - t) * (1 - t) +
    3 * cy1 * t * (1 - t) * (1 - t) +
    3 * cy2 * t * t * (1 - t) +
    y2 * t * t * t
  return [x | 0, y | 0]
}
登入後複製

在算出每一条的中心点位置后,在目标位置添加备注信息即可:

手把手帶你利用vue3.x繪製流程圖

节点的样式调整

节点的样式调整主要是位置及大小,而这些属性就是节点里面的 attr,在相应的事件下根据鼠标移动的方向及位置,来调整节点的样式。

8 (1).gif

还有批量操作也是同样,不过批量操作是要先计算出哪些节点的范围。

// 获取范围选中内的组件
export const getSelectedComponent = (componentList: WF.ComponentType[], areaPosi: WF.Attr) => {
  let selectedArea: WF.Attr | null = null
  let minx = Infinity, miny = Infinity, maxx = -Infinity, maxy = -Infinity
  const selectedComponents = componentList.filter((component: WF.ComponentType) => {

    const res = areaPosi.x <= component.attr.x &&
      areaPosi.y <= component.attr.y &&
      areaPosi.x + areaPosi.w >= component.attr.x + component.attr.w &&
      areaPosi.y + areaPosi.h >= component.attr.y + component.attr.h

    if (res) {
      minx = Math.min(minx, component.attr.x)
      miny = Math.min(miny, component.attr.y)
      maxx = Math.max(maxx, component.attr.x + component.attr.w)
      maxy = Math.max(maxy, component.attr.y + component.attr.h)
    }
    return res
  })

  if (selectedComponents.length) {
    selectedArea = {
      x: minx,
      y: miny,
      w: maxx - minx,
      h: maxy - miny
    }
    return {
      selectedArea, selectedComponents
    }
  }
  return null
}
登入後複製

手把手帶你利用vue3.x繪製流程圖

这个有个小功能没有做,就是在批量调整大小的时候,节点间的相对距离应该是不动的,这里忽略了。

节点移动时的吸附

这里的吸附功能其实是做了一个简单版的,就是 x 和 y 轴都只有一条校准线,且校准的优先级是从左至右,从上至下。

手把手帶你利用vue3.x繪製流程圖

这里吸附的标准是节点的 6 个点:X 轴的左中右,Y 轴的上中下,当前节点在移动的时候,会用当前节点的 6 个点,一一去与其它节点的 6 个点做比较,在误差正负 2px 的情况,自动更新为0,即自定对齐。

因为移动当前节点时候,其它的节点是不动的,所以这里是做了一步预处理,即在鼠标按下去的时候,把其它的节点的 6 个点都线算出来,用 Set 结构保存,在移动的过程的比较中,计算量会相对较少。

// 计算其它节点的所有点位置
export const clearupPostions = (componentList: WF.ComponentType[], currId: string) => {
  // x 坐标集合
  const coordx = new Set<number>()
  // y 坐标集合
  const coordy = new Set<number>()

  componentList.forEach((component: WF.ComponentType) => {
    if (component.id === currId) {
      return
    }
    const { x, y, w, h } = component.attr
    coordx.add(x)
    coordx.add(x + (w >> 1))
    coordx.add(x + w)
    coordy.add(y)
    coordy.add(y + (h >> 1))
    coordy.add(y + h)
  })

  return [coordx, coordy]
}
登入後複製

判读是否有可吸附的点

// 可吸附范围
const ADSORBRANGE = 2
// 查询是否有可吸附坐标
const hasAdsorbable = (
  coords: Set<number>[], x: number, y: number, w: number, h: number
) => {
  // x, y, w, h, w/2, h/2
  const coord: (number | null)[] = [null, null, null, null, null, null]
  // 查询 x 坐标
  for (let i = 0; i <= ADSORBRANGE; i++) {
    if (coords[0].has(x + i)) {
      coord[0] = i
      break
    }
    if (coords[0].has(x - i)) {
      coord[0] = -i
      break
    }
  }

  // 查询 y 坐标
  for (let i = 0; i <= ADSORBRANGE; i++) {
    if (coords[1].has(y + i)) {
      coord[1] = i
      break
    }
    if (coords[1].has(y - i)) {
      coord[1] = -i
      break
    }
  }

  // 查询 x + w 坐标
  for (let i = 0; i <= ADSORBRANGE; i++) {
    if (coords[0].has(x + w + i)) {
      coord[2] = i
      break
    }
    if (coords[0].has(x + w - i)) {
      coord[2] = -i
      break
    }
  }

  // 查询 y + h 坐标
  for (let i = 0; i <= ADSORBRANGE; i++) {
    if (coords[1].has(y + h + i)) {
      coord[3] = i
      break
    }
    if (coords[1].has(y + h - i)) {
      coord[3] = -i
      break
    }
  }

  // 查询 x + w/2 坐标
  for (let i = 0; i <= ADSORBRANGE; i++) {
    if (coords[0].has(x + (w >> 1) + i)) {
      coord[4] = i
      break
    }
    if (coords[0].has(x + (w >> 1) - i)) {
      coord[4] = -i
      break
    }
  }

  // 查询 y + h/2 坐标
  for (let i = 0; i <= ADSORBRANGE; i++) {
    if (coords[1].has(y + (h >> 1) + i)) {
      coord[5] = i
      break
    }
    if (coords[1].has(y + (h >> 1) - i)) {
      coord[5] = -i
      break
    }
  }

  return coord
}
登入後複製

最后更新状态。

// 获取修正后的 x, y,还有吸附线的状态
export const getAdsordXY = (
  coords: Set<number>[], x: number, y: number, w: number, h: number
) => {
  const vals = hasAdsorbable(
    coords, x, y, w, h
  )

  let linex = null
  let liney = null

  if (vals[0] !== null) { // x
    x += vals[0]
    linex = x
  } else if (vals[2] !== null) { // x + w
    x += vals[2]
    linex = x + w
  } else if (vals[4] !== null) { // x + w/2
    x += vals[4]
    linex = x + (w >> 1)
  }

  if (vals[1] !== null) { // y
    y += vals[1]
    liney = y
  } else if (vals[3] !== null) { // y + h
    y += vals[3]
    liney = y + h
  } else if (vals[5] !== null) { // y + h/2
    y += vals[5]
    liney = y + (h >> 1)
  }

  return {
    x, y, linex, liney
  }
}
登入後複製

撤销和恢复

撤销和恢复的功能是比较简单的,其实就是用栈来保存每一次需要保存的配置结构,就是要考虑哪些操作是可以撤销和恢复的,就是像节点移动,节点的新增和删除,连接线的连接,连接线的备注新增和编辑等等,在相关的操作下面入栈即可。

// 撤销和恢复操作
const cacheComponentList = ref<WF.ComponentType[][]>([])
const currentComponentIndex = ref(-1)
// 撤销
const undo = () => {
  componentRenderList.value = JSON.parse(JSON.stringify(cacheComponentList.value[--currentComponentIndex.value]))
  // 更新视图
  updateCanvas(true)
  cancelSelected()
}
// 恢复
const redo = () => {
  componentRenderList.value = JSON.parse(JSON.stringify(cacheComponentList.value[++currentComponentIndex.value]))
  // 更新视图
  updateCanvas(true)
  cancelSelected()
}
// 缓存入栈
const chacheStack = () => {
  if (cacheComponentList.value.length - 1 > currentComponentIndex.value) {
    cacheComponentList.value.length = currentComponentIndex.value + 1
  }
  cacheComponentList.value.push(JSON.parse(JSON.stringify(componentRenderList.value)))
  currentComponentIndex.value++
}
登入後複製

手把手帶你利用vue3.x繪製流程圖

最后

这里主要的已经差不多都写了,其实最红还有一个挺有用的功能还没有做。就是改变已经绘制的连接线的起止点。

这里的思路是:先选中需要改变起止点的连接线,然后把鼠标移动到起止点的位置,将它从已经绘制的状态改为正在绘制的状态,然后再选择它的开始位置或者结束位置。这个后面看情况吧,有空就加上。

(学习视频分享:web前端开发编程基础视频

以上是手把手帶你利用vue3.x繪製流程圖的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:juejin.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板