v-tabs.vue 8.9 KB

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