320 lines
8.9 KiB
TypeScript
320 lines
8.9 KiB
TypeScript
import { View, Text, Image } from "@tarojs/components";
|
|
import ListCard from "@/components/ListCard";
|
|
import ListLoadError from "@/components/ListLoadError";
|
|
import ListCardSkeleton from "@/components/ListCardSkeleton";
|
|
import { useReachBottom } from "@tarojs/taro";
|
|
import Taro from "@tarojs/taro";
|
|
import {
|
|
useUserInfo,
|
|
useUserActions,
|
|
useLastTestResult,
|
|
} from "@/store/userStore";
|
|
import { NTRPTestEntryCard } from "@/components";
|
|
import { EvaluateScene } from "@/store/evaluateStore";
|
|
import { waitForAuthInit } from "@/utils/authInit";
|
|
import "./index.scss";
|
|
import { useRef, useEffect, useState, useMemo } from "react";
|
|
import { useDictionaryStore } from "@/store/dictionaryStore";
|
|
|
|
const ListContainer = (props) => {
|
|
const {
|
|
loading,
|
|
isShowNoData,
|
|
data = [],
|
|
error,
|
|
reload,
|
|
// recommendList,
|
|
loadMoreMatches,
|
|
errorImg,
|
|
emptyText,
|
|
btnText,
|
|
btnImg,
|
|
style,
|
|
collapse = false,
|
|
defaultShowNum,
|
|
evaluateFlag,
|
|
enableHomeCards = false, // 仅首页需要 banner 和 NTRP 测评卡片
|
|
listLoadErrorWrapperHeight,
|
|
listLoadErrorWidth,
|
|
listLoadErrorHeight,
|
|
listLoadErrorScale,
|
|
} = props;
|
|
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
|
const loadingStartTimeRef = useRef<number | null>(null);
|
|
const skeletonTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
const [showNumber, setShowNumber] = useState(0);
|
|
const [showSkeleton, setShowSkeleton] = useState(false);
|
|
|
|
const userInfo = useUserInfo();
|
|
const { fetchUserInfo, fetchLastTestResult } = useUserActions();
|
|
// 使用全局状态中的测试结果,避免重复调用接口
|
|
const lastTestResult = useLastTestResult();
|
|
const {
|
|
bannerListImage,
|
|
bannerDetailImage,
|
|
bannerListIndex = 0,
|
|
} = useDictionaryStore((s) => s.bannerDict) || {};
|
|
useReachBottom(() => {
|
|
// 加载更多方法
|
|
if (loading) {
|
|
return;
|
|
}
|
|
// timerRef.current = setTimeout(() => {
|
|
loadMoreMatches();
|
|
// }, 500);
|
|
});
|
|
|
|
useEffect(() => {
|
|
setShowNumber(() => {
|
|
return defaultShowNum === undefined ? data?.length : defaultShowNum;
|
|
});
|
|
}, [data]);
|
|
|
|
// 控制骨架屏显示逻辑
|
|
useEffect(() => {
|
|
if (loading) {
|
|
// 开始加载时记录时间
|
|
loadingStartTimeRef.current = Date.now();
|
|
|
|
// 延迟 300ms 后再显示骨架屏
|
|
skeletonTimerRef.current = setTimeout(() => {
|
|
setShowSkeleton(true);
|
|
}, 600);
|
|
} else {
|
|
// 加载完成,清除定时器并隐藏骨架屏
|
|
if (skeletonTimerRef.current) {
|
|
clearTimeout(skeletonTimerRef.current);
|
|
skeletonTimerRef.current = null;
|
|
}
|
|
setShowSkeleton(false);
|
|
loadingStartTimeRef.current = null;
|
|
}
|
|
}, [loading]);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
if (timerRef.current) {
|
|
clearTimeout(timerRef.current);
|
|
}
|
|
if (skeletonTimerRef.current) {
|
|
clearTimeout(skeletonTimerRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
// 获取测试结果,判断最近一个月是否有测试记录(仅首页需要)
|
|
useEffect(() => {
|
|
const init = async () => {
|
|
if (!evaluateFlag || !enableHomeCards) return;
|
|
// 先等待静默登录完成
|
|
await waitForAuthInit();
|
|
// 然后再获取用户信息
|
|
const userInfoId = userInfo && "id" in userInfo ? userInfo.id : null;
|
|
if (!userInfoId) {
|
|
await fetchUserInfo();
|
|
return; // 等待下一次 useEffect 触发(此时 userInfo.id 已有值)
|
|
}
|
|
// 如果全局状态中没有测试结果,则调用接口(使用请求锁,多个组件同时调用时只会请求一次)
|
|
if (!lastTestResult) {
|
|
await fetchLastTestResult();
|
|
}
|
|
};
|
|
init();
|
|
}, [
|
|
evaluateFlag,
|
|
enableHomeCards,
|
|
userInfo,
|
|
lastTestResult,
|
|
fetchLastTestResult,
|
|
]);
|
|
|
|
// 从全局状态中获取测试状态
|
|
const hasTestInLastMonth = lastTestResult?.has_test_in_last_month || false;
|
|
|
|
if (error) {
|
|
return <ListLoadError reload={reload} />;
|
|
}
|
|
|
|
const renderSkeleton = () => {
|
|
return (
|
|
<>
|
|
{new Array(10).fill(0).map(() => {
|
|
return <ListCardSkeleton />;
|
|
})}
|
|
</>
|
|
);
|
|
};
|
|
|
|
// showNumber 为 0 表示尚未同步,不参与截断;截断时只限制「数据条数」,插卡不占数据条数
|
|
const shouldLimitByShowNumber = showNumber > 0;
|
|
|
|
// 插入 banner 卡片(在 bannerListIndex 位置插入,不替换数据)
|
|
function insertBannerCard(list) {
|
|
if (!bannerListImage) return list;
|
|
if (!list || !Array.isArray(list)) {
|
|
list = [];
|
|
}
|
|
const idx = Number(bannerListIndex);
|
|
return [
|
|
...list.slice(0, idx),
|
|
{
|
|
type: "banner",
|
|
banner_image_url: bannerListImage,
|
|
banner_detail_url: bannerDetailImage,
|
|
},
|
|
...list.slice(idx),
|
|
];
|
|
}
|
|
|
|
// 对于没有 ntrp 等级的用户每个月展示一次,插在第 2 条数据后面;插卡是插入不替换,保留全部 showNumber 条数据
|
|
function insertEvaluateCard(list) {
|
|
if (!list || !Array.isArray(list)) return insertBannerCard(list ?? []);
|
|
|
|
const limitedList = shouldLimitByShowNumber
|
|
? list.slice(0, showNumber)
|
|
: list;
|
|
|
|
if (!evaluateFlag || hasTestInLastMonth) {
|
|
return insertBannerCard(limitedList);
|
|
}
|
|
|
|
if (limitedList.length <= 2) {
|
|
return insertBannerCard([...limitedList, { type: "evaluateCard" }]);
|
|
}
|
|
|
|
const [item1, item2, ...rest] = limitedList;
|
|
const result = [item1, item2, { type: "evaluateCard" }, ...rest];
|
|
return insertBannerCard(result);
|
|
}
|
|
|
|
const memoizedList = useMemo(
|
|
() => (enableHomeCards ? insertEvaluateCard(data) : data),
|
|
[
|
|
enableHomeCards,
|
|
evaluateFlag,
|
|
data,
|
|
hasTestInLastMonth,
|
|
showNumber,
|
|
bannerListImage,
|
|
bannerDetailImage,
|
|
bannerListIndex,
|
|
]
|
|
);
|
|
|
|
// 渲染 banner 卡片
|
|
const renderBanner = (item, index) => {
|
|
if (!item?.banner_image_url) {
|
|
return null;
|
|
}
|
|
return (
|
|
<View
|
|
key={item.id || `banner-${index}`}
|
|
onClick={() => {
|
|
const target = item.banner_detail_url;
|
|
if (target) {
|
|
(Taro as any).navigateTo({
|
|
url: `/other_pages/bannerDetail/index?img=${encodeURIComponent(
|
|
target
|
|
)}`,
|
|
});
|
|
}
|
|
}}
|
|
style={{
|
|
height: "100px",
|
|
overflow: "hidden",
|
|
borderRadius: "12px",
|
|
backgroundImage: `url(${item.banner_image_url})`,
|
|
backgroundSize: "cover",
|
|
backgroundPosition: "center",
|
|
backgroundRepeat: "no-repeat",
|
|
}}
|
|
></View>
|
|
);
|
|
};
|
|
|
|
const showNoData = isShowNoData && !loading && memoizedList?.length === 0;
|
|
|
|
// 渲染列表
|
|
const renderList = () => {
|
|
// 请求数据为空
|
|
if (showNoData) {
|
|
return (
|
|
<ListLoadError
|
|
reload={reload}
|
|
errorImg={errorImg}
|
|
btnText={btnText}
|
|
btnImg={btnImg}
|
|
text={emptyText || "暂无数据"}
|
|
wrapperHeight={listLoadErrorWrapperHeight || ""}
|
|
width={listLoadErrorWidth || ""}
|
|
height={listLoadErrorHeight || ""}
|
|
scale={listLoadErrorScale || ""}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// 渲染数据
|
|
return (
|
|
<>
|
|
{memoizedList.map((match, index) => {
|
|
if (enableHomeCards && match?.type === "banner") {
|
|
return renderBanner(match, index);
|
|
}
|
|
if (enableHomeCards && match?.type === "evaluateCard") {
|
|
return (
|
|
<NTRPTestEntryCard
|
|
key={`evaluate-${index}`}
|
|
type={EvaluateScene.list}
|
|
/>
|
|
);
|
|
}
|
|
return <ListCard key={match?.id || index} {...match} />;
|
|
})}
|
|
</>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<View className="listContentWrapper" style={style}>
|
|
{renderList()}
|
|
{/* 显示骨架屏 - 只有在 loading 超过 300ms 时才显示 */}
|
|
{loading && showSkeleton && renderSkeleton()}
|
|
{/* <View className="recommendTextWrapper">
|
|
<Text className="recommendText">搜索结果较少,已为你推荐其他内容</Text>
|
|
</View>
|
|
{renderList(recommendList)} */}
|
|
{/* 到底了 */}
|
|
{collapse ? (
|
|
data?.length > defaultShowNum ? (
|
|
data?.length > showNumber ? (
|
|
<View
|
|
className="collapse-btn fold"
|
|
onClick={() => {
|
|
setShowNumber(data?.length);
|
|
}}
|
|
>
|
|
<Text>更多球局</Text>
|
|
<Image src={require("@/static/userInfo/fold.svg")}></Image>
|
|
</View>
|
|
) : (
|
|
<View
|
|
className="collapse-btn"
|
|
onClick={() => {
|
|
setShowNumber(defaultShowNum);
|
|
}}
|
|
>
|
|
<Text>收起</Text>
|
|
<Image src={require("@/static/userInfo/fold.svg")}></Image>
|
|
</View>
|
|
)
|
|
) : null
|
|
) : (
|
|
data?.length > 0 && <View className="bottomTextWrapper">到底了</View>
|
|
)}
|
|
</View>
|
|
);
|
|
};
|
|
|
|
export default ListContainer;
|