|
|
|
@ -1,6 +1,6 @@ |
|
|
|
<template> |
|
|
|
<!-- 拼团单一商家 --> |
|
|
|
<view class="page1" @touchmove="onPageTouchMove"> |
|
|
|
<view class="page1"> |
|
|
|
<!-- 固定顶部导航栏(悬浮在背景图之上) --> |
|
|
|
<view class="nav-bar" |
|
|
|
:style="{'padding-top': menuButtonInfo.top +'px','background':((orderListWait.length > 0 && lastScrollTop>217) || (orderListWait.length == 0 && lastScrollTop>149))?'#fff':''}"> |
|
|
|
@ -111,12 +111,11 @@ |
|
|
|
</swiper-item> |
|
|
|
</swiper> |
|
|
|
</view> |
|
|
|
<!-- 分类栏:移到goods-list外层,position:sticky才能生效 --> |
|
|
|
<!-- 占位元素:当container吸顶时保持页面高度不塌缩 --> |
|
|
|
<view v-if="isContainerSticky" :style="'height:' + containerHeight + 'px;margin-top:20rpx;'"></view> |
|
|
|
<view class="container goods-panel" :style="containerStyle"> |
|
|
|
<!-- 整页 page-scroll:左侧分类条 sticky 吸顶,右侧商品跟随页面滚动 --> |
|
|
|
<view class="catalog-row" id="catalog-row"> |
|
|
|
<view class="catalog-menu-wrap" :style="{ top: navBarHeight + 'px' }"> |
|
|
|
<scroll-view scroll-y id="menuList" class="menu-scroll" |
|
|
|
style="border-right: 1px solid #eee;font-weight: 700;font-size: 28rpx;height: 100%;width: 160rpx;"> |
|
|
|
:style="{ height: stickyInnerHeight + 'px' }"> |
|
|
|
<view class="menu1" @tap="checkTab(index)" v-for="(item,index) in menuList" :key="index" |
|
|
|
:style="{'border-top-right-radius':item.checked?'20rpx':'','border-bottom-right-radius':item.checked?'20rpx':'','color':item.checked?'rgba(0, 35, 28, 1)':'#777','background':item.checked?'#fff':''}"> |
|
|
|
<image class="menu-active-dot" v-if="item.checked" src="/static/images/img/loading.gif" |
|
|
|
@ -124,9 +123,8 @@ |
|
|
|
<view style="width: 160rpx;">{{item.categoryName}}</view> |
|
|
|
</view> |
|
|
|
</scroll-view> |
|
|
|
<scroll-view class="goods-list" scroll-y :scroll-into-view="scrollIntoViewId" |
|
|
|
:scroll-top="goodsListScrollTop" @scroll="onGoodsListScroll" @scrolltoupper="onGoodsListScrollToTop" |
|
|
|
:scroll-with-animation="true"> |
|
|
|
</view> |
|
|
|
<view class="catalog-goods"> |
|
|
|
<view class="goods-member goods-card" :id="'category-' + item.categoryId" |
|
|
|
v-for="(item,index) in productItem" :key="index" @tap="goDetail('product',item)"> |
|
|
|
<view class="goods-card-shine"></view> |
|
|
|
@ -192,8 +190,7 @@ |
|
|
|
</view> |
|
|
|
<uni-load-more :status="loadStatus" @change="onChange" /> |
|
|
|
<view style="width: 100%;height: 160rpx;"></view> |
|
|
|
</scroll-view> |
|
|
|
|
|
|
|
</view> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
<view style="width: 100%;height: 160rpx;"></view> |
|
|
|
@ -684,18 +681,11 @@ |
|
|
|
// 导航栏高度(用于sticky top偏移) |
|
|
|
navBarHeight: 0, |
|
|
|
orderListWait: [], |
|
|
|
// 分类栏距离页面顶部的距离(用于上滑检测) |
|
|
|
menuListOffsetTop: 0, |
|
|
|
lastScrollTop: 0, |
|
|
|
// 防止切换分类之后 onPageScroll 闪回的标志 |
|
|
|
isSwitching: false, |
|
|
|
containerOriginalTop: 0, |
|
|
|
scrollIntoViewId: '', |
|
|
|
goodsListScrollTop: 0, |
|
|
|
isAutoScrolling: false, |
|
|
|
isContainerSticky: false, |
|
|
|
containerHeight: 0, |
|
|
|
pageScrollTop: 0, |
|
|
|
// onPageScroll 节流时间戳(非响应式语义,但放 data 防止 undefined) |
|
|
|
_lastDetectTs: 0, |
|
|
|
searchForm: { |
|
|
|
shopId: '', |
|
|
|
delFlag: 1, |
|
|
|
@ -733,17 +723,15 @@ |
|
|
|
cartTotalPrice() { |
|
|
|
return this.cartItems.reduce((acc, curr) => acc + (curr.quantity * curr.price), 0).toFixed(2); |
|
|
|
}, |
|
|
|
containerStyle() { |
|
|
|
if (this.isContainerSticky) { |
|
|
|
// 吸顶时:fixed定位,精确计算高度 = 屏幕高度 - 导航栏高度 - 底部栏高度 |
|
|
|
stickyInnerHeight() { |
|
|
|
// 左侧分类条 sticky 吸顶时的可视区高度(像素)。 |
|
|
|
// 必须为像素值,避免百分比高度在大屏 iPhone 上无法解析导致菜单条无法滚动。 |
|
|
|
const sysInfo = uni.getSystemInfoSync(); |
|
|
|
const bottomBarPx = Math.round(80 * sysInfo.windowWidth / 375); |
|
|
|
const h = sysInfo.windowHeight - this.navBarHeight - bottomBarPx; |
|
|
|
return 'display:flex;position:fixed;left:0;top:' + this.navBarHeight + |
|
|
|
'px;margin:0 auto;right:0;height:' + h + 'px;z-index:50;background:#F5F8F5;box-sizing:border-box;'; |
|
|
|
} else { |
|
|
|
return 'display:flex;height:72%;margin-top:20rpx;'; |
|
|
|
} |
|
|
|
const safeBottom = sysInfo.safeArea ? Math.max(sysInfo.windowHeight - sysInfo.safeArea.bottom, 0) : 0; |
|
|
|
const checkoutBarPx = Math.round(126 * sysInfo.windowWidth / 750); |
|
|
|
const checkoutOffsetPx = Math.round(18 * sysInfo.windowWidth / 750); |
|
|
|
const bottomReservedPx = checkoutBarPx + checkoutOffsetPx + safeBottom; |
|
|
|
return Math.max(240, sysInfo.windowHeight - this.navBarHeight - bottomReservedPx); |
|
|
|
} |
|
|
|
}, |
|
|
|
filters: { |
|
|
|
@ -821,94 +809,74 @@ |
|
|
|
onShow() { |
|
|
|
this.menuButtonInfo = uni.getMenuButtonBoundingClientRect(); |
|
|
|
this.fetchCoupons(); |
|
|
|
// 计算导航栏高度用于sticky offset:statusBarHeight + 40px内容区 |
|
|
|
// 通过胶囊按钮(menuButton)真实坐标计算自定义导航栏高度, |
|
|
|
// 兼容带灵动岛/异形屏的 iPhone(旧版 statusBarHeight + 40 估算会偏差)。 |
|
|
|
const info = uni.getSystemInfoSync(); |
|
|
|
if (this.menuButtonInfo && this.menuButtonInfo.height) { |
|
|
|
const top = this.menuButtonInfo.top; |
|
|
|
const padding = Math.max(top - info.statusBarHeight, 0); |
|
|
|
this.navBarHeight = info.statusBarHeight + padding * 2 + this.menuButtonInfo.height; |
|
|
|
} else { |
|
|
|
this.navBarHeight = info.statusBarHeight + 40; |
|
|
|
} |
|
|
|
}, |
|
|
|
onPullDownRefresh() { |
|
|
|
this.getCategory(this.shopItem.id); |
|
|
|
this.getProduct(''); |
|
|
|
}, |
|
|
|
onReady() { |
|
|
|
// 记录container相对页面的初始偏移量 |
|
|
|
setTimeout(() => { |
|
|
|
const query = uni.createSelectorQuery().in(this); |
|
|
|
query.select('.container').boundingClientRect(rect => { |
|
|
|
if (rect) { |
|
|
|
this.containerOriginalTop = rect.top; |
|
|
|
this.menuListOffsetTop = rect.top; |
|
|
|
this.containerHeight = rect.height; |
|
|
|
} |
|
|
|
}).exec(); |
|
|
|
}, 500); |
|
|
|
}, |
|
|
|
onPageScroll(e) { |
|
|
|
this.lastScrollTop = e.scrollTop; |
|
|
|
// 当页面滚动到container应该触顶的位置时,启用吸顶 |
|
|
|
const stickyThreshold = this.containerOriginalTop - this.navBarHeight; |
|
|
|
if (stickyThreshold <= 0) return; |
|
|
|
if (e.scrollTop >= stickyThreshold) { |
|
|
|
if (!this.isContainerSticky) { |
|
|
|
this.isContainerSticky = true; |
|
|
|
} |
|
|
|
// 限制页面滚动不超过吸顶阈值,防止吸顶后页面继续滚动 |
|
|
|
if (e.scrollTop > stickyThreshold + 5) { |
|
|
|
uni.pageScrollTo({ |
|
|
|
scrollTop: stickyThreshold, |
|
|
|
duration: 0 |
|
|
|
}); |
|
|
|
} |
|
|
|
} else { |
|
|
|
if (this.isContainerSticky) { |
|
|
|
this.isContainerSticky = false; |
|
|
|
} |
|
|
|
} |
|
|
|
// 节流:80ms 内不重复检测,避免频繁的 selectorQuery |
|
|
|
const now = Date.now(); |
|
|
|
if (now - this._lastDetectTs < 80) return; |
|
|
|
this._lastDetectTs = now; |
|
|
|
this.detectCurrentCategory(); |
|
|
|
}, |
|
|
|
methods: { |
|
|
|
nowMakeMethod(){ |
|
|
|
this.nowMake = !this.nowMake |
|
|
|
}, |
|
|
|
// 页面touchmove事件:吸顶后阻止页面整体滑动(让goods-list接管滚动) |
|
|
|
onPageTouchMove(e) { |
|
|
|
|
|
|
|
}, |
|
|
|
// goods-list滚动事件:检测分类切换 + 滚回顶部取消吸顶 |
|
|
|
onGoodsListScroll(e) { |
|
|
|
if (this.isAutoScrolling) return; |
|
|
|
const scrollTop = e.detail.scrollTop; |
|
|
|
|
|
|
|
// 根据滚动位置自动选中对应分类 |
|
|
|
this.detectCurrentCategory(); |
|
|
|
}, |
|
|
|
onGoodsListScrollToTop() { |
|
|
|
if (this.isAutoScrolling) return; |
|
|
|
|
|
|
|
scrollGoodsListToTop() { |
|
|
|
// 滚到 catalog-row 顶部并紧贴 nav-bar |
|
|
|
const query = uni.createSelectorQuery().in(this); |
|
|
|
query.select('#catalog-row').boundingClientRect(); |
|
|
|
query.selectViewport().scrollOffset(); |
|
|
|
query.exec((res) => { |
|
|
|
if (!res || !res[0] || !res[1]) { |
|
|
|
this.isSwitching = false; |
|
|
|
return; |
|
|
|
} |
|
|
|
const target = Math.max(res[1].scrollTop + res[0].top - this.navBarHeight, 0); |
|
|
|
uni.pageScrollTo({ |
|
|
|
scrollTop: target, |
|
|
|
duration: 200 |
|
|
|
}); |
|
|
|
setTimeout(() => { |
|
|
|
this.isSwitching = false; |
|
|
|
}, 350); |
|
|
|
}); |
|
|
|
}, |
|
|
|
// 检测当前可见的分类 |
|
|
|
// 检测当前在 nav-bar 下沿可见的分类,并同步左侧菜单高亮 |
|
|
|
detectCurrentCategory() { |
|
|
|
if (this.isSwitching) return; |
|
|
|
const query = uni.createSelectorQuery().in(this); |
|
|
|
// 获取goods-list中所有goods-member的位置 |
|
|
|
query.selectAll('.goods-member').boundingClientRect(); |
|
|
|
query.select('.goods-list').boundingClientRect(); |
|
|
|
query.exec((res) => { |
|
|
|
if (!res || !res[0] || !res[1]) return; |
|
|
|
if (!res || !res[0]) return; |
|
|
|
const items = res[0]; |
|
|
|
const listRect = res[1]; |
|
|
|
const listTop = listRect.top; |
|
|
|
// 判定基准:相对视口顶端的 navBarHeight 偏移 |
|
|
|
const baseline = this.navBarHeight + 20; |
|
|
|
let currentCategoryId = ''; |
|
|
|
// 找到第一个顶部在goods-list可视区域内或刚过顶部的item |
|
|
|
for (let i = items.length - 1; i >= 0; i--) { |
|
|
|
if (items[i].top <= listTop + 10) { |
|
|
|
// 从productItem中获取categoryId |
|
|
|
if (items[i].top <= baseline) { |
|
|
|
if (this.productItem[i]) { |
|
|
|
currentCategoryId = this.productItem[i].categoryId; |
|
|
|
} |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
if (currentCategoryId !== '' || currentCategoryId === '') { |
|
|
|
// 在menuList中找到匹配的菜单并选中 |
|
|
|
let found = false; |
|
|
|
for (let i = 0; i < this.menuList.length; i++) { |
|
|
|
if (String(this.menuList[i].id) === String(currentCategoryId)) { |
|
|
|
@ -916,19 +884,15 @@ |
|
|
|
for (let j = 0; j < this.menuList.length; j++) { |
|
|
|
this.menuList[j].checked = (j === i); |
|
|
|
} |
|
|
|
this.$forceUpdate(); |
|
|
|
} |
|
|
|
found = true; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
// 如果没找到匹配的分类(如categoryId为空),选中"猜你喜欢" |
|
|
|
if (!found && currentCategoryId === '') { |
|
|
|
for (let j = 0; j < this.menuList.length; j++) { |
|
|
|
this.menuList[j].checked = (j === 0); |
|
|
|
} |
|
|
|
this.$forceUpdate(); |
|
|
|
} |
|
|
|
} |
|
|
|
}); |
|
|
|
}, |
|
|
|
@ -1014,49 +978,34 @@ |
|
|
|
} |
|
|
|
|
|
|
|
const selectedCategory = this.menuList[index]; |
|
|
|
// 如果是"推荐"(id为tuijian或空),滚动到顶部 |
|
|
|
if (!selectedCategory.id && selectedCategory.id !== 0) { |
|
|
|
this.goodsListScrollTop = 0; |
|
|
|
this.$nextTick(() => { |
|
|
|
this.goodsListScrollTop = 0; |
|
|
|
}); |
|
|
|
setTimeout(() => { |
|
|
|
this.isSwitching = false; |
|
|
|
}, 300); |
|
|
|
this.$forceUpdate(); |
|
|
|
this.scrollGoodsListToTop(); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// 滚动到对应分类的首个商品 |
|
|
|
const doScrollToCategory = () => { |
|
|
|
const targetId = 'category-' + selectedCategory.id; |
|
|
|
this.scrollIntoViewId = ''; |
|
|
|
// 通过页面 scroll 滚到 #category-<id> 紧贴 nav-bar 下沿 |
|
|
|
const targetSelector = '#category-' + selectedCategory.id; |
|
|
|
this.$nextTick(() => { |
|
|
|
this.isAutoScrolling = true; |
|
|
|
this.scrollIntoViewId = targetId; |
|
|
|
setTimeout(() => { |
|
|
|
this.isAutoScrolling = false; |
|
|
|
const query = uni.createSelectorQuery().in(this); |
|
|
|
query.select(targetSelector).boundingClientRect(); |
|
|
|
query.selectViewport().scrollOffset(); |
|
|
|
query.exec((res) => { |
|
|
|
if (!res || !res[0] || !res[1]) { |
|
|
|
this.isSwitching = false; |
|
|
|
}, 500); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
// 如果还没吸顶,先滚动页面到吸顶位置,再滚动商品列表 |
|
|
|
if (!this.isContainerSticky) { |
|
|
|
const stickyThreshold = this.containerOriginalTop - this.navBarHeight; |
|
|
|
return; |
|
|
|
} |
|
|
|
const rect = res[0]; |
|
|
|
const vp = res[1]; |
|
|
|
const target = Math.max(vp.scrollTop + rect.top - this.navBarHeight, 0); |
|
|
|
uni.pageScrollTo({ |
|
|
|
scrollTop: stickyThreshold, |
|
|
|
duration: 200, |
|
|
|
complete: () => { |
|
|
|
scrollTop: target, |
|
|
|
duration: 200 |
|
|
|
}); |
|
|
|
setTimeout(() => { |
|
|
|
doScrollToCategory(); |
|
|
|
}, 250); |
|
|
|
} |
|
|
|
this.isSwitching = false; |
|
|
|
}, 350); |
|
|
|
}); |
|
|
|
}); |
|
|
|
} else { |
|
|
|
doScrollToCategory(); |
|
|
|
} |
|
|
|
this.$forceUpdate(); |
|
|
|
}, |
|
|
|
openOrderWait(item) { |
|
|
|
this.chooseWaitType = false |
|
|
|
@ -2051,8 +2000,9 @@ |
|
|
|
|
|
|
|
.goods-list { |
|
|
|
flex: 1; |
|
|
|
height: 100%; |
|
|
|
min-height: 0; |
|
|
|
border-radius: 20rpx; |
|
|
|
overflow: hidden; |
|
|
|
} |
|
|
|
.fee-value { |
|
|
|
color: #00231C; |
|
|
|
@ -2805,9 +2755,37 @@ |
|
|
|
box-shadow: 0 -8rpx 28rpx rgba(0, 35, 28, 0.04); |
|
|
|
} |
|
|
|
|
|
|
|
/* 单一 page-scroll 结构下的目录布局:左侧分类条 sticky,右侧商品跟随页面滚动 */ |
|
|
|
.catalog-row { |
|
|
|
display: flex; |
|
|
|
align-items: flex-start; |
|
|
|
margin-top: 20rpx; |
|
|
|
background: transparent; |
|
|
|
} |
|
|
|
|
|
|
|
.catalog-menu-wrap { |
|
|
|
position: sticky; |
|
|
|
z-index: 50; |
|
|
|
flex-shrink: 0; |
|
|
|
width: 160rpx; |
|
|
|
background: rgba(247, 255, 251, 0.95); |
|
|
|
border-right: 1rpx solid #eee; |
|
|
|
border-top-left-radius: 34rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.catalog-goods { |
|
|
|
flex: 1; |
|
|
|
min-width: 0; |
|
|
|
padding-left: 4rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.menu-scroll { |
|
|
|
background: rgba(247, 255, 251, 0.9); |
|
|
|
border-right: 0 !important; |
|
|
|
flex-shrink: 0; |
|
|
|
width: 160rpx; |
|
|
|
font-weight: 700; |
|
|
|
font-size: 28rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.menu1 { |
|
|
|
|