Skip to content

Commit 314c2e8

Browse files
feat: ✨ tabs支持左对齐 (#718)
Closes: #380
1 parent 8419564 commit 314c2e8

6 files changed

Lines changed: 121 additions & 40 deletions

File tree

docs/component/tabs.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,16 +127,31 @@ const tab = ref('例子')
127127
</wd-tabs>
128128
```
129129

130+
## 左对齐超出即可滚动 <el-tag text style="vertical-align: middle;margin-left:8px;" effect="plain">1.3.15</el-tag>
131+
132+
`slidable`设置为`always`时,所有的标签会向左侧收缩对齐,超出即可滑动。
133+
134+
```html
135+
<wd-tabs v-model="tab" slidable="always">
136+
<block v-for="item in 5" :key="item">
137+
<wd-tab :title="`超大标签${item}`">
138+
<view class="content">内容{{ item }}</view>
139+
</wd-tab>
140+
</block>
141+
</wd-tabs>
142+
```
143+
144+
130145
---
131146

132-
标签页在标签数大于等于 6 个时,可以滑动;当标签数大于等于 10 个时,将会显示导航地图,便于快速定位到某个标签。可以通过设置 `slidable-num` 修改可滑动的数量阈值;设置 `map-num` 修改显示导航地图的阈值。
147+
标签页在标签数大于等于 6 个时,可以滑动;当标签数大于等于 10 个时,将会显示导航地图,便于快速定位到某个标签。可以通过设置 `slidable-num` 修改可滑动的数量阈值;设置 `map-num` 修改显示导航地图的阈值。`slidable`设置为`always`时,所有的标签会向左侧收缩对齐,超出即可滑动。
133148

134149
## Tabs Attributes
135150

136151
| 参数 | 说明 | 类型 | 可选值 | 默认值 | 最低版本 |
137152
| ------------- | -------------------------------- | --------------- | ------ | ------ | -------- |
138153
| v-model | 绑定值 | string / number | - | - | - |
139-
| slidable-num | 可滑动的标签数阈值 | number | - | 6 | - |
154+
| slidable-num | 可滑动的标签数阈值`slidable`设置为`auto`时生效 | number | - | 6 | - |
140155
| map-num | 显示导航地图的标签数阈值 | number | - | 10 | - |
141156
| sticky | 粘性布局 | boolean | - | false | - |
142157
| offset-top | 粘性布局时距离窗口顶部距离 | number | - | 0 | - |
@@ -147,6 +162,7 @@ const tab = ref('例子')
147162
| inactiveColor | 非活动标签文字颜色 | string | - | - | - |
148163
| animated | 是否开启切换标签内容时的转场动画 | boolean | - | false | - |
149164
| duration | 切换动画过渡时间,单位毫秒 | number | - | 300 | - |
165+
| slidable | 是否开启滚动导航 | TabsSlidable | `always` | `auto` | $LOWEST_VERSION$ |
150166

151167
## Tab Attributes
152168

src/pages/tabs/Index.vue

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@
7474
</demo-block>
7575

7676
<demo-block title="数量大于6时可滚动" transparent>
77-
<wd-tabs v-model="tab6" lazy-render @change="handleChange">
77+
<wd-tabs v-model="tab6" @change="handleChange">
7878
<block v-for="item in 7" :key="item">
7979
<wd-tab :title="`标签${item}`">
8080
<view class="content">内容{{ tab6 + 1 }}</view>
@@ -83,6 +83,16 @@
8383
</wd-tabs>
8484
</demo-block>
8585

86+
<demo-block title="左对齐超出即可滚动" transparent>
87+
<wd-tabs v-model="tab9" slidable="always" @change="handleChange">
88+
<block v-for="item in 5" :key="item">
89+
<wd-tab :title="`超大标签${item}`">
90+
<view class="content">内容{{ tab9 + 1 }}</view>
91+
</wd-tab>
92+
</block>
93+
</wd-tabs>
94+
</demo-block>
95+
8696
<demo-block title="数量大于10时出现导航地图" transparent>
8797
<wd-tabs v-model="tab7" @change="handleChange">
8898
<block v-for="item in 11" :key="item">
@@ -108,6 +118,8 @@ const tab5 = ref<number>(0)
108118
const tab6 = ref<number>(0)
109119
const tab7 = ref<number>(0)
110120
const tab8 = ref<number>(0)
121+
const tab9 = ref<number>(0)
122+
111123
const toast = useToast()
112124
function handleClick({ index, name }: any) {
113125
console.log('event', { index, name })

src/uni_modules/wot-design-uni/components/wd-tab/wd-tab.vue

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<template>
22
<view :class="`wd-tab ${customClass}`" :style="customStyle">
3-
<view v-if="painted" class="wd-tab__body" :style="isShow ? '' : 'display: none;'">
3+
<view v-if="painted" class="wd-tab__body" :style="tabBodyStyle">
44
<slot />
55
</view>
66
</view>
@@ -16,8 +16,8 @@ export default {
1616
}
1717
</script>
1818
<script lang="ts" setup>
19-
import { getCurrentInstance, ref, watch } from 'vue'
20-
import { isDef, isNumber, isString } from '../common/util'
19+
import { getCurrentInstance, ref, watch, type CSSProperties } from 'vue'
20+
import { isDef, isNumber, isString, objToStyle } from '../common/util'
2121
import { useParent } from '../composables/useParent'
2222
import { TABS_KEY } from '../wd-tabs/types'
2323
import { computed } from 'vue'
@@ -35,6 +35,14 @@ const activeIndex = computed(() => {
3535
return isDef(tabs) ? tabs.state.activeIndex : 0
3636
})
3737
38+
const tabBodyStyle = computed(() => {
39+
const style: CSSProperties = {}
40+
if (!isShow.value && (!isDef(tabs) || !tabs.props.animated)) {
41+
style.display = 'none'
42+
}
43+
return objToStyle(style)
44+
})
45+
3846
watch(
3947
() => props.name,
4048
(newValue) => {

src/uni_modules/wot-design-uni/components/wd-tabs/index.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
}
9292

9393
@include e(nav-item) {
94+
position: relative;
9495
flex: 1;
9596
min-width: 0;
9697
text-align: center;
@@ -119,6 +120,11 @@
119120
width: $-tabs-nav-line-width;
120121
background: $-tabs-nav-line-bg-color;
121122
border-radius: calc($-tabs-nav-line-height / 2);
123+
124+
@include m(inner){
125+
left: 50%;
126+
transform: translateX(-50%)
127+
}
122128
}
123129

124130
@include e(container) {

src/uni_modules/wot-design-uni/components/wd-tabs/types.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
1-
import { type ExtractPropTypes, type InjectionKey } from 'vue'
1+
import { type ComponentPublicInstance, type ExtractPropTypes, type InjectionKey } from 'vue'
22
import { baseProps, makeBooleanProp, makeNumberProp, makeNumericProp, makeStringProp, numericProp } from '../common/props'
33

44
export type TabsProvide = {
55
state: {
66
activeIndex: number
7+
lineStyle: string // 激活项边框线样式
8+
inited: boolean // 是否初始化
9+
animating: boolean // 是否动画中
10+
mapShow: boolean // map的开关
11+
scrollLeft: number // scroll-view偏移量
712
}
13+
props: Partial<TabsProps>
814
}
915

16+
export type TabsSlidable = 'auto' | 'always'
17+
1018
export const TABS_KEY: InjectionKey<TabsProvide> = Symbol('wd-tabs')
1119

1220
export const tabsProps = {
@@ -58,7 +66,24 @@ export const tabsProps = {
5866
/**
5967
* 切换动画过渡时间,单位毫秒
6068
*/
61-
duration: makeNumberProp(300)
69+
duration: makeNumberProp(300),
70+
/**
71+
* 是否开启滚动导航
72+
* 可选值:'auto' | 'always'
73+
* @default auto
74+
*/
75+
slidable: makeStringProp<TabsSlidable>('auto')
76+
}
77+
78+
export type TabsExpose = {
79+
// 修改选中的tab Index
80+
setActive: (value: number | string, init: boolean, setScroll: boolean) => void
81+
// scroll-view滑动到active的tab_nav
82+
scrollIntoView: () => void
83+
// 更新底部条样式
84+
updateLineStyle: (animation: boolean) => void
6285
}
6386

6487
export type TabsProps = ExtractPropTypes<typeof tabsProps>
88+
89+
export type TabsInstance = ComponentPublicInstance<TabsProps, TabsExpose>

src/uni_modules/wot-design-uni/components/wd-tabs/wd-tabs.vue

Lines changed: 46 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
<template v-if="sticky">
33
<wd-sticky-box>
44
<view
5-
:class="`wd-tabs ${customClass} ${slidableNum < items.length ? 'is-slide' : ''} ${mapNum < items.length && mapNum !== 0 ? 'is-map' : ''}`"
5+
:class="`wd-tabs ${customClass} ${innerSlidable ? 'is-slide' : ''} ${mapNum < items.length && mapNum !== 0 ? 'is-map' : ''}`"
66
:style="customStyle"
77
>
88
<wd-sticky :offset-top="offsetTop">
99
<!--头部导航容器-->
1010
<view class="wd-tabs__nav wd-tabs__nav--sticky">
1111
<view class="wd-tabs__nav--wrap">
12-
<scroll-view :scroll-x="slidableNum < items.length" scroll-with-animation :scroll-left="state.scrollLeft">
12+
<scroll-view :scroll-x="innerSlidable" scroll-with-animation :scroll-left="state.scrollLeft">
1313
<view class="wd-tabs__nav-container">
1414
<!--nav列表-->
1515
<view
@@ -20,6 +20,7 @@
2020
:style="state.activeIndex === index ? (color ? 'color:' + color : '') : inactiveColor ? 'color:' + inactiveColor : ''"
2121
>
2222
{{ item.title }}
23+
<view class="wd-tabs__line wd-tabs__line--inner" v-if="state.activeIndex === index && state.useInnerLine"></view>
2324
</view>
2425
<!--下划线-->
2526
<view class="wd-tabs__line" :style="state.lineStyle"></view>
@@ -76,11 +77,11 @@
7677
</template>
7778

7879
<template v-else>
79-
<view :class="`wd-tabs ${customClass} ${slidableNum < items.length ? 'is-slide' : ''} ${mapNum < items.length && mapNum !== 0 ? 'is-map' : ''}`">
80+
<view :class="`wd-tabs ${customClass} ${innerSlidable ? 'is-slide' : ''} ${mapNum < items.length && mapNum !== 0 ? 'is-map' : ''}`">
8081
<!--头部导航容器-->
8182
<view class="wd-tabs__nav">
8283
<view class="wd-tabs__nav--wrap">
83-
<scroll-view :scroll-x="slidableNum < items.length" scroll-with-animation :scroll-left="state.scrollLeft">
84+
<scroll-view :scroll-x="innerSlidable" scroll-with-animation :scroll-left="state.scrollLeft">
8485
<view class="wd-tabs__nav-container">
8586
<!--nav列表-->
8687
<view
@@ -91,6 +92,7 @@
9192
:style="state.activeIndex === index ? (color ? 'color:' + color : '') : inactiveColor ? 'color:' + inactiveColor : ''"
9293
>
9394
{{ item.title }}
95+
<view class="wd-tabs__line wd-tabs__line--inner" v-if="state.activeIndex === index && state.useInnerLine"></view>
9496
</view>
9597
<!--下划线-->
9698
<view class="wd-tabs__line" :style="state.lineStyle"></view>
@@ -143,7 +145,6 @@ export default {
143145
import wdIcon from '../wd-icon/wd-icon.vue'
144146
import wdSticky from '../wd-sticky/wd-sticky.vue'
145147
import wdStickyBox from '../wd-sticky-box/wd-sticky-box.vue'
146-
147148
import { computed, getCurrentInstance, onMounted, watch, nextTick, reactive, type CSSProperties } from 'vue'
148149
import { addUnit, checkNumRange, debounce, getRect, isDef, isNumber, isString, objToStyle } from '../common/util'
149150
import { useTouch } from '../composables/useTouch'
@@ -162,6 +163,7 @@ const { translate } = useTranslate('tabs')
162163
const state = reactive({
163164
activeIndex: 0, // 选中值的索引,默认第一个
164165
lineStyle: 'display:none;', // 激活项边框线样式
166+
useInnerLine: false, // 是否使用内部激活项边框线,当外部激活下划线未成功渲染时显示内部定位的
165167
inited: false, // 是否初始化
166168
animating: false, // 是否动画中
167169
mapShow: false, // map的开关
@@ -171,12 +173,16 @@ const state = reactive({
171173
// map的开关
172174
173175
const { children, linkChildren } = useChildren(TABS_KEY)
174-
linkChildren({ state })
176+
linkChildren({ state, props })
175177
176178
const { proxy } = getCurrentInstance() as any
177179
178180
const touch = useTouch()
179181
182+
const innerSlidable = computed(() => {
183+
return props.slidable === 'always' || items.value.length > props.slidableNum
184+
})
185+
180186
// tabs数据
181187
const items = computed(() => {
182188
return children.map((child, index) => {
@@ -196,29 +202,33 @@ const bodyStyle = computed(() => {
196202
})
197203
})
198204
205+
/**
206+
* 更新激活项
207+
* @param value 激活值
208+
* @param init 是否已初始化
209+
* @param setScroll // 是否设置scroll-view滚动
210+
*/
211+
const updateActive = (value: number | string = 0, init: boolean = false, setScroll: boolean = true) => {
212+
// 没有tab子元素,不执行任何操作
213+
if (items.value.length === 0) return
214+
215+
value = getActiveIndex(value)
216+
// 被禁用,不执行任何操作
217+
if (items.value[value].disabled) return
218+
state.activeIndex = value
219+
if (setScroll) {
220+
updateLineStyle(init === false)
221+
scrollIntoView()
222+
}
223+
setActiveTab()
224+
}
225+
199226
/**
200227
* @description 修改选中的tab Index
201228
* @param {String |Number } value - radio绑定的value或者tab索引,默认值0
202229
* @param {Boolean } init - 是否伴随初始化操作
203230
*/
204-
const setActive = debounce(
205-
function (value: number | string = 0, init: boolean = false, setScroll: boolean = true) {
206-
// 没有tab子元素,不执行任何操作
207-
if (items.value.length === 0) return
208-
209-
value = getActiveIndex(value)
210-
// 被禁用,不执行任何操作
211-
if (items.value[value].disabled) return
212-
state.activeIndex = value
213-
if (setScroll) {
214-
updateLineStyle(init === false)
215-
scrollIntoView()
216-
}
217-
setActiveTab()
218-
},
219-
100,
220-
{ leading: false }
221-
)
231+
const setActive = debounce(updateActive, 100, { leading: false })
222232
223233
watch(
224234
() => props.modelValue,
@@ -282,7 +292,8 @@ watch(
282292
onMounted(() => {
283293
state.inited = true
284294
nextTick(() => {
285-
setActive(props.modelValue, true)
295+
updateActive(props.modelValue, true)
296+
state.useInnerLine = true
286297
})
287298
})
288299
@@ -305,15 +316,15 @@ function toggleMap() {
305316
}
306317
307318
/**
308-
* @description 更新navBar underline的偏移量
309-
* @param {Boolean} animation 是否伴随动画
319+
* 更新 underline的偏移量
320+
* @param animation 是否开启动画
310321
*/
311322
function updateLineStyle(animation: boolean = true) {
312323
if (!state.inited) return
313324
const { lineWidth, lineHeight } = props
325+
314326
getRect($item, true, proxy).then((rects) => {
315327
const lineStyle: CSSProperties = {}
316-
317328
if (isDef(lineWidth)) {
318329
lineStyle.width = addUnit(lineWidth)
319330
}
@@ -323,11 +334,14 @@ function updateLineStyle(animation: boolean = true) {
323334
}
324335
const rect = rects[state.activeIndex]
325336
let left = rects.slice(0, state.activeIndex).reduce((prev, curr) => prev + Number(curr.width), 0) + Number(rect.width) / 2
326-
lineStyle.transform = `translateX(${left}px) translateX(-50%)`
327-
if (animation) {
328-
lineStyle.transition = 'width 300ms ease, transform 300ms ease'
337+
if (left) {
338+
lineStyle.transform = `translateX(${left}px) translateX(-50%)`
339+
if (animation) {
340+
lineStyle.transition = 'width 300ms ease, transform 300ms ease'
341+
}
342+
state.useInnerLine = false
343+
state.lineStyle = objToStyle(lineStyle)
329344
}
330-
state.lineStyle = objToStyle(lineStyle)
331345
})
332346
}
333347
/**

0 commit comments

Comments
 (0)