Merge branch 'feature/juguohong/20250816'

This commit is contained in:
juguohong
2025-08-31 20:26:06 +08:00
28 changed files with 921 additions and 207 deletions

View File

@@ -1,8 +1,9 @@
export default defineAppConfig({ export default defineAppConfig({
pages: [ pages: [
'pages/list/index',
'pages/publishBall/index', 'pages/publishBall/index',
'pages/list/index', // 列表页
'pages/search/index', // 搜索页
'pages/searchResult/index', // 搜索结果页面
// 'pages/userInfo/myself/index', // 'pages/userInfo/myself/index',
'pages/login/index/index', 'pages/login/index/index',
'pages/login/verification/index', 'pages/login/verification/index',

View File

@@ -3,48 +3,4 @@
top: 0; top: 0;
z-index: 999; z-index: 999;
background-color: #ffffff; background-color: #ffffff;
.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

@@ -1,73 +1,23 @@
import { View, Text, Image } from "@tarojs/components"; import { View } from "@tarojs/components";
import img from "@/config/images";
import { getCurrentLocation } from "@/utils/locationUtils";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
import { useEffect } from "react";
import { useGlobalState } from "@/store/global"; import { useGlobalState } from "@/store/global";
import { useListState } from "@/store/listStore";
const ListHeader = () => { interface IProps {
const { children: any;
updateState, }
location,
getLocationText,
getLocationLoading,
statusNavbarHeightInfo,
} = useGlobalState();
const { gamesNum } = useListState();
console.log("===statusNavbarHeightInfo", statusNavbarHeightInfo);
const { statusBarHeight, navbarHeight, totalHeight } = statusNavbarHeightInfo;
// 获取位置信息 const CustomNavbar = (props: IProps) => {
const getCurrentLocal = () => { const { children } = props;
updateState({ const { statusNavbarHeightInfo } = useGlobalState();
getLocationLoading: true, const { totalHeight } = statusNavbarHeightInfo;
});
getCurrentLocation().then((res) => {
updateState({
getLocationLoading: false,
location: res || {},
});
});
};
useEffect(() => {
// getNavbarHeightInfo();
getCurrentLocal();
}, []);
const currentAddress = getLocationLoading
? getLocationText
: location?.address;
return ( return (
<View <View
className={styles.customerNavbar} className={styles.customerNavbar}
style={{ height: `${totalHeight}px` }} style={{ height: `${totalHeight}px` }}
> >
<View {children}
className={styles.container}
style={{
height: `${navbarHeight}px`,
paddingTop: `${statusBarHeight}px`,
}}
>
{/* logo */}
<Image src={img.ICON_LOGO} className={styles.logo} />
<View className={styles.line} />
<View className={styles.content}>
<View className={styles.cityWrapper}>
{/* 位置 */}
<Text className={styles.city}>{currentAddress}</Text>
{!getLocationLoading && (
<Image src={img.ICON_CHANGE} className={styles.change} />
)}
</View>
<View className={styles.infoWrapper}>
<Text className={styles.info}>${gamesNum}</Text>
</View>
</View>
</View>
</View> </View>
); );
}; };
export default ListHeader; export default CustomNavbar;

View File

@@ -0,0 +1,50 @@
.customerNavbarBack {
position: sticky;
top: 0;
z-index: 999;
background-color: #ffffff;
.container {
padding-left: 17px;
display: flex;
align-items: center;
gap: 8px;
}
.line {
width: 1px;
height: 25px;
background-color: #0000000F;
}
.back {
width: 32px;
height: 32px;
}
.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,32 @@
import { View, Image } from "@tarojs/components";
import img from "@/config/images";
import styles from "./index.module.scss";
import { useGlobalState } from "@/store/global";
import Taro from "@tarojs/taro";
const ListHeader = () => {
const { statusNavbarHeightInfo } = useGlobalState();
const { statusBarHeight, navbarHeight, totalHeight } = statusNavbarHeightInfo;
const handleBack = () => {
Taro.navigateBack();
}
return (
<View
className={styles.customerNavbarBack}
style={{ height: `${totalHeight}px` }}
>
<View
className={styles.container}
style={{
height: `${navbarHeight}px`,
paddingTop: `${statusBarHeight}px`,
}}
>
{/* back */}
<Image src={img.ICON_LIST_SEARCH_BACK} className={styles.back} onClick={handleBack} />
</View>
</View>
);
};
export default ListHeader;

View File

@@ -8,10 +8,12 @@ interface IProps {
isSelect: boolean; isSelect: boolean;
filterCount: number; filterCount: number;
onChange: (value: string) => void; onChange: (value: string) => void;
value: string;
onInputClick: () => void;
} }
const SearchBarComponent = (props: IProps) => { const SearchBarComponent = (props: IProps) => {
const { handleFilterIcon, isSelect, filterCount, onChange } = props; const { handleFilterIcon, isSelect, filterCount, onChange, value, onInputClick } = props;
const handleChange = (value: string) => { const handleChange = (value: string) => {
onChange && onChange(value); onChange && onChange(value);
@@ -19,6 +21,7 @@ const SearchBarComponent = (props: IProps) => {
return ( return (
<> <>
<SearchBar <SearchBar
clearable={false}
leftIn={ leftIn={
<View className={styles.searchBarLeft}> <View className={styles.searchBarLeft}>
<Image className={styles.searchIcon} src={img.ICON_SEARCH} /> <Image className={styles.searchIcon} src={img.ICON_SEARCH} />
@@ -43,6 +46,8 @@ const SearchBarComponent = (props: IProps) => {
className={styles.searchBar} className={styles.searchBar}
placeholder="搜索上海的球局和场地" placeholder="搜索上海的球局和场地"
onChange={handleChange} onChange={handleChange}
value={value}
onInputClick={onInputClick}
/> />
</> </>
); );

View File

@@ -50,4 +50,10 @@ export default {
ICON_LIST_PLAYING_GAME: require('@/static/list/icon-paying-game.svg'), ICON_LIST_PLAYING_GAME: require('@/static/list/icon-paying-game.svg'),
ICON_LIST_LOAD_ERROR: require('@/static/list/icon-load-error.svg'), ICON_LIST_LOAD_ERROR: require('@/static/list/icon-load-error.svg'),
ICON_LIST_RELOAD: require('@/static/list/icon-reload.svg'), ICON_LIST_RELOAD: require('@/static/list/icon-reload.svg'),
ICON_LIST_SEARCH_SEARCH: require('@/static/search/icon-search.svg'),
ICON_LIST_SEARCH_BACK: require('@/static/search/icon-back.svg'),
ICON_LIST_SEARCH_CLEAR: require('@/static/search/icon-search-clear.svg'),
ICON_LIST_SEARCH_CLEAR_HISTORY: require('@/static/search/icon-clear-history.svg'),
ICON_LIST_SEARCH_SUGGESTION: require('@/static/search/icon-search-suggestion.svg'),
ICON_LIST_INPUT_LOGO: require('@/static/list/icon-input-logo.svg'),
} }

View File

@@ -0,0 +1,47 @@
.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: 73%;
height: max-content;
padding-top: 5px;
}
.searchContainer {
width: 100%;
display: flex;
align-items: center;
gap: 5.85px;
padding: 7.8px;
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;
}
}
}

View File

@@ -0,0 +1,67 @@
import { View, Image } from "@tarojs/components";
import img from "@/config/images";
import { getCurrentLocation } from "@/utils/locationUtils";
import "./index.scss";
import { useEffect } from "react";
import { useGlobalState } from "@/store/global";
import { useListState } from "@/store/listStore";
import CustomNavbar from "@/components/CustomNavbar";
import { Input } from "@nutui/nutui-react-taro";
interface IProps {
icon: string;
}
const ListHeader = (props: IProps) => {
const { icon } = props;
const { updateState, statusNavbarHeightInfo } = useGlobalState();
const { searchValue } = useListState();
const { statusBarHeight, navbarHeight } = statusNavbarHeightInfo;
// 获取位置信息
const getCurrentLocal = () => {
updateState({
getLocationLoading: true,
});
getCurrentLocation().then((res) => {
updateState({
getLocationLoading: false,
location: res || {},
});
});
};
useEffect(() => {
getCurrentLocal();
}, []);
return (
<CustomNavbar>
<View
className="inputCustomerNavbarContainer"
style={{
height: `${navbarHeight}px`,
paddingTop: `${statusBarHeight}px`,
}}
>
<View className="navContent">
{/* logo */}
<Image src={icon} className="logo" />
{/* 搜索框 */}
<View className="searchContainer">
<Image
className="searchIcon icon16"
src={img.ICON_LIST_SEARCH_SEARCH}
/>
<Input
// className="inputSearch"
placeholder="搜索上海的球局和场地"
clearable={false}
value={searchValue}
/>
</View>
</View>
</View>
</CustomNavbar>
);
};
export default ListHeader;

View File

@@ -2,12 +2,9 @@ import { View } from "@tarojs/components";
import ListCard from "@/components/ListCard"; import ListCard from "@/components/ListCard";
import ListLoadError from "@/components/ListLoadError"; import ListLoadError from "@/components/ListLoadError";
import ListCardSkeleton from "@/components/ListCardSkeleton"; import ListCardSkeleton from "@/components/ListCardSkeleton";
// import { useGlobalState } from "@/store/global";
const ListContainer = (props) => { const ListContainer = (props) => {
const { loading, data = [], error, reload } = props; const { loading, data = [], error, reload } = props;
// const { statusNavbarHeightInfo } = useGlobalState() || {};
// const { totalHeight } = statusNavbarHeightInfo;
const renderList = () => { const renderList = () => {
if (loading && data.length > 0) { if (loading && data.length > 0) {

View File

@@ -0,0 +1,43 @@
.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,69 @@
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'
const ListHeader = () => {
const {
updateState,
location,
getLocationText,
getLocationLoading,
statusNavbarHeightInfo,
} = useGlobalState();
const { gamesNum } = 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;
return (
<CustomNavbar>
<View
className={styles.container}
style={{
height: `${navbarHeight}px`,
paddingTop: `${statusBarHeight}px`,
}}
>
{/* logo */}
<Image src={img.ICON_LOGO} className={styles.logo} />
<View className={styles.line} />
<View className={styles.content}>
<View className={styles.cityWrapper}>
{/* 位置 */}
<Text className={styles.city}>{currentAddress}</Text>
{!getLocationLoading && (
<Image src={img.ICON_CHANGE} className={styles.change} />
)}
</View>
<View className={styles.infoWrapper}>
<Text className={styles.info}>${gamesNum}</Text>
</View>
</View>
</View>
</CustomNavbar>
);
};
export default ListHeader;

View File

@@ -3,14 +3,14 @@
.listTopSearchWrapper { .listTopSearchWrapper {
padding: 0 15px; padding: 0 15px;
position: sticky; // position: sticky;
background: #fefefe; background: #fefefe;
z-index: 999; // z-index: 999;
} }
.isScroll { // .isScroll {
border-bottom: 0.5px solid #0000000F; // border-bottom: 0.5px solid #0000000F;
} // }
.listTopFilterWrapper { .listTopFilterWrapper {
display: flex; display: flex;

View File

@@ -8,15 +8,18 @@ import Taro, { usePageScroll, useReachBottom } 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 } from "@tarojs/components"; import { View } from "@tarojs/components";
import CustomerNavBar from "@/components/CustomNavbar"; import CustomerNavBar from "@/container/listCustomNavbar";
import InputCustomerBar from "@/container/inputCustomerNavbar";
import GuideBar from "@/components/GuideBar"; import GuideBar from "@/components/GuideBar";
import ListContainer from "@/container/listContainer"; import ListContainer from "@/container/listContainer";
import img from "@/config/images";
const ListPage = () => { const ListPage = () => {
// 从 store 获取数据和方法 // 从 store 获取数据和方法
const store = useListStore() || {}; const store = useListStore() || {};
const { statusNavbarHeightInfo } = useGlobalState() || {}; const { statusNavbarHeightInfo } = useGlobalState() || {};
const { totalHeight } = statusNavbarHeightInfo || {};
const { const {
isShowFilterPopup, isShowFilterPopup,
error, error,
@@ -33,11 +36,18 @@ const ListPage = () => {
quickFilterData, quickFilterData,
distanceQuickFilter, distanceQuickFilter,
isScrollTop, isScrollTop,
searchValue,
isShowInputCustomerNavBar,
} = store; } = store;
usePageScroll((res) => { usePageScroll((res) => {
if (res?.scrollTop > 0 && !isScrollTop) { // if (res?.scrollTop > 0 && !isScrollTop) {
updateState({ isScrollTop: true }); // updateState({ isScrollTop: true });
// }
if (res?.scrollTop >= totalHeight && !isScrollTop) {
updateState({ isShowInputCustomerNavBar: true });
} else {
updateState({ isShowInputCustomerNavBar: false });
} }
}); });
@@ -105,28 +115,40 @@ const ListPage = () => {
}); });
}; };
const handleSearchClick = () => {
Taro.navigateTo({
url: "/pages/search/index",
});
}
return ( return (
<> <>
{!isShowInputCustomerNavBar ? (
<CustomerNavBar /> <CustomerNavBar />
) : (
<InputCustomerBar icon={img.ICON_LIST_INPUT_LOGO} />
)}
<View className={styles.listPage}> <View className={styles.listPage}>
<View <View
className={`${styles.listTopSearchWrapper} ${ className={`${styles.listTopSearchWrapper} ${
isScrollTop ? styles.isScroll : "" isScrollTop ? styles.isScroll : ""
}`} }`}
style={{ // style={{
top: statusNavbarHeightInfo?.totalHeight, // top: statusNavbarHeightInfo?.totalHeight,
}} // }}
> >
<SearchBar <SearchBar
handleFilterIcon={toggleShowPopup} handleFilterIcon={toggleShowPopup}
isSelect={filterCount > 0} isSelect={filterCount > 0}
filterCount={filterCount} filterCount={filterCount}
onChange={handleSearchChange} onChange={handleSearchChange}
value={searchValue}
onInputClick={handleSearchClick}
/> />
{/* 综合筛选 */} {/* 综合筛选 */}
{isShowFilterPopup && ( {isShowFilterPopup && (
<div> <View>
<FilterPopup <FilterPopup
loading={loading} loading={loading}
onCancel={toggleShowPopup} onCancel={toggleShowPopup}
@@ -138,7 +160,7 @@ const ListPage = () => {
onClose={toggleShowPopup} onClose={toggleShowPopup}
statusNavbarHeigh={statusNavbarHeightInfo?.totalHeight} statusNavbarHeigh={statusNavbarHeightInfo?.totalHeight}
/> />
</div> </View>
)} )}
{/* 筛选 */} {/* 筛选 */}
<div className={styles.listTopFilterWrapper}> <div className={styles.listTopFilterWrapper}>

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '',
navigationStyle: 'custom',
})

120
src/pages/search/index.scss Normal file
View File

@@ -0,0 +1,120 @@
.listSearchContainer {
padding: 0 15px;
.icon16 {
width: 16px;
height: 16px;
}
.topSearch {
padding: 10px 16px 5px 12px;
display: flex;
align-items: center;
height: 44px;
box-sizing: border-box;
gap: 10px;
border-radius: 44px;
border: 0.5px solid rgba(0, 0, 0, 0.06);
background: #FFF;
box-shadow: 0 4px 48px 0 rgba(0, 0, 0, 0.08);
.nut-input {
padding: 0;
height: 100%;
}
}
.searchRight {
display: flex;
align-items: center;
gap: 12px;
.searchLine {
width: 1px;
height: 20px;
border-radius: 20px;
background: rgba(0, 0, 0, 0.06);
}
.searchText {
color: #000000;
font-size: 16px;
font-weight: 600;
line-height: 20px
}
}
.searchIcon {
width: 20px;
height: 20px;
}
.historySearchTitleWrapper {
display: flex;
padding: 12px 15px;
justify-content: space-between;
align-items: flex-end;
align-self: stretch;
.historySearchTitle,
.historySearchClear {
color: #000;
font-size: 14px;
font-weight: 600;
line-height: 20px;
}
.historySearchClear {
color: #9a9a9a;
display: flex;
align-items: center;
gap: 4px;
}
}
.historySearchList {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
.historySearchItem {
flex-shrink: 0;
flex-grow: 0;
display: flex;
height: 28px;
padding: 4px 12px;
justify-content: center;
align-items: center;
gap: 2px;
border-radius: 999px;
border: 0.5px solid rgba(0, 0, 0, 0.06);
background: rgba(0, 0, 0, 0.03);
}
}
.searchSuggestion {
padding: 6px 0;
.searchSuggestionItem {
padding: 10px 20px;
display: flex;
align-items: center;
justify-content: space-between;
.searchSuggestionItemLeft {
display: flex;
align-items: center;
gap: 12px;
color: rgba(60, 60, 67, 0.60);
font-size: 14px;
font-weight: 400;
line-height: 20px;
}
.highlight {
color: #000000;
}
}
}
}

201
src/pages/search/index.tsx Normal file
View File

@@ -0,0 +1,201 @@
import InputCustomerBar from "@/container/inputCustomerNavbar";
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";
import { useListState } from "@/store/listStore";
import img from "@/config/images";
import "./index.scss";
import Taro from "@tarojs/taro";
const ListSearch = () => {
const {
searchValue,
updateState,
getSearchHistory,
searchHistory = [],
clearHistory,
searchSuggestion,
suggestionList,
isShowSuggestion,
isShowInputCustomerNavBar,
} = useListState() || {};
const ref = useRef<any>(null);
useEffect(() => {
getSearchHistory();
}, []);
useEffect(() => {
if (ref?.current) {
ref.current.focus();
}
}, [ref.current]);
const regex = useMemo(() => {
return new RegExp(searchValue, "gi");
}, [searchValue]);
/**
* @description 输入
* @param value
*/
const handleChange = (value: string) => {
updateState({ searchValue: value });
if (value) {
searchSuggestion(value);
}
};
/**
* @description 点击清空输入内容
*/
const handleClear = () => {
updateState({ searchValue: "" });
};
/**
* @description 点击历史搜索
* @param value
*/
const handleHistoryClick = (value: string) => {
updateState({ searchValue: value });
handleSearch();
};
/**
* @description 清空历史搜索
*/
const handleClearHistory = () => {
clearHistory();
};
/**
* @description 点击联想词
*/
const handleSuggestionSearch = (val: string) => {
updateState({
searchValue: val,
isShowSuggestion: false,
});
handleSearch();
};
/**
* @description 点击搜索
*/
const handleSearch = () => {
Taro.navigateTo({
url: `/pages/searchResult/index`,
});
}
// 是否显示清空图标
const isShowClearIcon = searchValue && searchValue?.length > 0;
// 是否显示搜索历史
const isShowHistory =
!isShowClearIcon && searchHistory && searchHistory?.length > 0;
return (
<>
{!isShowInputCustomerNavBar ? (
<CustomerNavbarBack />
) : (
<InputCustomerBar icon={img.ICON_LIST_INPUT_LOGO} />
)}
<View className="listSearchContainer">
{/* 搜索 */}
<View className="topSearch">
<Image className="searchIcon" src={img.ICON_LIST_SEARCH_SEARCH} />
<Input
placeholder="搜索上海的球局和场地"
value={searchValue}
defaultValue={searchValue}
onChange={handleChange}
onClear={handleClear}
autoFocus
clearable={false}
ref={ref}
/>
<View className="searchRight">
{isShowClearIcon && (
<Image
className="clearIcon icon16"
src={img.ICON_LIST_SEARCH_CLEAR}
onClick={handleClear}
/>
)}
<View className="searchLine" />
<Text className="searchText" onClick={handleSearch}></Text>
</View>
</View>
{/* 联想词 */}
{isShowSuggestion && (
<View className="searchSuggestion">
{(suggestionList || [])?.map((item) => {
// 替换匹配文本为高亮版本
const highlightedText = item.replace(regex, (match) => {
// 如果匹配不到,则返回原文本
if (!match) return match;
return `<Text class="highlight">${match}</Text>`;
});
return (
<View
className="searchSuggestionItem"
onClick={() => handleSuggestionSearch(item)}
>
<View className="searchSuggestionItemLeft">
<Image
className="icon16"
src={img.ICON_LIST_SEARCH_SEARCH}
/>
<Text
dangerouslySetInnerHTML={{ __html: highlightedText }}
></Text>
</View>
<Image
className="icon16"
src={img.ICON_LIST_SEARCH_SUGGESTION}
/>
</View>
);
})}
</View>
)}
{/* 历史搜索 */}
{!isShowClearIcon && (
<View className="historySearch">
<View className="historySearchTitleWrapper">
<View className="historySearchTitle"></View>
<View className="historySearchClear" onClick={handleClearHistory}>
<Text></Text>
<Image
className="clearIcon icon16"
src={img.ICON_LIST_SEARCH_CLEAR_HISTORY}
/>
</View>
</View>
{isShowHistory && (
<View className="historySearchList">
{(searchHistory || [])?.map((item) => {
return (
<Text
className="historySearchItem"
onClick={() => handleHistoryClick(item)}
>
{item}
</Text>
);
})}
</View>
)}
</View>
)}
</View>
</>
);
};
export default ListSearch;

View File

@@ -0,0 +1,3 @@
.menuFilter {
color: red;
}

View File

@@ -0,0 +1,65 @@
import { View } from "@tarojs/components";
// import styles from "./index.scss";
import CityFilter from "@/components/CityFilter";
import Menu from "@/components/Menu";
import { useListState } from "@/store/listStore";
import ListContainer from "@/container/listContainer";
import "./index.scss";
const SearchResult = () => {
const {
distanceData,
quickFilterData,
distanceQuickFilter,
updateState,
matches,
loading,
error,
refreshMatches,
} = useListState() || {};
// 距离筛选
const handleDistanceOrQuickChange = (name, value) => {
updateState({
distanceQuickFilter: {
...distanceQuickFilter,
[name]: value,
},
});
};
return (
<View>
{/* 筛选 */}
<View className='searchResultFilterWrapper'>
{/* 全城筛选 */}
<CityFilter
options={distanceData}
value={distanceQuickFilter?.distance}
wrapperClassName='menuFilter'
onChange={handleDistanceOrQuickChange}
name="distance"
/>
{/* 智能排序 */}
<Menu
options={quickFilterData}
value={distanceQuickFilter?.quick}
onChange={handleDistanceOrQuickChange}
wrapperClassName='menuFilter'
name="quick"
/>
</View>
{/* 列表内容 */}
<ListContainer
data={matches}
loading={loading}
error={error}
reload={refreshMatches}
/>
</View>
);
};
export default SearchResult;

View File

@@ -1,4 +1,3 @@
import { TennisMatch } from "../store/listStore";
import httpService from "./httpService"; import httpService from "./httpService";
// 模拟网络延迟 // 模拟网络延迟
@@ -12,57 +11,6 @@ interface ApiResponse<T> {
timestamp: number; timestamp: number;
} }
// 模拟网球比赛数据
const mockTennisMatches: TennisMatch[] = [
{
id: "1",
title: "周一晚场浦东新区单打约球",
dateTime: "明天(周五)下午5点 2小时",
location: "仁恒河滨花园网球场",
distance: "3.5km",
shinei: "室内",
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",
"https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=200&h=200&fit=crop&crop=center",
"https://images.unsplash.com/photo-1551698618-1dfe5d97d256?w=200&h=200&fit=crop&crop=center",
],
},
{
id: "2",
title: "浦东新区单打约球",
dateTime: "明天(周五)下午5点 2小时",
location: "仁恒河滨花园网球场",
distance: "3.5km",
shinei: "室外",
registeredCount: 2,
maxCount: 4,
skillLevel: "2.0 至 2.5",
matchType: "双打",
images: [
"https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=200&h=200&fit=crop&crop=center",
"https://images.unsplash.com/photo-1551698618-1dfe5d97d256?w=200&h=200&fit=crop&crop=center",
],
},
{
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",
],
},
];
/** /**
* 获取网球比赛列表 * 获取网球比赛列表
* @param params 查询参数 * @param params 查询参数
@@ -86,10 +34,10 @@ export const getTennisMatches = async (params?: {
* 刷新网球比赛数据 * 刷新网球比赛数据
* @returns Promise<TennisMatch[]> * @returns Promise<TennisMatch[]>
*/ */
export const refreshTennisMatches = async (): Promise<TennisMatch[]> => { export const refreshTennisMatches = async (params) => {
try { try {
// 生成新的动态数据 // 生成新的动态数据
const matches = generateDynamicData(); const matches = generateDynamicData(params);
return matches; return matches;
} catch (error) { } catch (error) {
console.error("API刷新失败:", error); console.error("API刷新失败:", error);
@@ -97,3 +45,52 @@ export const refreshTennisMatches = async (): Promise<TennisMatch[]> => {
} }
}; };
/**
* 获取搜索历史记录的异步函数
* @param {Object} params - 查询参数对象
* @returns {Promise} - 返回一个Promise对象包含获取到的搜索历史数据
*/
export const getSearchHistory = async (params) => {
try {
// 调用HTTP服务获取搜索历史记录
return httpService.get('/games/search_history', params)
} catch (error) {
// 捕获并打印错误信息
console.error("历史记录获取失败:", error);
// 抛出错误以便上层处理
throw error;
}
}
/**
* @description 清除搜索历史
* @returns
*/
export const clearHistory = async () => {
try {
// 调用HTTP服务清除搜索历史记录
return httpService.post('/games/clear_history')
} catch (error) {
// 捕获并打印错误信息
console.error("清除历史记录失败:", error);
// 抛出错误以便上层处理
throw error;
}
}
/**
* @description 获取联想
* @param params 查询参数
* @returns
*/
export const searchSuggestion = async (params) => {
try {
// 调用HTTP服务获取搜索建议
return httpService.get('/games/search_suggestion', params)
} catch (error) {
// 捕获并打印错误信息
console.error("搜索建议获取失败:", error);
// 抛出错误以便上层处理
throw error;
}
}

View File

@@ -0,0 +1,3 @@
<svg width="27" height="13" viewBox="0 0 27 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.894158 7.15855V5.83157C0.894158 2.78355 2.80822 0.829274 5.87233 0.829274C7.08671 0.829274 8.21263 1.15096 9.03295 1.67371C9.83717 2.17233 10.3519 2.85593 10.3519 3.55561C10.3519 4.2955 9.82913 4.8102 9.0812 4.8102C8.75951 4.8102 8.49411 4.71369 8.26089 4.55285C7.60946 4.11857 7.09476 3.48323 6.12164 3.48323C4.74641 3.48323 4.07086 4.31962 4.07086 6.00046V7.15051C4.07086 8.80721 4.74641 9.68382 6.04122 9.68382C7.00629 9.68382 7.65772 9.12086 7.65772 8.30859V8.08341H7.16714C6.37095 8.08341 5.93667 7.69738 5.93667 6.9977C5.93667 6.29802 6.36291 5.93612 7.16714 5.93612H8.91231C10.1187 5.93612 10.6414 6.44278 10.6414 7.59283V8.02711C10.6414 10.5765 8.77559 12.2413 5.9045 12.2413C2.75192 12.2413 0.894158 10.3352 0.894158 7.15855ZM11.9251 7.15051V5.96025C11.9251 2.75138 13.7668 0.829274 16.9596 0.829274C20.1523 0.829274 21.986 2.75942 21.986 5.96025V7.15051C21.986 10.3352 20.1523 12.2413 16.9596 12.2413C13.7668 12.2413 11.9251 10.3272 11.9251 7.15051ZM15.1018 5.93612V7.14246C15.1018 8.75896 15.7854 9.69187 16.9596 9.69187C18.1337 9.69187 18.8093 8.75896 18.8093 7.14246V5.93612C18.8093 4.31962 18.1337 3.37868 16.9596 3.37868C15.7854 3.37868 15.1018 4.31962 15.1018 5.93612ZM23.8165 7.39982L23.5833 2.24471C23.5431 1.35202 24.2267 0.620174 25.1998 0.620174C26.1729 0.620174 26.8726 1.35202 26.8244 2.24471L26.567 7.39982C26.5188 8.17992 25.9317 8.69462 25.1918 8.69462C24.4358 8.69462 23.8487 8.14775 23.8165 7.39982ZM23.5994 10.7454C23.5994 9.8688 24.3232 9.32996 25.2079 9.32996C26.0845 9.32996 26.8002 9.8688 26.8002 10.7454C26.8002 11.6301 26.0845 12.1689 25.2079 12.1689C24.3232 12.1689 23.5994 11.6301 23.5994 10.7454Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
<path d="M20.6667 24L12.6667 16L20.6667 8" stroke="black" stroke-width="2.66667" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 234 B

View File

@@ -0,0 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.66669 1.97144H9.33335V4.63812H14.3334V7.30478H1.66669V4.63812H6.66669V1.97144Z" stroke="#9A9A9A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.66669 13.3333H13.3334V7.33325H2.66669V13.3333Z" stroke="#9A9A9A" stroke-width="1.33333" stroke-linejoin="round"/>
<path d="M5.33331 13.2992V11.3047" stroke="#9A9A9A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 13.2993V11.2993" stroke="#9A9A9A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.6667 13.2992V11.3047" stroke="#9A9A9A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 13.3333H12" stroke="#9A9A9A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 929 B

View File

@@ -0,0 +1,14 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3385_5398)">
<path d="M7.99998 14.6666C11.6819 14.6666 14.6666 11.6818 14.6666 7.99992C14.6666 4.31802 11.6819 1.33325 7.99998 1.33325C4.31808 1.33325 1.33331 4.31802 1.33331 7.99992C1.33331 11.6818 4.31808 14.6666 7.99998 14.6666Z" fill="#A0A0A0" stroke="#A0A0A0" stroke-width="1.33333" stroke-linejoin="round"/>
<path d="M9.88555 6.1145L6.11432 9.88574L9.88555 6.1145Z" fill="white"/>
<path d="M9.88555 6.1145L6.11432 9.88574" stroke="white" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.11444 6.1145L9.88567 9.88574L6.11444 6.1145Z" fill="white"/>
<path d="M6.11444 6.1145L9.88567 9.88574" stroke="white" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_3385_5398">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 954 B

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.33337 3.66675H12.3334V9.66675" stroke="#9A9A9A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.84802 12.152L12.3333 3.66675" stroke="#9A9A9A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 366 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M8.75002 15.8334C12.662 15.8334 15.8334 12.6621 15.8334 8.75008C15.8334 4.83808 12.662 1.66675 8.75002 1.66675C4.83802 1.66675 1.66669 4.83808 1.66669 8.75008C1.66669 12.6621 4.83802 15.8334 8.75002 15.8334Z" stroke="black" stroke-width="1.66667" stroke-linejoin="round"/>
<path d="M13.8423 13.8423L17.3779 17.3778" stroke="black" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 518 B

View File

@@ -1,5 +1,5 @@
import { create } from 'zustand' import { create } from 'zustand'
import { getTennisMatches } from '../services/listApi' import { getTennisMatches, getSearchHistory, clearHistory, searchSuggestion } from '../services/listApi'
import { ListActions, IFilterOptions, ListState } from '../../types/list/types' import { ListActions, IFilterOptions, ListState } from '../../types/list/types'
// 完整的 Store 类型 // 完整的 Store 类型
@@ -21,7 +21,8 @@ export const useListStore = create<TennisStore>()((set, get) => ({
matches: [], matches: [],
loading: false, loading: false,
error: null, error: null,
lastRefreshTime: null, // 搜索的value
searchValue: '',
// 是否展示综合筛选弹窗 // 是否展示综合筛选弹窗
isShowFilterPopup: false, isShowFilterPopup: false,
// 综合筛选项 // 综合筛选项
@@ -77,6 +78,19 @@ export const useListStore = create<TennisStore>()((set, get) => ({
gamesNum: 124, gamesNum: 124,
// 页面滚动距离顶部距离 是否大于0 // 页面滚动距离顶部距离 是否大于0
isScrollTop: false, isScrollTop: false,
// 搜索历史数据
searchHistory: ['上海', '黄浦', '上海', '静安', '徐汇', '黄浦', '普陀', '黄浦', '长宁', '黄浦'],
// 搜索历史数据默认 Top 15
searchHistoryParams: {
page: 1,
pageSize: 15,
},
// 联想词
suggestionList: [],
// 是否显示联想词
isShowSuggestion: false,
// 是否显示搜索框自定义导航
isShowInputCustomerNavBar: false,
// 获取比赛数据 // 获取比赛数据
fetchMatches: async (params) => { fetchMatches: async (params) => {
@@ -135,12 +149,51 @@ export const useListStore = create<TennisStore>()((set, get) => ({
set({ set({
matches: rows, matches: rows,
loading: false, loading: false,
lastRefreshTime: new Date().toISOString()
}) })
} catch (error) { } 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: () => { clearError: () => {
set({ error: null }) set({ error: null })

View File

@@ -23,7 +23,7 @@ export interface ListState {
matches: TennisMatch[] matches: TennisMatch[]
loading: boolean loading: boolean
error: string | null error: string | null
lastRefreshTime: string | null searchValue: string
isShowFilterPopup: boolean isShowFilterPopup: boolean
filterOptions: IFilterOptions filterOptions: IFilterOptions
filterCount: number filterCount: number
@@ -40,24 +40,11 @@ export interface ListState {
gamePlayOptions: BubbleOption[] gamePlayOptions: BubbleOption[]
gamesNum: number gamesNum: number
isScrollTop: boolean isScrollTop: boolean
} searchHistoryParams: Record<string, any>
searchHistory: string[]
export interface ListState { suggestionList: string[]
matches: TennisMatch[] isShowSuggestion: boolean
loading: boolean isShowInputCustomerNavBar: boolean
error: string | null
lastRefreshTime: string | null
isShowFilterPopup: boolean
filterOptions: IFilterOptions
filterCount: number
distance: string | number
quickFilter: string | number
distanceData: any[]
quickFilterData: any[]
distanceQuickFilter: {
distance: string
quick: string
}
} }
export interface ListActions { export interface ListActions {
@@ -72,6 +59,9 @@ export interface ListActions {
updateState: (payload: Record<string, any>) => void updateState: (payload: Record<string, any>) => void
updateFilterOptions: (payload: Record<string, any>) => void updateFilterOptions: (payload: Record<string, any>) => void
clearFilterOptions: () => void clearFilterOptions: () => void
getSearchHistory: () => Promise<void>
clearHistory: () => void
searchSuggestion: (val: string) => Promise<void>
} }
// 快捷筛选 // 快捷筛选