feat: Tabs 支持设置徽标 (#724)

 Closes: #689,#672
This commit is contained in:
不如摸鱼去 2024-11-21 11:54:12 +08:00 committed by GitHub
parent bb5b19325f
commit cd20581ca6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 199 additions and 128 deletions

View File

@ -57,6 +57,47 @@ const tab = ref('例子')
} }
``` ```
## 使用徽标<el-tag text style="vertical-align: middle;margin-left:8px;" effect="plain">1.3.15</el-tag>
使用`bage-props`设置徽标属性,可以参考[Badge 组件的 props](/component/badge#attributes)。
```html
<wd-tabs v-model="tabWithBadge" @change="handleChange">
<wd-tab v-for="(item, index) in tabsWithBadge" :key="index" :title="`${item.title}`" :badge-props="item.badgeProps">
<view class="content">{{ item.title }}徽标</view>
</wd-tab>
</wd-tabs>
```
```typescript
const tabWithBadge = ref(0)
const tabsWithBadge = ref([
{
title: '普通数值',
badgeProps: {
modelValue: 10,
right: '-8px'
}
},
{
title: '最大值',
badgeProps: {
modelValue: 100,
max: 99,
right: '-8px'
}
},
{
title: '点状',
badgeProps: {
isDot: true,
right: '-8px',
showZero: true
}
}
])
```
## 自动调整底部条宽度 ## 自动调整底部条宽度
设置 `auto-line-width` 属性,自动调整底部条宽度为文本内容宽度。 设置 `auto-line-width` 属性,自动调整底部条宽度为文本内容宽度。
@ -160,39 +201,39 @@ const tab = ref('Design')
</wd-tabs> </wd-tabs>
``` ```
--- ---
标签页在标签数大于等于 6 个时,可以滑动;当标签数大于等于 10 个时,将会显示导航地图,便于快速定位到某个标签。可以通过设置 `slidable-num` 修改可滑动的数量阈值;设置 `map-num` 修改显示导航地图的阈值。`slidable`设置为`always`时,所有的标签会向左侧收缩对齐,超出即可滑动。 标签页在标签数大于等于 6 个时,可以滑动;当标签数大于等于 10 个时,将会显示导航地图,便于快速定位到某个标签。可以通过设置 `slidable-num` 修改可滑动的数量阈值;设置 `map-num` 修改显示导航地图的阈值。`slidable`设置为`always`时,所有的标签会向左侧收缩对齐,超出即可滑动。
## Tabs Attributes ## Tabs Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | 最低版本 | | 参数 | 说明 | 类型 | 可选值 | 默认值 | 最低版本 |
| ------------- | -------------------------------- | --------------- | ------ | ------ | -------- | | ------------- | ------------------------------------------------- | --------------- | -------- | ------ | ---------------- |
| v-model | 绑定值 | string / number | - | - | - | | v-model | 绑定值 | string / number | - | - | - |
| slidable-num | 可滑动的标签数阈值,`slidable`设置为`auto`时生效 | number | - | 6 | - | | slidable-num | 可滑动的标签数阈值,`slidable`设置为`auto`时生效 | number | - | 6 | - |
| map-num | 显示导航地图的标签数阈值 | number | - | 10 | - | | map-num | 显示导航地图的标签数阈值 | number | - | 10 | - |
| map-title | 导航地图标题 | string | - | - | $LOWEST_VERSION$ | | map-title | 导航地图标题 | string | - | - | $LOWEST_VERSION$ |
| sticky | 粘性布局 | boolean | - | false | - | | sticky | 粘性布局 | boolean | - | false | - |
| offset-top | 粘性布局时距离窗口顶部距离 | number | - | 0 | - | | offset-top | 粘性布局时距离窗口顶部距离 | number | - | 0 | - |
| swipeable | 开启手势滑动 | boolean | - | false | - | | swipeable | 开启手势滑动 | boolean | - | false | - |
| autoLineWidth | 底部条宽度跟随文字,指定`lineWidth`时此选项不生效 | boolean | - | false | $LOWEST_VERSION$ | | autoLineWidth | 底部条宽度跟随文字,指定`lineWidth`时此选项不生效 | boolean | - | false | $LOWEST_VERSION$ |
| lineWidth | 底部条宽度,单位像素 | number | - | 19 | - | | lineWidth | 底部条宽度,单位像素 | number | - | 19 | - |
| lineHeight | 底部条高度,单位像素 | number | - | 3 | - | | lineHeight | 底部条高度,单位像素 | number | - | 3 | - |
| color | 文字颜色 | string | - | - | - | | color | 文字颜色 | string | - | - | - |
| inactiveColor | 非活动标签文字颜色 | string | - | - | - | | inactiveColor | 非活动标签文字颜色 | string | - | - | - |
| animated | 是否开启切换标签内容时的转场动画 | boolean | - | false | - | | animated | 是否开启切换标签内容时的转场动画 | boolean | - | false | - |
| duration | 切换动画过渡时间,单位毫秒 | number | - | 300 | - | | duration | 切换动画过渡时间,单位毫秒 | number | - | 300 | - |
| slidable | 是否开启滚动导航 | TabsSlidable | `always` | `auto` | $LOWEST_VERSION$ | | slidable | 是否开启滚动导航 | TabsSlidable | `always` | `auto` | $LOWEST_VERSION$ |
| badge-props | 自定义徽标的属性,传入的对象会被透传给 [Badge 组件的 props](/component/badge#attributes) | BadgeProps | - | - | $LOWEST_VERSION$ |
## Tab Attributes ## Tab Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | 最低版本 | | 参数 | 说明 | 类型 | 可选值 | 默认值 | 最低版本 |
| -------- | ---------- | ------- | ------ | ------ | -------- | | -------- | ------------------------------------------------------- | ------- | ------ | ------ | ---------------- |
| name | 标签页名称 | string | - | - | - | | name | 标签页名称 | string | - | - | - |
| title | 标题 | string | - | - | - | | title | 标题 | string | - | - | - |
| disabled | 禁用 | boolean | - | false | - | | disabled | 禁用 | boolean | - | false | - |
| lazy | 延迟渲染,默认开启,开启`animated`后此选项始终为`false` | boolean | - | true | $LOWEST_VERSION$ | | lazy | 延迟渲染,默认开启,开启`animated`后此选项始终为`false` | boolean | - | true | $LOWEST_VERSION$ |
## Tabs Events ## Tabs Events

View File

@ -23,6 +23,14 @@
</wd-tabs> </wd-tabs>
</demo-block> </demo-block>
<demo-block title="使用徽标" transparent>
<wd-tabs v-model="tabWithBadge" @change="handleChange">
<wd-tab v-for="(item, index) in tabsWithBadge" :key="index" :title="`${item.title}`" :badge-props="item.badgeProps">
<view class="content">{{ item.title }}徽标</view>
</wd-tab>
</wd-tabs>
</demo-block>
<demo-block title="自动调整底部条宽度" transparent> <demo-block title="自动调整底部条宽度" transparent>
<wd-tabs v-model="autoLineWidthTab" @change="handleChange" auto-line-width> <wd-tabs v-model="autoLineWidthTab" @change="handleChange" auto-line-width>
<block v-for="item in autoLineWidthTabs" :key="item"> <block v-for="item in autoLineWidthTabs" :key="item">
@ -120,6 +128,34 @@ import { ref } from 'vue'
const tabs = ref(['这', '是', '一', '个', '例子']) const tabs = ref(['这', '是', '一', '个', '例子'])
const tab = ref('一') const tab = ref('一')
const tabWithBadge = ref(0)
const tabsWithBadge = ref([
{
title: '普通数值',
badgeProps: {
modelValue: 10,
right: '-8px'
}
},
{
title: '最大值',
badgeProps: {
modelValue: 100,
max: 99,
right: '-8px'
}
},
{
title: '点状',
badgeProps: {
isDot: true,
right: '-8px',
showZero: true
}
}
])
const autoLineWidthTabs = ref(['Wot', 'Design', 'Uni']) const autoLineWidthTabs = ref(['Wot', 'Design', 'Uni'])
const autoLineWidthTab = ref('Design') const autoLineWidthTab = ref('Design')

View File

@ -31,6 +31,8 @@
@include when(fixed) { @include when(fixed) {
position: absolute; position: absolute;
top: 0px;
right: 0px;
transform: translateY(-50%) translateX(50%); transform: translateY(-50%) translateX(50%);
} }

View File

@ -1,14 +1,14 @@
/* /*
* @Author: weisheng * @Author: weisheng
* @Date: 2024-03-15 11:36:12 * @Date: 2024-03-15 11:36:12
* @LastEditTime: 2024-03-19 16:33:12 * @LastEditTime: 2024-11-20 20:29:03
* @LastEditors: weisheng * @LastEditors: weisheng
* @Description: * @Description:
* @FilePath: \wot-design-uni\src\uni_modules\wot-design-uni\components\wd-badge\types.ts * @FilePath: /wot-design-uni/src/uni_modules/wot-design-uni/components/wd-badge/types.ts
* *
*/ */
import type { ExtractPropTypes, PropType } from 'vue' import type { ExtractPropTypes, PropType } from 'vue'
import { baseProps, makeBooleanProp, makeStringProp } from '../common/props' import { baseProps, makeBooleanProp, makeStringProp, numericProp } from '../common/props'
export type BadgeType = 'primary' | 'success' | 'warning' | 'danger' | 'info' export type BadgeType = 'primary' | 'success' | 'warning' | 'danger' | 'info'
@ -17,10 +17,7 @@ export const badgeProps = {
/** /**
* *
*/ */
modelValue: { modelValue: numericProp,
type: [Number, String, null] as PropType<number | string | null>,
default: null
},
/** 当数值为 0 时,是否展示徽标 */ /** 当数值为 0 时,是否展示徽标 */
showZero: makeBooleanProp(false), showZero: makeBooleanProp(false),
bgColor: String, bgColor: String,
@ -43,11 +40,11 @@ export const badgeProps = {
/** /**
* *
*/ */
top: Number, top: numericProp,
/** /**
* *
*/ */
right: Number right: numericProp
} }
export type BadgeProps = ExtractPropTypes<typeof badgeProps> export type BadgeProps = ExtractPropTypes<typeof badgeProps>

View File

@ -2,7 +2,7 @@
<view :class="['wd-badge', customClass]" :style="customStyle"> <view :class="['wd-badge', customClass]" :style="customStyle">
<slot></slot> <slot></slot>
<view <view
v-if="isBadgeShow" v-if="shouldShowBadge"
:class="['wd-badge__content', 'is-fixed', type ? 'wd-badge__content--' + type : '', isDot ? 'is-dot' : '']" :class="['wd-badge__content', 'is-fixed', type ? 'wd-badge__content--' + type : '', isDot ? 'is-dot' : '']"
:style="contentStyle" :style="contentStyle"
> >
@ -21,42 +21,39 @@ export default {
} }
</script> </script>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref, watch } from 'vue' import { computed, type CSSProperties } from 'vue'
import { badgeProps } from './types' import { badgeProps } from './types'
import { addUnit, isDef, isNumber, objToStyle } from '../common/util'
const props = defineProps(badgeProps) const props = defineProps(badgeProps)
const content = ref<number | string | null>(null) const content = computed(() => {
const { modelValue, max, isDot } = props
watch( if (isDot) return ''
[() => props.modelValue, () => props.max, () => props.isDot], let value = modelValue
() => { if (value && max && isNumber(value) && !Number.isNaN(value) && !Number.isNaN(max)) {
notice() value = max < value ? `${max}+` : value
}, }
{ immediate: true, deep: true } return value
) })
const contentStyle = computed(() => { const contentStyle = computed(() => {
return `background-color: ${props.bgColor};top:${props.top || 0}px;right:${props.right || 0}px` const style: CSSProperties = {}
if (isDef(props.bgColor)) {
style.backgroundColor = props.bgColor
}
if (isDef(props.top)) {
style.top = addUnit(props.top)
}
if (isDef(props.right)) {
style.right = addUnit(props.right)
}
return objToStyle(style)
}) })
// //
const isBadgeShow = computed(() => { const shouldShowBadge = computed(() => !props.hidden && (content.value || (content.value === 0 && props.showZero) || props.isDot))
let isBadgeShow: boolean = false
if (!props.hidden && (content.value || (content.value === 0 && props.showZero) || props.isDot)) {
isBadgeShow = true
}
return isBadgeShow
})
function notice() {
if (props.isDot) return
let value = props.modelValue
const max = props.max
if (value && max && typeof value === 'number' && !Number.isNaN(value) && !Number.isNaN(max)) {
value = max < value ? `${max}+` : value
}
content.value = value
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,5 +1,6 @@
import type { ExtractPropTypes } from 'vue' import type { ExtractPropTypes, PropType } from 'vue'
import { baseProps, makeBooleanProp, numericProp } from '../common/props' import { baseProps, makeBooleanProp, numericProp } from '../common/props'
import type { BadgeProps } from '../wd-badge/types'
export const tabProps = { export const tabProps = {
...baseProps, ...baseProps,
@ -19,7 +20,11 @@ export const tabProps = {
* tab时才加载内容 * tab时才加载内容
* @default true * @default true
*/ */
lazy: makeBooleanProp(true) lazy: makeBooleanProp(true),
/**
* Badge
*/
badgeProps: Object as PropType<Partial<BadgeProps>>
} }
export type TabProps = ExtractPropTypes<typeof tabProps> export type TabProps = ExtractPropTypes<typeof tabProps>

View File

@ -33,7 +33,7 @@
border: 1px solid $-tabs-nav-active-color; border: 1px solid $-tabs-nav-active-color;
background-color: $-dark-background; background-color: $-dark-background;
} }
@include when(disabled) { @include when(disabled) {
color: $-dark-color-gray; color: $-dark-color-gray;
border-color: #f4f4f4; border-color: #f4f4f4;
@ -92,15 +92,15 @@
@include e(nav-item) { @include e(nav-item) {
position: relative; position: relative;
display: flex;
align-items: center;
justify-content: center;
flex: 1; flex: 1;
min-width: 0; min-width: 0;
text-align: center;
height: $-tabs-nav-height; height: $-tabs-nav-height;
line-height: $-tabs-nav-height;
font-size: $-tabs-nav-fs; font-size: $-tabs-nav-fs;
color: $-tabs-nav-color; color: $-tabs-nav-color;
transition: color .3s; transition: color .3s;
@include lineEllipsis();
@include when(active) { @include when(active) {
font-weight: 600; font-weight: 600;
@ -111,6 +111,18 @@
} }
} }
@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) { @include e(line) {
position: absolute; position: absolute;
bottom: 4px; bottom: 4px;
@ -121,8 +133,8 @@
background: $-tabs-nav-line-bg-color; background: $-tabs-nav-line-bg-color;
border-radius: calc($-tabs-nav-line-height / 2); border-radius: calc($-tabs-nav-line-height / 2);
@include m(inner){ @include m(inner) {
left: 50%; left: 50%;
transform: translateX(-50%) transform: translateX(-50%)
} }
} }

View File

@ -2,33 +2,33 @@
<template v-if="sticky"> <template v-if="sticky">
<wd-sticky-box> <wd-sticky-box>
<view <view
:class="`wd-tabs ${customClass} ${innerSlidable ? 'is-slide' : ''} ${mapNum < items.length && mapNum !== 0 ? 'is-map' : ''}`" :class="`wd-tabs ${customClass} ${innerSlidable ? 'is-slide' : ''} ${mapNum < children.length && mapNum !== 0 ? 'is-map' : ''}`"
:style="customStyle" :style="customStyle"
> >
<wd-sticky :offset-top="offsetTop"> <wd-sticky :offset-top="offsetTop">
<!--头部导航容器-->
<view class="wd-tabs__nav wd-tabs__nav--sticky"> <view class="wd-tabs__nav wd-tabs__nav--sticky">
<view class="wd-tabs__nav--wrap"> <view class="wd-tabs__nav--wrap">
<scroll-view :scroll-x="innerSlidable" scroll-with-animation :scroll-left="state.scrollLeft"> <scroll-view :scroll-x="innerSlidable" scroll-with-animation :scroll-left="state.scrollLeft">
<view class="wd-tabs__nav-container"> <view class="wd-tabs__nav-container">
<!--nav列表-->
<view <view
@click="handleSelect(index)" @click="handleSelect(index)"
v-for="(item, index) in items" v-for="(item, index) in children"
:key="index" :key="index"
:class="`wd-tabs__nav-item ${state.activeIndex === index ? 'is-active' : ''} ${item.disabled ? 'is-disabled' : ''}`" :class="`wd-tabs__nav-item ${state.activeIndex === index ? 'is-active' : ''} ${item.disabled ? 'is-disabled' : ''}`"
:style="state.activeIndex === index ? (color ? 'color:' + color : '') : inactiveColor ? 'color:' + inactiveColor : ''" :style="state.activeIndex === index ? (color ? 'color:' + color : '') : inactiveColor ? 'color:' + inactiveColor : ''"
> >
<text class="wd-tabs__nav-item-text">{{ item.title }}</text> <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 class="wd-tabs__line wd-tabs__line--inner" v-if="state.activeIndex === index && state.useInnerLine"></view>
</view> </view>
<!--下划线-->
<view class="wd-tabs__line" :style="state.lineStyle"></view> <view class="wd-tabs__line" :style="state.lineStyle"></view>
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
<!--map表--> <view class="wd-tabs__map" v-if="mapNum < children.length && mapNum !== 0">
<view class="wd-tabs__map" v-if="mapNum < items.length && mapNum !== 0">
<view :class="`wd-tabs__map-btn ${state.animating ? 'is-open' : ''}`" @click="toggleMap"> <view :class="`wd-tabs__map-btn ${state.animating ? 'is-open' : ''}`" @click="toggleMap">
<view :class="`wd-tabs__map-arrow ${state.animating ? 'is-open' : ''}`"> <view :class="`wd-tabs__map-arrow ${state.animating ? 'is-open' : ''}`">
<wd-icon name="arrow-down" /> <wd-icon name="arrow-down" />
@ -38,7 +38,7 @@
{{ mapTitle || translate('all') }} {{ mapTitle || translate('all') }}
</view> </view>
<view :class="`wd-tabs__map-body ${state.animating ? 'is-open' : ''}`" :style="state.mapShow ? '' : 'display:none'"> <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 items" :key="index" @click="handleSelect(index)"> <view class="wd-tabs__map-nav-item" v-for="(item, index) in children" :key="index" @click="handleSelect(index)">
<view <view
:class="`wd-tabs__map-nav-btn ${state.activeIndex === index ? 'is-active' : ''} ${item.disabled ? 'is-disabled' : ''}`" :class="`wd-tabs__map-nav-btn ${state.activeIndex === index ? 'is-active' : ''} ${item.disabled ? 'is-disabled' : ''}`"
:style=" :style="
@ -59,14 +59,12 @@
</view> </view>
</wd-sticky> </wd-sticky>
<!--标签页-->
<view class="wd-tabs__container" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd" @touchcancel="onTouchEnd"> <view class="wd-tabs__container" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd" @touchcancel="onTouchEnd">
<view :class="['wd-tabs__body', animated ? 'is-animated' : '']" :style="bodyStyle"> <view :class="['wd-tabs__body', animated ? 'is-animated' : '']" :style="bodyStyle">
<slot /> <slot />
</view> </view>
</view> </view>
<!--map表的阴影浮层-->
<view <view
class="wd-tabs__mask" class="wd-tabs__mask"
:style="`${state.mapShow ? '' : 'display:none;'} ${state.animating ? 'opacity:1;' : ''}`" :style="`${state.mapShow ? '' : 'display:none;'} ${state.animating ? 'opacity:1;' : ''}`"
@ -77,30 +75,29 @@
</template> </template>
<template v-else> <template v-else>
<view :class="`wd-tabs ${customClass} ${innerSlidable ? 'is-slide' : ''} ${mapNum < items.length && mapNum !== 0 ? 'is-map' : ''}`"> <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">
<view class="wd-tabs__nav--wrap"> <view class="wd-tabs__nav--wrap">
<scroll-view :scroll-x="innerSlidable" scroll-with-animation :scroll-left="state.scrollLeft"> <scroll-view :scroll-x="innerSlidable" scroll-with-animation :scroll-left="state.scrollLeft">
<view class="wd-tabs__nav-container"> <view class="wd-tabs__nav-container">
<!--nav列表-->
<view <view
v-for="(item, index) in items" v-for="(item, index) in children"
@click="handleSelect(index)" @click="handleSelect(index)"
:key="index" :key="index"
:class="`wd-tabs__nav-item ${state.activeIndex === index ? 'is-active' : ''} ${item.disabled ? 'is-disabled' : ''}`" :class="`wd-tabs__nav-item ${state.activeIndex === index ? 'is-active' : ''} ${item.disabled ? 'is-disabled' : ''}`"
:style="state.activeIndex === index ? (color ? 'color:' + color : '') : inactiveColor ? 'color:' + inactiveColor : ''" :style="state.activeIndex === index ? (color ? 'color:' + color : '') : inactiveColor ? 'color:' + inactiveColor : ''"
> >
<text class="wd-tabs__nav-item-text">{{ item.title }}</text> <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 class="wd-tabs__line wd-tabs__line--inner" v-if="state.activeIndex === index && state.useInnerLine"></view>
</view> </view>
<!--下划线-->
<view class="wd-tabs__line" :style="state.lineStyle"></view> <view class="wd-tabs__line" :style="state.lineStyle"></view>
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
<!--map表--> <view class="wd-tabs__map" v-if="mapNum < children.length && mapNum !== 0">
<view class="wd-tabs__map" v-if="mapNum < items.length && mapNum !== 0">
<view class="wd-tabs__map-btn" @click="toggleMap"> <view class="wd-tabs__map-btn" @click="toggleMap">
<view :class="`wd-tabs__map-arrow ${state.animating ? 'is-open' : ''}`"> <view :class="`wd-tabs__map-arrow ${state.animating ? 'is-open' : ''}`">
<wd-icon name="arrow-down" /> <wd-icon name="arrow-down" />
@ -110,7 +107,7 @@
{{ translate('all') }} {{ translate('all') }}
</view> </view>
<view :class="`wd-tabs__map-body ${state.animating ? 'is-open' : ''}`" :style="state.mapShow ? '' : 'display:none'"> <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 items" :key="index" @click="handleSelect(index)"> <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' : ''}`"> <view :class="`wd-tabs__map-nav-btn ${state.activeIndex === index ? 'is-active' : ''} ${item.disabled ? 'is-disabled' : ''}`">
{{ item.title }} {{ item.title }}
</view> </view>
@ -119,14 +116,12 @@
</view> </view>
</view> </view>
<!--标签页-->
<view class="wd-tabs__container" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd" @touchcancel="onTouchEnd"> <view class="wd-tabs__container" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd" @touchcancel="onTouchEnd">
<view :class="['wd-tabs__body', animated ? 'is-animated' : '']" :style="bodyStyle"> <view :class="['wd-tabs__body', animated ? 'is-animated' : '']" :style="bodyStyle">
<slot /> <slot />
</view> </view>
</view> </view>
<!--map表的阴影浮层-->
<view class="wd-tabs__mask" :style="`${state.mapShow ? '' : 'display:none;'} ${state.animating ? 'opacity:1' : ''}`" @click="toggleMap"></view> <view class="wd-tabs__mask" :style="`${state.mapShow ? '' : 'display:none;'} ${state.animating ? 'opacity:1' : ''}`" @click="toggleMap"></view>
</view> </view>
</template> </template>
@ -145,7 +140,7 @@ export default {
import wdIcon from '../wd-icon/wd-icon.vue' import wdIcon from '../wd-icon/wd-icon.vue'
import wdSticky from '../wd-sticky/wd-sticky.vue' import wdSticky from '../wd-sticky/wd-sticky.vue'
import wdStickyBox from '../wd-sticky-box/wd-sticky-box.vue' import wdStickyBox from '../wd-sticky-box/wd-sticky-box.vue'
import { computed, getCurrentInstance, onMounted, watch, nextTick, reactive, type CSSProperties } from '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 { addUnit, checkNumRange, debounce, getRect, isDef, isNumber, isString, objToStyle } from '../common/util'
import { useTouch } from '../composables/useTouch' import { useTouch } from '../composables/useTouch'
import { TABS_KEY, tabsProps } from './types' import { TABS_KEY, tabsProps } from './types'
@ -171,8 +166,6 @@ const state = reactive({
scrollLeft: 0 // scroll-view scrollLeft: 0 // scroll-view
}) })
// map
const { children, linkChildren } = useChildren(TABS_KEY) const { children, linkChildren } = useChildren(TABS_KEY)
linkChildren({ state, props }) linkChildren({ state, props })
@ -181,14 +174,7 @@ const { proxy } = getCurrentInstance() as any
const touch = useTouch() const touch = useTouch()
const innerSlidable = computed(() => { const innerSlidable = computed(() => {
return props.slidable === 'always' || items.value.length > props.slidableNum return props.slidable === 'always' || children.length > props.slidableNum
})
// tabs
const items = computed(() => {
return children.map((child, index) => {
return { disabled: child.disabled, title: child.title, name: isDef(child.name) ? child.name : index }
})
}) })
const bodyStyle = computed(() => { const bodyStyle = computed(() => {
@ -203,6 +189,10 @@ const bodyStyle = computed(() => {
}) })
}) })
const getTabName = (tab: ComponentInstance<any>, index: number) => {
return isDef(tab.name) ? tab.name : index
}
/** /**
* 更新激活项 * 更新激活项
* @param value 激活值 * @param value 激活值
@ -211,11 +201,11 @@ const bodyStyle = computed(() => {
*/ */
const updateActive = (value: number | string = 0, init: boolean = false, setScroll: boolean = true) => { const updateActive = (value: number | string = 0, init: boolean = false, setScroll: boolean = true) => {
// tab // tab
if (items.value.length === 0) return if (children.length === 0) return
value = getActiveIndex(value) value = getActiveIndex(value)
// //
if (items.value[value].disabled) return if (children[value].disabled) return
state.activeIndex = value state.activeIndex = value
if (setScroll) { if (setScroll) {
updateLineStyle(init === false) updateLineStyle(init === false)
@ -298,11 +288,7 @@ onMounted(() => {
}) })
}) })
/**
* @description nav map list 开关
*/
function toggleMap() { function toggleMap() {
// displaytransition
if (state.mapShow) { if (state.mapShow) {
state.animating = false state.animating = false
setTimeout(() => { setTimeout(() => {
@ -353,22 +339,19 @@ async function updateLineStyle(animation: boolean = true) {
console.error('[wot design] error(wd-tabs): update line style failed', error) console.error('[wot design] error(wd-tabs): update line style failed', error)
} }
} }
/**
* @description 通过控制tab的active来展示选定的tab
*/
function setActiveTab() { function setActiveTab() {
if (!state.inited) return if (!state.inited) return
if (items.value[state.activeIndex].name !== props.modelValue) { const name = getTabName(children[state.activeIndex], state.activeIndex)
if (name !== props.modelValue) {
emit('change', { emit('change', {
index: state.activeIndex, index: state.activeIndex,
name: items.value[state.activeIndex].name name: name
}) })
emit('update:modelValue', items.value[state.activeIndex].name) emit('update:modelValue', name)
} }
} }
/**
* @description scroll-view滑动到active的tab_nav
*/
function scrollIntoView() { function scrollIntoView() {
if (!state.inited) return if (!state.inited) return
Promise.all([getRect($item, true, proxy), getRect($container, false, proxy)]).then(([navItemsRects, navRect]) => { Promise.all([getRect($item, true, proxy), getRect($container, false, proxy)]).then(([navItemsRects, navRect]) => {
@ -385,13 +368,16 @@ function scrollIntoView() {
} }
}) })
} }
/** /**
* @description 单击tab的处理 * @description 单击tab的处理
* @param index * @param index
*/ */
function handleSelect(index: number) { function handleSelect(index: number) {
if (index === undefined) return if (index === undefined) return
const { name, disabled } = items.value[index] const { disabled } = children[index]
const name = getTabName(children[index], index)
if (disabled) { if (disabled) {
emit('disabled', { emit('disabled', {
index, index,
@ -406,10 +392,6 @@ function handleSelect(index: number) {
name name
}) })
} }
/**
* @description touch handle
* @param event
*/
function onTouchStart(event: any) { function onTouchStart(event: any) {
if (!props.swipeable) return if (!props.swipeable) return
touch.touchStart(event) touch.touchStart(event)
@ -425,22 +407,21 @@ function onTouchEnd() {
if (direction.value === 'horizontal' && offsetX.value >= minSwipeDistance) { if (direction.value === 'horizontal' && offsetX.value >= minSwipeDistance) {
if (deltaX.value > 0 && state.activeIndex !== 0) { if (deltaX.value > 0 && state.activeIndex !== 0) {
setActive(state.activeIndex - 1) setActive(state.activeIndex - 1)
} else if (deltaX.value < 0 && state.activeIndex !== items.value.length - 1) { } else if (deltaX.value < 0 && state.activeIndex !== children.length - 1) {
setActive(state.activeIndex + 1)
setActive(state.activeIndex + 1) setActive(state.activeIndex + 1)
} }
} }
} }
function getActiveIndex(value: number | string) { function getActiveIndex(value: number | string) {
// nameitems0 // namechildren0
if (isNumber(value) && value >= items.value.length) { if (isNumber(value) && value >= children.length) {
// eslint-disable-next-line prettier/prettier // eslint-disable-next-line prettier/prettier
console.error('[wot design] warning(wd-tabs): the type of tabs\' value is Number shouldn\'t be less than its children') console.error('[wot design] warning(wd-tabs): the type of tabs\' value is Number shouldn\'t be less than its children')
value = 0 value = 0
} }
// 0 // 0
if (isString(value)) { if (isString(value)) {
const index = items.value.findIndex((item) => item.name === value) const index = children.findIndex((item) => item.name === value)
value = index === -1 ? 0 : index value = index === -1 ? 0 : index
} }