<template> <scroll-view ref="pageScrollView" class="page" :rebound="false" @startnestedscroll="onStartNestedScroll" @nestedprescroll="onNestedPreScroll" :nested-scroll-child="nestedScrollChildId" > <swiper ref="header" indicator-dots="true" circular="true"> <swiper-item v-for="i in 3" :item-id="i"> <image src="/static/shuijiao.jpg" style="width:100% ;height: 240px;"></image> <text style="position: absolute;">{{i}}</text> </swiper-item> </swiper> <view class="swiper-list"> <scroll-view ref="tabScroll" class="swiper-tabs" direction="horizontal" :show-scrollbar="false"> <view class="flex-row" style="align-self: flex-start;"> <text ref="swipertab" class="swiper-tabs-item" :class="swiperIndex==index ? 'swiper-tabs-item-active' : ''" v-for="(item, index) in swiperList" :key="index" @click="onTabClick(index)"> {{item.name}} </text> </view> <view ref="indicator" class="swiper-tabs-indicator"></view> </scroll-view> <swiper ref="swiper" class="swiper-view" :current="swiperIndex" @transition="onSwiperTransition" @animationfinish="onSwiperAnimationfinish"> <swiper-item class="swiper-item" v-for="(item, index) in swiperList" :key="index"> <long-page ref="longPage" :id="item.id" :type="item.type" :preload="item.preload"></long-page> </swiper-item> </swiper> </view> </scroll-view> </template> <script> import longPage from '../long-list/long-list-page.uvue'; type SwiperTabsItem = { x : number, w : number } type SwiperViewItem = { type : string, name : string, id: string, preload : boolean, } /** * 根据权重在两个值之间执行线性插值. * @constructor * @param {number} value1 - 第一个值,该值应为下限. * @param {number} value2 - 第二个值,该值应为上限. * @param {number} amount - 应介于 0 和 1 之间,指示内插的权重. * @returns {number} 内插值 */ function lerpNumber(value1 : number, value2 : number, amount : number) : number { return (value1 + (value2 - value1) * amount) } export default { components: { longPage }, data() { return { swiperList: [ { type: 'UpdatedDate', name: '最新上架', id: "list-id-1", preload: true } as SwiperViewItem, { type: 'FreeHot', name: '免费热榜', id: "list-id-2", preload: false } as SwiperViewItem, { type: 'PaymentHot', name: '付费热榜', id: "list-id-3", preload: false } as SwiperViewItem, { type: 'HotList', name: '热门总榜', id: "list-id-4", preload: false } as SwiperViewItem ] as SwiperViewItem[], swiperIndex: 0, pageScrollView: null as null | UniElement, headerHeight: 0, animationFinishIndex: 0, tabScrollView: null as null | UniElement, indicatorNode: null as null | UniElement, swiperWidth: 0, swiperTabsRect: [] as SwiperTabsItem[], nestedScrollChildId: "" } }, onReady() { this.pageScrollView = this.$refs['pageScrollView'] as UniElement; this.headerHeight = (this.$refs['header'] as UniElement).offsetHeight; this.tabScrollView = this.$refs['tabScroll'] as UniElement; this.indicatorNode = this.$refs['indicator'] as UniElement; (this.$refs["swiper"] as UniElement).getBoundingClientRectAsync()!.then((res : DOMRect) : Promise<void> => { this.swiperWidth = res.width return this.cacheTabItemsSize() }).then(() => { this.updateTabIndicator(this.swiperIndex, this.swiperIndex, 1) }); }, onPullDownRefresh() { (this.$refs["longPage"]! as ComponentPublicInstance[])[this.swiperIndex].$callMethod('refreshData', () => { uni.stopPullDownRefresh() }) }, methods: { // TODO onStartNestedScroll(_ : StartNestedScrollEvent) : boolean { return true }, onNestedPreScroll(event : NestedPreScrollEvent) { const deltaY = event.deltaY const scrollTop = this.pageScrollView!.scrollTop if (deltaY > 0) { // 向上滚动,如果父容器 header scrollTop < offsetHeight,先滚动父容器 if (scrollTop < this.headerHeight) { const difference = this.headerHeight - scrollTop - deltaY if (difference > 0) { this.pageScrollView!.scrollBy(event.deltaX, deltaY) event.consumed(event.deltaX, deltaY) } else { const top : number = deltaY + difference event.consumed(event.deltaX, top.toFloat()) this.pageScrollView!.scrollBy(event.deltaX, top.toFloat()) } } } }, onTabClick(index : number) { this.setSwiperIndex(index, false) }, onSwiperTransition(e : SwiperTransitionEvent) { // 微信 skyline 每项完成触发 Animationfinish,偏移值重置 // 微信 webview 全部完成触发 Animationfinish,偏移值累加 // 在滑动到下一个项的过程中,再次反向滑动,偏移值递减 // uni-app-x 和微信 webview 行为一致 const offset_x = e.detail.dx // 计算当前索引并重置差异 const current_offset_x = offset_x % this.swiperWidth const current_offset_i = offset_x / this.swiperWidth const current_index = this.animationFinishIndex + parseInt(current_offset_i + '') // 计算目标索引及边界检查 let move_to_index = current_index if (current_offset_x > 0 && move_to_index < this.swiperList.length - 1) { move_to_index += 1 } else if (current_offset_x < 0 && move_to_index > 0) { move_to_index -= 1 } // 计算偏移百分比 const percentage = Math.abs(current_offset_x) / this.swiperWidth // 通知更新指示线 if (current_index != move_to_index) { this.updateTabIndicator(current_index, move_to_index, percentage) } // 首次可见时初始化数据 this.initSwiperItemData(move_to_index) }, onSwiperAnimationfinish(e : SwiperAnimationFinishEvent) { this.setSwiperIndex(e.detail.current, true) this.animationFinishIndex = e.detail.current }, async cacheTabItemsSize() { this.swiperTabsRect.length = 0; const tabs = this.$refs["swipertab"] as UniElement[] for (let i = 0; i < tabs.length; i++) { const element = tabs[i]; const rect = await element.getBoundingClientRectAsync()! this.swiperTabsRect.push({ x: rect.left, w: rect.width } as SwiperTabsItem) } }, setSwiperIndex(index : number, updateIndicator : boolean) { if (this.swiperIndex === index) { return } this.swiperIndex = index this.initSwiperItemData(index) if (updateIndicator) { this.updateTabIndicator(index, index, 1) } }, updateTabIndicator(current_index : number, move_to_index : number, percentage : number) { const current_size = this.swiperTabsRect[current_index] const move_to_size = this.swiperTabsRect[move_to_index] // 计算指示线 左边距 和 宽度 在移动过程中的线性值 const indicator_line_x = lerpNumber(current_size.x, move_to_size.x, percentage) const indicator_line_w = lerpNumber(current_size.w, move_to_size.w, percentage) // 更新指示线 // #ifdef APP const x = indicator_line_x + indicator_line_w / 2 this.indicatorNode?.style?.setProperty('transform', `translateX(${x}px) scaleX(${indicator_line_w})`) this.initSwiperItemData(current_index) // #endif // #ifdef WEB || MP // TODO chrome windows系统 transform scaleX渲染bug const x = indicator_line_x this.indicatorNode?.style?.setProperty('width', `${indicator_line_w}px`) this.indicatorNode?.style?.setProperty('transform', `translateX(${x}px)`) // #endif // 滚动到水平中心位置 const scroll_x = x - this.swiperWidth / 2 // app 平台后续支持 scrollTo() // #ifndef MP-WEIXIN this.tabScrollView!.scrollLeft = scroll_x // #endif // #ifdef MP-WEIXIN this.tabScrollView!.scrollTo({ left: scroll_x }) // #endif }, initSwiperItemData(index : number) { if (!this.swiperList[index].preload) { this.swiperList[index].preload = true; (this.$refs["longPage"]! as ComponentPublicInstance[])[index].$callMethod('loadData', null) } //swiper页面切换需要重新赋值嵌套滚动子元素的id this.nestedScrollChildId = this.swiperList[index].id } } } </script> <style> .flex-row { flex-direction: row; } .page { flex: 1; } .search-bar { padding: 10px; } .swiper-list { height: 100%; /* #ifdef WEB */ flex: 1; /* #endif */ } .swiper-tabs { background-color: #ffffff; flex-direction: column; } .swiper-tabs-item { color: #555; font-size: 16px; padding: 12px 25px; white-space: nowrap; } .swiper-tabs-item-active { color: #007AFF; } .swiper-tabs-indicator { width: 1px; height: 2px; background-color: #007AFF; } .swiper-view { flex: 1; } .swiper-item { flex: 1; } </style>