想要实现的效果:
- 顶部tab栏标题与下面列表标题对应,滚动超过tab栏高度,吸顶定位
- 向下滚动时,列表出现标题与顶部tab栏高亮标题对应
- 点击顶部tab栏标题,下面列表滚动到对应标题,且tab标题栏相应左右滚动到合适位置
具体效果如图:
又到了说实现原理的时刻,让我想想哈,有了如下:
1.吸顶:
给页面添加一个滚动监听事件,在里面动态获取tab栏到顶部的距离, 以及滚动距离,二者比较确定是否添加吸顶类名
2.滚动定位:
由于顶部tab栏与下面列表栏都是用for循环生成的li标签,就可以通过遍历tab栏标题数组,当滚动距离+ 110(tab栏自身高度 + 到顶部距离) >= 列表中每个模块到顶部距离时,tab栏对应标题高亮,且左右滚动一段距离
3.点击定位:
获取点击标题,与内容列表中标题做判断,相同时,对应列表中模块进行滚动操作,滚动距离为 = 列表模块到顶部距离 - 110(tab栏自身高度 + 到顶部距离)
4.设置列表高度:
- 要想再点击最后一个标题,页面也滚动到最后列表模块的话,那就需要给列表设置足够的高度,有留白,好让他往上滚动
- 计算公式 = 屏幕高度 + 列表中最后模块到顶部距离offsetTop - (110 + 列表中最后模块高度 + padding)
- 通过行内属性,动态添加高度,为了让页面一打开就设置好高度,需要在 mounted 钩子中添加以上计算
tab栏横向滑动实现代码:
<!-- 标题栏 --> <header class="title_father"> <ul :class="['title_list_box',{'titleFixed':Fixed}]" ref="titleRef"> <li v-for="(item,index) in titleList" :key="index" class="title_list_item" :class="{active: tabChose == index}" @click="handleTab(index)" > <div :class="{active: tabChose == index}">{{item.title}}</div> </li> </ul> </header> /************ 样式 ****************/ .title_father { height: 1rem; background-color: #fff; width: 100%; overflow: hidden; ::-webkit-scrollbar { display: none; } .title_list_box { padding-bottom: 0.2rem; height: 1rem; background-color: #fff; z-index: 5; display: -webkit-box; overflow-x: scroll; overflow-y: hidden; -webkit-overflow-scrolling: touch; border-bottom: 1px solid #f5f5f9; &.titleFixed { position: fixed; top: 1rem; } .title_list_item { height: 1rem; display: flex; align-items: center; margin: 0 0.25rem; font-size: 0.28rem; font-weight: 700; color: #333333; position: relative; &.active { color: #fd1a84; // border-bottom: 0.04rem solid #1d8ff3; &::after { content: ""; position: absolute; width: 1.12rem; height: 0.04rem; background-color: #fd1a84; left: 0; bottom: 0.02rem; } } &:last-child { width: 1.7rem; border-bottom: none; position: relative; &.active { &::after { content: ""; position: absolute; width: 1.12rem; height: 0.04rem; background-color: #fd1a84; bottom: 0.02rem; } } } } } }
核心代码主要是上面标红部分:
- html结构是: 一个父div 包含 ul标签
- 父div,width:100%(一般是保持等于屏幕宽度),设置超出隐藏: overflow: hidden;
- 子元素ul 设置自适应布局:display:-webkit-box;(li元素全部排成一行),设置x轴滚动,y轴隐藏,以及滚动顺滑
overflow-x: scroll; overflow-y: hidden; -webkit-overflow-scrolling: touch;
滚动定位 + 点击定位 实现代码:
html:
<!-- 标题栏 --> <header class="title_father"> <ul :class="['title_list_box',{'titleFixed':Fixed}]" ref="titleRef"> <li v-for="(item,index) in titleList" :key="index" class="title_list_item" :class="{active: tabChose == index}" @click="handleTab(index)" > <div :class="{active: tabChose == index}">{{item.title}}</div> </li> </ul> </header> <!-- 内容列表 --> <ul :class="['content_list_box',{'contentFixed':Fixed}]" :style="{height:(screenHeight + lastTop - 260) + 'px'}" > <li v-for="(item,index) in contentBox" :key="index" class="content_list_item" @scroll="contentScroll(index)" ref="contentRef" @click="goPage(item)" > <div class="content_title" v-if="index" ref="contentBox">{{item.title}}</div> <div class="content_box"> <div v-for="(subItem,subIndex) in item.contentList" :key="subIndex" class="content_item" @click="goLifePage(subItem,subIndex)" > <div class="item_inner" :class="{'selectStatus':isEdit}"> <i :style="{background:`url(${require(`@/assets/images/life/index/${subItem.icon_name}.png`)}) no-repeat center/100%`}" class="icon" ></i> <p class="content_name">{{subItem.content_name}}</p> <i class="jia_icon" v-if="isEdit" :class="{'jianIcon':subItem.changeIcon}"></i> </div> </div> </div> <div class="line" v-if="index<contentBox.length-1"></div> </li> </ul>
js:
data() { return { titleList: [ { title: "视频娱乐", id: 0 }, { title: "卡密相关", id: 1 }, { title: "充值中心", id: 2 }, { title: "生活缴费", id: 3 }, { title: "车务交罚", id: 4 }, { title: "生活服务", id: 5 } ], // tab标题数据 tabChose: 0, // 高亮索引值 contentBox: [ { type: "videoEnt", title: "视频娱乐", contentList: [ { type: "iqiyiVideo", icon_name: "aiqiyi@2x", content_name: "爱奇艺", changeIcon: false }, { type: "tencentVideo", icon_name: "tengxunshipin@2x", content_name: "腾讯视频", changeIcon: false }, { type: "youkuVideo", icon_name: "youkushipin@2x", content_name: "优酷视频", changeIcon: false } ] }, { title: "卡密相关", contentList: [ { type: "jingdongEcard", icon_name: "jingdongeka@2x", content_name: "京东E卡卡密", changeIcon: false }, { type: "wangyiEcard", icon_name: "wangyi@2x", content_name: "网易严选", changeIcon: false } // { // icon_name: "weipinhui@2x", // content_name: "唯品会卡密" // }, // { // icon_name: "xingbake@2x", // content_name: "星巴克卡密" // }, // { // icon_name: "baiguoyuan@2x", // content_name: "百果园卡密" // }, // { // icon_name: "shenzhouzhuanche@2x", // content_name: "神州专车卡密" // } ] }, { title: "充值中心", contentList: [ { type: "FlowRecharge", icon_name: "liuliang@2x", content_name: "流量充值", changeIcon: false }, { type: "PhoneRecharge", icon_name: "huafei@2x", content_name: "话费充值", changeIcon: false } ] }, { title: "生活缴费", contentList: [ { type: "lifePayment", icon_name: "shuifei@2x", content_name: "水费查缴", changeIcon: false }, { type: "lifePayment", icon_name: "dianfei@2x", content_name: "电费查缴", changeIcon: false }, { type: "lifePayment", icon_name: "ranqi@2x", content_name: "燃气查缴", changeIcon: false } ] }, { title: "车务交罚", contentList: [ { type: "violationSearch", icon_name: "chaxun@2x", content_name: "违章查询", changeIcon: false }, { type: "violationPay", icon_name: "jiaofei@2x", content_name: "违章缴费", changeIcon: false } ] }, { type: "", title: "生活服务", contentList: [ { type: "weather", icon_name: "zhiliang@2x", content_name: "天气预报", changeIcon: false }, { type: "aqi", icon_name: "yubao@2x", content_name: "空气质量", changeIcon: false } ] } ], // 列表数据 Fixed: false, // 是否吸顶 scroll: 0, // y轴滚动距离 screenHeight: document.body.clientHeight, //屏幕高度 titleArr: "", // tab栏数组 contentArr: "", // 列表栏数组 titleBox: "", // tab栏父元素 titleTop: 0, // tab栏到顶部的距离 titleBoxHeight: 0, //标题栏的高度 lastTop: 0, //最后一个内容距离顶部的高度 }; }, mounted() { this.titleArr = document.querySelectorAll(".title_list_item"); this.contentArr = document.querySelectorAll(".content_list_item"); this.titleBox = document.querySelector(".title_list_box"); window.addEventListener("scroll", this.handleScroll); // 获取屏幕的高度 const that = this; window.onresize = () => { return (() => { window.screenHeight = document.body.clientHeight; that.screenHeight = window.screenHeight; })(); }; this.setContentHeight(); }, methods: { // tab栏切换 handleTab(index) { this.tabChose = index; if (this.titleList[index].title === this.contentBox[index].title) { this.contentScroll(index); } }, // 点击内容发生相应变化 contentScroll(index) { // 获取每一个内容offsetTop高度 let contentTop = this.contentArr[index].offsetTop; // 获取tab栏的高度 this.titleBoxHeight = this.titleBox.offsetHeight || this.titleBox.clientHeight; window.scrollTo(0, contentTop - 110); // 55 }, //滚动监听 handleScroll() { // 获取滚动距离 this.scroll = document.documentElement.scrollTop || document.body.scrollTop; // 获取tab栏到顶部的距离 if (document.querySelector(".title_list_box")) { this.titleTop = document.querySelector(".title_list_box ").offsetTop; } // 判断距离进行吸顶操作 if (this.scroll > this.titleTop) { this.Fixed = true; } else { this.Fixed = false; this.tabChose = 0; } this.titleTop = 40; // 循环判断 滚动的距离 + 标题盒子的高度 >= 某条内容的高度时,设置该标题高亮,标题栏发生相应的滚动 for (var i = 0; i < this.titleArr.length; i++) { if (this.scroll + this.titleTop * 2 >= this.contentArr[i].offsetTop) { // 1.5 this.tabChose = i + 1; // scrollLeft代替scrollTo,解决安卓手机在微信浏览器中scrollTo事件失效 document.querySelector(".title_list_box").scrollLeft = i * 30; // 25 } } }, // 动态设置内容总体提高 setContentHeight() { this.lastTop = this.contentArr[5].offsetTop; console.log(this.contentArr[5].offsetHeight); }, }
还有一部分样式代码,我就不在上传了,核心实现代码皆以上传