This commit is contained in:
张成
2025-11-15 11:48:59 +08:00
parent 0fa0dd5b22
commit 30857c131a
8 changed files with 33 additions and 9 deletions

View File

@@ -0,0 +1,412 @@
import SearchBar from "@/components/SearchBar";
import FilterPopup from "@/components/FilterPopup";
import styles from "@/game_pages/list/index.module.scss";
import { useEffect, useRef, useCallback, useState } from "react";
import Taro from "@tarojs/taro";
import { useListStore } from "@/store/listStore";
import { useGlobalState } from "@/store/global";
import { View, Image, Text, ScrollView } from "@tarojs/components";
import ListContainer from "@/container/listContainer";
import DistanceQuickFilter from "@/components/DistanceQuickFilter";
import { updateUserLocation } from "@/services/userService";
import { useUserActions } from "@/store/userStore";
import { useDictionaryStore } from "@/store/dictionaryStore";
import { saveImage, navigateTo } from "@/utils";
export interface ListPageContentProps {
onNavStateChange?: (state: {
isShowInputCustomerNavBar?: boolean;
isDistanceFilterVisible?: boolean;
isCityPickerVisible?: boolean;
}) => void;
onScrollToTop?: () => void; // 外部滚动到顶部方法(由主容器提供)
scrollToTopTrigger?: number; // 触发滚动的计数器
onDistanceFilterVisibleChange?: (visible: boolean) => void;
onCityPickerVisibleChange?: (visible: boolean) => void; // 保留接口,但由主容器直接处理
onFilterPopupVisibleChange?: (visible: boolean) => void; // 筛选弹窗显示/隐藏回调
}
const ListPageContent: React.FC<ListPageContentProps> = ({
onNavStateChange,
onScrollToTop: _onScrollToTop,
scrollToTopTrigger,
onDistanceFilterVisibleChange,
onCityPickerVisibleChange: _onCityPickerVisibleChange,
onFilterPopupVisibleChange,
}) => {
const store = useListStore() || {};
const { fetchUserInfo } = useUserActions();
const { statusNavbarHeightInfo, getCurrentLocationInfo } = useGlobalState() || {};
const { totalHeight = 98 } = statusNavbarHeightInfo || {};
const {
listPageState,
loading,
error,
searchValue,
distanceData,
quickFilterData,
getMatchesData,
updateState,
updateListPageState,
updateFilterOptions,
clearFilterOptions,
initialFilterSearch,
loadMoreMatches,
fetchGetGamesCount,
updateDistanceQuickFilter,
getCities,
getCityQrCode,
area,
cityQrCode,
} = store;
const {
isShowFilterPopup,
data: matches,
recommendList,
filterCount,
filterOptions,
distanceQuickFilter,
isShowInputCustomerNavBar,
pageOption,
isShowNoData,
} = listPageState || {};
const scrollContextRef = useRef(null);
const scrollViewRef = 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 scrollStartPositionRef = useRef(0);
const [showSearchBar, setShowSearchBar] = useState(true);
const [scrollTop, setScrollTop] = useState(0);
const [refreshing, setRefreshing] = useState(false);
// 处理距离筛选显示/隐藏
const handleDistanceFilterVisibleChange = useCallback((visible: boolean) => {
onDistanceFilterVisibleChange?.(visible);
onNavStateChange?.({ isDistanceFilterVisible: visible });
}, [onDistanceFilterVisibleChange, onNavStateChange]);
// 处理城市选择器显示/隐藏(由主容器统一管理,通过 onNavStateChange 通知)
// 注意CustomerNavBar 的 onCityPickerVisibleChange 由主容器直接处理
// 滚动到顶部(用于 ScrollView 内部滚动)
const scrollToTopInternal = useCallback(() => {
setScrollTop(prev => prev === 0 ? 0.1 : 0);
}, []);
// 监听外部滚动触发
useEffect(() => {
if (scrollToTopTrigger && scrollToTopTrigger > 0) {
scrollToTopInternal();
}
}, [scrollToTopTrigger, scrollToTopInternal]);
// 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 < 100) return;
const scrollDiff = currentScrollTop - lastScrollTop;
let newDirection = scrollDirectionRef.current;
if (Math.abs(scrollDiff) > 15) {
if (scrollDiff > 0) {
if (newDirection !== 'up') {
scrollStartPositionRef.current = lastScrollTop;
}
newDirection = 'up';
} else {
if (newDirection !== 'down') {
scrollStartPositionRef.current = lastScrollTop;
}
newDirection = 'down';
}
scrollDirectionRef.current = newDirection;
}
const totalScrollDistance = Math.abs(currentScrollTop - scrollStartPositionRef.current);
const positionThreshold = 120;
const distanceThreshold = 80;
if (newDirection === 'up' && currentScrollTop > positionThreshold && totalScrollDistance > distanceThreshold) {
if (showSearchBar || !isShowInputCustomerNavBar) {
setShowSearchBar(false);
updateListPageState({
isShowInputCustomerNavBar: true,
});
onNavStateChange?.({ isShowInputCustomerNavBar: true });
scrollStartPositionRef.current = currentScrollTop;
}
} else if ((newDirection === 'down' && totalScrollDistance > distanceThreshold) || currentScrollTop <= positionThreshold) {
if (!showSearchBar || isShowInputCustomerNavBar) {
setShowSearchBar(true);
updateListPageState({
isShowInputCustomerNavBar: false,
});
onNavStateChange?.({ isShowInputCustomerNavBar: false });
scrollStartPositionRef.current = currentScrollTop;
}
}
lastScrollTopRef.current = currentScrollTop;
lastScrollTimeRef.current = currentTime;
},
[showSearchBar, isShowInputCustomerNavBar, updateListPageState, onNavStateChange]
);
useEffect(() => {
getLocation();
fetchUserInfo();
getCities();
getCityQrCode();
}, []);
useEffect(() => {
if (pageOption?.page === 1 && matches?.length > 0) {
setShowSearchBar(true);
updateListPageState({
isShowInputCustomerNavBar: false,
});
onNavStateChange?.({ isShowInputCustomerNavBar: false });
}
}, [matches, pageOption?.page, updateListPageState, onNavStateChange]);
useEffect(() => {
return () => {
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
}
};
}, []);
const getLocation = async () => {
const location = await getCurrentLocationInfo();
updateState({ location });
if (location && location.latitude && location.longitude) {
try {
await updateUserLocation(location.latitude, location.longitude);
} catch (error) {
console.error("更新用户位置失败:", error);
}
}
fetchGetGamesCount();
getMatchesData();
return location;
};
const refreshMatches = async () => {
await initialFilterSearch(true);
};
const handleRefresh = async () => {
setRefreshing(true);
try {
await refreshMatches();
} catch (error) {
(Taro as any).showToast({
title: "刷新失败,请重试",
icon: "error",
duration: 1000,
});
} finally {
setTimeout(() => {
setRefreshing(false);
}, 0);
}
};
const handleFilterConfirm = () => {
toggleShowPopup();
getMatchesData();
};
const toggleShowPopup = () => {
const newVisible = !isShowFilterPopup;
// 先通知父组件筛选弹窗状态变化(设置 z-index
onFilterPopupVisibleChange?.(newVisible);
// 然后更新本地状态显示/隐藏弹窗
// 使用 setTimeout 确保 z-index 先设置,再显示弹窗
if (newVisible) {
setTimeout(() => {
updateListPageState({
isShowFilterPopup: newVisible,
});
}, 50);
} else {
// 关闭时直接更新状态
updateListPageState({
isShowFilterPopup: newVisible,
});
}
};
const handleUpdateFilterOptions = (params: Record<string, any>) => {
updateFilterOptions(params);
};
const handleSearchChange = () => { };
const handleDistanceOrQuickChange = (name, value) => {
updateDistanceQuickFilter({
[name]: value,
});
};
const handleSearchClick = () => {
navigateTo({
url: "/game_pages/search/index",
});
};
const initDictionaryData = async () => {
try {
const { fetchDictionary } = useDictionaryStore.getState();
await fetchDictionary();
} catch (error) {
console.error("初始化字典数据失败:", error);
}
};
useEffect(() => {
initDictionaryData();
}, []);
const area_city = area?.at(-2) || "上海";
function renderCityQrcode() {
let item = cityQrCode.find((item) => item.city_name === area_city);
if (!item) item = cityQrCode.find((item) => item.city_name === "其他");
return (
<View className={styles.cqContainer}>
{item ? (
<View className={styles.wrapper}>
<View className={styles.tips}>
<Text className={styles.tip1}></Text>
<Text className={styles.tip2}>
</Text>
</View>
<View className={styles.qrcodeWrappper}>
<Image
className={styles.qrcode}
src={item.qr_code_url}
mode="widthFix"
onClick={() => {
saveImage(item.qr_code_url);
}}
/>
<Text className={styles.qrcodeTip}>
使
</Text>
</View>
</View>
) : (
<View>
<Text>, </Text>
</View>
)}
</View>
);
}
return (
<>
{area_city !== "上海" ? (
renderCityQrcode()
) : (
<View ref={scrollContextRef}>
<View className={styles.listPage} style={{ paddingTop: totalHeight }}>
{isShowFilterPopup && (
<View>
<FilterPopup
loading={loading}
onCancel={toggleShowPopup}
onConfirm={handleFilterConfirm}
onChange={handleUpdateFilterOptions}
filterOptions={filterOptions}
onClear={clearFilterOptions}
visible={isShowFilterPopup}
onClose={toggleShowPopup}
statusNavbarHeigh={statusNavbarHeightInfo?.totalHeight}
/>
</View>
)}
<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}
onMenuVisibleChange={handleDistanceFilterVisibleChange}
/>
</View>
</View>
<ScrollView
ref={scrollViewRef}
scrollY
scrollTop={scrollTop}
className={styles.listScrollView}
scrollWithAnimation
enhanced
showScrollbar={false}
refresherEnabled={true}
refresherTriggered={refreshing}
onRefresherRefresh={handleRefresh}
lowerThreshold={100}
onScrollToLower={async () => {
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>
)}
</>
);
};
export default ListPageContent;