带你搞定低代码跨Iframe拖拽辅助线

3,894 阅读6分钟

前言

之前我们用 插件化的方式开发了拖拽通用库,并且基于这个库实现了 拖拽排序,换个思路轻松搞定 的效果和 穿梭的拖拽排序,轻松拿捏 的效果,本文将基于这个库在实现一个目前低代码项目中广泛使用的跨 Iframe 拖拽辅助线插件,效果图先呈上

跨iframe.gif

正篇开始前的一些问题解答

如何跨 Iframe?

一般在低代码项目中,为了布局、逻辑不污染全局,往往需要用到 iframe 元素,至于怎么用 iframe 元素作为低代码画布区域,无非就2种方式,这里我以Vue3 框架为例说明

  1. 使用路由(vue-router) + iframe 的 src 属性 结合

    优点:没有样式丢失的问题

    缺点:重载加载了一次应用,渲染速度可能会变慢

  2. 使用render 函数将画布内容挂载到iframe元素中

    优点:渲染速度跟正常组件一样,数据状态依旧保持

    缺点:一些link的样式以及动态新增的样式会丢失,需要初始化拷贝一次+监听动态新增的样式再次拷贝,监听可以使用 MutationObserve 搞定,此方法在搭配组件库的一些弹出类组件,如(dialog、modal)时会有一些奇怪的问题,具体原因没有深入排查,反正我使用的组件库没问题(😁)

回到正题,我们使用了 iframe 后拖拽事件就获取不到 iframe 内的元素了,也就是说 event.target 获取到的是 iframe 元素,而不是内部的元素,这个我们其实可以给 iframe 元素绑定事件,比如我们拿到 iframe 中的 document,然后给 document 绑定事件即可,类似如下代码

useEventListener(iframeGetter, 'load', () => {
   useEventListener(iframeDocumentGetter, 'mousedown', event => handleMouseDown(event, unref(iframeGetter)!))
   useEventListener(iframeDocumentGetter, 'mousemove', event => handleMouseMove(event, unref(iframeGetter)!))
   useEventListener(iframeDocumentGetter, 'mouseup', event => handleMouseUp(event, unref(iframeGetter)!))
})
  1. iframeGetteriframe 元素
  2. iframeDocumentGetter: iframe中的 document

可以看到,我们是通过监听 iframe 的 load 事件,等到加载完成后再执行拖拽事件的绑定,但是有一些情况是不会触发 load 事件的,比如我们没有指定 src 属性,而是利用 render 函数直接挂载到 iframe 元素中,这种情况我们在开发时也需要特殊处理一下

其实以上针对iframe的处理我是在插件的上下文中统一处理了,下文我们实现插件时不需要去关注这些逻辑,插件只需要监听拖拽过程中的钩子即可

实现辅助线效果需要输出哪些参数

其实看效果我们可以看到,就是一条线,只不过这条线的宽度高度位置在变化而已,宽度高度是根据参考元素得出,位置相对比较复杂一点,需要考虑的一些情况如下

  1. 鼠标位置
  2. 元素布局

针对鼠标位置这个情况,我们可以分为5种方向,分别是上下左右在内部,我们定义一个枚举示例一下

enum DirectionEnum {
  LEFT = 'LEFT',
  RIGHT = 'RIGHT',
  TOP = 'TOP',
  BOTTOM = 'BOTTOM',
  IN = 'IN',
}

而如何计算方向还要根据元素布局情况而定,比如grid布局,我们设定属性gridTemplateColumns只有1列,如下图所示

image.png

比如我们此时鼠标在grid-item--2边缘,我们的辅助线应该出现在上或者下,而不是左或者右,还有其他一些布局、宽度的因素,我们都会在实现插件的过程中考虑进去,当然针对布局我们可以按大类分成2种情况,分别是水平垂直,我们定义一个枚举示例一下

enum LayoutEnum {
  VERTICAL = 'VERTICAL',
  HORIZONTAL = 'HORIZONTAL',
}

好了,说完上面的情况,我们可以得知辅助线要被绘制出来需要以下的参数

  1. 参考元素
  2. 方向
  3. event

我们定义一个枚举示例一下

interface AuxiliaryLineLocation {
  element: HTMLElement
  event: EnhancedMouseEvent
  direction: `${DirectionEnum}`
}

绘制布局 + 引入鼠标跟随插件

这部分代码没有太多营养价值,我给贴上代码后稍微解释一下即可

<script setup lang="tsx">
import { useDragDrop } from '@drag-drop/core'
import { mouseFollowPlugin } from '@drag-drop/plugin-mouse-follow'
import { computed, ref } from 'vue'
import { IframeContainer } from '../../IframeContainer'

const iframeInstRef = ref()

const context = useDragDrop({
  canDraggable: event => !!event.target?.classList.contains('materiel-item'),
  canDropable: event => !!event.target?.classList.contains('node'),
  frames: [
    computed(() => iframeInstRef.value?.$el),
  ],
})

context.use(mouseFollowPlugin({
  text: event => event?.target?.textContent ?? '',
}))
</script>

<template>
  <div class="container">
    <div class="left-panel">
      <div class="materiel-item">
        物料1
      </div>
      <div class="materiel-item">
        物料2
      </div>
      <div class="materiel-item">
        物料3
      </div>
      <div class="materiel-item">
        物料4
      </div>
      <div class="materiel-item">
        物料5
      </div>
    </div>
    <div class="canvas-panel">
      <IframeContainer ref="iframeInstRef" :show-title="false">
        <div :style="{ display: 'flex', gap: '10px', padding: '5px', border: '1px solid #ccc', userSelect: 'none' }" class="node">
          <span :style="{ color: 'red', border: '1px solid #ccc' }" class="node">节点1</span>
          <span :style="{ color: 'pink', border: '1px solid #ccc' }" class="node">节点2</span>
          <span :style="{ color: 'skyblue', border: '1px solid #ccc' }" class="node">节点3</span>
        </div>
      </IframeContainer>
    </div>
  </div>
</template>

<style scoped>
.container{
  display: flex;
  margin: 100px 0 0 0;
}
.left-panel{
  display: flex;
  flex-wrap: wrap;
  width: 300px;
  gap: 10px;
  user-select: none;
}
.materiel-item{
  display: flex;
  align-items: center;
  justify-content: center;
  width: 70px;
  height: 70px;
  border: 1px solid #ccc;
}
</style>

解释一下一些内容

  1. useDragDrop:之前用 插件化的方式开发了拖拽通用库,里面封装了拖拽的一些事件,处理了 iframe 的情况,并向外暴露了一些钩子
  2. canDraggable 参数:指定哪些元素可以被拖拽
  3. canDropable 参数:指定哪些元素可以被放置,比如我们规定了只有类型为 node 的元素才能显示辅助线
  4. frames 参数:需要跨哪些iframe元素,我们这里只有一个iframe
  5. mouseFollowPlugin:根据鼠标坐标对可以被拖拽的元素显示一些内容
  6. IframeContainer 组件:内部将默认插槽的内容通过render函数渲染到一个iframe元素中

好了,上面代码实现后页面效果如下

base.gif

插件实现

我们将这个插件命名为auxiliaryLinePlugin,可以被如下方式调用

const context = useDragDrop(...)
context.use(auxiliaryLinePlugin())

接下来我们先将这个插件的外壳搞定,代码如下

interface AuxiliaryLineLocation {
  element: HTMLElement
  event: EnhancedMouseEvent
  direction: `${DirectionEnum}`
}

interface AuxiliaryLinePluginOptions extends Partial<Omit<DrapDropEventsCallback, 'onDragging'>> {
  thresholdSize?: number
  auxiliaryLineSize?: number
  onRender?: (location: AuxiliaryLineLocation | undefined) => VNodeChild
}

export function auxiliaryLinePlugin(options: AuxiliaryLinePluginOptions = {}) {
  return function ({ context }: DragDropPluginCtx) {
    const {
      onEnd,
      onMove,
      onStart,
      onRender,
      thresholdSize = 8,
      auxiliaryLineSize = 2,
    } = options

    const dropableRef = context.useCanDropable()
    const auxiliaryLineLocationRef = ref<AuxiliaryLineLocation>()

     context.onStart((event) => {
      onStart?.(event)
    })

    context.onMove((event) => {
      if (!unref(dropableRef)) return
      // 我们等下要在这里实现
      onMove?.(event)
    })

    context.onEnd((event) => {
      onEnd?.(event)
      auxiliaryLineLocationRef.value = undefined
    })

    return () => {
      return <div>12312312</div>
    }
  }
}

解释一下

  1. 插件必须是一个函数,在内部会将这个函数包装成vue 的 setup函数,所以可以使用响应式Api渲染函数
  2. auxiliaryLineSize参数:辅助线的大小,默认为2
  3. onRender参数:用户可以自定义渲染内容,不一定非要是一根线
  4. onStart参数:拖拽开始触发的钩子
  5. onMove参数:拖拽移动时触发的钩子
  6. onEnd参数:拖拽结束后触发的钩子
  7. useCanDropable:辅助线是否应该出现,我们之前规定了这个参数必须在类名为 node的元素上才能出现
  8. thresholdSize参数:阈值大小范围,鼠标移入到一定的范围后取的兄弟元素不一样,举个列子,看下图 image.png

如果鼠标点在某一个元素的阈值大小范围外,那么我们应该去找iframe中的直接子节点,也就是父元素1父元素2离鼠标最近的一个元素,然后辅助线应该出现在父元素1或者父元素2上下方向,那如果在阈值大小范围内,如下图

image.png

我们应该计算父元素1中的直接子节点,也就是元素1元素2元素3离鼠标最近的一个元素,然后辅助线应该出现在这3个元素中的某一个方向上

计算方向和参考元素前的准备工作

在计算方向和参考元素前,我们还需要得知以下一些内容

  1. 需要得到是不是在阈值大小范围内
  2. 根据是否在阈值大小范围内得到兄弟元素父元素
  3. 根据兄弟元素父元素判断布局

我们如果知道了布局兄弟元素,我们就可以算出兄弟元素中哪2个元素离鼠标最近,为什么这里是2个元素呢,那是因为鼠标点可能是下面这种情况

image.png

像上图,鼠标可能在2个元素中间位置,我们要算出这2个元素哪个离鼠标更近一点,从而得到参考元素和方向

好了,咱们根据列举的3条内容一个个单独实现

内容1(需要得到是不是在阈值大小范围内):我们可以很简单的实现,代码如下

export function isInThresholdRange(event: EnhancedMouseEvent, thresholdSize: number) {
  const { x, y, target } = event.originEvent // 获取鼠标 x,y 坐标
  const { left, right, top, bottom } = getBoundingClientRect(target) // 获取元素的一些坐标值
  // ---start
  // start 到 end 是 根据阈值范围大小 缩小了元素
  const thresholdTop = top + thresholdSize
  const thresholdLeft = left + thresholdSize
  const thresholdRight = right - thresholdSize
  const thresholdBottom = bottom - thresholdSize
  // --end

  const xIsInThresholdRange = x >= thresholdLeft && x <= thresholdRight
  const yIsInThresholdRange = y >= thresholdTop && y <= thresholdBottom

  // 判断鼠标是否在缩小的元素范围内
  return xIsInThresholdRange && yIsInThresholdRange
}

内容2(根据是否在阈值大小范围内得到兄弟元素父元素):我们也可以很简单的实现,代码如下

function getElementChildrenAndContainer(event:EnhancedMouseEvent, inThresholdRange:boolean) {
    const element = event.originEvent.target as HTMLElement;
    if (inThresholdRange) {
      // 如果在阈值范围内,直接获取当前元素和当前元素下的子节点,当然这里我们过滤掉了文本节点
      return {
        container: element,
        children: Array.from(element.childNodes).filter(isElementNode) as HTMLElement[],
      }
    }

    // 如果不在阈值范围内,获取当前元素的父元素及父元素下的子节点,当然这里我们过滤掉了文本节点
    return {
      container: element.parentElement!,
      children: Array.from(element.parentElement!.childNodes).filter(isElementNode) as HTMLElement[],
    }
},

如果有同学对是否在阈值范围内获取的父元素和兄弟元素不太明白的话,建议看下我上面在插件实现标题下对thresholdSize参数画的图

内容3(根据兄弟元素父元素判断布局)

const inThresholdRange = isInThresholdRange(event,thresholdSize)
const { container, children } = getElementChildrenAndContainer(event,inThresholdRange)
if (children.length <= 0) {
  // 如果没有孩子,说明方向是 IN
    return {
      event,
      element,
      direction: DirectionEnum.IN,
    }
}
// 取出第一个元素,看该元素在父元素中的布局
const firstElement = children[0]
const layout = calculateLayout(firstElement, container)

function calculateLayout(element: HTMLElement, container: HTMLElement) {
  // isFlexLayout方法:判断是否为 flex 布局
  if (isFlexLayout(container)) {
    // isFlexRowDirectionLayout方法:判断 flex-direction === 'row' || flex-direction === 'row-reverse'
    return isFlexRowDirectionLayout(container) ? LayoutEnum.HORIZONTAL : LayoutEnum.VERTICAL
  }
  // isGridLayout方法:判断是否为 grid 布局
  if (isGridLayout(container)) {
    // getGridTemplateColumnsLen方法:看 grid 布局一行有多少列
    const len = getGridTemplateColumnsLen(container)
    return len <= 1 ? LayoutEnum.VERTICAL : LayoutEnum.HORIZONTAL
  }
  // isBlockLayout方法:是否为块级布局,判断 element.display === 'block'
  // isFlexLayout方法:是否为flex布局,判断 element.display === 'flex'
  // isGridLayout方法:是否为grid布局,判断 element.display === 'grid'
  // isTableLayout方法:是否为表格布局,判断 element.display === 'table'
  // widthIsFullContainer方法:子元素宽度是否撑满父元素,下面我会单独写出该方法
  if (
    isBlockLayout(element)
    || isFlexLayout(element)
    || isGridLayout(element)
    || isTableLayout(element)
    || widthIsFullContainer(element, container)
  ) {
    return LayoutEnum.VERTICAL
  }

  return LayoutEnum.HORIZONTAL
}

export function widthIsFullContainer(element: HTMLElement, container: HTMLElement) {
  const elementStyle = window.getComputedStyle(element)
  const containerStyle = window.getComputedStyle(container)
  // pxToNumber方法:将px单位忽略,并将值转换成number类型
  const elementFullWidth = pxToNumber(elementStyle.width)
  + pxToNumber(elementStyle.paddingLeft)
  + pxToNumber(elementStyle.paddingRight)
  + pxToNumber(elementStyle.marginLeft)
  + pxToNumber(elementStyle.marginRight)
  + pxToNumber(elementStyle.borderLeft)
  + pxToNumber(elementStyle.borderRight)

  const containerInnerWidth = pxToNumber(containerStyle.width)
  - pxToNumber(containerStyle.paddingLeft)
  - pxToNumber(containerStyle.paddingRight)
  - pxToNumber(containerStyle.marginLeft)
  - pxToNumber(containerStyle.marginRight)
  - pxToNumber(containerStyle.borderLeft)
  - pxToNumber(containerStyle.borderRight)

  return elementFullWidth >= containerInnerWidth
}

计算方向和参考元素

我们在计算方向和参考元素的准备工作中,知道了一些重要信息,这里我列举一下

  1. 知道了兄弟元素
  2. 知道了布局模式

接下来咱们要做的事情我说明一下

  1. 根据兄弟元素布局模式获取到鼠标的前后2个元素
  2. 根据鼠标的前后2个元素得到最近的参考元素方向

还是一个个实现

内容1(根据兄弟元素布局模式获取到鼠标的前后2个元素)

// 获取鼠标后面的一个元素
function getMouseAfterElement(children: HTMLElement[], event: EnhancedMouseEvent, layout: `${LayoutEnum}`){
  function isMouseAfter(element: HTMLElement) {
    const { x, y } = event.originEvent
    const { left, top } = getBoundingClientRect(element)
    if (layout === LayoutEnum.HORIZONTAL) {
      return x <= left
    }
    return y <= top
  }

  for (let i = 0; i < children.length; i++) {
    const element = children[i]
    if (isMouseAfter(element)) {
      return element
    }
  }
}

// 获取鼠标前面的一个元素
function getMouseBeforeElement(children: HTMLElement[], event: EnhancedMouseEvent, layout: `${LayoutEnum}`){
  function isMouseBefore(element: HTMLElement) {
    const { x, y } = event.originEvent
    const { left, top } = getBoundingClientRect(element)
    if (layout === LayoutEnum.HORIZONTAL) {
      // 水平方向
      return x > left
    }
    // 垂直方向
    return y > top
  }

  for (let i = children.length - 1; i >= 0; i--) {
    const element = children[i]
    if (isMouseBefore(element)) {
      return element
    }
  }
}

const inThresholdRange = isInThresholdRange(event, thresholdSize)
const { container, children } = getElementChildrenAndContainer(event, inThresholdRange)

if (children.length <= 0) {
  return {
    event,
    element,
    direction: DirectionEnum.IN,
  }
}
const firstElement = children[0]
const layout = calculateLayout(firstElement, container)
let afterMouseElement = getMouseAfterElement(children, event, layout)
let beforeMouseElement = getMouseBeforeElement(children, event, layout)

if (!afterMouseElement) afterMouseElement = beforeMouseElement
if (!beforeMouseElement) beforeMouseElement = afterMouseElement

内容2(根据鼠标的前后2个元素得到最近的参考元素方向)

function calculateXLocation(beforeElement: HTMLElement, afterElement: HTMLElement, event: EnhancedMouseEvent): AuxiliaryLineLocation {
  const { x } = event.originEvent
  const { left: afterElementLeft, right: afterElementRight, width: afterElementWidth } = getBoundingClientRect(afterElement)
  const { left: beforeElementLeft, right: beforeElementRight, width: beforeElementWidth } = getBoundingClientRect(beforeElement)
  const xIsInAfterElementRange = x >= afterElementLeft && x <= afterElementRight
  const xIsInBeforeElementRange = x >= beforeElementLeft && x <= beforeElementRight

  if (xIsInAfterElementRange) {
    const isInLeftRange = x - (afterElementLeft + afterElementWidth / 2) < 0
    return {
      event,
      element: afterElement,
      direction: isInLeftRange ? DirectionEnum.LEFT : DirectionEnum.RIGHT,
    }
  }

  if (xIsInBeforeElementRange) {
    const isInLeftRange = x - (beforeElementLeft + beforeElementWidth / 2) < 0
    return {
      event,
      element: beforeElement,
      direction: isInLeftRange ? DirectionEnum.LEFT : DirectionEnum.RIGHT,
    }
  }

  // 鼠标在二个元素中间

  // 是否偏向beforeElement
  const isDeviationBeforeElement = Math.abs(x - beforeElementRight) - Math.abs(x - afterElementLeft) < 0
  return {
    event,
    element: isDeviationBeforeElement ? beforeElement : afterElement,
    direction: isDeviationBeforeElement ? DirectionEnum.RIGHT : DirectionEnum.LEFT,
  }
}

function calculateYLocation(beforeElement: HTMLElement, afterElement: HTMLElement, event: EnhancedMouseEvent): AuxiliaryLineLocation {
  const { y } = event.originEvent
  const { top: afterElementTop, bottom: afterElementBottom, height: afterElementHeight } = getBoundingClientRect(afterElement)
  const { top: beforeElementTop, bottom: beforeElementBottom, height: beforeElementHeight } = getBoundingClientRect(beforeElement)
  const yIsInAfterElementRange = y >= afterElementTop && y <= afterElementBottom // 鼠标在后面元素范围内
  const yIsInBeforeElementRange = y >= beforeElementTop && y <= beforeElementBottom // 鼠标在前面元素范围

  if (yIsInAfterElementRange) {
    const isInTopRange = y - (afterElementTop + afterElementHeight / 2) < 0
    return {
      event,
      element: afterElement,
      direction: isInTopRange ? DirectionEnum.TOP : DirectionEnum.BOTTOM,
    }
  }

  if (yIsInBeforeElementRange) {
    const isInToptRange = y - (beforeElementTop + beforeElementHeight / 2) < 0
    return {
      event,
      element: beforeElement,
      direction: isInToptRange ? DirectionEnum.TOP : DirectionEnum.BOTTOM,
    }
  }

  // 鼠标在二个元素中间

  // 是否偏向beforeElement
  const isDeviationBeforeElement = Math.abs(y - beforeElementBottom) - Math.abs(y - afterElementTop) < 0
  return {
    event,
    element: isDeviationBeforeElement ? beforeElement : afterElement,
    direction: isDeviationBeforeElement ? DirectionEnum.BOTTOM : DirectionEnum.TOP,
  }
}

...
let afterMouseElement = getMouseAfterElement(children, event, layout)
let beforeMouseElement = getMouseBeforeElement(children, event, layout)

if (!afterMouseElement) afterMouseElement = beforeMouseElement
if (!beforeMouseElement) beforeMouseElement = afterMouseElement

return layout === LayoutEnum.HORIZONTAL
  ? calculateXLocation(beforeMouseElement!, afterMouseElement!, event)
  : calculateYLocation(beforeMouseElement!, afterMouseElement!, event)

这里我要说明一下calculateXLocation方法(calculateYLocation 方法是一样的思路),实际上只做了3件事情

  1. 鼠标是不是在鼠标前一个元素范围内,如果在的话,看鼠标在元素的左侧还是右侧,返回方向和参考元素
  2. 鼠标是不是在鼠标后一个元素范围内,如果在的话,看鼠标在元素的左侧还是右侧,返回方向和参考元素
  3. 如果鼠标不在前后2个元素范围内,那就看鼠标离哪个元素更近,返回方向和参考元素

绘制辅助线

上面我们计算好了计算方向和参考元素,我们归纳一下代码

export function auxiliaryLinePlugin(options: AuxiliaryLinePluginOptions = {}) {
  return function ({ context }: DragDropPluginCtx) {
    const {
      onEnd,
      onMove,
      onStart,
      onRender,
      thresholdSize = 8,
      auxiliaryLineSize = 2,
    } = options

    const dropableRef = context.useCanDropable()
    const auxiliaryLineLocationRef = ref<AuxiliaryLineLocation>()

    function calculateAuxiliaryLineLocation(event: EnhancedMouseEvent): AuxiliaryLineLocation {
      const element = getTargetElementByEvent(event)
      const inThresholdRange = isInThresholdRange(element, event, thresholdSize)
      const { container, children } = getElementChildrenAndContainer(element, inThresholdRange)

      if (children.length <= 0) {
        return {
          event,
          element,
          direction: DirectionEnum.IN,
        }
      }
      const firstElement = children[0]
      const layout = calculateLayout(firstElement, container)
      let afterMouseElement = getMouseAfterElement(children, event, layout)
      let beforeMouseElement = getMouseBeforeElement(children, event, layout)

      if (!afterMouseElement) afterMouseElement = beforeMouseElement
      if (!beforeMouseElement) beforeMouseElement = afterMouseElement

      return layout === LayoutEnum.HORIZONTAL
        ? calculateXLocation(beforeMouseElement!, afterMouseElement!, event)
        : calculateYLocation(beforeMouseElement!, afterMouseElement!, event)
    }

    context.onStart((event) => {
      onStart?.(event)
    })

    context.onMove((event) => {
      if (!unref(dropableRef)) return
      const auxiliaryLineLocation = calculateAuxiliaryLineLocation(event)
      auxiliaryLineLocationRef.value = auxiliaryLineLocation
      onMove?.(event)
    })

    context.onEnd((event) => {
      onEnd?.(event)
      auxiliaryLineLocationRef.value = undefined
    })

    return () => {
      return <div>1123123</div>
    }
  }
}

方向和参考元素我们都有了,根据这些参数我们可以很简单的绘制出来我们的辅助线,代码如下

export function auxiliaryLinePlugin(options: AuxiliaryLinePluginOptions = {}) {
  return function ({ context }: DragDropPluginCtx) {
    const auxiliaryLineLocationRef = ref<AuxiliaryLineLocation>()
    ...

    const styleGetter = computed(() => {
      const location = unref(auxiliaryLineLocationRef)
      const basicStyle: CSSProperties = {
        position: 'fixed',
        background: '#0071e7',
        pointerEvents: 'none',
        zIndex: 'auto',
      }

      if (location && location.direction !== DirectionEnum.IN) {
        const { direction, element, event } = location
        let { left, top, right, bottom, width, height } = getBoundingClientRect(element)

        if (event.inIframe) {
          const { left: offsetLeft, top: offsetTop } = getBoundingClientRect(event.iframe)
          left += offsetLeft
          right += offsetLeft
          top += offsetTop
          bottom += offsetTop
        }

        const rectStyle = {
          [DirectionEnum.LEFT]: { width: numberToPx(auxiliaryLineSize) },
          [DirectionEnum.TOP]: { height: numberToPx(auxiliaryLineSize) },
          [DirectionEnum.RIGHT]: { width: numberToPx(auxiliaryLineSize), left: numberToPx(right) },
          [DirectionEnum.BOTTOM]: { height: numberToPx(auxiliaryLineSize), top: numberToPx(bottom) },
        }[direction]

        const r = {
          ...{
            left: numberToPx(left),
            top: numberToPx(top),
            width: numberToPx(width),
            height: numberToPx(height),
            ...rectStyle,
          },
          ...basicStyle,
        }
        return r
      }

      return basicStyle
    })

    return () => {
      const style = unref(styleGetter)
      const location = unref(auxiliaryLineLocationRef)

      if(onRender){
        // 用户自定义渲染
        return onRender(location,style)
      }

      if (!location || location.direction === DirectionEnum.IN){
         return null
      }
      
      return <div style={style}></div>
    }
  }
}

结语

  1. 完整代码在我的仓库, 仓库地址
  2. 实现拖拽插件化架构的文章,掘金文章地址
  3. 基础版本的拖拽排序文章,掘金文章地址
  4. 穿梭的拖拽排序文章,掘金文章地址

如有错误之处,请指正,谢谢大家~,实际代码还做了一些扩展性的参数,本文为了讲解内容就没有说明这些参数了