更新:登录功能
This commit is contained in:
299
uni_modules/wot-design-uni/components/wd-tabs/index.scss
Normal file
299
uni_modules/wot-design-uni/components/wd-tabs/index.scss
Normal file
@@ -0,0 +1,299 @@
|
||||
@import '../common/abstracts/variable';
|
||||
@import '../common/abstracts/mixin';
|
||||
|
||||
|
||||
.wot-theme-dark {
|
||||
@include b(tabs) {
|
||||
background: $-dark-background2;
|
||||
|
||||
@include e(nav) {
|
||||
background: $-dark-background2;
|
||||
}
|
||||
|
||||
@include e(nav-item) {
|
||||
color: $-dark-color3;
|
||||
|
||||
@include when(active) {
|
||||
font-weight: 600;
|
||||
color: $-dark-color;
|
||||
}
|
||||
|
||||
@include when(disabled) {
|
||||
color: $-dark-color-gray;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@include e(map-nav-btn) {
|
||||
background-color: $-dark-background4;
|
||||
color: $-dark-color3;
|
||||
|
||||
@include when(active) {
|
||||
color: $-dark-color;
|
||||
border: 1px solid $-tabs-nav-active-color;
|
||||
background-color: $-dark-background;
|
||||
}
|
||||
|
||||
@include when(disabled) {
|
||||
color: $-dark-color-gray;
|
||||
border-color: #f4f4f4;
|
||||
}
|
||||
}
|
||||
|
||||
@include e(map-btn) {
|
||||
background: $-dark-background2;
|
||||
color: $-dark-color3;
|
||||
}
|
||||
|
||||
@include e(map-header) {
|
||||
background: $-dark-background2;
|
||||
color: $-dark-color;
|
||||
|
||||
&::after {
|
||||
background: $-dark-background4;
|
||||
}
|
||||
}
|
||||
|
||||
@include e(map-body) {
|
||||
background: $-dark-background2;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@include b(tabs) {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
width: 100%;
|
||||
|
||||
@include e(nav) {
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: $-tabs-nav-height;
|
||||
background-color: #fff;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
@include m(wrap) {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@include m(sticky) {
|
||||
width: 100vw;
|
||||
}
|
||||
}
|
||||
|
||||
@include e(nav-container) {
|
||||
position: relative;
|
||||
display: flex;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@include e(nav-item) {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: $-tabs-nav-height;
|
||||
font-size: $-tabs-nav-fs;
|
||||
color: $-tabs-nav-color;
|
||||
transition: color .3s;
|
||||
|
||||
@include when(active) {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@include when(disabled) {
|
||||
color: $-tabs-nav-disabled-color;
|
||||
}
|
||||
}
|
||||
|
||||
@include e(nav-item-text) {
|
||||
@include lineEllipsis();
|
||||
}
|
||||
|
||||
@include edeep(nav-item-badge){
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@include e(line) {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
height: $-tabs-nav-line-height;
|
||||
width: $-tabs-nav-line-width;
|
||||
background: $-tabs-nav-line-bg-color;
|
||||
border-radius: calc($-tabs-nav-line-height / 2);
|
||||
|
||||
@include m(inner) {
|
||||
left: 50%;
|
||||
transform: translateX(-50%)
|
||||
}
|
||||
}
|
||||
|
||||
@include e(container) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@include e(body) {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@include when(animated) {
|
||||
display: flex;
|
||||
transition-property: left;
|
||||
}
|
||||
}
|
||||
|
||||
@include e(map) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@include e(map-btn) {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: $-tabs-nav-height;
|
||||
height: $-tabs-nav-height;
|
||||
line-height: $-tabs-nav-height;
|
||||
text-align: center;
|
||||
color: $-tabs-nav-map-arrow-color;
|
||||
z-index: 1;
|
||||
background: $-tabs-nav-bg;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
top: 0;
|
||||
left: -24px;
|
||||
width: 24px;
|
||||
height: $-tabs-nav-height - 1;
|
||||
background: $-tabs-nav-map-btn-before-bg;
|
||||
}
|
||||
}
|
||||
|
||||
@include e(map-arrow) {
|
||||
display: block;
|
||||
transition: transform .3s;
|
||||
|
||||
@include when(open) {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
@include e(map-header) {
|
||||
position: relative;
|
||||
padding-left: 17px;
|
||||
height: $-tabs-nav-height;
|
||||
line-height: $-tabs-nav-height;
|
||||
font-size: $-tabs-nav-map-fs;
|
||||
color: $-tabs-nav-map-color;
|
||||
transition: opacity .3s;
|
||||
background: #fff;
|
||||
opacity: 0;
|
||||
|
||||
@include halfPixelBorder;
|
||||
|
||||
&::after {
|
||||
z-index: 3;
|
||||
}
|
||||
}
|
||||
|
||||
@include e(map-body) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 20px 15px 10px;
|
||||
background: #fff;
|
||||
transition: transform .3s;
|
||||
transform: scaleY(0);
|
||||
transform-origin: center top;
|
||||
|
||||
@include when(open) {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
|
||||
@include e(map-nav-item) {
|
||||
flex-basis: 33%;
|
||||
|
||||
&:nth-child(3n + 2) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&:nth-child(3n + 3) {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
@include e(map-nav-btn) {
|
||||
@include buttonClear;
|
||||
@include lineEllipsis;
|
||||
display: inline-block;
|
||||
width: 107px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
background-color: $-tabs-nav-map-button-back-color;
|
||||
border-color: transparent;
|
||||
margin-bottom: 10px;
|
||||
border-radius: $-tabs-nav-map-button-radius;
|
||||
color: $-tabs-nav-map-color;
|
||||
font-size: $-tabs-nav-map-fs;
|
||||
text-align: center;
|
||||
transition: color .3s, border-color .3s;
|
||||
|
||||
@include when(active) {
|
||||
color: $-tabs-nav-active-color;
|
||||
border: 1px solid $-tabs-nav-active-color;
|
||||
background-color: $-tabs-nav-bg
|
||||
}
|
||||
|
||||
@include when(disabled) {
|
||||
color: $-tabs-nav-disabled-color;
|
||||
border-color: #f4f4f4;
|
||||
}
|
||||
}
|
||||
|
||||
@include e(mask) {
|
||||
position: absolute;
|
||||
top: $-tabs-nav-height;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: $-tabs-nav-map-modal-bg;
|
||||
opacity: 0;
|
||||
transition: opacity .3s;
|
||||
}
|
||||
|
||||
@include when(slide) {
|
||||
.wd-tabs__nav-item {
|
||||
flex: 0 0 auto;
|
||||
padding: 0 17px;
|
||||
}
|
||||
}
|
||||
|
||||
@include when(map) {
|
||||
.wd-tabs__nav--wrap {
|
||||
padding-right: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 330px) {
|
||||
.wd-tabs__map-nav-btn {
|
||||
width: 90px;
|
||||
}
|
||||
}
|
||||
107
uni_modules/wot-design-uni/components/wd-tabs/types.ts
Normal file
107
uni_modules/wot-design-uni/components/wd-tabs/types.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { type ComponentPublicInstance, type ExtractPropTypes, type InjectionKey } from 'vue'
|
||||
import { baseProps, makeBooleanProp, makeNumberProp, makeNumericProp, makeStringProp, numericProp } from '../common/props'
|
||||
|
||||
export type TabsProvide = {
|
||||
state: {
|
||||
activeIndex: number
|
||||
lineStyle: string // 激活项边框线样式
|
||||
inited: boolean // 是否初始化
|
||||
animating: boolean // 是否动画中
|
||||
mapShow: boolean // map的开关
|
||||
scrollLeft: number // scroll-view偏移量
|
||||
}
|
||||
props: Partial<TabsProps>
|
||||
}
|
||||
|
||||
export type TabsSlidable = 'auto' | 'always'
|
||||
|
||||
export const TABS_KEY: InjectionKey<TabsProvide> = Symbol('wd-tabs')
|
||||
|
||||
export const tabsProps = {
|
||||
...baseProps,
|
||||
/**
|
||||
* 绑定值
|
||||
*/
|
||||
modelValue: makeNumericProp(0),
|
||||
/**
|
||||
* 标签数超过阈值可滑动
|
||||
*/
|
||||
slidableNum: makeNumberProp(6),
|
||||
/**
|
||||
* 标签数超过阈值显示导航地图
|
||||
*/
|
||||
mapNum: makeNumberProp(10),
|
||||
/**
|
||||
* 导航地图的标题
|
||||
*/
|
||||
mapTitle: String,
|
||||
/**
|
||||
* 粘性布局
|
||||
*/
|
||||
sticky: makeBooleanProp(false),
|
||||
/**
|
||||
* 粘性布局吸顶位置
|
||||
*/
|
||||
offsetTop: makeNumberProp(0),
|
||||
/**
|
||||
* 开启手势滑动
|
||||
*/
|
||||
swipeable: makeBooleanProp(false),
|
||||
/**
|
||||
* 自动调整底部条宽度,设置了 lineWidth 后无效
|
||||
*/
|
||||
autoLineWidth: makeBooleanProp(false),
|
||||
/**
|
||||
* 底部条宽度,单位像素
|
||||
*/
|
||||
lineWidth: numericProp,
|
||||
/**
|
||||
* 底部条高度,单位像素
|
||||
*/
|
||||
lineHeight: numericProp,
|
||||
/**
|
||||
* 颜色
|
||||
*/
|
||||
color: makeStringProp(''),
|
||||
/**
|
||||
* 非活动状态颜色
|
||||
*/
|
||||
inactiveColor: makeStringProp(''),
|
||||
/**
|
||||
* 是否开启切换标签内容时的过渡动画
|
||||
*/
|
||||
animated: makeBooleanProp(false),
|
||||
/**
|
||||
* 切换动画过渡时间,单位毫秒
|
||||
*/
|
||||
duration: makeNumberProp(300),
|
||||
/**
|
||||
* 是否开启滚动导航
|
||||
* 可选值:'auto' | 'always'
|
||||
* @default auto
|
||||
*/
|
||||
slidable: makeStringProp<TabsSlidable>('auto')
|
||||
}
|
||||
|
||||
export type TabsExpose = {
|
||||
/**
|
||||
* 设置激活项
|
||||
* @param value 激活值
|
||||
* @param init 是否已初始化
|
||||
* @param setScroll 是否设置scroll-view滚动
|
||||
*/
|
||||
setActive: (value: number | string, init: boolean, setScroll: boolean) => void
|
||||
/**
|
||||
* 使选中项滚动到可视区域
|
||||
*/
|
||||
scrollIntoView: () => void
|
||||
/**
|
||||
* 更新激活项边框线样式
|
||||
* @param animation 是否开启动画,默认开启
|
||||
*/
|
||||
updateLineStyle: (animation?: boolean) => void
|
||||
}
|
||||
|
||||
export type TabsProps = ExtractPropTypes<typeof tabsProps>
|
||||
|
||||
export type TabsInstance = ComponentPublicInstance<TabsProps, TabsExpose>
|
||||
439
uni_modules/wot-design-uni/components/wd-tabs/wd-tabs.vue
Normal file
439
uni_modules/wot-design-uni/components/wd-tabs/wd-tabs.vue
Normal file
@@ -0,0 +1,439 @@
|
||||
<template>
|
||||
<template v-if="sticky">
|
||||
<wd-sticky-box>
|
||||
<view
|
||||
:class="`wd-tabs ${customClass} ${innerSlidable ? 'is-slide' : ''} ${mapNum < children.length && mapNum !== 0 ? 'is-map' : ''}`"
|
||||
:style="customStyle"
|
||||
>
|
||||
<wd-sticky :offset-top="offsetTop">
|
||||
<view class="wd-tabs__nav wd-tabs__nav--sticky">
|
||||
<view class="wd-tabs__nav--wrap">
|
||||
<scroll-view :scroll-x="innerSlidable" scroll-with-animation :scroll-left="state.scrollLeft">
|
||||
<view class="wd-tabs__nav-container">
|
||||
<view
|
||||
@click="handleSelect(index)"
|
||||
v-for="(item, index) in children"
|
||||
:key="index"
|
||||
:class="`wd-tabs__nav-item ${state.activeIndex === index ? 'is-active' : ''} ${item.disabled ? 'is-disabled' : ''}`"
|
||||
:style="state.activeIndex === index ? (color ? 'color:' + color : '') : inactiveColor ? 'color:' + inactiveColor : ''"
|
||||
>
|
||||
<wd-badge v-if="item.badgeProps" v-bind="item.badgeProps">
|
||||
<text class="wd-tabs__nav-item-text">{{ item.title }}</text>
|
||||
</wd-badge>
|
||||
<text v-else class="wd-tabs__nav-item-text">{{ item.title }}</text>
|
||||
|
||||
<view class="wd-tabs__line wd-tabs__line--inner" v-if="state.activeIndex === index && state.useInnerLine"></view>
|
||||
</view>
|
||||
<view class="wd-tabs__line" :style="state.lineStyle"></view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<view class="wd-tabs__map" v-if="mapNum < children.length && mapNum !== 0">
|
||||
<view :class="`wd-tabs__map-btn ${state.animating ? 'is-open' : ''}`" @click="toggleMap">
|
||||
<view :class="`wd-tabs__map-arrow ${state.animating ? 'is-open' : ''}`">
|
||||
<wd-icon name="arrow-down" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="wd-tabs__map-header" :style="`${state.mapShow ? '' : 'display:none;'} ${state.animating ? 'opacity:1;' : ''}`">
|
||||
{{ mapTitle || translate('all') }}
|
||||
</view>
|
||||
<view :class="`wd-tabs__map-body ${state.animating ? 'is-open' : ''}`" :style="state.mapShow ? '' : 'display:none'">
|
||||
<view class="wd-tabs__map-nav-item" v-for="(item, index) in children" :key="index" @click="handleSelect(index)">
|
||||
<view
|
||||
:class="`wd-tabs__map-nav-btn ${state.activeIndex === index ? 'is-active' : ''} ${item.disabled ? 'is-disabled' : ''}`"
|
||||
:style="
|
||||
state.activeIndex === index
|
||||
? color
|
||||
? 'color:' + color + ';border-color:' + color
|
||||
: ''
|
||||
: inactiveColor
|
||||
? 'color:' + inactiveColor
|
||||
: ''
|
||||
"
|
||||
>
|
||||
{{ item.title }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</wd-sticky>
|
||||
|
||||
<view class="wd-tabs__container" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd" @touchcancel="onTouchEnd">
|
||||
<view :class="['wd-tabs__body', animated ? 'is-animated' : '']" :style="bodyStyle">
|
||||
<slot />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="wd-tabs__mask"
|
||||
:style="`${state.mapShow ? '' : 'display:none;'} ${state.animating ? 'opacity:1;' : ''}`"
|
||||
@click="toggleMap"
|
||||
></view>
|
||||
</view>
|
||||
</wd-sticky-box>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<view :class="`wd-tabs ${customClass} ${innerSlidable ? 'is-slide' : ''} ${mapNum < children.length && mapNum !== 0 ? 'is-map' : ''}`">
|
||||
<view class="wd-tabs__nav">
|
||||
<view class="wd-tabs__nav--wrap">
|
||||
<scroll-view :scroll-x="innerSlidable" scroll-with-animation :scroll-left="state.scrollLeft">
|
||||
<view class="wd-tabs__nav-container">
|
||||
<view
|
||||
v-for="(item, index) in children"
|
||||
@click="handleSelect(index)"
|
||||
:key="index"
|
||||
:class="`wd-tabs__nav-item ${state.activeIndex === index ? 'is-active' : ''} ${item.disabled ? 'is-disabled' : ''}`"
|
||||
:style="state.activeIndex === index ? (color ? 'color:' + color : '') : inactiveColor ? 'color:' + inactiveColor : ''"
|
||||
>
|
||||
<wd-badge custom-class="wd-tabs__nav-item-badge" v-if="item.badgeProps" v-bind="item.badgeProps">
|
||||
<text class="wd-tabs__nav-item-text">{{ item.title }}</text>
|
||||
</wd-badge>
|
||||
<text v-else class="wd-tabs__nav-item-text">{{ item.title }}</text>
|
||||
<view class="wd-tabs__line wd-tabs__line--inner" v-if="state.activeIndex === index && state.useInnerLine"></view>
|
||||
</view>
|
||||
<view class="wd-tabs__line" :style="state.lineStyle"></view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<view class="wd-tabs__map" v-if="mapNum < children.length && mapNum !== 0">
|
||||
<view class="wd-tabs__map-btn" @click="toggleMap">
|
||||
<view :class="`wd-tabs__map-arrow ${state.animating ? 'is-open' : ''}`">
|
||||
<wd-icon name="arrow-down" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="wd-tabs__map-header" :style="`${state.mapShow ? '' : 'display:none;'} ${state.animating ? 'opacity:1;' : ''}`">
|
||||
{{ mapTitle || translate('all') }}
|
||||
</view>
|
||||
<view :class="`wd-tabs__map-body ${state.animating ? 'is-open' : ''}`" :style="state.mapShow ? '' : 'display:none'">
|
||||
<view class="wd-tabs__map-nav-item" v-for="(item, index) in children" :key="index" @click="handleSelect(index)">
|
||||
<view :class="`wd-tabs__map-nav-btn ${state.activeIndex === index ? 'is-active' : ''} ${item.disabled ? 'is-disabled' : ''}`">
|
||||
{{ item.title }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="wd-tabs__container" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd" @touchcancel="onTouchEnd">
|
||||
<view :class="['wd-tabs__body', animated ? 'is-animated' : '']" :style="bodyStyle">
|
||||
<slot />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="wd-tabs__mask" :style="`${state.mapShow ? '' : 'display:none;'} ${state.animating ? 'opacity:1' : ''}`" @click="toggleMap"></view>
|
||||
</view>
|
||||
</template>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'wd-tabs',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
virtualHost: true,
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import wdIcon from '../wd-icon/wd-icon.vue'
|
||||
import wdSticky from '../wd-sticky/wd-sticky.vue'
|
||||
import wdStickyBox from '../wd-sticky-box/wd-sticky-box.vue'
|
||||
import { computed, getCurrentInstance, onMounted, watch, nextTick, reactive, type CSSProperties, type ComponentInstance } from 'vue'
|
||||
import { addUnit, checkNumRange, debounce, getRect, isDef, isNumber, isString, objToStyle } from '../common/util'
|
||||
import { useTouch } from '../composables/useTouch'
|
||||
import { TABS_KEY, tabsProps, type TabsExpose } from './types'
|
||||
import { useChildren } from '../composables/useChildren'
|
||||
import { useTranslate } from '../composables/useTranslate'
|
||||
|
||||
const $item = '.wd-tabs__nav-item'
|
||||
const $itemText = '.wd-tabs__nav-item-text'
|
||||
const $container = '.wd-tabs__nav-container'
|
||||
|
||||
const props = defineProps(tabsProps)
|
||||
const emit = defineEmits(['change', 'disabled', 'click', 'update:modelValue'])
|
||||
|
||||
const { translate } = useTranslate('tabs')
|
||||
|
||||
const state = reactive({
|
||||
activeIndex: 0, // 选中值的索引,默认第一个
|
||||
lineStyle: 'display:none;', // 激活项边框线样式
|
||||
useInnerLine: false, // 是否使用内部激活项边框线,当外部激活下划线未成功渲染时显示内部定位的
|
||||
inited: false, // 是否初始化
|
||||
animating: false, // 是否动画中
|
||||
mapShow: false, // map的开关
|
||||
scrollLeft: 0 // scroll-view偏移量
|
||||
})
|
||||
|
||||
const { children, linkChildren } = useChildren(TABS_KEY)
|
||||
linkChildren({ state, props })
|
||||
|
||||
const { proxy } = getCurrentInstance() as any
|
||||
|
||||
const touch = useTouch()
|
||||
|
||||
const innerSlidable = computed(() => {
|
||||
return props.slidable === 'always' || children.length > props.slidableNum
|
||||
})
|
||||
|
||||
const bodyStyle = computed(() => {
|
||||
if (!props.animated) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return objToStyle({
|
||||
left: -100 * state.activeIndex + '%',
|
||||
'transition-duration': props.duration + 'ms',
|
||||
'-webkit-transition-duration': props.duration + 'ms'
|
||||
})
|
||||
})
|
||||
|
||||
const getTabName = (tab: ComponentInstance<any>, index: number) => {
|
||||
return isDef(tab.name) ? tab.name : index
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新激活项
|
||||
* @param value 激活值
|
||||
* @param init 是否已初始化
|
||||
* @param setScroll // 是否设置scroll-view滚动
|
||||
*/
|
||||
const updateActive = (value: number | string = 0, init: boolean = false, setScroll: boolean = true) => {
|
||||
// 没有tab子元素,不执行任何操作
|
||||
if (children.length === 0) return
|
||||
|
||||
value = getActiveIndex(value)
|
||||
// 被禁用,不执行任何操作
|
||||
if (children[value].disabled) return
|
||||
state.activeIndex = value
|
||||
if (setScroll) {
|
||||
updateLineStyle(init === false)
|
||||
scrollIntoView()
|
||||
}
|
||||
setActiveTab()
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 修改选中的tab Index
|
||||
* @param {String |Number } value - radio绑定的value或者tab索引,默认值0
|
||||
* @param {Boolean } init - 是否伴随初始化操作
|
||||
*/
|
||||
const setActive = debounce(updateActive, 100, { leading: true })
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
if (!isNumber(newValue) && !isString(newValue)) {
|
||||
console.error('[wot ui] error(wd-tabs): the type of value should be number or string')
|
||||
}
|
||||
// 保证不为非空字符串,小于0的数字
|
||||
if (newValue === '' || !isDef(newValue)) {
|
||||
// eslint-disable-next-line quotes
|
||||
console.error("[wot ui] error(wd-tabs): tabs's value cannot be '' null or undefined")
|
||||
}
|
||||
if (typeof newValue === 'number' && newValue < 0) {
|
||||
// eslint-disable-next-line quotes
|
||||
console.error("[wot ui] error(wd-tabs): tabs's value cannot be less than zero")
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
const index = getActiveIndex(newValue)
|
||||
setActive(newValue, false, index !== state.activeIndex)
|
||||
},
|
||||
{
|
||||
immediate: false,
|
||||
deep: true
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => children.length,
|
||||
() => {
|
||||
if (state.inited) {
|
||||
nextTick(() => {
|
||||
setActive(props.modelValue)
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.slidableNum,
|
||||
(newValue) => {
|
||||
checkNumRange(newValue, 'slidableNum')
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.mapNum,
|
||||
(newValue) => {
|
||||
checkNumRange(newValue, 'mapNum')
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
state.inited = true
|
||||
nextTick(() => {
|
||||
updateActive(props.modelValue, true)
|
||||
state.useInnerLine = true
|
||||
})
|
||||
})
|
||||
|
||||
function toggleMap() {
|
||||
if (state.mapShow) {
|
||||
state.animating = false
|
||||
setTimeout(() => {
|
||||
state.mapShow = false
|
||||
}, 300)
|
||||
} else {
|
||||
state.mapShow = true
|
||||
setTimeout(() => {
|
||||
state.animating = true
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 underline的偏移量
|
||||
* @param animation 是否开启动画
|
||||
*/
|
||||
async function updateLineStyle(animation: boolean = true) {
|
||||
if (!state.inited) return
|
||||
const { autoLineWidth, lineWidth, lineHeight } = props
|
||||
try {
|
||||
const lineStyle: CSSProperties = {}
|
||||
if (isDef(lineWidth)) {
|
||||
lineStyle.width = addUnit(lineWidth)
|
||||
} else {
|
||||
if (autoLineWidth) {
|
||||
const textRects = await getRect($itemText, true, proxy)
|
||||
const textWidth = Number(textRects[state.activeIndex].width)
|
||||
lineStyle.width = addUnit(textWidth)
|
||||
}
|
||||
}
|
||||
if (isDef(lineHeight)) {
|
||||
lineStyle.height = addUnit(lineHeight)
|
||||
lineStyle.borderRadius = `calc(${addUnit(lineHeight)} / 2)`
|
||||
}
|
||||
const rects = await getRect($item, true, proxy)
|
||||
const rect = rects[state.activeIndex]
|
||||
let left = rects.slice(0, state.activeIndex).reduce((prev, curr) => prev + Number(curr.width), 0) + Number(rect.width) / 2
|
||||
if (left) {
|
||||
lineStyle.transform = `translateX(${left}px) translateX(-50%)`
|
||||
if (animation) {
|
||||
lineStyle.transition = 'width 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);'
|
||||
}
|
||||
state.useInnerLine = false
|
||||
state.lineStyle = objToStyle(lineStyle)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[wot ui] error(wd-tabs): update line style failed', error)
|
||||
}
|
||||
}
|
||||
|
||||
function setActiveTab() {
|
||||
if (!state.inited) return
|
||||
const name = getTabName(children[state.activeIndex], state.activeIndex)
|
||||
if (name !== props.modelValue) {
|
||||
emit('change', {
|
||||
index: state.activeIndex,
|
||||
name: name
|
||||
})
|
||||
emit('update:modelValue', name)
|
||||
}
|
||||
}
|
||||
|
||||
function scrollIntoView() {
|
||||
if (!state.inited) return
|
||||
Promise.all([getRect($item, true, proxy), getRect($container, false, proxy)]).then(([navItemsRects, navRect]) => {
|
||||
// 选中元素
|
||||
const selectItem = navItemsRects[state.activeIndex]
|
||||
// 选中元素之前的节点的宽度总和
|
||||
const offsetLeft = (navItemsRects as any).slice(0, state.activeIndex).reduce((prev: any, curr: any) => prev + curr.width, 0)
|
||||
// scroll-view滑动到selectItem的偏移量
|
||||
const left = offsetLeft - ((navRect as any).width - Number(selectItem.width)) / 2
|
||||
if (left === state.scrollLeft) {
|
||||
state.scrollLeft = left + Math.random() / 10000
|
||||
} else {
|
||||
state.scrollLeft = left
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 单击tab的处理
|
||||
* @param index
|
||||
*/
|
||||
function handleSelect(index: number) {
|
||||
if (index === undefined) return
|
||||
const { disabled } = children[index]
|
||||
const name = getTabName(children[index], index)
|
||||
|
||||
if (disabled) {
|
||||
emit('disabled', {
|
||||
index,
|
||||
name
|
||||
})
|
||||
return
|
||||
}
|
||||
state.mapShow && toggleMap()
|
||||
setActive(index)
|
||||
emit('click', {
|
||||
index,
|
||||
name
|
||||
})
|
||||
}
|
||||
function onTouchStart(event: any) {
|
||||
if (!props.swipeable) return
|
||||
touch.touchStart(event)
|
||||
}
|
||||
function onTouchMove(event: any) {
|
||||
if (!props.swipeable) return
|
||||
touch.touchMove(event)
|
||||
}
|
||||
function onTouchEnd() {
|
||||
if (!props.swipeable) return
|
||||
const { direction, deltaX, offsetX } = touch
|
||||
const minSwipeDistance = 50
|
||||
if (direction.value === 'horizontal' && offsetX.value >= minSwipeDistance) {
|
||||
if (deltaX.value > 0 && state.activeIndex !== 0) {
|
||||
setActive(state.activeIndex - 1)
|
||||
} else if (deltaX.value < 0 && state.activeIndex !== children.length - 1) {
|
||||
setActive(state.activeIndex + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
function getActiveIndex(value: number | string) {
|
||||
// name代表的索引超过了children长度的边界,自动用0兜底
|
||||
if (isNumber(value) && value >= children.length) {
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.error('[wot ui] warning(wd-tabs): the type of tabs\' value is Number shouldn\'t be less than its children')
|
||||
value = 0
|
||||
}
|
||||
// 如果是字符串直接匹配,匹配不到用0兜底
|
||||
if (isString(value)) {
|
||||
const index = children.findIndex((item) => item.name === value)
|
||||
value = index === -1 ? 0 : index
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
defineExpose<TabsExpose>({
|
||||
setActive,
|
||||
scrollIntoView,
|
||||
updateLineStyle
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import './index.scss';
|
||||
</style>
|
||||
Reference in New Issue
Block a user