diff --git a/src/components/NTRPTestEntryCard/index.tsx b/src/components/NTRPTestEntryCard/index.tsx index ece7e53..e433ab6 100644 --- a/src/components/NTRPTestEntryCard/index.tsx +++ b/src/components/NTRPTestEntryCard/index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useCallback, memo } from "react"; import { View, Image, Text } from "@tarojs/components"; import Taro from "@tarojs/taro"; import { useUserInfo, useUserActions } from "@/store/userStore"; @@ -26,82 +26,87 @@ function NTRPTestEntryCard(props: { console.log(userInfo); useEffect(() => { - fetchUserInfo(); + if (!userInfo.id) { + fetchUserInfo(); + } evaluateService.getLastResult().then((res) => { setTestFlag(res.code === 0 && res.data.has_ntrp_level); }); - }, []); + }, [userInfo.id]); - function handleTest() { - switch (type) { - case EvaluateScene.list: - setCallback({ - type, - next: () => { - Taro.redirectTo({ url: "/game_pages/list/index" }); - }, - onCancel: () => { - // Taro.redirectTo({ url: "/game_pages/list/index" }); - Taro.navigateBack(); - }, - }); - break; - case EvaluateScene.share: - setCallback({ - type, - next: () => { - Taro.redirectTo({ url: "/main_pages/index" }); - }, - onCancel: () => { - Taro.redirectTo({ url: "/main_pages/index" }); - }, - }); - break; - case EvaluateScene.detail: - case EvaluateScene.publish: - setCallback(evaluateCallback as EvaluateCallback); - break; - case EvaluateScene.user: - setCallback({ - type, - next: () => { - Taro.redirectTo({ url: "/main_pages/index" }); - }, - onCancel: () => { - // Taro.redirectTo({ url: "/user_pages/myself/index" }); - Taro.navigateBack(); - }, - }); - break; - case EvaluateScene.userEdit: - setCallback({ - type, - next: () => { - Taro.redirectTo({ url: "/game_pages/list/index" }); - }, - onCancel: () => { - // Taro.redirectTo({ url: "/user_pages/edit/index" }); - Taro.navigateBack(); - }, - }); - break; - default: - setCallback({ - type, - next: () => { - Taro.redirectTo({ url: "/main_pages/index" }); - }, - onCancel: () => { - Taro.redirectTo({ url: "/main_pages/index" }); - }, - }); - } - Taro.navigateTo({ - url: `/other_pages/ntrp-evaluate/index?stage=${ - testFlag ? StageType.INTRO : StageType.TEST - }`, - }); - } + const handleTest = useCallback( + function () { + switch (type) { + case EvaluateScene.list: + setCallback({ + type, + next: () => { + Taro.redirectTo({ url: "/game_pages/list/index" }); + }, + onCancel: () => { + // Taro.redirectTo({ url: "/game_pages/list/index" }); + Taro.navigateBack(); + }, + }); + break; + case EvaluateScene.share: + setCallback({ + type, + next: () => { + Taro.redirectTo({ url: "/main_pages/index" }); + }, + onCancel: () => { + Taro.redirectTo({ url: "/main_pages/index" }); + }, + }); + break; + case EvaluateScene.detail: + case EvaluateScene.publish: + setCallback(evaluateCallback as EvaluateCallback); + break; + case EvaluateScene.user: + setCallback({ + type, + next: () => { + Taro.redirectTo({ url: "/main_pages/index" }); + }, + onCancel: () => { + // Taro.redirectTo({ url: "/user_pages/myself/index" }); + Taro.navigateBack(); + }, + }); + break; + case EvaluateScene.userEdit: + setCallback({ + type, + next: () => { + Taro.redirectTo({ url: "/game_pages/list/index" }); + }, + onCancel: () => { + // Taro.redirectTo({ url: "/user_pages/edit/index" }); + Taro.navigateBack(); + }, + }); + break; + default: + setCallback({ + type, + next: () => { + Taro.redirectTo({ url: "/main_pages/index" }); + }, + onCancel: () => { + Taro.redirectTo({ url: "/main_pages/index" }); + }, + }); + } + Taro.navigateTo({ + url: `/other_pages/ntrp-evaluate/index?stage=${ + testFlag ? StageType.INTRO : StageType.TEST + }`, + }); + }, + [setCallback] + ); return type === EvaluateScene.list ? ( @@ -169,4 +174,5 @@ function NTRPTestEntryCard(props: { ); } -export default NTRPTestEntryCard; +export default memo(NTRPTestEntryCard); +// export default NTRPTestEntryCard; diff --git a/src/container/listContainer/index.tsx b/src/container/listContainer/index.tsx index 6d1439c..cdcf4cf 100644 --- a/src/container/listContainer/index.tsx +++ b/src/container/listContainer/index.tsx @@ -8,7 +8,7 @@ import { setStorage, getStorage } from "@/store/storage"; import { NTRPTestEntryCard } from "@/components"; import { EvaluateScene } from "@/store/evaluateStore"; import "./index.scss"; -import { useRef, useEffect, useState } from "react"; +import { useRef, useEffect, useState, useMemo } from "react"; const ListContainer = (props) => { const { @@ -26,6 +26,7 @@ const ListContainer = (props) => { style, collapse = false, defaultShowNum, + evaluateFlag, } = props; const timerRef = useRef(null); const loadingStartTimeRef = useRef(null); @@ -47,19 +48,17 @@ const ListContainer = (props) => { }); useEffect(() => { - setShowNumber( - () => { - return defaultShowNum === undefined ? data?.length : defaultShowNum - - }) - }, [data]) + setShowNumber(() => { + return defaultShowNum === undefined ? data?.length : defaultShowNum; + }); + }, [data]); // 控制骨架屏显示逻辑 useEffect(() => { if (loading) { // 开始加载时记录时间 loadingStartTimeRef.current = Date.now(); - + // 延迟 300ms 后再显示骨架屏 skeletonTimerRef.current = setTimeout(() => { setShowSkeleton(true); @@ -102,6 +101,7 @@ const ListContainer = (props) => { // 对于没有ntrp等级的用户每个月展示一次, 插在第三个位置 function insertEvaluateCard(list) { + if (!evaluateFlag) return list; if (!list || list.length === 0) { return list; } @@ -122,6 +122,11 @@ const ListContainer = (props) => { return [item1, item2, item3, { type: "evaluateCard" }, ...rest]; } + const memoizedList = useMemo( + () => insertEvaluateCard(data), + [evaluateFlag, data, userInfo.ntrp_level] + ); + // 渲染列表 const renderList = (list) => { // 请求数据为空 @@ -137,12 +142,12 @@ const ListContainer = (props) => { ); } - showNumber !== undefined && (list = list.slice(0, showNumber)) + showNumber !== undefined && (list = list.slice(0, showNumber)); // 渲染数据 return ( <> - {insertEvaluateCard(list).map((match, index) => { + {memoizedList.map((match, index) => { if (match.type === "evaluateCard") { return ( @@ -164,20 +169,33 @@ const ListContainer = (props) => { {renderList(recommendList)} */} {/* 到底了 */} - {collapse ? - data?.length > defaultShowNum ? - data?.length > showNumber ? - { setShowNumber(data?.length) }}> + {collapse ? ( + data?.length > defaultShowNum ? ( + data?.length > showNumber ? ( + { + setShowNumber(data?.length); + }} + > 更多球局 - : - { setShowNumber(defaultShowNum) }}> + + ) : ( + { + setShowNumber(defaultShowNum); + }} + > 收起 - : - null - : data?.length > 0 && 到底了} + ) + ) : null + ) : ( + data?.length > 0 && 到底了 + )} ); }; diff --git a/src/game_pages/list/index.tsx b/src/game_pages/list/index.tsx index 502ff3a..1acc70e 100644 --- a/src/game_pages/list/index.tsx +++ b/src/game_pages/list/index.tsx @@ -66,7 +66,7 @@ const ListPage = () => { const scrollViewRef = useRef(null); // ScrollView 的 ref const scrollTimeoutRef = useRef(null); const lastScrollTopRef = useRef(0); - const scrollDirectionRef = useRef<'up' | 'down' | null>(null); + const scrollDirectionRef = useRef<"up" | "down" | null>(null); const lastScrollTimeRef = useRef(Date.now()); const loadingMoreRef = useRef(false); // 防止重复加载更多 const scrollStartPositionRef = useRef(0); // 记录开始滚动的位置 @@ -74,45 +74,54 @@ const ListPage = () => { const [scrollTop, setScrollTop] = useState(0); // 控制 ScrollView 滚动位置 // 动态控制 GuideBar 的 z-index - const [guideBarZIndex, setGuideBarZIndex] = useState<'low' | 'high'>('high'); + const [guideBarZIndex, setGuideBarZIndex] = useState<"low" | "high">("high"); const [isPublishMenuVisible, setIsPublishMenuVisible] = useState(false); const [isDistanceFilterVisible, setIsDistanceFilterVisible] = useState(false); const [isCityPickerVisible, setIsCityPickerVisible] = useState(false); - + // 处理 PublishMenu 显示/隐藏 const handlePublishMenuVisibleChange = useCallback((visible: boolean) => { setIsPublishMenuVisible(visible); }, []); - + // 处理 DistanceQuickFilter 显示/隐藏 const handleDistanceFilterVisibleChange = useCallback((visible: boolean) => { setIsDistanceFilterVisible(visible); }, []); - + // 处理 CityPicker 显示/隐藏 const handleCityPickerVisibleChange = useCallback((visible: boolean) => { setIsCityPickerVisible(visible); }, []); - + // 滚动到顶部的方法 const scrollToTop = useCallback(() => { // 使用一个唯一值触发 scrollTop 更新,确保每次都能滚动到顶部 - setScrollTop(prev => prev === 0 ? 0.1 : 0); + setScrollTop((prev) => (prev === 0 ? 0.1 : 0)); }, []); - + // 监听所有弹窗和菜单的状态,动态调整 GuideBar 的 z-index useEffect(() => { if (isPublishMenuVisible) { // PublishMenu 展开时,GuideBar 保持高层级 - setGuideBarZIndex('high'); - } else if (isShowFilterPopup || isDistanceFilterVisible || isCityPickerVisible) { + setGuideBarZIndex("high"); + } else if ( + isShowFilterPopup || + isDistanceFilterVisible || + isCityPickerVisible + ) { // 任何筛选组件或选择器展开时,GuideBar 降低层级 - setGuideBarZIndex('low'); + setGuideBarZIndex("low"); } else { // 都关闭时,GuideBar 保持高层级 - setGuideBarZIndex('high'); + setGuideBarZIndex("high"); } - }, [isShowFilterPopup, isPublishMenuVisible, isDistanceFilterVisible, isCityPickerVisible]); + }, [ + isShowFilterPopup, + isPublishMenuVisible, + isDistanceFilterVisible, + isCityPickerVisible, + ]); // ScrollView 滚动处理函数 const handleScrollViewScroll = useCallback( @@ -133,28 +142,34 @@ const ListPage = () => { if (Math.abs(scrollDiff) > 15) { if (scrollDiff > 0) { // 方向改变时,记录新的起始位置 - if (newDirection !== 'up') { + if (newDirection !== "up") { scrollStartPositionRef.current = lastScrollTop; } - newDirection = 'up'; + newDirection = "up"; } else { // 方向改变时,记录新的起始位置 - if (newDirection !== 'down') { + if (newDirection !== "down") { scrollStartPositionRef.current = lastScrollTop; } - newDirection = 'down'; + newDirection = "down"; } scrollDirectionRef.current = newDirection; } // 计算从开始滚动到现在的累计距离 - const totalScrollDistance = Math.abs(currentScrollTop - scrollStartPositionRef.current); + const totalScrollDistance = Math.abs( + currentScrollTop - scrollStartPositionRef.current + ); // 滚动阈值 const positionThreshold = 120; // 需要滚动到距离顶部120px const distanceThreshold = 80; // 需要连续滚动80px才触发 - if (newDirection === 'up' && currentScrollTop > positionThreshold && totalScrollDistance > distanceThreshold) { + if ( + newDirection === "up" && + currentScrollTop > positionThreshold && + totalScrollDistance > distanceThreshold + ) { // 上滑超过阈值,且连续滚动距离足够,隐藏搜索框 if (showSearchBar || !isShowInputCustomerNavBar) { setShowSearchBar(false); @@ -164,7 +179,10 @@ const ListPage = () => { // 重置起始位置 scrollStartPositionRef.current = currentScrollTop; } - } else if ((newDirection === 'down' && totalScrollDistance > distanceThreshold) || currentScrollTop <= positionThreshold) { + } else if ( + (newDirection === "down" && totalScrollDistance > distanceThreshold) || + currentScrollTop <= positionThreshold + ) { // 下滑且连续滚动距离足够,或者回到顶部附近,显示搜索框 if (!showSearchBar || isShowInputCustomerNavBar) { setShowSearchBar(true); @@ -296,7 +314,7 @@ const ListPage = () => { updateFilterOptions(params); }; - const handleSearchChange = () => { }; + const handleSearchChange = () => {}; // 距离筛选 const handleDistanceOrQuickChange = (name, value) => { @@ -433,7 +451,6 @@ const ListPage = () => { return ( <> - {/* 自定义导航 */} { {/* 固定在顶部的搜索框和筛选 */} {/* 搜索框 - 可隐藏 */} - + 0} @@ -507,7 +528,11 @@ const ListPage = () => { lowerThreshold={100} onScrollToLower={async () => { // 防止重复调用,检查 loading 状态和是否正在加载更多 - if (!loading && !loadingMoreRef.current && listPageState?.isHasMoreData) { + if ( + !loading && + !loadingMoreRef.current && + listPageState?.isHasMoreData + ) { loadingMoreRef.current = true; try { await loadMoreMatches(); @@ -529,14 +554,19 @@ const ListPage = () => { error={error} reload={refreshMatches} loadMoreMatches={loadMoreMatches} + evaluateFlag /> )} - diff --git a/src/main_pages/components/ListPageContent.tsx b/src/main_pages/components/ListPageContent.tsx index d0fc120..dd0a10a 100644 --- a/src/main_pages/components/ListPageContent.tsx +++ b/src/main_pages/components/ListPageContent.tsx @@ -36,9 +36,10 @@ const ListPageContent: React.FC = ({ }) => { const store = useListStore() || {}; const { fetchUserInfo } = useUserActions(); - const { statusNavbarHeightInfo, getCurrentLocationInfo } = useGlobalState() || {}; + const { statusNavbarHeightInfo, getCurrentLocationInfo } = + useGlobalState() || {}; const { totalHeight = 98 } = statusNavbarHeightInfo || {}; - + const { listPageState, loading, @@ -77,7 +78,7 @@ const ListPageContent: React.FC = ({ const scrollViewRef = useRef(null); const scrollTimeoutRef = useRef(null); const lastScrollTopRef = useRef(0); - const scrollDirectionRef = useRef<'up' | 'down' | null>(null); + const scrollDirectionRef = useRef<"up" | "down" | null>(null); const lastScrollTimeRef = useRef(Date.now()); const loadingMoreRef = useRef(false); const scrollStartPositionRef = useRef(0); @@ -86,17 +87,20 @@ const ListPageContent: React.FC = ({ const [refreshing, setRefreshing] = useState(false); // 处理距离筛选显示/隐藏 - const handleDistanceFilterVisibleChange = useCallback((visible: boolean) => { - onDistanceFilterVisibleChange?.(visible); - onNavStateChange?.({ isDistanceFilterVisible: visible }); - }, [onDistanceFilterVisibleChange, onNavStateChange]); + 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); + setScrollTop((prev) => (prev === 0 ? 0.1 : 0)); }, []); // 监听外部滚动触发 @@ -120,24 +124,30 @@ const ListPageContent: React.FC = ({ let newDirection = scrollDirectionRef.current; if (Math.abs(scrollDiff) > 15) { if (scrollDiff > 0) { - if (newDirection !== 'up') { + if (newDirection !== "up") { scrollStartPositionRef.current = lastScrollTop; } - newDirection = 'up'; + newDirection = "up"; } else { - if (newDirection !== 'down') { + if (newDirection !== "down") { scrollStartPositionRef.current = lastScrollTop; } - newDirection = 'down'; + newDirection = "down"; } scrollDirectionRef.current = newDirection; } - const totalScrollDistance = Math.abs(currentScrollTop - scrollStartPositionRef.current); + const totalScrollDistance = Math.abs( + currentScrollTop - scrollStartPositionRef.current + ); const positionThreshold = 120; const distanceThreshold = 80; - if (newDirection === 'up' && currentScrollTop > positionThreshold && totalScrollDistance > distanceThreshold) { + if ( + newDirection === "up" && + currentScrollTop > positionThreshold && + totalScrollDistance > distanceThreshold + ) { if (showSearchBar || !isShowInputCustomerNavBar) { setShowSearchBar(false); updateListPageState({ @@ -146,7 +156,10 @@ const ListPageContent: React.FC = ({ onNavStateChange?.({ isShowInputCustomerNavBar: true }); scrollStartPositionRef.current = currentScrollTop; } - } else if ((newDirection === 'down' && totalScrollDistance > distanceThreshold) || currentScrollTop <= positionThreshold) { + } else if ( + (newDirection === "down" && totalScrollDistance > distanceThreshold) || + currentScrollTop <= positionThreshold + ) { if (!showSearchBar || isShowInputCustomerNavBar) { setShowSearchBar(true); updateListPageState({ @@ -160,7 +173,12 @@ const ListPageContent: React.FC = ({ lastScrollTopRef.current = currentScrollTop; lastScrollTimeRef.current = currentTime; }, - [showSearchBar, isShowInputCustomerNavBar, updateListPageState, onNavStateChange] + [ + showSearchBar, + isShowInputCustomerNavBar, + updateListPageState, + onNavStateChange, + ] ); useEffect(() => { @@ -253,7 +271,7 @@ const ListPageContent: React.FC = ({ updateFilterOptions(params); }; - const handleSearchChange = () => { }; + const handleSearchChange = () => {}; const handleDistanceOrQuickChange = (name, value) => { updateDistanceQuickFilter({ @@ -341,7 +359,11 @@ const ListPageContent: React.FC = ({ )} - + 0} @@ -378,7 +400,11 @@ const ListPageContent: React.FC = ({ onRefresherRefresh={handleRefresh} lowerThreshold={100} onScrollToLower={async () => { - if (!loading && !loadingMoreRef.current && listPageState?.isHasMoreData) { + if ( + !loading && + !loadingMoreRef.current && + listPageState?.isHasMoreData + ) { loadingMoreRef.current = true; try { await loadMoreMatches(); @@ -399,6 +425,7 @@ const ListPageContent: React.FC = ({ error={error} reload={refreshMatches} loadMoreMatches={loadMoreMatches} + evaluateFlag /> @@ -409,4 +436,3 @@ const ListPageContent: React.FC = ({ }; export default ListPageContent; - diff --git a/src/main_pages/components/MyselfPageContent.tsx b/src/main_pages/components/MyselfPageContent.tsx index 0cc724c..3e52662 100644 --- a/src/main_pages/components/MyselfPageContent.tsx +++ b/src/main_pages/components/MyselfPageContent.tsx @@ -16,7 +16,7 @@ const MyselfPageContent: React.FC = () => { const pickerOption = usePickerOption(); const { statusNavbarHeightInfo } = useGlobalState() || {}; const { totalHeight = 98 } = statusNavbarHeightInfo || {}; - + const instance = (Taro as any).getCurrentInstance(); const user_id = instance.router?.params?.userid || ""; const is_current_user = !user_id; @@ -26,7 +26,9 @@ const MyselfPageContent: React.FC = () => { const [ended_game_records, setEndedGameRecords] = useState([]); const [loading] = useState(false); const [is_following, setIsFollowing] = useState(false); - const [active_tab, setActiveTab] = useState<"hosted" | "participated">("hosted"); + const [active_tab, setActiveTab] = useState<"hosted" | "participated">( + "hosted" + ); useEffect(() => { pickerOption.getCities(); @@ -66,7 +68,7 @@ const MyselfPageContent: React.FC = () => { const load_game_data = async () => { try { - if (!user_info || !('id' in user_info)) { + if (!user_info || !("id" in user_info)) { return; } let games_data; @@ -136,7 +138,10 @@ const MyselfPageContent: React.FC = () => { return ( - + { }; export default MyselfPageContent; - diff --git a/src/store/evaluateStore.ts b/src/store/evaluateStore.ts index f7e28cf..b08e264 100644 --- a/src/store/evaluateStore.ts +++ b/src/store/evaluateStore.ts @@ -10,21 +10,25 @@ export enum EvaluateScene { } export interface EvaluateCallback { - type: EvaluateScene | '' + type: EvaluateScene | ""; // flag是用来区分跳转ntrp测试后的操作和直接修改ntrp水平成功后的操作 // score是用在加入球局前判断是否满足球局要求的返回值,限定为必传 // next有两个地方调用:ntrp结果页handleGoon、ntrp弹窗(NTRPEvaluatePopup)直接修改点击保存按钮时 - next: ({ flag, score }: { flag?: boolean, score: string }) => void, - onCancel: () => void, + next: ({ flag, score }: { flag?: boolean; score: string }) => void; + onCancel: () => void; } export interface EvaluateCallbackType extends EvaluateCallback { - setCallback: (options: { type: EvaluateScene | '', next: () => void, onCancel: () => void }) => void, - clear: () => void, + setCallback: (options: { + type: EvaluateScene | ""; + next: ({ flag, score }: { flag?: boolean; score: string }) => void; + onCancel: () => void; + }) => void; + clear: () => void; } export const useEvaluateCallback = create()((set) => ({ - type: '', + type: "", next: () => { }, onCancel: () => { }, setCallback: ({ type, next, onCancel }) => { @@ -32,15 +36,18 @@ export const useEvaluateCallback = create()((set) => ({ type, next, onCancel, - }) + }); + }, + clear: () => { + set({ type: "", next: () => { }, onCancel: () => { } }); }, - clear: () => { set({ type: '', next: () => { }, onCancel: () => { } }) } })); -export const useEvaluate = () => useEvaluateCallback(({ type, next, onCancel, setCallback, clear }) => ({ - type, - next, - onCancel, - setCallback, - clear, -})) +export const useEvaluate = () => + useEvaluateCallback(({ type, next, onCancel, setCallback, clear }) => ({ + type, + next, + onCancel, + setCallback, + clear, + }));