您当前的位置:首页 > 电脑百科 > 程序开发 > 前端

基于 vue3.x 的流程图绘制

时间:2022-06-05 13:32:21  来源:  作者:李家梦想家
基于 vue3.x 的流程图绘制

 

Github-workflow

这里主要是针对于工作流场景的流程图绘制及实现方式。下面是效果图:

基于 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',
    // 线的类型(直线、折线、曲线)
    'l.NEType': '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 lang="ts" 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

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

// 切换的当前连接线
const getActiveLine = (line: Record<string, any>) => {
  console.log('active line', 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 || 'add')
  })
}

const init = () => {
  if (!processId) {
    ElMessageBox.alert('当前没有流程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>
复制代码

节点的拖拽与渲染及连接线的绘制

关于节点的拖拽就不多说了,就是 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()
}
复制代码
  • 曲线

曲线相对于折线来说,思路会简单很多,不需要考虑折线这么多场景。

基于 vue3.x 的流程图绘制

 

这里的折线是用三阶的贝塞尔曲线来绘制的,固定的取四个点,两个起止点,两个控制点,其中两个起止点是固定的,我们只需要求出两个控制点的坐标即可。这里代码不多,可以直接贴出来:

/**
 * Description: 计算三阶贝塞尔曲线的坐标
 */
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
  }
}
复制代码

简单一点来说,第一个控制点是根据起始点来算的,第二个控制点是跟根据结束点来算的。算的方式是根据当前点相对于节点的方向,继续往前算一段距离,而这段距离是根据起止两个点的最大相对距离的一半(可能有点绕...)。

绘制方法:

// 绘制贝塞尔曲线
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'    //目标图像上显示源图像
}
复制代码

节点与连接线的选择

节点是用 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 !== '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 求出对应的 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,在相应的事件下根据鼠标移动的方向及位置,来调整节点的样式。

基于 vue3.x 的流程图绘制

 

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

// 获取范围选中内的组件
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 的流程图绘制

 

最后

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

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



Tags:vue3   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
SpringBoot3+Vue3 开发高并发秒杀抢购系统
开发高并发秒杀抢购系统:使用SpringBoot3+Vue3的实践之旅随着互联网技术的发展,电商行业对秒杀抢购系统的需求越来越高。为了满足这种高并发、高流量的场景,我们决定使用Spring...【详细内容】
2024-01-14  Search: vue3  点击:(94)  评论:(0)  加入收藏
vue3中 ref和 reactive的区别 ?
最近有朋友在面试过程中经常被问到这么一个问题,vue3 中的ref 和 reactive的区别在哪里,为什么 要定义两个API 一个 api不能实现 响应式更新吗??带着这个疑问 ,我们 接下来进行逐...【详细内容】
2024-01-03  Search: vue3  点击:(47)  评论:(0)  加入收藏
React18 与 Vue3 全方面对比
1. 编程风格 & 视图风格1.1 编程风格 React 语法少、难度大;Vue 语法多,难度小例如指令:Vue<input v-model="username"/><ul> <li v-for="(item,index) in list" :key="inde...【详细内容】
2024-01-03  Search: vue3  点击:(78)  评论:(0)  加入收藏
Vue3 学习笔记,如何使用 Watch 监听数据变化
大家好,本篇文章我们继续学习和 Vue 相关的内容,今天我们归纳总结下如何使用 watch 监听组件中的数据变化,以及 computed 和 watch 的区别。什么是 watch,以及如何使用?watch 是...【详细内容】
2023-12-14  Search: vue3  点击:(178)  评论:(0)  加入收藏
Vue3 学习笔记,如何理解 Computed 计算属性
大家好,本篇文章我们继续学习和 Vue 相关的内容,今天我们归纳总结下什么是 computed 计算属性、如何使用和应用场景,以及 computed 和 Method 事件的区别和应用场景。什么是 co...【详细内容】
2023-12-11  Search: vue3  点击:(212)  评论:(0)  加入收藏
Vue3 学习笔记,如何定义事件以及如何理解响应式
如何定义事件在 Vue 中,可以使用 v-on 指令来绑定事件监听器。下面是一个示例,在点击按钮时触发事件处理程序:<template> <div> <button v-on:click="incrementCounter">{{...【详细内容】
2023-12-06  Search: vue3  点击:(142)  评论:(0)  加入收藏
最全Vue3开源管理系统汇总
搭建一个后台管理,从零开始开发,其实并不容易,在众多开源管理后台中,Vue3是一个备受瞩目的选择。它是一个现代化的前端框架,具有高效、灵活、易用等特点,今天,要为大家介绍几款开源...【详细内容】
2023-11-24  Search: vue3  点击:(219)  评论:(0)  加入收藏
Vue3问题:如何实现El-table内容超出省略提示?第三条很少有人会
一、需求分析,问题描述1、需求一个表格,分表头、表体、表尾三部分。当每个单元格的内容过长超出时,需要省略,用省略号代替超出的部分。同时,当鼠标移入上去时,会在上方弹出一个小...【详细内容】
2023-11-23  Search: vue3  点击:(98)  评论:(0)  加入收藏
Spring Boot + Vue3 前后端分离 实战wiki知识库系统
下栽の地止:https://www.itwangzi.cn/2508.html Spring Boot + Vue3 前后端分离 实战wiki知识库系统在当今的Web应用开发中,前后端分离已经成为了一种主流的开发模式。Spring...【详细内容】
2023-11-18  Search: vue3  点击:(147)  评论:(0)  加入收藏
深入详解Vue3中的Mock数据模拟
Vue3 前端开发时,需要api请求模拟和数据模拟,我将在接下来的时间写Mock模拟和json-server模拟。两种比较流行的模式,现在先介绍mock模式。使用NPM安装Mock.js库Mock.js是一个轻...【详细内容】
2023-11-07  Search: vue3  点击:(158)  评论:(0)  加入收藏
▌简易百科推荐
20k级别前端是怎么使用LocalStorage的,想知道吗?
当咱们把咱们想缓存的东西,存在localStorage、sessionStorage中,在开发过程中,确实有利于咱们的开发,咱们想看的时候也是一目了然,点击Application就可以看到。前言大家好,我是林...【详细内容】
2024-03-26  前端之神  微信公众号  Tags:前端   点击:(14)  评论:(0)  加入收藏
前端不存在了?盲测64%的人更喜欢GPT-4V的设计,杨笛一等团队新作
3 月 9 日央视的一档节目上,百度创始人、董事长兼 CEO 李彦宏指出,以后不会存在「程序员」这种职业了,因为只要会说话,人人都会具备程序员的能力。「未来的编程语言只会剩下两种...【详细内容】
2024-03-11  机器之心Pro    Tags:前端   点击:(17)  评论:(0)  加入收藏
前端开始“锈化”?Vue团队开源JS打包工具:基于Rust、速度极快、尤雨溪主导
Vue 团队已正式开源Rolldown &mdash;&mdash; 基于 Rust 的 JavaScrip 打包工具。Rolldown 是使用 Rust 开发的 Rollup 替代品,它提供与 Rollup 兼容的应用程序接口和插件接口...【详细内容】
2024-03-09  OSC开源社区    Tags:Vue   点击:(23)  评论:(0)  加入收藏
两年前端经验还不会手写Promise?
什么是promise?当我们处理异步操作时,我们经常需要进行一系列的操作,如请求数据、处理数据、渲染UI等。在过去,这些操作通常通过回调函数来处理,但是回调函数嵌套过多会导致代码...【详细内容】
2024-03-07  海燕技术栈  微信公众号  Tags:Promise   点击:(31)  评论:(0)  加入收藏
网站开发中的前端和后端开发有什么区别
前端开发和后端开发都是干什么的?有哪些区别?通俗地讲,前端干的工作是用户可以直接看得见的,而后端开发的工作主要在服务端,用户不太能直接看到。虽然前端开发和后端开发的工作有...【详细内容】
2024-02-21  CarryData    Tags:前端   点击:(37)  评论:(0)  加入收藏
网站程序开发中的前后端分离技术
随着互联网的快速发展和技术的不断创新,传统的网站开发模式已经难以满足日益增长的业务需求。为了提高开发效率、增强系统的可维护性和可扩展性,前后端分离技术逐渐成为了网站...【详细内容】
2024-01-31  网站建设派迪星航    Tags:前后端分离   点击:(27)  评论:(0)  加入收藏
如何优雅的实现前端国际化?
JavaScript 中每个常见问题都有许多成熟的解决方案。当然,国际化 (i18n) 也不例外,有很多成熟的 JavaScript i18n 库可供选择,下面就来分享一些热门的前端国际化库!i18nexti18ne...【详细内容】
2024-01-17  前端充电宝  微信公众号  Tags:前端   点击:(78)  评论:(0)  加入收藏
Vue中Scope是怎么做样式隔离的?
scope样式隔离在 Vue 中,样式隔离是通过 scoped 特性实现的。当在一个组件的 <style> 标签上添加 scoped 特性时,Vue 会自动为这个样式块中的所有选择器添加一个唯一的属性,以...【详细内容】
2024-01-04  海燕技术栈  微信公众号  Tags:Vue   点击:(85)  评论:(0)  加入收藏
vue3中 ref和 reactive的区别 ?
最近有朋友在面试过程中经常被问到这么一个问题,vue3 中的ref 和 reactive的区别在哪里,为什么 要定义两个API 一个 api不能实现 响应式更新吗??带着这个疑问 ,我们 接下来进行逐...【详细内容】
2024-01-03  互联网高级架构师  今日头条  Tags:vue3   点击:(47)  评论:(0)  加入收藏
React18 与 Vue3 全方面对比
1. 编程风格 & 视图风格1.1 编程风格 React 语法少、难度大;Vue 语法多,难度小例如指令:Vue<input v-model="username"/><ul> <li v-for="(item,index) in list" :key="inde...【详细内容】
2024-01-03  爱做梦的程序员  今日头条  Tags:Vue3   点击:(78)  评论:(0)  加入收藏
站内最新
站内热门
站内头条