tn-tabs.vue 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. <template>
  2. <view class="tn-tabs-class tn-tabs" :class="[backgroundColorClass]" :style="{backgroundColor: backgroundColorStyle, marginTop: $t.string.getLengthUnitValue(top, 'px')}">
  3. <!-- _tgetRect()对组件根节点无效,因为写了.in(this),故这里获取内层接点尺寸 -->
  4. <view :id="id">
  5. <scroll-view scroll-x class="tn-tabs__scroll-view" :scroll-left="scrollLeft" scroll-with-animation>
  6. <view class="tn-tabs__scroll-view__box" :class="{'tn-tabs__scroll-view--flex': !isScroll}">
  7. <!-- item -->
  8. <view
  9. v-for="(item, index) in list"
  10. :key="index"
  11. :id="'tn-tabs__scroll-view__item-' + index"
  12. class="tn-tabs__scroll-view__item tn-text-ellipsis"
  13. :style="[tabItemStyle(index)]"
  14. @tap="clickTab(index)"
  15. >
  16. <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>
  17. {{ item[name] || item['name'] }}
  18. </view>
  19. <!-- 底部滑块 -->
  20. <view v-if="showBar" class="tn-tabs__bar" :style="[tabBarStyle]"></view>
  21. </view>
  22. </scroll-view>
  23. </view>
  24. </view>
  25. </template>
  26. <script>
  27. import componentsColor from '../../libs/mixin/components_color.js'
  28. export default {
  29. mixins: [componentsColor],
  30. name: 'tn-tabs',
  31. props: {
  32. // 标签列表
  33. list: {
  34. type: Array,
  35. default() {
  36. return []
  37. }
  38. },
  39. // 列表数据tab名称的属性
  40. name: {
  41. type: String,
  42. default: 'name'
  43. },
  44. // 列表数据微标数量的属性
  45. count: {
  46. type: String,
  47. default: 'count'
  48. },
  49. // 当前活动的tab索引
  50. current: {
  51. type: Number,
  52. default: 0
  53. },
  54. // 菜单是否可以滑动
  55. isScroll: {
  56. type: Boolean,
  57. default: true
  58. },
  59. // 高度
  60. height: {
  61. type: Number,
  62. default: 80
  63. },
  64. // 距离顶部的距离(px)
  65. top: {
  66. type: Number,
  67. default: 0
  68. },
  69. // item的宽度
  70. itemWidth: {
  71. type: [String, Number],
  72. default: 'auto'
  73. },
  74. // 过渡动画时长
  75. duration: {
  76. type: Number,
  77. default: 0.3
  78. },
  79. // 选中时的颜色
  80. activeColor: {
  81. type: String,
  82. default: '#01BEFF'
  83. },
  84. // 未被选中时的颜色
  85. inactiveColor: {
  86. type: String,
  87. default: '#080808'
  88. },
  89. // 选中的item样式
  90. activeItemStyle: {
  91. type: Object,
  92. default() {
  93. return {}
  94. }
  95. },
  96. // 是否显示底部滑块
  97. showBar: {
  98. type: Boolean,
  99. default: true
  100. },
  101. // 底部滑块的宽度
  102. barWidth: {
  103. type: Number,
  104. default: 40
  105. },
  106. // 底部滑块的高度
  107. barHeight: {
  108. type: Number,
  109. default: 6
  110. },
  111. // 自定义底部滑块的样式
  112. barStyle: {
  113. type: Object,
  114. default() {
  115. return {}
  116. }
  117. },
  118. // 单个tab的左右内边距
  119. gutter: {
  120. type: Number,
  121. default: 30
  122. },
  123. // 微标的偏移数[top, right]
  124. badgeOffset: {
  125. type: Array,
  126. default() {
  127. return [20, 22]
  128. }
  129. },
  130. // 是否加粗字体
  131. bold: {
  132. type: Boolean,
  133. default: false
  134. }
  135. },
  136. computed: {
  137. // 底部滑块样式
  138. tabBarStyle() {
  139. let style = {
  140. width: this.$t.string.getLengthUnitValue(this.barWidth),
  141. height: this.$t.string.getLengthUnitValue(this.barHeight),
  142. borderRadius: `${this.barHeight / 2}rpx`,
  143. backgroundColor: this.activeColor,
  144. opacity: this.barMoveFirst ? 0 : 1,
  145. transform: `translate(${this.scrollBarLeft}px, -100%)`,
  146. transitionDuration: this.barMoveFirst ? '0s' : `${this.duration}s`
  147. }
  148. Object.assign(style, this.barStyle)
  149. return style
  150. },
  151. // tabItem样式
  152. tabItemStyle() {
  153. return index => {
  154. let style = {
  155. width: this.$t.string.getLengthUnitValue(this.itemWidth),
  156. height: this.$t.string.getLengthUnitValue(this.height),
  157. lineHeight: this.$t.string.getLengthUnitValue(this.height),
  158. fontSize: this.fontSizeStyle || '28rpx',
  159. padding: this.isScroll ? `0 ${this.gutter}rpx` : '',
  160. flex: this.isScroll ? 'auto' : '1',
  161. transitionDuration: `${this.duration}s`
  162. }
  163. if (index === this.currentIndex) {
  164. if (this.bold) {
  165. style.fontWeight = 'bold'
  166. }
  167. style.color = this.activeColor
  168. Object.assign(style, this.activeItemStyle)
  169. } else {
  170. style.color = this.inactiveColor
  171. }
  172. return style
  173. }
  174. }
  175. },
  176. data() {
  177. return {
  178. // id值
  179. id: this.$t.uuid(),
  180. // 滚动scroll-view的左边距离
  181. scrollLeft: 0,
  182. // 存放查询后tab菜单的节点信息
  183. tabQueryInfo: [],
  184. // 组件宽度
  185. componentWidth: 0,
  186. // 底部滑块的移动距离
  187. scrollBarLeft: 0,
  188. // 组件到屏幕左边的巨鹿
  189. componentLeft: 0,
  190. // 当前选中的itemIndex
  191. currentIndex: this.current,
  192. // 标记底部滑块是否第一次移动,第一次移动的时候不触发动画
  193. barMoveFirst: true
  194. }
  195. },
  196. watch: {
  197. // 监听tab的变化,重新计算tab菜单信息
  198. list(newValue, oldValue) {
  199. // list变化时,重置内部索引,防止出现超过数据边界的问题
  200. if (newValue.length !== oldValue.length) this.currentIndex = 0
  201. this.$nextTick(() => {
  202. this.init()
  203. })
  204. },
  205. current: {
  206. handler(val) {
  207. this.$nextTick(() => {
  208. this.currentIndex = val
  209. this.scrollByIndex()
  210. })
  211. },
  212. immediate: true
  213. }
  214. },
  215. mounted() {
  216. this.init()
  217. },
  218. methods: {
  219. // 初始化变量
  220. async init() {
  221. // 获取tabs组件的信息
  222. let tabRect = await this._tGetRect('#' + this.id)
  223. // 计算组件的宽度
  224. this.componentLeft = tabRect.left
  225. this.componentWidth = tabRect.width
  226. this.getTabRect()
  227. },
  228. // 点击tab菜单
  229. clickTab(index) {
  230. if (index === this.currentIndex) return
  231. this.$emit('change', index)
  232. },
  233. // 查询tab的布局信息
  234. getTabRect() {
  235. let query = uni.createSelectorQuery().in(this)
  236. // 遍历所有的tab
  237. for (let i = 0; i < this.list.length; i++) {
  238. query.select(`#tn-tabs__scroll-view__item-${i}`).fields({
  239. size: true,
  240. rect: true
  241. })
  242. }
  243. query.exec((res) => {
  244. this.tabQueryInfo = res
  245. // 初始滚动条和底部滑块的位置
  246. this.scrollByIndex()
  247. })
  248. },
  249. // 滚动scrollView,让活动的tab处于屏幕中间
  250. scrollByIndex() {
  251. // 当前获取tab的布局信息
  252. let tabInfo = this.tabQueryInfo[this.currentIndex]
  253. if (!tabInfo) return
  254. // 活动tab的宽度
  255. let tabWidth = tabInfo.width
  256. // 活动item的左边到组件左边的距离
  257. let offsetLeft = tabInfo.left - this.componentLeft
  258. // 计算scroll-view移动的距离
  259. let scrollLeft = offsetLeft - (this.componentWidth - tabWidth) / 2
  260. this.scrollLeft = scrollLeft < 0 ? 0 : scrollLeft
  261. // 计算当前滑块需要移动的距离,当前活动item的中点到左边的距离减去滑块宽度的一半
  262. let left = tabInfo.left + tabInfo.width / 2 - this.componentLeft
  263. // 计算当前活跃item到组件左边的距离
  264. this.scrollBarLeft = left - uni.upx2px(this.barWidth) / 2
  265. // 防止在计算时出错,所以延迟执行标记不是第一次移动
  266. if (this.barMoveFirst) {
  267. setTimeout(() => {
  268. this.barMoveFirst = false
  269. }, 100)
  270. }
  271. }
  272. }
  273. }
  274. </script>
  275. <style lang="scss" scoped>
  276. /* #ifndef APP-NVUE */
  277. ::-webkit-scrollbar {
  278. display: none;
  279. width: 0 !important;
  280. height: 0 !important;
  281. -webkit-appearance: none;
  282. background: transparent;
  283. }
  284. /* #endif */
  285. /* #ifdef H5 */
  286. // 通过样式穿透,隐藏H5下,scroll-view下的滚动条
  287. scroll-view ::v-deep ::-webkit-scrollbar {
  288. display: none;
  289. width: 0 !important;
  290. height: 0 !important;
  291. -webkit-appearance: none;
  292. background: transparent;
  293. }
  294. /* #endif */
  295. .tn-tabs {
  296. &__scroll-view {
  297. position: relative;
  298. width: 100%;
  299. white-space: nowrap;
  300. &__box {
  301. position: relative;
  302. /* #ifdef MP-TOUTIAO */
  303. white-space: nowrap;
  304. /* #endif */
  305. }
  306. &__item {
  307. position: relative;
  308. /* #ifndef APP-NVUE */
  309. display: inline-block;
  310. /* #endif */
  311. text-align: center;
  312. transition-property: background-color, color;
  313. }
  314. &--flex {
  315. display: flex;
  316. flex-direction: row;
  317. justify-content: space-between;
  318. }
  319. }
  320. &__bar {
  321. position: absolute;
  322. bottom: 0;
  323. }
  324. }
  325. </style>