613 lines
21 KiB
TypeScript
613 lines
21 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/DistanceQuickFilterV2";
|
||
import { updateUserLocation } from "@/services/userService";
|
||
import { useDictionaryStore } from "@/store/dictionaryStore";
|
||
import { saveImage, navigateTo } from "@/utils";
|
||
|
||
export interface ListPageContentProps {
|
||
isActive?: boolean; // 是否处于激活状态(当前显示的页面)
|
||
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> = ({
|
||
isActive = true,
|
||
onNavStateChange,
|
||
onScrollToTop: _onScrollToTop,
|
||
scrollToTopTrigger,
|
||
onDistanceFilterVisibleChange,
|
||
onCityPickerVisibleChange: _onCityPickerVisibleChange,
|
||
onFilterPopupVisibleChange,
|
||
}) => {
|
||
const store = useListStore() || {};
|
||
const { statusNavbarHeightInfo, getCurrentLocationInfo } =
|
||
useGlobalState() || {};
|
||
const { totalHeight = 98 } = statusNavbarHeightInfo || {};
|
||
|
||
const {
|
||
listPageState,
|
||
loading,
|
||
error,
|
||
searchValue,
|
||
distanceData,
|
||
quickFilterData,
|
||
getMatchesData,
|
||
updateState,
|
||
updateListPageState,
|
||
updateFilterOptions,
|
||
clearFilterOptions,
|
||
initialFilterSearch,
|
||
loadMoreMatches,
|
||
fetchGetGamesCount,
|
||
refreshBothLists,
|
||
updateDistanceQuickFilter,
|
||
getCities,
|
||
getCityQrCode,
|
||
getDistricts,
|
||
area,
|
||
cityQrCode,
|
||
districts,
|
||
gamesNum, // 新增:获取球局数量
|
||
} = 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 lastLoadedAreaRef = useRef<[string, string] | null>(null);
|
||
const prevIsActiveRef = useRef(isActive);
|
||
|
||
// 处理距离筛选显示/隐藏
|
||
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]);
|
||
|
||
// 使用 ref 保存最新的状态值,避免依赖项变化导致函数重新创建
|
||
const showSearchBarRef = useRef(showSearchBar);
|
||
const isShowInputCustomerNavBarRef = useRef(isShowInputCustomerNavBar);
|
||
|
||
useEffect(() => {
|
||
showSearchBarRef.current = showSearchBar;
|
||
isShowInputCustomerNavBarRef.current = isShowInputCustomerNavBar;
|
||
}, [showSearchBar, isShowInputCustomerNavBar]);
|
||
|
||
// ScrollView 滚动处理
|
||
const handleScrollViewScroll = useCallback(
|
||
(e: any) => {
|
||
const currentScrollTop = e?.detail?.scrollTop || 0;
|
||
const scrollHeight = e?.detail?.scrollHeight || 0;
|
||
const clientHeight = e?.detail?.clientHeight || 0;
|
||
const lastScrollTop = lastScrollTopRef.current;
|
||
const currentTime = Date.now();
|
||
const timeDiff = currentTime - lastScrollTimeRef.current;
|
||
|
||
if (timeDiff < 100) return;
|
||
|
||
// 计算距离底部的距离,提前加载(距离底部600px时开始加载)
|
||
// 注意:如果 scrollHeight 或 clientHeight 不可用,则使用 lowerThreshold 触发
|
||
if (scrollHeight > 0 && clientHeight > 0) {
|
||
const distanceToBottom = scrollHeight - currentScrollTop - clientHeight;
|
||
const preloadThreshold = 600; // 提前加载阈值
|
||
|
||
// 如果距离底部小于阈值,且正在向下滚动,且有更多数据,则提前加载
|
||
if (
|
||
distanceToBottom < preloadThreshold &&
|
||
distanceToBottom > 0 &&
|
||
!loading &&
|
||
!loadingMoreRef.current &&
|
||
listPageState?.isHasMoreData &&
|
||
currentScrollTop > lastScrollTop // 向下滚动
|
||
) {
|
||
loadingMoreRef.current = true;
|
||
loadMoreMatches().finally(() => {
|
||
loadingMoreRef.current = false;
|
||
});
|
||
}
|
||
}
|
||
|
||
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;
|
||
|
||
// 使用 ref 获取最新值,避免依赖项变化
|
||
const currentShowSearchBar = showSearchBarRef.current;
|
||
const currentIsShowInputCustomerNavBar = isShowInputCustomerNavBarRef.current;
|
||
|
||
if (
|
||
newDirection === "up" &&
|
||
currentScrollTop > positionThreshold &&
|
||
totalScrollDistance > distanceThreshold
|
||
) {
|
||
if (currentShowSearchBar || !currentIsShowInputCustomerNavBar) {
|
||
setShowSearchBar(false);
|
||
updateListPageState({
|
||
isShowInputCustomerNavBar: true,
|
||
});
|
||
onNavStateChange?.({ isShowInputCustomerNavBar: true });
|
||
scrollStartPositionRef.current = currentScrollTop;
|
||
}
|
||
} else if (
|
||
(newDirection === "down" && totalScrollDistance > distanceThreshold) ||
|
||
currentScrollTop <= positionThreshold
|
||
) {
|
||
if (!currentShowSearchBar || currentIsShowInputCustomerNavBar) {
|
||
setShowSearchBar(true);
|
||
updateListPageState({
|
||
isShowInputCustomerNavBar: false,
|
||
});
|
||
onNavStateChange?.({ isShowInputCustomerNavBar: false });
|
||
scrollStartPositionRef.current = currentScrollTop;
|
||
}
|
||
}
|
||
|
||
lastScrollTopRef.current = currentScrollTop;
|
||
lastScrollTimeRef.current = currentTime;
|
||
},
|
||
[updateListPageState, onNavStateChange, loading, loadMoreMatches, listPageState?.isHasMoreData]
|
||
// 移除 showSearchBar 和 isShowInputCustomerNavBar 依赖,使用 ref 获取最新值
|
||
);
|
||
|
||
useEffect(() => {
|
||
// 分批异步执行初始化操作,避免阻塞首屏渲染
|
||
// 1. 立即执行:获取城市、二维码和行政区列表(轻量操作)
|
||
getCities();
|
||
getCityQrCode();
|
||
getDistricts(); // 新增:获取行政区列表
|
||
|
||
// 只有当页面激活时才加载位置和列表数据
|
||
if (isActive) {
|
||
getLocation().catch((error) => {
|
||
console.error('获取位置信息失败:', error);
|
||
});
|
||
}
|
||
}, [isActive]);
|
||
|
||
// 记录上一次的城市,用于检测城市变化
|
||
const prevAreaRef = useRef<[string, string] | null>(null);
|
||
|
||
// 监听城市变化,重新获取行政区列表并清空已选择的行政区
|
||
useEffect(() => {
|
||
if (area && area.length >= 2) {
|
||
const currentProvince = area[1];
|
||
const prevProvince = prevAreaRef.current?.[1];
|
||
|
||
// 只有当城市真正改变时才执行(避免初始化时也触发)
|
||
if (prevProvince && prevProvince !== currentProvince) {
|
||
console.log("城市改变,重新获取行政区列表:", {
|
||
prevProvince,
|
||
currentProvince,
|
||
});
|
||
// 城市改变时,重新获取行政区列表
|
||
getDistricts();
|
||
// 清空已选择的行政区,避免显示错误的行政区
|
||
const currentState = useListStore.getState();
|
||
const currentPageState = currentState.isSearchResult
|
||
? currentState.searchPageState
|
||
: currentState.listPageState;
|
||
if (currentPageState?.distanceQuickFilter?.district) {
|
||
updateDistanceQuickFilter({ district: undefined });
|
||
}
|
||
}
|
||
// 更新记录的城市
|
||
prevAreaRef.current = area as [string, string];
|
||
}
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [area?.[1]]); // 只监听省份(area[1])的变化
|
||
|
||
// 当页面从非激活状态切换为激活状态时,检查城市是否变化,如果变化则重新加载数据
|
||
useEffect(() => {
|
||
// 如果从非激活状态变为激活状态(切回列表页)
|
||
if (isActive && !prevIsActiveRef.current) {
|
||
const currentArea = area;
|
||
const lastArea = lastLoadedAreaRef.current;
|
||
|
||
// 检查城市是否发生变化(比较省份)
|
||
const currentProvince = currentArea?.[1] || "";
|
||
const lastProvince = lastArea?.[1] || "";
|
||
|
||
// 如果城市发生变化,或者地址存在但不一致,需要重新加载数据
|
||
// 注意:即使 lastArea 为空,只要 currentArea 存在,也应该加载数据
|
||
if (currentProvince && (currentProvince !== lastProvince || !lastArea)) {
|
||
console.log("切回列表页,检测到地址变化或不一致,重新加载数据:", {
|
||
lastArea,
|
||
currentArea,
|
||
lastProvince,
|
||
currentProvince,
|
||
});
|
||
|
||
// 地址发生变化或不一致,重新加载数据和球局数量
|
||
// 先调用列表接口,然后在列表接口完成后调用数量接口
|
||
(async () => {
|
||
try {
|
||
if (refreshBothLists) {
|
||
await refreshBothLists();
|
||
}
|
||
// 列表接口完成后,再调用数量接口
|
||
if (fetchGetGamesCount) {
|
||
await fetchGetGamesCount();
|
||
}
|
||
// 数据加载完成后,更新记录的城市(记录为上一次在列表页加载数据时的城市)
|
||
if (currentArea) {
|
||
lastLoadedAreaRef.current = [...currentArea] as [string, string];
|
||
}
|
||
} catch (error) {
|
||
console.error("重新加载数据失败:", error);
|
||
}
|
||
})();
|
||
}
|
||
}
|
||
|
||
// 如果是首次加载且列表页激活,记录当前城市(用于后续比较)
|
||
if (isActive && !lastLoadedAreaRef.current && area) {
|
||
lastLoadedAreaRef.current = [...area] as [string, string];
|
||
}
|
||
|
||
// 更新上一次的激活状态
|
||
prevIsActiveRef.current = isActive;
|
||
}, [isActive, area, refreshBothLists, fetchGetGamesCount]);
|
||
|
||
// 监听城市变化(在列表页激活状态下),当城市切换后立即更新记录
|
||
// 注意:这个 useEffect 用于处理在列表页激活状态下切换城市的情况
|
||
// 当用户在列表页切换城市时,HomeNavbar 的 handleCityChange 已经会调用 refreshBothLists
|
||
// 这里只需要同步更新 lastLoadedAreaRef,确保后续检测逻辑正确
|
||
useEffect(() => {
|
||
// 如果页面激活且城市发生变化(用户在列表页切换了城市)
|
||
if (isActive && area) {
|
||
const currentProvince = area[1] || "";
|
||
const lastProvince = lastLoadedAreaRef.current?.[1] || "";
|
||
|
||
// 如果城市发生变化,立即更新记录(因为 refreshBothLists 已经在 HomeNavbar 中调用)
|
||
if (currentProvince && currentProvince !== lastProvince) {
|
||
// 立即更新记录,确保地址显示和使用的地址一致
|
||
lastLoadedAreaRef.current = [...area] as [string, string];
|
||
}
|
||
}
|
||
}, [isActive, area]);
|
||
|
||
useEffect(() => {
|
||
if (pageOption?.page === 1 && matches?.length > 0) {
|
||
setShowSearchBar(true);
|
||
updateListPageState({
|
||
isShowInputCustomerNavBar: false,
|
||
});
|
||
onNavStateChange?.({ isShowInputCustomerNavBar: false });
|
||
}
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [matches?.length, pageOption?.page]);
|
||
// 注意:updateListPageState 和 onNavStateChange 是稳定的函数引用,不需要加入依赖项
|
||
// 只依赖实际会变化的数据:matches 的长度和 pageOption.page
|
||
|
||
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);
|
||
}
|
||
}
|
||
// 先调用列表接口
|
||
await getMatchesData();
|
||
// 列表接口完成后,再调用数量接口
|
||
await fetchGetGamesCount();
|
||
// 初始数据加载完成后,记录当前城市
|
||
if (area && isActive) {
|
||
lastLoadedAreaRef.current = [...area] as [string, string];
|
||
}
|
||
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 {
|
||
// 使用 requestAnimationFrame 替代 setTimeout(0),性能更好
|
||
requestAnimationFrame(() => {
|
||
setRefreshing(false);
|
||
});
|
||
}
|
||
};
|
||
|
||
const handleFilterConfirm = () => {
|
||
toggleShowPopup();
|
||
getMatchesData();
|
||
};
|
||
|
||
const toggleShowPopup = () => {
|
||
const newVisible = !isShowFilterPopup;
|
||
// 先通知父组件筛选弹窗状态变化(设置 z-index)
|
||
onFilterPopupVisibleChange?.(newVisible);
|
||
// 然后更新本地状态显示/隐藏弹窗
|
||
// 使用 requestAnimationFrame 确保 z-index 先设置,再显示弹窗
|
||
if (newVisible) {
|
||
// 使用双帧延迟确保 z-index 已生效
|
||
requestAnimationFrame(() => {
|
||
requestAnimationFrame(() => {
|
||
updateListPageState({
|
||
isShowFilterPopup: newVisible,
|
||
});
|
||
});
|
||
});
|
||
} 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();
|
||
}, []);
|
||
|
||
// 获取省份名称(area 格式: ["中国", "省份"])
|
||
const province = area?.at(1) || "上海";
|
||
|
||
function renderCityQrcode() {
|
||
// 根据省份查找对应的二维码
|
||
let item = cityQrCode.find((item) => item.city_name === province);
|
||
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"
|
||
showMenuByLongpress
|
||
onClick={() => {
|
||
saveImage(item.qr_code_url);
|
||
}}
|
||
/>
|
||
<Text className={styles.qrcodeTip}>
|
||
点击图片保存,使用微信扫码加入群聊
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
) : (
|
||
<View>
|
||
<Text>当前城市暂无球局, 敬请期待</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
);
|
||
}
|
||
|
||
// 判定是否显示"暂无球局"页面
|
||
// 条件:省份不是上海 或 (已加载完成且球局数量为0)
|
||
const shouldShowNoGames = province !== "上海";
|
||
|
||
return (
|
||
<>
|
||
{shouldShowNoGames ? (
|
||
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}
|
||
districtOptions={districts || []}
|
||
cityName="distanceFilter"
|
||
quickName="order"
|
||
districtName="district"
|
||
cityValue={distanceQuickFilter?.distanceFilter}
|
||
quickValue={distanceQuickFilter?.order}
|
||
districtValue={distanceQuickFilter?.district}
|
||
onMenuVisibleChange={handleDistanceFilterVisibleChange}
|
||
/>
|
||
</View>
|
||
</View>
|
||
|
||
<ScrollView refresherBackground="#FAFAFA"
|
||
ref={scrollViewRef}
|
||
scrollY
|
||
scrollTop={scrollTop}
|
||
className={styles.listScrollView}
|
||
scrollWithAnimation
|
||
enhanced
|
||
showScrollbar={false}
|
||
refresherEnabled={true}
|
||
refresherTriggered={refreshing}
|
||
onRefresherRefresh={handleRefresh}
|
||
lowerThreshold={600}
|
||
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;
|