shop_uniapp/components/tabs/tabs.vue
2024-06-06 09:55:01 +08:00

387 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="tabs">
<u-sticky :enable="isFixed" :bg-color="stickyBgColor" :offset-top="top" :h5-nav-height="0">
<view :id="id" :style="{
background: bgColor,
'border-radius': borderRadius,
width: width,
margin: '0 auto'
}">
<scroll-view :style="{height: height + 'rpx'}" scroll-x class="scroll-view" :scroll-left="scrollLeft"
scroll-with-animation>
<view class="scroll-box" :class="{'tabs-scorll-flex': !isScroll}">
<view class="tab-item line1" :id="'tab-item-' + index" v-for="(item, index) in list"
:key="index" @tap="clickTab(index)" :style="[tabItemStyle(index)]">
<u-badge :count="item[count] || item['dot'] || 0" :offset="offset" size="mini"></u-badge>
{{ item[name] || item['name']}}
</view>
<view v-if="showBar" class="tab-bar" :style="[tabBarStyle]"></view>
</view>
</scroll-view>
</view>
</u-sticky>
<view class="tab-content">
<view>
<slot></slot>
</view>
</view>
</view>
</template>
<script>
import {
getRect
} from '@/utils/tools'
export default {
name: "tabs",
props: {
isScroll: {
type: Boolean,
default: true
},
current: {
type: [Number, String],
default: 0
},
height: {
type: [String, Number],
default: 80
},
fontSize: {
type: [String, Number],
default: 28
},
duration: {
type: [String, Number],
default: 0.3
},
activeColor: {
type: String,
default: '#FF2C3C'
},
inactiveColor: {
type: String,
default: '#333'
},
barWidth: {
type: [String, Number],
default: 40
},
barHeight: {
type: [String, Number],
default: 4
},
gutter: {
type: [String, Number],
default: 30
},
bgColor: {
type: String,
default: '#ffffff'
},
name: {
type: String,
default: 'name'
},
count: {
type: String,
default: 'count'
},
offset: {
type: Array,
default: () => {
return [5, 20]
}
},
bold: {
type: Boolean,
default: true
},
activeItemStyle: {
type: Object,
default () {
return {}
}
},
showBar: {
type: Boolean,
default: true
},
barStyle: {
type: Object,
default () {
return {}
}
},
itemWidth: {
type: [Number, String],
default: 'auto'
},
isFixed: {
type: Boolean,
default: false
},
top: {
type: [Number, String],
default: 0
},
width: {
type: [Number, String],
default: '100%'
},
stickyBgColor: {
type: String,
default: '#ffffff'
},
borderRadius: {
type: [Number, String],
default: 0
},
async: {
type: Boolean,
default: false
}
},
provide() {
return {
tabs: this
}
},
data() {
return {
list: [],
scrollLeft: 0, // 滚动scroll-view的左边滚动距离
tabQueryInfo: [], // 存放对tab菜单查询后的节点信息
componentWidth: 0, // 屏幕宽度,单位为px
scrollBarLeft: 0, // 移动bar需要通过translateX()移动的距离
parentLeft: 0, // 父元素(tabs组件)到屏幕左边的距离
id: 'cu-tab', // id值
currentIndex: this.current,
barFirstTimeMove: true, // 滑块第一次移动时(页面刚生成时),无需动画,否则给人怪异的感觉
isAsync: false
};
},
watch: {
// 监听tab的变化,重新计算tab菜单的布局信息,因为实际使用中菜单可能是通过
// 后台获取的(如新闻app顶部的菜单),获取返回需要一定时间,所以list变化时,重新获取布局信息
list(n, o) {
// list变动时,重制内部索引,否则可能导致超出数组边界的情况
if (!this.barFirstTimeMove && n.length !== o.length) {
this.currentIndex = 0;
}
// 用$nextTick等待视图更新完毕后再计算tab的局部信息,否则可能因为tab还没生成就获取,就会有问题
this.$nextTick(() => {
this.init();
});
},
current: {
immediate: true,
handler(nVal, oVal) {
// 视图更新后再执行移动操作、
this.$nextTick(() => {
this.currentIndex = nVal;
this.scrollByIndex();
});
}
},
async: {
immediate: true,
handler(nVal, oVal) {
this.isAsync = nVal;
}
},
},
computed: {
// 移动bar的样式
tabBarStyle() {
let style = {
width: this.barWidth + 'rpx',
transform: `translate(${this.scrollBarLeft}px, -100%)`,
// 滑块在页面渲染后第一次滑动时,无需动画效果
'transition-duration': `${this.barFirstTimeMove ? 0 : this.duration }s`,
'background-color': this.activeColor,
height: this.barHeight + 'rpx',
opacity: this.barFirstTimeMove ? 0 : 1,
// 设置一个很大的值,它会自动取能用的最大值,不用高度的一半,是因为高度可能是单数,会有小数出现
'border-radius': `${this.barHeight / 2}px`
};
Object.assign(style, this.barStyle);
return style;
},
// tab的样式
tabItemStyle() {
return (index) => {
let style = {
height: this.height + 'rpx',
'line-height': this.height + 'rpx',
'font-size': this.fontSize + 'rpx',
padding: this.isScroll ? `0 ${this.gutter}rpx` : '',
flex: this.isScroll ? 'auto' : '1',
width: `${this.itemWidth}rpx`
};
// 字体加粗
if (index == this.currentIndex && this.bold) style.fontWeight = 'bold';
if (index == this.currentIndex) {
style.color = this.activeColor;
// 给选中的tab item添加外部自定义的样式
style = Object.assign(style, this.activeItemStyle);
} else {
style.color = this.inactiveColor;
}
return style;
}
}
},
methods: {
updateTabs() {
this.list = this.childrens.map((item) => {
const {
name,
dot,
active,
inited,
updateRender
} = item
return {
name,
dot,
active,
inited,
updateRender
}
})
this.$nextTick(function() {
this.init()
})
},
async init() {
let tabRect = await getRect('#' + this.id, false, this);
this.parentLeft = tabRect.left;
this.componentWidth = tabRect.width;
this.getTabRect();
},
clickTab(index) {
this.$emit('change', index);
if(this.isAsync) return;
if (index == this.currentIndex) return;
this.$nextTick(() => {
this.currentIndex = index;
this.scrollByIndex();
});
},
getTabRect() {
let query = uni.createSelectorQuery().in(this);
for (let i = 0; i < this.list.length; i++) {
query.select(`#tab-item-${i}`).fields({
size: true,
rect: true
});
}
query.exec(
function(res) {
this.tabQueryInfo = res;
this.scrollByIndex();
}.bind(this)
);
},
scrollByIndex() {
let tabInfo = this.tabQueryInfo[this.currentIndex];
if (!tabInfo) return;
let tabWidth = tabInfo.width;
let offsetLeft = tabInfo.left - this.parentLeft;
let scrollLeft = offsetLeft - (this.componentWidth - tabWidth) / 2;
this.scrollLeft = scrollLeft < 0 ? 0 : scrollLeft;
let left = tabInfo.left + tabInfo.width / 2 - this.parentLeft;
this.scrollBarLeft = left - uni.upx2px(this.barWidth) / 2;
if (this.barFirstTimeMove == true) {
setTimeout(() => {
this.barFirstTimeMove = false;
}, 100)
}
this.childrens.forEach((item, ind) => {
let active = ind === this.currentIndex;
if (active !== item.active || !item.inited) {
item.updateRender(active, this);
}
});
}
},
created() {
this.childrens = []
},
mounted() {
this.updateTabs();
}
};
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
::-webkit-scrollbar,
::-webkit-scrollbar,
::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
-webkit-appearance: none;
background: transparent;
}
/* #endif */
.scroll-box {
height: 100%;
position: relative;
/* #ifdef MP-TOUTIAO */
white-space: nowrap;
/* #endif */
}
.tab-fixed {
position: sticky;
top: 0;
width: 100%;
}
/* #ifdef H5 */
// 通过样式穿透,隐藏H5下,scroll-view下的滚动条
scroll-view ::v-deep ::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
-webkit-appearance: none;
background: transparent;
}
/* #endif */
.scroll-view {
width: 100%;
white-space: nowrap;
position: relative;
}
.tab-item {
position: relative;
/* #ifndef APP-NVUE */
display: inline-block;
/* #endif */
text-align: center;
transition-property: background-color, color;
}
.tab-bar {
position: absolute;
bottom: 6rpx;
}
.tabs-scorll-flex {
display: flex;
justify-content: space-between;
}
</style>