美文网首页
使用vue开发一个对应楼层高亮导航组件

使用vue开发一个对应楼层高亮导航组件

作者: 羽晞yose | 来源:发表于2019-07-10 16:23 被阅读0次

目标是实现一个导航条组件,包含的功能:

  • 元素到达页面顶部时吸顶
  • 按钮对应楼层到达页面顶部时,高亮该按钮
  • 借助vue-awosome-swiper,实现导航条可拉动,高亮时自动滚动过去
  • 导航条可以配置展开更多按钮,展开更多时导航条不可拉动,选取对应楼层后重新恢复原有状态
最终效果展示

按钮是通过锚链接来跟楼层对应的,该导航遵循两种数据格式
一种为强关联,即按钮与楼层数据在一个字段里,需用开发者为其创建关联id
另外一种为数据映射,即按钮数据与对应楼层是通过配置里的floor_id来产生映射的

{
    "config": {
        /* 第一种数据格式,数据跟楼层按钮强制关联,不写floor_id */
        "navList_1": [
            {
                "nav_btn": "苹果",
                "list": [
                    "楼层内容1",
                    "楼层内容2",
                    "楼层内容3"
                ]
            },{
                "nav_btn": "oppo",
                "list": [
                    "楼层内容1",
                    "楼层内容2",
                    "楼层内容3"
                ]
            }
        ],

        /* 第二种数据格式,会把楼层id写在配置里,视图层读取,让导航条跟楼层对应 */
        "navList_2": [
            {
                "nav_btn": "楼层测试1",
                "floor_id": "test_1" // 对应下面的test楼层的floor_id
            },{
                "nav_btn": "楼层测试2",
                "floor_id": "test_2" // 对应下面的demo楼层的floor_id
            }
        ],
        "test": {
            "floor_id": "test_1",
            "content": ["随便写点东西1", "随便写点东西2"]
        }
        "demo": {
            "floor_id": "test_2",
            "content": ["随便写点东西1", "随便写点东西2"]
        }
    }
}

这篇文章主要会做三件事

  • 开发 scrollFn 单例,将其加到Vue的原型属性上,也就是Vue.prototype.$scrollFn,该单例作为单一状态管理,所有需要绑定滚动事件的元素都可以通过此方法快速实现
  • 开发 v-fixed 指令,该指令用于元素到达页面顶部时,添加类名来实现 position: fixed 定位,而删除类名的条件则根据指定 滚动类型 来做处理
  • 开发 scroll-nav 组件,也就是上面最终的效果

对到 滚动类型 ,这里个人根据已有经验分为两种

  • 交替类型 - 当某一个元素得到信号时,原来的元素信号消失,比如页面有多个吸顶导航条,则导航条应该是交替吸顶的,而不是全都依旧保持着吸顶状态
  • 区间滚动类型 - 根据起始元素的 offsetTop 到 另一个元素的 offsetTop + clientHeight,形成一个区间,当滚动至该区间时,订阅者得到信号,离开时得到取消信号

该文章除了基础的vue知识,还需要用稍微了解过设计模式,因为需要使用到单例模式及观察者模式,文章中的代码注释足够健全,所以不会在篇幅中过多的再次解释,具体查看代码注释即可


scrollFn 具体逻辑实现代码

scrollFn 是导航的核心功能,作用是根据订阅者的类型,在满足条件时发送通知执行函数。

import { G_SCROLL_FIXED, G_SCROLL_LIGHTHEIGHT, G_SCROLL_IN_SECTION } from './type.js';
/**
 * @name [ScorllFn单例]
 * @class ScrollFn
 * @author [yose]
 */
class ScrollFn {
    constructor () {
        this.scrollTop = 0; // 当前滚动高度
        this.isTicking = false; // 节流锁
        this.subscribes = {}; // 所有订阅类型存储对象
        this._itemFlag = {}; // 根据类型,均分配一个对象来缓存上一次高亮节点
        this.htmlHeight = 0; // 浏览器滚动高度
        this.documentHeight = document.documentElement.clientHeight; // 可见区域高度
        this._oldHtmlHgt = 0; // 浏览器滚动高度标记,用于判断文档是否发生回流
    }

    /**
     * @methods addSubscribes
     * @param {HtmlElement/Object} elm
     * @param {String} type
     * @param {Function} fn
     * @memberof ScrollFn
     * @description 添加订阅者
     */
    addSubscribes (elm, type, fn) {
        let obj = this._createObj(elm, fn);

        if (this.subscribes[type]) {
            this.subscribes[type].push(obj);
        } else {
            this.subscribes[type] = [obj];
            this._itemFlag[type] = {};
        }

        /* 将订阅者根据其 offsetTop 值进行从小到大排序 */
        this.subscribes[type].sort((a, b) => a.offsetTop - b.offsetTop);
    }

    /**
     * @methods _createObj
     * @param {HtmlElement/Object} element
     * @param {Function} fn
     * @memberof ScrollFn
     * @description 完善订阅者对象内容,根据不同订阅类型返回完善后的对象
     */
    _createObj (element, fn) {
        let obj = {};

        /* 传入的是object类型
         * 必须带有 elm 和 lastFloor 属性
         * {HtmlElement} elm 用于获取offsetTop值的节点
         * {HtmlElement} lastFloor 用于获取height的节点
         * 两个节点形成一个滑动距离响应区间,进入与离开区间内都会更改signal信号
         */
        if (element.constructor === Object) {
            let elm = element.elm;
            let lastFloor = element.lastFloor;

            obj = {
                elm,
                signal: 0,
                offsetTop: ~~elm.offsetTop - 2,
                lastFloor,
                lastFloorOffsetTop: lastFloor.offsetTop >> 0,
                lastFloorHeight: lastFloor.clientHeight >> 0,
                fn
            };
        } else {
            /* 单纯elm对象
             * 只需要获取 offsetTop 值
             * 根据 offsetTop 值进行替换,超过其 offsetTop 值的signal置1,其余为0
             */

            obj = {
                elm: element,
                signal: 0,
                offsetTop: ~~element.offsetTop - 2,
                fn
            };
        }

        return obj;
    }

    /**
     * @methods getSubscribesType
     * @param {String} type
     * @memberof ScrollFn
     * @description 获取订阅者类型,由于部分类型采用哈希值分组,这个方法用于去除哈希得到正确的滚动类型
     */
    getSubscribesType (type) {
        let result = type.replace(/(_*\d*)$/g, '');
        return result;
    }

    /**
     * @methods unsubscribe
     * @param {Object} observer
     * @memberof ScrollFn
     * @description 取消订阅,传入完善后的订阅者对象,暂时没有使用场景,暂留该函数待后续需要的时候再完善
     */
    // unSubscribes (observer) {
    //
    // }

    /**
     * @methods _notice
     * @param {String} type
     * @memberof ScrollFn
     * @description 发送通知,当某一类型的订阅者数组中有信号置换会执行该方法
     */
    _notice (type) {
        this.subscribes[type].forEach(item => {
            item.fn(item.signal);
        });
    }

    /**
     * @methods hashCode
     * @param {String} str
     * @returns
     * @memberof ScrollFn
     * @description 产生一个hash值,只有数字,规则和java的hashcode规则相同
     */
    hashCode (str) {
        let h = 0;
        let len = str.length;
        let t = 2147483648;

        for (let i = 0; i < len; i++) {
            h = 31 * h + str.charCodeAt(i);
            if (h > 2147483647) h %= t; // java 整型溢出则取模
        }

        return h;
    }

    /**
     * @methods randomWord
     * @param {Number} min 任意长度最小位(固定位数)
     * @param {Number} max 任意长度最大位
     * @memberof ScrollFn
     * @returns 产生任意长度随机字母数字组合
     */
    randomWord (min, max) {
        let str = '';
        let range = min;
        let arr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];

        range = Math.round(Math.random() * (max - min)) + min;
        for (let i = 0; i < range; i++) {
            let pos = Math.round(Math.random() * (arr.length - 1));
            str += arr[pos];
        }

        return str;
    }

    /**
     * @methods createHash
     * @returns {Number} hashcode
     * @memberof ScrollFn
     * @description 生成唯一Number类型哈希值,当订阅者属于区间类型时需要带上 _hash 的形式,比如 G_SCROLL_LIGHTHEIGHT_2059310118
     */
    createHash () {
        let timestamp = (new Date()).valueOf();
        let myRandom = this.randomWord(6, 10);

        return this.hashCode(myRandom + timestamp.toString());
    }

    /**
     * @methods isSubscribeActive
     * @memberof ScrollFn
     * @description 滑动事件主逻辑,根据不同订阅者执行不同滚动类型函数,判断订阅者是否发生信号置换
     */
    isSubscribeActive () {
        let groups = Object.keys(this.subscribes);

        groups.forEach((type) => {
            this.subscribes[type].forEach((item, index, arr) => {
                let scrollType = this.getSubscribesType(type); // 滚动类型
                let nextItemOST = arr[index + 1] ? arr[index + 1].offsetTop : 0; // 下一个订阅者的 offsetTop
                let itemOST = item.offsetTop; // 当前订阅者的 offsetTop
                let scrollTop = this.scrollTop; // 当前滚动高度值
                let isLastItem = (index === arr.length - 1); // 是否为最后一个订阅者

                switch (scrollType) {
                case G_SCROLL_FIXED:
                    this._replaceType(item, itemOST, nextItemOST, isLastItem, scrollTop, type, index);
                    break;
                case G_SCROLL_LIGHTHEIGHT:
                    this._replaceType(item, itemOST, nextItemOST, isLastItem, scrollTop, type, index);
                    this._setLastItemActive(type, item, isLastItem);
                    break;
                case G_SCROLL_IN_SECTION:
                    this._inSectionType(item, itemOST, item.lastFloorOffsetTop + item.lastFloorHeight, scrollTop, type);
                    break;
                default:
                    break;
                }
            });
        });
    }

    /**
     * @methods _setLastItemActive
     * @memberof ScrollFn
     * @param {String} type 订阅者类型
     * @param {Object} item 订阅者对象
     * @param {Boolean} isLastItem 是否为最后一个订阅者
     * @description 页面置底,高亮类型订阅者最后一个置为高亮信号(无需关注是否处于视觉层内)
     */
    _setLastItemActive (type, item, isLastItem) {
        let isArriveBtm = (this.scrollTop >= (this.htmlHeight - this.documentHeight) >> 0);
        if (!isArriveBtm || !isLastItem) return;

        item.signal = 1;
        this._itemFlag[type] = item;
        this._notice(type);
    }

    /**
     * @methods _replaceType
     * @memberof ScrollFn
     * @param {Object} item 订阅者对象
     * @param {Number} itemOST 订阅者对象的offsetTop
     * @param {Object} nextItemOST 下一个订阅者对象,用于是否仍处于激活状态判断
     * @param {Boolean} isLastItem  是否为最后一个订阅者
     * @param {Number} scrollTop  当前窗口滚动的 scrollTop 值
     * @param {String} type  订阅者类型
     * @param {Number} index  该订阅者在其数组中的索引值
     * @description 订阅者属于替换类型执行该事件,根据订阅者的 offsetTop 进行交替信号
     * 这里隐藏一个问题,会把吸顶的楼层做个标记,不会重新获取该订阅者的offsetTop值,也就是进入页面时处于吸顶的订阅者 offsetTop 不会更新
     * 但暂时想不出能触发此bug的场景,所以暂时认为是安全的
     */
    _replaceType (item, itemOST, nextItemOST, isLastItem, scrollTop, type, index) {
        // 超过HtmlElement的offsetTop
        if (scrollTop >= itemOST && (scrollTop < nextItemOST || isLastItem) && this._itemFlag[type] !== item) {
            this._itemFlag[type].signal = 0;
            this._itemFlag[type] = item;
            item.signal = 1;
            this._notice(type);
        }

        // 低于HtmlElement的offsetTop
        if (scrollTop < itemOST && item.signal === 1) {
            item.signal = 0;
            this._notice(type);
            if (!index) this._itemFlag[type] = {};
        }
    }

    /**
     * @methods _inSectionType
     * @memberof ScrollFn
     * @param {Object} item 订阅者对象
     * @param {Number} itemOST 订阅者对象的offsetTop
     * @param {Number} lastFloorHeight 订阅者最后一个HtmlElement楼层对象的高度,也可以非最后一个,形成滚动区间即可
     * @param {Number} scrollTop  当前窗口滚动的 scrollTop 值
     * @param {String} type  订阅者类型
     * @param {Number} index  该订阅者在其数组中的索引值
     * @description 订阅者属于滚动区间类型执行该事件,进入与离开区间内都会更改signal信号
     */
    _inSectionType (item, itemOST, lastFloorHeight, scrollTop, type, index) {
        // 处于滚动区间
        if (scrollTop >= itemOST && scrollTop < lastFloorHeight && this._itemFlag[type] !== item) {
            this._itemFlag[type].signal = 0;
            this._itemFlag[type] = item;
            item.signal = 1;
            this._notice(type);
        }

        // 离开滚动区间
        if ((scrollTop < itemOST || scrollTop > lastFloorHeight) && item.signal === 1) {
            item.signal = 0;
            this._notice(type);
            if (!index) this._itemFlag[type] = {};
        }
    }

    /**
     * @methods _inSectionType
     * @memberof ScrollFn
     * @description 文档高度是否发生发生变化
     */
    _isHtmlReflow () {
        this.htmlHeight = document.documentElement.scrollHeight;

        if (this.htmlHeight !== this._oldHtmlHgt) {
            this._oldHtmlHgt = this.htmlHeight;
            this._resetSubscribes();
        }
    }

    /**
     * @methods _resetSubscribes
     * @memberof ScrollFn
     * @description 重置所有监听者数据
     */
    _resetSubscribes () {
        let types = Object.keys(this.subscribes);
        let subscribes = this.subscribes;

        types.forEach(type => {
            subscribes[type].forEach(item => {
                // 有吸顶信号不去重写offsetTop,否则此时为0,将导致永远无法取消吸顶
                if (!item.signal) item['offsetTop'] = (item.elm.offsetTop >> 0) - 2;

                // 如果类型区域监听,重新获取最后楼层高度
                if (this.getSubscribesType(type) === G_SCROLL_IN_SECTION) {
                    item.lastFloorOffsetTop = item.lastFloor.offsetTop >> 0;
                    item.lastFloorHeight = (item.lastFloor.clientHeight - 10) >> 0;
                }
            });
        });
    }

    /**
     * @methods windowScrollFun
     * @memberof ScrollFn
     * @description 滚动事件
     */
    windowScrollFun () {
        this.scrollTop = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop;
        this._isHtmlReflow();
        this.isSubscribeActive();
        this.isTicking = false;
    }

    /**
     * @methods throttling
     * @memberof ScrollFn
     * @description 节流,提供外部调用,来执行滚动函数
     */
    throttling () {
        if (!this.isTicking) {
            requestAnimationFrame(this.windowScrollFun.bind(this));
            this.isTicking = true;
        }
    }
}

export default ScrollFn;

创建 pluging.js ,用于注册插件

// pluging.js 作为插件导出,并挂载到Vue原型属性上
import ScrollFn from './index.js';

/**
 * @name [创建ScorllFn单例]
 * @author [yose]
 * @returns ScorllFn
 */
export default {
    install (Vue) {
        Vue.prototype.$scrollFn = (function () {
            let instance;
            return function () {
                if (!instance) {
                    instance = new ScrollFn();
                    window.addEventListener('scroll', instance.throttling.bind(instance));
                }

                return instance;
            };
        })();
    }
};

v-fixexd 指令,作用与元素到达页面顶部时添加置顶类名。可对到吸顶导航,只置顶不取消肯定是不可行的,所以需要对其中vnode进行查询,如果存在高亮滚动导航条,则作为滚动区间类型处理,否则为交替类型

/**
 * @name [fixed指令]
 * @author [yose]
 * ---
 * 不传值,则默认吸顶状态添加common-bar类名
 * 否则吸顶状态添加传入类名
 */

import { G_SCROLL_FIXED, G_SCROLL_IN_SECTION } from '@/mod/util/scroll/h5/1.0/type.js';

Vue.directive('fixed', {
    bind: function (elm, binding, vnode) {
        let className = binding.value || 'common-bar';
        let tagFlag = false;

        Vue.prototype.$nextTick().then(() => {
            // 存在scroll-tap子组件,则吸顶判断为区间类型
            tagFlag = vnode.children.some(item => {
                let tag = item.componentOptions ? item.componentOptions.tag : false;
                if (tag && tag === 'scroll-tap') return true;
            });

            let instance = Vue.prototype.$scrollFn();
            let fn = (signal) => {
                signal ? elm.classList.add(className) : elm.classList.remove(className);
            };

            // 是否存在scroll-tap子组件 ? 区间滚动类型 : 交替类型
            tagFlag ? inSectionTypeFn(instance, elm, fn) : instance.addSubscribes(elm, G_SCROLL_FIXED, fn);
        });
    }
});

/**
 * @mtehods inSectionTypeFn
 * @param {Object} instance
 * @param {HtmlElement} elm 绑定指令的元素,这里指的是吸顶导航条的包裹层
 * @param {Function} fn
 * @description 根据 elm ,找到swiper-wrapper的最后一个子元素, 通过其 href 获取到对应的 htmlElement,创建成 {elm, lastFloor}
 */
function inSectionTypeFn (instance, elm, fn) {
    let lastChild = elm.querySelector('.swiper-wrapper').lastElementChild;
    let lastFloor = document.querySelector(lastChild.getAttribute('href'));
    let hash = instance.createHash();

    instance.addSubscribes({ elm, lastFloor }, `${G_SCROLL_IN_SECTION}_${hash}`, fn);
}

这里需要注意使用Vue.prototype.$nextTick().then(),不能单纯将函数放进$nextTick里处理,因为这个时候Dom节点还没被渲染出来


scroll-nav,它需要对两种不同数据进行处理,有对应floor_id的,不去自行添加floor_id,否则组件内部自动给对应的楼层数据添加id
虽然设计模式里,数据应该自上而下,不应该离散,但是根据真实业务场景,运营产品使用的时候没有这种概念,曾经还有需求是楼层导航里部分按钮是跳转链接的,情况很多,因此选择数据分散,依靠映射来处理
导航中接收四个数据,除了navList接受的导航条数据为必须,其余均按照真实场景进行选择

<div class="fix-bar-wrap" v-fixed>
    <h3 class="g-hd">{{fashion.title}}</h3>
    <scroll-nav
        :navList="bookNavList"
        :isShowBtn="true"
        :options="{slidesPerView : 5}"
        floorId="nav_comment_">
    </scroll-nav>
</div>

// scrollNav.vue 这里为了方便查看,改成单文件组件的形式

<template>
    <div class="nav-top" ref="scrollTapElm">
        <swiper ref="swiperNav" :options="swiperOption" :class="lockSwiper">
            <a class="swiper-slide" v-for="(item, index) of realNavList" :key="index"
                :data-fql-stat="`${realStat[index]}`" :href="`${floorSaveID[index]}`">
                <slot name="nav" :nav="item">{{item}}</slot>
            </a>
        </swiper>
        <slot name="navBtn" v-if="isShowBtn">
            <div ref="showMoreNavBtn" class="toggle-btn"></div>
            <p class="tips">请选择楼层</p>
        </slot>
    </div>
</template>

<script>
import { swiper } from 'vue-awesome-swiper';
import { G_SCROLL_LIGHTHEIGHT } from '@/mod/util/scroll/h5/1.0/type.js';

export default {
    name: 'scrollNav',
    components: {
        swiper
    },
    props: {
        /* 导航条数据 */
        navList: {
            type: Array,
            required: true,
            default: () => {
                return [];
            }
        },
        /* swiper属性 */
        options: {
            type: Object,
            default: () => {
                return {};
            }
        },
        floorId: '', // 是否需要内部帮助生成楼层ID
        isShowBtn: false // 是否为可展示更多导航条
    },
    computed: {
        /* 固定swiper不可拖动 */
        lockSwiper () {
            if (this.navList.length <= 1) return 'swiper-no-swiping';
        },
        /* 获取对应swiper实例 */
        swiper () {
            return this.$refs.swiperNav.swiper;
        },
        /* 合并swiper属性 */
        swiperOption () {
            return Object.assign({slidesPerView: 'auto'}, this.options);
        }
    },
    data () {
        return {
            realNavList: [], // 去除无效楼层后的navList
            realStat: [], // 生成hot-tag上报字段
            floorSaveID: [], // 对应楼层ID储存数组
            floorSaveArr: [], // 对应楼层HtmlElement储存数组
            floorNum: 0, // 当前激活楼层
            lockFlag: false // 标记展开状态,锁定swiper
        };
    },
    methods: {
        /**
         * @methods getFloors
         * @description 获取对应楼层 ID 写入数组缓存
         */
        getFloors () {
            // 不需要内部生成楼层id,item自带floor_id字段
            if (!this.floorId) {
                this.floorSaveID = this.navList.map((item) => {
                    return `#${item.floor_id}`;
                });
            }

            if (this.floorId) {
                let [i, len] = [0, this.navList.length];
                for (i; i < len; i++) {
                    this.floorSaveID.push(`#${this.floorId}${i}`);
                }
            }
        },

        /**
         * @methods saveFloors
         * @description 去除无效按钮,并暴露在控制台上
         */
        saveFloors () {
            this.floorSaveID.forEach((item, i) => {
                let elm = document.querySelector(item);

                if (elm) {
                    this.floorSaveArr.push(elm);
                } else {
                    delete this.floorSaveID[i];
                    console.log(`${item} is not found`);
                }
            });
        },

        /**
         * @methods resetNavList
         * @description 重置按钮数据,获取有效按钮
         */
        resetNavList () {
            // 抽离原navList中的有效按钮
            this.realNavList = this.navList.filter((item, i) => {
                if (this.floorSaveID[i]) return item;
            });

            // 去除无效楼层id
            this.floorSaveID = this.floorSaveID.filter(Boolean);

            // 有效按钮生成hot-tag上报字段
            this.realStat = this.floorSaveID.map(item => {
                let str = item.toLocaleUpperCase();
                return str.replace('#', 'NAV_BTN_');
            });
        },

        /**
         * @methods judgeBtnFlex
         * @description 判断按钮是否需要设置为flex: 1
         */
        judgeBtnFlex () {
            this.$nextTick(function () {
                let lastBtnLeft = this.swiper.slidesGrid[this.swiper.slidesGrid.length - 1];
                let lastBtnWidth = this.swiper.slidesSizesGrid[this.swiper.slidesSizesGrid.length - 1];

                if (this.swiper.slides.length && lastBtnLeft + lastBtnWidth <= this.swiper.width) {
                    this.swiper.lockSwipes();
                    let slides = Array.prototype.slice.apply(this.swiper.slides);

                    slides.map(item => {
                        item.classList.add('fx1');
                    });

                    // 有展开更多按钮,删除按钮并把空间释放出来
                    if (this.isShowBtn) {
                        this.isShowBtn = false;
                        this.$refs.scrollTapElm.style.paddingRight = 0;
                    }
                }
            });
        },

        /**
         * @methods lightHeight
         * @description 按钮高亮事件
         */
        lightHeight () {
            this.$nextTick(function () {
                let _this = this;
                let instance = Vue.prototype.$scrollFn();
                let params = Array.prototype.slice.call(this.swiper.slides);

                let hash = instance.createHash();

                // 初始化第一个按钮高亮
                params[0].classList.add('on');

                this.floorSaveArr.forEach((item, index, arr) => {
                    let _index = index;

                    // 区间类型,需要以G_SCROLL_LIGHTHEIGHT开头设置滚动类型,使用_hash方式进行分组
                    instance.addSubscribes(item, `${G_SCROLL_LIGHTHEIGHT}_${hash}`, function (signal) {
                        if (signal && _this.floorNum !== _index) {
                            params[_this.floorNum].classList.remove('on');
                            params[_index].classList.add('on');
                            _this.floorNum = _index;
                            _this.swiper.slideTo(_index - 1);
                        }
                    });
                });
            });
        },

        /**
         * @methods toggleSwiper
         * @description 导航条点击事件
         */
        toggleSwiper () {
            if (!this.isShowBtn) return;

            let showMoreBtn = this.$refs.showMoreNavBtn;
            let scrollTapElm = this.$refs.scrollTapElm;
            let swiperWrapper = this.swiper.wrapper[0];

            // 展开更多按钮事件
            showMoreBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                scrollTapElm.classList.toggle('open');

                if (!this.lockFlag) {
                    this.lockFlag = true;
                    this.swiper.setWrapperTranslate(0);
                    this.swiper.lockSwipes();
                    this.swiper.activeIndex = 0;
                } else {
                    this.recoverSwiper();
                }
            });

            // 导航条点击收起更多事件
            swiperWrapper.addEventListener('click', (e) => {
                e.stopPropagation();

                if (this.lockFlag) {
                    this.swiper.update(false);
                    scrollTapElm.classList.remove('open');
                    this.recoverSwiper();
                }
            });
        },

        /**
         * @methods recoverSwiper
         * @description 收起,还原最初状态,并滚动swiper
         */
        recoverSwiper () {
            this.lockFlag = false;
            this.swiper.unlockSwipes();
            this.swiper.update(true);
            this.swiper.slideTo(this.floorNum - 1, 0, false);
        },

        init () {
            this.getFloors();
            this.saveFloors();
            this.resetNavList();
            this.lightHeight();
            this.judgeBtnFlex();
            this.toggleSwiper();
        }
    },
    mounted () {
        this.init();
    }
};
</script>

<style>
@import "~@/inc/sales/style/mixin/fn.less";

.toggle-btn{
    position: absolute;
    z-index: 2;
    top: 0;
    right: 0;
}
.tips{
    position: absolute;
    z-index: 1;
    top: 0;
    width: 100%;
    padding-left: 36px;
    opacity: 0;
    box-sizing: border-box;
    pointer-events: none;
}
.open{
    .swiper-container{
        position: absolute;
        width: 100%;
    }
    /deep/.swiper-wrapper{
        -webkit-box-lines: multiple;
        flex-wrap: wrap;
    }
    .tips{opacity: 1;}
}
</style>

这里说一下为什么需要附上哈希,因为导航条存在吸顶与吸底,吸顶的导航条多数情况下只是响应页面某一个模块,但是吸底一般为整体页面所有模块的锚点,这个时候彼此数值存在交集。如果只有一组,那么在触发信号的时候会快速的连续置换满足条件的订阅者,这样就导致滚动的每一刻,所有滚动区间类型的订阅者都反复收到信号执行函数,因此需要将他们进行分组,带上哈希,前面为固定固定类型字段,后面带上哈希,形成多个分组

相关文章

网友评论

      本文标题:使用vue开发一个对应楼层高亮导航组件

      本文链接:https://www.haomeiwen.com/subject/xiyckctx.html