动效列表

大约 3 分钟

效果

源码
<template>
  <h2>待办事项</h2>
  <input v-model="inputValue" placeholder="试试添加事项" /><button
    class="btn-add"
    @click="add"
  >
    添加
  </button>
  <div class="list" ref="listRef">
    <div
      class="item"
      v-for="(item, index) in list"
      :key="item.id"
      :class="{
        leave: delIndex == index,
        up: delIndex != -1 && delIndex < index,
        down: topIndex > index,
      }"
    >
      <div class="left">{{ item.content }}</div>
      <div class="right">
        <button @click="top(index)">置顶</button>
        <button @click="del(index)">删除</button>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data: () => ({
    list: [
      {
        id: "1",
        content: "事项一",
      },
      {
        id: "2",
        content: "事项二",
      },
      {
        id: "3",
        content: "事项三",
      },
    ],
    animationTime: 500,
    delIndex: -1,
    topIndex: -1,
    inputValue: "",
  }),
  computed: {
    // 是否正在操作节点
    isHandle() {
      return this.delIndex == -1 && this.topIndex == -1 ? false : true;
    },
    // 当前执行操作节点
    nowHandleNode() {
      if (this.isHandle) {
        if (this.delIndex !== -1) {
          return this.$refs.listRef.children[this.delIndex];
        } else if (this.topIndex !== -1) {
          return this.$refs.listRef.children[this.topIndex];
        }
      } else {
        return {};
      }
    },
  },
  methods: {
    del(index) {
      // 当前已有元素在执行操作,退出
      if (this.isHandle) return;
      this.delIndex = index;
      setTimeout(() => {
        this.list.splice(index, 1);
        this.delIndex = -1;
      }, this.animationTime);
    },
    top(index) {
      // 当前已有元素在执行操作,退出
      if (this.isHandle) return;
      this.topIndex = index;
      this.nowHandleNode.style.transition = "all 0.5s";
      this.nowHandleNode.style.transform = `translateY(-${30 * index}px)`;
      setTimeout(() => {
        this.nowHandleNode.style.transition = "";
        this.nowHandleNode.style.transform = "";
        const item = this.list[index];
        this.list.splice(index, 1);
        this.list.unshift(item);
        this.topIndex = -1;
      }, this.animationTime);
    },
    add() {
      if (!this.inputValue.trim()) return;
      this.list.push({
        id: (this.list.length + 1).toString(),
        content: this.inputValue.trim(),
      });
      this.inputValue = "";

      this.$nextTick(() => {
        this.$refs.listRef.children[this.list.length - 1].classList.add("from");
        setTimeout(() => {
          this.$refs.listRef.children[this.list.length - 1].classList.remove(
            "from",
          );
        }, this.animationTime);
      });
    },
  },
};
</script>

<style>
.list {
  display: flex;
  flex-direction: column;
}
.list .item {
  width: 200px;
  height: 30px;
  display: flex;
  justify-content: space-between;
}
.btn-add {
  margin-left: 10px;
  margin-bottom: 10px;
}
.up {
  transition: all 0.5s;
  transform: translateY(-30px);
}
.leave {
  transition: all 0.5s;
  transform: translateX(200px);
  opacity: 0;
}
.down {
  transition: all 0.5s;
  transform: translateY(30px);
}
.from {
  animation: 0.5s linear 1 from;
}
@keyframes from {
  0% {
    transform: translateX(50px);
    opacity: 0;
  }
  100% {
    transform: none;
    opacity: 1;
  }
}
</style>

实现思路

删除操作

删除元素右移消失,下方元素往上移动一格

  1. 定义删除动画和上移动画
/* 往右渐变消失 */
.leave {
  transition: all 0.5s;
  transform: translateX(200px);
  opacity: 0;
}
/* 往上移动 */
.up {
  transition: all 0.5s;
  transform: translateY(-30px);
}
  1. 模板根据删除元素索引绑定删除动画和上移动画
<div
  class="item"
  v-for="(item, index) in list"
  :key="item.id"
  :class="{
    leave: delIndex == index,
    up: delIndex != -1 && delIndex < index,
  }"
></div>
  1. 点击删除按钮指定删除元素索引,删除动画播放完毕后,删除元素
del(index) {
  // 当前已有元素在执行操作,退出
  if (this.isHandle) return;
  this.delIndex = index;
  setTimeout(() => {
    this.list.splice(index, 1);
    this.delIndex = -1;
  }, this.animationTime);
}

置顶操作

置顶元素移到最上方,上方元素往下移动一格

  1. 定义下移动画(置顶动画需要手动计算移动距离)
/* 往下移动 */
.down {
  transition: all 0.5s;
  transform: translateY(30px);
}
  1. 模板根置顶元素索引绑定下移动画
<div
  class="item"
  v-for="(item, index) in list"
  :key="item.id"
  :class="{
    down: topIndex > index,
  }"
></div>
  1. 点击置顶按钮指定置顶元素索引,计算置顶需要移动的距离(索引 * 元素高度),绑定置顶动画,动画播放完毕后,移动元素
top(index) {
  // 当前已有元素在执行操作,退出
  if (this.isHandle) return;
  this.topIndex = index;
  // 置顶动画
  this.nowHandleNode.style.transition = "all 0.5s";
  this.nowHandleNode.style.transform = `translateY(-${30 * index}px)`;
  setTimeout(() => {
    // 移除置顶动画
    this.nowHandleNode.style.transition = "";
    this.nowHandleNode.style.transform = "";
    // 移动元素
    const item = this.list[index];
    this.list.splice(index, 1);
    this.list.unshift(item);
    this.topIndex = -1;
  }, this.animationTime);
}

添加操作

  1. 定义添加动画
/* 从右方渐变进入 */
.from {
  animation: 0.5s linear 1 from;
}
@keyframes from {
  0% {
    transform: translateX(50px);
    opacity: 0;
  }
  100% {
    transform: none;
    opacity: 1;
  }
}
  1. 点击添加按钮,添加元素,DOM 渲染后绑定添加动画,动画播放完毕后,移除样式
add() {
  if (!this.inputValue.trim()) return;
  // 添加元素
  this.list.push({
    id: (this.list.length + 1).toString(),
    content: this.inputValue.trim(),
  });
  this.inputValue = "";

  this.$nextTick(() => {
    // this.$refs.listRef.children[this.list.length - 1] 最后一个元素
    this.$refs.listRef.children[this.list.length - 1].
    classList.add("from");

    setTimeout(() => {
      this.$refs.listRef.children[this.list.length - 1].
      classList.remove("from");
    }, this.animationTime);
  });
}