uniapp tab切换及tab锚点效果(wx小程序及H5端)
1. uniapp tab切换及tab锚点效果(wx小程序及H5端)
1.1. 示例源码
1.1.1. 在 components 文件夹下,新建 tab-anchor.vue 文件,一键复制如下代码。
<!-- 可根据现有需求,"适当"更改组件源码! --><template><view :id="'luBarTabNav'+barId" class="lu-bar-tab-nav"><view v-if="!!barFixed" id="luTabFixed" class="lu-bar-tab lu-bar-tab-fixed" :style="{ top: barTopStyles,height:barHeightStyles,display:barShowStyles }"><view class="lu-tab-item" v-for="(item,index) in tabList" :key="index" :class="[selectedIndex==index? 'lu-active' : '',!!iconShow? 'lu-icon-show' : '']" :style="selectedIndex==index?tabActiveStyles:tabStyles"@tap="_scrollToTarget(index, item)"><view v-if="!!iconShow" class="lu-tab-icon" :class="selectedIndex==index?item.selectedIconClass:item.iconClass":style="selectedIndex==index?(!!item.selectedIconImage?'backgroundImage:url('+item.selectedIconImage+')':''):(!!item.iconImage?'backgroundImage:url('+item.iconImage+')':'')"></view><view:class="selectedIndex==index? 'lu-tab-text lu-tab-text-active' : 'lu-tab-text'">{{item.text}}</view></view></view><view id="luTabStatic" class="lu-bar-tab lu-bar-tab-static" :style="{height:barHeightStyles}"><view class="lu-tab-item" v-for="(item,index) in tabList" :key="index" :class="[selectedIndex==index? 'lu-active' : '',!!iconShow? 'lu-icon-show' : '']" :style="selectedIndex==index?tabActiveStyles:tabStyles"@tap="_scrollToTarget(index, item)"><view v-if="!!iconShow" class="lu-tab-icon" :class="selectedIndex==index?item.selectedIconClass:item.iconClass":style="selectedIndex==index?(!!item.selectedIconImage?'backgroundImage:url('+item.selectedIconImage+')':''):(!!item.iconImage?'backgroundImage:url('+item.iconImage+')':'')"></view><view:class="selectedIndex==index? 'lu-tab-text lu-tab-text-active' : 'lu-tab-text'">{{item.text}}</view></view></view><view class="lu-tab-content"><slot></slot></view></view>
</template><script>export default {props: {barFixed:{//选项卡是否启用浮动功能(可选)type:Boolean,default:true},iconShow:{//是否启用选项卡图标(可选)type:Boolean,default:false},transitionShow:{type:Boolean,default:false},barHeight:{type:[String,Number],default:44},barTop:{type:[String,Number],default:0},barId:{type:[String,Number],default:0},tabList: {type:Array,default:function () {return []}}},data() {return {barShow:false,selectedIndex:0,};},computed:{tabStyles:function () {return (!!this.color?'color:'+this.color+';':'')+(!!this.backgroundColor?'backgroundColor:'+this.backgroundColor+';':'');},tabActiveStyles:function () {return (!!this.selectedColor?'color:'+this.selectedColor+';':'')+(!!this.selectedBackgroundColor?'backgroundColor:'+this.selectedBackgroundColor+';':'');},barTopStyles:function () {// #ifndef H5return 'calc('+this.barTop+'px);';// #endif// #ifdef H5return 'calc('+this.barTop+'px + var(--window-top));';// #endif},barHeightStyles:function () {return this.barHeight?this.barHeight+'px':'44px';},barShowStyles:function () {return !this.barShow?'none':'';},},methods: {_barInit:async function (index){let navTargetTop = [];let duration = 0;let viewScrollTop = 0;let viewHeight = 0;for (let i = 0,len=this.tabList.length; i < len; i++) {navTargetTop[i]= await this._queryMultipleNodes(this.tabList[i]["navTarget"]).then(res => {let navTarget = res[0],viewPort = res[1];viewHeight = viewPort.height;viewScrollTop = viewPort.scrollTop;const scrollTop = parseInt(navTarget.top) + viewPort.scrollTop - this.barTop - this.barHeight;return scrollTop;});}if (!!this.transitionShow) {duration = 200;} return {navTargetTop:navTargetTop,duration:duration,viewHeight:viewHeight,viewScrollTop:viewScrollTop};},_pageScroll:async function(i){const elment = await this._barInit(i);let scrollTop = elment["navTargetTop"][i];let duration = elment["duration"];let viewHeight = elment["viewHeight"];let viewScrollTop = elment["viewScrollTop"];if (Math.abs(scrollTop-viewScrollTop)>viewHeight) {if (scrollTop>viewScrollTop) {await uni.pageScrollTo({scrollTop:(scrollTop-viewHeight),duration:0});}else{await uni.pageScrollTo({scrollTop:(scrollTop+viewHeight),duration:0});}}await uni.pageScrollTo({scrollTop:(scrollTop+1),duration:duration});// #ifndef H5const view = await this._queryMultipleNodes();viewScrollTop = view[0].scrollTop;if (scrollTop>viewScrollTop&&duration!==0) {uni.pageScrollTo({scrollTop:(scrollTop+1),duration: 0})}// #endif},_scrollToTarget:function(i, item) {this._pageScroll(i);// 通知父组件(把行数据传过去)// console.log(item)this.$emit('clickTabs', item)},_queryMultipleNodes:function (e,notThis) {return new Promise((resolve, reject) => {let view = uni.createSelectorQuery();if (!!notThis) {view.in(this);}if (!!e) {view.select(e).boundingClientRect();}view.selectViewport().fields({size: true,scrollOffset: true});view.exec(function(res) {resolve(res);});});},_showBarFixed:function () {this._queryMultipleNodes("#luTabStatic",true).then(res => {let tabNav = res[0];if (tabNav.top<=this.barTop) {this.barShow=true;}else{this.barShow=false;}});},_selectedTab:function (y) {this._barInit().then((res)=>{let itemIndex = 0;for (let i = 0,len=res["navTargetTop"].length; i < len; i++) {if (y >=res["navTargetTop"][i]) {itemIndex = i;}}this.selectedIndex = itemIndex;});if (!!this.barFixed) {this._showBarFixed();}}}};
</script><style lang="scss" >lu-bar-tab-nav{position:relative;width: 100%;}.lu-bar-tab-nav{position:relative;width: 100%;.lu-bar-tab{width: 100%;display: flex;flex-flow: row wrap;justify-content: space-around;align-items:center;// 选项卡背景颜色background-color: #fff;height: 44px;.lu-tab-item{//默认状态position: relative;flex: 1 1 auto;text-align: center;color: #333;height: 100%;font-size: 15px;display: flex;flex-flow: column nowrap;justify-content: center;align-items:center;// &::before{// position: absolute;// top: calc(50% - 15px);// left: 0px;// content: " ";// width: 1px;// height: 30px;// background-color: #eee;// }&:first-child::before{display: none;}.lu-tab-icon{font-size: inherit;color: inherit;}// 选项卡默认样式.lu-tab-text{font-size: inherit;color: inherit;// border: 1px solid red;padding-bottom: 6px;// 加个默认白色,防止高度不统一border-bottom: 3px solid #fff;}// 激活的选项卡-下划线.lu-tab-text-active {border-bottom: 3px solid #2979ff;}// 显示图标&.lu-icon-show{.lu-tab-icon{height: 24px;width: 24px;background-position: center center;background-repeat: no-repeat;background-size: 100% 100%;}.lu-tab-text{font-size: 12px;line-height: 16px;}}// 选中状态&.lu-active{color: #2979ff;// font-weight: bold;.lu-tab-icon{background-position: center center;background-repeat: no-repeat;background-size: 100% 100%;}}}}.lu-bar-tab-fixed{position:fixed;z-index: 1;top:calc(0px + var(--window-top));}.lu-bar-tab-static{position:static;z-index: 0;}}
</style>
1.1.2. 页面引用
<!-- 注意: 底部最好留空或业务内容占位,否则会出现最后一个锚点"激活时(到显示区域)",选项卡无高亮的问题 -->
<!-- 如果想要修改选项卡样式,请到组件中去修改 -->
<!-- 更多配置及用法,详见文章底部组件文档! --><template><view><!-- 占位内容 --><view class="seize"><text>顶部-占位内容(可有可无)</text></view><!-- END --><Anchor:tabList="tabs":barFixed="true":transitionShow="true"barHeight="50":barTop="0"barId="0"@clickTabs="clickTabs"ref="barTabNav"><!-- id与数据tabs->navTarget对应 --><view id="item1"><view class="box"><p v-for="item in 10">无论什么怪异容器,都能精准适应</p></view></view><view id="item2"><view class="box"><h1>只有简短的一行容器</h1></view></view><view id="item3"><view class="box"><p v-for="item in 30">红线就是每个锚点的区域</p></view></view></Anchor><!-- 占位内容 --><view class="seize"><text>底部-占位内容(可有可无)</text></view><!-- END --></view>
</template><script>// 注意引用位置
import Anchor from '@/components/anchor.vue'export default {components:{ Anchor },data() {return {// 选项卡// 注意: 必须有text/navTarget属性,其他键值不管随便写和用tabs: [{text: "选项一",//名称navTarget: "#item1",//锚点value: 'one'//比如},{text: "选项二",navTarget: "#item2",value: 'two'},{text: "选项三",navTarget: "#item3",value: 'three'}],// 当前选项卡valuecurrent: 'one'}},/*** 监听滚动* @description 传给子组件处理逻辑* @return void */onPageScroll(e) {this.$refs.barTabNav._selectedTab(e.scrollTop);},methods: {/*** 选项卡被点击* @description 你无需关心锚点如何工作,写你的业务即可* @param {Object} row - 行数据* @return void*/clickTabs(row) {// 避免重复点击if(this.current == row.value) return false;// 操作处理console.log(row)this.current = row.valueuni.showToast({title: row.text,icon: 'none'})},}
}
</script><style scoped>/* 占位内容 */
.seize {background: #dadada;padding: 40px;text-align: center;
}
/* END *//* 选项卡样式 */
.tabs {background: #fff;width: 100%;
}
/* END */.box {background: #dadada;padding: 20px;border-bottom: 5px solid red;
}
</style>
1.1.3. Props
注意:在APP下建议禁用transitionShow,不然会引起点击触发滚动中选项卡导航栏消失的bug。
1.1.4. tabList
除了这两个必填项,其他数据随便往里面追加,点击选项卡时会读出来。
1.2. 其它代码
1.2.1. tabAnchor.json
{"rows": [{"text": "选项一","navTarget": "#item1","navId": "item1","title": "one","subRows": [{"id": 1,"img": "../../../static/icon/base-menu/icon-base-community-add.png","title": "小区录入","url": "../eventInfo/eventInfo"},{"id": 2,"img": "../../../static/icon/base-menu/icon-base-building-add.png","title": "楼栋录入","url": "../baseInfo/baseInfo"},{"id": 3,"img": "../../../static/icon/base-menu/icon_base_house_add.png","title": "房屋录入","url": "../workCheckInfo/workCheckInfo"},{"id": 4,"img": "../../../static/icon/base-menu/icon_base_people_add.png","title": "人口录入","url": "../eventInfo/eventInfo"}]},{"text": "选项二","navTarget": "#item2","navId": "item2","title": "two","subRows": [{"id": 1,"img": "../../../static/icon/base-menu/icon-base-community-add.png","title": "小区录入","url": "../eventInfo/eventInfo"},{"id": 2,"img": "../../../static/icon/base-menu/icon-base-building-add.png","title": "楼栋录入","url": "../baseInfo/baseInfo"},{"id": 3,"img": "../../../static/icon/base-menu/icon_base_house_add.png","title": "房屋录入","url": "../workCheckInfo/workCheckInfo"},{"id": 4,"img": "../../../static/icon/base-menu/icon_base_people_add.png","title": "人口录入","url": "../eventInfo/eventInfo"},{"id": 4,"img": "../../../static/icon/base-menu/icon_base_people_add.png","title": "人口录入","url": "../eventInfo/eventInfo"},{"id": 4,"img": "../../../static/icon/base-menu/icon_base_people_add.png","title": "人口录入","url": "../eventInfo/eventInfo"},{"id": 4,"img": "../../../static/icon/base-menu/icon_base_people_add.png","title": "人口录入","url": "../eventInfo/eventInfo"},{"id": 4,"img": "../../../static/icon/base-menu/icon_base_people_add.png","title": "人口录入","url": "../eventInfo/eventInfo"},{"id": 4,"img": "../../../static/icon/base-menu/icon_base_people_add.png","title": "人口录入","url": "../eventInfo/eventInfo"},{"id": 4,"img": "../../../static/icon/base-menu/icon_base_people_add.png","title": "人口录入","url": "../eventInfo/eventInfo"}]},{"text": "选项三","navTarget": "#item3","navId": "item3","title": "three","subRows": [{"id": 1,"img": "../../../static/icon/base-menu/icon-base-community-add.png","title": "小区录入","url": "../eventInfo/eventInfo"},{"id": 2,"img": "../../../static/icon/base-menu/icon-base-building-add.png","title": "楼栋录入","url": "../baseInfo/baseInfo"},{"id": 3,"img": "../../../static/icon/base-menu/icon_base_house_add.png","title": "房屋录入","url": "../workCheckInfo/workCheckInfo"},{"id": 4,"img": "../../../static/icon/base-menu/icon_base_people_add.png","title": "人口录入","url": "../eventInfo/eventInfo"},{"id": 4,"img": "../../../static/icon/base-menu/icon_base_people_add.png","title": "人口录入","url": "../eventInfo/eventInfo"},{"id": 4,"img": "../../../static/icon/base-menu/icon_base_people_add.png","title": "人口录入","url": "../eventInfo/eventInfo"},{"id": 4,"img": "../../../static/icon/base-menu/icon_base_people_add.png","title": "人口录入","url": "../eventInfo/eventInfo"},{"id": 4,"img": "../../../static/icon/base-menu/icon_base_people_add.png","title": "人口录入","url": "../eventInfo/eventInfo"},{"id": 4,"img": "../../../static/icon/base-menu/icon_base_people_add.png","title": "人口录入","url": "../eventInfo/eventInfo"},{"id": 4,"img": "../../../static/icon/base-menu/icon_base_people_add.png","title": "人口录入","url": "../eventInfo/eventInfo"}]}]
}
1.2.2. tab-anchor.vue
<template><view :id="'luBarTabNav'+barId"><view v-if="!!barFixed"id="luTabFixed"class="stb-tab-layout stb-tab-fixed":style="{ top: barTopStyles,height:barHeightStyles}"><view v-for="(item,index) in tabList":key="index":class="selectedIndex===index? 'stb-tab-item stb-tab-active': 'stb-tab-item stb-tab-normal'"@tap="scrollToTarget(index, item)">{{ item.text }}</view></view><view id="luTabStatic"class="stb-tab-layout stb-tab-static":style="{height:barHeightStyles}"><view class=""v-for="(item,index) in tabList" :key="index":class="selectedIndex===index? 'stb-tab-item stb-tab-active': 'stb-tab-item stb-tab-normal'"@tap="scrollToTarget(index, item)">{{ item.text }}</view></view><view><slot></slot></view></view>
</template>
<script>
export default {props: {barFixed: {//选项卡是否启用浮动功能(可选)type: Boolean,default: true},/*** 是否动画切换*/transitionShow: {type: Boolean,default: false},barHeight: {type: [String, Number],default: 30},barTop: {type: [String, Number],default: 0},barId: {type: [String, Number],default: 0},tabList: {type: Array,default: function () {return []}}},data() {return {selectedIndex: 0,};},computed: {barTopStyles: function () {let barTopStyles = 'calc(' + this.barTop + 'px);'// #ifndef H5barTopStyles = 'calc(' + this.barTop + 'px);'// #endif// #ifdef APPbarTopStyles = 'calc(' + this.barTop + 'px + var(--window-top));';// #endifreturn barTopStyles;},barHeightStyles: function () {return this.barHeight ? this.barHeight + 'px' : '30px';},},methods: {/*** 初始化tab*/barInit: async function (index) {let navTargetTop = [];let duration = 0;let viewScrollTop = 0;let viewHeight = 0;for (let i = 0, len = this.tabList.length; i < len; i++) {navTargetTop[i] = await this.queryMultipleNodes(this.tabList[i]["navTarget"]).then(res => {let navTarget = res[0],viewPort = res[1];viewHeight = viewPort.height;viewScrollTop = viewPort.scrollTop;const scrollTop = parseInt(navTarget.top)+ viewPort.scrollTop- this.barTop - this.barHeight;return scrollTop;});}if (!!this.transitionShow) {duration = 200;}return {navTargetTop: navTargetTop,duration: duration,viewHeight: viewHeight,viewScrollTop: viewScrollTop};},pageScroll: async function (i) {const elment = await this.barInit(i);let scrollTop = elment["navTargetTop"][i];let duration = elment["duration"];let viewHeight = elment["viewHeight"];let viewScrollTop = elment["viewScrollTop"];if (Math.abs(scrollTop - viewScrollTop) > viewHeight) {if (scrollTop > viewScrollTop) {await uni.pageScrollTo({scrollTop: (scrollTop - viewHeight),duration: 0});} else {await uni.pageScrollTo({scrollTop: (scrollTop + viewHeight),duration: 0});}}await uni.pageScrollTo({scrollTop: (scrollTop + 1),duration: duration});// #ifndef H5const view = await this.queryMultipleNodes();viewScrollTop = view[0].scrollTop;if (scrollTop > viewScrollTop && duration !== 0) {uni.pageScrollTo({scrollTop: (scrollTop + 1),duration: 0})}// #endif},scrollToTarget: function (i, item) {this.pageScroll(i);// 通知父组件(把行数据传过去)this.$emit('clickTabs', item)},queryMultipleNodes: function (e, notThis) {return new Promise((resolve, reject) => {let view = uni.createSelectorQuery();if (!!notThis) {view.in(this);}if (!!e) {view.select(e).boundingClientRect();}view.selectViewport().fields({size: true,scrollOffset: true});view.exec(function (res) {resolve(res);});});},showBarFixed: function () {this.queryMultipleNodes("#luTabStatic",true).then(res => {let tabNav = res[0];this.barShow = tabNav.top <= this.barTop;});},selectedTab: function (y) {this.barInit().then((res) => {let itemIndex = 0;for (let i = 0, len = res["navTargetTop"].length;i < len; i++) {if (y >= res["navTargetTop"][i]) {itemIndex = i;}}this.selectedIndex = itemIndex;});if (!!this.barFixed) {this.showBarFixed();}}}
};
</script>
<style lang="scss">
.stb-tab-layout {width: 100%;display: flex;flex-flow: row wrap;justify-content: space-around;align-items: center;background-color: #fff;height: 30px;
}.stb-tab-fixed {position: fixed;z-index: 1;top: calc(0px + var(--window-top));
}.stb-tab-static {position: static;z-index: 0;
}.stb-tab-item {position: relative;flex: 1;height: 100%;display: flex;flex-flow: column nowrap;justify-content: center;border: 1px solid #2979ff;font-size: 12px;align-items: center;
}.stb-tab-normal {color: #2979ff;
}.stb-tab-active {background-color: #2979ff;color: white;
}</style>
1.2.3. tabAnchor.vue
<template><view><tab-anchor ref="barTabNav"barId="0"barHeight="40":tabList="tabList":barFixed="true":transitionShow="false":barTop="0"@clickTabs="clickTabs"><!--id与数据tabList->navTarget对应--><view class="anchor-menu-body"v-for="(item) in tabList"><view :id="item.navId"class="anchor-menu-main-layout"><view class="anchor-header-layout"><view class="anchor-header-left"><view class="anchor-header-icon"></view><text class="anchor-header-title">{{ item.title }}</text></view><view></view></view><view class="anchor-menu-layout"><view class="anchor-menu-item"v-for="(subItem) in item.subRows"@click="toDetail(subItem)"><image class="anchor-menu-item-image" :src="subItem.img"></image><view class="anchor-menu-item-title">{{ subItem.title }}</view></view></view></view></view></tab-anchor></view>
</template>
<script>
// 注意引用位置
import TabAnchor from "/components/tab-anchor.vue";
import tabAnchorJson from '../../../data/tabAnchor.json'export default {components: {TabAnchor},data() {return {// 注意: 必须有text/navTarget属性,其他键值不管随便写和用tabList: [],}},onLoad: function () {this.tabList = tabAnchorJson.rows},/*** 监听滚动* @description 传给子组件处理逻辑* @return void*/onPageScroll(e) {this.$refs.barTabNav.selectedTab(e.scrollTop);},methods: {/*** 选项卡被点击* @description 你无需关心锚点如何工作,写你的业务即可* @param {Object} row - 行数据* @return void*/clickTabs(row) {},}
}
</script><style scoped>.anchor-menu-body {display: flex;flex-direction: column;background: linear-gradient(180deg, #fff, #fff);
}.anchor-menu-main-layout {display: flex;flex-direction: column;
}.anchor-header-layout {display: flex;flex-direction: row;justify-content: space-between;align-items: center;margin-top:5px;
}.anchor-header-left {display: flex;flex-direction: row;align-items: center;
}.anchor-header-icon {width: 5px;height: 18px;margin: 0 5px 0 10px;border-radius: 3px;background-color: #007AFF;
}.anchor-header-title {font-size: 16px;color: #000000;
}.anchor-menu-layout {display: table;flex-wrap: wrap;justify-content: space-between;align-content: space-between;padding: 5px;
}.anchor-menu-item {width: 21%;display: flex;flex-direction: column;align-items: center;padding: 5px 0;margin: 5px 2%;border-radius: 10px;float: left;background-color: #EBF2F7;
}.anchor-menu-item-image {width: 32px;height: 32px;
}.anchor-menu-item-title {width: 100%;color: #06121e;font-size: 12px;text-align: center;overflow: hidden;display: -webkit-box;-webkit-line-clamp: 1;-webkit-box-orient: vertical;
}
</style>