Files
mini-programs/src/main_pages/components/ListPageContent.tsx

439 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SearchBar from "@/components/SearchBar";
import FilterPopup from "@/components/FilterPopup";
import styles from "./ListPageContent.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}
evaluateFlag
/>
</ScrollView>
</View>
</View>
)}
</>
);
};
export default ListPageContent;