chore: initialize qiming workspace repository
This commit is contained in:
347
qiming-mobile/uni_modules/lime-tabs/components/l-tabs/l-tabs.vue
Normal file
347
qiming-mobile/uni_modules/lime-tabs/components/l-tabs/l-tabs.vue
Normal file
@@ -0,0 +1,347 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user