Merge branch 'master' of http://git.bimwe.com/tennis/mini-programs
This commit is contained in:
@@ -3,7 +3,8 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
z-index: 9991;
|
z-index: 99;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
// 背景颜色通过 style 动态设置,默认透明
|
// 背景颜色通过 style 动态设置,默认透明
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
.distanceQuickFilterWrap {
|
.distanceQuickFilterWrap {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-family: "PingFang SC";
|
font-family: "PingFang SC";
|
||||||
|
position: relative;
|
||||||
|
|
||||||
.nut-menu-bar {
|
.nut-menu-bar {
|
||||||
--nutui-menu-bar-line-height: 30px;
|
--nutui-menu-bar-line-height: 30px;
|
||||||
background-color: unset;
|
background-color: #fafafa;
|
||||||
box-shadow: unset;
|
box-shadow: unset;
|
||||||
padding: 0 15px;
|
// padding: 0 15px;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nut-menu-title {
|
.nut-menu-title {
|
||||||
@@ -20,7 +22,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
border-radius: 999px;
|
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;
|
background: #ffffff;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -32,9 +34,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nut-menu-container-wrap {
|
.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-left-radius: 30px;
|
||||||
border-bottom-right-radius: 30px;
|
border-bottom-right-radius: 30px;
|
||||||
|
background-color: #fafafa !important;
|
||||||
|
z-index: 1000 !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.positionWrap {
|
.positionWrap {
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ import { DialogCalendarCard } from '@/components/index'
|
|||||||
import './TimeSelector.scss'
|
import './TimeSelector.scss'
|
||||||
import dayjs from 'dayjs'
|
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 {
|
export interface TimeRange {
|
||||||
start_time: string
|
start_time: string
|
||||||
end_time: string
|
end_time: string
|
||||||
@@ -39,7 +47,7 @@ const TimeSelector: React.FC<TimeSelectorProps> = ({
|
|||||||
if (onChange) onChange({start_time, end_time})
|
if (onChange) onChange({start_time, end_time})
|
||||||
}
|
}
|
||||||
const openPicker = (type: 'start' | 'end') => {
|
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)
|
setCurrentTimeType(type)
|
||||||
setVisible(true)
|
setVisible(true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,11 @@ const ListContainer = (props) => {
|
|||||||
defaultShowNum,
|
defaultShowNum,
|
||||||
} = props;
|
} = props;
|
||||||
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
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 [showNumber, setShowNumber] = useState(0);
|
||||||
|
const [showSkeleton, setShowSkeleton] = useState(false);
|
||||||
|
|
||||||
const userInfo = useUserInfo();
|
const userInfo = useUserInfo();
|
||||||
|
|
||||||
@@ -51,11 +54,35 @@ const ListContainer = (props) => {
|
|||||||
})
|
})
|
||||||
}, [data])
|
}, [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(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (timerRef.current) {
|
if (timerRef.current) {
|
||||||
clearTimeout(timerRef.current);
|
clearTimeout(timerRef.current);
|
||||||
}
|
}
|
||||||
|
if (skeletonTimerRef.current) {
|
||||||
|
clearTimeout(skeletonTimerRef.current);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -130,8 +157,8 @@ const ListContainer = (props) => {
|
|||||||
return (
|
return (
|
||||||
<View className="listContentWrapper" style={style}>
|
<View className="listContentWrapper" style={style}>
|
||||||
{renderList(data)}
|
{renderList(data)}
|
||||||
{/* 显示骨架屏 */}
|
{/* 显示骨架屏 - 只有在 loading 超过 300ms 时才显示 */}
|
||||||
{loading && renderSkeleton()}
|
{loading && showSkeleton && renderSkeleton()}
|
||||||
{/* <View className="recommendTextWrapper">
|
{/* <View className="recommendTextWrapper">
|
||||||
<Text className="recommendText">搜索结果较少,已为你推荐其他内容</Text>
|
<Text className="recommendText">搜索结果较少,已为你推荐其他内容</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -84,6 +84,7 @@
|
|||||||
padding: 7.8px;
|
padding: 7.8px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
background: #fff;
|
||||||
border-radius: 30px;
|
border-radius: 30px;
|
||||||
border: 0.488px solid rgba(0, 0, 0, 0.06);
|
border: 0.488px solid rgba(0, 0, 0, 0.06);
|
||||||
box-shadow: 0 3.902px 46.829px 0 rgba(0, 0, 0, 0.08);
|
box-shadow: 0 3.902px 46.829px 0 rgba(0, 0, 0, 0.08);
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ const ListHeader = (props: IProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomNavbar>
|
<CustomNavbar backgroundColor="#fafafa">
|
||||||
<View className="listNavWrapper">
|
<View className="listNavWrapper">
|
||||||
{/* 首页logo 导航*/}
|
{/* 首页logo 导航*/}
|
||||||
<View
|
<View
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationBarTitleText: '',
|
navigationBarTitleText: '',
|
||||||
enablePullDownRefresh: true,
|
enablePullDownRefresh: false, // 禁用页面级下拉刷新,使用 ScrollView 的下拉刷新
|
||||||
backgroundTextStyle: 'dark',
|
backgroundTextStyle: 'dark',
|
||||||
navigationStyle: 'custom',
|
navigationStyle: 'custom',
|
||||||
backgroundColor: '#FAFAFA',
|
backgroundColor: '#FAFAFA',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
:global {
|
:global {
|
||||||
.guide-bar {
|
.guide-bar {
|
||||||
z-index: 9990;
|
z-index: 999;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,22 +81,54 @@
|
|||||||
background-color: #FAFAFA;
|
background-color: #FAFAFA;
|
||||||
font-family: "PingFang SC";
|
font-family: "PingFang SC";
|
||||||
transition: padding-top 0.3s ease-in-out; // 添加过渡动画,让布局变化更平滑
|
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 {
|
.fixedHeader {
|
||||||
padding: 0 15px;
|
position: sticky;
|
||||||
background-color: #fff;
|
top: 0;
|
||||||
|
z-index: 90;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.listTopSearchWrapper {
|
||||||
|
background-color: #fafafa;
|
||||||
|
// 使用 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 {
|
.listTopFilterWrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
box-sizing: border-box;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-top: 10px;
|
padding: 0 15px 10px 15px;
|
||||||
padding-bottom: 10px;
|
// 上0 左右15px 下10px(与搜索框左右对齐,下边距一致)
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
position: sticky;
|
background-color: #fafafa;
|
||||||
background-color: #fff;
|
}
|
||||||
z-index: 100;
|
|
||||||
|
.listScrollView {
|
||||||
|
flex: 1;
|
||||||
|
height: 0; // 让 flex 生效
|
||||||
}
|
}
|
||||||
|
|
||||||
.menuFilter {
|
.menuFilter {
|
||||||
@@ -156,5 +188,5 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.guideBarList {
|
.guideBarList {
|
||||||
z-index: 99;
|
z-index: 999;
|
||||||
}
|
}
|
||||||
@@ -2,14 +2,10 @@ import SearchBar from "@/components/SearchBar";
|
|||||||
import FilterPopup from "@/components/FilterPopup";
|
import FilterPopup from "@/components/FilterPopup";
|
||||||
import styles from "./index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
import { useEffect, useRef, useCallback, useState } from "react";
|
import { useEffect, useRef, useCallback, useState } from "react";
|
||||||
import Taro, {
|
import Taro from "@tarojs/taro";
|
||||||
usePageScroll,
|
|
||||||
// useShareAppMessage,
|
|
||||||
// useShareTimeline,
|
|
||||||
} from "@tarojs/taro";
|
|
||||||
import { useListStore } from "@/store/listStore";
|
import { useListStore } from "@/store/listStore";
|
||||||
import { useGlobalState } from "@/store/global";
|
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 CustomerNavBar from "@/container/listCustomNavbar";
|
||||||
import GuideBar from "@/components/GuideBar";
|
import GuideBar from "@/components/GuideBar";
|
||||||
import ListContainer from "@/container/listContainer";
|
import ListContainer from "@/container/listContainer";
|
||||||
@@ -65,45 +61,83 @@ const ListPage = () => {
|
|||||||
isShowNoData,
|
isShowNoData,
|
||||||
} = listPageState || {};
|
} = 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 scrollContextRef = useRef(null);
|
||||||
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const lastScrollTopRef = useRef(0);
|
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(() => {
|
useEffect(() => {
|
||||||
getLocation();
|
getLocation();
|
||||||
@@ -112,15 +146,16 @@ const ListPage = () => {
|
|||||||
getCityQrCode();
|
getCityQrCode();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 监听数据变化,如果是第一页就滚动到顶部
|
// 监听数据变化,如果是第一页就恢复显示搜索框
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pageOption?.page === 1 && matches?.length > 0) {
|
if (pageOption?.page === 1 && matches?.length > 0) {
|
||||||
Taro.pageScrollTo({
|
// 恢复搜索框显示
|
||||||
scrollTop: 0,
|
setShowSearchBar(true);
|
||||||
duration: 300,
|
updateListPageState({
|
||||||
|
isShowInputCustomerNavBar: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [matches, pageOption?.page]);
|
}, [matches, pageOption?.page, updateListPageState]);
|
||||||
|
|
||||||
// 清理定时器
|
// 清理定时器
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -164,40 +199,32 @@ const ListPage = () => {
|
|||||||
return location;
|
return location;
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshMatches = () => {
|
const refreshMatches = async () => {
|
||||||
initialFilterSearch(true);
|
await initialFilterSearch(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// const getLoadMoreMatches = () => {
|
// 下拉刷新状态
|
||||||
// loadMoreMatches()
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
// }
|
|
||||||
|
|
||||||
// 下拉刷新处理函数 - 使用Taro生命周期钩子
|
// ScrollView 下拉刷新处理函数
|
||||||
Taro.usePullDownRefresh(async () => {
|
const handleRefresh = async () => {
|
||||||
|
setRefreshing(true);
|
||||||
try {
|
try {
|
||||||
// 调用刷新方法
|
// 调用刷新方法
|
||||||
await refreshMatches();
|
await refreshMatches();
|
||||||
|
|
||||||
// 刷新完成后停止下拉刷新动画
|
|
||||||
Taro.stopPullDownRefresh();
|
|
||||||
|
|
||||||
// 显示刷新成功提示
|
|
||||||
// Taro.showToast({
|
|
||||||
// title: "刷新成功",
|
|
||||||
// icon: "success",
|
|
||||||
// duration: 1000,
|
|
||||||
// });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 刷新失败时也停止动画
|
|
||||||
Taro.stopPullDownRefresh();
|
|
||||||
|
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: "刷新失败,请重试",
|
title: "刷新失败,请重试",
|
||||||
icon: "error",
|
icon: "error",
|
||||||
duration: 1000,
|
duration: 1000,
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
// 使用 setTimeout 确保状态更新在下一个事件循环中执行,让 ScrollView 能正确响应
|
||||||
|
setTimeout(() => {
|
||||||
|
setRefreshing(false);
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 综合筛选确认
|
* @description 综合筛选确认
|
||||||
@@ -226,7 +253,7 @@ const ListPage = () => {
|
|||||||
updateFilterOptions(params);
|
updateFilterOptions(params);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearchChange = () => {};
|
const handleSearchChange = () => { };
|
||||||
|
|
||||||
// 距离筛选
|
// 距离筛选
|
||||||
const handleDistanceOrQuickChange = (name, value) => {
|
const handleDistanceOrQuickChange = (name, value) => {
|
||||||
@@ -363,7 +390,7 @@ const ListPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
||||||
{/* 自定义导航 */}
|
{/* 自定义导航 */}
|
||||||
<CustomerNavBar
|
<CustomerNavBar
|
||||||
config={{
|
config={{
|
||||||
@@ -392,44 +419,70 @@ const ListPage = () => {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
<View className={`${styles.listTopSearchWrapper}`}>
|
{/* 固定在顶部的搜索框和筛选 */}
|
||||||
<SearchBar
|
<View className={styles.fixedHeader}>
|
||||||
handleFilterIcon={toggleShowPopup}
|
{/* 搜索框 - 可隐藏 */}
|
||||||
isSelect={filterCount > 0}
|
<View className={`${styles.listTopSearchWrapper} ${showSearchBar ? styles.show : styles.hide}`}>
|
||||||
filterCount={filterCount}
|
<SearchBar
|
||||||
onChange={handleSearchChange}
|
handleFilterIcon={toggleShowPopup}
|
||||||
value={searchValue}
|
isSelect={filterCount > 0}
|
||||||
onInputClick={handleSearchClick}
|
filterCount={filterCount}
|
||||||
/>
|
onChange={handleSearchChange}
|
||||||
</View>
|
value={searchValue}
|
||||||
{/* 筛选 */}
|
onInputClick={handleSearchClick}
|
||||||
<View
|
/>
|
||||||
className={styles.listTopFilterWrapper}
|
</View>
|
||||||
style={{
|
{/* 筛选 - 始终显示,固定在原位置 */}
|
||||||
top: totalHeight - 1,
|
<View className={styles.listTopFilterWrapper}>
|
||||||
}}
|
<DistanceQuickFilter
|
||||||
>
|
cityOptions={distanceData}
|
||||||
<DistanceQuickFilter
|
quickOptions={quickFilterData}
|
||||||
cityOptions={distanceData}
|
onChange={handleDistanceOrQuickChange}
|
||||||
quickOptions={quickFilterData}
|
cityName="distanceFilter"
|
||||||
onChange={handleDistanceOrQuickChange}
|
quickName="order"
|
||||||
cityName="distanceFilter"
|
cityValue={distanceQuickFilter?.distanceFilter}
|
||||||
quickName="order"
|
quickValue={distanceQuickFilter?.order}
|
||||||
cityValue={distanceQuickFilter?.distanceFilter}
|
/>
|
||||||
quickValue={distanceQuickFilter?.order}
|
</View>
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 列表内容 */}
|
{/* 可滚动的列表内容 */}
|
||||||
<ListContainer
|
<ScrollView
|
||||||
data={matches}
|
scrollY
|
||||||
recommendList={recommendList}
|
className={styles.listScrollView}
|
||||||
loading={loading}
|
scrollWithAnimation
|
||||||
isShowNoData={isShowNoData}
|
enhanced
|
||||||
error={error}
|
showScrollbar={false}
|
||||||
reload={refreshMatches}
|
refresherEnabled={true}
|
||||||
loadMoreMatches={loadMoreMatches}
|
refresherTriggered={refreshing}
|
||||||
/>
|
onRefresherRefresh={handleRefresh}
|
||||||
|
lowerThreshold={100}
|
||||||
|
onScrollToLower={async () => {
|
||||||
|
// 防止重复调用,检查 loading 状态和是否正在加载更多
|
||||||
|
if (!loading && !loadingMoreRef.current && listPageState?.isHasMoreData) {
|
||||||
|
loadingMoreRef.current = true;
|
||||||
|
try {
|
||||||
|
await loadMoreMatches();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("加载更多失败:", error);
|
||||||
|
} finally {
|
||||||
|
// 接口完成后重置状态,允许下次加载
|
||||||
|
loadingMoreRef.current = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onScroll={handleScrollViewScroll}
|
||||||
|
>
|
||||||
|
<ListContainer
|
||||||
|
data={matches}
|
||||||
|
recommendList={recommendList}
|
||||||
|
loading={loading}
|
||||||
|
isShowNoData={isShowNoData}
|
||||||
|
error={error}
|
||||||
|
reload={refreshMatches}
|
||||||
|
loadMoreMatches={loadMoreMatches}
|
||||||
|
/>
|
||||||
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
padding: 5px 15px 10px 15px;
|
padding: 5px 15px 10px 15px;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: -1px;
|
top: -1px;
|
||||||
background-color: #fefefe;
|
background-color: #fafafa;
|
||||||
z-index: 123;
|
z-index: 123;
|
||||||
|
|
||||||
.nut-menu-bar {
|
.nut-menu-bar {
|
||||||
|
|||||||
@@ -520,13 +520,13 @@ export const useListStore = create<TennisStore>()((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 加载更多数据
|
// 加载更多数据
|
||||||
loadMoreMatches: () => {
|
loadMoreMatches: async () => {
|
||||||
const state = get();
|
const state = get();
|
||||||
const currentPageState = state.isSearchResult ? state.searchPageState : state.listPageState;
|
const currentPageState = state.isSearchResult ? state.searchPageState : state.listPageState;
|
||||||
const { pageOption, isHasMoreData } = currentPageState || {};
|
const { pageOption, isHasMoreData } = currentPageState || {};
|
||||||
|
|
||||||
if (!isHasMoreData) {
|
if (!isHasMoreData) {
|
||||||
return;
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPageOption = {
|
const newPageOption = {
|
||||||
@@ -539,7 +539,7 @@ export const useListStore = create<TennisStore>()((set, get) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 加载更多时追加数据到现有数组
|
// 加载更多时追加数据到现有数组
|
||||||
state.fetchMatches({}, false, true);
|
return await state.fetchMatches({}, false, true);
|
||||||
},
|
},
|
||||||
|
|
||||||
// 初始化搜索条件 重新搜索
|
// 初始化搜索条件 重新搜索
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
import dayjs from "dayjs";
|
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 的字符串
|
* @returns 格式为 YYYY-MM-DD HH:mm 的字符串
|
||||||
@@ -88,7 +104,7 @@ export const getTime = (time: string): string => {
|
|||||||
export const formatRelativeTime = (timeStr: string): string => {
|
export const formatRelativeTime = (timeStr: string): string => {
|
||||||
if (!timeStr) return "";
|
if (!timeStr) return "";
|
||||||
|
|
||||||
const date = new Date(timeStr);
|
const date = parseDate(timeStr);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
// 获取日期部分(年-月-日),忽略时间
|
// 获取日期部分(年-月-日),忽略时间
|
||||||
@@ -150,7 +166,7 @@ export const formatRelativeTime = (timeStr: string): string => {
|
|||||||
export const formatShortRelativeTime = (timeStr: string): string => {
|
export const formatShortRelativeTime = (timeStr: string): string => {
|
||||||
if (!timeStr) return "";
|
if (!timeStr) return "";
|
||||||
|
|
||||||
const date = new Date(timeStr);
|
const date = parseDate(timeStr);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
// 获取日期部分(年-月-日),忽略时间
|
// 获取日期部分(年-月-日),忽略时间
|
||||||
@@ -204,7 +220,7 @@ export const formatShortRelativeTime = (timeStr: string): string => {
|
|||||||
export const formatGameTime = (timeStr: string): string => {
|
export const formatGameTime = (timeStr: string): string => {
|
||||||
if (!timeStr) return "";
|
if (!timeStr) return "";
|
||||||
|
|
||||||
const date = new Date(timeStr);
|
const date = parseDate(timeStr);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
// 获取星期几
|
// 获取星期几
|
||||||
@@ -287,8 +303,8 @@ export const calculateDuration = (
|
|||||||
): string => {
|
): string => {
|
||||||
if (!startTime || !endTime) return "";
|
if (!startTime || !endTime) return "";
|
||||||
|
|
||||||
const start = new Date(startTime);
|
const start = parseDate(startTime);
|
||||||
const end = new Date(endTime);
|
const end = parseDate(endTime);
|
||||||
|
|
||||||
// 计算时间差(毫秒)
|
// 计算时间差(毫秒)
|
||||||
const diffMs = end.getTime() - start.getTime();
|
const diffMs = end.getTime() - start.getTime();
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export interface ListActions {
|
|||||||
clearHistory: () => void;
|
clearHistory: () => void;
|
||||||
searchSuggestion: (val: string) => Promise<void>;
|
searchSuggestion: (val: string) => Promise<void>;
|
||||||
getSearchParams: () => Record<string, any>;
|
getSearchParams: () => Record<string, any>;
|
||||||
loadMoreMatches: () => void;
|
loadMoreMatches: () => Promise<void>;
|
||||||
initialFilterSearch: (isSearchData?: boolean) => void;
|
initialFilterSearch: (isSearchData?: boolean) => void;
|
||||||
setListData: (payload: IPayload) => void;
|
setListData: (payload: IPayload) => void;
|
||||||
fetchGetGamesCount: () => Promise<void>;
|
fetchGetGamesCount: () => Promise<void>;
|
||||||
|
|||||||
Reference in New Issue
Block a user