feat: tabs支持左对齐 (#718)

 Closes: #380
This commit is contained in:
不如摸鱼去 2024-11-18 13:14:03 +08:00 committed by GitHub
parent 8419564e50
commit 314c2e8c9d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 121 additions and 40 deletions

View File

@ -127,16 +127,31 @@ const tab = ref('例子')
</wd-tabs> </wd-tabs>
``` ```
## 左对齐超出即可滚动 <el-tag text style="vertical-align: middle;margin-left:8px;" effect="plain">1.3.15</el-tag>
`slidable`设置为`always`时,所有的标签会向左侧收缩对齐,超出即可滑动。
```html
<wd-tabs v-model="tab" slidable="always">
<block v-for="item in 5" :key="item">
<wd-tab :title="`超大标签${item}`">
<view class="content">内容{{ item }}</view>
</wd-tab>
</block>
</wd-tabs>
```
--- ---
标签页在标签数大于等于 6 个时,可以滑动;当标签数大于等于 10 个时,将会显示导航地图,便于快速定位到某个标签。可以通过设置 `slidable-num` 修改可滑动的数量阈值;设置 `map-num` 修改显示导航地图的阈值。 标签页在标签数大于等于 6 个时,可以滑动;当标签数大于等于 10 个时,将会显示导航地图,便于快速定位到某个标签。可以通过设置 `slidable-num` 修改可滑动的数量阈值;设置 `map-num` 修改显示导航地图的阈值。`slidable`设置为`always`时,所有的标签会向左侧收缩对齐,超出即可滑动。
## Tabs Attributes ## Tabs Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | 最低版本 | | 参数 | 说明 | 类型 | 可选值 | 默认值 | 最低版本 |
| ------------- | -------------------------------- | --------------- | ------ | ------ | -------- | | ------------- | -------------------------------- | --------------- | ------ | ------ | -------- |
| v-model | 绑定值 | string / number | - | - | - | | v-model | 绑定值 | string / number | - | - | - |
| slidable-num | 可滑动的标签数阈值 | number | - | 6 | - | | slidable-num | 可滑动的标签数阈值`slidable`设置为`auto`时生效 | number | - | 6 | - |
| map-num | 显示导航地图的标签数阈值 | number | - | 10 | - | | map-num | 显示导航地图的标签数阈值 | number | - | 10 | - |
| sticky | 粘性布局 | boolean | - | false | - | | sticky | 粘性布局 | boolean | - | false | - |
| offset-top | 粘性布局时距离窗口顶部距离 | number | - | 0 | - | | offset-top | 粘性布局时距离窗口顶部距离 | number | - | 0 | - |
@ -147,6 +162,7 @@ const tab = ref('例子')
| inactiveColor | 非活动标签文字颜色 | string | - | - | - | | inactiveColor | 非活动标签文字颜色 | string | - | - | - |
| animated | 是否开启切换标签内容时的转场动画 | boolean | - | false | - | | animated | 是否开启切换标签内容时的转场动画 | boolean | - | false | - |
| duration | 切换动画过渡时间,单位毫秒 | number | - | 300 | - | | duration | 切换动画过渡时间,单位毫秒 | number | - | 300 | - |
| slidable | 是否开启滚动导航 | TabsSlidable | `always` | `auto` | $LOWEST_VERSION$ |
## Tab Attributes ## Tab Attributes

View File

@ -74,7 +74,7 @@
</demo-block> </demo-block>
<demo-block title="数量大于6时可滚动" transparent> <demo-block title="数量大于6时可滚动" transparent>
<wd-tabs v-model="tab6" lazy-render @change="handleChange"> <wd-tabs v-model="tab6" @change="handleChange">
<block v-for="item in 7" :key="item"> <block v-for="item in 7" :key="item">
<wd-tab :title="`标签${item}`"> <wd-tab :title="`标签${item}`">
<view class="content">内容{{ tab6 + 1 }}</view> <view class="content">内容{{ tab6 + 1 }}</view>
@ -83,6 +83,16 @@
</wd-tabs> </wd-tabs>
</demo-block> </demo-block>
<demo-block title="左对齐超出即可滚动" transparent>
<wd-tabs v-model="tab9" slidable="always" @change="handleChange">
<block v-for="item in 5" :key="item">
<wd-tab :title="`超大标签${item}`">
<view class="content">内容{{ tab9 + 1 }}</view>
</wd-tab>
</block>
</wd-tabs>
</demo-block>
<demo-block title="数量大于10时出现导航地图" transparent> <demo-block title="数量大于10时出现导航地图" transparent>
<wd-tabs v-model="tab7" @change="handleChange"> <wd-tabs v-model="tab7" @change="handleChange">
<block v-for="item in 11" :key="item"> <block v-for="item in 11" :key="item">
@ -108,6 +118,8 @@ const tab5 = ref<number>(0)
const tab6 = ref<number>(0) const tab6 = ref<number>(0)
const tab7 = ref<number>(0) const tab7 = ref<number>(0)
const tab8 = ref<number>(0) const tab8 = ref<number>(0)
const tab9 = ref<number>(0)
const toast = useToast() const toast = useToast()
function handleClick({ index, name }: any) { function handleClick({ index, name }: any) {
console.log('event', { index, name }) console.log('event', { index, name })

View File

@ -1,6 +1,6 @@
<template> <template>
<view :class="`wd-tab ${customClass}`" :style="customStyle"> <view :class="`wd-tab ${customClass}`" :style="customStyle">
<view v-if="painted" class="wd-tab__body" :style="isShow ? '' : 'display: none;'"> <view v-if="painted" class="wd-tab__body" :style="tabBodyStyle">
<slot /> <slot />
</view> </view>
</view> </view>
@ -16,8 +16,8 @@ export default {
} }
</script> </script>
<script lang="ts" setup> <script lang="ts" setup>
import { getCurrentInstance, ref, watch } from 'vue' import { getCurrentInstance, ref, watch, type CSSProperties } from 'vue'
import { isDef, isNumber, isString } from '../common/util' import { isDef, isNumber, isString, objToStyle } from '../common/util'
import { useParent } from '../composables/useParent' import { useParent } from '../composables/useParent'
import { TABS_KEY } from '../wd-tabs/types' import { TABS_KEY } from '../wd-tabs/types'
import { computed } from 'vue' import { computed } from 'vue'
@ -35,6 +35,14 @@ const activeIndex = computed(() => {
return isDef(tabs) ? tabs.state.activeIndex : 0 return isDef(tabs) ? tabs.state.activeIndex : 0
}) })
const tabBodyStyle = computed(() => {
const style: CSSProperties = {}
if (!isShow.value && (!isDef(tabs) || !tabs.props.animated)) {
style.display = 'none'
}
return objToStyle(style)
})
watch( watch(
() => props.name, () => props.name,
(newValue) => { (newValue) => {

View File

@ -91,6 +91,7 @@
} }
@include e(nav-item) { @include e(nav-item) {
position: relative;
flex: 1; flex: 1;
min-width: 0; min-width: 0;
text-align: center; text-align: center;
@ -119,6 +120,11 @@
width: $-tabs-nav-line-width; width: $-tabs-nav-line-width;
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){
left: 50%;
transform: translateX(-50%)
}
} }
@include e(container) { @include e(container) {

View File

@ -1,12 +1,20 @@
import { type ExtractPropTypes, type InjectionKey } from 'vue' import { type ComponentPublicInstance, type ExtractPropTypes, type InjectionKey } from 'vue'
import { baseProps, makeBooleanProp, makeNumberProp, makeNumericProp, makeStringProp, numericProp } from '../common/props' import { baseProps, makeBooleanProp, makeNumberProp, makeNumericProp, makeStringProp, numericProp } from '../common/props'
export type TabsProvide = { export type TabsProvide = {
state: { state: {
activeIndex: number 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 TABS_KEY: InjectionKey<TabsProvide> = Symbol('wd-tabs')
export const tabsProps = { export const tabsProps = {
@ -58,7 +66,24 @@ export const tabsProps = {
/** /**
* *
*/ */
duration: makeNumberProp(300) duration: makeNumberProp(300),
/**
*
* 'auto' | 'always'
* @default auto
*/
slidable: makeStringProp<TabsSlidable>('auto')
}
export type TabsExpose = {
// 修改选中的tab Index
setActive: (value: number | string, init: boolean, setScroll: boolean) => void
// scroll-view滑动到active的tab_nav
scrollIntoView: () => void
// 更新底部条样式
updateLineStyle: (animation: boolean) => void
} }
export type TabsProps = ExtractPropTypes<typeof tabsProps> export type TabsProps = ExtractPropTypes<typeof tabsProps>
export type TabsInstance = ComponentPublicInstance<TabsProps, TabsExpose>

View File

@ -2,14 +2,14 @@
<template v-if="sticky"> <template v-if="sticky">
<wd-sticky-box> <wd-sticky-box>
<view <view
:class="`wd-tabs ${customClass} ${slidableNum < items.length ? 'is-slide' : ''} ${mapNum < items.length && mapNum !== 0 ? 'is-map' : ''}`" :class="`wd-tabs ${customClass} ${innerSlidable ? 'is-slide' : ''} ${mapNum < items.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="slidableNum < items.length" 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列表--> <!--nav列表-->
<view <view
@ -20,6 +20,7 @@
:style="state.activeIndex === index ? (color ? 'color:' + color : '') : inactiveColor ? 'color:' + inactiveColor : ''" :style="state.activeIndex === index ? (color ? 'color:' + color : '') : inactiveColor ? 'color:' + inactiveColor : ''"
> >
{{ item.title }} {{ item.title }}
<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>
@ -76,11 +77,11 @@
</template> </template>
<template v-else> <template v-else>
<view :class="`wd-tabs ${customClass} ${slidableNum < items.length ? 'is-slide' : ''} ${mapNum < items.length && mapNum !== 0 ? 'is-map' : ''}`"> <view :class="`wd-tabs ${customClass} ${innerSlidable ? 'is-slide' : ''} ${mapNum < items.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="slidableNum < items.length" 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列表--> <!--nav列表-->
<view <view
@ -91,6 +92,7 @@
:style="state.activeIndex === index ? (color ? 'color:' + color : '') : inactiveColor ? 'color:' + inactiveColor : ''" :style="state.activeIndex === index ? (color ? 'color:' + color : '') : inactiveColor ? 'color:' + inactiveColor : ''"
> >
{{ item.title }} {{ item.title }}
<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>
@ -143,7 +145,6 @@ 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 } 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'
@ -162,6 +163,7 @@ const { translate } = useTranslate('tabs')
const state = reactive({ const state = reactive({
activeIndex: 0, // activeIndex: 0, //
lineStyle: 'display:none;', // 线 lineStyle: 'display:none;', // 线
useInnerLine: false, // 使线线
inited: false, // inited: false, //
animating: false, // animating: false, //
mapShow: false, // map mapShow: false, // map
@ -171,12 +173,16 @@ const state = reactive({
// map // map
const { children, linkChildren } = useChildren(TABS_KEY) const { children, linkChildren } = useChildren(TABS_KEY)
linkChildren({ state }) linkChildren({ state, props })
const { proxy } = getCurrentInstance() as any const { proxy } = getCurrentInstance() as any
const touch = useTouch() const touch = useTouch()
const innerSlidable = computed(() => {
return props.slidable === 'always' || items.value.length > props.slidableNum
})
// tabs // tabs
const items = computed(() => { const items = computed(() => {
return children.map((child, index) => { return children.map((child, index) => {
@ -197,12 +203,12 @@ const bodyStyle = computed(() => {
}) })
/** /**
* @description 修改选中的tab Index * 更新激活项
* @param {String |Number } value - radio绑定的value或者tab索引默认值0 * @param value 激活值
* @param {Boolean } init - 是否伴随初始化操作 * @param init 是否已初始化
* @param setScroll // scroll-view
*/ */
const setActive = debounce( const updateActive = (value: number | string = 0, init: boolean = false, setScroll: boolean = true) => {
function (value: number | string = 0, init: boolean = false, setScroll: boolean = true) {
// tab // tab
if (items.value.length === 0) return if (items.value.length === 0) return
@ -215,10 +221,14 @@ const setActive = debounce(
scrollIntoView() scrollIntoView()
} }
setActiveTab() setActiveTab()
}, }
100,
{ leading: false } /**
) * @description 修改选中的tab Index
* @param {String |Number } value - radio绑定的value或者tab索引默认值0
* @param {Boolean } init - 是否伴随初始化操作
*/
const setActive = debounce(updateActive, 100, { leading: false })
watch( watch(
() => props.modelValue, () => props.modelValue,
@ -282,7 +292,8 @@ watch(
onMounted(() => { onMounted(() => {
state.inited = true state.inited = true
nextTick(() => { nextTick(() => {
setActive(props.modelValue, true) updateActive(props.modelValue, true)
state.useInnerLine = true
}) })
}) })
@ -305,15 +316,15 @@ function toggleMap() {
} }
/** /**
* @description 更新navBar underline的偏移量 * 更新 underline的偏移量
* @param {Boolean} animation 是否伴随动画 * @param animation 是否开启动画
*/ */
function updateLineStyle(animation: boolean = true) { function updateLineStyle(animation: boolean = true) {
if (!state.inited) return if (!state.inited) return
const { lineWidth, lineHeight } = props const { lineWidth, lineHeight } = props
getRect($item, true, proxy).then((rects) => { getRect($item, true, proxy).then((rects) => {
const lineStyle: CSSProperties = {} const lineStyle: CSSProperties = {}
if (isDef(lineWidth)) { if (isDef(lineWidth)) {
lineStyle.width = addUnit(lineWidth) lineStyle.width = addUnit(lineWidth)
} }
@ -323,11 +334,14 @@ function updateLineStyle(animation: boolean = true) {
} }
const rect = rects[state.activeIndex] const rect = rects[state.activeIndex]
let left = rects.slice(0, state.activeIndex).reduce((prev, curr) => prev + Number(curr.width), 0) + Number(rect.width) / 2 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%)` lineStyle.transform = `translateX(${left}px) translateX(-50%)`
if (animation) { if (animation) {
lineStyle.transition = 'width 300ms ease, transform 300ms ease' lineStyle.transition = 'width 300ms ease, transform 300ms ease'
} }
state.useInnerLine = false
state.lineStyle = objToStyle(lineStyle) state.lineStyle = objToStyle(lineStyle)
}
}) })
} }
/** /**