index.vue 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. <template>
  2. <uni-shadow-root class="vant-tabs-index"><view :class="'custom-class '+(utils.bem('tabs', [type]))">
  3. <view :style="'z-index: '+(zIndex)+'; '+(wrapStyle)" :class="(utils.bem('tabs__wrap', { scrollable }))+' '+(type === 'line' && border ? 'van-hairline--top-bottom' : '')">
  4. <slot name="nav-left"></slot>
  5. <scroll-view :scroll-x="scrollable" scroll-with-animation :scroll-left="scrollLeft" :class="'van-tabs__scroll--'+(type)" :style="color ? 'border-color: ' + color : ''">
  6. <view :class="(utils.bem('tabs__nav', [type]))+' nav-class'">
  7. <view v-if="type === 'line'" class="van-tabs__line" :style="lineStyle"></view>
  8. <view v-for="(item,index) in (tabs)" :key="item.index" :data-index="index" :class="'van-ellipsis tab-class '+(index === active ? 'tab-active-class' : '')+' '+(utils.bem('tab', { active: index === active, disabled: item.disabled }))" :style="(color && index !== active && type === 'card' && !item.disabled ? 'color: ' + color : '')+' '+(color && index === active && type === 'card' ? ';background-color:' + color : '')+' '+(color ? ';border-color: ' + color : '')+' '+(scrollable ? ';flex-basis:' + (88 / swipeThreshold) + '%' : '')" @click="onTap">
  9. <view :class="'van-ellipsis '+(utils.bem('tab__title', { dot: item.dot }))" :style="item.titleStyle">
  10. {{ item.title }}
  11. <van-info v-if="item.info !== null" :info="item.info" custom-class="van-tab__title__info"></van-info>
  12. </view>
  13. </view>
  14. </view>
  15. </scroll-view>
  16. <slot name="nav-right"></slot>
  17. </view>
  18. <view class="van-tabs__content" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd" @touchcancel="onTouchEnd">
  19. <view class="van-tabs__track" :style="trackStyle">
  20. <slot></slot>
  21. </view>
  22. </view>
  23. </view></uni-shadow-root>
  24. </template>
  25. <wxs src="../wxs/utils.wxs" module="utils"></wxs>
  26. <script>
  27. import VanInfo from '../info/index.vue'
  28. global['__wxVueOptions'] = {components:{'van-info': VanInfo}}
  29. global['__wxRoute'] = 'vant/tabs/index'
  30. import { VantComponent } from '../common/component';
  31. import { touch } from '../mixins/touch';
  32. import { nextTick } from '../common/utils';
  33. VantComponent({
  34. mixins: [touch],
  35. classes: ['nav-class', 'tab-class', 'tab-active-class', 'line-class'],
  36. relation: {
  37. name: 'tab',
  38. type: 'descendant',
  39. linked(child) {
  40. this.child.push(child);
  41. this.updateTabs(this.data.tabs.concat(child.data));
  42. },
  43. unlinked(child) {
  44. const index = this.child.indexOf(child);
  45. const { tabs } = this.data;
  46. tabs.splice(index, 1);
  47. this.child.splice(index, 1);
  48. this.updateTabs(tabs);
  49. }
  50. },
  51. props: {
  52. color: String,
  53. sticky: Boolean,
  54. animated: Boolean,
  55. swipeable: Boolean,
  56. lineWidth: {
  57. type: Number,
  58. value: -1
  59. },
  60. lineHeight: {
  61. type: Number,
  62. value: -1
  63. },
  64. active: {
  65. type: Number,
  66. value: 0
  67. },
  68. type: {
  69. type: String,
  70. value: 'line'
  71. },
  72. border: {
  73. type: Boolean,
  74. value: true
  75. },
  76. duration: {
  77. type: Number,
  78. value: 0.3
  79. },
  80. zIndex: {
  81. type: Number,
  82. value: 1
  83. },
  84. swipeThreshold: {
  85. type: Number,
  86. value: 4
  87. },
  88. offsetTop: {
  89. type: Number,
  90. value: 0
  91. }
  92. },
  93. data: {
  94. tabs: [],
  95. lineStyle: '',
  96. scrollLeft: 0,
  97. scrollable: false,
  98. trackStyle: '',
  99. wrapStyle: '',
  100. position: ''
  101. },
  102. watch: {
  103. swipeThreshold() {
  104. this.set({
  105. scrollable: this.child.length > this.data.swipeThreshold
  106. });
  107. },
  108. color: 'setLine',
  109. lineWidth: 'setLine',
  110. lineHeight: 'setLine',
  111. active: 'setActiveTab',
  112. animated: 'setTrack',
  113. offsetTop: 'setWrapStyle'
  114. },
  115. beforeCreate() {
  116. this.child = [];
  117. },
  118. mounted() {
  119. this.setLine(true);
  120. this.setTrack();
  121. this.scrollIntoView();
  122. this.getRect('.van-tabs__wrap').then((rect) => {
  123. this.navHeight = rect.height;
  124. this.observerContentScroll();
  125. });
  126. },
  127. destroyed() {
  128. // @ts-ignore
  129. this.createIntersectionObserver().disconnect();
  130. },
  131. methods: {
  132. updateTabs(tabs) {
  133. tabs = tabs || this.data.tabs;
  134. this.set({
  135. tabs,
  136. scrollable: tabs.length > this.data.swipeThreshold
  137. });
  138. this.setActiveTab();
  139. },
  140. trigger(eventName, index) {
  141. this.$emit(eventName, {
  142. index,
  143. title: this.data.tabs[index].title
  144. });
  145. },
  146. onTap(event) {
  147. const { index } = event.currentTarget.dataset;
  148. if (this.data.tabs[index].disabled) {
  149. this.trigger('disabled', index);
  150. }
  151. else {
  152. this.trigger('click', index);
  153. this.setActive(index);
  154. }
  155. },
  156. setActive(active) {
  157. if (active !== this.data.active) {
  158. this.trigger('change', active);
  159. this.set({ active });
  160. this.setActiveTab();
  161. }
  162. },
  163. setLine(skipTransition) {
  164. if (this.data.type !== 'line') {
  165. return;
  166. }
  167. const { color, active, duration, lineWidth, lineHeight } = this.data;
  168. this.getRect('.van-tab', true).then((rects) => {
  169. const rect = rects[active];
  170. const width = lineWidth !== -1 ? lineWidth : rect.width / 2;
  171. const height = lineHeight !== -1 ? `height: ${lineHeight}px;` : '';
  172. let left = rects
  173. .slice(0, active)
  174. .reduce((prev, curr) => prev + curr.width, 0);
  175. left += (rect.width - width) / 2;
  176. const transition = skipTransition
  177. ? ''
  178. : `transition-duration: ${duration}s; -webkit-transition-duration: ${duration}s;`;
  179. this.set({
  180. lineStyle: `
  181. ${height}
  182. width: ${width}px;
  183. background-color: ${color};
  184. -webkit-transform: translateX(${left}px);
  185. transform: translateX(${left}px);
  186. ${transition}
  187. `
  188. });
  189. });
  190. },
  191. setTrack() {
  192. const { animated, active, duration } = this.data;
  193. if (!animated)
  194. return '';
  195. this.getRect('.van-tabs__content').then((rect) => {
  196. const { width } = rect;
  197. this.set({
  198. trackStyle: `
  199. width: ${width * this.child.length}px;
  200. left: ${-1 * active * width}px;
  201. transition: left ${duration}s;
  202. display: -webkit-box;
  203. display: flex;
  204. `
  205. });
  206. const props = { width, animated };
  207. this.child.forEach((item) => {
  208. item.set(props);
  209. });
  210. });
  211. },
  212. setActiveTab() {
  213. this.child.forEach((item, index) => {
  214. const data = {
  215. active: index === this.data.active
  216. };
  217. if (data.active) {
  218. data.inited = true;
  219. }
  220. if (data.active !== item.data.active) {
  221. item.set(data);
  222. }
  223. });
  224. nextTick(() => {
  225. this.setLine();
  226. this.setTrack();
  227. this.scrollIntoView();
  228. });
  229. },
  230. // scroll active tab into view
  231. scrollIntoView() {
  232. const { active, scrollable } = this.data;
  233. if (!scrollable) {
  234. return;
  235. }
  236. Promise.all([
  237. this.getRect('.van-tab', true),
  238. this.getRect('.van-tabs__nav')
  239. ]).then(([tabRects, navRect]) => {
  240. const tabRect = tabRects[active];
  241. const offsetLeft = tabRects
  242. .slice(0, active)
  243. .reduce((prev, curr) => prev + curr.width, 0);
  244. this.set({
  245. scrollLeft: offsetLeft - (navRect.width - tabRect.width) / 2
  246. });
  247. });
  248. },
  249. onTouchStart(event) {
  250. if (!this.data.swipeable)
  251. return;
  252. this.touchStart(event);
  253. },
  254. onTouchMove(event) {
  255. if (!this.data.swipeable)
  256. return;
  257. this.touchMove(event);
  258. },
  259. // watch swipe touch end
  260. onTouchEnd() {
  261. if (!this.data.swipeable)
  262. return;
  263. const { active, tabs } = this.data;
  264. const { direction, deltaX, offsetX } = this;
  265. const minSwipeDistance = 50;
  266. if (direction === 'horizontal' && offsetX >= minSwipeDistance) {
  267. if (deltaX > 0 && active !== 0) {
  268. this.setActive(active - 1);
  269. }
  270. else if (deltaX < 0 && active !== tabs.length - 1) {
  271. this.setActive(active + 1);
  272. }
  273. }
  274. },
  275. setWrapStyle() {
  276. const { offsetTop, position } = this.data;
  277. let wrapStyle;
  278. switch (position) {
  279. case 'top':
  280. wrapStyle = `
  281. top: ${offsetTop}px;
  282. position: fixed;
  283. `;
  284. break;
  285. case 'bottom':
  286. wrapStyle = `
  287. top: auto;
  288. bottom: 0;
  289. `;
  290. break;
  291. default:
  292. wrapStyle = '';
  293. }
  294. // cut down `set`
  295. if (wrapStyle === this.data.wrapStyle)
  296. return;
  297. this.set({ wrapStyle });
  298. },
  299. observerContentScroll() {
  300. if (!this.data.sticky) {
  301. return;
  302. }
  303. const { offsetTop } = this.data;
  304. const { windowHeight } = wx.getSystemInfoSync();
  305. // @ts-ignore
  306. this.createIntersectionObserver().disconnect();
  307. // @ts-ignore
  308. this.createIntersectionObserver()
  309. .relativeToViewport({ top: -(this.navHeight + offsetTop) })
  310. .observe('.van-tabs', (res) => {
  311. const { top } = res.boundingClientRect;
  312. if (top > offsetTop) {
  313. return;
  314. }
  315. const position = res.intersectionRatio > 0 ? 'top' : 'bottom';
  316. this.$emit('scroll', {
  317. scrollTop: top + offsetTop,
  318. isFixed: position === 'top'
  319. });
  320. this.setPosition(position);
  321. });
  322. // @ts-ignore
  323. this.createIntersectionObserver()
  324. .relativeToViewport({ bottom: -(windowHeight - 1 - offsetTop) })
  325. .observe('.van-tabs', (res) => {
  326. const { top, bottom } = res.boundingClientRect;
  327. if (bottom < this.navHeight) {
  328. return;
  329. }
  330. const position = res.intersectionRatio > 0 ? 'top' : '';
  331. this.$emit('scroll', {
  332. scrollTop: top + offsetTop,
  333. isFixed: position === 'top'
  334. });
  335. this.setPosition(position);
  336. });
  337. },
  338. setPosition(position) {
  339. if (position !== this.data.position) {
  340. this.set({ position }).then(() => {
  341. this.setWrapStyle();
  342. });
  343. }
  344. }
  345. }
  346. });
  347. export default global['__wxComponents']['vant/tabs/index']
  348. </script>
  349. <style platform="mp-weixin">
  350. @import '../common/index.css';.van-tabs{position:relative;-webkit-tap-highlight-color:transparent}.van-tabs__wrap{position:absolute;top:0;right:0;left:0;display:-webkit-flex;display:flex;background-color:#fff}.van-tabs__wrap--page-top{position:fixed}.van-tabs__wrap--content-bottom{top:auto;bottom:0}.van-tabs__wrap--scrollable .van-tab{-webkit-flex:0 0 22%;flex:0 0 22%}.van-tabs__scroll--card{border:1px solid #f44;border-radius:2px}.van-tabs__nav{position:relative;display:-webkit-flex;display:flex;-webkit-user-select:none;user-select:none}.van-tabs__nav--line{height:100%}.van-tabs__nav--card{height:30px}.van-tabs__nav--card .van-tab{line-height:30px;color:#f44;border-right:1px solid #f44}.van-tabs__nav--card .van-tab:last-child{border-right:none}.van-tabs__nav--card .van-tab.van-tab--active{color:#fff;background-color:#f44}.van-tabs__line{position:absolute;bottom:0;left:0;z-index:1;height:3px;background-color:#f44;border-radius:3px}.van-tabs--line{padding-top:44px}.van-tabs--line .van-tabs__wrap{height:44px}.van-tabs--card{padding-top:30px;margin:0 15px}.van-tabs--card .van-tabs__wrap{height:30px}.van-tabs__content{overflow:hidden}.van-tab,.van-tabs__track{position:relative}.van-tab{min-width:0;padding:0 5px;font-size:14px;line-height:44px;color:#7d7e80;text-align:center;cursor:pointer;box-sizing:border-box;-webkit-flex:1;flex:1}.van-tab--active{font-weight:500;color:#333}.van-tab--disabled{color:#c9c9c9}.van-tab__title--dot:after{display:inline-block;width:8px;height:8px;vertical-align:middle;background-color:#f44;border-radius:100%;content:""}.van-tab__title__info{position:relative!important;top:-1px!important;display:inline-block;-webkit-transform:translateX(0)!important;transform:translateX(0)!important}
  351. </style>