v-tabs.vue 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. <template>
  2. <view class="v-tabs">
  3. <scroll-view :id="getDomId" :scroll-x="scroll" :scroll-left="scroll ? scrollLeft : 0" :scroll-with-animation="scroll"
  4. :style="{ position: fixed ? 'fixed' : 'relative', zIndex }">
  5. <view class="v-tabs__container" :style="{
  6. display: scroll ? 'inline-flex' : 'flex',
  7. whiteSpace: scroll ? 'nowrap' : 'normal',
  8. background: bgColor,
  9. height,
  10. padding
  11. }">
  12. <view :class="['v-tabs__container-item', { disabled: !!v.disabled }, { active: current == i }]"
  13. v-for="(v, i) in tabs" :key="i" :style="{
  14. color: current == i ? activeColor : color,
  15. fontSize: current == i ? fontSize : fontSize,
  16. fontWeight: bold && current == i ? 'bold' : '',
  17. justifyContent: !scroll ? 'center' : '',
  18. flex: scroll ? '' : 1,
  19. padding: paddingItem,
  20. background: current == i ? '' : pillsUnColor,
  21. borderRadius: pillsBorderRadius,
  22. margin: itemMargin,
  23. }" @click="change(i)">
  24. <slot :row="v" :index="i">{{ field ? v[field] : v }}</slot>
  25. </view>
  26. <template v-if="!!tabs.length">
  27. <view v-if="!pills" :class="['v-tabs__container-line', { animation: lineAnimation }]" :style="{
  28. background: lineColor,
  29. width: lineWidth + 'px',
  30. height: lineHeight,
  31. borderRadius: lineRadius,
  32. transform: `translate3d(${lineLeft}px, 0, 0)`
  33. }" />
  34. <view v-else :class="['v-tabs__container-pills', { animation: lineAnimation }]" :style="{
  35. background: pillsColor,
  36. width: currentWidth + 'px',
  37. transform: `translate3d(${pillsLeft}px, 0, 0)`,
  38. borderRadius: pillsBorderRadius,
  39. height
  40. }" />
  41. </template>
  42. </view>
  43. </scroll-view>
  44. <!-- fixed 的站位高度 -->
  45. <view class="v-tabs__placeholder" :style="{ height: fixed ? height : '0', padding }"></view>
  46. </view>
  47. </template>
  48. <script>
  49. import { startMicroTask } from './utils'
  50. import props from './props'
  51. /**
  52. * v-tabs
  53. * @property {Number} value 选中的下标
  54. * @property {Array} tabs tabs 列表
  55. * @property {String} bgColor = '#fff' 背景颜色
  56. * @property {String} color = '#333' 默认颜色
  57. * @property {String} activeColor = '#2979ff' 选中文字颜色
  58. * @property {String} fontSize = '28rpx' 默认文字大小
  59. * @property {String} activeFontSize = '28rpx' 选中文字大小
  60. * @property {Boolean} bold = [true | false] 选中文字是否加粗
  61. * @property {Boolean} scroll = [true | false] 是否滚动
  62. * @property {String} height = '60rpx' tab 的高度
  63. * @property {String} lineHeight = '10rpx' 下划线的高度
  64. * @property {String} lineColor = '#2979ff' 下划线的颜色
  65. * @property {Number} lineScale = 0.5 下划线的宽度缩放比例
  66. * @property {String} lineRadius = '10rpx' 下划线圆角
  67. * @property {Boolean} pills = [true | false] 是否胶囊样式
  68. * @property {String} pillsColor = '#2979ff' 胶囊背景色
  69. * @property {String} pillsBorderRadius = '10rpx' 胶囊圆角大小
  70. * @property {String} field 如果是对象,显示的键名
  71. * @property {Boolean} fixed = [true | false] 是否固定
  72. * @property {String} paddingItem = '0 22rpx' 选项的边距
  73. * @property {Boolean} lineAnimation = [true | false] 下划线是否有动画
  74. * @property {Number} zIndex = 1993 默认层级
  75. *
  76. * @event {Function(current)} change 改变标签触发
  77. */
  78. export default {
  79. name: 'VTabs',
  80. props,
  81. data() {
  82. return {
  83. lineWidth: 30,
  84. currentWidth: 0, // 当前选项的宽度
  85. lineLeft: 0, // 滑块距离左侧的位置
  86. pillsLeft: 0, // 胶囊距离左侧的位置
  87. scrollLeft: 0, // 距离左边的位置
  88. container: { width: 0, height: 0, left: 0, right: 0 }, // 容器的宽高,左右距离
  89. current: 0, // 当前选中项
  90. scrollWidth: 0 // 可以滚动的宽度
  91. }
  92. },
  93. computed: {
  94. getDomId() {
  95. const len = 16
  96. const $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678' /****默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1****/
  97. const maxPos = $chars.length
  98. let pwd = ''
  99. for (let i = 0; i < len; i++) {
  100. pwd += $chars.charAt(Math.floor(Math.random() * maxPos))
  101. }
  102. return `xfjpeter_${pwd}`
  103. }
  104. },
  105. watch: {
  106. value: {
  107. immediate: true,
  108. handler(newVal) {
  109. this.current = newVal
  110. this.$nextTick(this.update)
  111. }
  112. }
  113. },
  114. methods: {
  115. // 切换事件
  116. change(index) {
  117. const isDisabled = !!this.tabs[index].disabled
  118. if (this.current !== index && !isDisabled) {
  119. this.current = index
  120. this.$emit('input', index)
  121. this.$emit('change', index)
  122. }
  123. },
  124. createQueryHandler() {
  125. const query = uni
  126. .createSelectorQuery()
  127. // #ifndef MP-ALIPAY
  128. .in(this)
  129. // #endif
  130. return query
  131. },
  132. update() {
  133. const _this = this
  134. startMicroTask(() => {
  135. // 没有列表的时候,不执行
  136. if (!this.tabs.length) return
  137. _this
  138. .createQueryHandler()
  139. .select(`#${this.getDomId}`)
  140. .boundingClientRect(data => {
  141. const { width, height, left, right } = data || {}
  142. // 获取容器的相关属性
  143. this.container = { width, height, left, right: right - width }
  144. _this.calcScrollWidth()
  145. _this.setScrollLeft()
  146. _this.setLine()
  147. })
  148. .exec()
  149. })
  150. },
  151. // 计算可以滚动的宽度
  152. calcScrollWidth(callback) {
  153. const view = this.createQueryHandler().select(`#${this.getDomId}`)
  154. view.fields({ scrollOffset: true })
  155. view
  156. .scrollOffset(res => {
  157. if (typeof callback === 'function') {
  158. callback(res)
  159. } else {
  160. // 获取滚动条的宽度
  161. this.scrollWidth = res.scrollWidth
  162. }
  163. })
  164. .exec()
  165. },
  166. // 设置滚动条滚动的进度
  167. setScrollLeft() {
  168. this.calcScrollWidth(res => {
  169. // 动态读取 scrollLeft
  170. let scrollLeft = res.scrollLeft
  171. this.createQueryHandler()
  172. .select(`#${this.getDomId} .v-tabs__container-item.active`)
  173. .boundingClientRect(data => {
  174. if (!data) return
  175. // 除开当前选项外容器的一半宽度
  176. let curHalfWidth = (this.container.width - data.width) / 2
  177. let scrollDiff = this.scrollWidth - this.container.width
  178. // 在原有滚动条的基础上 + (当前元素距离左侧的距离 - 计算的一半宽度) - 容器的外边距之类的
  179. scrollLeft += data.left - curHalfWidth - this.container.left
  180. // 已经滚动在左侧了
  181. if (scrollLeft < 0) scrollLeft = 0
  182. // 已经超出右侧了
  183. else if (scrollLeft > scrollDiff) scrollLeft = scrollDiff
  184. this.scrollLeft = scrollLeft
  185. })
  186. .exec()
  187. })
  188. },
  189. setLine() {
  190. this.calcScrollWidth(res => {
  191. const scrollLeft = res.scrollLeft
  192. this.createQueryHandler()
  193. .select(`#${this.getDomId} .v-tabs__container-item.active`)
  194. .boundingClientRect(data => {
  195. if (!data) return
  196. if (this.pills) {
  197. this.currentWidth = data.width
  198. this.pillsLeft = scrollLeft + data.left - this.container.left
  199. } else {
  200. this.lineWidth = data.width * this.lineScale
  201. this.lineLeft = scrollLeft + data.left + (data.width - data.width * this.lineScale) / 2 - this.container.left
  202. }
  203. })
  204. .exec()
  205. })
  206. }
  207. }
  208. }
  209. </script>
  210. <style lang="scss" scoped>
  211. .v-tabs {
  212. width: 100%;
  213. box-sizing: border-box;
  214. overflow: hidden;
  215. /* #ifdef H5 */
  216. ::-webkit-scrollbar {
  217. display: none;
  218. }
  219. /* #endif */
  220. &__container {
  221. min-width: 100%;
  222. position: relative;
  223. display: inline-flex;
  224. align-items: center;
  225. white-space: nowrap;
  226. overflow: hidden;
  227. &-item {
  228. flex-shrink: 0;
  229. display: flex;
  230. align-items: center;
  231. height: 100%;
  232. position: relative;
  233. z-index: 10;
  234. transition: all 0.3s;
  235. white-space: nowrap;
  236. &.disabled {
  237. opacity: 0.5;
  238. color: #999;
  239. }
  240. }
  241. &-line {
  242. position: absolute;
  243. left: 0;
  244. bottom: 0;
  245. }
  246. &-pills {
  247. position: absolute;
  248. z-index: 9;
  249. }
  250. &-line,
  251. &-pills {
  252. &.animation {
  253. transition: all 0.3s linear;
  254. }
  255. }
  256. }
  257. }
  258. </style>