tn-tabs-swiper.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. <template>
  2. <view class="tn-tabs-swiper-class tn-tabs-swiper" :class="[backgroundColorClass]" :style="{backgroundColor: backgroundColorStyle, marginTop: $t.string.getLengthUnitValue(top, 'px'), zIndex: zIndex}">
  3. <scroll-view scroll-x class="tn-tabs-swiper__scroll-view" :scroll-left="scrollLeft" scroll-with-animation :style="{zIndex: zIndex + 1}">
  4. <view class="tn-tabs-swiper__scroll-view__box" :class="{'tn-tabs-swiper__scroll-view--flex': !isScroll}">
  5. <!-- item -->
  6. <view
  7. v-for="(item, index) in list"
  8. :key="index"
  9. :id="'tn-tabs-swiper__scroll-view__item-' + index"
  10. class="tn-tabs-swiper__scroll-view__item tn-text-ellipsis"
  11. :style="[tabItemStyle(index)]"
  12. @tap="emit(index)"
  13. >
  14. <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>
  15. {{ item[name] || item['name'] }}
  16. </view>
  17. <!-- 底部滑块 -->
  18. <view v-if="showBar" class="tn-tabs-swiper__bar" :style="[tabBarStyle]"></view>
  19. </view>
  20. </scroll-view>
  21. </view>
  22. </template>
  23. <script>
  24. import componentsColor from '../../libs/mixin/components_color.js'
  25. const { windowWidth } = uni.getSystemInfoSync()
  26. export default {
  27. mixins: [componentsColor],
  28. name: 'tn-tabs-swiper',
  29. props: {
  30. // 标签列表
  31. list: {
  32. type: Array,
  33. default() {
  34. return []
  35. }
  36. },
  37. // 列表数据tab名称的属性
  38. name: {
  39. type: String,
  40. default: 'name'
  41. },
  42. // 列表数据微标数量的属性
  43. count: {
  44. type: String,
  45. default: 'count'
  46. },
  47. // 当前活动的tab索引
  48. current: {
  49. type: Number,
  50. default: 0
  51. },
  52. // 菜单是否可以滑动
  53. isScroll: {
  54. type: Boolean,
  55. default: true
  56. },
  57. // 高度
  58. height: {
  59. type: Number,
  60. default: 80
  61. },
  62. // 距离顶部的距离(px)
  63. top: {
  64. type: Number,
  65. default: 0
  66. },
  67. // item的高度
  68. itemWidth: {
  69. type: [String, Number],
  70. default: 'auto'
  71. },
  72. // swiper的宽度
  73. swiperWidth: {
  74. type: Number,
  75. default: 750
  76. },
  77. // 选中时的颜色
  78. activeColor: {
  79. type: String,
  80. default: '#01BEFF'
  81. },
  82. // 未被选中时的颜色
  83. inactiveColor: {
  84. type: String,
  85. default: '#080808'
  86. },
  87. // 选中的item样式
  88. activeItemStyle: {
  89. type: Object,
  90. default() {
  91. return {}
  92. }
  93. },
  94. // 是否显示底部滑块
  95. showBar: {
  96. type: Boolean,
  97. default: true
  98. },
  99. // 底部滑块的宽度
  100. barWidth: {
  101. type: Number,
  102. default: 40
  103. },
  104. // 底部滑块的高度
  105. barHeight: {
  106. type: Number,
  107. default: 6
  108. },
  109. // 自定义底部滑块的样式
  110. barStyle: {
  111. type: Object,
  112. default() {
  113. return {}
  114. }
  115. },
  116. // 单个tab的左右内边距
  117. gutter: {
  118. type: Number,
  119. default: 30
  120. },
  121. // 微标的偏移数[top, right]
  122. badgeOffset: {
  123. type: Array,
  124. default() {
  125. return [20, 22]
  126. }
  127. },
  128. // 是否加粗字体
  129. bold: {
  130. type: Boolean,
  131. default: false
  132. },
  133. // 滚动至中心目标类型
  134. autoCenterMode: {
  135. type: String,
  136. default: 'window'
  137. },
  138. zIndex: {
  139. type: Number,
  140. default: 1
  141. }
  142. },
  143. computed: {
  144. currentIndex() {
  145. const current = Number(this.current)
  146. // 判断是否超出
  147. if (current > this.list.length - 1) {
  148. return this.list.length - 1
  149. }
  150. if (current < 0) return 0
  151. return current
  152. },
  153. // 滑块需要移动的距离
  154. scrollBarLeft() {
  155. return Number(this.tabLineDx) + Number(this.tabLineAddDx)
  156. },
  157. // 滑块宽度转换为px
  158. barWidthPx() {
  159. return uni.upx2px(this.barWidth)
  160. },
  161. // 将swiper宽度转换为px
  162. swiperWidthPx() {
  163. return uni.upx2px(this.swiperWidth)
  164. },
  165. // tab样式
  166. tabItemStyle() {
  167. return index => {
  168. let style = {
  169. height: this.$t.string.getLengthUnitValue(this.height),
  170. lineHeight: this.$t.string.getLengthUnitValue(this.height),
  171. fontSize: this.fontSizeStyle || '28rpx',
  172. color: this.tabsInfo.length > 0 ? (this.tabsInfo[index] ? this.tabsInfo[index].color : this.activeColor) : this.inactiveColor,
  173. padding: this.isScroll ? `0 ${this.gutter}rpx` : '',
  174. flex: this.isScroll ? 'auto' : '1',
  175. zIndex: this.zIndex + 2
  176. }
  177. if (index === this.currentIndex) {
  178. if (this.bold) {
  179. style.fontWeight = 'bold'
  180. }
  181. Object.assign(style, this.activeItemStyle)
  182. }
  183. return style
  184. }
  185. },
  186. // 底部滑块样式
  187. tabBarStyle() {
  188. let style = {
  189. width: this.$t.string.getLengthUnitValue(this.barWidth),
  190. height: this.$t.string.getLengthUnitValue(this.barHeight),
  191. borderRadius: `${this.barHeight / 2}rpx`,
  192. backgroundColor: this.activeColor,
  193. left: this.scrollBarLeft + 'px'
  194. }
  195. Object.assign(style, this.barStyle)
  196. return style
  197. },
  198. },
  199. data() {
  200. return {
  201. // 滚动scroll-view的左边滚动距离
  202. scrollLeft: 0,
  203. // 存放tab菜单节点信息
  204. tabsInfo: [],
  205. // 屏幕宽度
  206. windowWidth: 0,
  207. // 滑动动画结束后对应的标签Index
  208. animationFinishCurrent: this.current,
  209. // 组件的宽度
  210. componentsWidth: 0,
  211. // 移动距离
  212. tabLineAddDx: 0,
  213. tabLineDx: 0,
  214. // 颜色渐变数组
  215. colorGradientArr: [],
  216. // 两个颜色之间的渐变等分
  217. colorStep: 100,
  218. }
  219. },
  220. watch: {
  221. current(value) {
  222. this.change(value)
  223. this.setFinishCurrent(value)
  224. },
  225. list() {
  226. this.$nextTick(() => {
  227. this.init()
  228. })
  229. }
  230. },
  231. mounted() {
  232. this.init()
  233. },
  234. methods: {
  235. // 初始化
  236. async init() {
  237. await this.getTabsInfo()
  238. this.countLine3Dx()
  239. this.getQuery(() => {
  240. this.setScrollViewToCenter()
  241. })
  242. // 获取渐变颜色数组
  243. this.colorGradientArr = this.$t.color.colorGradient(this.inactiveColor, this.activeColor, this.colorStep)
  244. },
  245. // 发送事件
  246. emit(index) {
  247. this.$emit('change', index)
  248. },
  249. // tabs发生变化
  250. change() {
  251. this.setScrollViewToCenter()
  252. },
  253. // 获取各个tab的节点信息
  254. getTabsInfo() {
  255. return new Promise((resolve, reject) => {
  256. let view = uni.createSelectorQuery().in(this)
  257. for (let i = 0; i < this.list.length; i++) {
  258. view.select('#tn-tabs-swiper__scroll-view__item-'+i).boundingClientRect()
  259. }
  260. view.exec(res => {
  261. const arr = []
  262. for (let i = 0; i < res.length; i++) {
  263. // 添加颜色属性
  264. res[i].color = this.inactiveColor
  265. if (i === this.currentIndex) res[i].color = this.activeColor
  266. arr.push(res[i])
  267. }
  268. this.tabsInfo = arr
  269. resolve()
  270. })
  271. })
  272. },
  273. // 查询components信息
  274. getQuery(cb) {
  275. try {
  276. let view = uni.createSelectorQuery().in(this).select('.tn-tabs-swiper')
  277. view.fields({
  278. size: true
  279. },
  280. data => {
  281. if (data) {
  282. this.componentsWidth = data.width
  283. if (cb && typeof cb === 'function') cb(data)
  284. } else {
  285. this.getQuery(cb)
  286. }
  287. }
  288. ).exec()
  289. } catch (e) {
  290. this.componentsWidth = windowWidth
  291. }
  292. },
  293. // 当swiper滑动结束的时候,计算滑块最终停留的位置
  294. countLine3Dx() {
  295. const tab = this.tabsInfo[this.animationFinishCurrent]
  296. // 让滑块中心点和当前tab中心重合
  297. if (tab) this.tabLineDx = tab.left + tab.width / 2 - this.barWidthPx / 2 - this.tabsInfo[0].left
  298. },
  299. // 把活动的tab移动到屏幕中心
  300. setScrollViewToCenter() {
  301. let tab = this.tabsInfo[this.animationFinishCurrent]
  302. if (tab) {
  303. let tabCenter = tab.left + tab.width / 2
  304. let parentWidth
  305. // 活动tab移动到中心时,以屏幕还是tab组件宽度为基准
  306. if (this.autoCenterMode === 'window') {
  307. parentWidth = windowWidth
  308. } else {
  309. parentWidth = this.componentsWidth
  310. }
  311. this.scrollLeft = tabCenter - parentWidth / 2
  312. }
  313. },
  314. // 设置偏移位置
  315. setDx(dx) {
  316. // 计算下一个标签的步进值
  317. let nextIndexStep = Math.ceil(Math.abs(dx / this.swiperWidthPx))
  318. let nextTabIndex = dx > 0 ? this.animationFinishCurrent + 1 : this.animationFinishCurrent - 1
  319. // 处理索引超出边界问题
  320. nextTabIndex = nextTabIndex <= 0 ? 0 : nextTabIndex
  321. nextTabIndex = nextTabIndex >= this.list.length ? this.list.length - 1 : nextTabIndex
  322. // 当前tab中心点x轴坐标
  323. let currentTab = this.tabsInfo[this.animationFinishCurrent]
  324. let currentTabX = currentTab.left + currentTab.width / 2
  325. // 下一个tab中心点x轴坐标
  326. let nextTab = this.tabsInfo[nextTabIndex]
  327. let nextTabX = nextTab.left + nextTab.width / 2
  328. // 两个tab之间的距离
  329. let distanceX = Math.abs(nextTabX - currentTabX)
  330. this.tabLineAddDx = (dx / this.swiperWidthPx) * distanceX
  331. this.setTabColor(this.animationFinishCurrent, nextTabIndex, dx)
  332. },
  333. // 设置tab的颜色
  334. setTabColor(currentTabIndex, nextTabIndex, dx) {
  335. let nextIndexStep = Math.ceil(Math.abs(dx / this.swiperWidthPx))
  336. if (Math.abs(dx) > this.swiperWidthPx) {
  337. dx = dx > 0 ? dx - (this.swiperWidthPx * (nextIndexStep - 1)) : dx + (this.swiperWidthPx * (nextIndexStep - 1))
  338. }
  339. let colorIndex = Math.abs(Math.ceil((dx / this.swiperWidthPx) * 100))
  340. let colorLength = this.colorGradientArr.length
  341. // 处理超出索引边界
  342. colorIndex = colorIndex >= colorLength ? colorLength - 1 : colorIndex <= 0 ? 0 : colorIndex
  343. if (nextIndexStep > 1) {
  344. // 设置下一个tab的颜色
  345. // 设置之前tab的颜色为默认颜色
  346. if (dx > 0) {
  347. this.tabsInfo[nextTabIndex + (nextIndexStep - 1) > this.tabsInfo.length - 1 ? this.tabsInfo.length - 1 : nextTabIndex + (nextIndexStep - 1)].color = this.colorGradientArr[colorIndex]
  348. this.tabsInfo[nextTabIndex + (nextIndexStep - 2) > this.tabsInfo.length - 1 ? this.tabsInfo.length - 1 : nextTabIndex + (nextIndexStep - 2)].color = this.colorGradientArr[colorLength - 1 - colorIndex]
  349. } else {
  350. this.tabsInfo[nextTabIndex - (nextIndexStep - 1) < 0 ? 0 : nextTabIndex - (nextIndexStep - 1)].color = this.colorGradientArr[colorIndex]
  351. this.tabsInfo[nextTabIndex - (nextIndexStep - 2) < 0 ? 0 : nextTabIndex - (nextIndexStep - 2)].color = this.colorGradientArr[colorLength - 1 - colorIndex]
  352. }
  353. } else {
  354. // 设置下一个tab的颜色
  355. this.tabsInfo[nextTabIndex].color = this.colorGradientArr[colorIndex]
  356. // 设置当前tab的颜色
  357. this.tabsInfo[currentTabIndex].color = this.colorGradientArr[colorLength - 1 - colorIndex]
  358. }
  359. },
  360. // swiper滑动结束
  361. setFinishCurrent(current) {
  362. // 如果滑动的索引不一致,修改tab颜色变化,因为可能会有直接点击tab的情况
  363. this.tabsInfo.map((item, index) => {
  364. if (current == index) item.color = this.activeColor
  365. else item.color = this.inactiveColor
  366. return item
  367. })
  368. this.tabLineAddDx = 0
  369. this.animationFinishCurrent = current
  370. this.countLine3Dx()
  371. }
  372. }
  373. }
  374. </script>
  375. <style lang="scss" scoped>
  376. /* #ifndef APP-NVUE */
  377. ::-webkit-scrollbar {
  378. display: none;
  379. width: 0 !important;
  380. height: 0 !important;
  381. -webkit-appearance: none;
  382. background: transparent;
  383. }
  384. /* #endif */
  385. /* #ifdef H5 */
  386. // 通过样式穿透,隐藏H5下,scroll-view下的滚动条
  387. scroll-view ::v-deep ::-webkit-scrollbar {
  388. display: none;
  389. width: 0 !important;
  390. height: 0 !important;
  391. -webkit-appearance: none;
  392. background: transparent;
  393. }
  394. /* #endif */
  395. .tn-tabs-swiper {
  396. &__scroll-view {
  397. position: relative;
  398. width: 100%;
  399. white-space: nowrap;
  400. &__box {
  401. position: relative;
  402. /* #ifdef MP-TOUTIAO */
  403. white-space: nowrap;
  404. /* #endif */
  405. }
  406. &__item {
  407. position: relative;
  408. /* #ifndef APP-NVUE */
  409. display: inline-block;
  410. /* #endif */
  411. text-align: center;
  412. transition-property: background-color, color;
  413. }
  414. &--flex {
  415. display: flex;
  416. flex-direction: row;
  417. justify-content: space-between;
  418. }
  419. }
  420. &__bar {
  421. position: absolute;
  422. bottom: 0;
  423. }
  424. }
  425. </style>