更新:登录功能
This commit is contained in:
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