Files
qiming/qiming-mobile/uni_modules/lime-tabs/components/l-tabs/l-tabs.vue

347 lines
11 KiB
Vue
Raw Permalink 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="l-tabs">
<view class="l-tabs__wrap" :style="[styles, lStyle]">
<slot name="left"></slot>
<scroll-view class="l-tabs__scroll" :class="{'l-tabs__scroll--split': split}" enhanced enable-flex
:scroll-left="scrollLeft" scroll-x scroll-anchoring scroll-with-animation enable-passive
:show-scrollbar="false" @scroll="onScroll">
<view class="l-tabs__nav">
<view class="l-tabs__item"
:style="{color: !item.disabled && (index == currentIndex ? activeColor : color)}" :class="[
'l-tabs__item-text--' + size,
{
'l-tabs__item--active': index == currentIndex,
'l-tabs__item--evenly': spaceEvenly,
'l-tabs__item--disabled': item.disabled
}
]" v-for="(item, index) in tabs" :key="index" @tap="onClick(index, item)" aria-role="tab">
<slot name="label" :item="item" :active="index == currentIndex" :disabled="item.disabled">
<l-badge v-if="item.dot == true || item.badge" :dot="item.dot" :offset="item.offset"
:content="item.badge">
<view class="l-tabs__item-text l-ellipsis">
{{item.label}}
</view>
</l-badge>
<view v-else class="l-tabs__item-text l-ellipsis">
{{item.label}}
</view>
</slot>
</view>
<view class="l-tabs__track" :style="[trackStyle]"></view>
</view>
</scroll-view>
<slot name="right"></slot>
</view>
<view class="l-tabs__content" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd"
@touchcancel="onTouchEnd" :class="{'l-tabs__content--animated': animated}" v-if="$slots['default']">
<view class="l-tabs__content-inner" :style="[innerStyle]" ref="innerRef">
<slot></slot>
</view>
</view>
</view>
</template>
<script lang="ts">
// @ts-nocheck
/**
* Tabs 标签页组件
* @description 用于内容分类展示和切换,支持多种样式和交互效果
* <br>插件类型LTabsComponentPublicInstance
* @tutorial https://ext.dcloud.net.cn/plugin?name=lime-tabs
*
* @property {TabPanel[]} list 选项卡配置数组
* @property {boolean} ellipsis 是否省略超长文本默认true
* @property {boolean} animated 是否启用切换动画默认false
* @property {number} duration 动画时长ms默认300
* @property {boolean} showLine 显示底部激活线条默认true
* @property {'medium' | 'large'} size 组件尺寸(默认:"medium"
* @value medium
* @value large
* @property {boolean} spaceEvenly 均分选项卡宽度默认false
* @property {boolean} swipeable 支持滑动切换默认true
* @property {number} value 当前激活选项卡索引支持v-model
* @property {string} color 默认文字颜色(默认:"#333"
* @property {string} activeColor 激活状态颜色(默认:主题色)
* @property {string} lineColor 激活线条颜色默认跟随activeColor
* @property {string} lineWidth 激活线条宽度(默认:"20px"
* @property {string} lineHeight 激活线条高度(默认:"3px"
* @property {string} bgColor 背景颜色
* @property {string} fontSize 文字大小支持CSS单位
* @property {string} padding 内边距支持CSS简写
* @property {boolean} split 显示分隔线默认false
* @property {boolean} visible 是否显示组件默认true
* @event {Function} change 选项卡切换时触发(返回激活索引)
* @event {Function} click 点击时触发(返回激活索引)
*/
import { defineComponent, ref, provide, reactive, getCurrentInstance, watch, watchEffect, computed, type ComputedRef, onUnmounted, nextTick, onMounted } from '@/uni_modules/lime-shared/vue';
import tabsProps from './props'
import { TabPanel } from './type';
import { getRect, getAllRect } from '@/uni_modules/lime-shared/getRect'
import { clamp } from '@/uni_modules/lime-shared/clamp'
import { calcScrollOffset, ease, calculateTabPositions } from './utils'
import { useTouch } from './touch';
// import { onReady } from '@dcloudio/uni-app'
export default defineComponent({
name: 'l-tabs',
props: tabsProps,
options: {
addGlobalClass: true,
virtualHost: true,
},
emits: ['update:modelValue', 'change', 'input', 'click', 'total-width'],
setup(props, { emit }) {
const context = getCurrentInstance().proxy!
const scrollLeft = ref(0)
const lastLeft = ref(0)
const trackDistance = ref(0)
const children = ref([])
const innerStyle = reactive({
width: '',
'transition-duration': ``,
'transform': ``,
})
const defaultValue = ref(props.value ?? props.modelValue ?? 0)
const currentValue = computed({
set(value : number) {
defaultValue.value = value
emit('update:modelValue', value)
emit('change', value)
// #ifdef VUE2
emit('input', value)
// #endif
},
get() : number {
return props.value ?? props.modelValue ?? defaultValue.value
}
} as WritableComputedOptions<number>);
const styles = computed(() => {
const style : Record<string, any> = {};
if (props.bgColor) {
style['--l-tab-nav-bg-color'] = props.bgColor
}
if (!['medium', 'large'].includes(props.size)) {
style['--l-tab-item-height'] = props.size
}
if (props.padding) {
style['--l-tab-item-padding'] = props.padding
}
if (props.fontSize) {
style['--l-tab-font-size'] = props.fontSize
}
return style
})
const trackStyle = computed(() : Map<string, any> => {
const style : Record<string, any> = {
'-webkit-transform': `translateX(${trackDistance.value}px)`,
'transform': `translateX(${trackDistance.value}px)`,
};
if (props.lineColor) {
style['background'] = props.lineColor!
}
if (props.lineWidth) {
style['width'] = props.lineWidth!
}
if (props.lineHeight) {
style['height'] = props.lineHeight!
}
return style
})
const tabs : ComputedRef<TabPanel[]> = computed(() => {
if (props.list) return props.list as TabPanel[]
return children.value
})
const currentIndex = computed(() => {
const index = tabs.value.findIndex((child, index) => (child.value || index) == currentValue.value)
if (index >= 0) return index
return 0
})
const tabRects = ref<DOMRect[]>([]) // 存储每个tab的尺寸位置
const totalTabsWidth = computed(() : number => {
if (tabRects.value.length == 0) return 0
return tabRects.value.reduce((sum : number, rect : DOMRect) : number => sum + rect.width, 0)
})
const moveToActiveTab = () => {
nextTick(() => {
try {
Promise.all([
getRect(`.l-tabs__scroll`, context),
getRect(`.l-tabs__track`, context),
getAllRect(`.l-tabs__item`, context)
]).then(([scrollRect, trackRect, tabsRects]) => {
const index = currentIndex.value
const tabRect = tabsRects[index];
tabRects.value = tabsRects
if (!tabRect) return;
let [distance] = calculateTabPositions(tabsRects, index)
if (totalTabsWidth.value == 0) return
const offset = calcScrollOffset(scrollRect.width, tabRect.left, tabRect.width, lastLeft.value);
const maxOffset = totalTabsWidth.value - scrollRect.width;
scrollLeft.value = clamp(offset, 0, maxOffset)
// distance += (tabRect.width - trackRect.width) / 2;
trackDistance.value = distance + (tabRect.width - trackRect.width) / 2;
})
} catch (e) {
console.error(e)
}
})
}
const updateInnerStyle = (offset : number) => {
if (props.list && props.list.length > 0) return
nextTick(() => {
Promise.all(
[
getRect('.l-tabs__content', context),
getRect('.l-tabs__content-inner', context)
]).then(([parent, inner]) => {
const left = -parent.width * currentIndex.value + offset;
if (offset != 0) {
innerStyle.transform = `translateX(${left}px)`
innerStyle['transition-duration'] = `0s`
} else {
if (props.animated) {
innerStyle['transition-duration'] = offset != 0 || !props.animated ? '0s' : `${props.duration}s`
}
innerStyle.transform = `translateX(${left}px)`
}
})
})
}
const onScroll = (e) => {
lastLeft.value = e.detail.scrollLeft
}
const onClick = (index : number, item : TabPanel) => {
const { value = index, disabled, label } = item;
if (disabled || currentValue.value === value) { return }
currentValue.value = value
emit('click', value)
moveToActiveTab()
}
const getAvailableTabIndex = (deltaX : number) : number => {
const step = deltaX > 0 ? -1 : 1;
const len = tabs.value.length;
for (let i = step; currentIndex.value + step >= 0 && currentIndex.value + step < len; i += step) {
const newIndex = currentIndex.value + i;
if (newIndex >= 0 && newIndex < len && tabs.value.length > newIndex && !tabs.value[newIndex].disabled) {
return newIndex;
}
}
return -1;
}
const touch = useTouch();
const onTouchStart = (event : UniTouchEvent) => {
if (!props.swipeable) return;
touch.start(event);
}
const onTouchMove = (event : UniTouchEvent) => {
if (!props.swipeable) return;
touch.move(event);
const { direction, deltaX, startX } = touch
if (direction.value != 'horizontal') return
if (!props.animated) return
const isAtFirstTab = currentIndex.value == 0;
const isAtLastTab = currentIndex.value == tabs.value.length - 1;
if ((isAtFirstTab && deltaX.value > 0) || (isAtLastTab && deltaX.value < 0)) {
const base = isAtFirstTab ? 1 : -1;
const adjustedDelta = ease(deltaX.value, base);
updateInnerStyle(adjustedDelta);
} else {
updateInnerStyle(deltaX.value);
}
}
const onTouchEnd = () => {
if (!props.swipeable) return;
const { direction, deltaX, offsetX } = touch
const minSwipeDistance = 50;
if (direction.value == 'horizontal' && offsetX.value >= minSwipeDistance) {
const index = getAvailableTabIndex(deltaX.value);
if (index != -1) {
onClick(index, tabs.value[index])
}
}
updateInnerStyle(0)
}
watchEffect(() => {
// console.log('tabs', tabs.value)
if (tabs.value.length == 0) return
moveToActiveTab()
})
// const stopWatch = watch(tabs, (v) => {
// moveToActiveTab()
// })
const stopValueWatch = watch(currentValue, (_v : number) => {
moveToActiveTab()
updateInnerStyle(0)
})
const stopVisibleWatch = watch(() => props.visible, (v) => {
if (!v) return
setTimeout(() => {
moveToActiveTab()
updateInnerStyle(0)
}, 100)
})
// 添加监听器当总宽度变化时emit事件
watch(totalTabsWidth, (newWidth : number, oldWidth : number) => {
if (newWidth !== oldWidth && newWidth > 0) {
emit('total-width', newWidth)
}
})
onMounted(() => {
nextTick(() => {
setTimeout(() => {
if (tabs.value.length == 0) return
moveToActiveTab()
updateInnerStyle(0)
}, 100)
})
})
onUnmounted(() => {
// stopWatch()
stopVisibleWatch()
stopValueWatch()
})
provide('LimeTabs', children)
return {
styles,
tabs,
scrollLeft,
currentValue,
currentIndex,
trackStyle,
innerStyle,
onScroll,
onClick,
onTouchStart,
onTouchMove,
onTouchEnd
}
}
})
</script>
<style lang="scss">
@import './index';
</style>