Files
mini-programs/src/container/listContainer/index.tsx

288 lines
8.3 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,
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) 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, 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 />;
})}
</>
);
};
// 插入 banner 卡片
function insertBannerCard(list) {
if (!bannerListImage) return list;
return [
...list.slice(0, Number(bannerListIndex)),
{ type: "banner", banner_image_url: bannerListImage, banner_detail_url: bannerDetailImage },
...list.slice(Number(bannerListIndex))
];
}
// 对于没有ntrp等级的用户每个月展示一次, 插在第二个位置后面
function insertEvaluateCard(list) {
if (!evaluateFlag)
return showNumber !== undefined ? list.slice(0, showNumber) : list;
if (!list || list.length === 0) {
return list;
}
// 如果最近一个月有测试记录,则不插入 card
if (hasTestInLastMonth) {
return showNumber !== undefined ? list.slice(0, showNumber) : list;
}
if (list.length <= 2) {
return [...list, { type: "evaluateCard" }];
}
const [item1, item2, ...rest] = list;
let result = [
item1,
item2,
{ type: "evaluateCard" },
...(showNumber !== undefined ? rest.slice(0, showNumber - 3) : rest),
];
if (bannerListImage) {
return insertBannerCard(result);
}
return result;
}
const memoizedList = useMemo(
() => insertEvaluateCard(data),
[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 renderList = () => {
// 请求数据为空
if (isShowNoData) {
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 (match?.type === "banner") {
return renderBanner(match, index);
}
if (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;