1
Huangxiaoqiang-03
2024-11-11 d100db102ded4dc2047f1b92f4ed0ed4c18d8ee4
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
<template>
  <view class="tn-tabs-class tn-tabs" :class="[backgroundColorClass]" :style="{backgroundColor: backgroundColorStyle, marginTop: $t.string.getLengthUnitValue(top, 'px')}">
    
    <!-- _tgetRect()对组件根节点无效,因为写了.in(this),故这里获取内层接点尺寸 -->
    <view :id="id">
      <scroll-view scroll-x class="tn-tabs__scroll-view" :scroll-left="scrollLeft" scroll-with-animation>
        <view class="tn-tabs__scroll-view__box" :class="{'tn-tabs__scroll-view--flex': !isScroll}">
          <!-- item -->
          <view
            v-for="(item, index) in list"
            :key="index"
            :id="'tn-tabs__scroll-view__item-' + index"
            class="tn-tabs__scroll-view__item tn-text-ellipsis"
            :style="[tabItemStyle(index)]"
            @tap="clickTab(index)"
          >
            <tn-badge v-if="item[count] || item['count']" backgroundColor="tn-bg-red" fontColor="#FFFFFF" :absolute="true" :top="badgeOffset[0] || 0" :right="badgeOffset[1] || 0">{{ item[count] || item['count']}}</tn-badge>
            {{ item[name] || item['name'] }}
          </view>
          
          <!-- 底部滑块 -->
          <view v-if="showBar" class="tn-tabs__bar" :style="[tabBarStyle]"></view>
        </view>
      </scroll-view>
    </view>
  </view>
</template>
 
<script>
  import componentsColor from '../../libs/mixin/components_color.js'
  export default {
    mixins: [componentsColor],
    name: 'tn-tabs',
    props: {
      // 标签列表
      list: {
        type: Array,
        default() {
          return []
        }
      },
      // 列表数据tab名称的属性
      name: {
        type: String,
        default: 'name'
      },
      // 列表数据微标数量的属性
      count: {
        type: String,
        default: 'count'
      },
      // 当前活动的tab索引
      current: {
        type: Number,
        default: 0
      },
      // 菜单是否可以滑动
      isScroll: {
        type: Boolean,
        default: true
      },
      // 高度
      height: {
        type: Number,
        default: 80
      },
      // 距离顶部的距离(px)
      top: {
        type: Number,
        default: 0
      },
      // item的宽度
      itemWidth: {
        type: [String, Number],
        default: 'auto'
      },
      // 过渡动画时长
      duration: {
        type: Number,
        default: 0.3
      },
      // 选中时的颜色
      activeColor: {
        type: String,
        default: '#01BEFF'
      },
      // 未被选中时的颜色
      inactiveColor: {
        type: String,
        default: '#080808'
      },
      // 选中的item样式
      activeItemStyle: {
        type: Object,
        default() {
          return {}
        }
      },
      // 是否显示底部滑块
      showBar: {
        type: Boolean,
        default: true
      },
      // 底部滑块的宽度
      barWidth: {
        type: Number,
        default: 40
      },
      // 底部滑块的高度
      barHeight: {
        type: Number,
        default: 6
      },
      // 自定义底部滑块的样式
      barStyle: {
        type: Object,
        default() {
          return {}
        }
      },
      // 单个tab的左右内边距
      gutter: {
        type: Number,
        default: 30
      },
      // 微标的偏移数[top, right]
      badgeOffset: {
        type: Array,
        default() {
          return [20, 22]
        }
      },
      // 是否加粗字体
      bold: {
        type: Boolean,
        default: false
      }
    },
    computed: {
      // 底部滑块样式
      tabBarStyle() {
        let style = {
          width: this.$t.string.getLengthUnitValue(this.barWidth),
          height: this.$t.string.getLengthUnitValue(this.barHeight),
          borderRadius: `${this.barHeight / 2}rpx`,
          backgroundColor: this.activeColor,
          opacity: this.barMoveFirst ? 0 : 1,
          transform: `translate(${this.scrollBarLeft}px, -100%)`,
          transitionDuration: this.barMoveFirst ? '0s' : `${this.duration}s`
        }
        Object.assign(style, this.barStyle)
        return style
      },
      // tabItem样式
      tabItemStyle() {
        return index => {
          let style = {
            width: this.$t.string.getLengthUnitValue(this.itemWidth),
            height: this.$t.string.getLengthUnitValue(this.height),
            lineHeight: this.$t.string.getLengthUnitValue(this.height),
            fontSize: this.fontSizeStyle || '28rpx',
            padding: this.isScroll ? `0 ${this.gutter}rpx` : '',
            flex: this.isScroll ? 'auto' : '1',
            transitionDuration: `${this.duration}s`
          }
          if (index === this.currentIndex) {
            if (this.bold) {
              style.fontWeight = 'bold'
            }
            style.color = this.activeColor
            Object.assign(style, this.activeItemStyle)
          } else {
            style.color = this.inactiveColor
          }
          return style
        }
      }
    },
    data() {
      return {
        // id值
        id: this.$t.uuid(),
        // 滚动scroll-view的左边距离
        scrollLeft: 0,
        // 存放查询后tab菜单的节点信息
        tabQueryInfo: [],
        // 组件宽度
        componentWidth: 0,
        // 底部滑块的移动距离
        scrollBarLeft: 0,
        // 组件到屏幕左边的巨鹿
        componentLeft: 0,
        // 当前选中的itemIndex
        currentIndex: this.current,
        // 标记底部滑块是否第一次移动,第一次移动的时候不触发动画
        barMoveFirst: true
      }
    },
    watch: {
      // 监听tab的变化,重新计算tab菜单信息
      list(newValue, oldValue) {
        // list变化时,重置内部索引,防止出现超过数据边界的问题
        if (newValue.length !== oldValue.length) this.currentIndex = 0
        this.$nextTick(() => {
          this.init()
        })
      },
      current: {
        handler(val) {
          this.$nextTick(() => {
            this.currentIndex = val
            this.scrollByIndex()
          })
        },
        immediate: true
      }
    },
    mounted() {
      this.init()
    },
    methods: {
      // 初始化变量
      async init() {
        // 获取tabs组件的信息
        let tabRect = await this._tGetRect('#' + this.id)
        // 计算组件的宽度
        this.componentLeft = tabRect.left
        this.componentWidth = tabRect.width
        this.getTabRect()
      },
      // 点击tab菜单
      clickTab(index) {
        if (index === this.currentIndex) return
        this.$emit('change', index)
      },
      // 查询tab的布局信息
      getTabRect() {
        let query = uni.createSelectorQuery().in(this)
        // 遍历所有的tab
        for (let i = 0; i < this.list.length; i++) {
          query.select(`#tn-tabs__scroll-view__item-${i}`).fields({
            size: true,
            rect: true
          })
        }
        query.exec((res) => {
          this.tabQueryInfo = res
          // 初始滚动条和底部滑块的位置
          this.scrollByIndex()
        })
      },
      // 滚动scrollView,让活动的tab处于屏幕中间
      scrollByIndex() {
        // 当前获取tab的布局信息
        let tabInfo = this.tabQueryInfo[this.currentIndex]
        if (!tabInfo) return
        
        // 活动tab的宽度
        let tabWidth = tabInfo.width
        // 活动item的左边到组件左边的距离
        let offsetLeft = tabInfo.left - this.componentLeft
        // 计算scroll-view移动的距离
        let scrollLeft = offsetLeft - (this.componentWidth - tabWidth) / 2
        this.scrollLeft = scrollLeft < 0 ? 0 : scrollLeft
        
        // 计算当前滑块需要移动的距离,当前活动item的中点到左边的距离减去滑块宽度的一半
        let left = tabInfo.left + tabInfo.width / 2 - this.componentLeft
        
        // 计算当前活跃item到组件左边的距离
        this.scrollBarLeft = left - uni.upx2px(this.barWidth) / 2
        
        // 防止在计算时出错,所以延迟执行标记不是第一次移动
        if (this.barMoveFirst) {
          setTimeout(() => {
            this.barMoveFirst = false
          }, 100)
        }
      }
    }
  }
</script>
 
<style lang="scss" scoped>
  
  /* #ifndef APP-NVUE */
  ::-webkit-scrollbar {
    display: none;
    width: 0 !important;
    height: 0 !important;
    -webkit-appearance: none;
    background: transparent;
  }
  /* #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 */
  
  .tn-tabs {
    &__scroll-view {
      position: relative;
      width: 100%;
      white-space: nowrap;
      
      &__box {
        position: relative;
        /* #ifdef MP-TOUTIAO */
        white-space: nowrap;
        /* #endif */
      }
      
      &__item {
        position: relative;
        /* #ifndef APP-NVUE */
        display: inline-block;
        /* #endif */
        text-align: center;
        transition-property: background-color, color;
      }
      
      &--flex {
        display: flex;
        flex-direction: row;
        justify-content: space-between;
      }
    }
    
    &__bar {
      position: absolute;
      bottom: 0;
    }
  }
</style>