于肖磊修改数据提示
27fcf755创建于 3月13日历史提交
<template>
    <!-- #ifndef APP-NVUE -->
    <view v-if="isShow" ref="ani" class="page-width-max" :animation="animationData" :class="propCustomClass" :style="transformStyles" @click="onClick">
        <slot></slot>
    </view>
    <!-- #endif -->
    <!-- #ifdef APP-NVUE -->
    <view v-if="isShow" ref="ani" class="page-width-max" :animation="animationData" :class="propCustomClass" :style="transformStyles" @click="onClick">
        <slot></slot>
    </view>
    <!-- #endif -->
</template>
<script>
    import { createAnimation } from './createAnimation';
    /**
     * transition  动画组件
     * @description
     * @tutorial
     * @property {Boolean}	show	控制组件显示或关闭 (默认 false )
     * @property {Array | String}	mode	内置过渡动画类型 (默认 'fade' )
     * @value fade 渐隐渐出过渡
     * @value slide-top 由上至下过渡
     * @value slide-bottom 由下至上过渡
     * @value slide-left 由左至右过渡
     * @value slide-right 由右至左过渡
     * @value zoom-in 由小到大过渡
     * @value zoom-out 由大到小过渡
     * @property {String | Number}	duration	动画的执行时间,单位ms (默认 300 )
     * @property {Object} customStyle 组件样式,同 css 样式,注意带’-‘连接符的属性需要使用小驼峰写法如:`backgroundColor:red`
     * @property {String} timingFunction	使用的动画过渡函数 (默认 'ease-out' )
     * @property {String} customClass	自定义类名
     * @event {Function} click 点击组件触发
     * @event {Function} change	过渡动画结束时触发
     * @example
     */
    export default {
        name: 'u-transition',
        emits: ['click', 'change'],
        props: {
            // 是否展示组件
            propShow: {
                type: Boolean,
                default: false,
            },
            // 使用的动画模式
            propMode: {
                type: [Array, String, null],
                default() {
                    return 'fade';
                },
            },
            // 动画的执行时间,单位ms
            propDuration: {
                type: [String, Number],
                default: 300,
            },
            // 使用的动画过渡函数
            propTimingFunction: {
                type: String,
                default: 'ease-out',
            },
            propCustomStyle: {
                type: Object,
                default() {
                    return {};
                },
            },
            propCustomClass: {
                type: String,
                default: '',
            },
            // nvue模式下 是否直接显示,在uv-list等cell下面使用就需要设置
            propCellChild: {
                type: Boolean,
                default: false,
            },
        },
        data() {
            return {
                isShow: false,
                transform: '',
                opacity: 1,
                animationData: {},
                durationTime: 300,
                config: {},
            };
        },
        watch: {
            propShow: {
                handler(newVal) {
                    if (newVal) {
                        this.open();
                    } else {
                        // 避免上来就执行 close,导致动画错乱
                        if (this.isShow) {
                            this.close();
                        }
                    }
                },
                immediate: true,
            },
        },
        computed: {
            // 初始化动画条件
            transformStyles() {
                const style = {
                    transform: this.transform,
                    opacity: this.opacity,
                    ...this.addStyle(this.propCustomStyle),
                    'transition-duration': `${this.propDuration / 1000}s`,
                };
                return this.addStyle(style, 'string');
            },
        },
        created() {
            // 动画默认配置
            this.config = {
                duration: this.propDuration,
                timingFunction: this.propTimingFunction,
                transformOrigin: '50% 50%',
                delay: 0,
            };
            this.durationTime = this.propDuration;
        },
        methods: {
            /**
             * @description 样式转换
             * 对象转字符串,或者字符串转对象
             * @param {object | string} customStyle 需要转换的目标
             * @param {String} target 转换的目的,object-转为对象,string-转为字符串
             * @returns {object|string}
             */
            addStyle (customStyle, target = 'object') {
                // 字符串转字符串,对象转对象情形,直接返回
                if (this.empty(customStyle) || typeof (customStyle) === 'object' && target === 'object' || target === 'string' &&
                    typeof (customStyle) === 'string') {
                    return customStyle
                }
                // 字符串转对象
                if (target === 'object') {
                    // 去除字符串样式中的两端空格(中间的空格不能去掉,比如padding: 20px 0如果去掉了就错了),空格是无用的
                    customStyle = this.trim(customStyle)
                    // 根据";"将字符串转为数组形式
                    const styleArray = customStyle.split(';')
                    const style = {}
                    // 历遍数组,拼接成对象
                    for (let i = 0; i < styleArray.length; i++) {
                        // 'font-size:20px;color:red;',如此最后字符串有";"的话,会导致styleArray最后一个元素为空字符串,这里需要过滤
                        if (styleArray[i]) {
                            const item = styleArray[i].split(':')
                            style[this.trim(item[0])] = this.trim(item[1])
                        }
                    }
                    return style
                }
                // 这里为对象转字符串形式
                let string = ''
                for (const i in customStyle) {
                    // 驼峰转为中划线的形式,否则css内联样式,无法识别驼峰样式属性名
                    const key = i.replace(/([A-Z])/g, '-$1').toLowerCase()
                    string += `${key}:${customStyle[i]};`
                }
                // 去除两端空格
                return this.trim(string)
            },
            /**
             * 判断是否为空
             */
            empty (value) {
                switch (typeof value) {
                    case 'undefined':
                        return true
                    case 'string':
                        if (value.replace(/(^[ \t\n\r]*)|([ \t\n\r]*$)/g, '').length == 0) return true
                        break
                    case 'boolean':
                        if (!value) return true
                        break
                    case 'number':
                        if (value === 0 || isNaN(value)) return true
                        break
                    case 'object':
                        if (value === null || value.length === 0) return true
                        for (const i in value) {
                            return false
                        }
                        return true
                }
                return false
            },
            /**
             * @description 去除空格
             * @param {String} str 需要去除空格的字符串
             * @param {String} pos both(左右)|left|right|all 默认both
             */
            trim (str, pos = 'both') {
                str = String(str)
                if (pos == 'both') {
                    return str.replace(/^\s+|\s+$/g, '')
                }
                if (pos == 'left') {
                    return str.replace(/^\s*/, '')
                }
                if (pos == 'right') {
                    return str.replace(/(\s*$)/g, '')
                }
                if (pos == 'all') {
                    return str.replace(/\s+/g, '')
                }
                return str
            },
            /**
             *  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) {}
                }
                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();
                        // #ifdef APP-NVUE
                        if (this.propCellChild) {
                            this.opacity = 1;
                        } else {
                            this.animation.run();
                        }
                        // #endif
                        // #ifndef APP-NVUE
                        this.animation.run();
                        // #endif
                        // #ifdef VUE3
                        // #ifdef H5
                        this.opacity = 1;
                        // #endif
                        // #endif
                        this.$emit('change', {
                            detail: this.isShow,
                        });
                        // #ifdef H5
                        // #ifdef VUE3
                        this.transform = '';
                        // #endif
                        // #endif
                    }, 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.propMode === 'string') {
                    buildStyle(type, this.propMode);
                } else {
                    this.propMode.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.propMode === 'string') {
                    buildTranfrom(type, this.propMode);
                } else {
                    this.propMode.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>