chore: initialize qiming workspace repository
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
/* #ifdef MP */
|
||||
:host {
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
.l-tab__panel {
|
||||
width: 100%;
|
||||
/* #ifdef UNI-APP-X */
|
||||
flex: 1;
|
||||
/* #endif */
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
/* #ifndef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
/* #endif */
|
||||
// &--active {
|
||||
// height: auto;
|
||||
// }
|
||||
|
||||
// &--inactive {
|
||||
// height: 0;
|
||||
// overflow: visible;
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<view class="l-tab__panel" aria-role="tabpanel">
|
||||
<slot/>
|
||||
</view>
|
||||
</template>
|
||||
<script lang="uts" setup>
|
||||
/**
|
||||
* TabPanel 标签页面板组件
|
||||
* @description 用于构建Tabs组件的单个内容面板,必须作为Tabs的子组件使用
|
||||
* <br>插件类型:LTabPanelComponentPublicInstance
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?name=lime-tabs
|
||||
*
|
||||
* @property {any} badge 徽标配置
|
||||
* @property {number[]} offset 徽标位置偏移量[x,y]
|
||||
* @property {boolean} dot 是否显示圆点徽标(默认:false)
|
||||
* @property {boolean} destroyOnHide 内容隐藏时销毁DOM(默认:false)
|
||||
* @property {boolean} disabled 禁用当前选项卡(默认:false)
|
||||
* @property {string} label 选项卡标题内容
|
||||
* @property {boolean} lazy 启用懒加载(默认:false)
|
||||
* @property {number} value 选项卡唯一标识
|
||||
*/
|
||||
|
||||
import { TabPanelProps } from './type';
|
||||
import { TabPanel } from '../l-tabs/type'
|
||||
const props = withDefaults(defineProps<TabPanelProps>(), {})
|
||||
const children = inject<LTabPanelComponentPublicInstance[]|null>('LimeTabs', null) as Ref<LTabPanelComponentPublicInstance[]>|null;
|
||||
const instance = getCurrentInstance()!.proxy!
|
||||
onMounted(()=>{
|
||||
if(children == null) return
|
||||
children.value.push(instance as LTabPanelComponentPublicInstance)
|
||||
})
|
||||
|
||||
|
||||
// #ifdef APP-ANDROID
|
||||
// 安卓端数组属性存在BUG 死循环
|
||||
const innderOffset = ref<any[]>([])
|
||||
watch((): any[]|null => props.offset, (n: any[]|null) => {
|
||||
if(innderOffset.value.join('') == n?.join('')) return
|
||||
innderOffset.value = n ?? []
|
||||
}, {immediate: true})
|
||||
|
||||
defineExpose({
|
||||
innderOffset
|
||||
})
|
||||
// #endif
|
||||
|
||||
|
||||
onUnmounted(()=>{
|
||||
if(children == null) return
|
||||
children.value = children.value.filter((it):boolean => it != (instance))
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import './index';
|
||||
</style>
|
||||
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<view class="l-tab__panel" aria-role="tabpanel">
|
||||
<slot/>
|
||||
</view>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* TabPanel 标签页面板组件
|
||||
* @description 用于构建Tabs组件的单个内容面板,必须作为Tabs的子组件使用
|
||||
* <br>插件类型:LTabPanelComponentPublicInstance
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?name=lime-tabs
|
||||
*
|
||||
* @property {any} badge 徽标配置
|
||||
* @property {number[]} offset 徽标位置偏移量[x,y]
|
||||
* @property {boolean} dot 是否显示圆点徽标(默认:false)
|
||||
* @property {boolean} destroyOnHide 内容隐藏时销毁DOM(默认:false)
|
||||
* @property {boolean} disabled 禁用当前选项卡(默认:false)
|
||||
* @property {string} label 选项卡标题内容
|
||||
* @property {boolean} lazy 启用懒加载(默认:false)
|
||||
* @property {number} value 选项卡唯一标识
|
||||
*/
|
||||
import {onMounted, defineComponent, inject, onUnmounted} from '@/uni_modules/lime-shared/vue';
|
||||
import panelProps from './props';
|
||||
export default defineComponent({
|
||||
name: 'l-tab-panel',
|
||||
props: panelProps,
|
||||
setup(props) {
|
||||
const children = inject<LTabPanelComponentPublicInstance[]|null>('LimeTabs', null) as Ref<LTabPanelComponentPublicInstance[]>|null;
|
||||
onMounted(()=>{
|
||||
if(!children) return
|
||||
children.value.push(props)
|
||||
})
|
||||
|
||||
onUnmounted(()=>{
|
||||
if(!children) return
|
||||
children.value = children.value.filter((it):boolean => it != props)
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import './index';
|
||||
</style>
|
||||
@@ -0,0 +1,48 @@
|
||||
export default {
|
||||
/**
|
||||
* 透传至 Badge 组件
|
||||
*/
|
||||
badge: {
|
||||
type: [String, Number]
|
||||
},
|
||||
offset: {
|
||||
type: Array
|
||||
},
|
||||
dot: {
|
||||
type: Boolean,
|
||||
defalut: false
|
||||
},
|
||||
/**
|
||||
* 选项卡内容隐藏时是否销毁
|
||||
*/
|
||||
destroyOnHide: {
|
||||
type: Boolean,
|
||||
defalut: false
|
||||
},
|
||||
/**
|
||||
* 是否禁用当前选项卡
|
||||
*/
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
defalut: false
|
||||
},
|
||||
/**
|
||||
* 选项卡名称,可自定义选项卡导航内容
|
||||
*/
|
||||
label: {
|
||||
type: String
|
||||
},
|
||||
/**
|
||||
* 是否启用选项卡懒加载
|
||||
*/
|
||||
lazy: {
|
||||
type: Boolean,
|
||||
defalut: false
|
||||
},
|
||||
/**
|
||||
* 选项卡的值,唯一标识
|
||||
*/
|
||||
value: {
|
||||
type: [Number, String]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// @ts-nocheck
|
||||
export interface TabPanelProps {
|
||||
/**
|
||||
* 透传至 Badge 组件
|
||||
*/
|
||||
badge?: any;
|
||||
offset?: any[];
|
||||
dot?: boolean;
|
||||
/**
|
||||
* 选项卡内容隐藏时是否销毁
|
||||
*/
|
||||
destroyOnHide?: boolean;
|
||||
/**
|
||||
* 是否禁用当前选项卡
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* 选项卡名称,可自定义选项卡导航内容
|
||||
*/
|
||||
label?: string;
|
||||
/**
|
||||
* 是否启用选项卡懒加载
|
||||
*/
|
||||
lazy?: boolean;
|
||||
/**
|
||||
* 选项卡的值,唯一标识
|
||||
*/
|
||||
value?: number;
|
||||
}
|
||||
|
||||
217
qiming-mobile/uni_modules/lime-tabs/components/l-tabs/index.scss
Normal file
217
qiming-mobile/uni_modules/lime-tabs/components/l-tabs/index.scss
Normal file
@@ -0,0 +1,217 @@
|
||||
@import '@/uni_modules/lime-style/index.scss';
|
||||
@import '@/uni_modules/lime-style/mixins/hairline.scss';
|
||||
|
||||
/* #ifdef uniVersion >= 4.75 */
|
||||
$use-css-var: true;
|
||||
/* #endif */
|
||||
|
||||
$tabs: l-tabs;
|
||||
$item: l-tabs__item;
|
||||
|
||||
$text-color-disabled: create-var(text-color-disabled, $text-color-4);
|
||||
|
||||
$tab-font-size: create-var(tab-font-size, 14px);
|
||||
$tab-large-font-size: create-var(tab-font-size, 16px);
|
||||
$tab-nav-bg-color: create-var(tab-nav-bg-color, $bg-color-container);
|
||||
$tab-content-bg-color: create-var(tab-content-bg-color, $bg-color-container);
|
||||
|
||||
$tab-item-height: create-var(tab-item-height, 48px);
|
||||
$tab-item-padding-x: create-var(tab-item-padding-x, 16px);
|
||||
$tab-item-padding-y: create-var(tab-item-padding-y, 0);
|
||||
|
||||
$tab-item-color: create-var(tab-item-color, $text-color-1);
|
||||
$tab-item-active-color: create-var(tab-item-active-color, $primary-color);
|
||||
$tab-item-disabled-color: create-var(tab-item-disabled-color, $text-color-4);
|
||||
// track
|
||||
$tab-split-color: create-var(tab-split-color, $border-color-2);
|
||||
$tab-track-color: create-var(tab-track-color, $primary-color);
|
||||
$tab-track-height: create-var(tab-track-height, 3px);
|
||||
$tab-track-width: create-var(tab-track-width, 16px);
|
||||
$tab-track-radius: create-var(tab-track-radius, 4px);
|
||||
|
||||
.#{$tabs} {
|
||||
position: relative;
|
||||
/* #ifndef UNI-APP-X & APP */
|
||||
font-size: $tab-font-size;
|
||||
/* #endif */
|
||||
// background-color: $tab-nav-bg-color;
|
||||
// flex-wrap: wrap;
|
||||
&__wrap {
|
||||
/* #ifndef UNI-APP-X */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex: 1;
|
||||
background-color: $tab-nav-bg-color;
|
||||
flex-direction: row;
|
||||
}
|
||||
&__scroll {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
/* #ifdef UNI-APP-X & APP || UNI-APP-X & WEB */
|
||||
flex-direction: row;
|
||||
/* #endif */
|
||||
/* #ifndef UNI-APP-X & APP */
|
||||
:deep(.uni-scroll-view) {
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
// &::-webkit-scrollbar {
|
||||
// display: none;
|
||||
// width: 0 !important;
|
||||
// height: 0 !important;
|
||||
// -webkit-appearance: none;
|
||||
// background: transparent;
|
||||
// }
|
||||
/* #endif */
|
||||
|
||||
&--split {
|
||||
/* #ifndef UNI-APP-X & APP */
|
||||
&:after {
|
||||
@include hairline-bottom($tab-split-color);
|
||||
}
|
||||
/* #endif */
|
||||
/* #ifdef UNI-APP-X & APP */
|
||||
// border-bottom: 1rpx solid $tab-split-color;
|
||||
border-bottom-color: $tab-split-color;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-width: 0.7px;
|
||||
/* #endif */
|
||||
}
|
||||
}
|
||||
&__nav {
|
||||
/* #ifndef UNI-APP-X */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
/* #ifndef UNI-APP-X & APP */
|
||||
user-select: none;
|
||||
min-width: fit-content;
|
||||
width: 100%;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
position: relative;
|
||||
// width: 100%;
|
||||
// align-self: flex-start;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
&__track {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
transition-duration: 0.3s;
|
||||
background-color: $tab-track-color;
|
||||
left: 0;
|
||||
bottom: 1rpx;
|
||||
width: $tab-track-width;
|
||||
height: $tab-track-height;
|
||||
// border-radius: $tab-track-radius;
|
||||
@include border-radius($tab-track-radius);
|
||||
}
|
||||
|
||||
&__content {
|
||||
width: 100%;
|
||||
// flex: 1;
|
||||
overflow: hidden;
|
||||
background-color: $tab-content-bg-color;
|
||||
&-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
overflow: visible;
|
||||
/* #ifdef UNI-APP-X & APP */
|
||||
// 为了不让首次把所有界面显示 先隐藏一下
|
||||
opacity: 0;
|
||||
/* #endif */
|
||||
}
|
||||
// &--animated {
|
||||
// display: flex;
|
||||
// transition-property: left;
|
||||
// }
|
||||
&--animated .l-tabs__content-inner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* #ifndef UNI-APP-X & APP */
|
||||
display: flex;
|
||||
will-change: left;
|
||||
/* #endif */
|
||||
transition-property: transform;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.#{$item} {
|
||||
position: relative;
|
||||
/* #ifndef UNI-APP-X */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: visible;
|
||||
// padding: $tab-item-padding;
|
||||
@include padding($tab-item-padding-y $tab-item-padding-x);
|
||||
box-sizing: border-box;
|
||||
|
||||
height: $tab-item-height;
|
||||
|
||||
/* #ifndef UNI-APP-X & APP */
|
||||
font-weight: 400;
|
||||
color: $tab-item-color;
|
||||
white-space: nowrap;
|
||||
/* #endif */
|
||||
|
||||
|
||||
&-text {
|
||||
font-weight: 400;
|
||||
font-size: $tab-font-size;
|
||||
white-space: nowrap;
|
||||
transition: color 300ms;
|
||||
|
||||
color: $tab-item-color;
|
||||
&--large {
|
||||
font-size: $tab-large-font-size;
|
||||
/* #ifndef UNI-APP-X */
|
||||
.l-tabs__item-text {
|
||||
font-size: inherit;
|
||||
}
|
||||
/* #endif */
|
||||
}
|
||||
&--active {
|
||||
font-weight: 700;
|
||||
color: $tab-item-active-color;
|
||||
|
||||
}
|
||||
&--disabled {
|
||||
color: $tab-item-disabled-color;
|
||||
}
|
||||
/* #ifdef UNI-APP-X & APP */
|
||||
/* #endif */
|
||||
|
||||
}
|
||||
&--evenly {
|
||||
flex: 1;
|
||||
}
|
||||
/* #ifndef UNI-APP-X & APP */
|
||||
&--active {
|
||||
font-weight: 700;
|
||||
color: $tab-item-active-color;
|
||||
/* #ifndef UNI-APP-X */
|
||||
.l-tabs__item-text {
|
||||
font-weight: inherit;
|
||||
color: inherit
|
||||
}
|
||||
/* #endif */
|
||||
}
|
||||
&--disabled {
|
||||
color: $tab-item-disabled-color;
|
||||
/* #ifndef UNI-APP-X */
|
||||
.l-tabs__item-text {
|
||||
color: inherit
|
||||
}
|
||||
/* #endif */
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
}
|
||||
@@ -0,0 +1,612 @@
|
||||
<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}"
|
||||
<!-- #ifdef APP || WEB -->
|
||||
ref="scrollRef"
|
||||
<!-- #endif -->
|
||||
:scroll-left="scrollLeft"
|
||||
direction="horizontal"
|
||||
:scroll-x="true"
|
||||
:scroll-with-animation="true"
|
||||
:show-scrollbar="false"
|
||||
:enhanced="true"
|
||||
@scroll="onScroll">
|
||||
<!-- #ifndef APP -->
|
||||
<view ref="navRef" class="l-tabs__nav">
|
||||
<!-- #endif -->
|
||||
<view class="l-tabs__item" v-for="(item, index) in tabs" :key="index" :style="[itemStyle]" :class="{
|
||||
'l-tabs__item--active': index == currentIndex,
|
||||
'l-tabs__item--evenly': spaceEvenly,
|
||||
'l-tabs__item--disabled': item.disabled}" @click="onClick(index, item)">
|
||||
<slot name="label" :item="item" :active="index == currentIndex" :disabled="item.disabled">
|
||||
<l-badge v-if="item.dot == true || item.badge != null" :dot="item.dot" :offset="item.offset"
|
||||
:content="item.badge">
|
||||
<text :style="[
|
||||
textStyles,
|
||||
!item.disabled && color != null && index != currentIndex ? ('color:' + color) : '',
|
||||
!item.disabled && activeColor != null && index == currentIndex ? 'color:' + activeColor : '',
|
||||
]" class="l-tabs__item-text" :class="[
|
||||
'l-tabs__item-text--' + size,
|
||||
{
|
||||
'l-tabs__item-text--disabled': item.disabled,
|
||||
'l-tabs__item-text--active': index == currentIndex
|
||||
}
|
||||
]">{{item.label}}</text>
|
||||
</l-badge>
|
||||
<text v-else :style="[
|
||||
textStyles,
|
||||
!item.disabled && color != null && index != currentIndex ? ('color:' + color) : '',
|
||||
!item.disabled && activeColor != null && index == currentIndex ? 'color:' + activeColor : '',
|
||||
]" class="l-tabs__item-text" :class="[
|
||||
'l-tabs__item-text--' + size,
|
||||
{
|
||||
'l-tabs__item-text--disabled': item.disabled,
|
||||
'l-tabs__item-text--active': index == currentIndex
|
||||
}
|
||||
]">{{item.label}}</text>
|
||||
</slot>
|
||||
</view>
|
||||
<view ref="trackRef" :style="[trackStyle]" class="l-tabs__track"></view>
|
||||
<!-- #ifndef APP -->
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</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'] != null">
|
||||
<!-- #ifdef APP || WEB -->
|
||||
<view class="l-tabs__content-inner" :style="[innerStyle]" ref="innerRef">
|
||||
<slot></slot>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef APP || WEB -->
|
||||
<view class="l-tabs__content-inner" :style="[innerStyle, {width: tabs.length * 100 + '%'}]" ref="innerRef">
|
||||
<slot></slot>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts" setup>
|
||||
/**
|
||||
* 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)
|
||||
* @property {number} swiperProgress 与swiper联动,传入[-1, 1] 范围的滚动进度
|
||||
* @property {boolean} syncSwiper 是否启用与swiper的同步联动
|
||||
* @event {Function} change 选项卡切换时触发(返回激活索引)
|
||||
* @event {Function} click 点击时触发(返回激活索引)
|
||||
*/
|
||||
|
||||
import { TabsProps, TabPanel } from './type';
|
||||
import { TabPanelProps } from '../l-tab-panel/type';
|
||||
import { calcScrollOffset, ease, calculateTabPositions } from './utils';
|
||||
import { clamp } from '@/uni_modules/lime-shared/clamp';
|
||||
// import { getRect } from '@/uni_modules/lime-shared/getRect';
|
||||
import { getRect, getAllRect } from '@/uni_modules/lime-shared/getRect'
|
||||
import { useTouch } from './touch'
|
||||
const emits = defineEmits(['change', 'click', 'total-width'])
|
||||
// const slots = defineSlots<{
|
||||
// label : {
|
||||
// item : TabPanel,
|
||||
// active : boolean,
|
||||
// disabled : boolean,
|
||||
// }
|
||||
// }>()
|
||||
|
||||
const slots = defineSlots<{
|
||||
label(props : { item : TabPanel, active : boolean, disabled : boolean }) : any,
|
||||
default(props : {}) : any,
|
||||
left(props : {}) : any,
|
||||
right(props : {}) : any,
|
||||
}>()
|
||||
|
||||
const props = withDefaults(defineProps<TabsProps>(), {
|
||||
showLine: true,
|
||||
visible: false,
|
||||
ellipsis: false,
|
||||
size: 'medium',
|
||||
spaceEvenly: true,
|
||||
swipeable: false,
|
||||
duration: 0.3,
|
||||
animated: false,
|
||||
split: true,
|
||||
swiperProgress: 0,
|
||||
syncSwiper: false
|
||||
})
|
||||
|
||||
const children = ref<LTabPanelComponentPublicInstance[]>([])
|
||||
const scrollLeft = ref(0);
|
||||
const lastLeft = ref(0);
|
||||
|
||||
// #ifndef WEB || APP
|
||||
const context = getCurrentInstance().proxy!;
|
||||
const trackDistance = ref(0)
|
||||
const innerStyle = reactive({
|
||||
width: '',
|
||||
'transition-duration': ``,
|
||||
'transform': ``,
|
||||
})
|
||||
// #endif
|
||||
// #ifdef WEB || APP
|
||||
const innerStyle = {
|
||||
|
||||
}
|
||||
// #endif
|
||||
|
||||
const modelValue = defineModel({ type: Number })
|
||||
const currentValue = computed({
|
||||
set(value : number) {
|
||||
modelValue.value = value;
|
||||
emits('change', value)
|
||||
},
|
||||
get() : number {
|
||||
return props.value ?? modelValue.value
|
||||
}
|
||||
} as WritableComputedOptions<number>);
|
||||
|
||||
const styles = computed(() : Map<string, any> => {
|
||||
const style = new Map<string, any>();
|
||||
// #ifdef APP
|
||||
if (props.bgColor != null) {
|
||||
style.set('background-color', props.bgColor!)
|
||||
}
|
||||
// #endif
|
||||
// #ifndef APP
|
||||
if (props.bgColor != null) {
|
||||
style.set('--l-tab-nav-bg-color', props.bgColor!)
|
||||
}
|
||||
if (!['medium', 'large'].includes(props.size)) {
|
||||
style.set('--l-tab-item-height', props.size)
|
||||
}
|
||||
if (props.padding != null) {
|
||||
style.set('--l-tab-item-padding', props.padding!)
|
||||
}
|
||||
if (props.fontSize != null) {
|
||||
style.set('--l-tab-font-size', props.fontSize!)
|
||||
}
|
||||
// #endif
|
||||
return style
|
||||
})
|
||||
const trackStyle = computed(() : Map<string, any> => {
|
||||
const style = new Map<string, any>([
|
||||
// #ifndef APP || WEB
|
||||
['-webkit-transform', `translateX(${trackDistance.value}px)`],
|
||||
['transform', `translateX(${trackDistance.value}px)`],
|
||||
// #endif
|
||||
]);
|
||||
|
||||
if (props.lineColor != null) {
|
||||
style.set('background', props.lineColor!)
|
||||
}
|
||||
if (props.lineWidth != null) {
|
||||
style.set('width', props.lineWidth!)
|
||||
}
|
||||
if (props.lineHeight != null) {
|
||||
style.set('height', props.lineHeight!)
|
||||
}
|
||||
return style
|
||||
})
|
||||
const itemStyle = computed(() : Map<string, any> => {
|
||||
const style = new Map<string, any>()
|
||||
// #ifdef APP
|
||||
if (!['medium', 'large'].includes(props.size)) {
|
||||
style.set('height', props.size)
|
||||
}
|
||||
if (props.padding != null) {
|
||||
style.set('padding', props.padding!)
|
||||
}
|
||||
// #endif
|
||||
return style
|
||||
})
|
||||
const textStyles = computed(() : Map<string, any> => {
|
||||
const style = new Map<string, any>()
|
||||
// #ifdef APP
|
||||
if (props.fontSize != null) {
|
||||
style.set('font-size', props.fontSize!)
|
||||
}
|
||||
// #endif
|
||||
return style
|
||||
})
|
||||
|
||||
const tabs = computed(() : TabPanel[] => {
|
||||
if (props.list != null && props.list!.length > 0) {
|
||||
return props.list!.map((item) : TabPanel => {
|
||||
return {
|
||||
badge: item.get('badge'),
|
||||
dot: item.getBoolean('dot') ?? false,
|
||||
disabled: item.getBoolean('disabled') ?? false,
|
||||
label: item.getString('label'),
|
||||
offset: item.getArray('offset') as any[] | null,
|
||||
value: item.getNumber('value'),
|
||||
node: item
|
||||
} as TabPanel
|
||||
})
|
||||
}
|
||||
return children.value.map((item) : TabPanel => {
|
||||
|
||||
// 安卓端数组属性存在BUG 死循环
|
||||
// #ifdef APP-ANDROID
|
||||
const offset = item.innderOffset
|
||||
// #endif
|
||||
// #ifndef APP-ANDROID
|
||||
const offset = item.offset ?? []
|
||||
// #endif
|
||||
|
||||
|
||||
return {
|
||||
badge: item.badge,
|
||||
dot: item.dot ?? false,
|
||||
disabled: item.disabled ?? false,
|
||||
label: item.label,
|
||||
offset: offset,
|
||||
value: item.value,
|
||||
node: {
|
||||
badge: item.badge,
|
||||
dot: item.dot ?? false,
|
||||
disabled: item.disabled ?? false,
|
||||
label: item.label,
|
||||
offset: offset,
|
||||
value: item.value,
|
||||
}
|
||||
} as TabPanel
|
||||
})
|
||||
})
|
||||
|
||||
const currentIndex = computed(() : number => {
|
||||
const index = tabs.value.findIndex((child, index) : boolean => (child.value ?? index) == currentValue.value)
|
||||
return index > -1 ? index : 0;
|
||||
})
|
||||
|
||||
|
||||
const scrollRef = ref<UniScrollViewElement | null>(null)
|
||||
const trackRef = ref<UniElement | null>(null)
|
||||
const navRef = ref<UniElement | null>(null)
|
||||
const innerRef = ref<UniElement | null>(null)
|
||||
|
||||
const tabRects = ref<DOMRect[]>([]) // 存储每个tab的尺寸位置
|
||||
const containerWidth = ref(0) // 容器宽度
|
||||
const trackLineWidth = ref(0)
|
||||
const isInteracting = ref(false) // 是否正在交互
|
||||
let timer = 0
|
||||
|
||||
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 measureTabs = () : Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
// nextTick(() => {
|
||||
if(tabRects.value.length > 0 && tabRects.value[0].width > 0) {
|
||||
resolve()
|
||||
}
|
||||
// #ifdef APP || WEB
|
||||
if (scrollRef.value == null) { // navRef.value == null ||
|
||||
resolve()
|
||||
return
|
||||
};
|
||||
// #ifdef WEB
|
||||
const elements = Array.from(navRef.value!.children)
|
||||
// #endif
|
||||
// #ifndef WEB
|
||||
const elements = scrollRef.value!.children;
|
||||
// #endif
|
||||
|
||||
const tabsRects:DOMRect[] = []
|
||||
elements.forEach((el)=>{
|
||||
if(el.tagName == 'VIEW') {
|
||||
tabsRects.push(el.getBoundingClientRect())
|
||||
}
|
||||
})
|
||||
tabRects.value = tabsRects
|
||||
containerWidth.value = scrollRef.value?.offsetWidth ?? 0
|
||||
trackLineWidth.value = trackRef.value?.offsetWidth ?? 0
|
||||
resolve()
|
||||
// #endif
|
||||
|
||||
|
||||
// #ifndef APP || WEB
|
||||
Promise.all([
|
||||
getRect(`.l-tabs__scroll`, context),
|
||||
getRect(`.l-tabs__track`, context),
|
||||
getAllRect(`.l-tabs__item`, context)
|
||||
]).then(([scrollRect, trackRect, tabsRects]) => {
|
||||
containerWidth.value = scrollRect.width
|
||||
tabRects.value = tabsRects
|
||||
trackLineWidth.value = trackRect.width
|
||||
resolve()
|
||||
})
|
||||
// #endif
|
||||
|
||||
})
|
||||
// })
|
||||
|
||||
}
|
||||
|
||||
const moveToActiveTab = () => {
|
||||
measureTabs().then(() => {
|
||||
const index = currentIndex.value
|
||||
if (tabRects.value.length <= index) return
|
||||
const tabRect = tabRects.value[index]
|
||||
|
||||
let offset = (tabRect.width - trackLineWidth.value) / 2
|
||||
let [distance] = calculateTabPositions(tabRects.value, index)
|
||||
if (totalTabsWidth.value == 0) return
|
||||
|
||||
// #ifdef APP || WEB
|
||||
trackRef.value?.style.setProperty('transform', `translateX(${distance + offset}px)`)
|
||||
// #endif
|
||||
// #ifndef APP || WEB
|
||||
trackDistance.value = distance + offset
|
||||
// #endif
|
||||
|
||||
|
||||
// 计算scroll-view需要滚动的距离
|
||||
const scrollOffset = calcScrollOffset(
|
||||
containerWidth.value,
|
||||
tabRect.left,
|
||||
tabRect.width,
|
||||
lastLeft.value
|
||||
)
|
||||
|
||||
const maxOffset = totalTabsWidth.value - containerWidth.value;
|
||||
|
||||
scrollLeft.value = clamp(scrollOffset, 0, maxOffset)
|
||||
// isInteracting.value = false
|
||||
})
|
||||
}
|
||||
const updateInnerStyle = (offset : number) => {
|
||||
nextTick(() => {
|
||||
// #ifdef WEB || APP
|
||||
if (innerRef.value == null) return;
|
||||
const width = innerRef.value!.parentElement?.offsetWidth ?? 0;
|
||||
innerRef.value!.style.setProperty('width', `${tabs.value.length * width}px`)
|
||||
innerRef.value!.style.setProperty('opacity', `1`)
|
||||
|
||||
const left = -width * currentIndex.value + offset;
|
||||
if (offset != 0) {
|
||||
innerRef.value!.style.setProperty('transition-duration', `0s`)
|
||||
innerRef.value!.style.setProperty('transform', `translateX(${left}px)`)
|
||||
} else {
|
||||
if (props.animated) {
|
||||
innerRef.value!.style.setProperty('transition-duration', offset != 0 || !props.animated ? '0s' : `${props.duration}s`)
|
||||
}
|
||||
nextTick(() => {
|
||||
innerRef.value!.style.setProperty('transform', `translateX(${left}px)`)
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
// #ifndef WEB || APP
|
||||
// const width = innerRef.value!.parentElement?.offsetWidth ?? 0;
|
||||
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)`
|
||||
}
|
||||
}).catch(err => {
|
||||
|
||||
})
|
||||
// #endif
|
||||
|
||||
})
|
||||
}
|
||||
const onScroll = (e : UniScrollEvent) => {
|
||||
lastLeft.value = e.detail.scrollLeft;
|
||||
}
|
||||
const updateDuration = (duration: number) => {
|
||||
// #ifdef APP || WEB
|
||||
trackRef.value?.style.setProperty('transition-duration', `${duration}s`)
|
||||
// #endif
|
||||
// #ifndef APP || WEB
|
||||
innerStyle['transition-duration'] = `${duration}s`
|
||||
// #endif
|
||||
}
|
||||
const onClick = (index : number, item : TabPanel) => {
|
||||
isInteracting.value = true
|
||||
const { value = index, disabled, label } = item;
|
||||
if (disabled || currentValue.value == value) return
|
||||
currentValue.value = value;
|
||||
emits('click', value)
|
||||
updateDuration(0.3)
|
||||
nextTick(()=> {
|
||||
moveToActiveTab()
|
||||
})
|
||||
|
||||
clearTimeout(timer)
|
||||
timer = setTimeout(()=>{
|
||||
isInteracting.value = false
|
||||
},500)
|
||||
|
||||
}
|
||||
|
||||
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) => {
|
||||
isInteracting.value = true
|
||||
if (!props.swipeable) return;
|
||||
touch.start(event);
|
||||
updateDuration(0.3)
|
||||
}
|
||||
|
||||
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 = () => {
|
||||
isInteracting.value = false
|
||||
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)
|
||||
}
|
||||
|
||||
const stopWatch = watch(tabs, (_v : TabPanel[]) => {
|
||||
// 鸿蒙增加项时渲染比较慢 需要延时一下
|
||||
setTimeout(() => {
|
||||
moveToActiveTab()
|
||||
}, 50)
|
||||
|
||||
})
|
||||
|
||||
const stopValueWatch = watch(currentValue, (_v : number) => {
|
||||
|
||||
if (tabs.value.length == 0) return
|
||||
moveToActiveTab()
|
||||
updateInnerStyle(0)
|
||||
|
||||
|
||||
})
|
||||
const stopVisibleWatch = watch(() : boolean => props.visible, (v : boolean) => {
|
||||
if (!v) return
|
||||
setTimeout(() => {
|
||||
moveToActiveTab()
|
||||
updateInnerStyle(0)
|
||||
}, 100)
|
||||
})
|
||||
|
||||
const updateTrackPosition = (progress: number)=>{
|
||||
if (!props.syncSwiper || !props.showLine || progress == 0 || isInteracting.value) return
|
||||
updateDuration(0)
|
||||
const currentIdx = currentIndex.value
|
||||
const next = ()=> {
|
||||
|
||||
const direction = progress > 0 ? 1 : -1
|
||||
const nextIdx = currentIdx + direction
|
||||
|
||||
// 边界检查
|
||||
if (nextIdx < 0 || nextIdx >= tabRects.value.length ) return
|
||||
|
||||
// 计算插值比例 (0~1)
|
||||
const ratio = Math.abs(progress)
|
||||
let currentOffset = (tabRects.value[currentIdx].width - trackLineWidth.value) / 2
|
||||
let nextOffset = (tabRects.value[nextIdx].width - trackLineWidth.value) / 2
|
||||
let [currentPos, nextPos] = calculateTabPositions(tabRects.value, currentIdx, nextIdx)
|
||||
currentPos = currentPos + currentOffset
|
||||
nextPos = nextPos + nextOffset
|
||||
// 根据滚动方向计算最终位置
|
||||
const newPosition = direction > 0
|
||||
? currentPos + (nextPos - currentPos) * ratio
|
||||
: currentPos - (currentPos - nextPos) * ratio
|
||||
|
||||
// #ifdef APP || WEB
|
||||
nextTick(()=>{
|
||||
requestAnimationFrame(()=>{
|
||||
trackRef.value?.style.setProperty('transform', `translateX(${newPosition}px)`)
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
// #ifndef APP || WEB
|
||||
trackDistance.value = newPosition
|
||||
// #endif
|
||||
}
|
||||
// requestAnimationFrame(()=>{
|
||||
measureTabs().then(next)
|
||||
// })
|
||||
|
||||
|
||||
}
|
||||
// 监听swiper进度变化
|
||||
watch(():number => props.swiperProgress, (progress: number) => {
|
||||
updateTrackPosition(progress)
|
||||
})
|
||||
|
||||
|
||||
// 添加监听器,当总宽度变化时emit事件
|
||||
watch(totalTabsWidth, (newWidth: number, oldWidth: number) => {
|
||||
if (newWidth != oldWidth && newWidth > 0) {
|
||||
emits('total-width', newWidth)
|
||||
}
|
||||
})
|
||||
onMounted(() => {
|
||||
// 鸿蒙时间上不确定
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
if (tabs.value.length == 0) return
|
||||
moveToActiveTab()
|
||||
updateInnerStyle(0)
|
||||
}, 100)
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopWatch()
|
||||
stopValueWatch()
|
||||
stopVisibleWatch()
|
||||
})
|
||||
|
||||
provide('LimeTabs', children)
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import './index';
|
||||
</style>
|
||||
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>
|
||||
@@ -0,0 +1,90 @@
|
||||
export default {
|
||||
list: {
|
||||
type: Array,
|
||||
},
|
||||
ellipsis: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
animated: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
duration: {
|
||||
type: [Number, String],
|
||||
default: 0.3,
|
||||
},
|
||||
showLine: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'medium' // large
|
||||
},
|
||||
/** 选项卡头部空间是否均分 */
|
||||
spaceEvenly: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
split: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
/** 是否可以滑动切换 */
|
||||
swipeable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
/** 激活的选项卡值 */
|
||||
value: {
|
||||
type: [String, Number],
|
||||
default: undefined,
|
||||
},
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: undefined,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
activeColor: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
lineColor: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
lineWidth: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
lineHeight: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
fontSize: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
padding: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
/**
|
||||
* 由于小程序popup会一开始就渲染导致下划线位置不对,所以增加这个值
|
||||
*/
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
lStyle: {
|
||||
type: [String, Object],
|
||||
}
|
||||
}
|
||||
102
qiming-mobile/uni_modules/lime-tabs/components/l-tabs/touch.ts
Normal file
102
qiming-mobile/uni_modules/lime-tabs/components/l-tabs/touch.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
// @ts-nocheck
|
||||
// #ifndef UNI-APP-X
|
||||
import {type Ref, ref} from '@/uni_modules/lime-shared/vue'
|
||||
type UniTouchEvent = TouchEvent
|
||||
// #endif
|
||||
|
||||
type Direction = '' | 'vertical' | 'horizontal';
|
||||
|
||||
function getDirection(x : number, y : number) : Direction {
|
||||
if (x > y) {
|
||||
return 'horizontal';
|
||||
}
|
||||
if (y > x) {
|
||||
return 'vertical';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
type TouchEventHandler = (event : UniTouchEvent) => void
|
||||
type BooleanFunction = () => boolean;
|
||||
type UseTouchResult = {
|
||||
start : TouchEventHandler,
|
||||
move : TouchEventHandler,
|
||||
startX : Ref<number>,
|
||||
startY : Ref<number>,
|
||||
deltaX : Ref<number>,
|
||||
deltaY : Ref<number>,
|
||||
offsetX : Ref<number>,
|
||||
offsetY : Ref<number>,
|
||||
direction : Ref<Direction>,
|
||||
isVertical : BooleanFunction,
|
||||
isHorizontal : BooleanFunction,
|
||||
isTap : Ref<Boolean>,
|
||||
}
|
||||
export function useTouch() : UseTouchResult {
|
||||
const startX = ref<number>(0);
|
||||
const startY = ref<number>(0);
|
||||
const deltaX = ref<number>(0);
|
||||
const deltaY = ref<number>(0);
|
||||
const offsetX = ref<number>(0);
|
||||
const offsetY = ref<number>(0);
|
||||
const direction = ref<Direction>('');
|
||||
const isTap = ref(true);
|
||||
|
||||
const isVertical = () : boolean => direction.value === 'vertical';
|
||||
const isHorizontal = () : boolean => direction.value === 'horizontal';
|
||||
|
||||
const reset = () => {
|
||||
deltaX.value = 0;
|
||||
deltaY.value = 0;
|
||||
offsetX.value = 0;
|
||||
offsetY.value = 0;
|
||||
direction.value = '';
|
||||
isTap.value = true;
|
||||
};
|
||||
|
||||
const start = (event : UniTouchEvent) => {
|
||||
reset();
|
||||
startX.value = event.touches[0].clientX;
|
||||
startY.value = event.touches[0].clientY;
|
||||
}
|
||||
const move = (event : UniTouchEvent) => {
|
||||
const touch = event.touches[0];
|
||||
// safari back will set clientX to negative number
|
||||
deltaX.value = (touch.clientX < 0 ? 0 : touch.clientX) - startX.value;
|
||||
deltaY.value = touch.clientY - startY.value;
|
||||
offsetX.value = Math.abs(deltaX.value);
|
||||
offsetY.value = Math.abs(deltaY.value);
|
||||
|
||||
// lock direction when distance is greater than a certain value
|
||||
const LOCK_DIRECTION_DISTANCE = 10;
|
||||
const TAP_OFFSET = 5;
|
||||
if (
|
||||
direction.value == '' ||
|
||||
(offsetX.value < LOCK_DIRECTION_DISTANCE &&
|
||||
offsetY.value < LOCK_DIRECTION_DISTANCE)
|
||||
) {
|
||||
direction.value = getDirection(offsetX.value, offsetY.value);
|
||||
}
|
||||
|
||||
if (
|
||||
isTap.value &&
|
||||
(offsetX.value > TAP_OFFSET || offsetY.value > TAP_OFFSET)
|
||||
) {
|
||||
isTap.value = false;
|
||||
}
|
||||
}
|
||||
return {
|
||||
start,
|
||||
move,
|
||||
startX,
|
||||
startY,
|
||||
deltaX,
|
||||
deltaY,
|
||||
offsetX,
|
||||
offsetY,
|
||||
direction,
|
||||
isVertical,
|
||||
isHorizontal,
|
||||
isTap,
|
||||
} as UseTouchResult
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// @ts-nocheck
|
||||
export type TabPanel = {
|
||||
/**
|
||||
* 透传至 Badge 组件
|
||||
*/
|
||||
badge ?: any;
|
||||
dot ?: boolean;
|
||||
offset ?: any[];
|
||||
/**
|
||||
* 选项卡内容隐藏时是否销毁
|
||||
*/
|
||||
destroyOnHide ?: boolean;
|
||||
/**
|
||||
* 是否禁用当前选项卡
|
||||
*/
|
||||
disabled : boolean;
|
||||
/**
|
||||
* 选项卡名称,可自定义选项卡导航内容
|
||||
*/
|
||||
label ?: string;
|
||||
/**
|
||||
* 是否启用选项卡懒加载
|
||||
*/
|
||||
lazy ?: boolean;
|
||||
/**
|
||||
* 选项卡的值,唯一标识
|
||||
*/
|
||||
value ?: number;
|
||||
ccs?: UTSJSONObject
|
||||
|
||||
node: UTSJSONObject
|
||||
}
|
||||
|
||||
|
||||
export interface TabsProps {
|
||||
/**
|
||||
* 选项卡列表
|
||||
*/
|
||||
list ?: UTSJSONObject[];
|
||||
ellipsis : boolean;
|
||||
/**
|
||||
* 动画效果设置
|
||||
*/
|
||||
animated : boolean;
|
||||
|
||||
duration : number
|
||||
|
||||
/**
|
||||
* 是否展示底部激活线条
|
||||
*/
|
||||
showLine : boolean;
|
||||
/**
|
||||
* 组件尺寸
|
||||
*/
|
||||
size : 'medium' | 'large';
|
||||
/**
|
||||
* 选项卡头部空间是否均分
|
||||
*/
|
||||
spaceEvenly : boolean;
|
||||
/**
|
||||
* 是否可以滑动切换
|
||||
*/
|
||||
swipeable : boolean;
|
||||
/**
|
||||
* 激活的选项卡值
|
||||
*/
|
||||
value?: number;
|
||||
|
||||
color?: string;
|
||||
activeColor ?: string;
|
||||
lineColor ?: string;
|
||||
lineWidth ?: string;
|
||||
lineHeight ?: string;
|
||||
bgColor ?: string;
|
||||
fontSize ?: string;
|
||||
padding ?: string;
|
||||
split: boolean;
|
||||
visible: boolean;
|
||||
/**
|
||||
* 和swiper联动 暂时未实现
|
||||
*/
|
||||
swiperProgress : number
|
||||
syncSwiper: boolean
|
||||
|
||||
lStyle ?: string | UTSJSONObject
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* 计算偏移
|
||||
* @param {number} containerWidth 容器宽度
|
||||
* @param {number} targetLeft 当前元素left值
|
||||
* @param {number} targetWidth 当前元素宽度
|
||||
* @param {number} offset 上一次 scroll-view left值
|
||||
* @return
|
||||
*/
|
||||
export function calcScrollOffset(containerWidth : number, targetLeft : number, targetWidth : number, offset : number) : number {
|
||||
return offset + targetLeft - (1 / 2) * containerWidth + targetWidth / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据提供的移动距离和基数进行缓动处理。
|
||||
* 如果移动距离的绝对值超过最大阈值,则应用缓动效果。
|
||||
*
|
||||
* @param {number} moveX - 水平移动的距离。
|
||||
* @param {number} base - 应用于缓动效果的基数。
|
||||
* @returns {number} 缓动处理后的移动距离。
|
||||
*/
|
||||
export function ease(moveX : number, base : number) : number{
|
||||
const absDistance = Math.abs(moveX);
|
||||
const max = 50
|
||||
if (absDistance > max) {
|
||||
return (max + (absDistance - max) * 0.2) * base;
|
||||
}
|
||||
return moveX;
|
||||
};
|
||||
|
||||
|
||||
// export function calculateTabDistances(tabRects: DOMRect[], index: number, offset: number = 0): number[] {
|
||||
// let count = 0;
|
||||
// let distance = 0;
|
||||
// let totalSize = 0;
|
||||
// tabRects.forEach((item) => {
|
||||
// if (count < index) {
|
||||
// distance += item.width;
|
||||
// count += 1;
|
||||
// }
|
||||
// totalSize += item.width;
|
||||
// });
|
||||
|
||||
// return [distance + offset, totalSize]
|
||||
// }
|
||||
|
||||
// 高效版本,只遍历一次到最大索引
|
||||
export function calculateTabPositions(
|
||||
tabRects: DOMRect[],
|
||||
currentIdx: number,
|
||||
nextIdx: number = 0
|
||||
): number[] {
|
||||
let currentDistance = 0;
|
||||
let nextDistance = 0;
|
||||
|
||||
const maxIndex = Math.max(currentIdx, nextIdx);
|
||||
|
||||
// 只遍历到最大索引位置
|
||||
for (let i = 0; i < maxIndex && i < tabRects.length; i++) {
|
||||
if (i < currentIdx) {
|
||||
currentDistance += tabRects[i].width;
|
||||
}
|
||||
if (i < nextIdx) {
|
||||
nextDistance += tabRects[i].width;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
currentDistance,
|
||||
nextDistance
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user