让用户感受到互动性的秘密武器Transition, TransitionGroup,css 动画

104 阅读11分钟

交互体验在项目开发是非常重要的东西,好的用户体验能为你的公司为你的产品带来意想不到的价值。若能开发出能让用户感受到我们的产品在和他友好互动。 相信用户的心情一定会很愉悦。那么我们有什么办法能开发出这样的产品呢? 今天就通过3个案例来给大家介绍下提升用户交互体验的一些技巧。

一、一个tab交互的优化过程

components 下新建ArticleTab 文件夹

ArticleTab下新建HotArticle 和LatestArticles两个组件

编写HotArticle 组件代码:

<template>
  <div>
    <div class="list">
      <ul>
        <li v-for="item in list" :key="item.id">
          <a href="#">{{ item.title }}</a>
          <span>{{ item.time }}</span>
        </li>
      </ul>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
const list = ref([
  {
    id: 1,
    title: 'DeepSeek建议:没存款的35岁该选什么工作?这3个选择比送外卖强'
  },
  {
    id: 2,
    title: '《哪吒2》:20句经典台词,句句封神,直戳人心!'
  },
  {
    id: 3,
    title: '中国保姆机器人火了!2025年正式量产,做饭保洁样样精通'
  },
  {
    id: 4,
    title: '单依纯北京演唱会造型,礼服设计的很独特呀'
  },
  {
    id: 5,
    title: '新能源汽车或将迎来有史以来最大的降价'
  }
])
</script>

<style lang="scss" scoped>
.list {
  height: 355px;
  overflow-y: auto;
}
.list ul{
  padding-left: 20px;

  li {
    padding: 10px 0;
    display: flex;
    justify-content: space-between;

    a {
      width: calc(100% - 80px);
      text-decoration: none;
      font-size: 18px;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      color: #333;
    }
  }
}
</style>

编写LatestArticles 组件代码:

<template>
  <div>
    <div class="list">
      <ul>
        <li v-for="item in list" :key="item.id">
          <a href="#">{{ item.title }}</a>
          <span>{{ item.time }}</span>
        </li>
      </ul>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
const list = ref([
  {
    id: 1,
    title: '“数字杭州”科技引领活力奔涌'
  },
  {
    id: 2,
    title: '以更多实实在在的成果造福中非人民'
  },
  {
    id: 3,
    title: '“新”意满满 多地中小学迎接新学期'
  },
  {
    id: 4,
    title: '学习贯彻党的二十届三中全会精神'
  },
  {
    id: 5,
    title: '推动农民工向高素质产业工人转型'
  }
])
</script>

<style lang="scss" scoped>
.list {
  height: 355px;
  overflow-y: auto;
}
.list ul{
  padding-left: 20px;

  li {
    padding: 10px 0;
    display: flex;
    justify-content: space-between;

    a {
      width: calc(100% - 80px);
      text-decoration: none;
      font-size: 18px;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      color: #333;
    }
  }
}
</style>

1. 生硬的tab 切换

<template>
  <div class="wrap">
    <div class="tab-bar">
      <span 
        v-for="item in tabBars" 
        @click="setTab(item.name)" 
        :class="{ active: current === item.name}"
      >{{ item.title }}</span>
    </div>
    <div class="tab-content">
      <HotArticle v-if="current === 'HotArticle'" />
      <LatestArticles v-else="current === 'LatestArticles'" />
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import HotArticle from '@/components/ArticleTab/HotArticle.vue';
import LatestArticles from '@/components/ArticleTab/LatestArticles.vue';

const tabBars = [
  { name: 'HotArticle', title: '热门文章'},
  { name: 'LatestArticles', title: '最新文章'},
]

const current = ref('HotArticle')

const setTab = (tab) => {
  current.value = tab
}
</script>

<style lang="scss" scoped>
.wrap {
  width: 600px;
  height: 400px;
  border: 1px solid #ddd;
}
.tab-bar {
  display: flex;
  justify-content: space-between;

  span {
    border-left: 1px solid #ddd;
    border-bottom: 1px solid #ddd;
    padding: 10px 0;
    width: 50%;
    text-align: center;
    cursor: pointer;
    
    &:nth-child(1) {
      border-left: none;
    }

    &.active {
      background: #f04142;
      color: #fff;
    }
  }
}
</style>


这样一个普通的tab 切换功能就开发完成了, 我们来看下效果:

tab.gif

切换功能是实现了, 但是这非常生硬, 看着不是很自然。 有没有办法在切换的时候有个过程, 比如说慢慢的隐藏上一个,慢慢的显示下一个。我们可以借助Vue提供的一个内置组件Transition,我们不需要额外的导入,直接使用即可。

来看看具体使用方式:

2. 舒适的tab切换

<template>
  <div class="wrap">
    <div class="tab-bar">
      <span 
        v-for="item in tabBars" 
        @click="setTab(item.name)" 
        :class="{ active: current === item.name}"
      >{{ item.title }}</span>
    </div>
    <div class="tab-content">
      <Transition>
        <HotArticle v-if="current === 'HotArticle'" key="HotArticle" />
        <LatestArticles v-else="current === 'LatestArticles'" key="LatestArticles" />
      </Transition>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import HotArticle from '@/components/ArticleTab/HotArticle.vue';
import LatestArticles from '@/components/ArticleTab/LatestArticles.vue';

const tabBars = [
  { name: 'HotArticle', title: '热门文章'},
  { name: 'LatestArticles', title: '最新文章'},
]

const current = ref('HotArticle')

const setTab = (tab) => {
  current.value = tab
}
</script>

<style lang="scss" scoped>
.wrap {
  width: 600px;
  height: 400px;
  border: 1px solid #ddd;
}
.tab-bar {
  display: flex;
  justify-content: space-between;

  span {
    border-left: 1px solid #ddd;
    border-bottom: 1px solid #ddd;
    padding: 10px 0;
    width: 50%;
    text-align: center;
    cursor: pointer;
    
    &:nth-child(1) {
      border-left: none;
    }

    &.active {
      background: #f04142;
      color: #fff;
    }
  }
}

.v-enter-from,
.v-leave-to {
  opacity: 0;
}
.v-enter-active,
.v-leave-active{
  transition: opacity 0.5s ease-in;
}
.v-enter-to,
.v-leave-from{
  opacity: 1;
}
</style>

这个例子中我们主要做一下优化:

  • 增加了Transtion 组件包裹tab 内容
  • style 里面增加了一段样式 如下:
.v-enter-from,
.v-leave-to {
  opacity: 0;
}
.v-enter-active,
.v-leave-active{
  transition: opacity 0.5s ease-in;
}
.v-enter-to,
.v-leave-from{
  opacity: 1;
}
  • v-enter-from 进入的动画,刚进入的那一刻
  • v-enter-active 进入动画从进入到进入完成的过程
  • v-enter-to 进入动画完成的那一刻
  • v-leave-from 离开动画刚离开的那一刻
  • v-leave-active 离开动画从开始到结束的过程
  • v-leave-to 离开结束的那一刻

现在我们来看下效果:

tab-donghua.gif

可以看到,过渡的效果是有了,但是有一个问题, 我们切换的时候tab 下方多了一个。 这是怎么回事呢? 元婴是我们切换的时候,进入的动画和离开的动画同时进行了。 我们怎么解决这个问题呢? 只需要修改下进入的动画。延迟0.5s 即可,即等离开的动画结束之后再开始进入的动画。修改如下:

.v-enter-active,
.v-leave-active{
  transition: opacity 0.5s ease-in;
}

修改成:

.v-leave-active{
  transition: opacity 0.5s ease-in;
}
.v-enter-active {
  transition: opacity 0.5s ease-in 0.5s;
}

我们再来查看效果:

tab-donghua2.gif

可以看到现在交效果就比较舒坦了。当然如果这个动画效果你的话,可以根据自己的想法调试出自己喜欢的一个效果。

这里效果我们是完成了, 但是我们现在的类名和现在的效果看起来有些怪异。我们现在做的是一个淡入淡出的效果。类名是v-... 这样的形式,有没有办法可以修改呢? 其实是可以的,我们只需要给Transition 组件上加上一个name="fade" 属性即可。

现在我们动画相关的样式即可以写成以下形式了:

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
.fade-leave-active{
  transition: opacity 0.5s ease-in;
}
.fade-enter-active {
  transition: opacity 0.5s ease-in 0.5s;
}
.fade-enter-to,
.fade-leave-from{
  opacity: 1;
}

现在来看看效果是否正常:

fade3.gif

可以看到效果依然好使。

Transition 除了能和v-if, v-else 一起使用外,还能和v-show, compenent动态组件一起使用。下面我们以动态组件为例来尝试下。

修改App.vue , 将

 <HotArticle v-if="current === 'HotArticle'" key="HotArticle" />
 <LatestArticles v-else="current === 'LatestArticles'" key="LatestArticles" />

改成:

<component :is="tabComs[current]"></component>

js 代码新增:

const tabComs = {
  HotArticle,
  LatestArticles
}

现在我们再来查看下效果:

component.gif

二、表单提交的交互优化

这个例子中我们通过表单提交成功和提交失败的功能。 优化前后对比用户体验。

1.无情的表单交互

<template>
  <div class="wrap">
    <form @submit.prevent="submitForm">
      <div class="form-item" >
        <span>用户名:</span>
        <input v-model="userName" />
      </div>
      <div class="form-item">
        <span>密码:</span>
        <input v-model="password" type="password" />
      </div>
      <div class="form-bottom">
        <button type="submit">提交</button>
      </div>
    </form>
    <div class="gray" v-if="showSuccess || showError"></div>
    <div class="tip success-tip" v-if="showSuccess">
      <img src="@/assets/images/tongue.svg"> 提交成功
    </div>

    <div class="tip error-tip" v-if="showError">
      <img src="@/assets/images/face_worst.svg"> 提交失败
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const userName = ref('')
const password = ref('')
const showSuccess = ref(false)
const showError = ref(false)
const submitForm = () => {
  if (userName.value && password.value) {
    showSuccess.value = true
    setTimeout(() => {
      showSuccess.value = false
    }, 3000)
  } else {
    showError.value = true
    setTimeout(() => {
      showError.value = false
    }, 3000)
  }

  return false
}
</script>

<style lang="scss" scoped>
.form-item {
  display: flex;
  margin-top: 10px;
  span {
    width: 80px;
  }
  input {
    border: none;
    outline: none;
    box-shadow: 0 0 0 1px #dcdfe6 inset;
    border-radius: 4px;
    padding: 1px 11px;
    line-height: 28px;
  }
}

.gray {
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.5);
  position: fixed;
  left: 0;
  top: 0;
}
.form-bottom {
  margin-top: 20px;
  button {
    border: 1px solid #dcdfe6;
    border-radius: 4px;
    color: #606266;
    padding: 8px 15px;
    background: #fff;
    cursor: pointer;
    margin-left: 80px;
  }
}

.tip {
  position: fixed;
  width: 180px;
  height: 60px;
  left: 50%;
  top: 50%;
  background: #fff;
  border-radius: 4px;
  transform: translate(-50%, -50%);
  box-shadow: 0 0 3px 5px rgba(0, 0, 0, 0.3);
  display: flex;
  justify-content: center;
  align-items: center;
  color: #fff;

  img {
    width: 28px;
    margin-right: 10px;
  }
}
.success-tip {
  background: #67c23a;
  color: #fff;
}
.error-tip {
  background: #f56c6c;
}
</style>

上面代码中:

  • 我们只要有一个输入框没有输入东西,点击提交按钮时都会弹出提交失败的提示
  • 当我们都输入了东西时, 点击提交按钮就会弹出提交成功的提示

来看看运行效果:

form3.gif

可以看到我们的交互效果也是非常生硬。没有感情。现在我们同样也为他添加一下动画效果。这里我们会用到一个第三方的动画库, animate.css

2. 具有互动性的表单交互开发过程

安装animate.css

pnpm i animate.css

修改之前的代码:

<template>
  <div class="wrap">
    <form @submit.prevent="submitForm">
      <div class="form-item" >
        <span>用户名:</span>
        <input v-model="userName" />
      </div>
      <div class="form-item">
        <span>密码:</span>
        <input v-model="password" type="password" />
      </div>
      <div class="form-bottom">
        <button type="submit">提交</button>
      </div>
    </form>
    <div class="gray" v-if="showSuccess || showError"></div>
    <Transition 
      name="custom-classes"
      enter-active-class="animate__animated animate__shakeY"
      leave-active-class="animate__animated animate__fadeOutUp"
    >
      <div class="tip success-tip" v-if="showSuccess">
        <img src="@/assets/images/tongue.svg"> 提交成功
      </div>
    </Transition>
    
    <Transition 
      enter-active-class="animate__animated animate__shakeX"
      leave-active-class="animate__animated animate__fadeOutRightBig"
    >
      <div class="tip error-tip" v-if="showError">
        <img src="@/assets/images/face_worst.svg"> 提交失败
      </div>
    </Transition>
    
  </div>
</template>

<script setup>
import { ref } from 'vue'
import "animate.css";

const userName = ref('')
const password = ref('')
const showSuccess = ref(false)
const showError = ref(false)
const submitForm = () => {
  if (userName.value && password.value) {
    showSuccess.value = true
    setTimeout(() => {
      showSuccess.value = false
    }, 3000)
  } else {
    showError.value = true
    setTimeout(() => {
      showError.value = false
    }, 3000)
  }

  return false
}
</script>

<style lang="scss" scoped>
.form-item {
  display: flex;
  margin-top: 10px;
  span {
    width: 80px;
  }
  input {
    border: none;
    outline: none;
    box-shadow: 0 0 0 1px #dcdfe6 inset;
    border-radius: 4px;
    padding: 1px 11px;
    line-height: 28px;
  }
}

.gray {
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.5);
  position: fixed;
  left: 0;
  top: 0;
}
.form-bottom {
  margin-top: 20px;
  button {
    border: 1px solid #dcdfe6;
    border-radius: 4px;
    color: #606266;
    padding: 8px 15px;
    background: #fff;
    cursor: pointer;
    margin-left: 80px;
  }
}

.tip {
  position: fixed;
  width: 180px;
  height: 60px;
  left: 50%;
  top: 50%;
  background: #fff;
  border-radius: 4px;
  transform: translate(-50%, -50%);
  box-shadow: 0 0 3px 5px rgba(0, 0, 0, 0.3);
  display: flex;
  justify-content: center;
  align-items: center;
  color: #fff;

  img {
    width: 28px;
    margin-right: 10px;
  }
}
.success-tip {
  background: #67c23a;
  color: #fff;
}
.error-tip {
  background: #f56c6c;
}
</style>

在上述代码中:

  • 我们增加了Transition 组件包裹成功提示和失败的提示
  • Transition 组件上我们增加了enter-active-class属性和leave-active-class 属性。 这两个属性是用来自定义动画类名的,相当于之前的v-enter-activev-leave-active
  • 我们导入了animate.css

我们来看下运行效果:

GIFShow_视频转GIF_3.gif.gif

可以看到现在效果看起来就舒服多了,成功了跳两下,就像真的在和你互动一样,成功了,开心得跳起来。失败了摇头表示不开心。

我们现在看到的都是单个的效果, 但是我们经常还会做一些列表的交互。下面我们来做一个列表相关的操作。

三、 生硬的表格交互效果

我们平常写的表格交互如下:

<template>
  <table class="ui-table">
    <thead>
      <tr>
        <td>日期</td>
        <td>姓名</td>
        <td>地址</td>
        <td>操作</td>
      </tr>
    </thead>
    <tbody>
      <tr v-for="(item, index) in tableData" :key="index">
        <td>{{ item.date }}</td>
        <td>{{ item.name }}</td>
        <td>{{ item.address }}</td>
        <td>
          <button class="del-button" @click="deleteItem(index)">删除</button>
        </td>
      </tr>
    </tbody>
  </table>
</template>
<script setup>
import { ref } from 'vue'
const tableData = ref([
  {
    date: '2016-05-03',
    name: 'Tom',
    address: 'No. 189, Grove St, Los Angeles',
  },
  {
    date: '2016-05-02',
    name: 'Tom',
    address: 'No. 189, Grove St, Los Angeles',
  },
  {
    date: '2016-05-04',
    name: 'Tom',
    address: 'No. 189, Grove St, Los Angeles',
  },
  {
    date: '2016-05-01',
    name: 'Tom',
    address: 'No. 189, Grove St, Los Angeles',
  },
])

const deleteItem = (index) => {
  tableData.splice(index, 1)
}


</script>
<style lang="scss" scoped>
.ui-table{
  border-collapse: collapse; /* 合并边框 */
  border: 1px solid #dcdfe6; /* 设置表格外边框 */
  width: 100%;
  tr {
    border-bottom: 1px solid #dcdfe6;
    :deep(td){
      padding: 10px;
    }
  }
  .del-button {
    border: 1px solid #f56c6c;
    border-radius: 4px;
    color: #606266;
    padding: 8px 15px;
    background: #f56c6c;
    cursor: pointer;
    color: #fff;
  }
}
</style>

在上面代码中:

  • 我们写了个table 列表
  • 点击删除按钮可以进行删除 来查看下效果:

table-del.gif

可以看到效果也是非常的生硬。

2. 让人感受到舒适的交互效果

现在我们来给他加下动画效果, 不过这次要使用新的组件TransitionGroup, 因为Transition 只能实现只有一个根标签的动画, 而我们这里是一个列表,所以需要使用TransitionGroup。 来看下具体使用方式:

<template>
  <table class="ui-table">
    <thead>
      <tr>
        <td>日期</td>
        <td>姓名</td>
        <td>地址</td>
        <td>操作</td>
      </tr>
    </thead>
    <TransitionGroup name="fade" tag="tbody">
      <tr v-for="(item, index) in tableData" :key="index">
        <td>{{ item.date }}</td>
        <td>{{ item.name }}</td>
        <td>{{ item.address }}</td>
        <td>
          <button class="del-button" @click="deleteItem(index)">删除</button>
        </td>
      </tr>
    </TransitionGroup>
  </table>
</template>
<script setup>
import { ref } from 'vue'
const tableData = ref([
  {
    date: '2016-05-03',
    name: 'Tom',
    address: 'No. 189, Grove St, Los Angeles',
  },
  {
    date: '2016-05-02',
    name: 'Tom',
    address: 'No. 189, Grove St, Los Angeles',
  },
  {
    date: '2016-05-04',
    name: 'Tom',
    address: 'No. 189, Grove St, Los Angeles',
  },
  {
    date: '2016-05-01',
    name: 'Tom',
    address: 'No. 189, Grove St, Los Angeles',
  },
])

const deleteItem = (index) => {
  tableData.value.splice(index, 1)
}


</script>
<style lang="scss" scoped>
.ui-table{
  border-collapse: collapse; /* 合并边框 */
  border: 1px solid #dcdfe6; /* 设置表格外边框 */
  width: 100%;
  tr {
    border-bottom: 1px solid #dcdfe6;
    :deep(td){
      padding: 10px;
    }
  }
  .del-button {
    border: 1px solid #f56c6c;
    border-radius: 4px;
    color: #606266;
    padding: 8px 15px;
    background: #f56c6c;
    cursor: pointer;
    color: #fff;
  }
}

.fade-enter-from,
.fade-leave-to{
  opacity: 0;
}
.fade-enter-active,
.fade-leave-active{
  transition: all 0.5s;
}

.fade-enter-to,
.fade-leave-from {
  opacity: 1;
}
</style>

在上面代码中:

  • 我们用TransitionGroup 包裹了tbody 中的tr
  • TransitionGroup 添加了name 属性自定义类名的开头
  • TransitionGroup 添加了tag 属性,用来指定渲染tr标签。 TransitionGroup 默认不渲染任何标签的,只有增加了tag 属性才会渲染指定的标签。
  • 删除了tbody 中的tr , 因为他已经被TransitionGroup 指定的tag属性代替
  • 增加动画样式

现在来看下效果:

table-donghua222.gif

可以看到现在删除就会有一个过渡的效果,看着就比较舒服了。

总结

本篇通过3个案例介绍了几个常用优化用户体验的方法:

  1. 通过Transition 给一些单个根元素的结构提供一些过渡
  2. 还可以通过Transition 结合第三方动画库animate.css 来提升用户的互动体验
  3. 通过TransitionGroup 可以提升一些列表交互的用户体验。

本文的介绍就介绍到这里了,感谢您的收看。