实现购物车动画效果

292 阅读2分钟

1、页面点击

//引入购物车
import cart from './cart.vue'

//点击加入/删除购物车
clickItem: throttle(function (item) {
  this.count = item.flag ? this.count + 1 : this.count - 1
  let dom = document.getElementById(`item-${item.spuCode}`)
  //调用购物车初始化方法
  this.$refs.cartRef.init(dom, item.flag)
  item.flag = !item.flag
}, 500),

2、购物车组件

其中采用了transition的事件(before-enter、enter、after-enter),主要思路考虑了动画开始时的位置样式以及动画结束后的位置样式。在购物车组件中占位一个dom,用以承载添加购物车动画结束后的位置以及删除购物车动画开始时的位置。

<template>
  <div class="shop-box">
    <transition
      @before-enter="beforeEnter"
      @enter="enter"
      @after-enter="afterEnter"
    >
      <div id="cloneBox" v-show="isShow"></div>
    </transition>
    <div class="shop-content">
      <div class="shop-count">
        {{ count > 999 ? '999+' : count }}
      </div>
      <svg-icon iconClass="icon_gouwuche" style="font-size: 30px"></svg-icon>
    </div>
  </div>
</template>

<script>
import addShop from '@/utils/addShop'
export default {
  props: {
    count: {
      type: Number,
      default: 0
    }
  },
  data() {
    return {
      addShop: '',
      isShow: false,
      addCart: null
    }
  },
  methods: {
    //dom:点击的dom, state:true添加/false删除
    init(dom, state) {
      this.addCart = new addShop({
        dom, // 需要克隆的dom
        state, // 删除还是新增,
        cloneBoxId: 'cloneBox', // 克隆dom的容器id
        shopClass: 'shop-content', // 购物车dom的类
        shopAnmationClass: 'shop-ani' //添加/删除购物车的动画
      })
      this.addCart.init()
      this.isShow = true
    },
    // el表示要执行动画的dom元素
    beforeEnter(el) {
      this.addCart.beforeEnter(el)
    },
    // 设置完成动画之后的结束状态
    enter(el, done) {
      this.addCart.enter(el, done)
    },
    // 动画完成之后调用afterEnter函数
    afterEnter(el) {
      this.addCart.afterEnter(el).then(() => {
        this.isShow = false
      })
    }
  }
}
</script>

<style lang="less">
.shop-box {
  position: fixed;
  bottom: 200px;
  right: 300px;
  width: 100px;
  height: 100px;
  transition: all 0.4s;
  i {
    font-size: 100px;
  }
}
.shop-count {
  width: 45px;
  height: 20px;
  font-size: 14px;
  color: red;
  background: white;
  border-radius: 10px;
  text-align: center;
  line-height: 18px;
  border: 1px solid red;
  position: absolute;
  right: 25px;
  top: 6px;
}
.shop-ani {
  animation: mymove 0.5s infinite;
  animation-iteration-count: 1;
}
@keyframes mymove {
  0% {
    transform: scale(1); /*开始为原始大小*/
  }
  50% {
    transform: scale(1.2);
  }
  100% {
    transform: scale(1);
  }
}
</style>

3、实现动画方法

export default class addShop {
  constructor({ dom, shopClass, state, cloneBoxId, shopAnmationClass }) {
    this.dom = dom // 点击的dom
    this.shopDom = '' // 购物车dom
    this.shopClass = shopClass
    this.shopAnmationClass = shopAnmationClass
    this.state = state // 判断是添加还是删除
    this.cloneDiv = '' // 复制的dom
    // 装复制dom的容器
    this.cloneDomBox = ''
    this.cloneBoxId = cloneBoxId
    this.domX = 0
    this.domY = 0
    // 点击添加/删除的dom信息
    this.domWidth = 0
    this.domHeight = 0
    // 购物车的位置信息
    this.shopX = 0
    this.shopY = 0
  }
  init() {
    this.shopDom = document.getElementsByClassName(this.shopClass)[0] // 购物车dom
    this.cloneDomBox = document.getElementById(this.cloneBoxId) // 克隆dom 的容器
    // 初始化被点击dom的信息
    const { width, height, x, y } = this.dom.getBoundingClientRect()
    this.domWidth = width
    this.domHeight = height
    this.domX = x
    this.domY = y
    // 克隆点击的dom
    this.cloneDiv = this.dom.cloneNode(true)
    // 将克隆的元素添加到盒子里
    this.cloneDomBox.appendChild(this.cloneDiv)
    // 初始化购物车的位置
    const oRectShop = this.shopDom.getBoundingClientRect()
    this.shopX = oRectShop.x
    this.shopY = oRectShop.y
    console.log('init', this.dom, this.cloneDiv, this.cloneDomBox)
  }
  //设置动画开始的状态
  beforeEnter(el) {
    console.log('beforeEnter', el)
    //通过状态是增加还是删除确定动画起始位置是点击dom还是购物车dom
    let top = this.state ? this.domY : this.shopY
    let left = this.state ? this.domX : this.shopX
    el.style.width = this.domWidth + 'px'
    el.style.height = this.domHeight + 'px'
    el.style.position = 'fixed'
    el.style.left = left + 'px'
    el.style.top = top + 'px'
    // 动画过程禁止滚动
    document.getElementsByTagName('html')[0].style.overflowY = 'hidden'
    // 如果是删除购物车,则需要从将dom图缩小,实现从小到大的变化: 0.1->1
    if (!this.state) {
      el.style.transform = 'scale(0.1)'
      el.style.transformOrigin = '0 0'
      this.shopDom.classList.add(this.shopAnmationClass) //删除购物车实现购物车dom动画
    }
  }
  // 设置完成动画之后的结束状态
  enter(el, done) {
    el.offsetWidth // 强制刷新动画
    // 结束位置
    let enterX = this.domX - this.shopX
    let enterY = this.shopY - this.domY
    // 这里要考虑enterX,enterY为负数的情况
    let x = this.state ? (enterX > 0 ? '-' + enterX : Math.abs(enterX)) : enterX
    let y = this.state ? enterY : enterY > 0 ? '-' + enterY : Math.abs(enterX)
    let scaleNum = this.state ? '0.1' : '1' //增加: 1->0.1, 删除: 0.1->1
    // 1秒实现动画
    el.style.transform = `translate(${x}px,${y}px) scale(${scaleNum})`
    el.style.transformOrigin = '0 0'
    el.style.transition = 'all 0.5s ease'
    // 执行done函数,完成下面钩子函数
    setTimeout(() => {
      done()
    }, 500)
  }
  // 动画完成之后调用afterEnter函数
  afterEnter(el) {
    //如果是添加购物车,则移除购物车dom动画,如果是删除购物车,则实现购物车dom动画
    this.shopDom.classList[this.state ? 'add' : 'remove'](
      this.shopAnmationClass
    )
    return new Promise((resolve) => {
      setTimeout(() => {
        this.cloneDomBox.style = {
          position: 'fixed',
          'z-index': 9999,
          background: 'white'
        }
        this.cloneDiv && this.cloneDiv.remove()
        this.shopDom.classList.remove(this.shopAnmationClass)
        // 动画结束允许滚动
        document.getElementsByTagName('html')[0].style.overflowY = 'auto'
        resolve()
      }, 500)
    })
  }
}

以上主要来源于juejin.cn/post/696686…