货无忧
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

239 lines
5.7 KiB

2 years ago
<template>
<view class="hd-tabs" @touchmove.prevent>
<scroll-view scroll-x="true" scroll-with-animation :scroll-left="tabsScrollLeft" @scroll="scroll" class="hd-tabs--wrap" @touchmove.prevent>
<view class="hd-tab" :id="tabId + 'tab_list'" @touchmove.prevent>
<view
v-for="(item, index) in tabList"
:key="index"
:class="['hd-tab-item', { 'hd-tab-item--active': currentIndex === index }]"
:style="{ color: currentIndex === index ? `${activeColor}` : '' }"
:id="tabId + 'tab_item'"
@click="select(item, index)"
@touchmove.prevent
>
{{ item.title }}
</view>
</view>
<view class="hd-tab-line-wrap">
<view
class="hd-tab-line"
:style="{
background: lineColor,
width: lineStyle.width,
transform: lineStyle.transform || `translateX(calc(${100 / tabList.length / 2}vw - ${lineWidth / 2}px))`,
transitionDuration: lineStyle.transitionDuration
}"
></view>
</view>
</scroll-view>
<slot></slot>
</view>
</template>
<script lang="ts" setup>
import { getCurrentInstance, nextTick, onMounted, provide, ref, watch } from 'vue'
interface Props {
/**
* 选中项
*/
modelValue: number
/**
* tab选中颜色
*/
activeColor?: string
/**
* 选中项下划线颜色
*/
lineColor?: string
/**
* 下划线长度
*/
lineWidth?: number
/**
* 是否展示下划线动画
*/
lineAnimated?: boolean
}
const props = withDefaults(defineProps<Props>(), {
modelValue: 0,
activeColor: '',
lineColor: '',
lineWidth: 0,
lineAnimated: true
})
const currentIndex = ref<number>(0) // 当前选中项
const lineStyle = ref<any>({}) // 当前选中项
const scrollLeft = ref<number>(0) // 当前选中项
const tabList = ref<any>([]) // tab列表
const tabsScrollLeft = ref<number>(0) // 当前选中项
const tabId = ref<string>('') // 当前选中项
const duration = ref<number>(0.3) // 当前选中项
const { proxy } = getCurrentInstance() as any
provide('$tabList', tabList)
provide('$current', currentIndex)
watch(tabList, () => {
setTabList()
})
watch(
() => props.modelValue,
() => {
currentIndex.value = props.modelValue
setTabList()
}
)
const emit = defineEmits(['update:modelValue', 'change'])
onMounted(() => {
currentIndex.value = props.modelValue
setTabList()
if (!props.lineAnimated) {
duration.value = 0
}
})
function select(item, index) {
if (!item.title) {
return
}
emit('update:modelValue', index)
if (index !== currentIndex.value) {
// 选中项改变时触发
// @arg value:选择器的值,建议通过v-model双向绑定输入值,而不是监听此事件的形式
emit('change', index)
}
}
function setTabList() {
nextTick(() => {
if (tabList.value.length > 0) {
nextTick(() => {
setLine()
scrollIntoView()
})
}
})
}
function setLine() {
let lineWidth = 0
let lineLeft = 0
getElementData(`#${tabId.value}tab_item`, (data) => {
const el = data[currentIndex.value]
if (props.lineWidth > 0 && props.lineWidth <= el.width) {
lineWidth = props.lineWidth
} else {
lineWidth = el.width / 5
}
// lineLeft = el.width * (currentIndex.value + 0.5) // 此种只能针对每个item长度一致的
lineLeft = (el.width - lineWidth) / 2 + -data[0].left + el.left
lineStyle.value = {
width: `${lineWidth}px`,
transform: `translateX(${lineLeft}px) `,
transitionDuration: `${duration.value}s`
}
})
}
function scrollIntoView() {
// item滚动
let lineLeft = 0
getElementData(`#${tabId.value}tab_list`, (data) => {
const list = data[0]
getElementData(`#${tabId.value}tab_item`, (data) => {
const el = data[currentIndex.value]
// lineLeft = el.width * (currentIndex.value + 0.5) - list.width / 2 - scrollLeft.value
lineLeft = el.width / 2 + -list.left + el.left - list.width / 2 - scrollLeft.value
tabsScrollLeft.value = scrollLeft.value + lineLeft
})
})
}
function getElementData(el, callback) {
// eslint-disable-next-line no-undef
uni
.createSelectorQuery()
// #ifndef MP-ALIPAY
.in(proxy)
// #endif
.selectAll(el)
.boundingClientRect()
.exec((data) => {
callback(data[0])
})
}
function scroll(e) {
scrollLeft.value = e.detail.scrollLeft
}
defineExpose({
tabList
})
</script>
<style lang="scss">
.hd-tabs {
position: relative;
background: #fff;
&--wrap {
position: relative;
.hd-tab {
position: relative;
display: flex;
font-size: 28rpx;
white-space: nowrap;
&-item {
flex: 1;
min-width: 22%;
box-sizing: border-box;
padding: 0 10rpx;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-align: center;
line-height: 100rpx;
font-size: 28rpx;
color: #666666;
&--active {
font-weight: 500;
font-size: 32rpx;
color: #333333;
}
}
}
.hd-tab-line-wrap {
width: 100%;
position: relative;
.hd-tab-line {
display: block;
height: 8rpx;
width: 64rpx;
position: absolute;
bottom: 8rpx;
left: 0;
transform: translateY(-50%);
z-index: 1;
background: $color-primary;
border-radius: 4rpx;
}
}
}
&--wrap::after {
position: absolute;
box-sizing: border-box;
content: ' ';
pointer-events: none;
right: 0;
bottom: 0;
left: 0;
border-bottom: 2rpx solid #ebedf0;
-webkit-transform: scaleY(0.5);
transform: scaleY(0.5);
}
}
</style>