vue实现拖拽如此简单

1,480 阅读2分钟

列表拖拽超简单实现

微信截图_20240406153410.png

常见数组列表拖拽

先看一下例子和代码实现,下面再做讲解:

是不是就实现了vue-draggable这个库的效果了;其核心就是vue中自带的<transition-group>过渡组件,加上dragover事件,最后处理队列数据项的位置就搞定。

  • 将上面的例子代码封装成钩子函数方便复用
interface ListDragOption<T> {
  /** 返回列表函数 */
  list(): Array<T>;
  /**
   * 触发更新列表函数
   * @param newList 更新后的新数组
   */
  update(newList: Array<T>): void;
  /**
   * 向上查找节点的最大层数,默认`3`
   * - 目的是为了找到`:data-key`的值
   */
  findLevel?: number;
  /**
   * 默认`"key"`,则元素绑定`<element data-key="xxx">`
   * - 当有多种拖拽列表处于同一场景时,设置该值作为区分用
   */
  dataKey?: string;
}

/**
 * 列表拖拽
 * - `item`节点一定要绑定`<element :data-key="唯一值">`
 * @param option 
 */
export function useListDrag<T>(option: ListDragOption<T>) {
  const maxLevel = option.findLevel || 3;
  const dataKey = option.dataKey || "key";
  const current = {
    index: -1
  }

  const target = {
    key: ""
  }

  function findDataKey(el: HTMLElement, level = 1) {
    const key = el.dataset[dataKey];
    if (key) return key;
    if (level < maxLevel && el.parentElement) {
      return findDataKey(el.parentElement, level + 1);
    }
    // console.warn(`找不到<element :data-${dataKey}="xxx">绑定值,请检查是否在元素中设置绑定值或调整 findLevel`);
    return undefined;
  }

  function onDragStart(index: number) {
    current.index = index;
  }

  function onDropEnd() {
    current.index = -1;
  }

  function onDragMove(event: DragEvent, targetIndex: number) {
    event.preventDefault();
    if (current.index < 0) return;
    const targetKey = findDataKey(event.target as HTMLElement);
    if (!targetKey || targetKey === target.key) return;
    target.key = targetKey;
    // 记录原始数据字符串,下面做对比用
    const str = JSON.stringify(option.list());
    // 拷贝响应数据
    const ls: Array<T> = JSON.parse(str);
    // 交替数组位置
    [ls[current.index], ls[targetIndex]] = [ls[targetIndex], ls[current.index]];
    // 上一次修改如果和当前数组一致则不重新赋值
    if (str === JSON.stringify(ls)) return;
    // 最终赋值给响应数据
    option.update(ls);
    // 更新当前索引,必须!!!
    current.index = targetIndex;
  }
  return {
    onDragStart,
    onDragMove,
    onDropEnd
  }
}
  • 使用方式

<template>
  <transition-group name="the-group" tag="div">
    <div
      v-for="(item, itemIndex) in state.list"
      :key="item.id"
      class="item"
      :data-key="item.id"
      :draggable="true"
      @dragstart="onDragStart(itemIndex)"
      @dragover="onDragMove($event, itemIndex)"
      @drop="onDropEnd()"
    >
      {{ item.label }}
    </div>
  </transition-group>
</template>
<script lang="ts" setup>
import { reactive } from "vue";
import { useListDrag } from "@/hooks/common";

const state = reactive({
  list: Array.from({ length: 10 }).map((_, index) => ({ label: `item-${index}`, id: index }))
});

const { onDragStart, onDragMove, onDropEnd } = useListDrag({
  list: () => state.list,
  update(newList) {
    state.list = newList;
  },
  // findLevel: 2 // 可选,根据布局嵌套层级进行设置
});
</script>
<style lang="scss">
.item {
  width: 100%;
  margin-bottom: 10px;
  border: solid #ccc 1px;
  border-radius: 2px;
  font-size: 14px;
  line-height: 26px;
  padding: 0 12px;
}

.the-group-move,
.the-group-enter-active,
.the-group-leave-active {
  transition: .3s all;
}

.the-group-enter-from,
.the-group-leave-to {
  opacity: 0;
  transform: translate3d(0, 30px, 0);
}

.the-group-leave-active {
  position: absolute;
}
</style>

上面的处理方式还存在一些瑕疵,当列表数据包含了函数或者其他js类型时,就会导致重新设置的数据丢失,但是用深克隆的方式代替JSON.stringify又会产生另一个问题:当数组项内有大量数据时,深克隆会产生性能问题;所以在优化实现的设计中,将不再替换数组进行赋值处理,而是记录改变之后的队列位置,最后将原数组进行排序即可,这样确保性能最优,同时无需关心克隆问题。

优化之后的封装实现:

interface ListDragOption<T> {
  /** 返回列表函数 */
  list(): Array<T>;
  /** 数组项唯一值 */
  key: keyof T;
  /**
   * 向上查找节点的最大层数,默认`3`
   * - 目的是为了找到`:data-key`的值
   */
  findLevel?: number;
  /**
   * 默认`"key"`,则元素绑定`<element data-key="xxx">`
   * - 当有多种拖拽列表处于同一场景时,设置该值作为区分用
   */
  dataKey?: string;
}

/**
 * [列表拖拽](https://juejin.cn/post/7354039500811845670)
 * - `item`节点一定要绑定`<element :data-key="唯一值">`
 * @param option 
 */
export function useListDrag<T extends object>(option: ListDragOption<T>) {
  const maxLevel = option.findLevel || 3;
  const dataKey = option.dataKey || "key";
  const current = {
    index: -1
  }

  const target = {
    key: ""
  }

  function findDataKey(el: HTMLElement, level = 1) {
    const key = el.dataset[dataKey];
    if (key) return key;
    if (level < maxLevel && el.parentElement) {
      return findDataKey(el.parentElement, level + 1);
    }
    // console.warn(`找不到<element :data-${dataKey}="xxx">绑定值,请检查是否在元素中设置绑定值或调整 findLevel`);
    return undefined;
  }

  /**
   * 获取排序对比对象
   * @param list 
   */
  function getSortMap(list: Array<T[keyof T]>) {
    const indexMap: Record<string, number> = {};
    list.forEach((item, index) => (indexMap[item as string] = index));
    return indexMap;
  } 

  function onDragStart(index: number) {
    current.index = index;
  }

  function onDropEnd() {
    current.index = -1;
  }

  function onDragMove(event: DragEvent, targetIndex: number) {
    event.preventDefault();
    if (current.index < 0) return;
    const targetKey = findDataKey(event.target as HTMLElement);
    if (!targetKey || targetKey === target.key) return;
    target.key = targetKey;
    const optionList = option.list();
    const before = optionList.map(item => item[option.key]);
    // 记录原始数据字符串,下面做对比用
    const beforeStr = JSON.stringify(before);
    // 拷贝原来数组
    const next: typeof before = JSON.parse(beforeStr);
    // 交替数组位置
    [next[current.index], next[targetIndex]] = [next[targetIndex], next[current.index]];
    // 上一次修改如果和当前数组一致则不作处理
    if (beforeStr === JSON.stringify(next)) return;
    // 最后设置排序
    const indexMap = getSortMap(next);
    optionList.sort((a, b) => {
      const key = option.key;
      const valueA = indexMap[a[key] as string];
      const valueB = indexMap[b[key] as string];
      return valueA - valueB;
    });
    // 更新当前索引,必须!!!
    current.index = targetIndex;
  }

  return {
    onDragStart,
    onDragMove,
    onDropEnd
  }
}

这样在使用时就不需要通过原先的update函数去重新设置数组了

import { useListDrag } from "@/hooks/common";

const state = reactive({
  list: Array.from({ length: 10 }).map((_, index) => ({ label: `item-${index}`, id: index }))
});

const { onDragStart, onDragMove, onDropEnd } = useListDrag({
  list: () => state.list,
  key: "id"
 });

跨数组列表拖拽

你可能觉得上面这种太简单,使用场景偏窄,那再看看下面的这个

是不是就是常见的夸容器拖拽了,核心还是在绑定的节点dragover事件中去处理,当有多个容器时,在对应的dragover事件中做相应的判断处理即可,简单到不得了!!!