skyouc
2024-12-21 c9c263dc43ad90f95f24a036cee9e6b47afb596c
uni_modules/uni-transition/components/uni-transition/uni-transition.vue
@@ -1,277 +1,277 @@
<template>
   <view v-if="isShow" ref="ani" :animation="animationData" :class="customClass" :style="transformStyles" @click="onClick"><slot></slot></view>
</template>
<script>
import { createAnimation } from './createAnimation'
/**
 * Transition 过渡动画
 * @description 简单过渡动画组件
 * @tutorial https://ext.dcloud.net.cn/plugin?id=985
 * @property {Boolean} show = [false|true] 控制组件显示或隐藏
 * @property {Array|String} modeClass = [fade|slide-top|slide-right|slide-bottom|slide-left|zoom-in|zoom-out] 过渡动画类型
 *  @value fade 渐隐渐出过渡
 *  @value slide-top 由上至下过渡
 *  @value slide-right 由右至左过渡
 *  @value slide-bottom 由下至上过渡
 *  @value slide-left 由左至右过渡
 *  @value zoom-in 由小到大过渡
 *  @value zoom-out 由大到小过渡
 * @property {Number} duration 过渡动画持续时间
 * @property {Object} styles 组件样式,同 css 样式,注意带’-‘连接符的属性需要使用小驼峰写法如:`backgroundColor:red`
 */
export default {
   name: 'uniTransition',
   emits:['click','change'],
   props: {
      show: {
         type: Boolean,
         default: false
      },
      modeClass: {
         type: [Array, String],
         default() {
            return 'fade'
         }
      },
      duration: {
         type: Number,
         default: 300
      },
      styles: {
         type: Object,
         default() {
            return {}
         }
      },
      customClass:{
         type: String,
         default: ''
      }
   },
   data() {
      return {
         isShow: false,
         transform: '',
         opacity: 1,
         animationData: {},
         durationTime: 300,
         config: {}
      }
   },
   watch: {
      show: {
         handler(newVal) {
            if (newVal) {
               this.open()
            } else {
               // 避免上来就执行 close,导致动画错乱
               if (this.isShow) {
                  this.close()
               }
            }
         },
         immediate: true
      }
   },
   computed: {
      // 生成样式数据
      stylesObject() {
         let styles = {
            ...this.styles,
            'transition-duration': this.duration / 1000 + 's'
         }
         let transform = ''
         for (let i in styles) {
            let line = this.toLine(i)
            transform += line + ':' + styles[i] + ';'
         }
         return transform
      },
      // 初始化动画条件
      transformStyles() {
         return 'transform:' + this.transform + ';' + 'opacity:' + this.opacity + ';' + this.stylesObject
      }
   },
   created() {
      // 动画默认配置
      this.config = {
         duration: this.duration,
         timingFunction: 'ease',
         transformOrigin: '50% 50%',
         delay: 0
      }
      this.durationTime = this.duration
   },
   methods: {
      /**
       *  ref 触发 初始化动画
       */
      init(obj = {}) {
         if (obj.duration) {
            this.durationTime = obj.duration
         }
         this.animation = createAnimation(Object.assign(this.config, obj),this)
      },
      /**
       * 点击组件触发回调
       */
      onClick() {
         this.$emit('click', {
            detail: this.isShow
         })
      },
      /**
       * ref 触发 动画分组
       * @param {Object} obj
       */
      step(obj, config = {}) {
         if (!this.animation) return
         for (let i in obj) {
            try {
               if(typeof obj[i] === 'object'){
                  this.animation[i](...obj[i])
               }else{
                  this.animation[i](obj[i])
               }
            } catch (e) {
               console.error(`方法 ${i} 不存在`)
            }
         }
         this.animation.step(config)
         return this
      },
      /**
       *  ref 触发 执行动画
       */
      run(fn) {
         if (!this.animation) return
         this.animation.run(fn)
      },
      // 开始过度动画
      open() {
         clearTimeout(this.timer)
         this.transform = ''
         this.isShow = true
         let { opacity, transform } = this.styleInit(false)
         if (typeof opacity !== 'undefined') {
            this.opacity = opacity
         }
         this.transform = transform
         // 确保动态样式已经生效后,执行动画,如果不加 nextTick ,会导致 wx 动画执行异常
         this.$nextTick(() => {
            // TODO 定时器保证动画完全执行,目前有些问题,后面会取消定时器
            this.timer = setTimeout(() => {
               this.animation = createAnimation(this.config, this)
               this.tranfromInit(false).step()
               this.animation.run()
               this.$emit('change', {
                  detail: this.isShow
               })
            }, 20)
         })
      },
      // 关闭过度动画
      close(type) {
         if (!this.animation) return
         this.tranfromInit(true)
            .step()
            .run(() => {
               this.isShow = false
               this.animationData = null
               this.animation = null
               let { opacity, transform } = this.styleInit(false)
               this.opacity = opacity || 1
               this.transform = transform
               this.$emit('change', {
                  detail: this.isShow
               })
            })
      },
      // 处理动画开始前的默认样式
      styleInit(type) {
         let styles = {
            transform: ''
         }
         let buildStyle = (type, mode) => {
            if (mode === 'fade') {
               styles.opacity = this.animationType(type)[mode]
            } else {
               styles.transform += this.animationType(type)[mode] + ' '
            }
         }
         if (typeof this.modeClass === 'string') {
            buildStyle(type, this.modeClass)
         } else {
            this.modeClass.forEach(mode => {
               buildStyle(type, mode)
            })
         }
         return styles
      },
      // 处理内置组合动画
      tranfromInit(type) {
         let buildTranfrom = (type, mode) => {
            let aniNum = null
            if (mode === 'fade') {
               aniNum = type ? 0 : 1
            } else {
               aniNum = type ? '-100%' : '0'
               if (mode === 'zoom-in') {
                  aniNum = type ? 0.8 : 1
               }
               if (mode === 'zoom-out') {
                  aniNum = type ? 1.2 : 1
               }
               if (mode === 'slide-right') {
                  aniNum = type ? '100%' : '0'
               }
               if (mode === 'slide-bottom') {
                  aniNum = type ? '100%' : '0'
               }
            }
            this.animation[this.animationMode()[mode]](aniNum)
         }
         if (typeof this.modeClass === 'string') {
            buildTranfrom(type, this.modeClass)
         } else {
            this.modeClass.forEach(mode => {
               buildTranfrom(type, mode)
            })
         }
         return this.animation
      },
      animationType(type) {
         return {
            fade: type ? 1 : 0,
            'slide-top': `translateY(${type ? '0' : '-100%'})`,
            'slide-right': `translateX(${type ? '0' : '100%'})`,
            'slide-bottom': `translateY(${type ? '0' : '100%'})`,
            'slide-left': `translateX(${type ? '0' : '-100%'})`,
            'zoom-in': `scaleX(${type ? 1 : 0.8}) scaleY(${type ? 1 : 0.8})`,
            'zoom-out': `scaleX(${type ? 1 : 1.2}) scaleY(${type ? 1 : 1.2})`
         }
      },
      // 内置动画类型与实际动画对应字典
      animationMode() {
         return {
            fade: 'opacity',
            'slide-top': 'translateY',
            'slide-right': 'translateX',
            'slide-bottom': 'translateY',
            'slide-left': 'translateX',
            'zoom-in': 'scale',
            'zoom-out': 'scale'
         }
      },
      // 驼峰转中横线
      toLine(name) {
         return name.replace(/([A-Z])/g, '-$1').toLowerCase()
      }
   }
}
</script>
<style></style>
<template>
   <view v-if="isShow" ref="ani" :animation="animationData" :class="customClass" :style="transformStyles" @click="onClick"><slot></slot></view>
</template>
<script>
import { createAnimation } from './createAnimation'
/**
 * Transition 过渡动画
 * @description 简单过渡动画组件
 * @tutorial https://ext.dcloud.net.cn/plugin?id=985
 * @property {Boolean} show = [false|true] 控制组件显示或隐藏
 * @property {Array|String} modeClass = [fade|slide-top|slide-right|slide-bottom|slide-left|zoom-in|zoom-out] 过渡动画类型
 *  @value fade 渐隐渐出过渡
 *  @value slide-top 由上至下过渡
 *  @value slide-right 由右至左过渡
 *  @value slide-bottom 由下至上过渡
 *  @value slide-left 由左至右过渡
 *  @value zoom-in 由小到大过渡
 *  @value zoom-out 由大到小过渡
 * @property {Number} duration 过渡动画持续时间
 * @property {Object} styles 组件样式,同 css 样式,注意带’-‘连接符的属性需要使用小驼峰写法如:`backgroundColor:red`
 */
export default {
   name: 'uniTransition',
   emits:['click','change'],
   props: {
      show: {
         type: Boolean,
         default: false
      },
      modeClass: {
         type: [Array, String],
         default() {
            return 'fade'
         }
      },
      duration: {
         type: Number,
         default: 300
      },
      styles: {
         type: Object,
         default() {
            return {}
         }
      },
      customClass:{
         type: String,
         default: ''
      }
   },
   data() {
      return {
         isShow: false,
         transform: '',
         opacity: 1,
         animationData: {},
         durationTime: 300,
         config: {}
      }
   },
   watch: {
      show: {
         handler(newVal) {
            if (newVal) {
               this.open()
            } else {
               // 避免上来就执行 close,导致动画错乱
               if (this.isShow) {
                  this.close()
               }
            }
         },
         immediate: true
      }
   },
   computed: {
      // 生成样式数据
      stylesObject() {
         let styles = {
            ...this.styles,
            'transition-duration': this.duration / 1000 + 's'
         }
         let transform = ''
         for (let i in styles) {
            let line = this.toLine(i)
            transform += line + ':' + styles[i] + ';'
         }
         return transform
      },
      // 初始化动画条件
      transformStyles() {
         return 'transform:' + this.transform + ';' + 'opacity:' + this.opacity + ';' + this.stylesObject
      }
   },
   created() {
      // 动画默认配置
      this.config = {
         duration: this.duration,
         timingFunction: 'ease',
         transformOrigin: '50% 50%',
         delay: 0
      }
      this.durationTime = this.duration
   },
   methods: {
      /**
       *  ref 触发 初始化动画
       */
      init(obj = {}) {
         if (obj.duration) {
            this.durationTime = obj.duration
         }
         this.animation = createAnimation(Object.assign(this.config, obj),this)
      },
      /**
       * 点击组件触发回调
       */
      onClick() {
         this.$emit('click', {
            detail: this.isShow
         })
      },
      /**
       * ref 触发 动画分组
       * @param {Object} obj
       */
      step(obj, config = {}) {
         if (!this.animation) return
         for (let i in obj) {
            try {
               if(typeof obj[i] === 'object'){
                  this.animation[i](...obj[i])
               }else{
                  this.animation[i](obj[i])
               }
            } catch (e) {
               console.error(`方法 ${i} 不存在`)
            }
         }
         this.animation.step(config)
         return this
      },
      /**
       *  ref 触发 执行动画
       */
      run(fn) {
         if (!this.animation) return
         this.animation.run(fn)
      },
      // 开始过度动画
      open() {
         clearTimeout(this.timer)
         this.transform = ''
         this.isShow = true
         let { opacity, transform } = this.styleInit(false)
         if (typeof opacity !== 'undefined') {
            this.opacity = opacity
         }
         this.transform = transform
         // 确保动态样式已经生效后,执行动画,如果不加 nextTick ,会导致 wx 动画执行异常
         this.$nextTick(() => {
            // TODO 定时器保证动画完全执行,目前有些问题,后面会取消定时器
            this.timer = setTimeout(() => {
               this.animation = createAnimation(this.config, this)
               this.tranfromInit(false).step()
               this.animation.run()
               this.$emit('change', {
                  detail: this.isShow
               })
            }, 20)
         })
      },
      // 关闭过度动画
      close(type) {
         if (!this.animation) return
         this.tranfromInit(true)
            .step()
            .run(() => {
               this.isShow = false
               this.animationData = null
               this.animation = null
               let { opacity, transform } = this.styleInit(false)
               this.opacity = opacity || 1
               this.transform = transform
               this.$emit('change', {
                  detail: this.isShow
               })
            })
      },
      // 处理动画开始前的默认样式
      styleInit(type) {
         let styles = {
            transform: ''
         }
         let buildStyle = (type, mode) => {
            if (mode === 'fade') {
               styles.opacity = this.animationType(type)[mode]
            } else {
               styles.transform += this.animationType(type)[mode] + ' '
            }
         }
         if (typeof this.modeClass === 'string') {
            buildStyle(type, this.modeClass)
         } else {
            this.modeClass.forEach(mode => {
               buildStyle(type, mode)
            })
         }
         return styles
      },
      // 处理内置组合动画
      tranfromInit(type) {
         let buildTranfrom = (type, mode) => {
            let aniNum = null
            if (mode === 'fade') {
               aniNum = type ? 0 : 1
            } else {
               aniNum = type ? '-100%' : '0'
               if (mode === 'zoom-in') {
                  aniNum = type ? 0.8 : 1
               }
               if (mode === 'zoom-out') {
                  aniNum = type ? 1.2 : 1
               }
               if (mode === 'slide-right') {
                  aniNum = type ? '100%' : '0'
               }
               if (mode === 'slide-bottom') {
                  aniNum = type ? '100%' : '0'
               }
            }
            this.animation[this.animationMode()[mode]](aniNum)
         }
         if (typeof this.modeClass === 'string') {
            buildTranfrom(type, this.modeClass)
         } else {
            this.modeClass.forEach(mode => {
               buildTranfrom(type, mode)
            })
         }
         return this.animation
      },
      animationType(type) {
         return {
            fade: type ? 1 : 0,
            'slide-top': `translateY(${type ? '0' : '-100%'})`,
            'slide-right': `translateX(${type ? '0' : '100%'})`,
            'slide-bottom': `translateY(${type ? '0' : '100%'})`,
            'slide-left': `translateX(${type ? '0' : '-100%'})`,
            'zoom-in': `scaleX(${type ? 1 : 0.8}) scaleY(${type ? 1 : 0.8})`,
            'zoom-out': `scaleX(${type ? 1 : 1.2}) scaleY(${type ? 1 : 1.2})`
         }
      },
      // 内置动画类型与实际动画对应字典
      animationMode() {
         return {
            fade: 'opacity',
            'slide-top': 'translateY',
            'slide-right': 'translateX',
            'slide-bottom': 'translateY',
            'slide-left': 'translateX',
            'zoom-in': 'scale',
            'zoom-out': 'scale'
         }
      },
      // 驼峰转中横线
      toLine(name) {
         return name.replace(/([A-Z])/g, '-$1').toLowerCase()
      }
   }
}
</script>
<style></style>