cl-tabs.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. <template>
  2. <view class="tabs">
  3. <scroll-view class="tab-bar" :scroll="false" scroll-x scroll-with-animation
  4. :show-scrollbar="false" :scroll-into-view="scrollInto">
  5. <view class="tab-box" id="tab-box" :style="{justifyContent: center?'center':'flex-start'}">
  6. <view v-for="(item,index) in tabBars" class="tab" @tap="tapTab(index)" :id="`tab_${index}`" ref="tab"
  7. :key="index">
  8. <view :animation="animationData[index]" class="title" :id="`text_${index}`"
  9. :style="{color:index==tabIndex?selectColor:textColor,width:tabWidth}">{{item.tab||item}}
  10. </view>
  11. </view>
  12. <block v-if="type!='default'">
  13. <view :class="[type]" :animation="animationSlider" ref="slider" id="slider"
  14. :style="sliderBgColor+sliderPosition"></view>
  15. </block>
  16. </view>
  17. </scroll-view>
  18. </view>
  19. </template>
  20. <script>
  21. const sysWidth = uni.getSystemInfoSync().screenWidth
  22. export default {
  23. props: {
  24. tabBars: {
  25. type: Array,
  26. default: () => []
  27. },
  28. tabIndex: {
  29. type: Number,
  30. default: -1
  31. },
  32. scale: { //放大倍数
  33. type: Number,
  34. default: 1
  35. },
  36. type: { //类型fill文字被包含 float文字上浮 hang悬空
  37. type: String,
  38. default: 'default'
  39. },
  40. aniType: { //动画类型 extend
  41. type: String,
  42. default: 'default'
  43. },
  44. sliderColor: { //滑块颜色
  45. type: String,
  46. default: '#ff461f'
  47. },
  48. textColor: { //字体颜色
  49. type: String,
  50. default: 'black'
  51. },
  52. selectColor: { //选中字体颜色
  53. type: String,
  54. default: 'black'
  55. },
  56. sliderMargin: { //延长滑块
  57. type: Number,
  58. default: 0
  59. },
  60. tabWidth: { //tab宽度
  61. type: String,
  62. default: ''
  63. },
  64. center: { //居中
  65. type: Boolean,
  66. default: false
  67. },
  68. },
  69. data() {
  70. return {
  71. animationData: {},
  72. largeAni: null,
  73. sliderAni: null,
  74. sliderAniEnd: null,
  75. animationSlider: {},
  76. sliderLeft: 0,
  77. sliderRight: 0,
  78. sliderWidth: 0,//滑块宽度
  79. sliderMove: 0,//滑块移动距离
  80. scrollInto: '',
  81. pos: 0,
  82. direction: 1
  83. }
  84. },
  85. created() {
  86. //放大动画
  87. this.largeAni = uni.createAnimation({duration: 0});
  88. //滑块动画
  89. this.sliderAni = uni.createAnimation({duration: 0});
  90. this.sliderAniEnd = uni.createAnimation({duration: 100});
  91. },
  92. mounted() {
  93. },
  94. methods: {
  95. promise(time = 0) {
  96. let promise = new Promise((resolve, reject) => {
  97. setTimeout(() => {
  98. resolve()
  99. }, time)
  100. })
  101. return promise
  102. },
  103. getDataByEl(el) {
  104. let promise = new Promise((resolve, reject) => {
  105. let tab = uni.createSelectorQuery().in(this)
  106. tab.select(el).boundingClientRect()
  107. tab.exec(async (tabData) => {
  108. resolve(tabData[0])
  109. })
  110. })
  111. return promise
  112. },
  113. //点击
  114. async tapTab(index) {
  115. this.$emit('tabChange', index)
  116. },
  117. //触摸
  118. move(dx) {
  119. //计算滑动index
  120. let ratio = dx / sysWidth
  121. //计算率
  122. let yRatio = dx % sysWidth / sysWidth
  123. //两边禁止
  124. if (this.tabIndex + ratio >= this.tabBars.length - 1 || this.tabIndex + ratio <= 0) return
  125. if (this.aniType == 'extend') {
  126. this.extendAni(ratio, yRatio)
  127. } else if (this.aniType == 'default') {
  128. this.defaultAni(ratio, yRatio)
  129. } else if (this.aniType == 'movExtend') {
  130. this.movExtendAni(ratio, yRatio)
  131. }
  132. this.textAni(ratio, yRatio)
  133. },
  134. defaultAni(ratio, yRatio) {
  135. let yR = Math.abs(yRatio * 2) > 1 ? 1 : yRatio * 2
  136. let translateX = this.sliderMove * ratio
  137. this.sliderAni.left(this.sliderLeft + translateX).step()
  138. this.animationSlider = this.sliderAni.export()
  139. },
  140. movExtendAni(ratio, yRatio) {
  141. let yR = Math.abs(yRatio * 2) > 1 ? 1 : yRatio * 2
  142. let maxTranslateX = this.sliderMove / 2 * ratio / Math.abs(ratio)
  143. let translateX = Math.abs(this.sliderMove * ratio) > Math.abs(maxTranslateX) ? maxTranslateX : this.sliderMove * ratio
  144. let width = this.sliderWidth + this.sliderMove * Math.floor(Math.abs(ratio)) + this.sliderMove * Math.abs(yR) - Math.abs(translateX)
  145. this.sliderAni.width(width).step()
  146. this.animationSlider = this.sliderAni.export()
  147. if (width + Math.abs(translateX) > this.sliderWidth + this.sliderMove) return
  148. this.sliderAni.translateX(translateX).step()
  149. this.animationSlider = this.sliderAni.export()
  150. if (ratio < 0) {
  151. this.direction = -1
  152. this.pos = sysWidth - this.sliderRight
  153. } else if (ratio > 0) {
  154. this.direction = 1
  155. this.pos = this.sliderLeft
  156. }
  157. },
  158. extendAni(ratio, yRatio) {
  159. let yR = Math.abs(yRatio * 2) > 1 ? 1 : yRatio * 2
  160. let width = this.sliderWidth + this.sliderMove * Math.floor(Math.abs(ratio)) + this.sliderMove * Math.abs(yR)
  161. if (ratio < 0) {
  162. this.direction = -1
  163. this.pos = sysWidth - this.sliderRight
  164. } else if (ratio > 0) {
  165. this.direction = 1
  166. this.pos = this.sliderLeft
  167. }
  168. this.sliderAni.width(width).step()
  169. this.animationSlider = this.sliderAni.export()
  170. },
  171. textAni(ratio, yRatio) {
  172. //取到结果值
  173. let currentIndex = ratio > 0 ? Math.ceil(this.tabIndex + ratio) : Math.floor(this.tabIndex + ratio)
  174. let scale = this.scale + (1 - this.scale) * (Math.abs(yRatio)) < 1 ? 1 : this.scale + (1 - this.scale) * (Math.abs(yRatio))
  175. if (yRatio != 0) {
  176. //复原
  177. this.largeAni.scale(scale).step()
  178. this.animationData[currentIndex - (ratio > 0 ? 1 : -1)] = this.largeAni.export()
  179. scale = 1 - (1 - this.scale) * (Math.abs(yRatio)) > this.scale ? this.scale : 1 - (1 - this.scale) * (Math.abs(yRatio))
  180. //放大
  181. this.largeAni.scale(scale).step()
  182. this.animationData[currentIndex] = this.largeAni.export()
  183. }
  184. },
  185. async reset(newVal, oldVal) {
  186. if (this.aniType == 'movExtend' && oldVal != -1) {
  187. if (newVal > oldVal) {
  188. this.direction = -1
  189. this.pos = sysWidth - this.sliderRight - (newVal - oldVal) * this.sliderMove
  190. } else if (newVal < oldVal) {
  191. this.direction = 1
  192. this.pos = this.sliderLeft + (newVal - oldVal) * this.sliderMove
  193. }
  194. await this.promise()
  195. this.sliderAni.width(this.sliderMove / 2 + this.sliderWidth).translateX(0).step()
  196. this.animationSlider = this.sliderAni.export()
  197. }
  198. let res = await this.getDataByEl('#tab-box')
  199. let tab = await this.getDataByEl(`#tab_${this.tabIndex}`)
  200. let tabData = await this.getDataByEl(`#text_${this.tabIndex}`)
  201. this.sliderLeft = tabData.left - res.left - this.sliderMargin / 2
  202. this.sliderRight = tabData.right - res.left + this.sliderMargin / 2
  203. this.sliderWidth = tabData.width + this.sliderMargin
  204. //滑块移动距离
  205. this.sliderMove = tab.width
  206. if (this.aniType == 'default') {
  207. await this.promise()
  208. this.sliderAni.left(this.sliderLeft).step()
  209. this.animationSlider = this.sliderAni.export()
  210. } else if (this.aniType == 'extend' || this.aniType == 'movExtend') {
  211. if (oldVal == -1) {
  212. this.pos = this.sliderLeft
  213. return
  214. }
  215. if (newVal > oldVal) {
  216. this.direction = -1
  217. this.pos = sysWidth - this.sliderRight
  218. } else if (newVal < oldVal) {
  219. this.direction = 1
  220. this.pos = this.sliderLeft
  221. }
  222. this.sliderAniEnd.width(this.sliderWidth).step()
  223. this.animationSlider = this.sliderAniEnd.export()
  224. }
  225. }
  226. },
  227. watch: {
  228. tabBars: {
  229. immediate: true,
  230. handler(newVal, oldVal) {
  231. for (var i = 0; i < newVal.length; i++) {
  232. let a = {}
  233. a[i] = {}
  234. this.animationData = {...this.animationData, ...a}
  235. }
  236. }
  237. },
  238. tabIndex: {
  239. handler: async function (newVal, oldVal) {
  240. for (let key in this.animationData) {
  241. if (key != this.tabIndex) {
  242. this.largeAni.scale(1).step()
  243. this.animationData[key] = this.largeAni.export()
  244. } else {
  245. this.largeAni.scale(this.scale).step()
  246. this.animationData[this.tabIndex] = this.largeAni.export()
  247. }
  248. }
  249. await this.promise()
  250. this.reset(newVal, oldVal)
  251. await this.promise(250)
  252. let scrollTab = newVal - 2 > 0 ? newVal - 2 : 0
  253. this.scrollInto = `tab_${scrollTab}`
  254. }
  255. },
  256. },
  257. computed: {
  258. sliderBgColor() {
  259. return `background-color:${this.sliderColor};width:${this.sliderWidth}px;`
  260. },
  261. sliderPosition() {
  262. let pos = this.direction > 0 ? `left:${this.pos}px;` : `right:${this.pos}px;`
  263. if (this.aniType == 'default') pos = ''
  264. return pos
  265. }
  266. }
  267. }
  268. </script>
  269. <style lang="scss" scoped>
  270. .tabs {
  271. background-color: $base-color;
  272. width: 750 rpx;
  273. display: flex;
  274. flex-direction: row;
  275. padding: 0 0;
  276. align-items: center;
  277. }
  278. .tab {
  279. display: flex;
  280. white-space: nowrap;
  281. flex-direction: column;
  282. align-items: center;
  283. justify-content: center;
  284. height: 90 rpx;
  285. padding: 10 rpx 30 rpx;
  286. font-size: 30 rpx;
  287. z-index: 99;
  288. }
  289. .title {
  290. text-align: center;
  291. }
  292. .tab-bar {
  293. width: 750 rpx;
  294. height: 110 rpx;
  295. }
  296. .tab-box {
  297. flex-direction: row;
  298. display: flex;
  299. position: relative;
  300. align-items: center;
  301. }
  302. .float {
  303. position: absolute;
  304. bottom: 6px;
  305. height: 20 rpx;
  306. border-radius: 10 rpx;
  307. }
  308. .fill {
  309. position: absolute;
  310. height: 56 rpx;
  311. border-radius: 50 rpx;
  312. }
  313. .hang {
  314. bottom: 3px;
  315. position: absolute;
  316. border-radius: 10 rpx;
  317. height: 5px;
  318. }
  319. </style>