pan-mini/components/smart-tabs/smart-tabs.vue

436 lines
9.7 KiB
Vue

<template>
<view class="custom-tabs">
<!-- 选项卡头部 -->
<scroll-view
class="tabs-header"
:scroll-x="scrollEnable"
:scroll-left="scrollLeft"
scroll-with-animation
:show-scrollbar="scrollEnable"
:scroll-into-view="activeTabId"
:style="getScrollViewStyle()"
@scroll="handleScroll"
>
<view
v-for="(tab, index) in tabs"
:key="index"
class="smart-tab-item"
:id="`tab_${index}`"
:class="{ 'active': currentIndex === index }"
:style="getTabStyle(index)"
@click="handleTabClick(index)"
>
<text class="tab-text">{{ tab.label }}</text>
<view v-if="showBadge && tab.badge" class="tab-badge">
{{ tab.badge > 99 ? '99+' : tab.badge }}
</view>
</view>
</scroll-view>
</view>
</template>
<script>
export default {
name: 'SmartTabs',
props: {
// 是否支持滚动
scrollEnable:{
type: Boolean,
required: false,
default: false
},
// 选项卡配置
tabs: {
type: Array,
required: true,
default: () => []
},
// 初始选中的索引
initialIndex: {
type: Number,
default: 0
},
// 动画时长
duration: {
type: Number,
default: 300
},
// 是否显示徽章
showBadge: {
type: Boolean,
default: true
},
// 激活状态颜色
activeColor: {
type: String,
default: '#007AFF'
},
// 非激活状态颜色
inactiveColor: {
type: String,
default: '#333'
},
// 最小项宽度(用于平铺模式)
minItemWidth: {
type: Number,
default: 120 // rpx
}
},
data() {
return {
currentIndex: this.initialIndex,
scrollLeft: 0,
tabWidths: [],
containerWidth: 0,
activeTabId: `tab_${this.initialIndex}`,
canScrollLeft: false,
canScrollRight: false,
}
},
mounted() {
this.$nextTick(() => {
this.calculateLayout();
});
},
methods: {
// 计算布局
calculateLayout() {
this.getContainerWidth().then(() => {
this.calculateTabPositions().then(() => {
this.checkScrollability();
this.scrollToCurrentTab();
});
});
},
// 获取容器宽度
getContainerWidth() {
return new Promise((resolve) => {
const query = uni.createSelectorQuery().in(this);
query.select('.tabs-header').boundingClientRect();
query.exec(res => {
if (res && res[0]) {
this.containerWidth = res[0].width;
}
resolve();
});
});
},
// 计算选项卡位置
calculateTabPositions() {
return new Promise((resolve) => {
const query = uni.createSelectorQuery().in(this);
this.tabs.forEach((_, index) => {
query.select(`#tab_${index}`).boundingClientRect();
});
query.exec(res => {
this.tabWidths = res.map(item => item ? item.width : 0);
resolve();
});
});
},
// 检查是否需要滚动
checkScrollability() {
this.checkScrollState();
},
// 检查滚动状态
checkScrollState() {
if (!this.$props.scrollEnable) {
this.canScrollLeft = false;
this.canScrollRight = false;
return;
}
this.canScrollLeft = this.scrollLeft > 0;
const totalWidth = this.tabWidths.reduce((sum, width) => sum + width, 0);
this.canScrollRight = this.scrollLeft < totalWidth - this.containerWidth;
},
// 滚动到当前选项卡
scrollToCurrentTab() {
if (this.tabWidths.length === 0 || !this.$props.scrollEnable) return;
let totalWidth = 0;
for (let i = 0; i < this.currentIndex; i++) {
totalWidth += this.tabWidths[i];
}
// 计算滚动位置,使当前选项卡居中
const currentTabWidth = this.tabWidths[this.currentIndex];
const scrollLeft = totalWidth - (this.containerWidth - currentTabWidth) / 2;
this.scrollLeft = scrollLeft;
this.activeTabId = `tab_${this.currentIndex}`;
this.checkScrollState();
},
// 处理滚动事件
handleScroll(e) {
this.scrollLeft = e.detail.scrollLeft;
this.checkScrollState();
},
// 处理选项卡点击
handleTabClick(index) {
if (this.currentIndex !== index) {
this.currentIndex = index;
this.scrollToCurrentTab();
this.$emit('change', index);
}
},
// 处理滑动切换
handleSwiperChange(e) {
const index = e.detail.current;
if (this.currentIndex !== index) {
this.currentIndex = index;
this.scrollToCurrentTab();
this.$emit('change', index);
}
},
// 获取选项卡样式
getTabStyle(index) {
const isActive = this.currentIndex === index;
const style = {
color: isActive ? this.activeColor : this.inactiveColor,
fontWeight: isActive ? 'bold' : 'normal',
width: this.scrollEnable ? 'auto' : `${100 / this.tabs.length}%`,
};
// 如果不是滚动模式,设置固定宽度
if (!this.$props.scrollEnable) {
style.flex = '1';
style.minWidth = '0';
style.textAlign = 'center';
}
return style;
},
getScrollViewStyle() {
console.log("aaa",this.$props.scrollEnable)
const isActive = this.$props.scrollEnable;
const style = {
'--scroll-content-display': isActive ? 'block' : 'flex'
};
console.log("aaa",this.$props.scrollEnable)
return style
},
// 切换到指定选项卡
switchTo(index) {
if (index >= 0 && index < this.tabs.length && index !== this.currentIndex) {
this.currentIndex = index;
this.scrollToCurrentTab();
this.$emit('change', index);
}
},
// 向左滚动
scrollLeftByStep() {
this.scrollLeft = Math.max(0, this.scrollLeft - 100);
this.checkScrollState();
},
// 向右滚动
scrollRightByStep() {
const totalWidth = this.tabWidths.reduce((sum, width) => sum + width, 0);
const maxScroll = totalWidth - this.containerWidth;
this.scrollLeft = Math.min(maxScroll, this.scrollLeft + 100);
this.checkScrollState();
}
},
watch: {
initialIndex(newVal) {
if (newVal !== this.currentIndex) {
this.currentIndex = newVal;
this.scrollToCurrentTab();
}
},
tabs() {
this.$nextTick(() => {
this.calculateLayout();
});
}
}
}
</script>
<style lang="scss" scoped>
.custom-tabs {
display: flex;
flex-direction: column;
height: 100%;
}
.tabs-header {
display: flex;
justify-content:center;
white-space: nowrap;
padding: 10rpx 0;
background-color: #fff;
border-bottom: 1rpx solid #eee;
position: relative;
z-index: 10;
/* #ifdef MP-WEIXIN */
scrollbar-width: none;
-ms-overflow-style: none;
/* #endif */
// 滚动模式下的阴影效果
&.scrollable {
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 20rpx;
height: 100%;
background: linear-gradient(to right, rgba(255,255,255,1), rgba(255,255,255,0));
pointer-events: none;
z-index: 20;
}
&::after {
content: '';
position: absolute;
top: 0;
right: 0;
width: 20rpx;
height: 100%;
background: linear-gradient(to left, rgba(255,255,255,1), rgba(255,255,255,0));
pointer-events: none;
z-index: 20;
}
}
}
/* #ifdef MP-WEIXIN */
.tabs-header ::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
color: transparent;
}
/* #endif */
.smart-tab-item {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 12rpx 30rpx;
font-size: 28rpx;
position: relative;
transition: all 0.3s ease;
box-sizing: border-box;
// 平铺模式下的样式
.tabs-header:not(.scrollable) & {
flex: 1;
min-width: 0;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
justify-content: center;
}
&.active {
.tab-text {
position: relative;
&::after {
content: '';
position: absolute;
bottom: -10rpx;
left: 0;
right: 0;
height: 4rpx;
background-color: v-bind(activeColor);
border-radius: 2rpx;
}
}
}
}
/* #ifdef H5 */
::v-deep .tabs-header .uni-scroll-view-content {
// display: flex;
display: var(--scroll-content-display, block);
}
/* #endif */
/* 微信小程序需要特殊处理 */
/* #ifdef MP-WEIXIN */
.tabs-header .uni-scroll-view-content {
display: var(--scroll-content-display, block) !important;
}
/* #endif */
/* #ifdef MP-ALIPAY */
.tabs-header ::-webkit-scroll-view-content {
display: var(--scroll-content-display, block);
}
/* #endif */
.tab-badge {
position: absolute;
top: -10rpx;
right: 10rpx;
min-width: 36rpx;
height: 36rpx;
padding: 0 6rpx;
background-color: #ff3b30;
color: #fff;
font-size: 20rpx;
border-radius: 18rpx;
display: flex;
align-items: center;
justify-content: center;
transform: scale(0.85);
}
.tabs-content {
flex: 1;
height: 0; // 解决部分平台高度问题
}
// 滚动提示按钮
.scroll-button {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 40rpx;
height: 40rpx;
background-color: rgba(0,0,0,0.3);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
z-index: 30;
cursor: pointer;
&.left {
left: 10rpx;
}
&.right {
right: 10rpx;
}
.icon {
color: white;
font-size: 24rpx;
}
}
</style>