移动端 Tab 组件实现(包含手势滑动切换和滑动动画)

261 阅读1分钟

功能介绍:

  1. 切换动画
  2. 手势滑动切换
  3. lazyRender
  4. 阿拉伯语等页面镜像后的动画调整(isRtl替换为项目中自己定义的属性,此处未做props)
// 样式中的颜色自行更改
<template>
    <div class="tabs">
        <div class="tab-list">
            <div
                class="tab"
                :class="{'tab_active': index === activeIndex}"
                v-for="(tab, index) in tabs" :key="index"
                @click="handleTabClick(index)"
                :style="{
                    'background-color': index === activeIndex ? activeBgColor : '',
                    'color': index === activeIndex ? activeColor : ''
                }"
            >
                {{ tab }}
            </div>
        </div>
        <div
            class="tab-content"
            ref="conetnt"
            @touchstart="handleTouchStart"
            @touchmove="handleTouchmove"
            @touchend="handleTouchEnd"
        >
            <div
                ref="lazyContainer"
                class="tab-pane"
                v-for="(_, index) in tabs" :key="index"
                :data-index="index"
                :style="{
                    display: (animated || swipeable) ? 'block' : index === activeIndex ? 'block' : 'none',
                    // 用于 lazy render
                    marginLeft: index === activeIndex && $store.state.params.isRtl ? `${marginValue}px` : '',
                    marginRight: index === activeIndex && !$store.state.params.isRtl ? `${marginValue}px` : '',
                }"
                :class="[{'tab-pane_animated': animated || swipeable}, {'current': index === activeIndex}]"
            >
                <slot v-if="loadedTabs.includes(`${index}`)"></slot>
            </div>
        </div>
    </div>
</template>

<script>
export default {
    props: {
        // tab 列表
        tabs: {
            type: Array,
            default: () => []
        },
        // 选中的背景颜色
        activeBgColor: {
            type: String,
            default: ''
        },
        // 选中的字体颜色
        activeColor: {
            type: String,
            default: ''
        },
        // 默认选中的 tab
        active: {
            type: Number,
            default: 0
        },
        // 是否开启切换动画
        animated: {
            type: Boolean,
            default: false
        },
        // 是否开启滑动切换
        swipeable: {
            type: Boolean,
            default: false
        },
        // 是否开启懒加载
        lazyRender: {
            type: Boolean,
            default: true
        },
    },
    computed: {
        istest(index) {
            return this.loadedTabs.includes(index); 
        }
    },
    data() {
        return {
            activeIndex: this.active,
            startX: 0,
            endX: 0,
            loadedTabs: [],
            observer: null,
            marginValue: 0,
        };
    },
    methods: {
        handleTabClick(index) {
            this.activeIndex = index;
            // 开启切换动画
            this.animated && this.handleChangeTab(index);
            this.$emit('click', index);
        },

        handleChangeTab(index) {
            const { isRtl } = this.$store.state.params;
            const content = this.$refs.conetnt;
            content.style.transform = `translateX(${isRtl ? '+' : '-'}${index * 100}%)`;
            content.style.transition = 'all .3s';
            this.$emit('change', index);
        },
        
        handleTouchStart(event) {
            if (!this.swipeable) return;
            this.startX = event.touches[0].clientX;
        },

        handleTouchmove(event) {
            if (!this.swipeable) return;
            this.endX = event.touches[0].clientX;
        },

        handleTouchEnd() {
            if (!this.swipeable) return;
            const deltaX = this.startX - this.endX;
            if (deltaX > 100 && this.activeIndex < this.tabs.length - 1 && this.endX) {
                this.activeIndex += 1;
                this.handleChangeTab(this.activeIndex);
            }
            else if (deltaX < -100 && this.activeIndex > 0 && this.endX) {
                this.activeIndex -= 1;
                this.handleChangeTab(this.activeIndex);
            }
        },

        initObserver() {
            this.observer = new IntersectionObserver(entries => {
                entries.forEach((entry) => {
                    if (entry.isIntersecting) {
                        const index = this.loadedTabs.indexOf(entry.target.getAttribute('data-index'));
                        if (index === -1) {
                            this.loadedTabs.push(entry.target.getAttribute('data-index'));
                        }
                    }
                });
            });
            const target = this.$refs.lazyContainer;
            target.forEach(item => {
                this.observer.observe(item);
            });
        },
        disconnectObserver() {
            this.observer && this.observer.disconnect();
        }
    },
    beforeDestroy() {
        this.disconnectObserver();
    },
    mounted() {
        // 是否开启 lazy render
        if (this.lazyRender) {
            this.initObserver();
        }
        else {
            this.loadedTabs = this.tabs.map((_, index) => {
                return `${index}`;
            });
        }
        // 获取当前 tab 占据满屏幕所需的差值
        const content = this.$refs.conetnt;
        this.marginValue = (window.screen.width - content.offsetWidth) / 2;
    },
};
</script>

<style lang="less" scoped>
    .tabs {
        display: flex;
        flex-direction: column;
        flex: 1;
        overflow: hidden;
        .tab-list {
            display: flex;
            align-items: center;
            margin-bottom: 20px;
            .tab {
                padding: 2px 10px;
                color: var(--color-content-2);
            }
            .tab_active {
                font-weight: bold;
                border-radius: 4px;
            }
        }
        .tab-content {
            display: flex;
            flex: 1;
            .tab-pane {
                flex: 1;
                display: none;
            }
            .tab-pane_animated {
                flex: 1 0 100%;
            }
        }
    }
</style>