溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

如何利用vue3.x繪制流程圖

發(fā)布時(shí)間:2022-06-08 13:52:07 來源:億速云 閱讀:345 作者:iii 欄目:編程語(yǔ)言

這篇文章主要介紹“如何利用vue3.x繪制流程圖”的相關(guān)知識(shí),小編通過實(shí)際案例向大家展示操作過程,操作方法簡(jiǎn)單快捷,實(shí)用性強(qiáng),希望這篇“如何利用vue3.x繪制流程圖”文章能幫助大家解決問題。

如何利用vue3.x繪制流程圖

下面是效果圖:

如何利用vue3.x繪制流程圖

整體結(jié)構(gòu)布局:

如何利用vue3.x繪制流程圖

需要實(shí)現(xiàn)的功能列表:

  • 節(jié)點(diǎn)與連接線的可配置

  • 節(jié)點(diǎn)的拖拽與渲染及連接線的繪制

  • 節(jié)點(diǎn)與連接線的選擇

  • 節(jié)點(diǎn)的樣式調(diào)整

  • 節(jié)點(diǎn)移動(dòng)時(shí)的吸附

  • 撤銷和恢復(fù)

節(jié)點(diǎn)與連接線的可配置

  • 節(jié)點(diǎn)配置信息

[
  {
    'id': '', // 每次渲染會(huì)生成一個(gè)新的id
    'name': 'start', // 節(jié)點(diǎn)名稱,也就是類型
    'label': '開始', // 左側(cè)列表節(jié)點(diǎn)的名稱
    'displayName': '開始', // 渲染節(jié)點(diǎn)的顯示名稱(可修改)
    'className': 'icon-circle start', // 節(jié)點(diǎn)在渲染時(shí)候的class,可用于自定義節(jié)點(diǎn)的樣式
    'attr': { // 節(jié)點(diǎn)的屬性
      'x': 0, // 節(jié)點(diǎn)相對(duì)于畫布的 x 位置
      'y': 0, // 節(jié)點(diǎn)相對(duì)于畫布的 y 位置
      'w': 70, // 節(jié)點(diǎn)的初始寬度
      'h': 70  // 節(jié)點(diǎn)的初始高度
    },
    'next': [], // 節(jié)點(diǎn)出度的線
    'props': [] // 節(jié)點(diǎn)可配置的業(yè)務(wù)屬性
  },
  // ...
]
  • 連接線配置信息

// next
[
  {
    // 連接線的id
    'id': 'ee1c5fa3-f822-40f1-98a1-f76db6a2362b',
    // 連接線的結(jié)束節(jié)點(diǎn)id
    'targetComponentId': 'fa7fbbfa-fc43-4ac8-8911-451d0098d0cb',
    // 連接線在起始節(jié)點(diǎn)的方向
    'directionStart': 'right',
    // 連接線在結(jié)束節(jié)點(diǎn)的方向
    'directionEnd': 'left',
    // 線的類型(直線、折線、曲線)
    'lineType': 'straight',
    // 顯示在連接線中點(diǎn)的標(biāo)識(shí)信息
    'extra': '',
    // 連接線在起始節(jié)點(diǎn)的id
    'componentId': 'fde2a040-3795-4443-a57b-af412d06c023'
  },
  // ...
]
  • 節(jié)點(diǎn)的屬性配置結(jié)構(gòu)

// props
[
  {
    // 表單的字段
    name: 'displayName',
    // 表單的標(biāo)簽
    label: '顯示名稱',
    // 字段的值
    value: '旅客運(yùn)輸',
    // 編輯的類型
    type: 'input',
    // 屬性的必填字段
    required: true,
    // 表單組件的其它屬性
    props: {
        placeholder: 'xxx'
    }
  },
  // ...
]

對(duì)于下拉選擇的數(shù)據(jù),如果下拉的數(shù)據(jù)非常多,那么配置保存的數(shù)據(jù)量也會(huì)很大,所以可以把所有的下拉數(shù)據(jù)統(tǒng)一管理,在獲取左側(cè)的配置節(jié)點(diǎn)的信息時(shí),將所有的下拉數(shù)據(jù)提取出來,以 props 的 name 值為 key 保存起來,在用的時(shí)候用 props.name 來取對(duì)應(yīng)的下拉數(shù)據(jù)。

另外還需要配置連接線的屬性,相對(duì)于節(jié)點(diǎn)的屬性,每一個(gè)節(jié)點(diǎn)的屬性都有可能不一樣,但是連接線在沒有節(jié)點(diǎn)的時(shí)候是沒有的,所以我們要先準(zhǔn)備好連接線的屬性,在連接線生成的時(shí)候,在加到連接線的屬性里去。當(dāng)然我們可以把連接線的屬性設(shè)置為一樣的,也可以根據(jù)節(jié)點(diǎn)的不同來設(shè)置不同連接線的屬性。

最后使用的方式:

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


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

const route = useRoute()

const processId = route.query.processId // || 'testca08c433c34046e4bb2a8d3ce3ebc'
const processType = route.query.processType

// 切換的當(dāng)前節(jié)點(diǎn)
const getActiveComponent = (component: Record<string, any>) => {
  console.log('active component', component)
}

// 切換的當(dāng)前連接線
const getActiveLine = (line: Record<string, any>) => {
  console.log('active line', line)
}

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

// 獲取配置的節(jié)點(diǎn)列表
const getConfig = () => {
  commonRequest(`/workflow/getWorkflowConfig?processType=${processType}`).then((res: Record<string, any>) => {
    // 需要把所有的屬性根據(jù)name轉(zhuǎn)換成 key - value 形式
    const props: Record<string, any> = {}
    transferOptions(res.result.nodes, props)
    // 設(shè)置左側(cè)配置的節(jié)點(diǎn)數(shù)據(jù)
    workflowRef.value?.setConfig(res.result)
    getData(props)
  })
}
// 獲取之前已經(jīng)配置好的數(shù)據(jù)
const getData = (props: Record<string, any>) => {
  commonRequest(`/workflow/getWfProcess/${processId}`).then((res: Record<string, any>) => {
    // 調(diào)整屬性,這里是為了當(dāng)配置列表的節(jié)點(diǎn)或者屬性有更新,從而更新已配置的節(jié)點(diǎn)的屬性
    adjustProps(props, res.result.processJson)
    // 設(shè)置已配置好的數(shù)據(jù),并渲染
    workflowRef.value?.setData(res.result.processJson, res.result.type || 'add')
  })
}

const init = () => {
  if (!processId) {
    ElMessageBox.alert('當(dāng)前沒有流程id')
    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>

節(jié)點(diǎn)的拖拽與渲染及連接線的繪制

關(guān)于節(jié)點(diǎn)的拖拽就不多說了,就是 drag 相關(guān)的用法,主要是渲染區(qū)域的節(jié)點(diǎn)和連接線的設(shè)計(jì)。

這里的渲染區(qū)域的思路是:以 canvas 元素作為畫布背景,節(jié)點(diǎn)是以 div 的方式渲染拖拽進(jìn)去的節(jié)點(diǎn),拖拽的位置將是以 canvas 的相對(duì)位置來移動(dòng),大概的結(jié)構(gòu)如下:

<template>
    <!-- 渲染區(qū)域的祖先元素 -->
    <div>
        <!-- canvas 畫布,絕對(duì)于父級(jí)元素定位, inset: 0; -->
        <canvas></canvas>
        <!-- 節(jié)點(diǎn)列表渲染的父級(jí)元素,絕對(duì)于父級(jí)元素定位, inset: 0; -->
        <div>
            <!-- 節(jié)點(diǎn)1,絕對(duì)于父級(jí)元素定位 -->
            <div></div>
            <!-- 節(jié)點(diǎn)2,絕對(duì)于父級(jí)元素定位 -->
            <div></div>
            <!-- 節(jié)點(diǎn)3,絕對(duì)于父級(jí)元素定位 -->
            <div></div>
            <!-- 節(jié)點(diǎn)4,絕對(duì)于父級(jí)元素定位 -->
            <div></div>
        </div>
    </div>
</template>

而連接線的繪制是根據(jù) next 字段的信息,查找到 targetComponentId 組件的位置,然后在canvas上做兩點(diǎn)間的 線條繪制。

鏈接的類型分為3種: 直線,折線,曲線

  • 直線

直線的繪制最為簡(jiǎn)單,取兩個(gè)點(diǎn)連接就行。

// 繪制直線
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])
  // 是否是當(dāng)前選中的連接線,當(dāng)前連接線高亮
  shadowLine(ctx, highlight)
  ctx.stroke()
  ctx.restore()
  ctx.closePath()
}

如何利用vue3.x繪制流程圖

  • 折線

折線的方式比較復(fù)雜,因?yàn)檎劬€需要盡可能的不要把連接線和節(jié)點(diǎn)重合,所以它要判斷每一種連接線的場(chǎng)景,還有兩個(gè)節(jié)點(diǎn)的寬度和高度也需要考慮計(jì)算。如下:

如何利用vue3.x繪制流程圖

起始節(jié)點(diǎn)有四個(gè)方向,目標(biāo)節(jié)點(diǎn)也有四個(gè)方向,還有目標(biāo)節(jié)點(diǎn)相對(duì)于起始節(jié)點(diǎn)有四個(gè)象限,所以嚴(yán)格來說,總共有 4 * 4 * 4 = 64 種場(chǎng)景。這些場(chǎng)景中的折線點(diǎn)也不一樣,最多的有 4 次, 最少的折 0 次,單求出這 64 種坐標(biāo)點(diǎn)就用了 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()
}
  • 曲線

曲線相對(duì)于折線來說,思路會(huì)簡(jiǎn)單很多,不需要考慮折線這么多場(chǎng)景。

如何利用vue3.x繪制流程圖

這里的折線是用三階的貝塞爾曲線來繪制的,固定的取四個(gè)點(diǎn),兩個(gè)起止點(diǎn),兩個(gè)控制點(diǎn),其中兩個(gè)起止點(diǎn)是固定的,我們只需要求出兩個(gè)控制點(diǎn)的坐標(biāo)即可。這里代碼不多,可以直接貼出來:

/**
 * Description: 計(jì)算三階貝塞爾曲線的坐標(biāo)
 */
import WF from '../type'

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 'down':
      points.push([startx, starty + p])
      break
    case 'up':
      points.push([startx, starty - p])
      break
    case 'left':
      points.push([startx - p, starty])
      break
    case 'right':
      points.push([startx + p, starty])
      break
    // no default
  }
  switch (destDire) {
    case 'down':
      points.push([destx, desty + p])
      break
    case 'up':
      points.push([destx, desty - p])
      break
    case 'left':
      points.push([destx - p, desty])
      break
    case 'right':
      points.push([destx + p, desty])
      break
    // no default
  }
}

簡(jiǎn)單一點(diǎn)來說,第一個(gè)控制點(diǎn)是根據(jù)起始點(diǎn)來算的,第二個(gè)控制點(diǎn)是跟根據(jù)結(jié)束點(diǎn)來算的。算的方式是根據(jù)當(dāng)前點(diǎn)相對(duì)于節(jié)點(diǎn)的方向,繼續(xù)往前算一段距離,而這段距離是根據(jù)起止兩個(gè)點(diǎn)的最大相對(duì)距離的一半(可能有點(diǎn)繞...)。

繪制方法:

// 繪制貝塞爾曲線
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 = 'source-over'    //目標(biāo)圖像上顯示源圖像
}

節(jié)點(diǎn)與連接線的選擇

節(jié)點(diǎn)是用 div 來渲染的,所以節(jié)點(diǎn)的選擇可以忽略,然后就是連接點(diǎn)的選擇,首先第一點(diǎn)是鼠標(biāo)在移動(dòng)的時(shí)候都要判斷鼠標(biāo)的當(dāng)前位置下面是否有連接線,所以這里就有 3 種判斷方法,呃... 嚴(yán)格來說是兩種,因?yàn)檎劬€是多條直線,所以是按直線的判斷方法來。

// 判斷當(dāng)前鼠標(biāo)位置是否有線
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 !== 'bezier') {
      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 求出對(duì)應(yīng)的 t,用 t 求相應(yīng)位置的 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]
          }
        }
      }
      // 如果上述沒有結(jié)果,則用 y 求出對(duì)應(yīng)的 t,再用 t 求出對(duì)應(yīng)的 x,與 offsetX 進(jìn)行匹配
      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
}

直線的判斷方法是點(diǎn)到線段的距離:

/**
 * 求點(diǎn)到線段的距離
 * @param {number} pt 直線外的點(diǎn)
 * @param {number} p 直線內(nèi)的點(diǎn)1
 * @param {number} q 直線內(nèi)的點(diǎn)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線段長(zhǎng)度的平方
  let t = pqx * dx + pqy * dy     // p pt向量 點(diǎn)積 pq 向量(p相當(dāng)于A點(diǎn),q相當(dāng)于B點(diǎn),pt相當(dāng)于P點(diǎn))
  if (d > 0) {  // 除數(shù)不能為0; 如果為零 t應(yīng)該也為零。下面計(jì)算結(jié)果仍然成立。                   
    t /= d      // 此時(shí)t 相當(dāng)于 上述推導(dǎo)中的 r。
  }
  if (t < 0) {  // 當(dāng)t(r)< 0時(shí),最短距離即為 pt點(diǎn) 和 p點(diǎn)(A點(diǎn)和P點(diǎn))之間的距離。
    t = 0
  } else if (t > 1) { // 當(dāng)t(r)> 1時(shí),最短距離即為 pt點(diǎn) 和 q點(diǎn)(B點(diǎn)和P點(diǎn))之間的距離。
    t = 1
  }

  // t = 0,計(jì)算 pt點(diǎn) 和 p點(diǎn)的距離; t = 1, 計(jì)算 pt點(diǎn) 和 q點(diǎn) 的距離; 否則計(jì)算 pt點(diǎn) 和 投影點(diǎn) 的距離。
  dx = p[0] + t * pqx - pt[0]
  dy = p[1] + t * pqy - pt[1]

  return dx * dx + dy * dy
}

關(guān)于曲線的判斷方法比較復(fù)雜,這里就不多介紹, 想了解的可以去看這篇:如何判斷一個(gè)坐標(biāo)點(diǎn)是否在三階貝塞爾曲線附近

連接線還有一個(gè)功能就是雙擊連接線后可以編輯這條連接線的備注信息。這個(gè)備注信息的位置是在當(dāng)前連接線的中心點(diǎn)位置。所以我們需要求出中心點(diǎn),這個(gè)相對(duì)簡(jiǎn)單。

// 獲取一條直線的中點(diǎn)坐標(biāo)
const getStraightLineCenterPoint = ([[x1, y1], [x2, y2]]: [number, number][]): [number, number] => {
  return [(x1 + x2) / 2, (y1 + y2) / 2]
}

// 獲取一條折線的中點(diǎn)坐標(biāo)
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]
}

曲線的中心點(diǎn)位置,可以直接拿三階貝塞爾曲線公式求出

// 獲取三階貝塞爾曲線的中點(diǎn)坐標(biāo)
const getBezierCenterPoint = (points: [number, number][]) => {
  return getThreeBezierPoint(
    0.5, points[0], points[1], points[2], points[3]
  )
}

/**
 * @desc 獲取三階貝塞爾曲線的線上坐標(biāo)
 * @param {number} t 當(dāng)前百分比
 * @param {Array} p1 起點(diǎn)坐標(biāo)
 * @param {Array} p2 終點(diǎn)坐標(biāo)
 * @param {Array} cp1 控制點(diǎn)1
 * @param {Array} cp2 控制點(diǎn)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]
}

在算出每一條的中心點(diǎn)位置后,在目標(biāo)位置添加備注信息即可:

如何利用vue3.x繪制流程圖

節(jié)點(diǎn)的樣式調(diào)整

節(jié)點(diǎn)的樣式調(diào)整主要是位置及大小,而這些屬性就是節(jié)點(diǎn)里面的 attr,在相應(yīng)的事件下根據(jù)鼠標(biāo)移動(dòng)的方向及位置,來調(diào)整節(jié)點(diǎn)的樣式。

如何利用vue3.x繪制流程圖

還有批量操作也是同樣,不過批量操作是要先計(jì)算出哪些節(jié)點(diǎn)的范圍。

// 獲取范圍選中內(nèi)的組件
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繪制流程圖

這個(gè)有個(gè)小功能沒有做,就是在批量調(diào)整大小的時(shí)候,節(jié)點(diǎn)間的相對(duì)距離應(yīng)該是不動(dòng)的,這里忽略了。

節(jié)點(diǎn)移動(dòng)時(shí)的吸附

這里的吸附功能其實(shí)是做了一個(gè)簡(jiǎn)單版的,就是 x 和 y 軸都只有一條校準(zhǔn)線,且校準(zhǔn)的優(yōu)先級(jí)是從左至右,從上至下。

如何利用vue3.x繪制流程圖

這里吸附的標(biāo)準(zhǔn)是節(jié)點(diǎn)的 6 個(gè)點(diǎn):X 軸的左中右,Y 軸的上中下,當(dāng)前節(jié)點(diǎn)在移動(dòng)的時(shí)候,會(huì)用當(dāng)前節(jié)點(diǎn)的 6 個(gè)點(diǎn),一一去與其它節(jié)點(diǎn)的 6 個(gè)點(diǎn)做比較,在誤差正負(fù) 2px 的情況,自動(dòng)更新為0,即自定對(duì)齊。

因?yàn)橐苿?dòng)當(dāng)前節(jié)點(diǎn)時(shí)候,其它的節(jié)點(diǎn)是不動(dòng)的,所以這里是做了一步預(yù)處理,即在鼠標(biāo)按下去的時(shí)候,把其它的節(jié)點(diǎn)的 6 個(gè)點(diǎn)都線算出來,用 Set 結(jié)構(gòu)保存,在移動(dòng)的過程的比較中,計(jì)算量會(huì)相對(duì)較少。

// 計(jì)算其它節(jié)點(diǎn)的所有點(diǎn)位置
export const clearupPostions = (componentList: WF.ComponentType[], currId: string) => {
  // x 坐標(biāo)集合
  const coordx = new Set<number>()
  // y 坐標(biāo)集合
  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]
}

判讀是否有可吸附的點(diǎn)

// 可吸附范圍
const ADSORBRANGE = 2
// 查詢是否有可吸附坐標(biāo)
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 坐標(biāo)
  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 坐標(biāo)
  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 坐標(biāo)
  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 坐標(biāo)
  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 坐標(biāo)
  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 坐標(biāo)
  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
}

最后更新狀態(tài)。

// 獲取修正后的 x, y,還有吸附線的狀態(tài)
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
  }
}

撤銷和恢復(fù)

撤銷和恢復(fù)的功能是比較簡(jiǎn)單的,其實(shí)就是用棧來保存每一次需要保存的配置結(jié)構(gòu),就是要考慮哪些操作是可以撤銷和恢復(fù)的,就是像節(jié)點(diǎn)移動(dòng),節(jié)點(diǎn)的新增和刪除,連接線的連接,連接線的備注新增和編輯等等,在相關(guān)的操作下面入棧即可。

// 撤銷和恢復(fù)操作
const cacheComponentList = ref<WF.ComponentType[][]>([])
const currentComponentIndex = ref(-1)
// 撤銷
const undo = () => {
  componentRenderList.value = JSON.parse(JSON.stringify(cacheComponentList.value[--currentComponentIndex.value]))
  // 更新視圖
  updateCanvas(true)
  cancelSelected()
}
// 恢復(fù)
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繪制流程圖

關(guān)于“如何利用vue3.x繪制流程圖”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí),可以關(guān)注億速云行業(yè)資訊頻道,小編每天都會(huì)為大家更新不同的知識(shí)點(diǎn)。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

vue
AI