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
|
||||
];
|
||||
}
|
||||
|
||||
110
qiming-mobile/uni_modules/lime-tabs/package.json
Normal file
110
qiming-mobile/uni_modules/lime-tabs/package.json
Normal file
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"id": "lime-tabs",
|
||||
"displayName": "lime-tabs 选项卡",
|
||||
"version": "0.2.7",
|
||||
"description": "lime-tabs 标签页选项卡组件,用于在不同的内容区域之间进行切换。兼容uniapp/uniappx",
|
||||
"keywords": [
|
||||
"lime-tabs",
|
||||
"tabs",
|
||||
"选项卡",
|
||||
"标签页"
|
||||
],
|
||||
"repository": "",
|
||||
"engines": {
|
||||
"HBuilderX": "^4.27",
|
||||
"uni-app": "^4.44",
|
||||
"uni-app-x": "^4.75"
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "component-vue",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": "",
|
||||
"darkmode": "x",
|
||||
"i18n": "x",
|
||||
"widescreen": "x"
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [
|
||||
"lime-shared",
|
||||
"lime-badge",
|
||||
"lime-style"
|
||||
],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "√",
|
||||
"aliyun": "√",
|
||||
"alipay": "√"
|
||||
},
|
||||
"client": {
|
||||
"uni-app": {
|
||||
"vue": {
|
||||
"vue2": "√",
|
||||
"vue3": "√"
|
||||
},
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"vue": "√",
|
||||
"nvue": "-",
|
||||
"android": {
|
||||
"extVersion": "",
|
||||
"minVersion": "21"
|
||||
},
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√",
|
||||
"alipay": "-",
|
||||
"toutiao": "-",
|
||||
"baidu": "-",
|
||||
"kuaishou": "-",
|
||||
"jd": "-",
|
||||
"harmony": "-",
|
||||
"qq": "-",
|
||||
"lark": "-"
|
||||
},
|
||||
"quickapp": {
|
||||
"huawei": "-",
|
||||
"union": "-"
|
||||
}
|
||||
},
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"android": {
|
||||
"extVersion": "",
|
||||
"minVersion": "21"
|
||||
},
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
259
qiming-mobile/uni_modules/lime-tabs/readme_old.md
Normal file
259
qiming-mobile/uni_modules/lime-tabs/readme_old.md
Normal file
@@ -0,0 +1,259 @@
|
||||
# lime-tabs 选项卡
|
||||
- 标签页选项卡组件,用于在不同的内容区域之间进行切换。兼容uniapp/uniappx
|
||||
- 插件依赖`lime-shared`,`lime-badge`,`lime-style`不喜勿下
|
||||
|
||||
## 安装
|
||||
在插件市场导入即可,首次导入可能需要重新编译
|
||||
|
||||
## 文档
|
||||
[tabs](https://limex.qcoon.cn/components/tabs.html)
|
||||
|
||||
## 代码演示
|
||||
### 基础使用
|
||||
通过 `v-model` 绑定当前激活标签对应的索引值,默认情况下启用第一个标签。通过`list`设置选项卡列表
|
||||
|
||||
```html
|
||||
<l-tabs v-model="value" :list="list" />
|
||||
```
|
||||
```js
|
||||
const value = ref(0)
|
||||
const list = [
|
||||
{
|
||||
value: 1,
|
||||
label: '选项',
|
||||
},
|
||||
{
|
||||
value: 2,
|
||||
label: '选项',
|
||||
},
|
||||
{
|
||||
value: 3,
|
||||
label: '选项',
|
||||
},
|
||||
{
|
||||
value: 4,
|
||||
label: '选项',
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 子组件
|
||||
除了使用`list`设置列表,还可以通过`l-tab-panel`子组件设置标签
|
||||
|
||||
```html
|
||||
<l-tabs>
|
||||
<l-tab-panel :value="0" label="选项" />
|
||||
<l-tab-panel :value="1" label="选项" />
|
||||
<l-tab-panel :value="2" label="选项" />
|
||||
</l-tabs>
|
||||
```
|
||||
|
||||
### 标签栏滚动
|
||||
通过设置`space-evenly`取消等距,当数量宽度超过父级时可滚动。
|
||||
|
||||
```html
|
||||
<l-tabs :space-evenly="false">
|
||||
<l-tab-panel :value="0" label="选项" />
|
||||
<l-tab-panel :value="1" label="选项" />
|
||||
<l-tab-panel :value="2" label="选项" />
|
||||
<l-tab-panel :value="3" label="选项" />
|
||||
<l-tab-panel :value="4" label="选项" />
|
||||
<l-tab-panel :value="5" label="选项" />
|
||||
<l-tab-panel :value="6" label="选项" />
|
||||
</l-tabs>
|
||||
```
|
||||
|
||||
|
||||
### 徽标
|
||||
通过设置`dot`为`true`可显示小红点,通过设置`badge`设置徽标内容,通过设置`offset`设置徽标偏移量。
|
||||
|
||||
```html
|
||||
<l-tabs :space-evenly="false">
|
||||
<l-tab-panel :value="0" label="选项" />
|
||||
<l-tab-panel :value="1" label="选项" :dot="true" />
|
||||
<l-tab-panel :value="2" label="选项" />
|
||||
<l-tab-panel :value="3" label="选项" badge="8" :offset="[-8,3]"/>
|
||||
<l-tab-panel :value="4" label="选项" />
|
||||
<l-tab-panel :value="5" label="选项" />
|
||||
<l-tab-panel :value="6" label="选项" />
|
||||
</l-tabs>
|
||||
```
|
||||
|
||||
### 切换动画
|
||||
通过 `animated` 属性可以开启切换标签内容时的转场动画。
|
||||
|
||||
```html
|
||||
<l-tabs v-model="value" :space-evenly="false" :animated="true">
|
||||
<l-tab-panel :value="0" label="选项">
|
||||
<text style="padding: 15px;">选项卡内容1</text>
|
||||
</l-tab-panel>
|
||||
<l-tab-panel :value="1" label="选项">
|
||||
<text style="padding: 15px;">选项卡内容2</text>
|
||||
</l-tab-panel>
|
||||
<l-tab-panel :value="2" label="选项">
|
||||
<text style="padding: 15px;">选项卡内容3</text>
|
||||
</l-tab-panel>
|
||||
<l-tab-panel :value="3" label="选项">
|
||||
<text style="padding: 15px;">选项卡内容4</text>
|
||||
</l-tab-panel>
|
||||
</l-tabs>
|
||||
```
|
||||
|
||||
### 滑动切换
|
||||
通过 `swipeable` 属性可以开启滑动切换标签页。
|
||||
|
||||
```html
|
||||
<l-tabs v-model="value" :space-evenly="false" :animated="true" :swipeable="true">
|
||||
<l-tab-panel :value="0" label="选项">
|
||||
<text style="padding: 15px;">选项卡内容1</text>
|
||||
</l-tab-panel>
|
||||
<l-tab-panel :value="1" label="选项">
|
||||
<text style="padding: 15px;">选项卡内容2</text>
|
||||
</l-tab-panel>
|
||||
<l-tab-panel :value="2" label="选项">
|
||||
<text style="padding: 15px;">选项卡内容3</text>
|
||||
</l-tab-panel>
|
||||
<l-tab-panel :value="3" label="选项">
|
||||
<text style="padding: 15px;">选项卡内容4</text>
|
||||
</l-tab-panel>
|
||||
</l-tabs>
|
||||
```
|
||||
|
||||
|
||||
### 受控
|
||||
通过 `onClick` 属性可以在切换标签前执行特定的逻辑。
|
||||
|
||||
```html
|
||||
<l-tabs :value="value" @click="onClick">
|
||||
<l-tab-panel :value="0" label="选项">
|
||||
<text style="padding: 15px;">选项卡内容1</text>
|
||||
</l-tab-panel>
|
||||
<l-tab-panel :value="1" label="选项">
|
||||
<text style="padding: 15px;">选项卡内容2</text>
|
||||
</l-tab-panel>
|
||||
<l-tab-panel :value="2" label="选项">
|
||||
<text style="padding: 15px;">选项卡内容3</text>
|
||||
</l-tab-panel>
|
||||
<l-tab-panel :value="3" label="选项">
|
||||
<text style="padding: 15px;">选项卡内容4</text>
|
||||
</l-tab-panel>
|
||||
</l-tabs>
|
||||
```
|
||||
```js
|
||||
const value = ref(1);
|
||||
const onClick = (index: number) => {
|
||||
uni.showLoading({title:'校验中'})
|
||||
setTimeout(()=>{
|
||||
uni.hideLoading()
|
||||
value.value = index
|
||||
},1000)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 查看示例
|
||||
- 导入后直接使用这个标签查看演示效果
|
||||
|
||||
```html
|
||||
// 代码位于 uni_modules/lime-tabs/compoents/lime-tabs
|
||||
<lime-tabs />
|
||||
```
|
||||
|
||||
### 插件标签
|
||||
- 默认 l-tabs 为 component
|
||||
- 默认 l-tab-panel 为 component
|
||||
- 默认 lime-tabs 为 demo
|
||||
|
||||
### 关于vue2的使用方式
|
||||
- 插件使用了`composition-api`, 如果你希望在vue2中使用请按官方的教程[vue-composition-api](https://uniapp.dcloud.net.cn/tutorial/vue-composition-api.html)配置
|
||||
- 关键代码是: 在main.js中 在vue2部分加上这一段即可.
|
||||
|
||||
```js
|
||||
// vue2
|
||||
import Vue from 'vue'
|
||||
import VueCompositionAPI from '@vue/composition-api'
|
||||
Vue.use(VueCompositionAPI)
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### Tabs Props
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| ------ | ---------------------- | --------- | ------- |
|
||||
| v-model | 绑定当前选中标签的标识符 | _number_ | `0` |
|
||||
| value | 当前选中标签的标识符 | _number_ | `0` |
|
||||
| list | 选项卡列表 | _TabPanelProps[]_ | `[]` |
|
||||
| animated | 是否开启切换标签内容时的转场动画 | _boolean_ | `false` |
|
||||
| duration | 动画时间,单位秒,设置为 0 可以禁用动画 | _number_ | `0.3` |
|
||||
| spaceEvenly | 选项卡头部空间是否均分 | _boolean_ | `true` |
|
||||
| swipeable | 是否开启手势左右滑动切换 | _boolean_ | `false` |
|
||||
| split | 是否展示分割线 | _boolean_ | `true` |
|
||||
| color | 标签文本颜色 | _string_ | `-` |
|
||||
| activeColor | 标签激活文本颜色 | _string_ | `-` |
|
||||
| lineColor | 底部条颜色 | _string_ | `-` |
|
||||
| lineWidth | 底部条宽度 | _string_ | `-` |
|
||||
| lineHeight | 底部条高度 | _string_ | `-` |
|
||||
| bgColor | 选项卡背景色 | _string_ | `-` |
|
||||
| size | 组件尺寸,内置'medium', 'large',如果传具体的值就是设置高度 | _string_ | `-` |
|
||||
| padding | 标题 padding | _string_ | `-` |
|
||||
| swiperProgress | 与swiper联动,传入[-1, 1] 范围的滚动进度 uniappx有效 | _number_ | `-` |
|
||||
| syncSwiper | 是否启用与swiper的同步联动, uniappx有效 | _boolean_ | `false` |
|
||||
|
||||
### Tabs Slots 插槽
|
||||
|
||||
| 插槽名称 | 作用域参数 | 说明 |
|
||||
|----------------|--------------------------------------------|---------------------------------------------------------------------|
|
||||
| **label** | `{ item: TabPanel, active: boolean, disabled: boolean }` | 自定义标签内容(会覆盖默认的标签渲染逻辑) |
|
||||
| **left** | - | 导航栏左侧扩展区域(支持固定位置内容,如返回按钮) |
|
||||
| **right** | - | 导航栏右侧扩展区域(支持固定位置内容,如更多按钮) |
|
||||
| **default** | - | 内容区域(需配合 `<l-tab-panel>` 子组件使用或`list`属性) |
|
||||
|
||||
|
||||
### TabPanel Props
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| value | 选项卡的值,唯一标识 | _number_ | - |
|
||||
| label | 选项卡名称 | _string_ | - |
|
||||
| disabled | 是否禁用选项 | _boolean_ | `false` |
|
||||
| dot | 是否显示标签右上角小红点 | _boolean_ | `false` |
|
||||
| badge | 标签右上角徽标的内容 | _string \| number_ | `_` |
|
||||
| offset | 徽标偏移 | _string \| number[]_ | `[]` |
|
||||
|
||||
### GridItem Events
|
||||
|
||||
| 事件名 | 说明 | 回调参数 |
|
||||
| ------ | ---------------- | ------------------- |
|
||||
| click | 点击标签时触发 | _index: number_ |
|
||||
| change | 当前激活的标签改变时触发 | _index: number_ |
|
||||
|
||||
|
||||
|
||||
## 主题定制
|
||||
|
||||
### 样式变量
|
||||
|
||||
组件提供了下列 CSS 变量,可用于自定义样式)。uvue app无效。
|
||||
|
||||
| 名称 | 默认值 | 描述 |
|
||||
| --- | --- | --- |
|
||||
| --l-tab-font-size: | _28rpx_ | - |
|
||||
| --l-tab-nav-bg-color: | _white_ | - |
|
||||
| --l-tab-item-height: | _96rpx_ | - |
|
||||
| --l-tab-item-padding: | _0 32rpx_ | - |
|
||||
| --l-tab-item-color: | _$text-color-1_ | - |
|
||||
| --l-tab-item-active-color: | _$primary-color_ | - |
|
||||
| --l-tab-item-disabled-color: | _$text-color-disabled_ | - |
|
||||
| --l-tab-track-color: | _$primary-color_ | - |
|
||||
| --l-tab-track-height: | _6rpx_ | - |
|
||||
| --l-tab-track-width: | _32rpx_ | - |
|
||||
| --l-tab-track-radius: | _8rpx_ | - |
|
||||
|
||||
|
||||
|
||||
## 打赏
|
||||
|
||||
如果你觉得本插件,解决了你的问题,赠人玫瑰,手留余香。
|
||||

|
||||

|
||||
Reference in New Issue
Block a user