目錄
節點的拖曳與渲染及連接線的繪製
节点与连接线的选择
节点的样式调整
节点移动时的吸附
撤销和恢复
最后
首頁 web前端 Vue.js 手把手帶你利用vue3.x繪製流程圖

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

Jun 08, 2022 am 11:57 AM
vue vue.js vue3

利用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中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

熱門話題

Java教學
1664
14
CakePHP 教程
1421
52
Laravel 教程
1315
25
PHP教程
1266
29
C# 教程
1239
24
vue.js vs.反應:特定於項目的考慮因素 vue.js vs.反應:特定於項目的考慮因素 Apr 09, 2025 am 12:01 AM

Vue.js適合中小型項目和快速迭代,React適用於大型複雜應用。 1)Vue.js易於上手,適用於團隊經驗不足或項目規模較小的情況。 2)React的生態系統更豐富,適合有高性能需求和復雜功能需求的項目。

vue怎麼給按鈕添加函數 vue怎麼給按鈕添加函數 Apr 08, 2025 am 08:51 AM

可以通過以下步驟為 Vue 按鈕添加函數:將 HTML 模板中的按鈕綁定到一個方法。在 Vue 實例中定義該方法並編寫函數邏輯。

vue多頁面開發是啥意思 vue多頁面開發是啥意思 Apr 07, 2025 pm 11:57 PM

Vue 多頁面開發是一種使用 Vue.js 框架構建應用程序的方法,其中應用程序被劃分為獨立的頁面:代碼維護性:將應用程序拆分為多個頁面可以使代碼更易於管理和維護。模塊化:每個頁面都可以作為獨立的模塊,便於重用和替換。路由簡單:頁面之間的導航可以通過簡單的路由配置來管理。 SEO 優化:每個頁面都有自己的 URL,這有助於搜索引擎優化。

React與Vue:Netflix使用哪個框架? React與Vue:Netflix使用哪個框架? Apr 14, 2025 am 12:19 AM

NetflixusesAcustomFrameworkcalled“ Gibbon” BuiltonReact,notReactorVuedIrectly.1)TeamSperience:selectBasedonFamiliarity.2)ProjectComplexity:vueforsimplerprojects:reactforforforproproject,reactforforforcompleplexones.3)cocatizationneedneeds:reactoffipicatizationneedneedneedneedneedneeds:reactoffersizationneedneedneedneedneeds:reactoffersizatization needefersmoreflexibleise.4)

vue的div怎麼跳轉 vue的div怎麼跳轉 Apr 08, 2025 am 09:18 AM

Vue 中 div 元素跳轉的方法有兩種:使用 Vue Router,添加 router-link 組件。添加 @click 事件監聽器,調用 this.$router.push() 方法跳轉。

反應,vue和Netflix前端的未來 反應,vue和Netflix前端的未來 Apr 12, 2025 am 12:12 AM

Netflix主要使用React作為前端框架,輔以Vue用於特定功能。 1)React的組件化和虛擬DOM提升了Netflix應用的性能和開發效率。 2)Vue在Netflix的內部工具和小型項目中應用,其靈活性和易用性是關鍵。

vue怎麼a標籤跳轉 vue怎麼a標籤跳轉 Apr 08, 2025 am 09:24 AM

實現 Vue 中 a 標籤跳轉的方法包括:HTML 模板中使用 a 標籤指定 href 屬性。使用 Vue 路由的 router-link 組件。使用 JavaScript 的 this.$router.push() 方法。可通過 query 參數傳遞參數,並在 router 選項中配置路由以進行動態跳轉。

Netflix的前端:React(或VUE)的示例和應用 Netflix的前端:React(或VUE)的示例和應用 Apr 16, 2025 am 12:08 AM

Netflix使用React作為其前端框架。 1)React的組件化開發模式和強大生態系統是Netflix選擇它的主要原因。 2)通過組件化,Netflix將復雜界面拆分成可管理的小塊,如視頻播放器、推薦列表和用戶評論。 3)React的虛擬DOM和組件生命週期優化了渲染效率和用戶交互管理。

See all articles