diff --git a/src/components/DistanceQuickFilter/index.scss b/src/components/DistanceQuickFilter/index.scss index dba6306..6419320 100644 --- a/src/components/DistanceQuickFilter/index.scss +++ b/src/components/DistanceQuickFilter/index.scss @@ -6,7 +6,7 @@ --nutui-menu-bar-line-height: 30px; background-color: unset; box-shadow: unset; - padding: 0 15px; + // padding: 0 15px; gap: 5px; } 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/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/game_pages/list/index.config.ts b/src/game_pages/list/index.config.ts index a197496..36ec1ae 100644 --- a/src/game_pages/list/index.config.ts +++ b/src/game_pages/list/index.config.ts @@ -1,6 +1,6 @@ export default definePageConfig({ navigationBarTitleText: '', - enablePullDownRefresh: true, + enablePullDownRefresh: false, // 禁用页面级下拉刷新,使用 ScrollView 的下拉刷新 backgroundTextStyle: 'dark', navigationStyle: 'custom', backgroundColor: '#FAFAFA', diff --git a/src/game_pages/list/index.module.scss b/src/game_pages/list/index.module.scss index 644d806..839ff18 100644 --- a/src/game_pages/list/index.module.scss +++ b/src/game_pages/list/index.module.scss @@ -81,22 +81,54 @@ background-color: #FAFAFA; font-family: "PingFang SC"; transition: padding-top 0.3s ease-in-out; // 添加过渡动画,让布局变化更平滑 + display: flex; + flex-direction: column; + height: calc(100vh - var(--status-bar-height, 0px) - var(--nav-bar-height, 0px) - 112px); // 减去底部导航栏高度 112px + overflow: hidden; - .listTopSearchWrapper { - padding: 0 15px; + .fixedHeader { + position: sticky; + top: 0; + z-index: 100; background-color: #fff; + display: flex; + flex-direction: column; } + .listTopSearchWrapper { + background-color: #fff; + // 使用 margin-top 负值来控制可见性,保持元素高度不变,筛选项位置固定 + transition: margin-top 0.25s cubic-bezier(0.4, 0, 0.2, 1), + opacity 0.2s ease-out; + padding: 10px 15px 10px 15px; // 统一边距:上下10px 左右15px + box-sizing: border-box; + overflow: hidden; + will-change: margin-top, opacity; + + &.show { + opacity: 1; + margin-top: 0; // 正常显示 + } + + &.hide { + opacity: 0; + margin-top: -64px; // 使用负 margin 向上隐藏,但仍占据布局空间 (44px内容 + 20px padding = 64px) + pointer-events: none; // 隐藏时禁用交互 + } + } .listTopFilterWrapper { display: flex; + box-sizing: border-box; align-items: center; - padding-top: 10px; - padding-bottom: 10px; + padding: 0 15px 10px 15px; // 上0 左右15px 下10px(与搜索框左右对齐,下边距一致) gap: 5px; - position: sticky; background-color: #fff; - z-index: 100; + } + + .listScrollView { + flex: 1; + height: 0; // 让 flex 生效 } .menuFilter { diff --git a/src/game_pages/list/index.tsx b/src/game_pages/list/index.tsx index d6f15a2..b3789af 100644 --- a/src/game_pages/list/index.tsx +++ b/src/game_pages/list/index.tsx @@ -2,14 +2,10 @@ import SearchBar from "@/components/SearchBar"; import FilterPopup from "@/components/FilterPopup"; import styles from "./index.module.scss"; import { useEffect, useRef, useCallback, useState } from "react"; -import Taro, { - usePageScroll, - // useShareAppMessage, - // useShareTimeline, -} from "@tarojs/taro"; +import Taro from "@tarojs/taro"; import { useListStore } from "@/store/listStore"; import { useGlobalState } from "@/store/global"; -import { View, Image, Text } from "@tarojs/components"; +import { View, Image, Text, ScrollView } from "@tarojs/components"; import CustomerNavBar from "@/container/listCustomNavbar"; import GuideBar from "@/components/GuideBar"; import ListContainer from "@/container/listContainer"; @@ -65,46 +61,75 @@ const ListPage = () => { isShowNoData, } = listPageState || {}; - // 防抖的滚动处理函数 - const handleScroll = useCallback( - (res) => { - const currentScrollTop = res?.scrollTop || 0; + // 滚动相关状态 + 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 [showSearchBar, setShowSearchBar] = useState(true); // 控制搜索框显示/隐藏(筛选始终显示) - // 添加缓冲区,避免临界点频繁切换 - const buffer = 10; // 10px 缓冲区 - const shouldShowInputNav = currentScrollTop >= totalHeight + buffer; - const shouldHideInputNav = currentScrollTop < totalHeight - buffer; + // 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; + + // 节流:如果时间差太小,跳过本次处理 + if (timeDiff < 16) return; // 约60fps,避免过于频繁的计算 + + // 计算滚动距离 + const scrollDiff = currentScrollTop - lastScrollTop; + + // 判断滚动方向(提高阈值避免微小滚动误判) + let newDirection = scrollDirectionRef.current; + if (scrollDiff > 5) { + newDirection = 'up'; + } else if (scrollDiff < -5) { + newDirection = 'down'; + } + + scrollDirectionRef.current = newDirection; // 清除之前的定时器 if (scrollTimeoutRef.current) { clearTimeout(scrollTimeoutRef.current); } - // 防抖处理,避免频繁更新状态 - scrollTimeoutRef.current = setTimeout(() => { - // 只有在状态真正需要改变时才更新 - if (shouldShowInputNav && !isShowInputCustomerNavBar) { + // 滚动阈值(提高阈值减少误触发) + const threshold = 80; + + if (newDirection === 'up' && currentScrollTop > threshold) { + // 上滑超过阈值,隐藏搜索框 + if (showSearchBar) { + setShowSearchBar(false); + } + if (!isShowInputCustomerNavBar) { updateListPageState({ isShowInputCustomerNavBar: true, }); - } else if (shouldHideInputNav && isShowInputCustomerNavBar) { + } + } else if (newDirection === 'down' || currentScrollTop <= threshold) { + // 下滑或回到顶部,显示搜索框 + if (!showSearchBar) { + setShowSearchBar(true); + } + if (isShowInputCustomerNavBar) { updateListPageState({ isShowInputCustomerNavBar: false, }); } + } - lastScrollTopRef.current = currentScrollTop; - }, 16); // 约60fps的防抖间隔 + lastScrollTopRef.current = currentScrollTop; + lastScrollTimeRef.current = currentTime; }, - [totalHeight, isShowInputCustomerNavBar, updateState] + [showSearchBar, isShowInputCustomerNavBar, updateListPageState] ); - usePageScroll(handleScroll); - - const scrollContextRef = useRef(null); - const scrollTimeoutRef = useRef(null); - const lastScrollTopRef = useRef(0); - useEffect(() => { getLocation(); fetchUserInfo(); @@ -112,15 +137,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 +190,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 +244,7 @@ const ListPage = () => { updateFilterOptions(params); }; - const handleSearchChange = () => {}; + const handleSearchChange = () => { }; // 距离筛选 const handleDistanceOrQuickChange = (name, value) => { @@ -363,7 +381,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/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;