scs-scroll-navbar.vue 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. <!--
  2. * @Author: jmy
  3. * @Date: 2025-12-13 12:02:41
  4. * @LastEditors: Please set LastEditors
  5. * @LastEditTime: 2025-12-19 15:02:01
  6. * @Description:
  7. -->
  8. <template>
  9. <scroll-view class="scs-scroll-navbar" scroll-x :scroll-left="scrollLeft" scroll-with-animation="true">
  10. <view v-for="(item, index) in tabsData" :key="index" class="nav-item"
  11. :class="index === tabCurrentIndex ? 'act-current' : ''" @tap="tabChange(index)" :id="'item-' + index">
  12. <view class="item-title">
  13. {{ item[nameKey] }}
  14. </view>
  15. </view>
  16. <view class="nav-underline" :style="underlineStyle"></view>
  17. </scroll-view>
  18. </template>
  19. <script>
  20. export default {
  21. name: 'scs-scroll-navbar',
  22. props: {
  23. // 导航项数据
  24. tabsData: {
  25. type: Array,
  26. default() {
  27. return [];
  28. },
  29. },
  30. // 当前选中的导航项索引
  31. tabCurrentIndex: {
  32. type: Number,
  33. default: 0,
  34. },
  35. // 导航项名称键名
  36. nameKey: {
  37. type: String,
  38. default: 'name',
  39. },
  40. // 导航项间距rpx
  41. calcGap: {
  42. type: Number,
  43. default: 40,
  44. },
  45. },
  46. data() {
  47. return {
  48. scrollLeft: 0,
  49. widthList: [],
  50. screenWidth: 0,
  51. itemPositions: [], // 存储每个导航项的位置信息
  52. };
  53. },
  54. computed: {
  55. // 计算下划线的样式
  56. underlineStyle() {
  57. if (this.itemPositions.length === 0 || this.tabCurrentIndex >= this.itemPositions.length) {
  58. return {};
  59. }
  60. const position = this.itemPositions[this.tabCurrentIndex];
  61. if (this.tabCurrentIndex === 0) {
  62. position.left = 1
  63. }
  64. return `width:${position.width}px;left:${position.left}px;opacity:1`;
  65. },
  66. },
  67. watch: {
  68. tabCurrentIndex: function (val) {
  69. this.tabScroll(val);
  70. },
  71. tabsData: function (val) {
  72. this.init();
  73. },
  74. },
  75. methods: {
  76. // 导航栏点击
  77. tabChange(index) {
  78. let self = this;
  79. self.$emit('tabChange', index);
  80. },
  81. // 导航栏滚动
  82. tabScroll(index) {
  83. var widthArr = this.widthList;
  84. var scrollWidth = 0;
  85. for (var i = 0; i < index + 1; i++) {
  86. scrollWidth += widthArr[i];
  87. }
  88. let currentWidth = widthArr[index];
  89. // scrollView 居中算法
  90. // 减去固定宽度位移
  91. // 减去选中的bar的宽度的一半
  92. scrollWidth -= this.screenWidth / 2;
  93. scrollWidth -= currentWidth / 2;
  94. this.scrollLeft = scrollWidth;
  95. },
  96. //滚动左侧
  97. tabScrollLeft() {
  98. this.scrollLeft = 0.5;
  99. setTimeout(() => {
  100. this.scrollLeft = 0;
  101. }, 500);
  102. },
  103. // 计算导航项宽度
  104. calculateItemWidth() {
  105. let widthArr = [];
  106. let positions = [];
  107. Promise.all(
  108. this.tabsData.map((_, index) =>
  109. new Promise((resolve) => {
  110. uni
  111. .createSelectorQuery()
  112. .in(this)
  113. .select(`#item-${index}`)
  114. .boundingClientRect((data) => {
  115. if (data) {
  116. // 在元素实际宽度基础上增加 this.calcGap 的左右 padding,保证滚动时两侧留白
  117. widthArr.push(data.width + this.calcGap / 2);
  118. positions.push({ left: data.left - 12, width: data.width });
  119. } else {
  120. // 兜底默认宽度
  121. widthArr.push(100);
  122. positions.push({ left: index * 100, width: 100 });
  123. }
  124. resolve();
  125. })
  126. .exec();
  127. })
  128. )
  129. ).then(() => {
  130. // 所有节点查询完毕,一次性赋值
  131. this.widthList = widthArr;
  132. this.itemPositions = positions;
  133. });
  134. },
  135. // 计算窗口宽度
  136. calculateWindowWidth() {
  137. var info = uni.getSystemInfoSync();
  138. this.screenWidth = info.screenWidth;
  139. },
  140. init() {
  141. const that = this;
  142. this.calculateWindowWidth();
  143. setTimeout(function () {
  144. that.calculateItemWidth();
  145. }, 1000);
  146. },
  147. },
  148. created() {
  149. this.init();
  150. },
  151. };
  152. </script>
  153. <style lang="scss" scoped>
  154. ::v-deep.uni-scroll-view::-webkit-scrollbar {
  155. display: none;
  156. }
  157. .scs-scroll-navbar {
  158. width: 100%;
  159. height: 90rpx;
  160. white-space: nowrap;
  161. .nav-item {
  162. height: 100%;
  163. text-align: center;
  164. color: #999999;
  165. display: inline-block;
  166. position: relative;
  167. font-size: 40rpx;
  168. margin-right: 32rpx;
  169. .item-title {
  170. line-height: 90rpx;
  171. display: inline-block;
  172. position: relative;
  173. z-index: 10;
  174. }
  175. }
  176. .act-current {
  177. color: #333333;
  178. font-size: 40rpx;
  179. font-weight: bold;
  180. }
  181. .nav-underline {
  182. position: relative;
  183. height: 16rpx;
  184. background: linear-gradient( 90deg, rgba(56,217,125,0.5) 0%, rgba(56,217,125,0) 100%);
  185. border-radius: 8rpx;
  186. bottom: 28rpx;
  187. left: 10px;
  188. width: 0;
  189. visibility: visible;
  190. transition: all 0.3s ease;
  191. }
  192. }
  193. </style>