439 lines
13 KiB
TypeScript
439 lines
13 KiB
TypeScript
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;
|