FLIP动画 拖拽排序

100 阅读1分钟

技术栈:Vue3,核心API:nextTick、dragstart事件、dragover事件

<script setup lang="ts">
import { ref, reactive, nextTick, onMounted } from 'vue';
import { createChildElementRectMap } from '../../utils/flip';
// import { throttle } from '../../utils/debounce';
const state = reactive({
  list: [1, 2],
});
const ul = ref<HTMLElement | null>(null);
const first = ref<HTMLElement | null>(null);
const next = ref<HTMLElement | null>(null);
onMounted(() => {
  for (let i = 1; i <= 100; i++) {
    state.list.push(state.list.length + 1);
  }
});
// 加法
function increment(total: number, sort: boolean = false): void {
  const arr = state.list;
  for (let i = 1; i <= total; i++) {
    arr.unshift(state.list.length + 1);
    if (sort) {
      arr.sort(() => 0.5 - Math.random());
    }
  }
  state.list = arr;
  const res = createChildElementRectMap(ul.value);
  invert(res);
}
// 减法
function decrease(total: number, sort: boolean = false): void {
  const arr = state.list;
  for (let i = 1; i <= total; i++) {
    arr.shift();
    if (sort) {
      arr.sort(() => 0.5 - Math.random());
    }
  }
  state.list = arr;
  const res = createChildElementRectMap(ul.value);
  invert(res);
}
// 乱序
function sort(): void {
  state.list = state.list.sort(() => 0.5 - Math.random());
  const res = createChildElementRectMap(ul.value);
  invert(res);
}
// 平移执行动画
function invert(res: any, delay: number = 400): void {
  nextTick(() => {
    const res2 = createChildElementRectMap(ul.value);
    res2.forEach((item, node) => {
      const current = res.get(node);
      if (current) {
        const invert = {
          left: current.left - item.left,
          top: current.top - item.top,
        };
        const keyframes = [
          {
            transform: `translate(${invert.left}px, ${invert.top}px)`,
          },
          { transform: 'translate(0, 0)' },
        ];
        node?.animate(keyframes, {
          duration: delay,
          easing: 'cubic-bezier(0.25, 0.8, 0.25, 1)',
        });
      }
    });
  });
}
function dragstart(event: DragEvent): void {
  const el = event.target as HTMLElement;
  first.value = el;
}
// const dragover = throttle((event: MouseEvent) => {
//   event.preventDefault();
//   const el = event.target as HTMLElement;
//   if (el.nodeName == 'UL') return;
//   // const firstRect = first.value?.getBoundingClientRect();
//   const firstText = first.value?.innerText;
//   if (firstText == el.innerText) return;
//   exchange(Number(firstText), Number(el.innerText), el);
//   console.log(firstText, el.innerText);
// }, 100);
function dragover(event: DragEvent): void {
  event.preventDefault();
  const el = event.target as HTMLElement;
  if (el.nodeName == 'UL') return;
  const firstText = first.value?.innerText;
  const nextText = next.value?.innerText;
  if (firstText == el.innerText || nextText == el.innerText) return;

  console.log(first.value, el);
  exchange(Number(firstText), Number(el.innerText), el);
}
function exchange(first1: number, last: number, el: HTMLElement) {
  for (let i = 0; i < state.list.length; i++) {
    if (state.list[i] == first1) {
      state.list[i] = last;
    } else if (state.list[i] == last) {
      state.list[i] = first1;
    }
  }
  next.value = el;
  const res = createChildElementRectMap(ul.value);
  invert(res);
}
</script>

<template>
  <div class="test">
    <el-scrollbar>
      <el-button type="primary" plain @click="increment(50)">加</el-button>
      <el-button type="primary" plain @click="decrease(25)">减</el-button>

      <el-button type="primary" plain @click="increment(25, true)"
        >乱加</el-button
      >
      <el-button type="primary" plain @click="decrease(15, true)"
        >乱减</el-button
      >
      <el-button plain type="primary" @click="sort">打乱</el-button>
      <ul ref="ul" @dragstart="dragstart" @dragover="dragover">
        <li draggable="true" v-for="item in state.list" :key="item" ref="li">
          {{ item }}
        </li>
      </ul>
    </el-scrollbar>
  </div>
</template>

<style lang="scss" scoped>
.test {
  height: 100%;
  box-sizing: border-box;
  padding: 50px;
  background-color: rgb(189, 201, 237);
  ul {
    // display: flex;
    flex-wrap: wrap;
    list-style: none;
    li {
      // width: 50px;
      // height: 50px;
      text-align: center;
      line-height: 50px;
      background-color: chocolate;
      border-radius: 4px;
      margin-right: 10px;
      margin-bottom: 10px;
      box-shadow: 5px 10px 10px gray;
      color: white;
    }
  }
}
</style>



//flip.ts


export function createChildElementRectMap(node: HTMLElement | null): Map<HTMLElement | null | undefined, any> {
  if (!node) return new Map()
  const elements = Array.prototype.slice.call(node.children)
  return new Map(elements.map(item => [item, item.getBoundingClientRect()]))
}