me-tabs.vue 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. <template>
  2. <view class="me-tabs" :class="{'tabs-fixed': fixed}" :style="{height: tabHeightVal}">
  3. <scroll-view
  4. v-if="tabs.length"
  5. :id="viewId"
  6. :scroll-left="scrollLeft"
  7. scroll-x
  8. scroll-with-animation
  9. :scroll-animation-duration="300"
  10. :show-scrollbar="false"
  11. @scroll="onScroll"
  12. >
  13. <view class="tabs-container">
  14. <!-- tab项 -->
  15. <view
  16. class="tab-item"
  17. v-for="(tab, i) in tabs"
  18. :id="'tabitem'+i"
  19. :ref="'tabitem'+i"
  20. :class="{'active': value===i}"
  21. :style="{
  22. height: tabHeightVal,
  23. 'line-height': tabHeightVal,
  24. 'color': value===i ? actColor : norColor,
  25. 'margin-right': i < tabs.length - 1 ? spacingVal : '0'
  26. }"
  27. :key="i"
  28. @click="tabClick(i)"
  29. >
  30. <text class="tab-text">{{getTabName(tab)}}</text>
  31. </view>
  32. <!-- 下划线 -->
  33. <view
  34. class="tabs-line"
  35. :style="{
  36. left: lineLeft,
  37. width: lineWidth,
  38. 'background-color': actColor
  39. }"
  40. ></view>
  41. </view>
  42. </scroll-view>
  43. </view>
  44. </template>
  45. <script>
  46. export default {
  47. props: {
  48. tabs: {
  49. type: Array,
  50. default() {
  51. return []
  52. }
  53. },
  54. nameKey: {
  55. type: String,
  56. default: 'dictLabel'
  57. },
  58. value: {
  59. type: [String, Number],
  60. default: 0
  61. },
  62. fixed: Boolean,
  63. height: {
  64. type: Number,
  65. default: 64
  66. },
  67. norColor: {
  68. type: String,
  69. default: '#333333'
  70. },
  71. actColor: {
  72. type: String,
  73. default: '#FF5C03'
  74. },
  75. lineWidth: {
  76. type: String,
  77. default: '50rpx'
  78. },
  79. spacing: { // 间距控制
  80. type: Number,
  81. default: 2 // 默认30rpx间距(15px)
  82. },
  83. minWidth: { // 最小宽度
  84. type: Number,
  85. default: 80 // 默认80rpx
  86. }
  87. },
  88. data() {
  89. return {
  90. viewId: 'id_' + Math.random().toString(36).substr(2, 16),
  91. scrollLeft: 0,
  92. tabListSize: [],
  93. lineLeft: '0px',
  94. warpWidth: null,
  95. scrollTimer: null,
  96. tabsItemRect: null,
  97. isScrolling: false,
  98. lastScrollLeft: 0
  99. }
  100. },
  101. computed: {
  102. tabHeightPx() {
  103. return uni.upx2px(this.height);
  104. },
  105. tabHeightVal() {
  106. return this.tabHeightPx + 'px';
  107. },
  108. minWidthVal() {
  109. return uni.upx2px(this.minWidth) + 'px';
  110. },
  111. spacingVal() {
  112. return uni.upx2px(this.spacing) + 'px';
  113. }
  114. },
  115. watch: {
  116. tabs: {
  117. handler() {
  118. this.warpWidth = null;
  119. this.$nextTick(() => {
  120. this.updateTabs();
  121. });
  122. },
  123. immediate: true
  124. },
  125. value: {
  126. handler(newVal, oldVal) {
  127. if (newVal !== oldVal && !this.isScrolling) {
  128. this.$nextTick(() => {
  129. this.updateTabs();
  130. });
  131. }
  132. },
  133. immediate: true
  134. },
  135. spacing() {
  136. this.$nextTick(() => {
  137. this.updateTabs();
  138. });
  139. }
  140. },
  141. methods: {
  142. getTabName(tab) {
  143. return typeof tab === "object" ? tab[this.nameKey] : tab;
  144. },
  145. tabClick(i) {
  146. if (this.value !== i) {
  147. this.$emit("input", i);
  148. this.$emit("change", i);
  149. }
  150. },
  151. async updateTabs() {
  152. await this.selectorQuery();
  153. this.scrollCenter();
  154. this.updateLineLeft();
  155. },
  156. async scrollCenter() {
  157. if (!this.warpWidth) {
  158. let rect = await this.initWarpRect();
  159. this.warpWidth = rect ? rect.width : uni.getSystemInfoSync().windowWidth;
  160. }
  161. const currentTab = this.tabListSize[this.value];
  162. if (!currentTab || !this.tabsItemRect) return;
  163. // 计算当前tab在容器中的位置
  164. const tabLeft = currentTab.left - this.tabsItemRect.left;
  165. const tabCenter = tabLeft + currentTab.width / 2;
  166. // 计算需要滚动的距离
  167. const scrollLeft = tabCenter - this.warpWidth / 2;
  168. // 限制滚动范围
  169. const maxScrollLeft = this.tabsItemRect.width - this.warpWidth;
  170. this.scrollLeft = Math.max(0, Math.min(maxScrollLeft, scrollLeft));
  171. // 头条小程序兼容处理
  172. // #ifdef MP-TOUTIAO
  173. this.scrollTimer && clearTimeout(this.scrollTimer);
  174. this.scrollTimer = setTimeout(() => {
  175. this.scrollLeft = Math.ceil(this.scrollLeft);
  176. }, 400);
  177. // #endif
  178. },
  179. initWarpRect() {
  180. return new Promise(resolve => {
  181. setTimeout(() => {
  182. let query = uni.createSelectorQuery();
  183. // #ifndef MP-ALIPAY
  184. query = query.in(this);
  185. // #endif
  186. query.select('#' + this.viewId).boundingClientRect(data => {
  187. resolve(data);
  188. }).exec();
  189. }, 20);
  190. });
  191. },
  192. selectorQuery() {
  193. return new Promise(resolve => {
  194. if (this.tabs.length === 0) {
  195. resolve();
  196. return;
  197. }
  198. // 获取tabs容器尺寸
  199. uni.createSelectorQuery()
  200. .in(this)
  201. .select('.tabs-container')
  202. .boundingClientRect(rect => {
  203. this.tabsItemRect = rect;
  204. })
  205. .exec();
  206. // 获取所有tab项尺寸
  207. uni.createSelectorQuery()
  208. .in(this)
  209. .selectAll('.tab-item')
  210. .boundingClientRect(rects => {
  211. this.tabListSize = rects;
  212. resolve();
  213. })
  214. .exec();
  215. });
  216. },
  217. updateLineLeft() {
  218. if (this.tabs.length === 0 || !this.tabsItemRect) return;
  219. if (this.tabListSize.length > 0) {
  220. const currentSize = this.tabListSize[this.value];
  221. if (currentSize) {
  222. // 计算下划线位置:tab中心点 - 下划线宽度的一半
  223. const lineWidthPx = uni.upx2px(parseInt(this.lineWidth));
  224. // 计算相对于容器的位置
  225. const tabCenter = currentSize.left - this.tabsItemRect.left + currentSize.width / 2;
  226. this.lineLeft = `${tabCenter - lineWidthPx / 2}px`;
  227. }
  228. }
  229. },
  230. onScroll(e) {
  231. this.isScrolling = true;
  232. this.lastScrollLeft = e.detail.scrollLeft;
  233. // 滚动结束后重置状态
  234. clearTimeout(this.scrollEndTimer);
  235. this.scrollEndTimer = setTimeout(() => {
  236. this.isScrolling = false;
  237. }, 300);
  238. }
  239. },
  240. mounted() {
  241. this.$nextTick(() => {
  242. this.updateTabs();
  243. });
  244. }
  245. }
  246. </script>
  247. <style lang="scss">
  248. .me-tabs{
  249. position: relative;
  250. font-size: 28rpx;
  251. background-color: #fff;
  252. border-bottom: 0rpx solid #eee;
  253. box-sizing: border-box;
  254. overflow-y: hidden;
  255. background-color: #fff;
  256. &.tabs-fixed{
  257. z-index: 990;
  258. position: fixed;
  259. top: var(--window-top);
  260. left: 0;
  261. width: 100%;
  262. }
  263. scroll-view {
  264. width: 100%;
  265. white-space: nowrap;
  266. .tabs-container {
  267. position: relative;
  268. display: inline-flex;
  269. padding: 0 20rpx;
  270. box-sizing: border-box;
  271. }
  272. }
  273. .tab-item{
  274. position: relative;
  275. display: inline-flex;
  276. align-items: center;
  277. justify-content: center;
  278. text-align: center;
  279. box-sizing: border-box;
  280. color:#333;
  281. font-size: 32rpx;
  282. transition: color 0.3s;
  283. flex-shrink: 0;
  284. min-width: v-bind(minWidthVal); /* 设置最小宽度 */
  285. .tab-text {
  286. overflow: hidden;
  287. text-overflow: ellipsis;
  288. white-space: nowrap;
  289. max-width: 100%;
  290. padding: 0 20rpx; /* 左右内边距 */
  291. }
  292. &.active{
  293. font-weight: bold;
  294. color: #FF5C03;
  295. }
  296. }
  297. // 选中tab的线
  298. .tabs-line{
  299. z-index: 1;
  300. position: absolute;
  301. bottom: 10rpx;
  302. height: 6rpx;
  303. border-radius: 4rpx;
  304. transition: left 0.3s ease-in-out;
  305. }
  306. }
  307. </style>