u-tabs.vue
11.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
<template>
<view class="u-tabs" :style="{
background: bgColor
}">
<!-- $u.getRect()对组件根节点无效,因为写了.in(this),故这里获取内层接点尺寸 -->
<view>
<scroll-view scroll-x class="u-scroll-view" :scroll-left="scrollLeft" scroll-with-animation>
<view class="u-scroll-box" :id="id" :class="{'u-tabs-scorll-flex': !isScroll}">
<view class="u-tab-item u-line-1" :id="'u-tab-item-' + index" v-for="(item, index) in list" :key="index" @tap="clickTab(index)"
:style="[tabItemStyle(index)]">
<u-badge :count="item[count] || item['count'] || 0" :offset="offset" size="mini"></u-badge>
{{ item[name] || item['name']}}
</view>
<view v-if="showBar" class="u-tab-bar" :style="[tabBarStyle]"></view>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
/**
* tabs 标签
* @description 该组件,是一个tabs标签组件,在标签多的时候,可以配置为左右滑动,标签少的时候,可以禁止滑动。 该组件的一个特点是配置为滚动模式时,激活的tab会自动移动到组件的中间位置。
* @tutorial https://www.uviewui.com/components/tabs.html
* @property {Boolean} is-scroll tabs是否可以左右拖动(默认true)
* @property {Array} list 标签数组,元素为对象,如[{name: '推荐'}]
* @property {String Number} current 指定哪个tab为激活状态(默认0)
* @property {String Number} height 导航栏的高度,单位rpx(默认80)
* @property {String Number} font-size tab文字大小,单位rpx(默认30)
* @property {String Number} duration 滑块移动一次所需的时间,单位秒(默认0.5)
* @property {String} active-color 滑块和激活tab文字的颜色(默认#2979ff)
* @property {String} inactive-color tabs文字颜色(默认#303133)
* @property {String Number} bar-width 滑块宽度,单位rpx(默认40)
* @property {Object} active-item-style 活动tabs item的样式,对象形式
* @property {Object} bar-style 底部滑块的样式,对象形式
* @property {Boolean} show-bar 是否显示底部的滑块(默认true)
* @property {String Number} bar-height 滑块高度,单位rpx(默认6)
* @property {String Number} item-width 标签的宽度(默认auto)
* @property {String Number} gutter 单个tab标签的左右内边距之和,单位rpx(默认40)
* @property {String} bg-color tabs导航栏的背景颜色(默认#ffffff)
* @property {String} name 组件内部读取的list参数中的属性名(tab名称),见官网说明(默认name)
* @property {String} count 组件内部读取的list参数中的属性名(badge徽标数),同name属性的使用,见官网说明(默认count)
* @property {Array} offset 设置badge徽标数的位置偏移,格式为 [x, y],也即设置的为top和right的值,单位rpx(默认[5, 20])
* @property {Boolean} bold 激活选项的字体是否加粗(默认true)
* @event {Function} change 点击标签时触发
* @example <u-tabs ref="tabs" :list="list" :is-scroll="false"></u-tabs>
*/
export default {
name: "u-tabs",
props: {
// 导航菜单是否需要滚动,如只有2或者3个的时候,就不需要滚动了,此时使用flex平分tab的宽度
isScroll: {
type: Boolean,
default: true
},
//需循环的标签列表
list: {
type: Array,
default () {
return [];
}
},
// 当前活动tab的索引
current: {
type: [Number, String],
default: 0
},
// 导航栏的高度和行高
height: {
type: [String, Number],
default: 80
},
// 字体大小
fontSize: {
type: [String, Number],
default: 30
},
// 过渡动画时长, 单位ms
duration: {
type: [String, Number],
default: 0.5
},
// 选中项的主题颜色
activeColor: {
type: String,
default: '#2979ff'
},
// 未选中项的颜色
inactiveColor: {
type: String,
default: '#303133'
},
// 菜单底部移动的bar的宽度,单位rpx
barWidth: {
type: [String, Number],
default: 40
},
// 移动bar的高度
barHeight: {
type: [String, Number],
default: 6
},
// 单个tab的左或有内边距(左右相同)
gutter: {
type: [String, Number],
default: 30
},
// 导航栏的背景颜色
bgColor: {
type: String,
default: '#ffffff'
},
// 读取传入的数组对象的属性(tab名称)
name: {
type: String,
default: 'name'
},
// 读取传入的数组对象的属性(徽标数)
count: {
type: String,
default: 'count'
},
// 徽标数位置偏移
offset: {
type: Array,
default: () => {
return [5, 20]
}
},
// 活动tab字体是否加粗
bold: {
type: Boolean,
default: true
},
// 当前活动tab item的样式
activeItemStyle: {
type: Object,
default() {
return {}
}
},
// 是否显示底部的滑块
showBar: {
type: Boolean,
default: true
},
// 底部滑块的自定义样式
barStyle: {
type: Object,
default() {
return {}
}
},
// 标签的宽度
itemWidth: {
type: [Number, String],
default: 'auto'
}
},
data() {
return {
scrollLeft: 0, // 滚动scroll-view的左边滚动距离
tabQueryInfo: [], // 存放对tab菜单查询后的节点信息
componentWidth: 0, // 屏幕宽度,单位为px
scrollBarLeft: 0, // 移动bar需要通过translateX()移动的距离
parentLeft: 0, // 父元素(tabs组件)到屏幕左边的距离
id: this.$u.guid(), // id值
currentIndex: this.current,
barFirstTimeMove: true, // 滑块第一次移动时(页面刚生成时),无需动画,否则给人怪异的感觉
};
},
watch: {
// 监听tab的变化,重新计算tab菜单的布局信息,因为实际使用中菜单可能是通过
// 后台获取的(如新闻app顶部的菜单),获取返回需要一定时间,所以list变化时,重新获取布局信息
list(n, o) {
// list变动时,重制内部索引,否则可能导致超出数组边界的情况
if(n.length !== o.length) this.currentIndex = 0;
// 用$nextTick等待视图更新完毕后再计算tab的局部信息,否则可能因为tab还没生成就获取,就会有问题
this.$nextTick(() => {
this.init();
});
},
current: {
immediate: true,
handler(nVal, oVal) {
// 视图更新后再执行移动操作
this.$nextTick(() => {
this.currentIndex = nVal;
this.scrollByIndex();
});
}
},
},
computed: {
// 移动bar的样式
tabBarStyle() {
let style = {
width: this.barWidth + 'rpx',
transform: `translate(${this.scrollBarLeft}px, -100%)`,
// 滑块在页面渲染后第一次滑动时,无需动画效果
'transition-duration': `${this.barFirstTimeMove ? 0 : this.duration }s`,
'background-color': this.activeColor,
height: this.barHeight + 'rpx',
opacity: this.barFirstTimeMove ? 0 : 1,
// 设置一个很大的值,它会自动取能用的最大值,不用高度的一半,是因为高度可能是单数,会有小数出现
'border-radius': `${this.barHeight / 2}px`
};
Object.assign(style, this.barStyle);
return style;
},
// tab的样式
tabItemStyle() {
return (index) => {
let style = {
height: this.height + 'rpx',
'line-height': this.height + 'rpx',
'font-size': this.fontSize + 'rpx',
'transition-duration': `${this.duration}s`,
padding: this.isScroll ? `0 ${this.gutter}rpx` : '',
flex: this.isScroll ? 'auto' : '1',
width: this.$u.addUnit(this.itemWidth)
};
// 字体加粗
if (index == this.currentIndex && this.bold) style.fontWeight = 'bold';
if (index == this.currentIndex) {
style.color = this.activeColor;
// 给选中的tab item添加外部自定义的样式
style = Object.assign(style, this.activeItemStyle);
} else {
style.color = this.inactiveColor;
}
return style;
}
}
},
methods: {
// 设置一个init方法,方便多处调用
async init() {
// 获取tabs组件的尺寸信息
let tabRect = await this.$uGetRect('#' + this.id);
// tabs组件距离屏幕左边的宽度
this.parentLeft = tabRect.left;
// tabs组件的宽度
this.componentWidth = tabRect.width;
this.getTabRect();
},
// 点击某一个tab菜单
clickTab(index) {
// 点击当前活动tab,不触发事件
if(index == this.currentIndex) return ;
// 发送事件给父组件
this.$emit('change', index);
},
// 查询tab的布局信息
getTabRect() {
// 创建节点查询
let query = uni.createSelectorQuery().in(this);
// 历遍所有tab,这里是执行了查询,最终使用exec()会一次性返回查询的数组结果
for (let i = 0; i < this.list.length; i++) {
// 只要size和rect两个参数
query.select(`#u-tab-item-${i}`).fields({
size: true,
rect: true
});
}
// 执行查询,一次性获取多个结果
query.exec(
function(res) {
this.tabQueryInfo = res;
// 初始化滚动条和移动bar的位置
this.scrollByIndex();
}.bind(this)
);
},
// 滚动scroll-view,让活动的tab处于屏幕的中间位置
scrollByIndex() {
// 当前活动tab的布局信息,有tab菜单的width和left(为元素左边界到父元素左边界的距离)等信息
let tabInfo = this.tabQueryInfo[this.currentIndex];
if (!tabInfo) return;
// 活动tab的宽度
let tabWidth = tabInfo.width;
// 活动item的左边到tabs组件左边的距离,用item的left减去tabs的left
let offsetLeft = tabInfo.left - this.parentLeft;
// 将活动的tabs-item移动到屏幕正中间,实际上是对scroll-view的移动
let scrollLeft = offsetLeft - (this.componentWidth - tabWidth) / 2;
this.scrollLeft = scrollLeft < 0 ? 0 : scrollLeft;
// 当前活动item的中点点到左边的距离减去滑块宽度的一半,即可得到滑块所需的移动距离
let left = tabInfo.left + tabInfo.width / 2 - this.parentLeft;
// 计算当前活跃item到组件左边的距离
this.scrollBarLeft = left - uni.upx2px(this.barWidth) / 2;
// 第一次移动滑块的时候,barFirstTimeMove为true,放到延时中将其设置false
// 延时是因为scrollBarLeft作用于computed计算时,需要一个过程需,否则导致出错
if(this.barFirstTimeMove == true) {
setTimeout(() => {
this.barFirstTimeMove = false;
}, 100)
}
}
},
mounted() {
this.init();
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
view,
scroll-view {
box-sizing: border-box;
}
/* #ifndef APP-NVUE */
::-webkit-scrollbar,
::-webkit-scrollbar,
::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
--webkit-appearance: none;
background: transparent;
}
/* #endif */
.u-scroll-box {
position: relative;
/* #ifdef MP-TOUTIAO */
white-space: nowrap;
/* #endif */
}
/* #ifdef H5 */
// 通过样式穿透,隐藏H5下,scroll-view下的滚动条
scroll-view ::v-deep ::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
--webkit-appearance: none;
background: transparent;
}
/* #endif */
.u-scroll-view {
width: 100%;
white-space: nowrap;
position: relative;
}
.u-tab-item {
position: relative;
/* #ifndef APP-NVUE */
display: inline-block;
/* #endif */
text-align: center;
transition-property: background-color, color;
}
.u-tab-bar {
position: absolute;
bottom: 0;
}
.u-tabs-scorll-flex {
@include vue-flex;
justify-content: space-between;
}
</style>