时空网前端
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

198 lines
5.3 KiB

  1. <!-- tab组件: <me-tabs v-model="tabIndex" :tabs="tabs" @change="tabChange"></me-tabs> -->
  2. <template>
  3. <view class="me-tabs bg-white" :class="{'tabs-fixed': fixed}" :style="{height: tabHeightVal}">
  4. <scroll-view v-if="tabs.length" :id="viewId" :scroll-left="scrollLeft" scroll-x scroll-with-animation
  5. :scroll-animation-duration="300">
  6. <view class="tabs-item" :class="{'tabs-flex':!isScroll, 'tabs-scroll':isScroll}">
  7. <!-- tab -->
  8. <view class="tab-item" :style="{width: tabWidthVal, height: tabHeightVal, 'line-height':tabHeightVal}" v-for="(tab, i) in tabs"
  9. :class="{'active': value===i}" :key="i" @click="tabClick(i)">
  10. {{getTabName(tab)}}
  11. </view>
  12. <!-- 下划线 -->
  13. <view class="tabs-line" :style="{left:lineLeft}"></view>
  14. </view>
  15. </scroll-view>
  16. </view>
  17. </template>
  18. <script>
  19. export default {
  20. props: {
  21. tabs: { // 支持格式: ['全部', '待付款'] 或 [{name:'全部'}, {name:'待付款'}]
  22. type: Array,
  23. default () {
  24. return []
  25. }
  26. },
  27. nameKey: { // 取name的字段
  28. type: String,
  29. default: 'name'
  30. },
  31. value: { // 当前显示的下标 (使用v-model语法糖: 1.props需为value; 2.需回调input事件)
  32. type: [String, Number],
  33. default: 0
  34. },
  35. fixed: Boolean, // 是否悬浮,默认false
  36. tabWidth: Number, // 每个tab的宽度,默认不设置值,为flex平均分配; 如果指定宽度,则不使用flex,每个tab居左,超过则水平滑动(单位默认rpx)
  37. height: { // 高度,单位rpx
  38. type: Number,
  39. default: 64
  40. }
  41. },
  42. data() {
  43. return {
  44. viewId: 'id_' + Math.random().toString(36).substr(2, 16),
  45. scrollLeft: 0
  46. }
  47. },
  48. computed: {
  49. isScroll() {
  50. return this.tabWidth && this.tabs.length // 指定了tabWidth的宽度,则支持水平滑动
  51. },
  52. tabHeightPx() {
  53. return uni.upx2px(this.height)
  54. },
  55. tabHeightVal() {
  56. return this.tabHeightPx + 'px'
  57. },
  58. tabWidthPx() {
  59. return uni.upx2px(this.tabWidth)
  60. },
  61. tabWidthVal() {
  62. return this.isScroll ? this.tabWidthPx + 'px' : ''
  63. },
  64. lineLeft() {
  65. if (this.isScroll) {
  66. return this.tabWidthPx * this.value + this.tabWidthPx / 2 + 'px' // 需转为px (用rpx的话iOS真机显示有误差)
  67. } else {
  68. return 100 / this.tabs.length * (this.value + 1) - 100 / (this.tabs.length * 2) + '%'
  69. }
  70. }
  71. },
  72. watch: {
  73. tabs() {
  74. this.warpWidth = null; // 重新计算容器宽度
  75. this.scrollCenter(); // 水平滚动到中间
  76. },
  77. value() {
  78. this.scrollCenter(); // 水平滚动到中间
  79. }
  80. },
  81. methods: {
  82. getTabName(tab) {
  83. return typeof tab === "object" ? tab[this.nameKey] : tab
  84. },
  85. tabClick(i) {
  86. if (this.value != i) {
  87. this.$emit("input", i);
  88. this.$emit("change", i);
  89. }
  90. },
  91. async scrollCenter() {
  92. if (!this.isScroll) return;
  93. if (!this.warpWidth) { // tabs容器的宽度
  94. let rect = await this.initWarpRect()
  95. this.warpWidth = rect ? rect.width : uni.getSystemInfoSync().windowWidth; // 某些情况下取不到宽度,暂时取屏幕宽度
  96. }
  97. let tabLeft = this.tabWidthPx * this.value + this.tabWidthPx / 2; // 当前tab中心点到左边的距离
  98. let diff = tabLeft - this.warpWidth / 2 // 如果超过tabs容器的一半,则滚动差值
  99. this.scrollLeft = diff;
  100. // #ifdef MP-TOUTIAO
  101. this.scrollTimer && clearTimeout(this.scrollTimer)
  102. this.scrollTimer = setTimeout(() => { // 字节跳动小程序,需延时再次设置scrollLeft,否则tab切换跨度较大时不生效
  103. this.scrollLeft = Math.ceil(diff)
  104. }, 400)
  105. // #endif
  106. },
  107. initWarpRect() {
  108. return new Promise(resolve => {
  109. setTimeout(() => { // 延时确保dom已渲染, 不使用$nextclick
  110. let query = uni.createSelectorQuery();
  111. // #ifndef MP-ALIPAY
  112. query = query.in(this) // 支付宝小程序不支持in(this),而字节跳动小程序必须写in(this), 否则都取不到值
  113. // #endif
  114. query.select('#' + this.viewId).boundingClientRect(data => {
  115. resolve(data)
  116. }).exec();
  117. }, 20)
  118. })
  119. }
  120. },
  121. mounted() {
  122. this.scrollCenter() // 滚动到当前下标
  123. }
  124. }
  125. </script>
  126. <style lang="scss">
  127. .me-tabs {
  128. position: relative;
  129. box-sizing: border-box;
  130. overflow-y: hidden;
  131. // box-shadow: 0 0.06rem 0.3rem rgba(0, 0, 0, .1);
  132. &.tabs-fixed {
  133. width: 100%;
  134. }
  135. .tabs-item {
  136. display: flex;
  137. position: relative;
  138. white-space: nowrap;
  139. padding-bottom: 30rpx; // 撑开高度,再配合me-tabs的overflow-y: hidden,以达到隐藏滚动条的目的
  140. box-sizing: border-box;
  141. &::after {
  142. position: absolute;
  143. bottom: 31rpx;
  144. left: 0;
  145. width: 100%;
  146. height: 1rpx;
  147. content: '';
  148. border-bottom: 1rpx solid rgba(255, 255, 255, 0.2);
  149. }
  150. .tab-item {
  151. flex: 1;
  152. position: relative;
  153. text-align: center;
  154. box-sizing: border-box;
  155. &.active {
  156. // font-weight: bold;
  157. color: red;
  158. }
  159. }
  160. }
  161. // 平分的方式显示item
  162. .tabs-flex {
  163. display: flex;
  164. .tab-item {
  165. flex: 1;
  166. }
  167. }
  168. // 居左显示item,支持水平滑动
  169. .tabs-scroll {
  170. .tab-item {
  171. display: inline-block;
  172. }
  173. }
  174. // 选中tab的线
  175. .tabs-line {
  176. z-index: 1;
  177. position: absolute;
  178. bottom: 30rpx; // 至少与.tabs-item的padding-bottom一致,才能保证在底部边缘
  179. width: 100rpx;
  180. height: 5rpx;
  181. transform: translateX(-50%);
  182. border-radius: 4rpx;
  183. transition: left .3s;
  184. background: red;
  185. }
  186. }
  187. </style>