列表过渡

使用 <transition-group> 组件可以为 v-for 渲染的列表添加过渡效果,包括添加、删除和重排动画。

transition-group 基础

<transition-group><transition> 的区别:

  1. 默认渲染为 <span>,可通过 tag 属性指定
  2. 内部元素必须有唯一的 key 属性
  3. 支持列表的添加、删除、重排动画
<div id="app">
  <button @click="add">添加</button>
  <button @click="remove">删除</button>
  
  <transition-group name="list" tag="ul">
    <li v-for="item in items" :key="item.id" class="list-item">
      {{ item.text }}
    </li>
  </transition-group>
</div>

<style>
.list-item {
  display: block;
  padding: 10px;
  margin: 5px 0;
  background: #f0f0f0;
  border-radius: 4px;
}

.list-enter, .list-leave-to {
  opacity: 0;
  transform: translateX(30px);
}

.list-enter-active, .list-leave-active {
  transition: all 0.5s;
}

.list-leave-active {
  position: absolute;
}

.list-move {
  transition: transform 0.5s;
}
</style>

<script>
new Vue({
  el: '#app',
  data: {
    items: [
      { id: 1, text: '项目一' },
      { id: 2, text: '项目二' },
      { id: 3, text: '项目三' }
    ],
    nextId: 4
  },
  methods: {
    add() {
      this.items.push({
        id: this.nextId++,
        text: '项目 ' + this.nextId
      })
    },
    remove() {
      this.items.splice(Math.random() * this.items.length, 1)
    }
  }
})
</script>

列表的移动动画

当列表项位置变化时,使用 v-move 类名:

.list-move {
  transition: transform 0.5s;
}

Vue 使用 FLIP 动画技术实现平滑的位置过渡。

列表删除动画

删除时需要设置 position: absolute

.list-leave-active {
  position: absolute;
}

这样其他元素才能平滑移动到新位置。

完整示例:待办事项列表

<div id="app">
  <input 
    v-model="newTodo" 
    @keyup.enter="addTodo"
    placeholder="添加待办事项"
  >
  
  <transition-group name="todo" tag="ul" class="todo-list">
    <li v-for="todo in todos" :key="todo.id" class="todo-item">
      <span>{{ todo.text }}</span>
      <button @click="removeTodo(todo.id)">删除</button>
    </li>
  </transition-group>
  
  <p v-if="!todos.length">暂无待办事项</p>
</div>

<style>
.todo-list {
  list-style: none;
  padding: 0;
  position: relative;
}

.todo-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 16px;
  margin: 8px 0;
  background: white;
  border-radius: 4px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.todo-enter {
  opacity: 0;
  transform: translateY(-20px);
}

.todo-enter-active {
  transition: all 0.3s ease;
}

.todo-leave {
  opacity: 1;
  transform: translateX(0);
}

.todo-leave-active {
  transition: all 0.3s ease;
  position: absolute;
  width: 100%;
}

.todo-leave-to {
  opacity: 0;
  transform: translateX(50px);
}

.todo-move {
  transition: transform 0.3s ease;
}
</style>

<script>
new Vue({
  el: '#app',
  data: {
    newTodo: '',
    todos: [
      { id: 1, text: '学习 Vue 基础' },
      { id: 2, text: '掌握组件化开发' },
      { id: 3, text: '实践项目开发' }
    ],
    nextId: 4
  },
  methods: {
    addTodo() {
      if (!this.newTodo.trim()) return
      
      this.todos.push({
        id: this.nextId++,
        text: this.newTodo
      })
      this.newTodo = ''
    },
    removeTodo(id) {
      const index = this.todos.findIndex(t => t.id === id)
      if (index !== -1) {
        this.todos.splice(index, 1)
      }
    }
  }
})
</script>

列表排序动画

结合计算属性实现排序动画:

<div id="app">
  <button @click="shuffle">随机排序</button>
  <button @click="sort">按数字排序</button>
  
  <transition-group name="flip-list" tag="div" class="container">
    <div v-for="item in items" :key="item.id" class="item">
      {{ item.value }}
    </div>
  </transition-group>
</div>

<style>
.container {
  display: flex;
  flex-wrap: wrap;
  position: relative;
}

.item {
  width: 80px;
  height: 80px;
  margin: 5px;
  background: #42b983;
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 24px;
  border-radius: 8px;
}

.flip-list-move {
  transition: transform 0.5s;
}
</style>

<script>
new Vue({
  el: '#app',
  data: {
    items: [
      { id: 1, value: 1 },
      { id: 2, value: 2 },
      { id: 3, value: 3 },
      { id: 4, value: 4 },
      { id: 5, value: 5 },
      { id: 6, value: 6 }
    ]
  },
  methods: {
    shuffle() {
      this.items = this.items.sort(() => Math.random() - 0.5)
    },
    sort() {
      this.items = this.items.sort((a, b) => a.value - b.value)
    }
  }
})
</script>

交错动画

使用 JavaScript 钩子实现交错效果:

<div id="app">
  <button @click="add">添加项目</button>
  
  <transition-group
    tag="ul"
    @before-enter="beforeEnter"
    @enter="enter"
    @leave="leave"
  >
    <li 
      v-for="(item, index) in items" 
      :key="item.id"
      :data-index="index"
    >
      {{ item.text }}
    </li>
  </transition-group>
</div>

<script>
new Vue({
  el: '#app',
  data: {
    items: [],
    nextId: 1
  },
  methods: {
    add() {
      for (let i = 0; i < 5; i++) {
        this.items.push({
          id: this.nextId++,
          text: '项目 ' + this.nextId
        })
      }
    },
    beforeEnter(el) {
      el.style.opacity = 0
      el.style.transform = 'translateY(20px)'
    },
    enter(el, done) {
      const delay = el.dataset.index * 100
      
      setTimeout(() => {
        el.style.transition = 'all 0.3s ease'
        el.style.opacity = 1
        el.style.transform = 'translateY(0)'
        
        el.addEventListener('transitionend', done)
      }, delay)
    },
    leave(el, done) {
      el.style.transition = 'all 0.3s ease'
      el.style.opacity = 0
      el.style.transform = 'translateY(-20px)'
      
      el.addEventListener('transitionend', done)
    }
  }
})
</script>

网格布局动画

<div id="app">
  <button @click="shuffle">打乱</button>
  
  <transition-group name="grid" tag="div" class="grid">
    <div 
      v-for="cell in cells" 
      :key="cell.id" 
      class="cell"
      :style="{ background: cell.color }"
    >
      {{ cell.number }}
    </div>
  </transition-group>
</div>

<style>
.grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 10px;
  width: 400px;
}

.cell {
  height: 80px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-size: 24px;
  border-radius: 8px;
}

.grid-move {
  transition: transform 0.5s ease;
}

.grid-enter-active, .grid-leave-active {
  transition: all 0.3s ease;
}

.grid-enter, .grid-leave-to {
  opacity: 0;
  transform: scale(0.5);
}

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

<script>
new Vue({
  el: '#app',
  data: {
    cells: Array.from({ length: 16 }, (_, i) => ({
      id: i + 1,
      number: i + 1,
      color: `hsl(${(i * 22.5)}, 70%, 60%)`
    }))
  },
  methods: {
    shuffle() {
      this.cells = this.cells.sort(() => Math.random() - 0.5)
    }
  }
})
</script>

FLIP 动画原理

Vue 的列表移动动画基于 FLIP 技术:

  1. First:记录元素当前位置
  2. Last:记录元素最终位置
  3. Invert:计算位置差,用 transform 反向偏移
  4. Play:移除 transform,让元素动画到最终位置

这就是为什么只需要添加 .list-move 类就能实现平滑的位置过渡。

注意事项

  1. 必须设置 key:每个列表项必须有唯一的 key
  2. key 不要用索引:使用索引会导致动画异常
  3. 设置 position: absolute:删除动画需要绝对定位
  4. flex 布局注意:可能需要额外样式配合

列表过渡让应用的交互更加流畅自然。掌握这些技巧,你可以创建出色的列表动画效果。