列表联调

This commit is contained in:
李瑞
2025-09-07 18:54:36 +08:00
parent 2d0d728969
commit 6feb7057af
28 changed files with 1225 additions and 740 deletions

View File

@@ -6,9 +6,6 @@ import { useDictionaryStore } from './store/dictionaryStore'
import { useGlobalStore } from './store/global'
import { check_login_status } from './services/loginService';
// import { getNavbarHeight } from "@/utils/getNavbarHeight";
interface AppProps {
children: ReactNode
}
@@ -31,6 +28,7 @@ class App extends Component<AppProps> {
// 初始化字典数据
this.initDictionaryData()
this.getNavBarHeight()
this.getLocation()
}
componentDidShow() { }
@@ -51,7 +49,12 @@ class App extends Component<AppProps> {
getNavBarHeight = () => {
const { getNavbarHeightInfo } = useGlobalStore.getState()
getNavbarHeightInfo()
}
// 获取位置信息
getLocation = () => {
const { getCurrentLocationInfo } = useGlobalStore.getState()
getCurrentLocationInfo()
}
render() {

View File

@@ -30,6 +30,7 @@
}
.arrow {
width: 10px;
height: 20px;
position: relative;
}
.arrow.left {

View File

@@ -35,18 +35,18 @@
border-bottom-right-radius: 30px;
}
.nut-menu-container-item {
color: rgba(60, 60, 67, 0.60);
font-size: 14px;
font-weight: 600;
line-height: 20px;
}
// .nut-menu-container-item {
// color: rgba(60, 60, 67, 0.6);
// font-size: 14px;
// font-weight: 600;
// line-height: 20px;
// }
.nut-menu-container-item.active {
flex-direction: row-reverse;
justify-content: space-between;
color: #000;
}
// .nut-menu-container-item.active {
// flex-direction: row-reverse;
// justify-content: space-between;
// color: #000;
// }
.positionWrap {
display: flex;
@@ -88,4 +88,40 @@
width: 20px;
height: 20px;
}
.quickOptionsWrapper {
width: 100%;
display: flex;
flex-direction: column;
.quickItem {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 0;
color: rgba(60, 60, 67, 0.6);
font-size: 14px;
font-weight: 600;
line-height: 20px;
&.active {
color: #000;
}
}
}
}
.distanceQuickFilterWrap_0 .nut-menu-title-0 {
background-color: #000;
color: #fff;
&.active {
color: #ffffff;
}
}
.distanceQuickFilterWrap_1 .nut-menu-title-1 {
background-color: #000;
color: #fff;
&.active {
color: #ffffff;
}
}

View File

@@ -1,6 +1,6 @@
import React, { useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { Menu, Button } from "@nutui/nutui-react-taro";
import { Image } from "@tarojs/components";
import { Image, View, Text } from "@tarojs/components";
import img from "@/config/images";
import Bubble from "../Bubble";
import "./index.scss";
@@ -15,23 +15,46 @@ const Demo3 = (props) => {
cityValue,
quickValue,
} = props;
const cityRef = useRef(null);
const quickRef = useRef(null);
const [changePosition, setChangePosition] = useState<number[]>([]);
const itemRef = useRef(null);
// 全城筛选显示的标题
const cityTitle = cityOptions.find((item) => item.value === cityValue)?.label;
const handleChange = (name: string, value: string) => {
// 快捷筛选显示的标题
const quickTitle = quickOptions.find(
(item) => item.value === quickValue
)?.label;
// className
const filterWrapperClassName = changePosition.reduce((pre, cur) => {
return `${pre} distanceQuickFilterWrap_${cur}`;
}, "");
const handleChange = (
name: string,
value: string | number,
index: number
) => {
setChangePosition((preState) => {
const newData = new Set([...preState, index]);
return Array.from(newData);
});
onChange && onChange(name, value);
(itemRef.current as any)?.toggle(false);
};
// const cityTitle = cityOptions.find((item) => item.value === cityValue)?.label;
// 控制隐藏
index === 0 && (cityRef.current as any)?.toggle(false);
index === 1 && (quickRef.current as any)?.toggle(false);
};
return (
<Menu
className="distanceQuickFilterWrap"
className={`distanceQuickFilterWrap ${filterWrapperClassName}`}
>
<Menu.Item
title={cityValue}
ref={itemRef}
title={cityTitle}
ref={cityRef}
icon={<Image src={img.ICON_MENU_ITEM_SELECTED} />}
>
<div className="positionWrap">
@@ -42,7 +65,7 @@ const Demo3 = (props) => {
<Bubble
options={cityOptions}
value={cityValue}
onChange={handleChange}
onChange={(name, value) => handleChange(name, value, 0)}
layout="grid"
size="small"
columns={4}
@@ -52,30 +75,35 @@ const Demo3 = (props) => {
</div>
</Menu.Item>
<Menu.Item
options={quickOptions}
title={quickTitle}
ref={quickRef}
defaultValue={quickValue}
onChange={(value) => handleChange(quickName, value)}
icon={<Image className="itemIcon" src={img.ICON_MENU_ITEM_SELECTED} />}
// options={quickOptions}
// onChange={(value) => handleChange(quickName, value, 1)}
// icon={<Image className="itemIcon" src={img.ICON_MENU_ITEM_SELECTED} />}
>
<View className="quickOptionsWrapper">
{quickOptions.map((item) => {
const active = quickValue === item?.value;
return (
<View
className={`quickItem ${active && "active"}`}
onClick={() => handleChange(quickName, item.value, 1)}
>
<View>{item?.label}</View>
{active && (
<View>
<Image
className="itemIcon"
src={img.ICON_MENU_ITEM_SELECTED}
/>
{/* <Menu.Item title="筛选" ref={itemRef}>
<div
style={{
width: '50%',
lineHeight: '28px',
padding: '0 30px',
}}
>
自定义内容
</div>
<Button
size="small"
onClick={() => {
;(itemRef.current as any)?.toggle(false)
}}
>
确认
</Button>
</Menu.Item> */}
</View>
)}
</View>
);
})}
</View>
</Menu.Item>
</Menu>
);
};

View File

@@ -5,12 +5,23 @@ import styles from "./index.module.scss";
import { Button } from "@nutui/nutui-react-taro";
import { useListStore } from "src/store/listStore";
import { BubbleOption, FilterPopupProps } from "../../../types/list/types";
import CalendarCard from "@/components/CalendarCard/index";
import dateRangeUtils from '@/utils/dateRange'
// 场地
import CourtType from "@/components/CourtType";
// 玩法
import GamePlayType from "@/components/GamePlayType";
import { useDictionaryActions } from "@/store/dictionaryStore";
import { useMemo } from "react";
import { View } from "@tarojs/components";
const dateTrabseformMap = {
'1': dateRangeUtils.getThisWeekend,
'2': dateRangeUtils.getNextWeekRange,
'3': dateRangeUtils.getNextMonthRange
}
const FilterPopup = (props: FilterPopupProps) => {
const {
@@ -24,34 +35,75 @@ const FilterPopup = (props: FilterPopupProps) => {
onClose,
statusNavbarHeigh,
} = props;
const store = useListStore() || {};
const { getDictionaryValue } = useDictionaryActions() || {};
const { timeBubbleData } = store;
const { timeBubbleData, gamesNum, dateRangeOptions } = store;
/**
* @description 处理字典选项
* @param dictionaryValue 字典选项
* @returns 选项列表
*/
const handleOptions = (dictionaryValue: []) => {
return dictionaryValue?.map((item) => ({ label: item, value: item })) || [];
};
/**
* @description 场地类型选项
*/
const courtType = getDictionaryValue("court_type") || [];
const locationOptions: BubbleOption[] = useMemo(() => {
return courtType ? handleOptions(courtType) : [];
}, [courtType]);
/**
* @description 玩法选项
*/
const gamePlay = getDictionaryValue("game_play") || [];
const gamePlayOptions = useMemo(() => {
return gamePlay ? handleOptions(gamePlay) : [];
}, [gamePlay]);
/**
* @description 筛选选项改变
* @param name 选项名称
* @param value 选项值
*/
const handleFilterChange = (name, value) => {
onChange({ [name]: value });
};
/**
* @description 清空筛选
*/
const handleClearFilter = () => {
onClear();
onCancel();
};
/**
* @description 日期选择
* @param date 日期
*/
const handleDateRangeChange = (date: Date) => {
onChange({
'dateRange': [date, date],
'dateRangeQuick': '',
})
}
/**
* @description 点击 本周末 一周内 一月内
*/
const handleDateRangeQuickClick = (name, value) => {
const date = dateTrabseformMap?.[value]()
onChange({
'dateRange': [date?.start, date?.end],
[name]: value,
})
}
return (
<>
<Popup
@@ -62,17 +114,38 @@ const FilterPopup = (props: FilterPopupProps) => {
onClose={onClose}
style={{ marginTop: statusNavbarHeigh + "px" }}
overlayStyle={{ marginTop: statusNavbarHeigh + "px" }}
zIndex={1001}
>
<div className={styles.filterPopupWrapper}>
{/* 日历 */}
<View>
{/* 快捷选日期 */}
<View>
<Bubble
options={dateRangeOptions}
value={filterOptions?.dateRangeQuick}
onChange={handleDateRangeQuickClick}
layout="grid"
size="small"
columns={3}
name="dateRangeQuick"
/>
</View>
<CalendarCard
value={filterOptions?.dateRange?.[0]}
// minDate={}
// maxDate={}
onChange={handleDateRangeChange} />
</View>
{/* 时间气泡选项 */}
<Bubble
options={timeBubbleData}
value={filterOptions?.time}
value={filterOptions?.timeSlot}
onChange={handleFilterChange}
layout="grid"
size="small"
columns={3}
name="time"
name="timeSlot"
/>
{/* 范围选择 */}
@@ -131,7 +204,7 @@ const FilterPopup = (props: FilterPopupProps) => {
loading={loading}
onClick={onConfirm}
>
332
{gamesNum}
</Button>
</div>
</div>

View File

@@ -51,7 +51,7 @@
}
.location-position {
max-width: 66%;
max-width: 50%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;

View File

@@ -7,18 +7,23 @@ import "./index.scss";
const ListCard: React.FC<ListCardProps> = ({
id,
title,
dateTime,
venue_description,
start_time,
location,
distance_km,
registeredCount,
maxCount,
skillLevel,
current_players,
max_players,
skill_level_min,
skill_level_max,
play_type,
images = [],
image_list = [],
court_type,
key
}) => {
const renderItemImage = (src: string) => {
return <Image src={src} className="image" mode="aspectFill" />;
return <Image src={src} className="image" mode="aspectFill"
lazyLoad
defaultSource='https://images.unsplash.com/photo-1554068865-24cecd4e34b8?w=200&h=200&fit=crop&crop=center'
/>;
};
const handleViewDetail = () => {
@@ -30,21 +35,21 @@ const ListCard: React.FC<ListCardProps> = ({
// 根据图片数量决定展示样式
const renderImages = () => {
if (images?.length === 0) return null;
if (image_list?.length === 0) return null;
if (images?.length === 1) {
if (image_list?.length === 1) {
return (
<View className="single-image">
<View className="image-container">{renderItemImage(images[0])}</View>
<View className="image-container">{renderItemImage(image_list?.[0])}</View>
</View>
);
}
if (images?.length === 2) {
if (image_list?.length === 2) {
return (
<View className="double-image">
<View className="image-container">{renderItemImage(images[0])}</View>
<View className="image-container">{renderItemImage(images[1])}</View>
<View className="image-container">{renderItemImage(image_list?.[0])}</View>
<View className="image-container">{renderItemImage(image_list?.[1])}</View>
</View>
);
}
@@ -52,14 +57,14 @@ const ListCard: React.FC<ListCardProps> = ({
// 3张或更多图片
return (
<View className="triple-image">
<View className="image-container">{renderItemImage(images?.[0])}</View>
<View className="image-container">{renderItemImage(images?.[1])}</View>
<View className="image-container">{renderItemImage(images?.[2])}</View>
<View className="image-container">{renderItemImage(image_list?.[0])}</View>
<View className="image-container">{renderItemImage(image_list?.[1])}</View>
<View className="image-container">{renderItemImage(image_list?.[2])}</View>
</View>
);
};
return (
<View className="listCard">
<View className="listCard" key={key}>
<View className="listItem" onClick={handleViewDetail}>
{/* 左侧内容区域 */}
<View className="content">
@@ -75,17 +80,17 @@ const ListCard: React.FC<ListCardProps> = ({
{/* 时间信息 */}
<View className="date-time">
<Text>{dateTime}</Text>
<Text>{start_time}</Text>
</View>
{/* 地点,室内外,距离 */}
<View className="location">
{venue_description &&
<Text className="location-text location-position">{venue_description}</Text>}
{location &&
<Text className="location-text location-position">{location}</Text>}
<Text className="location-text location-time-distance">
{court_type && `${court_type}`}
{distance_km && `${distance_km}`}
{distance_km && `${distance_km}km`}
</Text>
</View>
@@ -93,7 +98,7 @@ const ListCard: React.FC<ListCardProps> = ({
<View className="bottom-info">
<View className="left-section">
<View className="avatar-group">
{Array.from({ length: Math.min(registeredCount, 3) }).map(
{Array.from({ length: 3 }).map(
(_, index) => (
<View key={index} className="avatar">
<Image
@@ -110,12 +115,12 @@ const ListCard: React.FC<ListCardProps> = ({
<View className="tags">
<View className="tag">
<Text className="tag-text">
{registeredCount}/
<Text className="tag-text-max">{maxCount}</Text>
{current_players}/
<Text className="tag-text-max">{max_players}</Text>
</Text>
</View>
<View className="tag">
<Text className="tag-text">{skill_level_max} zh {skill_level_max}</Text>
<Text className="tag-text">{skill_level_min} {skill_level_max}</Text>
</View>
{play_type && <View className="tag">
<Text className="tag-text">{play_type}</Text>

View File

@@ -2,7 +2,13 @@ import { Image, View, Text, Button } from "@tarojs/components";
import styles from "./index.module.scss";
import img from "@/config/images";
const ListLoadError = ({ reload }: { reload: () => void }) => {
interface IProps {
reload?: () => void;
text?: string;
}
const ListLoadError = (props: IProps) => {
const { reload, text } = props;
const handleReload = () => {
reload && typeof reload === "function" && reload();
};
@@ -13,11 +19,13 @@ const ListLoadError = ({ reload }: { reload: () => void }) => {
className={styles.listLoadErrorImg}
src={img.ICON_LIST_LOAD_ERROR}
/>
<Text className={styles.listLoadErrorText}></Text>
{text && <Text className={styles.listLoadErrorText}>{text}</Text>}
{reload && (
<Button className={styles.listLoadErrorBtn} onClick={handleReload}>
<Image src={img?.ICON_LIST_RELOAD} className={styles.reloadIcon} />
</Button>
)}
</View>
);
};

View File

@@ -35,6 +35,11 @@ const NtrpRange: React.FC<RangeProps> = ({
}, [JSON.stringify(value || [])]);
const handleChange = (val: [number, number]) => {
setCurrentValue(val);
// onChange?.(name, val);
};
const handleEndChange = (val: [number, number]) => {
setCurrentValue(val);
onChange?.(name, val);
};
@@ -76,7 +81,7 @@ const NtrpRange: React.FC<RangeProps> = ({
max={max}
step={step}
value={currentValue}
onEnd={handleChange}
onEnd={handleEndChange}
onChange={handleChange}
disabled={disabled}
defaultValue={[min, max]}

View File

@@ -20,20 +20,20 @@ const ListHeader = (props: IProps) => {
const { statusBarHeight, navbarHeight } = statusNavbarHeightInfo;
// 获取位置信息
const getCurrentLocal = () => {
updateState({
getLocationLoading: true,
});
getCurrentLocation().then((res) => {
updateState({
getLocationLoading: false,
location: res || {},
});
});
};
useEffect(() => {
getCurrentLocal();
}, []);
// const getCurrentLocal = () => {
// updateState({
// getLocationLoading: true,
// });
// getCurrentLocation().then((res) => {
// updateState({
// getLocationLoading: false,
// location: res || {},
// });
// });
// };
// useEffect(() => {
// getCurrentLocal();
// }, []);
const handleInputClick = () => {
Taro.navigateTo({

View File

@@ -1,25 +1,33 @@
import { View, Text } from "@tarojs/components";
import { View } from "@tarojs/components";
import ListCard from "@/components/ListCard";
import ListLoadError from "@/components/ListLoadError";
import ListCardSkeleton from "@/components/ListCardSkeleton";
// import { VirtualList } from '@nutui/nutui-react-taro'
import "./index.scss";
import { useReachBottom } from "@tarojs/taro";
const ListContainer = (props) => {
const { loading, data = [], error, reload, recommendList } = props;
const {
loading,
data = [],
error,
reload,
recommendList,
loadMoreMatches,
} = props;
console.log("===data", data);
useReachBottom(() => {
console.log("触底了");
// 调用 store 的加载更多方法
// loadMoreMatches();
loadMoreMatches();
});
if (error) {
return <ListLoadError reload={reload} />;
}
const renderList = (list) => {
if (loading && list.length === 0) {
const renderSkeleton = () => {
return (
<>
{new Array(10).fill(0).map(() => {
@@ -27,10 +35,38 @@ const ListContainer = (props) => {
})}
</>
);
};
// 渲染列表
const renderList = (list) => {
// 请求未回来显示骨架屏
// if (loading && list?.length === 0) {
// return (
// <>
// {new Array(10).fill(0).map(() => {
// return <ListCardSkeleton />;
// })}
// </>
// );
// }
// 请求数据为空
if (!loading && list?.length === 0) {
return <ListLoadError reload={reload} text="暂无数据" />;
}
// 渲染数据
return (
<>
{/* <VirtualList
containerHeight={1000}
itemHeight={144}
// itemEqual={false}
list={list}
itemRender={(data) => {
return <ListCard {...data}/>
}}
/> */}
{list?.map((match, index) => (
<ListCard key={match.id || index} {...match} />
))}
@@ -41,12 +77,14 @@ const ListContainer = (props) => {
return (
<View className="listContentWrapper">
{renderList(data)}
<View className="recommendTextWrapper">
{/* 显示骨架屏 */}
{loading && renderSkeleton()}
{/* <View className="recommendTextWrapper">
<Text className="recommendText">搜索结果较少,已为你推荐其他内容</Text>
</View>
{renderList(recommendList)}
{renderList(recommendList)} */}
{/* 到底了 */}
<View className="bottomTextWrapper"></View>
{data?.length > 0 && <View className="bottomTextWrapper"></View>}
</View>
);
};

View File

@@ -1,43 +0,0 @@
.container {
padding-left: 17px;
display: flex;
align-items: center;
gap: 8px;
.line {
width: 1px;
height: 25px;
background-color: #0000000F;
}
.logo {
width: 60px;
height: 34px;
}
.change {
width: 12px;
height: 12px;
}
.cityWrapper {
line-height: 20px;
}
.city {
font-weight: 600;
font-size: 13px;
line-height: 20px;
}
.infoWrapper {
line-height: 12px;
}
.info {
font-weight: 400;
font-size: 10px;
line-height: 12px;
color: #3C3C4399;
}
}

View File

@@ -0,0 +1,144 @@
.listNavWrapper {
position: relative;
}
.listNavContainer {
.listNavLine {
width: 1px;
height: 25px;
background-color: #0000000f;
}
.listNavLogo {
width: 60px;
height: 34px;
}
.listNavChange {
width: 12px;
height: 12px;
}
.listNavContentWrapper {
padding-left: 17px;
display: flex;
align-items: center;
gap: 8px;
}
.listNavCityWrapper {
line-height: 20px;
}
.listNavCity {
font-weight: 600;
font-size: 13px;
line-height: 20px;
}
.infoWrapper {
line-height: 12px;
}
.listNavInfoWrapper {
font-weight: 400;
font-size: 10px;
line-height: 12px;
color: #3c3c4399;
}
}
.inputCustomerNavbarContainer {
padding-left: 17px;
display: flex;
gap: 8px;
.logo {
width: 28px;
height: 16px;
}
.icon16 {
width: 16px;
height: 16px;
}
.navContent {
display: flex;
align-items: center;
gap: 4px;
width: 65%;
height: max-content;
// padding-top: 5px;
}
.searchContainer {
width: 100%;
display: flex;
align-items: center;
gap: 5.85px;
padding: 7.8px;
height: 30px;
box-sizing: border-box;
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);
height: 30px;
box-sizing: border-box;
font-size: 13.659px;
font-style: normal;
font-weight: 400;
line-height: 17.561px;
flex: 1;
.nut-input {
padding: 0;
}
}
}
.inputCustomerNavbarShowInput {
padding-left: 10px;
.logo {
width: 32px;
height: 32px;
}
}
.toggleElement {
/* 绝对定位使两个元素重叠 */
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
/* 过渡动画设置,实现平滑切换 */
transition: opacity 0.5s ease, transform 0.5s ease;
}
/* 第一个元素样式 */
.firstElement {
// background-color: #4a90e2;
}
/* 第二个元素样式 */
.secondElement {
// background-color: #5cb85c;
/* 初始状态:透明且稍微偏移 */
opacity: 0;
// transform: translateY(20px);
}
/* 隐藏状态 */
.hidden {
opacity: 0;
transform: translateY(20px);
// pointer-events: none; /* 隐藏时不响应鼠标事件 */
}
/* 可见状态 */
.visible {
opacity: 1;
transform: translateY(0);
}

View File

@@ -1,65 +1,134 @@
import { View, Text, Image } from "@tarojs/components";
import img from "@/config/images";
import { getCurrentLocation } from "@/utils/locationUtils";
import styles from "./index.module.scss";
import { useEffect } from "react";
import { useGlobalState } from "@/store/global";
import { useListState } from "@/store/listStore";
import CustomNavbar from '@/components/CustomNavbar'
import CustomNavbar from "@/components/CustomNavbar";
import { Input } from "@nutui/nutui-react-taro";
import Taro from "@tarojs/taro";
import "./index.scss";
const ListHeader = () => {
interface IProps {
config?: {
showInput: boolean;
inputLeftIcon: string;
iconPath?: string;
leftIconClick: () => void;
};
}
const ListHeader = (props: IProps) => {
const { config } = props;
const {
showInput = false,
inputLeftIcon,
leftIconClick,
} = config || {};
const {
updateState,
location,
getLocationText,
getLocationLoading,
statusNavbarHeightInfo,
} = useGlobalState();
const { gamesNum } = useListState();
const { gamesNum, searchValue, isShowInputCustomerNavBar } = useListState();
const { statusBarHeight, navbarHeight } = statusNavbarHeightInfo;
// 获取位置信息
const getCurrentLocal = () => {
updateState({
getLocationLoading: true,
});
getCurrentLocation().then((res) => {
updateState({
getLocationLoading: false,
location: res || {},
});
});
};
useEffect(() => {
getCurrentLocal();
}, []);
const currentAddress = getLocationLoading
? getLocationText
: location?.address;
const handleInputClick = () => {
const pages = Taro.getCurrentPages();
const currentPage = pages[pages.length - 1];
const currentPagePath = currentPage.route;
if (currentPagePath === "pages/searchResult/index") {
Taro.navigateBack();
} else {
Taro.navigateTo({
url: "/pages/search/index",
});
}
};
// 点击logo
const handleLogoClick = () => {
Taro.redirectTo({
url: "pages/list/index", // 列表页
});
};
const handleInputLeftIconClick = () => {
if (leftIconClick) {
leftIconClick();
} else {
handleLogoClick();
}
};
return (
<CustomNavbar>
<View className="listNavWrapper">
{/* 首页logo 导航*/}
<View
className={styles.container}
className={`listNavContainer toggleElement firstElement hidden ${
(!isShowInputCustomerNavBar && !showInput) && "visible"
}`}
style={{
height: `${navbarHeight}px`,
paddingTop: `${statusBarHeight}px`,
}}
>
<View className="listNavContentWrapper">
{/* logo */}
<Image src={img.ICON_LOGO} className={styles.logo} />
<View className={styles.line} />
<View className={styles.content}>
<View className={styles.cityWrapper}>
<Image
src={img.ICON_LOGO}
className="listNavLogo"
onClick={handleLogoClick}
/>
<View className="listNavLine" />
<View className="listNavContent">
<View className="listNavCityWrapper">
{/* 位置 */}
<Text className={styles.city}>{currentAddress}</Text>
{!getLocationLoading && (
<Image src={img.ICON_CHANGE} className={styles.change} />
<Text className="listNavCity">{currentAddress}</Text>
{!getLocationLoading && currentAddress && (
<Image src={img.ICON_CHANGE} className="listNavChange" />
)}
</View>
<View className={styles.infoWrapper}>
<Text className={styles.info}>${gamesNum}</Text>
<View className="listNavInfoWrapper">
<Text className="listNavInfo">${gamesNum}</Text>
</View>
</View>
</View>
</View>
{/* 搜索导航 */}
<View
className={`inputCustomerNavbarContainer toggleElement secondElement hidden ${
(isShowInputCustomerNavBar || showInput) && "visible"
} ${showInput && "inputCustomerNavbarShowInput"}`}
style={{
height: `${navbarHeight}px`,
paddingTop: `${statusBarHeight}px`,
}}
>
<View className="navContent">
{/* logo */}
<Image
src={inputLeftIcon || img.ICON_LIST_INPUT_LOGO}
className="logo"
onClick={handleInputLeftIconClick}
/>
{/* 搜索框 */}
<View className="searchContainer">
<Image
className="searchIcon icon16"
src={img.ICON_LIST_SEARCH_SEARCH}
/>
<Input
placeholder="搜索上海的球局和场地"
clearable={false}
value={searchValue}
onClick={handleInputClick}
/>
</View>
</View>
</View>
</View>

View File

@@ -24,3 +24,53 @@
padding: 0;
}
}
.listNavWrapper {
position: relative;
}
.toggleElement {
/* 绝对定位使两个元素重叠 */
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
/* 过渡动画设置,实现平滑切换 */
transition: opacity 0.5s ease, transform 0.5s ease;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
border-radius: 8px;
}
/* 第一个元素样式 */
.firstElement {
background-color: #4a90e2;
color: white;
}
/* 第二个元素样式 */
.secondElement {
background-color: #5cb85c;
color: white;
/* 初始状态:透明且稍微偏移 */
opacity: 0;
transform: translateY(20px);
}
/* 可见状态 */
.visible {
opacity: 1;
transform: translateY(0);
}
/* 隐藏状态 */
.hidden {
opacity: 0;
transform: translateY(20px);
pointer-events: none; /* 隐藏时不响应鼠标事件 */
}

View File

@@ -7,17 +7,15 @@ import { useListStore } from "@/store/listStore";
import { useGlobalState } from "@/store/global";
import { View } from "@tarojs/components";
import CustomerNavBar from "@/container/listCustomNavbar";
import InputCustomerBar from "@/container/inputCustomerNavbar";
import GuideBar from "@/components/GuideBar";
import ListContainer from "@/container/listContainer";
import DistanceQuickFilter from "@/components/DistanceQuickFilter";
import img from "@/config/images";
const ListPage = () => {
// 从 store 获取数据和方法
const store = useListStore() || {};
const { statusNavbarHeightInfo, location } = useGlobalState() || {};
const { statusNavbarHeightInfo, location = {} } = useGlobalState() || {};
const { totalHeight } = statusNavbarHeightInfo || {};
const {
isShowFilterPopup,
@@ -25,8 +23,7 @@ const ListPage = () => {
matches,
recommendList,
loading,
fetchMatches,
refreshMatches,
getMatchesData,
updateState,
filterCount,
updateFilterOptions, // 更新筛选条件
@@ -38,50 +35,66 @@ const ListPage = () => {
isScrollTop,
searchValue,
isShowInputCustomerNavBar,
initialFilterSearch,
loadMoreMatches,
dateRangeOptions
} = store;
usePageScroll((res) => {
if (res?.scrollTop >= totalHeight && !isScrollTop) {
!isShowInputCustomerNavBar && updateState({ isShowInputCustomerNavBar: true });
if (res?.scrollTop >= totalHeight) {
!isShowInputCustomerNavBar &&
updateState({ isShowInputCustomerNavBar: true });
} else {
isShowInputCustomerNavBar && updateState({ isShowInputCustomerNavBar: false });
isShowInputCustomerNavBar &&
updateState({ isShowInputCustomerNavBar: false });
}
});
useEffect(() => {
// 页面加载时获取数据
fetchMatches();
if (location?.address) {
// 保存位置
updateState({ location });
}, []);
// 页面加载时获取数据
getMatchesData();
}
}, [location]);
const refreshMatches = () => {
initialFilterSearch();
};
// const getLoadMoreMatches = () => {
// loadMoreMatches()
// }
// 下拉刷新处理函数 - 使用Taro生命周期钩子
Taro.usePullDownRefresh(() => {
console.log("触发下拉刷新");
console.log("===触发下拉刷新");
clearFilterOptions()
// 调用 store 的刷新方法
refreshMatches()
.then(() => {
// 刷新完成后停止下拉刷新动画
Taro.stopPullDownRefresh();
// refreshMatches()
// .then(() => {
// // 刷新完成后停止下拉刷新动画
// Taro.stopPullDownRefresh();
// 显示刷新成功提示
Taro.showToast({
title: "刷新成功",
icon: "success",
duration: 1500,
});
})
.catch(() => {
// 刷新失败时也停止动画
Taro.stopPullDownRefresh();
// // 显示刷新成功提示
// Taro.showToast({
// title: "刷新成功",
// icon: "success",
// duration: 1500,
// });
// })
// .catch(() => {
// // 刷新失败时也停止动画
// Taro.stopPullDownRefresh();
Taro.showToast({
title: "刷新失败",
icon: "error",
duration: 1500,
});
});
// Taro.showToast({
// title: "刷新失败",
// icon: "error",
// duration: 1500,
// });
// });
});
const toggleShowPopup = () => {
@@ -115,21 +128,14 @@ const ListPage = () => {
};
return (
<>
{!isShowInputCustomerNavBar ? (
<View>
{/* 自定义导航 */}
<CustomerNavBar />
) : (
<InputCustomerBar icon={img.ICON_LIST_INPUT_LOGO} />
)}
<View className={styles.listPage}>
<View
className={`${styles.listTopSearchWrapper} ${
isScrollTop ? styles.isScroll : ""
}`}
// style={{
// top: statusNavbarHeightInfo?.totalHeight,
// }}
>
<SearchBar
handleFilterIcon={toggleShowPopup}
@@ -176,11 +182,12 @@ const ListPage = () => {
loading={loading}
error={error}
reload={refreshMatches}
loadMoreMatches={loadMoreMatches}
/>
</View>
<GuideBar currentPage="list" />
</>
</View>
);
};

View File

@@ -1,5 +1,6 @@
.listSearchContainer {
padding: 0 15px;
padding-top: 16px;
.icon16 {
width: 16px;

View File

@@ -1,4 +1,4 @@
import CustomerNavbarBack from "@/components/CustomerNavbarBack";
// import CustomerNavbarBack from "@/components/CustomerNavbarBack";
import { View, Image, Text } from "@tarojs/components";
import { Input } from "@nutui/nutui-react-taro";
import { useEffect, useMemo, useRef } from "react";
@@ -18,11 +18,13 @@ const ListSearch = () => {
suggestionList,
isShowSuggestion,
} = useListState() || {};
const ref = useRef<any>(null);
useEffect(() => {
getSearchHistory();
return () => {
handleClear();
};
}, []);
useEffect(() => {
@@ -35,6 +37,12 @@ const ListSearch = () => {
return new RegExp(searchValue, "gi");
}, [searchValue]);
// 是否显示清空图标
const isShowClearIcon = searchValue && searchValue?.length > 0;
// 是否显示搜索历史
const isShowHistory = searchHistory && searchHistory?.length > 0;
/**
* @description 输入
* @param value
@@ -54,16 +62,16 @@ const ListSearch = () => {
* @description 点击清空输入内容
*/
const handleClear = () => {
updateState({ searchValue: "" });
updateState({ searchValue: "", isShowSuggestion: false });
};
/**
* @description 点击历史搜索
* @param value
*/
const handleHistoryClick = (value: string) => {
updateState({ searchValue: value });
handleSearch();
const handleHistoryClick = (item: { id: number; title: string }) => {
updateState({ searchValue: item?.title });
handleSearch(item?.title);
};
/**
@@ -81,25 +89,21 @@ const ListSearch = () => {
searchValue: val,
isShowSuggestion: false,
});
handleSearch();
handleSearch(val);
};
/**
* @description 点击搜索
*/
const handleSearch = () => {
const handleSearch = (val?: string) => {
if (!val) {
return;
}
Taro.navigateTo({
url: `/pages/searchResult/index`,
});
};
// 是否显示清空图标
const isShowClearIcon = searchValue && searchValue?.length > 0;
// 是否显示搜索历史
const isShowHistory =
!isShowClearIcon && searchHistory && searchHistory?.length > 0;
return (
<>
<View className="listSearchContainer">
@@ -125,7 +129,7 @@ const ListSearch = () => {
/>
)}
<View className="searchLine" />
<Text className="searchText" onClick={handleSearch}>
<Text className="searchText" onClick={() => handleSearch(searchValue)}>
</Text>
</View>
@@ -180,12 +184,15 @@ const ListSearch = () => {
{isShowHistory && (
<View className="historySearchList">
{(searchHistory || [])?.map((item) => {
if (!item?.title) {
return null;
}
return (
<Text
className="historySearchItem"
onClick={() => handleHistoryClick(item)}
>
{item}
{item?.title}
</Text>
);
})}

View File

@@ -1,4 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '搜索结果',
// navigationStyle: 'custom',
navigationStyle: 'custom',
})

View File

@@ -2,6 +2,7 @@
position: relative;
.searchResultFilterWrapper {
position: sticky;
display: flex;
align-items: center;
gap: 5px;

View File

@@ -1,24 +1,24 @@
import { View, Image, Text } from "@tarojs/components";
import { useSearchResultState } from "@/store/searchResultStore";
// import { useSearchResultState } from "@/store/searchResultStore";
import { useListStore } from "@/store/listStore";
import { useGlobalState } from "@/store/global";
import ListContainer from "@/container/listContainer";
import img from "@/config/images";
import "./index.scss";
import CustomerNavBar from "@/container/listCustomNavbar";
import DistanceQuickFilter from "@/components/DistanceQuickFilter";
import { useEffect } from "react";
import FilterPopup from "@/components/FilterPopup";
import "./index.scss";
import Taro from "@tarojs/taro";
const SearchResult = () => {
const {
isShowFilterPopup,
error,
distanceQuickFilter,
matches,
searchResultData,
recommendList,
loading,
fetchMatches,
refreshMatches,
updateState,
filterCount,
updateFilterOptions, // 更新筛选条件
@@ -26,14 +26,29 @@ const SearchResult = () => {
clearFilterOptions,
distanceData,
quickFilterData,
} = useSearchResultState() || {};
loadMoreMatches,
getMatchesData
} = useListStore() || {};
const { statusNavbarHeightInfo } = useGlobalState() || {};
const { totalHeight } = statusNavbarHeightInfo || {};
const isSelect = filterCount > 0;
useEffect(() => {
const pages = Taro.getCurrentPages()
const currentPage = pages?.[pages.length - 1];
updateState({currentPage, isSearchResult: true})
// if (location?.address) {
// 保存位置
// updateState({ location });
// 页面加载时获取数据
fetchMatches();
console.log('===搜索结果页===')
getMatchesData();
// }
return () => {
console.log('===搜索结果组件卸载')
updateState({currentPage: '', isSearchResult: false})
}
}, []);
/**
@@ -58,14 +73,30 @@ const SearchResult = () => {
});
};
const refreshMatches = () => {
getMatchesData();
};
const handleLeftIconClick = () => {
Taro.navigateBack()
}
return (
<View className="searchResultPage">
{/* 自定义导航 */}
<CustomerNavBar
config={{
showInput: true,
inputLeftIcon: img.ICON_LIST_SEARCH_BACK,
leftIconClick: handleLeftIconClick
}}
/>
{/* 筛选 */}
<View
className="searchResultFilterWrapper"
style={
{
// top: `${totalHeight}px`
top: `${totalHeight}px`
}
}
>
@@ -79,10 +110,13 @@ const SearchResult = () => {
quickValue={distanceQuickFilter?.quick}
/>
{/* 筛选 icon */}
<View className={`filterIconWrapper ${isSelect && 'active'}`} onClick={toggleShowPopup}>
<View
className={`filterIconWrapper ${isSelect && "active"}`}
onClick={toggleShowPopup}
>
<Image
src={isSelect ? img.ICON_FILTER_SELECTED : img.ICON_FILTER}
className={`filterIcon ${isSelect && 'active'}`}
className={`filterIcon ${isSelect && "active"}`}
/>
{isSelect && <Text className="filterCount">{filterCount}</Text>}
</View>
@@ -99,7 +133,8 @@ const SearchResult = () => {
onClear={clearFilterOptions}
visible={isShowFilterPopup}
onClose={toggleShowPopup}
statusNavbarHeigh={0}
// statusNavbarHeigh={0}
statusNavbarHeigh={statusNavbarHeightInfo?.totalHeight}
/>
</View>
)}
@@ -107,11 +142,12 @@ const SearchResult = () => {
{/* 列表内容 */}
<ListContainer
data={matches}
data={searchResultData}
recommendList={recommendList}
loading={loading}
error={error}
reload={refreshMatches}
loadMoreMatches={loadMoreMatches}
/>
</View>
);

View File

@@ -16,35 +16,36 @@ interface ApiResponse<T> {
* @param params 查询参数
* @returns Promise<TennisMatch[]>
*/
export const getGamesList = async (params?: {
page?: number;
pageSize?: number;
location?: string;
skillLevel?: string;
}) => {
export const getGamesList = async (params?: Record<string, any>) => {
try {
return httpService.post('/games/list', params, { showLoading: false })
/**
* 判断请求哪个接口
* 筛选智能排序:/games/integrate_list
* 常规搜索:/games/list
*/
// const isIntegrate = params?.order === '0';
// const fetchApi = isIntegrate ? '/games/integrate_list' : '/games/list'
return httpService.post('/games/list', params, { showLoading: true })
} catch (error) {
console.error("列表数据获取失败:", error);
throw error;
}
};
export const getGamesIntegrateList = async (params?: {
page?: number;
pageSize?: number;
location?: string;
skillLevel?: string;
}) => {
/**
* 获取列表
* @param params 查询参数
* @returns Promise<TennisMatch[]>
*/
export const getGamesIntegrateList = async (params?: Record<string, any>) => {
try {
return httpService.post('/games/integrate_list', params, { showLoading: false })
return httpService.post('/games/integrate_list', params, { showLoading: true })
} catch (error) {
console.error("列表数据获取失败:", error);
throw error;
}
};
/**
* 获取搜索历史记录的异步函数
* @param {Object} params - 查询参数对象
@@ -53,7 +54,7 @@ export const getGamesIntegrateList = async (params?: {
export const getSearchHistory = async (params) => {
try {
// 调用HTTP服务获取搜索历史记录
return httpService.get('/games/search_history', params)
return httpService.post('/games/search_history', params, { showLoading: true })
} catch (error) {
// 捕获并打印错误信息
console.error("历史记录获取失败:", error);
@@ -66,10 +67,10 @@ export const getSearchHistory = async (params) => {
* @description 清除搜索历史
* @returns
*/
export const clearHistory = async () => {
export const clearHistory = async (params) => {
try {
// 调用HTTP服务清除搜索历史记录
return httpService.post('/search_history/delete_all')
return httpService.post('/games/search_history/delete_all', params, { showLoading: true })
} catch (error) {
// 捕获并打印错误信息
console.error("清除历史记录失败:", error);
@@ -86,7 +87,7 @@ export const clearHistory = async () => {
export const searchSuggestion = async (params) => {
try {
// 调用HTTP服务获取搜索建议
return httpService.get('/games/search_recommendations', params)
return httpService.post('/games/search_recommendations', params)
} catch (error) {
// 捕获并打印错误信息
console.error("搜索建议获取失败:", error);

View File

@@ -1,38 +1,40 @@
import { create } from 'zustand'
import { getNavbarHeight } from '@/utils/getNavbarHeight'
import { create } from "zustand";
import { getNavbarHeight } from "@/utils/getNavbarHeight";
import { getCurrentLocation } from "@/utils/locationUtils";
interface GlobalState {
location: Record<string, any>
getLocationLoading: boolean
getLocationText: string
location: Record<string, any>;
getLocationLoading: boolean;
getLocationText: string;
statusNavbarHeightInfo: {
statusBarHeight: number
navbarHeight: number
totalHeight: number
}
statusBarHeight: number;
navbarHeight: number;
totalHeight: number;
};
}
interface GlobalActions {
updateState: (payload: Record<string, any>) => void
getNavbarHeightInfo: () => void
updateState: (payload: Record<string, any>) => void;
getNavbarHeightInfo: () => void;
getCurrentLocationInfo: () => void;
}
// 完整的 Store 类型
type GlobalStore = GlobalState & GlobalActions
type GlobalStore = GlobalState & GlobalActions;
// 创建 store
export const useGlobalStore = create<GlobalStore>()((set, get) => ({
// 位置信息
location: {},
// 正在获取位置信息
getLocationLoading: false,
getLocationLoading: true,
// 获取位置信息文本
getLocationText: '定位中...',
getLocationText: "定位中...",
// 状态栏和导航栏高度信息
statusNavbarHeightInfo: {
statusBarHeight: 0,
navbarHeight: 0,
totalHeight: 0
totalHeight: 0,
},
// 获取导航栏高度信息
@@ -42,9 +44,19 @@ export const useGlobalStore = create<GlobalStore>()((set, get) => ({
statusNavbarHeightInfo: {
statusBarHeight,
navbarHeight,
totalHeight: statusBarHeight + navbarHeight
}
})
totalHeight: statusBarHeight + navbarHeight,
},
});
},
// 获取位置信息
getCurrentLocationInfo: () => {
getCurrentLocation().then((res) => {
set({
getLocationLoading: false,
location: res || {},
});
});
},
// 更新store数据
@@ -52,10 +64,10 @@ export const useGlobalStore = create<GlobalStore>()((set, get) => ({
const state = get();
set({
...state,
...(payload || {})
})
}
}))
...(payload || {}),
});
},
}));
// 导出便捷的 hooks
export const useGlobalState = () => useGlobalStore((state) => state)
export const useGlobalState = () => useGlobalStore((state) => state);

View File

@@ -1,22 +1,49 @@
import { create } from 'zustand'
import { getGamesList, getGamesIntegrateList, getSearchHistory, clearHistory, searchSuggestion } from '../services/listApi'
import { ListActions, IFilterOptions, ListState } from '../../types/list/types'
import { create } from "zustand";
import {
getGamesList,
getGamesIntegrateList,
getSearchHistory,
clearHistory,
searchSuggestion,
} from "../services/listApi";
import {
ListActions,
IFilterOptions,
ListState,
IPayload,
} from "../../types/list/types";
import dateRangeUtils from '@/utils/dateRange'
// 完整的 Store 类型
type TennisStore = ListState & ListActions
type TennisStore = ListState & ListActions;
const defaultFilterOptions: IFilterOptions = {
dateRange: [], // 日期区间
timeSlot: '', // 时间段
dateRange: [new Date(), new Date()], // 日期区间
dateRangeQuick: '',
timeSlot: "", // 时间段
ntrp: [1, 5], // NTRP 水平区间
venueType: '', // 场地类型
playType: '', // 玩法
venueType: "", // 场地类型
playType: "", // 玩法
};
const defaultDistance = 'all'; // 默认距离
// const defaultDistance = "all"; // 默认距离
const defaultDistanceQuickFilter = {
distance: "",
quick: "0",
};
const defaultPageOption = {
page: 1,
pageSize: 20,
};
// 创建 store
export const useListStore = create<TennisStore>()((set, get) => ({
currentPage: "",
isSearchResult: false,
searchResultData: [],
// dateRange: [new Date(), new Date()], // 日期区间
// 初始状态
matches: [],
// 推荐列表
@@ -26,10 +53,10 @@ export const useListStore = create<TennisStore>()((set, get) => ({
longitude: 0,
}, // 位置
// 是否加载中
loading: false,
loading: true,
error: null,
// 搜索的value
searchValue: '',
searchValue: "",
// 是否展示综合筛选弹窗
isShowFilterPopup: false,
// 综合筛选项
@@ -37,56 +64,46 @@ export const useListStore = create<TennisStore>()((set, get) => ({
// 综合筛选 选择的筛选数量
filterCount: 0,
// 距离筛选
distance: defaultDistance,
// distance: defaultDistance,
// 快捷筛选
quickFilter: 1, // 1: 默认 2: 好评 3: 销量
// 距离筛选数据
distanceData: [
{ id: 0, label: "全城", value: "0" },
{ id: 0, label: "全城", value: "" },
{ id: 1, label: "3km", value: "3" },
{ id: 2, label: "5km", value: "5" },
{ id: 3, label: "10km", value: "10" },
],
// 快捷筛选数据
quickFilterData: [
{ text: "默认排序", value: "0" },
{ text: "好评排序", value: "1" },
{ text: "销量排序", value: "2" },
{ label: "智能排序", value: "0" },
{ label: "距离更近", value: "1" },
{ label: "时间更近", value: "2" },
],
// 距离筛选和快捷筛选
distanceQuickFilter: {
distance: '全城',
quick: '0',
},
distanceQuickFilter: defaultDistanceQuickFilter,
// 气泡日期范围
dateRangeOptions: [
{ id: 1, label: "本周末", value: "1" },
{ id: 2, label: "一周内", value: "2" },
{ id: 3, label: "一月内", value: "3" },
],
// 时间气泡数据
timeBubbleData: [
{ id: 1, label: "晨间 6:00-10:00", value: "1" },
{ id: 2, label: "上午 10:00-12:00", value: "2" },
{ id: 3, label: "中午 12:00-14:00", value: "3" },
{ id: 4, label: "下午 14:00-18:00", value: "4" },
{ id: 5, label: "晚上 18:00-22:00", value: "5" },
{ id: 6, label: "夜间 22:00-24:00", value: "6" },
],
// 场地类型数据
locationOptions: [
{ id: 1, label: "室内", value: "1" },
{ id: 2, label: "室外", value: "2" },
{ id: 3, label: "半室外", value: "3" },
],
// 玩法数据
gamePlayOptions: [
{ id: 1, label: "不限", value: "不限" },
{ id: 2, label: "单打", value: "单打" },
{ id: 3, label: "双打", value: "双打" },
{ id: 4, label: "娱乐", value: "娱乐" },
{ id: 5, label: "拉球", value: "拉球" },
{ id: 1, label: "晨间 6:00-10:00", value: "6:00-10:00" },
{ id: 2, label: "上午 10:00-12:00", value: "10:00-12:00" },
{ id: 3, label: "中午 12:00-14:00", value: "12:00-14:00" },
{ id: 4, label: "下午 14:00-18:00", value: "14:00-18:00" },
{ id: 5, label: "晚上 18:00-22:00", value: "18:00-22:00" },
{ id: 6, label: "夜间 22:00-24:00", value: "22:00-24:00" },
],
// 球局数量
gamesNum: 124,
gamesNum: 0,
// 页面滚动距离顶部距离 是否大于0
isScrollTop: false,
// 搜索历史数据
searchHistory: ['上海', '黄浦', '上海', '静安', '徐汇', '黄浦', '普陀', '黄浦', '长宁', '黄浦'],
searchHistory: [],
// 搜索历史数据默认 Top 15
searchHistoryParams: {
page: 1,
@@ -105,157 +122,249 @@ export const useListStore = create<TennisStore>()((set, get) => ({
// 打开快捷筛选框
isOpenQuickFilterPopup: false,
// 分页
pageOption: {
page: 1,
pageSize: 20,
},
pageOption: defaultPageOption,
// 组装搜索数据
getSearchParams: () => {
const state = get()
const state = get();
const filterOptions = state?.filterOptions || {};
console.log('===1111filterOptions', filterOptions)
const distanceQuickFilter = state?.distanceQuickFilter || {};
const params = {
pageOption: state.pageOption,
seachOption: {
...filterOptions,
title: state.searchValue,
ntrpMin: filterOptions?.ntrp?.[0],
ntrpMax: filterOptions?.ntrp?.[1],
dateRange: [
dateRangeUtils.formatDate(filterOptions?.dateRange?.[0]),
dateRangeUtils.formatDate(filterOptions?.dateRange?.[1])
],
},
order: '',
order: distanceQuickFilter?.quick,
lat: state?.location?.latitude,
lng: state?.location?.longitude,
}
};
console.log('===列表参数params', params)
return params;
},
// 初始化获取比赛数据
// 设置搜索的列表结果
setListData: (payload: IPayload) => {
const { isSearchResult } = get();
const { error, data, loading, gamesNum } = payload;
const saveKey = isSearchResult ? "searchResultData" : "matches";
const saveData = { error, loading, gamesNum, [saveKey]: data };
set(saveData);
},
// 获取列表数据(常规搜索)
fetchMatches: async (params) => {
set({ loading: true, error: null })
set({ loading: true, error: null });
const { getSearchParams, setListData, distanceQuickFilter } = get();
try {
const { getSearchParams } = get();
const searchParams = getSearchParams() || {};
const reqParams = {
...(searchParams || {}),
...params,
};
const isIntegrate = distanceQuickFilter?.quick === "0";
let fetchFn = getGamesList;
if (isIntegrate) {
reqParams.order = "";
fetchFn = getGamesIntegrateList;
}
const resData = await getGamesList(reqParams) || {};
console.log('===resData', resData)
console.log("===fetchMatches 获取列表数据参数:", reqParams);
const resData = (await fetchFn(reqParams)) || {};
const { data = {}, code } = resData;
if (code !== 0) {
set({
error: '1',
matches: [],
setListData({
error: "-1",
data: [],
loading: false,
})
gamesNum: 0,
});
}
const { count, rows } = data;
// const list = (rows || []).map(() => {
// return {
// id: "3",
// title: "黄浦区双打约球",
// dateTime: "7月20日(周日)下午6点 2小时",
// location: "仁恒河滨花园网球场",
// distance: "3.5km",
// registeredCount: 3,
// maxCount: 4,
// skillLevel: "2.0 至 2.5",
// matchType: "双打",
// images: [
// "https://images.unsplash.com/photo-1554068865-24cecd4e34b8?w=200&h=200&fit=crop&crop=center",
// ],
// }
// })
set({
matches: rows || [],
recommendList: rows || [],
error: null,
setListData({
// recommendList: rows || [],
error: '',
data: rows || [],
loading: false,
gamesNum: count,
})
});
} catch (error) {
set({
error,
matches: [],
setListData({
error: "-1",
data: [],
loading: false,
})
gamesNum: 0,
});
}
},
// 获取列表数据(智能筛选)
// getIntegrateListData: async (params) => {
// set({ loading: true, error: null });
// const { getSearchParams, setListData } = get();
// try {
// const searchParams = getSearchParams() || {};
// const reqParams = {
// ...(searchParams || {}),
// ...params,
// };
// reqParams.order = "";
// console.log("===getGamesIntegrateList 获取列表数据参数:", reqParams);
// const resData = (await getGamesIntegrateList(reqParams)) || {};
// const { data = {}, code } = resData;
// if (code !== 0) {
// setListData({
// error: '-1',
// data: [],
// loading: false,
// gamesNum: 0,
// });
// }
// const { count, rows } = data;
// setListData({
// // recommendList: rows || [],
// error: null,
// data: rows || [],
// loading: false,
// gamesNum: count,
// });
// } catch (error) {
// setListData({
// error: null,
// matches: [],
// loading: false,
// gamesNum: 0,
// });
// }
// },
// 获取列表数据
getMatchesData: () => {
const { fetchMatches } = get();
fetchMatches();
// if (distanceQuickFilter?.quick === "0") {
// getIntegrateListData();
// } else {
// fetchMatches();
// }
},
// 获取历史搜索数据
getSearchHistory: async () => {
try {
const params = get()?.searchHistoryParams || {}
const resData = await getSearchHistory(params) || {};
console.log('===resData', resData)
} catch (error) {
}
const params = get()?.searchHistoryParams || {};
const resData = (await getSearchHistory(params)) || {};
const searchHistory = resData?.data?.records || [];
set({
searchHistory,
});
} catch (error) { }
},
// 清空历史记录
clearHistory: async () => {
try {
const resData = await clearHistory() || {};
} catch (error) {
}
const params = {};
const resData = (await clearHistory(params)) || {};
if (resData?.code === 0) {
set({
searchHistory: [],
})
});
}
} catch (error) { }
},
// 获取联想
searchSuggestion: async (val: string) => {
try {
const resData = await searchSuggestion({ val }) || {};
console.log('===获取联想', resData)
// set({
// suggestionList: ['上海球局', '黄浦球局', '上海球局', '静安球局', '徐汇球局', '黄浦球局', '普陀球局', '黄浦球局', '长宁球局', '黄浦球局'],
// isShowSuggestion: true,
// })
const resData = (await searchSuggestion({ val, limit: 10 })) || {};
const recommendations = resData?.data?.recommendations || [];
const total = resData?.data?.total;
set({
suggestionList: recommendations,
isShowSuggestion: total > 0,
});
} catch (error) {
set({
suggestionList: ['上海球局', '黄浦球局', '上海球局', '静安球局', '徐汇球局', '黄浦球局', '普陀球局', '黄浦球局', '长宁球局', '黄浦球局'],
suggestionList: [],
isShowSuggestion: true,
})
});
}
},
// 清除错误信息
clearError: () => {
set({ error: null })
set({ error: null });
},
// 更新综合筛选项
updateFilterOptions: (payload: Record<string, any>) => {
const preFilterOptions = get()?.filterOptions || {}
const filterOptions = { ...preFilterOptions, ...payload }
const filterCount = Object.values(filterOptions).filter(Boolean).length
console.log('===更新综合筛选项', filterOptions, filterCount)
const { filterOptions: preFilterOptions, getMatchesData } = get() || {};
const filterOptions = { ...preFilterOptions, ...payload };
const filterCount = Object.values(filterOptions).filter(Boolean).length;
console.log("===更新综合筛选项", filterOptions, filterCount);
set({
filterOptions,
filterCount
})
filterCount,
pageOption: defaultPageOption,
});
// 重新搜索数据
getMatchesData();
},
// 清空综合筛选选项
clearFilterOptions: () => {
const { getMatchesData } = get() || {};
set({
filterOptions: defaultFilterOptions,
filterCount: 0
})
filterCount: 0,
pageOption: defaultPageOption,
});
getMatchesData();
},
// 加载更多数据
loadMoreMatches: () => {
const { pageOption, getMatchesData } = get() || {};
set({
pageOption: {
page: pageOption?.page + 1,
pageSize: 20,
},
});
getMatchesData();
},
// 初始化搜索条件 重新搜索
initialFilterSearch: () => {
const { getMatchesData } = get();
set({
distanceQuickFilter: defaultDistanceQuickFilter,
filterOptions: defaultFilterOptions,
pageOption: defaultPageOption,
});
getMatchesData();
},
// 更新store数据
updateState: (payload: Record<string, any>) => {
const state = get();
console.log('Store: 更新数据:', state);
console.log("Store: 更新数据:", state);
set({
...(payload || {})
})
}
}))
...(payload || {}),
});
},
}));
// 导出便捷的 hooks
export const useListState = () => useListStore((state) => state)
export const useListState = () => useListStore((state) => state);

View File

@@ -1,255 +0,0 @@
import { create } from 'zustand'
import { getTennisMatches, getSearchHistory, clearHistory, searchSuggestion } from '../services/listApi'
import { ListActions, ISearchOptions, ListState } from '../../types/list/types'
// 完整的 Store 类型
type TennisStore = ListState & ListActions
const defaultSearchOptions: ISearchOptions = {
title: '', // 标题
dateRange: ['', ''], // 日期区间
timeSlot: '', // 时间段
ntrpMin: 1, // NTRP 最小值
ntrpMax: 5, // NTRP 最大值
venueType: '', // 场地类型
playType: '', // 玩法
distanceFilter: '', // 距离筛选
lat: '', // 纬度
lng: '', // 经度
};
const defaultDistance = 'all'; // 默认距离
// 创建 store
export const useSearchResultStore = create<TennisStore>()((set, get) => ({
// 初始状态
matches: [],
// 推荐列表
recommendList: [],
// 是否加载中
loading: false,
error: null,
// 搜索的value
searchValue: '',
// 是否展示综合筛选弹窗
isShowFilterPopup: false,
// 综合筛选项
searchOption: defaultSearchOptions,
// 综合筛选 选择的筛选数量
filterCount: 0,
// 距离筛选
distance: defaultDistance,
// 快捷筛选
quickFilter: 1, // 1: 默认 2: 好评 3: 销量
// 距离筛选数据
distanceData: [
{ id: 0, label: "全城", value: "全城" },
{ id: 1, label: "3km", value: "3km" },
{ id: 2, label: "5km", value: "5km" },
{ id: 3, label: "10km", value: "10km" },
],
// 快捷筛选数据
quickFilterData: [
{ text: "默认排序", value: "0" },
{ text: "好评排序", value: "1" },
{ text: "销量排序", value: "2" },
],
// 距离筛选和快捷筛选
distanceQuickFilter: {
distance: '全城',
quick: '0',
},
// 时间气泡数据
timeBubbleData: [
{ id: 1, label: "晨间 6:00-10:00", value: "1" },
{ id: 2, label: "上午 10:00-12:00", value: "2" },
{ id: 3, label: "中午 12:00-14:00", value: "3" },
{ id: 4, label: "下午 14:00-18:00", value: "4" },
{ id: 5, label: "晚上 18:00-22:00", value: "5" },
{ id: 6, label: "夜间 22:00-24:00", value: "6" },
],
// 场地类型数据
locationOptions: [
{ id: 1, label: "室内", value: "1" },
{ id: 2, label: "室外", value: "2" },
{ id: 3, label: "半室外", value: "3" },
],
// 玩法数据
gamePlayOptions: [
{ id: 1, label: "不限", value: "不限" },
{ id: 2, label: "单打", value: "单打" },
{ id: 3, label: "双打", value: "双打" },
{ id: 4, label: "娱乐", value: "娱乐" },
{ id: 5, label: "拉球", value: "拉球" },
],
// 球局数量
gamesNum: 124,
// 页面滚动距离顶部距离 是否大于0
isScrollTop: false,
// 搜索历史数据
searchHistory: ['上海', '黄浦', '上海', '静安', '徐汇', '黄浦', '普陀', '黄浦', '长宁', '黄浦'],
// 搜索历史数据默认 Top 15
searchHistoryParams: {
page: 1,
pageSize: 15,
},
// 联想词
suggestionList: [],
// 是否显示联想词
isShowSuggestion: false,
// 列表页是否显示搜索框自定义导航
isShowInputCustomerNavBar: false,
// 结果页是否显示搜索框自定义导航
isShowResultInputCustomerNavBar: false,
// 打开距离筛选框
isOpenDistancePopup: false,
// 打开快捷筛选框
isOpenQuickFilterPopup: false,
// 分页
pageOption: {
page: 1,
pageSize: 10,
},
// 获取比赛数据
fetchMatches: async (params) => {
set({ loading: true, error: null })
try {
const resData = await getTennisMatches(params) || {};
const { data = {}, code } = resData;
if (code !== 0) {
set({
error: '1',
matches: [],
loading: false,
})
}
const { count, rows } = data;
const list = (rows || []).map(() => {
return {
id: "3",
title: "黄浦区双打约球",
dateTime: "7月20日(周日)下午6点 2小时",
location: "仁恒河滨花园网球场",
distance: "3.5km",
registeredCount: 3,
maxCount: 4,
skillLevel: "2.0 至 2.5",
matchType: "双打",
images: [
"https://images.unsplash.com/photo-1554068865-24cecd4e34b8?w=200&h=200&fit=crop&crop=center",
],
}
})
set({
matches: list || rows || [],
recommendList: list || rows || [],
error: null,
loading: false,
gamesNum: count,
})
} catch (error) {
set({
error,
matches: [],
loading: false,
})
}
},
// 刷新比赛数据
refreshMatches: async () => {
set({ loading: true, error: null })
try {
const resData = await getTennisMatches() || {};
const { data = {}, code } = resData;
const { count, rows } = data;
set({
matches: rows,
loading: false,
})
} catch (error) {
}
},
// 获取历史搜索数据
getSearchHistory: async () => {
try {
const params = get()?.searchHistoryParams || {}
const resData = await getSearchHistory(params) || {};
console.log('===resData', resData)
} catch (error) {
}
},
// 清空历史记录
clearHistory: async () => {
try {
const resData = await clearHistory() || {};
} catch (error) {
}
set({
searchHistory: [],
})
},
// 获取联想
searchSuggestion: async (val: string) => {
try {
const resData = await searchSuggestion({ val }) || {};
console.log('===获取联想', resData)
// set({
// suggestionList: ['上海球局', '黄浦球局', '上海球局', '静安球局', '徐汇球局', '黄浦球局', '普陀球局', '黄浦球局', '长宁球局', '黄浦球局'],
// isShowSuggestion: true,
// })
} catch (error) {
set({
suggestionList: ['上海球局', '黄浦球局', '上海球局', '静安球局', '徐汇球局', '黄浦球局', '普陀球局', '黄浦球局', '长宁球局', '黄浦球局'],
isShowSuggestion: true,
})
}
},
// 清除错误信息
clearError: () => {
set({ error: null })
},
// 更新综合筛选项
updateSearchOptions: (payload: Record<string, any>) => {
console.log('===更新综合筛选项', payload)
const preSearchOptions = get()?.searchOption || {}
const searchOption = { ...preSearchOptions, ...payload }
const filterCount = Object.values(searchOption).filter(Boolean).length
set({
searchOption,
filterCount
})
},
// 清空综合筛选选项
clearSearchOptions: () => {
set({
searchOption: defaultSearchOptions,
filterCount: 0
})
},
// 更新store数据
updateState: (payload: Record<string, any>) => {
const state = get();
console.log('Store: 更新数据:', state);
set({
...(payload || {})
})
}
}))
// 导出便捷的 hooks
export const useSearchResultState = () => useSearchResultStore((state) => state)

133
src/utils/dateRange.ts Normal file
View File

@@ -0,0 +1,133 @@
/**
* 日期范围计算工具函数
* 提供从当前日期开始计算本周末、一周内和一个月内的开始和结束日期
*/
interface DateRange {
start: Date;
end: Date;
}
interface FormattedDateRange {
start: string;
end: string;
}
interface AllFormattedRanges {
thisWeekend: FormattedDateRange;
nextWeek: FormattedDateRange;
nextMonth: FormattedDateRange;
}
/**
* 获取当前日期
* @returns {Date} 当前日期对象
*/
const getCurrentDate = (): Date => {
return new Date();
};
/**
* 计算本周末(当前周的周六和周日)
* @param {Date} [baseDate] 基准日期,默认为当前日期
* @returns {DateRange} 包含start和end的日期对象
*/
const getThisWeekend = (baseDate?: Date): DateRange => {
const current = baseDate ? new Date(baseDate) : getCurrentDate();
// 计算到周六的天数getDay()返回0-60是周日6是周六
const daysToSaturday = 6 - current.getDay();
// 计算周六日期
const saturday = new Date(current);
saturday.setDate(current.getDate() + daysToSaturday);
// 计算周日日期
const sunday = new Date(saturday);
sunday.setDate(saturday.getDate() + 1);
return {
start: saturday,
end: sunday
};
};
/**
* 计算一周内从基准日期开始的7天
* @param {Date} [baseDate] 基准日期,默认为当前日期
* @returns {DateRange} 包含start和end的日期对象
*/
const getNextWeekRange = (baseDate?: Date): DateRange => {
const start = baseDate ? new Date(baseDate) : getCurrentDate();
const end = new Date(start);
end.setDate(start.getDate() + 6); // 今天 + 6天 = 7天后
return {
start,
end
};
};
/**
* 计算一个月内从基准日期开始的30天
* @param {Date} [baseDate] 基准日期,默认为当前日期
* @returns {DateRange} 包含start和end的日期对象
*/
const getNextMonthRange = (baseDate?: Date): DateRange => {
const start = baseDate ? new Date(baseDate) : getCurrentDate();
const end = new Date(start);
end.setDate(start.getDate() + 29); // 今天 + 29天 = 30天后
return {
start,
end
};
};
/**
* 格式化日期为 YYYY-MM-DD 格式
* @param {Date} date 日期对象
* @returns {string} 格式化后的日期字符串
* @throws {Error} 如果传入的不是有效的日期对象则抛出错误
*/
const formatDate = (date: Date): string => {
if (!(date instanceof Date) || isNaN(date.getTime())) {
throw new Error('Invalid date object: 请传入有效的Date实例');
}
return date.toISOString().split('T')[0];
};
/**
* 获取所有日期范围的格式化结果
* @param {Date} [baseDate] 基准日期,默认为当前日期
* @returns {AllFormattedRanges} 包含所有格式化后的日期范围
*/
const getAllFormattedRanges = (baseDate?: Date): AllFormattedRanges => {
const weekend = getThisWeekend(baseDate);
const week = getNextWeekRange(baseDate);
const month = getNextMonthRange(baseDate);
return {
thisWeekend: {
start: formatDate(weekend.start),
end: formatDate(weekend.end)
},
nextWeek: {
start: formatDate(week.start),
end: formatDate(week.end)
},
nextMonth: {
start: formatDate(month.start),
end: formatDate(month.end)
}
};
};
export const dateRangeUtils = {
getCurrentDate,
getThisWeekend,
getNextWeekRange,
getNextMonthRange,
formatDate,
getAllFormattedRanges
};
export default dateRangeUtils;

View File

@@ -20,6 +20,7 @@ export const getCurrentLocation = (): Promise<LocationInfo> => {
Taro.getLocation({
type: 'wgs84',
success: (res) => {
console.log('===获取地理位置', res)
// 使用逆地理编码获取地址信息
reverseGeocode(res.latitude, res.longitude)
.then(address => {

View File

@@ -13,13 +13,18 @@ export interface TennisMatch {
shinei: string
}
export interface IFilterOptions {
dateRange: [], // 日期区间
dateRange: [Date, Date], // 日期区间
dateRangeQuick?: string, // 日期快捷选择
timeSlot?: string, // 时间段
ntrp?: [number, number], // NTRP 水平区间
venueType?: string, // 场地类型
playType?: string, // 玩法
distanceFilter?: string
}
export interface ListState {
currentPage: string
isSearchResult: boolean
searchResultData: TennisMatch[]
matches: TennisMatch[]
recommendList: TennisMatch[]
location: {
@@ -32,7 +37,7 @@ export interface ListState {
isShowFilterPopup: boolean
filterOptions: IFilterOptions
filterCount: number
distance: string | number
// distance: string | number
quickFilter: string | number
distanceData: any[]
quickFilterData: any[]
@@ -41,12 +46,11 @@ export interface ListState {
quick: string
}
timeBubbleData: BubbleOption[]
locationOptions: BubbleOption[]
gamePlayOptions: BubbleOption[]
dateRangeOptions: BubbleOption[]
gamesNum: number
isScrollTop: boolean
searchHistoryParams: Record<string, any>
searchHistory: string[]
searchHistory: {id: number, title: string}[]
suggestionList: string[]
isShowSuggestion: boolean
isShowInputCustomerNavBar: boolean
@@ -60,13 +64,9 @@ export interface ListState {
}
export interface ListActions {
fetchMatches: (params?: {
page?: number
pageSize?: number
location?: string
skillLevel?: string
}) => Promise<void>
// refreshMatches: () => Promise<void>
fetchMatches: (params?: Record<string, any>) => Promise<void>
// getIntegrateListData: (params?: Record<string, any>) => Promise<void>
getMatchesData: () => void
clearError: () => void
updateState: (payload: Record<string, any>) => void
updateFilterOptions: (payload: Record<string, any>) => void
@@ -75,6 +75,16 @@ export interface ListActions {
clearHistory: () => void
searchSuggestion: (val: string) => Promise<void>
getSearchParams: () => Record<string, any>
loadMoreMatches: () => void
initialFilterSearch: () => void
setListData: (payload: IPayload) => void
}
export interface IPayload {
error: string;
data: TennisMatch[];
loading: boolean;
gamesNum: number;
}
// 快捷筛选
@@ -143,27 +153,32 @@ export interface BubbleItemProps {
export interface FilterPopupProps {
onCancel: () => void;
onConfirm: () => void;
onChange: (params: Record<string, string>) => void;
onChange: (params: Record<string, any>) => void;
loading: boolean;
filterOptions: Record<string, any>;
onClear: () => void;
visible: boolean;
onClose: () => void;
statusNavbarHeigh: number
}
// 列表卡片
export interface ListCardProps {
id: number;
title: string;
dateTime: string;
location: string;
distance: string;
registeredCount: number;
maxCount: number;
skillLevel: string;
id: string | number,
title: string,
start_time: string,
location: string,
distance_km: number,
current_players: number,
max_players: number,
skill_level_min: number,
skill_level_max: number,
play_type: string,
image_list: string[],
court_type: string,
matchType: string;
images: string[];
shinei: string;
showSkeleton?: boolean;
key?: string
}