k-tabs-swiper.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. <template>
  2. <view class="v-tabs">
  3. <scroll-view :id="getDomId" :scroll-x="scroll" :scroll-left="scroll ? scrollLeft : 0"
  4. :scroll-with-animation="scroll" :style="{ position: fixed ? 'fixed' : 'relative' }">
  5. <view class="v-tabs__container" :style="{
  6. display: scroll ? 'inline-flex' : 'flex',
  7. whiteSpace: scroll ? 'nowrap' : 'normal',
  8. background: bgColor,
  9. padding
  10. }">
  11. <view :class="['v-tabs__container-item', { disabled: !!v.disabled }, { active: current == i }]"
  12. v-for="(v, i) in tabs" :key="i" :style="{
  13. color: current == i ? activeColor : color,
  14. fontSize: current == i ? fontSize : fontSize,
  15. fontWeight: bold && current == i ? 'bold' : '',
  16. justifyContent: !scroll ? 'center' : '',
  17. flex: scroll ? '' : 1,
  18. padding: paddingItem
  19. }" @click="change(i,'tab')">
  20. <view class="column-c">
  21. <image class="wh-60 m-tb10 r-100" :class="current == i?'double-border':''"
  22. style="display: block;" :src="v.icon" mode=""></image>
  23. <slot :row="v" :index="i">{{ field ? v[field] : v }}</slot>
  24. </view>
  25. </view>
  26. <template v-if="!!tabs.length">
  27. <view v-if="lineSlot" class="lineSlot"
  28. :class="['v-tabs__container-line', { animation: lineAnimation }]" :style="{
  29. transform: `translate3d(${lineLeft}px, 0, 0)`
  30. }">
  31. <slot name="line" />
  32. </view>
  33. <view class="column-c" v-else-if="!pills"
  34. :class="['v-tabs__container-line', { animation: lineAnimation }]" :style="{
  35. width: lineWidth + 'px',
  36. height: '30rpx',
  37. borderRadius: lineRadius,
  38. transform: `translate3d(${lineLeft}px, 0, 0)`
  39. }">
  40. <view class="triangle"></view>
  41. </view>
  42. <view v-else :class="['v-tabs__container-pills', { animation: lineAnimation }]" :style="{
  43. background: pillsColor,
  44. borderRadius: pillsBorderRadius,
  45. width: currentWidth + 'px',
  46. transform: `translate3d(${pillsLeft}px, 0, 0)`,
  47. height
  48. }" />
  49. </template>
  50. </view>
  51. </scroll-view>
  52. <!-- fixed 的站位高度 -->
  53. <view class="v-tabs__placeholder" :style="{ height: fixed ? height : '0', padding }"></view>
  54. <view class="v-tabs__content" v-if="tabsSwiper">
  55. <swiper class="activity-swiper" v-if="!swiperType" :current="value" :duration="duration"
  56. :circular="circular" @change="onSwiperChange"
  57. :style="{ height: contentHeight ? contentHeight : `calc(100vh - ${height} - ${paddingBottom})`, paddingBottom: paddingBottom, paddingTop: paddingTop}">
  58. <swiper-item class="activity-swiper-item" v-for="(item,index) in tabs" :key="item.key"
  59. :item-id="item.key" :style="{ paddingBottom: 0, paddingTop: 0 }">
  60. <scroll-view scroll-y class="scroll-list" @scrolltolower="onBottom" :style="{ height: '100%' }">
  61. <slot :index="index" name="swiperContent" />
  62. </scroll-view>
  63. </swiper-item>
  64. </swiper>
  65. <view class="data_list" @touchstart="touchStart" @touchend="touchEnd" :animation="animationData" v-else>
  66. <view class="data_list_content" v-for="(item,index) in tabs" :key="index">
  67. <scroll-view scroll-y class="scroll-list" @scrolltolower="onBottom"
  68. :style="{ height: contentHeight ? contentHeight : `calc(100vh - ${height} - ${paddingBottom})`, paddingBottom: paddingBottom, paddingTop: paddingTop}">
  69. <slot :index="index" name="swiperContent" />
  70. </scroll-view>
  71. </view>
  72. </view>
  73. </view>
  74. </view>
  75. </template>
  76. <script>
  77. import {
  78. startMicroTask
  79. } from './utils'
  80. import props from './props'
  81. /**
  82. * v-tabs
  83. * @property {Number} value 选中的下标
  84. * @property {Array} tabs tabs 列表
  85. * @property {String} bgColor = '#fff' 背景颜色
  86. * @property {String} color = '#333' 默认颜色
  87. * @property {String} activeColor = '#2979ff' 选中文字颜色
  88. * @property {String} fontSize = '28rpx' 默认文字大小
  89. * @property {String} activeFontSize = '28rpx' 选中文字大小
  90. * @property {Boolean} bold = [true | false] 选中文字是否加粗
  91. * @property {Boolean} scroll = [true | false] 是否滚动
  92. * @property {String} height = '60rpx' tab 的高度
  93. * @property {String} lineHeight = '10rpx' 下划线的高度
  94. * @property {String} lineColor = '#2979ff' 下划线的颜色
  95. * @property {Number} lineScale = 0.5 下划线的宽度缩放比例
  96. * @property {String} lineRadius = '10rpx' 下划线圆角
  97. * @property {Boolean} pills = [true | false] 是否胶囊样式
  98. * @property {String} pillsColor = '#2979ff' 胶囊背景色
  99. * @property {String} pillsBorderRadius = '10rpx' 胶囊圆角大小
  100. * @property {String} field 如果是对象,显示的键名
  101. * @property {Boolean} fixed = [true | false] 是否固定
  102. * @property {String} paddingItem = '0 22rpx' 选项的边距
  103. * @property {Boolean} lineAnimation = [true | false] 下划线是否有动画
  104. * @property {Number} zIndex = 1993 默认层级
  105. * @property {Boolean} lineSlot true 是否自定义底部滑块
  106. * @property {Boolean} tabsSwiper false 是否使用swiper
  107. * @property {Boolean} swiperType false swiper类型
  108. * @property {Boolean} circular false 是否采用衔接滑动,即播放到末尾后重新回到开头
  109. * @property {String} paddingBottom 0rpx swiper下边padding
  110. * @property {String} paddingTop 0rpx swiper上边padding
  111. * @property {Number} duration 500 swiper滑动动画时长
  112. * @property {String} contentHeight '' swiper内容高度
  113. *
  114. * @event {Function(current)} change 改变标签触发
  115. */
  116. export default {
  117. name: 'kTabsSwiper',
  118. props,
  119. data() {
  120. return {
  121. lineWidth: 30,
  122. currentWidth: 0, // 当前选项的宽度
  123. lineLeft: 0, // 滑块距离左侧的位置
  124. pillsLeft: 0, // 胶囊距离左侧的位置
  125. scrollLeft: 0, // 距离左边的位置
  126. container: {
  127. width: 0,
  128. height: 0,
  129. left: 0,
  130. right: 0
  131. }, // 容器的宽高,左右距离
  132. current: 0, // 当前选中项
  133. scrollWidth: 0, // 可以滚动的宽度
  134. lineSlotWidth: 0, //自定义底部的宽度
  135. startX: 0,
  136. startY: 0,
  137. animationData: {}, // 动画
  138. }
  139. },
  140. computed: {
  141. getDomId() {
  142. const len = 16
  143. const $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678' /****默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1****/
  144. const maxPos = $chars.length
  145. let pwd = ''
  146. for (let i = 0; i < len; i++) {
  147. pwd += $chars.charAt(Math.floor(Math.random() * maxPos))
  148. }
  149. return `xfjpeter_${pwd}`
  150. }
  151. },
  152. watch: {
  153. value: {
  154. immediate: true,
  155. handler(newVal) {
  156. this.current = newVal
  157. this.$nextTick(this.update)
  158. }
  159. },
  160. },
  161. methods: {
  162. initScroll() {
  163. this.scrollY = new Array(10).fill(0);
  164. },
  165. touchStart(event) {
  166. this.startX = event.touches[0].pageX;
  167. this.startY = event.touches[0].pageY;
  168. },
  169. touchEnd(event) {
  170. let deltaX = event.changedTouches[0].pageX - this.startX;
  171. let deltaY = event.changedTouches[0].pageY - this.startY;
  172. if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 60) {
  173. if (deltaX < 0) { //往左
  174. if (this.current == this.tabs.length - 1) {
  175. if (this.circular) {
  176. this.change(0);
  177. } else {
  178. return
  179. }
  180. } else {
  181. this.change(this.current * 1 + 1);
  182. }
  183. this.animate();
  184. } else if (deltaX > 0) { //往右
  185. if (this.current == 0) {
  186. if (this.circular) {
  187. this.change(this.tabs.length - 1);
  188. } else {
  189. return
  190. }
  191. } else {
  192. this.change(this.current * 1 - 1);
  193. }
  194. this.animate();
  195. } else { // 挪动距离0
  196. }
  197. } else {
  198. }
  199. },
  200. onSwiperChange(e) {
  201. this.change(e.detail.current);
  202. },
  203. onBottom() {
  204. this.$emit("load", this.current)
  205. },
  206. animate() {
  207. this.animation.translateX(-this.swiperWidthPx * this.current + "px").step({
  208. duration: this.duration
  209. })
  210. this.animationData = this.animation.export()
  211. setTimeout(() => {
  212. this.animationData = {}
  213. }, this.duration)
  214. },
  215. // 切换事件
  216. change(index, source) {
  217. const isDisabled = !!this.tabs[index].disabled
  218. if (this.current !== index && !isDisabled) {
  219. this.current = index
  220. this.$emit('input', index)
  221. this.$emit('change', index)
  222. }
  223. //如果是tab切换
  224. if (this.swiperType && source == 'tab') {
  225. this.animate();
  226. }
  227. },
  228. createQueryHandler() {
  229. const query = uni
  230. .createSelectorQuery()
  231. // #ifndef MP-ALIPAY
  232. .in(this)
  233. // #endif
  234. return query
  235. },
  236. update() {
  237. const _this = this
  238. startMicroTask(() => {
  239. // 没有列表的时候,不执行
  240. if (!this.tabs.length) return
  241. _this
  242. .createQueryHandler()
  243. .select(`#${this.getDomId}`)
  244. .boundingClientRect(data => {
  245. const {
  246. width,
  247. height,
  248. left,
  249. right
  250. } = data || {}
  251. // 获取容器的相关属性
  252. this.container = {
  253. width,
  254. height,
  255. left,
  256. right: right - width
  257. }
  258. _this.lineScrollWidth();
  259. _this.calcScrollWidth()
  260. _this.setScrollLeft()
  261. _this.setLine()
  262. if (this.swiperType) {
  263. _this.getSwiperWidth();
  264. _this.initScroll();
  265. // 创建动画实例
  266. _this.animation = uni.createAnimation({
  267. timingFunction: 'ease',
  268. duration: 120
  269. })
  270. }
  271. })
  272. .exec()
  273. })
  274. },
  275. getSwiperWidth() {
  276. this.createQueryHandler()
  277. .select(`.v-tabs__content`)
  278. .boundingClientRect(data => {
  279. if (!data) return;
  280. this.swiperWidthPx = data.width;
  281. })
  282. .exec()
  283. },
  284. // 计算可以滚动的宽度
  285. calcScrollWidth(callback) {
  286. const view = this.createQueryHandler().select(`#${this.getDomId}`)
  287. view.fields({
  288. scrollOffset: true
  289. })
  290. view
  291. .scrollOffset(res => {
  292. if (typeof callback === 'function') {
  293. callback(res)
  294. } else {
  295. // 获取滚动条的宽度
  296. this.scrollWidth = res.scrollWidth
  297. }
  298. })
  299. .exec()
  300. },
  301. // 设置滚动条滚动的进度
  302. setScrollLeft() {
  303. this.calcScrollWidth(res => {
  304. // 动态读取 scrollLeft
  305. let scrollLeft = res.scrollLeft
  306. this.createQueryHandler()
  307. .select(`#${this.getDomId} .v-tabs__container-item.active`)
  308. .boundingClientRect(data => {
  309. if (!data) return
  310. // 除开当前选项外容器的一半宽度
  311. let curHalfWidth = (this.container.width - data.width) / 2
  312. let scrollDiff = this.scrollWidth - this.container.width
  313. // 在原有滚动条的基础上 + (当前元素距离左侧的距离 - 计算的一半宽度) - 容器的外边距之类的
  314. scrollLeft += data.left - curHalfWidth - this.container.left
  315. // 已经滚动在左侧了
  316. if (scrollLeft < 0) scrollLeft = 0
  317. // 已经超出右侧了
  318. else if (scrollLeft > scrollDiff) scrollLeft = scrollDiff
  319. this.scrollLeft = scrollLeft
  320. })
  321. .exec()
  322. })
  323. },
  324. lineScrollWidth() {
  325. this.createQueryHandler()
  326. .select(`#${this.getDomId} .lineSlot`)
  327. .boundingClientRect(data => {
  328. if (!data) return;
  329. this.lineSlotWidth = data.width;
  330. })
  331. .exec()
  332. },
  333. setLine() {
  334. this.calcScrollWidth(res => {
  335. const scrollLeft = res.scrollLeft
  336. this.createQueryHandler()
  337. .select(`#${this.getDomId} .v-tabs__container-item.active`)
  338. .boundingClientRect(data => {
  339. if (!data) return
  340. if (this.lineSlot) {
  341. this.lineWidth = data.width * this.lineScale
  342. this.lineLeft = scrollLeft + data.left + data.width / 2 - this.lineSlotWidth /
  343. 2 - this.container.left
  344. } else if (this.pills) {
  345. this.currentWidth = data.width
  346. this.pillsLeft = scrollLeft + data.left - this.container.left
  347. } else {
  348. this.lineWidth = data.width * this.lineScale
  349. this.lineLeft = scrollLeft + data.left + (data.width - data.width * this
  350. .lineScale) / 2 - this.container.left
  351. }
  352. })
  353. .exec()
  354. })
  355. }
  356. }
  357. }
  358. </script>
  359. <style lang="scss" scoped>
  360. .v-tabs {
  361. width: 100%;
  362. box-sizing: border-box;
  363. overflow: hidden;
  364. border-radius: 10px;
  365. padding: 0 20rpx;
  366. background: #fff;
  367. /* #ifdef H5 */
  368. ::-webkit-scrollbar {
  369. display: none;
  370. }
  371. /* #endif */
  372. &__container {
  373. min-width: 100%;
  374. position: relative;
  375. display: inline-flex;
  376. align-items: center;
  377. white-space: nowrap;
  378. overflow: hidden;
  379. padding: 20rpx !important;
  380. border-radius: 10px;
  381. background: #fff;
  382. &-item {
  383. flex-shrink: 0;
  384. display: flex;
  385. align-items: center;
  386. height: 100%;
  387. position: relative;
  388. z-index: 10;
  389. transition: all 0.3s;
  390. white-space: nowrap;
  391. padding: 10rpx 40rpx !important;
  392. &.disabled {
  393. opacity: 0.5;
  394. color: #999;
  395. }
  396. }
  397. &-line {
  398. position: absolute;
  399. left: 0;
  400. bottom: -20rpx;
  401. }
  402. &-pills {
  403. position: absolute;
  404. z-index: 9;
  405. }
  406. &-line,
  407. &-pills {
  408. &.animation {
  409. transition: all 0.3s linear;
  410. }
  411. }
  412. }
  413. }
  414. .data_list {
  415. display: flex;
  416. &_content {
  417. flex-shrink: 0;
  418. width: 100%;
  419. }
  420. }
  421. .triangle {
  422. width: 20rpx;
  423. height: 20rpx;
  424. background: #10B261;
  425. border-radius: 50%;
  426. }
  427. .double-border {
  428. box-shadow: 0 0 0 6rpx #fff, 0 0 0 12rpx #10B261;
  429. }
  430. </style>