Files
mini-programs/src/container/listContainer/index.tsx
2026-02-11 23:14:22 +08:00

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;