guangchao.xu | 070005a | 2020-12-07 09:56:40 +0800 | [diff] [blame^] | 1 | <template> |
| 2 | <view class="u-tabs" :style="{ |
| 3 | zIndex: zIndex, |
| 4 | background: bgColor |
| 5 | }"> |
| 6 | <scroll-view scroll-x class="u-scroll-view" :scroll-left="scrollLeft" scroll-with-animation :style="{ zIndex: zIndex + 1 }"> |
| 7 | <view class="u-tabs-scroll-box" :class="{'u-tabs-scorll-flex': !isScroll}"> |
| 8 | <view class="u-tabs-item" :style="[tabItemStyle(index)]" |
| 9 | v-for="(item, index) in getTabs" :key="index" :class="[preId + index]" @tap="emit(index)"> |
| 10 | <u-badge :count="item[count] || item['count'] || 0" :offset="offset" size="mini"></u-badge> |
| 11 | {{ item[name] || item['name']}} |
| 12 | </view> |
| 13 | <view v-if="showBar" class="u-scroll-bar" :style="[tabBarStyle]"></view> |
| 14 | </view> |
| 15 | </scroll-view> |
| 16 | </view> |
| 17 | </template> |
| 18 | |
| 19 | <script> |
| 20 | import colorGradient from '../../libs/function/colorGradient'; |
| 21 | let color = colorGradient; |
| 22 | const { windowWidth } = uni.getSystemInfoSync(); |
| 23 | const preId = 'UEl_'; |
| 24 | |
| 25 | /** |
| 26 | * tabsSwiper 全屏选项卡 |
| 27 | * @description 该组件内部实现主要依托于uniapp的scroll-view和swiper组件,主要特色是切换过程中,tabsSwiper文字的颜色可以渐变,底部滑块可以 跟随式滑动,活动tab滚动居中等。应用场景可以用于需要左右切换页面,比如商城的订单中心(待收货-待付款-待评价-已退货)等应用场景。 |
| 28 | * @tutorial https://www.uviewui.com/components/tabsSwiper.html |
| 29 | * @property {Boolean} is-scroll tabs是否可以左右拖动(默认true) |
| 30 | * @property {Array} list 标签数组,元素为对象,如[{name: '推荐'}] |
| 31 | * @property {String Number} current 指定哪个tab为激活状态(默认0) |
| 32 | * @property {String Number} height 导航栏的高度,单位rpx(默认80) |
| 33 | * @property {String Number} font-size tab文字大小,单位rpx(默认30) |
| 34 | * @property {String Number} swiper-width tabs组件外部swiper的宽度,默认为屏幕宽度,单位rpx(默认750) |
| 35 | * @property {String} active-color 滑块和激活tab文字的颜色(默认#2979ff) |
| 36 | * @property {String} inactive-color tabs文字颜色(默认#303133) |
| 37 | * @property {String Number} bar-width 滑块宽度,单位rpx(默认40) |
| 38 | * @property {String Number} bar-height 滑块高度,单位rpx(默认6) |
| 39 | * @property {Object} bar-style 底部滑块的样式,对象形式 |
| 40 | * @property {Object} active-item-style 活动tabs item的样式,对象形式 |
| 41 | * @property {Boolean} show-bar 是否显示底部的滑块(默认true) |
| 42 | * @property {String Number} gutter 单个tab标签的左右内边距之和,单位rpx(默认40) |
| 43 | * @property {String} bg-color tabs导航栏的背景颜色(默认#ffffff) |
| 44 | * @property {String} name 组件内部读取的list参数中的属性名,见官网说明(默认name) |
| 45 | * @property {String} count 组件内部读取的list参数中的属性名(badge徽标数),同name属性的使用,见官网说明(默认count) |
| 46 | * @property {Array} offset 设置badge徽标数的位置偏移,格式为 [x, y],也即设置的为top和right的值,单位rpx(默认[5, 20]) |
| 47 | * @property {Boolean} bold 激活选项的字体是否加粗(默认true) |
| 48 | * @event {Function} change 点击标签时触发 |
| 49 | * @example <u-tabs-swiper ref="tabs" :list="list" :is-scroll="false"></u-tabs-swiper> |
| 50 | */ |
| 51 | export default { |
| 52 | name: "u-tabs-swiper", |
| 53 | props: { |
| 54 | // 导航菜单是否需要滚动,如只有2或者3个的时候,就不需要滚动了,此时使用flex平分tab的宽度 |
| 55 | isScroll: { |
| 56 | type: Boolean, |
| 57 | default: true |
| 58 | }, |
| 59 | //需循环的标签列表 |
| 60 | list: { |
| 61 | type: Array, |
| 62 | default () { |
| 63 | return []; |
| 64 | } |
| 65 | }, |
| 66 | // 当前活动tab的索引 |
| 67 | current: { |
| 68 | type: [Number, String], |
| 69 | default: 0 |
| 70 | }, |
| 71 | // 导航栏的高度和行高,单位rpx |
| 72 | height: { |
| 73 | type: [Number, String], |
| 74 | default: 80 |
| 75 | }, |
| 76 | // 字体大小,单位rpx |
| 77 | fontSize: { |
| 78 | type: [Number, String], |
| 79 | default: 30 |
| 80 | }, |
| 81 | // 过渡动画时长, 单位s |
| 82 | // duration: { |
| 83 | // type: [Number, String], |
| 84 | // default: 0.5 |
| 85 | // }, |
| 86 | swiperWidth: { |
| 87 | //line3生效, 外部swiper的宽度, 单位rpx |
| 88 | type: [String, Number], |
| 89 | default: 750 |
| 90 | }, |
| 91 | // 选中项的主题颜色 |
| 92 | activeColor: { |
| 93 | type: String, |
| 94 | default: '#2979ff' |
| 95 | }, |
| 96 | // 未选中项的颜色 |
| 97 | inactiveColor: { |
| 98 | type: String, |
| 99 | default: '#303133' |
| 100 | }, |
| 101 | // 菜单底部移动的bar的宽度,单位rpx |
| 102 | barWidth: { |
| 103 | type: [Number, String], |
| 104 | default: 40 |
| 105 | }, |
| 106 | // 移动bar的高度 |
| 107 | barHeight: { |
| 108 | type: [Number, String], |
| 109 | default: 6 |
| 110 | }, |
| 111 | // 单个tab的左或右内边距(各占一半),单位rpx |
| 112 | gutter: { |
| 113 | type: [Number, String], |
| 114 | default: 40 |
| 115 | }, |
| 116 | // 如果是绝对定位,添加z-index值 |
| 117 | zIndex: { |
| 118 | type: [Number, String], |
| 119 | default: 1 |
| 120 | }, |
| 121 | // 导航栏的背景颜色 |
| 122 | bgColor: { |
| 123 | type: String, |
| 124 | default: '#ffffff' |
| 125 | }, |
| 126 | //滚动至中心目标类型 |
| 127 | autoCenterMode: { |
| 128 | type: String, |
| 129 | default: 'window' |
| 130 | }, |
| 131 | // 读取传入的数组对象的属性(tab名称) |
| 132 | name: { |
| 133 | type: String, |
| 134 | default: 'name' |
| 135 | }, |
| 136 | // 读取传入的数组对象的属性(徽标数) |
| 137 | count: { |
| 138 | type: String, |
| 139 | default: 'count' |
| 140 | }, |
| 141 | // 徽标数位置偏移 |
| 142 | offset: { |
| 143 | type: Array, |
| 144 | default: () => { |
| 145 | return [5, 20] |
| 146 | } |
| 147 | }, |
| 148 | // 活动tab字体是否加粗 |
| 149 | bold: { |
| 150 | type: Boolean, |
| 151 | default: true |
| 152 | }, |
| 153 | // 当前活动tab item的样式 |
| 154 | activeItemStyle: { |
| 155 | type: Object, |
| 156 | default() { |
| 157 | return {} |
| 158 | } |
| 159 | }, |
| 160 | // 是否显示底部的滑块 |
| 161 | showBar: { |
| 162 | type: Boolean, |
| 163 | default: true |
| 164 | }, |
| 165 | // 底部滑块的自定义样式 |
| 166 | barStyle: { |
| 167 | type: Object, |
| 168 | default() { |
| 169 | return {} |
| 170 | } |
| 171 | } |
| 172 | }, |
| 173 | data() { |
| 174 | return { |
| 175 | scrollLeft: 0, // 滚动scroll-view的左边滚动距离 |
| 176 | tabQueryInfo: [], // 存放对tab菜单查询后的节点信息 |
| 177 | windowWidth: 0, // 屏幕宽度,单位为px |
| 178 | //scrollBarLeft: 0, // 移动bar需要通过translateX()移动的距离 |
| 179 | animationFinishCurrent: this.current, |
| 180 | componentsWidth: 0, |
| 181 | line3AddDx: 0, |
| 182 | line3Dx: 0, |
| 183 | preId, |
| 184 | sW: 0, |
| 185 | tabsInfo: [], |
| 186 | colorGradientArr: [], |
| 187 | colorStep: 100 // 两个颜色之间的渐变等分 |
| 188 | }; |
| 189 | }, |
| 190 | computed: { |
| 191 | // 获取当前活跃的current值 |
| 192 | getCurrent() { |
| 193 | const current = Number(this.current); |
| 194 | // 判断是否超出边界 |
| 195 | if (current > this.getTabs.length - 1) { |
| 196 | return this.getTabs.length - 1; |
| 197 | } |
| 198 | if (current < 0) return 0; |
| 199 | return current; |
| 200 | }, |
| 201 | getTabs() { |
| 202 | return this.list; |
| 203 | }, |
| 204 | // 滑块需要移动的距离 |
| 205 | scrollBarLeft() { |
| 206 | return Number(this.line3Dx) + Number(this.line3AddDx); |
| 207 | }, |
| 208 | // 滑块的宽度转为px单位 |
| 209 | barWidthPx() { |
| 210 | return uni.upx2px(this.barWidth); |
| 211 | }, |
| 212 | // tab的样式 |
| 213 | tabItemStyle() { |
| 214 | return (index) => { |
| 215 | let style = { |
| 216 | height: this.height + 'rpx', |
| 217 | lineHeight: this.height + 'rpx', |
| 218 | padding: `0 ${this.gutter / 2}rpx`, |
| 219 | color: this.tabsInfo.length > 0 ? (this.tabsInfo[index] ? this.tabsInfo[index].color : this.activeColor) : this.inactiveColor, |
| 220 | fontSize: this.fontSize + 'rpx', |
| 221 | zIndex: this.zIndex + 2, |
| 222 | fontWeight: (index == this.getCurrent && this.bold) ? 'bold' : 'normal' |
| 223 | }; |
| 224 | if(index == this.getCurrent) { |
| 225 | // 给选中的tab item添加外部自定义的样式 |
| 226 | style = Object.assign(style, this.activeItemStyle); |
| 227 | } |
| 228 | return style; |
| 229 | } |
| 230 | }, |
| 231 | // 底部滑块的样式 |
| 232 | tabBarStyle() { |
| 233 | let style = { |
| 234 | width: this.barWidthPx + 'px', |
| 235 | height: this.barHeight + 'rpx', |
| 236 | borderRadius: '100px', |
| 237 | backgroundColor: this.activeColor, |
| 238 | left: this.scrollBarLeft + 'px' |
| 239 | }; |
| 240 | return Object.assign(style, this.barStyle); |
| 241 | } |
| 242 | }, |
| 243 | watch: { |
| 244 | current(n, o) { |
| 245 | this.change(n); |
| 246 | this.setFinishCurrent(n); |
| 247 | }, |
| 248 | list() { |
| 249 | this.$nextTick(() => { |
| 250 | this.init(); |
| 251 | }) |
| 252 | } |
| 253 | }, |
| 254 | mounted() { |
| 255 | this.init(); |
| 256 | }, |
| 257 | methods: { |
| 258 | async init() { |
| 259 | this.countPx(); |
| 260 | await this.getTabsInfo(); |
| 261 | this.countLine3Dx(); |
| 262 | this.getQuery(() => { |
| 263 | this.setScrollViewToCenter(); |
| 264 | }); |
| 265 | // 颜色渐变过程数组 |
| 266 | this.colorGradientArr = color.colorGradient(this.inactiveColor, this.activeColor, this.colorStep); |
| 267 | }, |
| 268 | // 获取各个tab的节点信息 |
| 269 | getTabsInfo() { |
| 270 | return new Promise((resolve, reject) => { |
| 271 | let view = uni.createSelectorQuery().in(this); |
| 272 | for (let i = 0; i < this.list.length; i++) { |
| 273 | view.select('.' + preId + i).boundingClientRect(); |
| 274 | } |
| 275 | view.exec(res => { |
| 276 | const arr = []; |
| 277 | for (let i = 0; i < res.length; i++) { |
| 278 | // 给每个tab添加其文字颜色属性 |
| 279 | res[i].color = this.inactiveColor; |
| 280 | // 当前tab直接赋予activeColor |
| 281 | if (i == this.getCurrent) res[i].color = this.activeColor; |
| 282 | arr.push(res[i]); |
| 283 | } |
| 284 | this.tabsInfo = arr; |
| 285 | resolve(); |
| 286 | }); |
| 287 | }) |
| 288 | }, |
| 289 | // 当swiper滑动结束,计算滑块最终要停留的位置 |
| 290 | countLine3Dx() { |
| 291 | const tab = this.tabsInfo[this.animationFinishCurrent]; |
| 292 | // 让滑块中心点和当前tab中心重合 |
| 293 | if (tab) this.line3Dx = tab.left + tab.width / 2 - this.barWidthPx / 2 - this.tabsInfo[0].left; |
| 294 | }, |
| 295 | countPx() { |
| 296 | // swiper宽度由rpx转为px单位,因为dx等,都是px单位 |
| 297 | this.sW = uni.upx2px(Number(this.swiperWidth)); |
| 298 | }, |
| 299 | emit(index) { |
| 300 | this.$emit('change', index); |
| 301 | }, |
| 302 | change() { |
| 303 | this.setScrollViewToCenter(); |
| 304 | }, |
| 305 | getQuery(cb) { |
| 306 | try { |
| 307 | let view = uni.createSelectorQuery().in(this).select('.u-tabs'); |
| 308 | view.fields({ |
| 309 | size: true |
| 310 | }, |
| 311 | data => { |
| 312 | if (data) { |
| 313 | this.componentsWidth = data.width; |
| 314 | if (cb && typeof cb === 'function') cb(data); |
| 315 | } else { |
| 316 | this.getQuery(cb); |
| 317 | } |
| 318 | } |
| 319 | ).exec(); |
| 320 | } catch (e) { |
| 321 | this.componentsWidth = windowWidth; |
| 322 | } |
| 323 | }, |
| 324 | // 把活动tab移动到屏幕中心点 |
| 325 | setScrollViewToCenter() { |
| 326 | let tab; |
| 327 | tab = this.tabsInfo[this.animationFinishCurrent]; |
| 328 | if (tab) { |
| 329 | let tabCenter = tab.left + tab.width / 2; |
| 330 | let fatherWidth; |
| 331 | // 活动tab移动到中心时,以屏幕还是tab组件为宽度为基准 |
| 332 | if (this.autoCenterMode === 'window') { |
| 333 | fatherWidth = windowWidth; |
| 334 | } else { |
| 335 | fatherWidth = this.componentsWidth; |
| 336 | } |
| 337 | this.scrollLeft = tabCenter - fatherWidth / 2; |
| 338 | } |
| 339 | }, |
| 340 | setDx(dx) { |
| 341 | let nextTabIndex = dx > 0 ? this.animationFinishCurrent + 1 : this.animationFinishCurrent - 1; |
| 342 | // 判断索引是否超出边界 |
| 343 | nextTabIndex = nextTabIndex <= 0 ? 0 : nextTabIndex; |
| 344 | nextTabIndex = nextTabIndex >= this.list.length ? this.list.length - 1 : nextTabIndex; |
| 345 | const tab = this.tabsInfo[nextTabIndex]; |
| 346 | // 当前tab中心点x轴坐标 |
| 347 | let nowTab = this.tabsInfo[this.animationFinishCurrent]; |
| 348 | let nowTabX = nowTab.left + nowTab.width / 2; |
| 349 | // 下一个tab |
| 350 | let nextTab = this.tabsInfo[nextTabIndex]; |
| 351 | let nextTabX = nextTab.left + nextTab.width / 2; |
| 352 | // 两个tab之间的距离,因为下一个tab可能在当前tab的左边或者右边,取绝对值即可 |
| 353 | let distanceX = Math.abs(nextTabX - nowTabX); |
| 354 | this.line3AddDx = (dx / this.sW) * distanceX; |
| 355 | this.setTabColor(this.animationFinishCurrent, nextTabIndex, dx); |
| 356 | }, |
| 357 | // 设置tab的颜色 |
| 358 | setTabColor(nowTabIndex, nextTabIndex, dx) { |
| 359 | let colorIndex = Math.abs(Math.ceil((dx / this.sW) * 100)); |
| 360 | let colorLength = this.colorGradientArr.length; |
| 361 | // 处理超出索引边界的情况 |
| 362 | colorIndex = colorIndex >= colorLength ? colorLength - 1 : colorIndex <= 0 ? 0 : colorIndex; |
| 363 | // 设置下一个tab的颜色 |
| 364 | this.tabsInfo[nextTabIndex].color = this.colorGradientArr[colorIndex]; |
| 365 | // 设置当前tab的颜色 |
| 366 | this.tabsInfo[nowTabIndex].color = this.colorGradientArr[colorLength - 1 - colorIndex]; |
| 367 | }, |
| 368 | // swiper结束滑动 |
| 369 | setFinishCurrent(current) { |
| 370 | // 如果滑动的索引不一致,修改tab颜色变化,因为可能会有直接点击tab的情况 |
| 371 | this.tabsInfo.map((val, index) => { |
| 372 | if (current == index) val.color = this.activeColor; |
| 373 | else val.color = this.inactiveColor; |
| 374 | return val; |
| 375 | }); |
| 376 | this.line3AddDx = 0; |
| 377 | this.animationFinishCurrent = current; |
| 378 | this.countLine3Dx(); |
| 379 | } |
| 380 | } |
| 381 | }; |
| 382 | </script> |
| 383 | |
| 384 | <style scoped lang="scss"> |
| 385 | @import "../../libs/css/style.components.scss"; |
| 386 | |
| 387 | view, |
| 388 | scroll-view { |
| 389 | box-sizing: border-box; |
| 390 | } |
| 391 | |
| 392 | .u-tabs { |
| 393 | width: 100%; |
| 394 | transition-property: background-color, color; |
| 395 | } |
| 396 | |
| 397 | /* #ifndef APP-NVUE */ |
| 398 | ::-webkit-scrollbar, |
| 399 | ::-webkit-scrollbar, |
| 400 | ::-webkit-scrollbar { |
| 401 | display: none; |
| 402 | width: 0 !important; |
| 403 | height: 0 !important; |
| 404 | -webkit-appearance: none; |
| 405 | background: transparent; |
| 406 | } |
| 407 | /* #endif */ |
| 408 | |
| 409 | /* #ifdef H5 */ |
| 410 | // 通过样式穿透,隐藏H5下,scroll-view下的滚动条 |
| 411 | scroll-view ::v-deep ::-webkit-scrollbar { |
| 412 | display: none; |
| 413 | width: 0 !important; |
| 414 | height: 0 !important; |
| 415 | -webkit-appearance: none; |
| 416 | background: transparent; |
| 417 | } |
| 418 | |
| 419 | /* #endif */ |
| 420 | |
| 421 | .u-scroll-view { |
| 422 | width: 100%; |
| 423 | white-space: nowrap; |
| 424 | position: relative; |
| 425 | } |
| 426 | |
| 427 | .u-tabs-scroll-box { |
| 428 | position: relative; |
| 429 | } |
| 430 | |
| 431 | .u-tabs-scorll-flex { |
| 432 | @include vue-flex; |
| 433 | justify-content: space-between; |
| 434 | } |
| 435 | |
| 436 | .u-tabs-scorll-flex .u-tabs-item { |
| 437 | flex: 1; |
| 438 | } |
| 439 | |
| 440 | .u-tabs-item { |
| 441 | position: relative; |
| 442 | display: inline-block; |
| 443 | text-align: center; |
| 444 | transition-property: background-color, color, font-weight; |
| 445 | } |
| 446 | |
| 447 | .content { |
| 448 | overflow: hidden; |
| 449 | white-space: nowrap; |
| 450 | text-overflow: ellipsis; |
| 451 | } |
| 452 | |
| 453 | .boxStyle { |
| 454 | pointer-events: none; |
| 455 | position: absolute; |
| 456 | transition-property: all; |
| 457 | } |
| 458 | |
| 459 | .boxStyle2 { |
| 460 | pointer-events: none; |
| 461 | position: absolute; |
| 462 | bottom: 0; |
| 463 | transition-property: all; |
| 464 | transform: translateY(-100%); |
| 465 | } |
| 466 | |
| 467 | .itemBackgroundBox { |
| 468 | pointer-events: none; |
| 469 | position: absolute; |
| 470 | top: 0; |
| 471 | transition-property: left, background-color; |
| 472 | @include vue-flex; |
| 473 | flex-direction: row; |
| 474 | justify-content: center; |
| 475 | align-items: center; |
| 476 | } |
| 477 | |
| 478 | .itemBackground { |
| 479 | height: 100%; |
| 480 | width: 100%; |
| 481 | transition-property: all; |
| 482 | } |
| 483 | |
| 484 | .u-scroll-bar { |
| 485 | position: absolute; |
| 486 | bottom: 4rpx; |
| 487 | } |
| 488 | </style> |