217 lines
4.6 KiB
Vue
217 lines
4.6 KiB
Vue
<template>
|
||
<view class="tab-container">
|
||
<!-- tab标题栏 -->
|
||
<scroll-view
|
||
scroll-x
|
||
class="tab-scroll-view"
|
||
:scroll-left="scrollLeft"
|
||
scroll-with-animation
|
||
show-scrollbar="false"
|
||
:id="tabScrollId"
|
||
>
|
||
<view class="tab-items-container">
|
||
<view
|
||
v-for="(item, index) in tabs"
|
||
:key="index"
|
||
class="tab-item"
|
||
:class="{ active: currentIndex === index }"
|
||
@click="handleTabClick(index)"
|
||
:id="`tab-item-${index}`"
|
||
>
|
||
<text class="tab-text">{{ item.title }}</text>
|
||
</view>
|
||
|
||
<!-- 独立的滑块元素 -->
|
||
<view
|
||
class="tab-line"
|
||
:style="{
|
||
transform: `translateX(${lineLeft}px)`,
|
||
width: `${lineWidth}rpx`,
|
||
backgroundColor: props.themeColor,
|
||
}"
|
||
></view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, watch, nextTick, onMounted } from 'vue'
|
||
|
||
// 唯一ID,防止多个Tab组件冲突
|
||
const tabScrollId = `tab-scroll-${Date.now()}`
|
||
|
||
// 定义组件属性
|
||
const props = defineProps({
|
||
// tabs数据,格式:[{title: '标签1'}, {title: '标签2'}]
|
||
tabs: {
|
||
type: Array,
|
||
default: () => [],
|
||
},
|
||
// 默认选中的索引
|
||
modelValue: {
|
||
type: Number,
|
||
default: 0,
|
||
},
|
||
// 主题色
|
||
themeColor: {
|
||
type: String,
|
||
default: '#3C9CFD',
|
||
},
|
||
// 滑块宽度
|
||
lineWidth: {
|
||
type: [Number, String],
|
||
default: 48,
|
||
},
|
||
})
|
||
|
||
// 定义事件
|
||
const emit = defineEmits(['update:modelValue', 'change'])
|
||
|
||
// 当前激活的索引
|
||
const currentIndex = ref(props.modelValue)
|
||
|
||
// 滚动位置
|
||
const scrollLeft = ref(0)
|
||
|
||
// 滑块位置
|
||
const lineLeft = ref(0)
|
||
|
||
// 监听props变化
|
||
watch(
|
||
() => props.modelValue,
|
||
(newVal) => {
|
||
if (currentIndex.value !== newVal) {
|
||
currentIndex.value = newVal
|
||
updateTabPosition()
|
||
}
|
||
},
|
||
)
|
||
|
||
// 监听currentIndex变化
|
||
watch(
|
||
() => currentIndex.value,
|
||
(newVal, oldVal) => {
|
||
if (newVal !== oldVal) {
|
||
updateTabPosition()
|
||
// 向父组件同步更新
|
||
emit('update:modelValue', newVal)
|
||
emit('change', {
|
||
index: newVal,
|
||
item: props.tabs[newVal] || {},
|
||
})
|
||
}
|
||
},
|
||
)
|
||
|
||
// 监听tabs变化,重新计算滑块位置
|
||
watch(
|
||
() => props.tabs,
|
||
() => {
|
||
nextTick(() => {
|
||
updateTabPosition()
|
||
})
|
||
},
|
||
{ deep: true },
|
||
)
|
||
|
||
// 组件挂载后初始化
|
||
onMounted(() => {
|
||
nextTick(() => {
|
||
updateTabPosition()
|
||
})
|
||
})
|
||
|
||
const instance = getCurrentInstance()
|
||
|
||
// 更新标签位置和滑块位置 - 优化版
|
||
const updateTabPosition = () => {
|
||
nextTick(() => {
|
||
// 创建查询对象
|
||
const query = uni.createSelectorQuery().in(instance)
|
||
|
||
// 获取滚动视图和当前选中标签的信息
|
||
query.select(`#${tabScrollId}`).boundingClientRect()
|
||
query.select(`#tab-item-${currentIndex.value}`).boundingClientRect()
|
||
|
||
query.exec((res) => {
|
||
if (res && res[0] && res[1]) {
|
||
const scrollView = res[0]
|
||
const currentTab = res[1]
|
||
|
||
// 1. 计算滑块位置 - 直接使用当前标签的中心位置
|
||
const tabCenter = currentTab.left + currentTab.width / 2 - scrollView.left
|
||
|
||
// 2. 计算滑块左侧应该在的位置(居中)
|
||
const lineWidthPx = uni.upx2px(Number(props.lineWidth))
|
||
lineLeft.value = tabCenter - lineWidthPx / 2
|
||
|
||
// 3. 计算滚动位置,使选中的标签居中显示在滚动视图中
|
||
const offsetLeft = currentTab.left - scrollView.left
|
||
scrollLeft.value = offsetLeft - scrollView.width / 2 + currentTab.width / 2
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
// 点击标签切换
|
||
const handleTabClick = (index) => {
|
||
if (currentIndex.value !== index) {
|
||
currentIndex.value = index
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.tab-container {
|
||
width: 100%;
|
||
}
|
||
|
||
.tab-scroll-view {
|
||
white-space: nowrap;
|
||
width: 100%;
|
||
height: 88rpx;
|
||
background-color: #ffffff;
|
||
border-bottom: 1rpx solid #f5f5f5;
|
||
position: relative;
|
||
}
|
||
|
||
.tab-items-container {
|
||
display: inline-flex;
|
||
height: 100%;
|
||
position: relative;
|
||
width: 100%;
|
||
justify-content: space-around;
|
||
}
|
||
|
||
.tab-item {
|
||
display: inline-flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0 32rpx;
|
||
position: relative;
|
||
height: 100%;
|
||
}
|
||
|
||
.tab-text {
|
||
font-size: 28rpx;
|
||
color: #333333;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.tab-item.active .tab-text {
|
||
color: v-bind('props.themeColor');
|
||
}
|
||
|
||
.tab-line {
|
||
position: absolute;
|
||
height: 6rpx;
|
||
border-radius: 6rpx;
|
||
bottom: 0;
|
||
left: 0;
|
||
/* 平滑过渡效果 */
|
||
transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||
}
|
||
</style>
|