tabs.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. <template>
  2. <view class="tabs">
  3. <u-sticky :enable="isFixed" :bg-color="stickyBgColor" :offset-top="top" :h5-nav-height="0">
  4. <view :id="id" :style="{
  5. background: bgColor,
  6. 'border-radius': borderRadius,
  7. width: width,
  8. margin: '0 auto'
  9. }">
  10. <scroll-view :style="{height: height + 'rpx'}" scroll-x class="scroll-view" :scroll-left="scrollLeft"
  11. scroll-with-animation>
  12. <view class="scroll-box" :class="{'tabs-scorll-flex': !isScroll}">
  13. <view class="tab-item line1" :id="'tab-item-' + index" v-for="(item, index) in list"
  14. :key="index" @tap="clickTab(index)" :style="[tabItemStyle(index)]">
  15. <u-badge :count="item[count] || item['dot'] || 0" :offset="offset" size="mini"></u-badge>
  16. {{ item[name] || item['name']}}
  17. </view>
  18. <view v-if="showBar" class="tab-bar" :style="[tabBarStyle]"></view>
  19. </view>
  20. </scroll-view>
  21. </view>
  22. </u-sticky>
  23. <view class="tab-content">
  24. <view>
  25. <slot></slot>
  26. </view>
  27. </view>
  28. </view>
  29. </template>
  30. <script>
  31. import {
  32. getRect
  33. } from '@/utils/tools'
  34. export default {
  35. name: "tabs",
  36. props: {
  37. // 导航菜单是否需要滚动,如只有2或者3个的时候,就不需要滚动了,此时使用flex平分tab的宽度
  38. isScroll: {
  39. type: Boolean,
  40. default: true
  41. },
  42. // 当前活动tab的索引
  43. current: {
  44. type: [Number, String],
  45. default: 0
  46. },
  47. // 导航栏的高度和行高
  48. height: {
  49. type: [String, Number],
  50. default: 80
  51. },
  52. // 字体大小
  53. fontSize: {
  54. type: [String, Number],
  55. default: 28
  56. },
  57. // 过渡动画时长, 单位ms
  58. duration: {
  59. type: [String, Number],
  60. default: 0.3
  61. },
  62. // 选中项的主题颜色
  63. activeColor: {
  64. type: String,
  65. default: '#FF2C3C'
  66. },
  67. // 未选中项的颜色
  68. inactiveColor: {
  69. type: String,
  70. default: '#333'
  71. },
  72. // 菜单底部移动的bar的宽度,单位rpx
  73. barWidth: {
  74. type: [String, Number],
  75. default: 40
  76. },
  77. // 移动bar的高度
  78. barHeight: {
  79. type: [String, Number],
  80. default: 4
  81. },
  82. // 单个tab的左或有内边距(左右相同)
  83. gutter: {
  84. type: [String, Number],
  85. default: 30
  86. },
  87. // 导航栏的背景颜色
  88. bgColor: {
  89. type: String,
  90. default: '#ffffff'
  91. },
  92. // 读取传入的数组对象的属性(tab名称)
  93. name: {
  94. type: String,
  95. default: 'name'
  96. },
  97. // 读取传入的数组对象的属性(徽标数)
  98. count: {
  99. type: String,
  100. default: 'count'
  101. },
  102. // 徽标数位置偏移
  103. offset: {
  104. type: Array,
  105. default: () => {
  106. return [5, 20]
  107. }
  108. },
  109. // 活动tab字体是否加粗
  110. bold: {
  111. type: Boolean,
  112. default: true
  113. },
  114. // 当前活动tab item的样式
  115. activeItemStyle: {
  116. type: Object,
  117. default () {
  118. return {}
  119. }
  120. },
  121. // 是否显示底部的滑块
  122. showBar: {
  123. type: Boolean,
  124. default: true
  125. },
  126. // 底部滑块的自定义样式
  127. barStyle: {
  128. type: Object,
  129. default () {
  130. return {}
  131. }
  132. },
  133. // 标签的宽度
  134. itemWidth: {
  135. type: [Number, String],
  136. default: 'auto'
  137. },
  138. isFixed: {
  139. type: Boolean,
  140. default: false
  141. },
  142. top: {
  143. type: [Number, String],
  144. default: 0
  145. },
  146. width: {
  147. type: [Number, String],
  148. default: '100%'
  149. },
  150. stickyBgColor: {
  151. type: String,
  152. default: 'transparent'
  153. },
  154. borderRadius: {
  155. type: [Number, String],
  156. default: 0
  157. },
  158. // 异步使用:需手动更改这个值才能点击
  159. async: {
  160. type: Boolean,
  161. default: false
  162. }
  163. },
  164. provide() {
  165. return {
  166. tabs: this
  167. }
  168. },
  169. data() {
  170. return {
  171. list: [],
  172. scrollLeft: 0, // 滚动scroll-view的左边滚动距离
  173. tabQueryInfo: [], // 存放对tab菜单查询后的节点信息
  174. componentWidth: 0, // 屏幕宽度,单位为px
  175. scrollBarLeft: 0, // 移动bar需要通过translateX()移动的距离
  176. parentLeft: 0, // 父元素(tabs组件)到屏幕左边的距离
  177. id: 'cu-tab', // id值
  178. currentIndex: this.current,
  179. barFirstTimeMove: true, // 滑块第一次移动时(页面刚生成时),无需动画,否则给人怪异的感觉
  180. isAsync: false
  181. };
  182. },
  183. watch: {
  184. // 监听tab的变化,重新计算tab菜单的布局信息,因为实际使用中菜单可能是通过
  185. // 后台获取的(如新闻app顶部的菜单),获取返回需要一定时间,所以list变化时,重新获取布局信息
  186. list(n, o) {
  187. // list变动时,重制内部索引,否则可能导致超出数组边界的情况
  188. if (!this.barFirstTimeMove && n.length !== o.length) {
  189. this.currentIndex = 0;
  190. }
  191. // 用$nextTick等待视图更新完毕后再计算tab的局部信息,否则可能因为tab还没生成就获取,就会有问题
  192. this.$nextTick(() => {
  193. this.init();
  194. });
  195. },
  196. current: {
  197. immediate: true,
  198. handler(nVal, oVal) {
  199. // 视图更新后再执行移动操作、
  200. this.$nextTick(() => {
  201. this.currentIndex = nVal;
  202. this.scrollByIndex();
  203. });
  204. }
  205. },
  206. async: {
  207. immediate: true,
  208. handler(nVal, oVal) {
  209. this.isAsync = nVal;
  210. }
  211. },
  212. },
  213. computed: {
  214. // 移动bar的样式
  215. tabBarStyle() {
  216. let style = {
  217. width: this.barWidth + 'rpx',
  218. transform: `translate(${this.scrollBarLeft}px, -100%)`,
  219. // 滑块在页面渲染后第一次滑动时,无需动画效果
  220. 'transition-duration': `${this.barFirstTimeMove ? 0 : this.duration }s`,
  221. 'background-color': this.activeColor,
  222. height: this.barHeight + 'rpx',
  223. opacity: this.barFirstTimeMove ? 0 : 1,
  224. // 设置一个很大的值,它会自动取能用的最大值,不用高度的一半,是因为高度可能是单数,会有小数出现
  225. 'border-radius': `${this.barHeight / 2}px`
  226. };
  227. Object.assign(style, this.barStyle);
  228. return style;
  229. },
  230. // tab的样式
  231. tabItemStyle() {
  232. return (index) => {
  233. let style = {
  234. height: this.height + 'rpx',
  235. 'line-height': this.height + 'rpx',
  236. 'font-size': this.fontSize + 'rpx',
  237. padding: this.isScroll ? `0 ${this.gutter}rpx` : '',
  238. flex: this.isScroll ? 'auto' : '1',
  239. width: `${this.itemWidth}rpx`
  240. };
  241. // 字体加粗
  242. if (index == this.currentIndex && this.bold) style.fontWeight = 'bold';
  243. if (index == this.currentIndex) {
  244. style.color = this.activeColor;
  245. // 给选中的tab item添加外部自定义的样式
  246. style = Object.assign(style, this.activeItemStyle);
  247. } else {
  248. style.color = this.inactiveColor;
  249. }
  250. return style;
  251. }
  252. }
  253. },
  254. methods: {
  255. updateTabs() {
  256. this.list = this.childrens.map((item) => {
  257. const {
  258. name,
  259. dot,
  260. active,
  261. inited,
  262. updateRender
  263. } = item
  264. return {
  265. name,
  266. dot,
  267. active,
  268. inited,
  269. updateRender
  270. }
  271. })
  272. this.$nextTick(function() {
  273. this.init()
  274. })
  275. },
  276. // 设置一个init方法,方便多处调用
  277. async init() {
  278. // 获取tabs组件的尺寸信息
  279. let tabRect = await getRect('#' + this.id, false, this);
  280. // tabs组件距离屏幕左边的宽度
  281. this.parentLeft = tabRect.left;
  282. // tabs组件的宽度
  283. this.componentWidth = tabRect.width;
  284. this.getTabRect();
  285. },
  286. // 点击某一个tab菜单
  287. clickTab(index) {
  288. // 发送事件给父组件
  289. this.$emit('change', index);
  290. // 点击当前活动tab,不触发事件
  291. if(this.isAsync) return;
  292. if (index == this.currentIndex) return;
  293. this.$nextTick(() => {
  294. this.currentIndex = index;
  295. this.scrollByIndex();
  296. });
  297. },
  298. // 查询tab的布局信息
  299. getTabRect() {
  300. // 创建节点查询
  301. let query = uni.createSelectorQuery().in(this);
  302. // 历遍所有tab,这里是执行了查询,最终使用exec()会一次性返回查询的数组结果
  303. for (let i = 0; i < this.list.length; i++) {
  304. // 只要size和rect两个参数
  305. query.select(`#tab-item-${i}`).fields({
  306. size: true,
  307. rect: true
  308. });
  309. }
  310. // 执行查询,一次性获取多个结果
  311. query.exec(
  312. function(res) {
  313. this.tabQueryInfo = res;
  314. // 初始化滚动条和移动bar的位置
  315. this.scrollByIndex();
  316. }.bind(this)
  317. );
  318. },
  319. // 滚动scroll-view,让活动的tab处于屏幕的中间位置
  320. scrollByIndex() {
  321. // 当前活动tab的布局信息,有tab菜单的width和left(为元素左边界到父元素左边界的距离)等信息
  322. let tabInfo = this.tabQueryInfo[this.currentIndex];
  323. if (!tabInfo) return;
  324. // 活动tab的宽度
  325. let tabWidth = tabInfo.width;
  326. // 活动item的左边到tabs组件左边的距离,用item的left减去tabs的left
  327. let offsetLeft = tabInfo.left - this.parentLeft;
  328. // 将活动的tabs-item移动到屏幕正中间,实际上是对scroll-view的移动
  329. let scrollLeft = offsetLeft - (this.componentWidth - tabWidth) / 2;
  330. this.scrollLeft = scrollLeft < 0 ? 0 : scrollLeft;
  331. // 当前活动item的中点点到左边的距离减去滑块宽度的一半,即可得到滑块所需的移动距离
  332. let left = tabInfo.left + tabInfo.width / 2 - this.parentLeft;
  333. // 计算当前活跃item到组件左边的距离
  334. this.scrollBarLeft = left - uni.upx2px(this.barWidth) / 2;
  335. // 第一次移动滑块的时候,barFirstTimeMove为true,放到延时中将其设置false
  336. // 延时是因为scrollBarLeft作用于computed计算时,需要一个过程需,否则导致出错
  337. if (this.barFirstTimeMove == true) {
  338. setTimeout(() => {
  339. this.barFirstTimeMove = false;
  340. }, 100)
  341. }
  342. // 更新子组件的显示
  343. this.childrens.forEach((item, ind) => {
  344. let active = ind === this.currentIndex;
  345. if (active !== item.active || !item.inited) {
  346. item.updateRender(active, this);
  347. }
  348. });
  349. }
  350. },
  351. created() {
  352. this.childrens = []
  353. },
  354. mounted() {
  355. this.updateTabs();
  356. }
  357. };
  358. </script>
  359. <style lang="scss" scoped>
  360. /* #ifndef APP-NVUE */
  361. ::-webkit-scrollbar,
  362. ::-webkit-scrollbar,
  363. ::-webkit-scrollbar {
  364. display: none;
  365. width: 0 !important;
  366. height: 0 !important;
  367. -webkit-appearance: none;
  368. background: transparent;
  369. }
  370. /* #endif */
  371. .scroll-box {
  372. height: 100%;
  373. position: relative;
  374. /* #ifdef MP-TOUTIAO */
  375. white-space: nowrap;
  376. /* #endif */
  377. }
  378. .tab-fixed {
  379. position: sticky;
  380. top: 0;
  381. width: 100%;
  382. }
  383. /* #ifdef H5 */
  384. // 通过样式穿透,隐藏H5下,scroll-view下的滚动条
  385. scroll-view ::v-deep ::-webkit-scrollbar {
  386. display: none;
  387. width: 0 !important;
  388. height: 0 !important;
  389. -webkit-appearance: none;
  390. background: transparent;
  391. }
  392. /* #endif */
  393. .scroll-view {
  394. width: 100%;
  395. white-space: nowrap;
  396. position: relative;
  397. }
  398. .tab-item {
  399. position: relative;
  400. /* #ifndef APP-NVUE */
  401. display: inline-block;
  402. /* #endif */
  403. text-align: center;
  404. transition-property: background-color, color;
  405. }
  406. .tab-bar {
  407. position: absolute;
  408. bottom: 6rpx;
  409. }
  410. .tabs-scorll-flex {
  411. display: flex;
  412. justify-content: space-between;
  413. }
  414. </style>