This commit is contained in:
张成
2025-11-09 18:05:03 +08:00
parent 712ebe6463
commit 0090fc45c6
9 changed files with 211 additions and 110 deletions

View File

@@ -1,6 +1,6 @@
export default definePageConfig({
navigationBarTitleText: '',
enablePullDownRefresh: true,
enablePullDownRefresh: false, // 禁用页面级下拉刷新,使用 ScrollView 的下拉刷新
backgroundTextStyle: 'dark',
navigationStyle: 'custom',
backgroundColor: '#FAFAFA',

View File

@@ -81,22 +81,54 @@
background-color: #FAFAFA;
font-family: "PingFang SC";
transition: padding-top 0.3s ease-in-out; // 添加过渡动画,让布局变化更平滑
display: flex;
flex-direction: column;
height: calc(100vh - var(--status-bar-height, 0px) - var(--nav-bar-height, 0px) - 112px); // 减去底部导航栏高度 112px
overflow: hidden;
.listTopSearchWrapper {
padding: 0 15px;
.fixedHeader {
position: sticky;
top: 0;
z-index: 100;
background-color: #fff;
display: flex;
flex-direction: column;
}
.listTopSearchWrapper {
background-color: #fff;
// 使用 margin-top 负值来控制可见性,保持元素高度不变,筛选项位置固定
transition: margin-top 0.25s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.2s ease-out;
padding: 10px 15px 10px 15px; // 统一边距上下10px 左右15px
box-sizing: border-box;
overflow: hidden;
will-change: margin-top, opacity;
&.show {
opacity: 1;
margin-top: 0; // 正常显示
}
&.hide {
opacity: 0;
margin-top: -64px; // 使用负 margin 向上隐藏,但仍占据布局空间 (44px内容 + 20px padding = 64px)
pointer-events: none; // 隐藏时禁用交互
}
}
.listTopFilterWrapper {
display: flex;
box-sizing: border-box;
align-items: center;
padding-top: 10px;
padding-bottom: 10px;
padding: 0 15px 10px 15px; // 上0 左右15px 下10px与搜索框左右对齐下边距一致
gap: 5px;
position: sticky;
background-color: #fff;
z-index: 100;
}
.listScrollView {
flex: 1;
height: 0; // 让 flex 生效
}
.menuFilter {

View File

@@ -2,14 +2,10 @@ import SearchBar from "@/components/SearchBar";
import FilterPopup from "@/components/FilterPopup";
import styles from "./index.module.scss";
import { useEffect, useRef, useCallback, useState } from "react";
import Taro, {
usePageScroll,
// useShareAppMessage,
// useShareTimeline,
} from "@tarojs/taro";
import Taro from "@tarojs/taro";
import { useListStore } from "@/store/listStore";
import { useGlobalState } from "@/store/global";
import { View, Image, Text } from "@tarojs/components";
import { View, Image, Text, ScrollView } from "@tarojs/components";
import CustomerNavBar from "@/container/listCustomNavbar";
import GuideBar from "@/components/GuideBar";
import ListContainer from "@/container/listContainer";
@@ -65,46 +61,75 @@ const ListPage = () => {
isShowNoData,
} = listPageState || {};
// 防抖的滚动处理函数
const handleScroll = useCallback(
(res) => {
const currentScrollTop = res?.scrollTop || 0;
// 滚动相关状态
const scrollContextRef = useRef(null);
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const lastScrollTopRef = useRef(0);
const scrollDirectionRef = useRef<'up' | 'down' | null>(null);
const lastScrollTimeRef = useRef(Date.now());
const loadingMoreRef = useRef(false); // 防止重复加载更多
const [showSearchBar, setShowSearchBar] = useState(true); // 控制搜索框显示/隐藏(筛选始终显示)
// 添加缓冲区,避免临界点频繁切换
const buffer = 10; // 10px 缓冲区
const shouldShowInputNav = currentScrollTop >= totalHeight + buffer;
const shouldHideInputNav = currentScrollTop < totalHeight - buffer;
// ScrollView 滚动处理函数
const handleScrollViewScroll = useCallback(
(e: any) => {
const currentScrollTop = e?.detail?.scrollTop || 0;
const lastScrollTop = lastScrollTopRef.current;
const currentTime = Date.now();
const timeDiff = currentTime - lastScrollTimeRef.current;
// 节流:如果时间差太小,跳过本次处理
if (timeDiff < 16) return; // 约60fps避免过于频繁的计算
// 计算滚动距离
const scrollDiff = currentScrollTop - lastScrollTop;
// 判断滚动方向(提高阈值避免微小滚动误判)
let newDirection = scrollDirectionRef.current;
if (scrollDiff > 5) {
newDirection = 'up';
} else if (scrollDiff < -5) {
newDirection = 'down';
}
scrollDirectionRef.current = newDirection;
// 清除之前的定时器
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
}
// 防抖处理,避免频繁更新状态
scrollTimeoutRef.current = setTimeout(() => {
// 只有在状态真正需要改变时才更新
if (shouldShowInputNav && !isShowInputCustomerNavBar) {
// 滚动阈值(提高阈值减少误触发)
const threshold = 80;
if (newDirection === 'up' && currentScrollTop > threshold) {
// 上滑超过阈值,隐藏搜索框
if (showSearchBar) {
setShowSearchBar(false);
}
if (!isShowInputCustomerNavBar) {
updateListPageState({
isShowInputCustomerNavBar: true,
});
} else if (shouldHideInputNav && isShowInputCustomerNavBar) {
}
} else if (newDirection === 'down' || currentScrollTop <= threshold) {
// 下滑或回到顶部,显示搜索框
if (!showSearchBar) {
setShowSearchBar(true);
}
if (isShowInputCustomerNavBar) {
updateListPageState({
isShowInputCustomerNavBar: false,
});
}
}
lastScrollTopRef.current = currentScrollTop;
}, 16); // 约60fps的防抖间隔
lastScrollTopRef.current = currentScrollTop;
lastScrollTimeRef.current = currentTime;
},
[totalHeight, isShowInputCustomerNavBar, updateState]
[showSearchBar, isShowInputCustomerNavBar, updateListPageState]
);
usePageScroll(handleScroll);
const scrollContextRef = useRef(null);
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const lastScrollTopRef = useRef(0);
useEffect(() => {
getLocation();
fetchUserInfo();
@@ -112,15 +137,16 @@ const ListPage = () => {
getCityQrCode();
}, []);
// 监听数据变化,如果是第一页就滚动到顶部
// 监听数据变化,如果是第一页就恢复显示搜索框
useEffect(() => {
if (pageOption?.page === 1 && matches?.length > 0) {
Taro.pageScrollTo({
scrollTop: 0,
duration: 300,
// 恢复搜索框显示
setShowSearchBar(true);
updateListPageState({
isShowInputCustomerNavBar: false,
});
}
}, [matches, pageOption?.page]);
}, [matches, pageOption?.page, updateListPageState]);
// 清理定时器
useEffect(() => {
@@ -164,40 +190,32 @@ const ListPage = () => {
return location;
};
const refreshMatches = () => {
initialFilterSearch(true);
const refreshMatches = async () => {
await initialFilterSearch(true);
};
// const getLoadMoreMatches = () => {
// loadMoreMatches()
// }
// 下拉刷新状态
const [refreshing, setRefreshing] = useState(false);
// 下拉刷新处理函数 - 使用Taro生命周期钩子
Taro.usePullDownRefresh(async () => {
// ScrollView 下拉刷新处理函数
const handleRefresh = async () => {
setRefreshing(true);
try {
// 调用刷新方法
await refreshMatches();
// 刷新完成后停止下拉刷新动画
Taro.stopPullDownRefresh();
// 显示刷新成功提示
// Taro.showToast({
// title: "刷新成功",
// icon: "success",
// duration: 1000,
// });
} catch (error) {
// 刷新失败时也停止动画
Taro.stopPullDownRefresh();
Taro.showToast({
title: "刷新失败,请重试",
icon: "error",
duration: 1000,
});
} finally {
// 使用 setTimeout 确保状态更新在下一个事件循环中执行,让 ScrollView 能正确响应
setTimeout(() => {
setRefreshing(false);
}, 0);
}
});
};
/**
* @description 综合筛选确认
@@ -226,7 +244,7 @@ const ListPage = () => {
updateFilterOptions(params);
};
const handleSearchChange = () => {};
const handleSearchChange = () => { };
// 距离筛选
const handleDistanceOrQuickChange = (name, value) => {
@@ -363,7 +381,7 @@ const ListPage = () => {
return (
<>
{/* 自定义导航 */}
<CustomerNavBar
config={{
@@ -392,44 +410,70 @@ const ListPage = () => {
/>
</View>
)}
<View className={`${styles.listTopSearchWrapper}`}>
<SearchBar
handleFilterIcon={toggleShowPopup}
isSelect={filterCount > 0}
filterCount={filterCount}
onChange={handleSearchChange}
value={searchValue}
onInputClick={handleSearchClick}
/>
</View>
{/* 筛选 */}
<View
className={styles.listTopFilterWrapper}
style={{
top: totalHeight - 1,
}}
>
<DistanceQuickFilter
cityOptions={distanceData}
quickOptions={quickFilterData}
onChange={handleDistanceOrQuickChange}
cityName="distanceFilter"
quickName="order"
cityValue={distanceQuickFilter?.distanceFilter}
quickValue={distanceQuickFilter?.order}
/>
{/* 固定在顶部的搜索框和筛选 */}
<View className={styles.fixedHeader}>
{/* 搜索框 - 可隐藏 */}
<View className={`${styles.listTopSearchWrapper} ${showSearchBar ? styles.show : styles.hide}`}>
<SearchBar
handleFilterIcon={toggleShowPopup}
isSelect={filterCount > 0}
filterCount={filterCount}
onChange={handleSearchChange}
value={searchValue}
onInputClick={handleSearchClick}
/>
</View>
{/* 筛选 - 始终显示,固定在原位置 */}
<View className={styles.listTopFilterWrapper}>
<DistanceQuickFilter
cityOptions={distanceData}
quickOptions={quickFilterData}
onChange={handleDistanceOrQuickChange}
cityName="distanceFilter"
quickName="order"
cityValue={distanceQuickFilter?.distanceFilter}
quickValue={distanceQuickFilter?.order}
/>
</View>
</View>
{/* 列表内容 */}
<ListContainer
data={matches}
recommendList={recommendList}
loading={loading}
isShowNoData={isShowNoData}
error={error}
reload={refreshMatches}
loadMoreMatches={loadMoreMatches}
/>
{/* 可滚动的列表内容 */}
<ScrollView
scrollY
className={styles.listScrollView}
scrollWithAnimation
enhanced
showScrollbar={false}
refresherEnabled={true}
refresherTriggered={refreshing}
onRefresherRefresh={handleRefresh}
lowerThreshold={100}
onScrollToLower={async () => {
// 防止重复调用,检查 loading 状态和是否正在加载更多
if (!loading && !loadingMoreRef.current && listPageState?.isHasMoreData) {
loadingMoreRef.current = true;
try {
await loadMoreMatches();
} catch (error) {
console.error("加载更多失败:", error);
} finally {
// 接口完成后重置状态,允许下次加载
loadingMoreRef.current = false;
}
}
}}
onScroll={handleScrollViewScroll}
>
<ListContainer
data={matches}
recommendList={recommendList}
loading={loading}
isShowNoData={isShowNoData}
error={error}
reload={refreshMatches}
loadMoreMatches={loadMoreMatches}
/>
</ScrollView>
</View>
</View>
)}