diff --git a/src/components/CustomNavbar/index.module.scss b/src/components/CustomNavbar/index.module.scss index 2e0c6a1..a9816cc 100644 --- a/src/components/CustomNavbar/index.module.scss +++ b/src/components/CustomNavbar/index.module.scss @@ -3,7 +3,8 @@ top: 0; left: 0; overflow: hidden; - z-index: 9991; + z-index: 99; width: 100%; // 背景颜色通过 style 动态设置,默认透明 + box-shadow: none; } diff --git a/src/components/DistanceQuickFilter/index.scss b/src/components/DistanceQuickFilter/index.scss index dba6306..e10c615 100644 --- a/src/components/DistanceQuickFilter/index.scss +++ b/src/components/DistanceQuickFilter/index.scss @@ -1,13 +1,15 @@ .distanceQuickFilterWrap { width: 100%; font-family: "PingFang SC"; + position: relative; .nut-menu-bar { --nutui-menu-bar-line-height: 30px; - background-color: unset; + background-color: #fafafa; box-shadow: unset; - padding: 0 15px; + // padding: 0 15px; gap: 5px; + justify-content: flex-start; } .nut-menu-title { @@ -20,7 +22,7 @@ align-items: center; gap: 2px; border-radius: 999px; - border: 0.5px solid rgba(0, 0, 0, 0.06); + // border: 0.5px solid rgba(0, 0, 0, 0.06); background: #ffffff; font-size: 14px; font-weight: 600; @@ -32,9 +34,15 @@ } .nut-menu-container-wrap { - width: 100vw; + position: fixed !important; + left: 0 !important; + right: 0 !important; + width: auto !important; border-bottom-left-radius: 30px; border-bottom-right-radius: 30px; + background-color: #fafafa !important; + z-index: 1000 !important; + box-sizing: border-box !important; } .positionWrap { diff --git a/src/components/TimeSelector/TimeSelector.tsx b/src/components/TimeSelector/TimeSelector.tsx index 86cc9d9..d1adb58 100644 --- a/src/components/TimeSelector/TimeSelector.tsx +++ b/src/components/TimeSelector/TimeSelector.tsx @@ -5,6 +5,14 @@ import { DialogCalendarCard } from '@/components/index' import './TimeSelector.scss' import dayjs from 'dayjs' +// 安全地解析日期字符串,兼容 iOS +const parseDate = (dateStr: string): Date => { + if (!dateStr) return new Date(); + // 将 "yyyy-MM-dd HH:mm:ss" 转换为 "yyyy-MM-ddTHH:mm:ss" + const isoStr = dateStr.replace(/^(\d{4}-\d{2}-\d{2})\s(\d{2}:\d{2}(?::\d{2})?)$/, '$1T$2'); + return new Date(isoStr); +}; + export interface TimeRange { start_time: string end_time: string @@ -39,7 +47,7 @@ const TimeSelector: React.FC = ({ if (onChange) onChange({start_time, end_time}) } const openPicker = (type: 'start' | 'end') => { - setCurrentTimeValue(type === 'start' ? new Date(value.start_time) : new Date(value.end_time)) + setCurrentTimeValue(type === 'start' ? parseDate(value.start_time) : parseDate(value.end_time)) setCurrentTimeType(type) setVisible(true) } diff --git a/src/container/listContainer/index.tsx b/src/container/listContainer/index.tsx index 28f5d1d..459acd5 100644 --- a/src/container/listContainer/index.tsx +++ b/src/container/listContainer/index.tsx @@ -28,8 +28,11 @@ const ListContainer = (props) => { defaultShowNum, } = props; const timerRef = useRef(null); + const loadingStartTimeRef = useRef(null); + const skeletonTimerRef = useRef(null); - const [showNumber, setShowNumber] = useState(0) + const [showNumber, setShowNumber] = useState(0); + const [showSkeleton, setShowSkeleton] = useState(false); const userInfo = useUserInfo(); @@ -51,11 +54,35 @@ const ListContainer = (props) => { }) }, [data]) + // 控制骨架屏显示逻辑 + useEffect(() => { + if (loading) { + // 开始加载时记录时间 + loadingStartTimeRef.current = Date.now(); + + // 延迟 300ms 后再显示骨架屏 + skeletonTimerRef.current = setTimeout(() => { + setShowSkeleton(true); + }, 300); + } 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); + } }; }, []); @@ -130,8 +157,8 @@ const ListContainer = (props) => { return ( {renderList(data)} - {/* 显示骨架屏 */} - {loading && renderSkeleton()} + {/* 显示骨架屏 - 只有在 loading 超过 300ms 时才显示 */} + {loading && showSkeleton && renderSkeleton()} {/* 搜索结果较少,已为你推荐其他内容 diff --git a/src/container/listCustomNavbar/index.scss b/src/container/listCustomNavbar/index.scss index d95d1a0..67dbd65 100644 --- a/src/container/listCustomNavbar/index.scss +++ b/src/container/listCustomNavbar/index.scss @@ -84,6 +84,7 @@ padding: 7.8px; height: 30px; box-sizing: border-box; + background: #fff; border-radius: 30px; border: 0.488px solid rgba(0, 0, 0, 0.06); box-shadow: 0 3.902px 46.829px 0 rgba(0, 0, 0, 0.08); diff --git a/src/container/listCustomNavbar/index.tsx b/src/container/listCustomNavbar/index.tsx index c964395..ada79a8 100644 --- a/src/container/listCustomNavbar/index.tsx +++ b/src/container/listCustomNavbar/index.tsx @@ -124,7 +124,7 @@ const ListHeader = (props: IProps) => { }; return ( - + {/* 首页logo 导航*/} { isShowNoData, } = listPageState || {}; - // 防抖的滚动处理函数 - const handleScroll = useCallback( - (res) => { - const currentScrollTop = res?.scrollTop || 0; - - // 添加缓冲区,避免临界点频繁切换 - const buffer = 10; // 10px 缓冲区 - const shouldShowInputNav = currentScrollTop >= totalHeight + buffer; - const shouldHideInputNav = currentScrollTop < totalHeight - buffer; - - // 清除之前的定时器 - if (scrollTimeoutRef.current) { - clearTimeout(scrollTimeoutRef.current); - } - - // 防抖处理,避免频繁更新状态 - scrollTimeoutRef.current = setTimeout(() => { - // 只有在状态真正需要改变时才更新 - if (shouldShowInputNav && !isShowInputCustomerNavBar) { - updateListPageState({ - isShowInputCustomerNavBar: true, - }); - } else if (shouldHideInputNav && isShowInputCustomerNavBar) { - updateListPageState({ - isShowInputCustomerNavBar: false, - }); - } - - lastScrollTopRef.current = currentScrollTop; - }, 16); // 约60fps的防抖间隔 - }, - [totalHeight, isShowInputCustomerNavBar, updateState] - ); - - usePageScroll(handleScroll); - + // 滚动相关状态 const scrollContextRef = useRef(null); const scrollTimeoutRef = useRef(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); // 控制搜索框显示/隐藏(筛选始终显示) + + // ScrollView 滚动处理函数 + const handleScrollViewScroll = useCallback( + (e: any) => { + const currentScrollTop = e?.detail?.scrollTop || 0; + const lastScrollTop = lastScrollTopRef.current; + const currentTime = Date.now(); + const timeDiff = currentTime - lastScrollTimeRef.current; + + // 节流:提高到100ms,减少触发频率 + if (timeDiff < 100) return; + + // 计算滚动距离 + const scrollDiff = currentScrollTop - lastScrollTop; + + // 判断滚动方向(提高阈值到15px) + 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; // 需要滚动到距离顶部120px + const distanceThreshold = 80; // 需要连续滚动80px才触发 + + if (newDirection === 'up' && currentScrollTop > positionThreshold && totalScrollDistance > distanceThreshold) { + // 上滑超过阈值,且连续滚动距离足够,隐藏搜索框 + if (showSearchBar || !isShowInputCustomerNavBar) { + setShowSearchBar(false); + updateListPageState({ + isShowInputCustomerNavBar: true, + }); + // 重置起始位置 + scrollStartPositionRef.current = currentScrollTop; + } + } else if ((newDirection === 'down' && totalScrollDistance > distanceThreshold) || currentScrollTop <= positionThreshold) { + // 下滑且连续滚动距离足够,或者回到顶部附近,显示搜索框 + if (!showSearchBar || isShowInputCustomerNavBar) { + setShowSearchBar(true); + updateListPageState({ + isShowInputCustomerNavBar: false, + }); + // 重置起始位置 + scrollStartPositionRef.current = currentScrollTop; + } + } + + lastScrollTopRef.current = currentScrollTop; + lastScrollTimeRef.current = currentTime; + }, + [showSearchBar, isShowInputCustomerNavBar, updateListPageState] + ); useEffect(() => { getLocation(); @@ -112,15 +146,16 @@ const ListPage = () => { getCityQrCode(); }, []); - // 监听数据变化,如果是第一页就滚动到顶部 + // 监听数据变化,如果是第一页就恢复显示搜索框 useEffect(() => { if (pageOption?.page === 1 && matches?.length > 0) { - Taro.pageScrollTo({ - scrollTop: 0, - duration: 300, + // 恢复搜索框显示 + setShowSearchBar(true); + updateListPageState({ + isShowInputCustomerNavBar: false, }); } - }, [matches, pageOption?.page]); + }, [matches, pageOption?.page, updateListPageState]); // 清理定时器 useEffect(() => { @@ -164,40 +199,32 @@ const ListPage = () => { return location; }; - const refreshMatches = () => { - initialFilterSearch(true); + const refreshMatches = async () => { + await initialFilterSearch(true); }; - // const getLoadMoreMatches = () => { - // loadMoreMatches() - // } + // 下拉刷新状态 + const [refreshing, setRefreshing] = useState(false); - // 下拉刷新处理函数 - 使用Taro生命周期钩子 - Taro.usePullDownRefresh(async () => { + // ScrollView 下拉刷新处理函数 + const handleRefresh = async () => { + setRefreshing(true); try { // 调用刷新方法 await refreshMatches(); - - // 刷新完成后停止下拉刷新动画 - Taro.stopPullDownRefresh(); - - // 显示刷新成功提示 - // Taro.showToast({ - // title: "刷新成功", - // icon: "success", - // duration: 1000, - // }); } catch (error) { - // 刷新失败时也停止动画 - Taro.stopPullDownRefresh(); - Taro.showToast({ title: "刷新失败,请重试", icon: "error", duration: 1000, }); + } finally { + // 使用 setTimeout 确保状态更新在下一个事件循环中执行,让 ScrollView 能正确响应 + setTimeout(() => { + setRefreshing(false); + }, 0); } - }); + }; /** * @description 综合筛选确认 @@ -226,7 +253,7 @@ const ListPage = () => { updateFilterOptions(params); }; - const handleSearchChange = () => {}; + const handleSearchChange = () => { }; // 距离筛选 const handleDistanceOrQuickChange = (name, value) => { @@ -363,7 +390,7 @@ const ListPage = () => { return ( <> - + {/* 自定义导航 */} { /> )} - - 0} - filterCount={filterCount} - onChange={handleSearchChange} - value={searchValue} - onInputClick={handleSearchClick} - /> - - {/* 筛选 */} - - + {/* 固定在顶部的搜索框和筛选 */} + + {/* 搜索框 - 可隐藏 */} + + 0} + filterCount={filterCount} + onChange={handleSearchChange} + value={searchValue} + onInputClick={handleSearchClick} + /> + + {/* 筛选 - 始终显示,固定在原位置 */} + + + - {/* 列表内容 */} - + {/* 可滚动的列表内容 */} + { + // 防止重复调用,检查 loading 状态和是否正在加载更多 + if (!loading && !loadingMoreRef.current && listPageState?.isHasMoreData) { + loadingMoreRef.current = true; + try { + await loadMoreMatches(); + } catch (error) { + console.error("加载更多失败:", error); + } finally { + // 接口完成后重置状态,允许下次加载 + loadingMoreRef.current = false; + } + } + }} + onScroll={handleScrollViewScroll} + > + + )} diff --git a/src/game_pages/searchResult/index.scss b/src/game_pages/searchResult/index.scss index b790fee..bad48e2 100644 --- a/src/game_pages/searchResult/index.scss +++ b/src/game_pages/searchResult/index.scss @@ -12,7 +12,7 @@ padding: 5px 15px 10px 15px; position: sticky; top: -1px; - background-color: #fefefe; + background-color: #fafafa; z-index: 123; .nut-menu-bar { diff --git a/src/store/listStore.ts b/src/store/listStore.ts index 847458b..b753d9e 100644 --- a/src/store/listStore.ts +++ b/src/store/listStore.ts @@ -520,13 +520,13 @@ export const useListStore = create()((set, get) => ({ }, // 加载更多数据 - loadMoreMatches: () => { + loadMoreMatches: async () => { const state = get(); const currentPageState = state.isSearchResult ? state.searchPageState : state.listPageState; const { pageOption, isHasMoreData } = currentPageState || {}; if (!isHasMoreData) { - return; + return Promise.resolve(); } const newPageOption = { @@ -539,7 +539,7 @@ export const useListStore = create()((set, get) => ({ }); // 加载更多时追加数据到现有数组 - state.fetchMatches({}, false, true); + return await state.fetchMatches({}, false, true); }, // 初始化搜索条件 重新搜索 diff --git a/src/utils/timeUtils.ts b/src/utils/timeUtils.ts index d422578..51c6215 100644 --- a/src/utils/timeUtils.ts +++ b/src/utils/timeUtils.ts @@ -1,5 +1,21 @@ import dayjs from "dayjs"; +/** + * 安全地将字符串转换为 Date 对象,兼容 iOS + * iOS 不支持 "yyyy-MM-dd HH:mm:ss" 格式,需要转换为 "yyyy-MM-ddTHH:mm:ss" 或 "yyyy/MM/dd HH:mm:ss" + * @param dateStr 日期字符串 + * @returns Date 对象 + */ +const parseDate = (dateStr: string): Date => { + if (!dateStr) return new Date(); + + // 将 "yyyy-MM-dd HH:mm:ss" 格式转换为 "yyyy-MM-ddTHH:mm:ss" (ISO 8601) + // 替换第一个空格为 T + const isoStr = dateStr.replace(/^(\d{4}-\d{2}-\d{2})\s(\d{2}:\d{2}(?::\d{2})?)$/, '$1T$2'); + + return new Date(isoStr); +}; + /** * 获取下一个整点时间 * @returns 格式为 YYYY-MM-DD HH:mm 的字符串 @@ -88,7 +104,7 @@ export const getTime = (time: string): string => { export const formatRelativeTime = (timeStr: string): string => { if (!timeStr) return ""; - const date = new Date(timeStr); + const date = parseDate(timeStr); const now = new Date(); // 获取日期部分(年-月-日),忽略时间 @@ -150,7 +166,7 @@ export const formatRelativeTime = (timeStr: string): string => { export const formatShortRelativeTime = (timeStr: string): string => { if (!timeStr) return ""; - const date = new Date(timeStr); + const date = parseDate(timeStr); const now = new Date(); // 获取日期部分(年-月-日),忽略时间 @@ -204,7 +220,7 @@ export const formatShortRelativeTime = (timeStr: string): string => { export const formatGameTime = (timeStr: string): string => { if (!timeStr) return ""; - const date = new Date(timeStr); + const date = parseDate(timeStr); const now = new Date(); // 获取星期几 @@ -287,8 +303,8 @@ export const calculateDuration = ( ): string => { if (!startTime || !endTime) return ""; - const start = new Date(startTime); - const end = new Date(endTime); + const start = parseDate(startTime); + const end = parseDate(endTime); // 计算时间差(毫秒) const diffMs = end.getTime() - start.getTime(); diff --git a/types/list/types.ts b/types/list/types.ts index 2559366..47b9ac9 100644 --- a/types/list/types.ts +++ b/types/list/types.ts @@ -110,7 +110,7 @@ export interface ListActions { clearHistory: () => void; searchSuggestion: (val: string) => Promise; getSearchParams: () => Record; - loadMoreMatches: () => void; + loadMoreMatches: () => Promise; initialFilterSearch: (isSearchData?: boolean) => void; setListData: (payload: IPayload) => void; fetchGetGamesCount: () => Promise;