diff --git a/src/app.ts b/src/app.ts index 6544603..7ce6cd4 100644 --- a/src/app.ts +++ b/src/app.ts @@ -2,6 +2,9 @@ import { Component, ReactNode } from 'react' import './nutui-theme.scss' import './app.scss' import { useDictionaryStore } from './store/dictionaryStore' +import { useGlobalStore } from './store/global' + +// import { getNavbarHeight } from "@/utils/getNavbarHeight"; interface AppProps { children: ReactNode @@ -11,11 +14,12 @@ class App extends Component { componentDidMount() { // 初始化字典数据 this.initDictionaryData() + this.getNavBarHeight() } - componentDidShow() {} + componentDidShow() { } - componentDidHide() {} + componentDidHide() { } // 初始化字典数据 private async initDictionaryData() { @@ -27,6 +31,13 @@ class App extends Component { } } + // 获取导航高度 + getNavBarHeight = () => { + const { getNavbarHeightInfo } = useGlobalStore.getState() + getNavbarHeightInfo() + + } + render() { // this.props.children 是将要会渲染的页面 return this.props.children diff --git a/src/components/CustomNavbar/index.module.scss b/src/components/CustomNavbar/index.module.scss index b3a7de1..9c921b2 100644 --- a/src/components/CustomNavbar/index.module.scss +++ b/src/components/CustomNavbar/index.module.scss @@ -1,5 +1,8 @@ .customerNavbar { - // background-color: red; + position: sticky; + top: 0; + z-index: 999; + background-color: #ffffff; .container { padding-left: 17px; diff --git a/src/components/CustomNavbar/index.tsx b/src/components/CustomNavbar/index.tsx index 4d2e368..45f96fb 100644 --- a/src/components/CustomNavbar/index.tsx +++ b/src/components/CustomNavbar/index.tsx @@ -1,22 +1,22 @@ import { View, Text, Image } from "@tarojs/components"; import img from "@/config/images"; import { getCurrentLocation } from "@/utils/locationUtils"; -import { getNavbarHeight } from "@/utils/getNavbarHeight"; import styles from "./index.module.scss"; import { useEffect } from "react"; import { useGlobalState } from "@/store/global"; import { useListState } from "@/store/listStore"; const ListHeader = () => { - const { statusBarHeight, navbarHeight, totalHeight } = getNavbarHeight(); const { updateState, location, getLocationText, getLocationLoading, - getNavbarHeightInfo, + statusNavbarHeightInfo, } = useGlobalState(); const { gamesNum } = useListState(); + console.log("===statusNavbarHeightInfo", statusNavbarHeightInfo); + const { statusBarHeight, navbarHeight, totalHeight } = statusNavbarHeightInfo; // 获取位置信息 const getCurrentLocal = () => { @@ -31,7 +31,7 @@ const ListHeader = () => { }); }; useEffect(() => { - getNavbarHeightInfo(); + // getNavbarHeightInfo(); getCurrentLocal(); }, []); diff --git a/src/components/GuideBar/index.scss b/src/components/GuideBar/index.scss index 608a03a..7cd40b1 100644 --- a/src/components/GuideBar/index.scss +++ b/src/components/GuideBar/index.scss @@ -14,6 +14,7 @@ justify-content: space-between; align-items: center; padding: 20px 12px env(safe-area-inset-bottom); + z-index: 999; &-pages { display: flex; diff --git a/src/components/ListCard/index.scss b/src/components/ListCard/index.scss index 3cf2e16..bcb39aa 100644 --- a/src/components/ListCard/index.scss +++ b/src/components/ListCard/index.scss @@ -1,4 +1,10 @@ -.list-item { +.listCard { + background: linear-gradient(90deg, rgba(183, 248, 113, 0.5) 0%, rgba(183, 248, 113, 0.1) 100%); + border-radius: 20px; + border-width: 0.5px; +} + +.listItem { display: flex; padding: 12px 15px; background: #ffffff; @@ -247,4 +253,43 @@ width: 100%; height: 100%; object-fit: cover; +} + +// 底部 +.smoothPlayingGame { + padding: 5px 12px; + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + + .smoothWrapper, + .localAreaWrapper { + line-height: 18px; + display: flex; + align-items: center; + gap: 5px; + } + + .smoothTitle { + font-size: 14px; + } + + .line { + height: 8px; + width: 1px; + background: #00000040; + border-radius: 99px; + } + + .iconListPlayingGame, + .localArea { + width: 14px; + height: 14px; + } + + .localArea { + border: 0.5px solid #FFFFFFA6; + border-radius: 50%; + } } \ No newline at end of file diff --git a/src/components/ListCard/index.tsx b/src/components/ListCard/index.tsx index ffaca58..df80400 100644 --- a/src/components/ListCard/index.tsx +++ b/src/components/ListCard/index.tsx @@ -1,5 +1,5 @@ import { View, Text, Image } from "@tarojs/components"; -import Taro from '@tarojs/taro' +import Taro from "@tarojs/taro"; import img from "../../config/images"; import { ListCardProps } from "../../../types/list/types"; import "./index.scss"; @@ -14,7 +14,7 @@ const ListCard: React.FC = ({ maxCount, skillLevel, matchType, - images=[], + images = [], shinei, }) => { const renderItemImage = (src: string) => { @@ -23,9 +23,9 @@ const ListCard: React.FC = ({ const handleViewDetail = () => { Taro.navigateTo({ - url: `/pages/detail/index?id=${id || 1}&from=list&autoShare=0` - }) - } + url: `/pages/detail/index?id=${id || 1}&from=list&autoShare=0`, + }); + }; // 根据图片数量决定展示样式 const renderImages = () => { @@ -34,9 +34,7 @@ const ListCard: React.FC = ({ if (images?.length === 1) { return ( - - {renderItemImage(images[0])} - + {renderItemImage(images[0])} ); } @@ -44,12 +42,8 @@ const ListCard: React.FC = ({ if (images?.length === 2) { return ( - - {renderItemImage(images[0])} - - - {renderItemImage(images[1])} - + {renderItemImage(images[0])} + {renderItemImage(images[1])} ); } @@ -64,72 +58,87 @@ const ListCard: React.FC = ({ ); }; return ( - - {/* 左侧内容区域 */} - - {/* 标题 */} - - {title} - - - - {/* 时间信息 */} - - - {dateTime} - - - {/* 地点,室内外,距离 */} - - - {location} - - {shinei && `・${shinei}`} - {distance && `・${distance}`} - - - - {/* 底部信息行:头像组、报名人数、技能等级、比赛类型 */} - - - - {Array.from({ length: Math.min(registeredCount, 3) }).map( - (_, index) => ( - - - - ) - )} - + + + {/* 左侧内容区域 */} + + {/* 标题 */} + + {title} + - - - - 报名人数 {registeredCount}/ - {maxCount} - + {/* 时间信息 */} + + + {dateTime} + + + {/* 地点,室内外,距离 */} + + + {location} + + {shinei && `・${shinei}`} + {distance && `・${distance}`} + + + + {/* 底部信息行:头像组、报名人数、技能等级、比赛类型 */} + + + + {Array.from({ length: Math.min(registeredCount, 3) }).map( + (_, index) => ( + + + + ) + )} + - - {skillLevel} - - - {matchType} + + + + + 报名人数 {registeredCount}/ + {maxCount} + + + + {skillLevel} + + + {matchType} + + + {/* 右侧图片区域 */} + {renderImages()} + + {/* 畅打球局 */} + + + + 畅打球局 + + + 场馆方: + + + 仁恒河滨花园网球场 + - - {/* 右侧图片区域 */} - {renderImages()} ); }; diff --git a/src/components/ListCardSkeleton/index.tsx b/src/components/ListCardSkeleton/index.tsx index d19bf56..77441a0 100644 --- a/src/components/ListCardSkeleton/index.tsx +++ b/src/components/ListCardSkeleton/index.tsx @@ -13,7 +13,6 @@ const ListCard = () => { {/* 时间信息 */} - diff --git a/src/components/ListLoadError/index.module.scss b/src/components/ListLoadError/index.module.scss new file mode 100644 index 0000000..1629275 --- /dev/null +++ b/src/components/ListLoadError/index.module.scss @@ -0,0 +1,44 @@ +.listLoadError { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 400px; + + .listLoadErrorImg { + width: 154px; + height: 154px; + } + + .listLoadErrorText { + margin-top: 35px; + margin-bottom: 12px; + font-weight: 500; + font-style: Medium; + font-size: 14px; + line-height: 24px; + letter-spacing: 0px; + } + + .listLoadErrorBtn { + display: flex; + align-items: center; + justify-content: center; + width: 76px; + background: #00000008; + border: 0.5px solid #0000001F; + border-radius: 12px; + padding: 12px 0; + font-weight: 500; + font-style: Medium; + font-size: 14px; + line-height: 24px; + letter-spacing: 0px; + + } + + .reloadIcon { + width: 16px; + height: 16px; + } +} \ No newline at end of file diff --git a/src/components/ListLoadError/index.tsx b/src/components/ListLoadError/index.tsx new file mode 100644 index 0000000..420393d --- /dev/null +++ b/src/components/ListLoadError/index.tsx @@ -0,0 +1,24 @@ +import { Image, View, Text, Button } from "@tarojs/components"; +import styles from "./index.module.scss"; +import img from "@/config/images"; + +const ListLoadError = ({ reload }: { reload: () => void }) => { + const handleReload = () => { + reload && typeof reload === "function" && reload(); + }; + + return ( + + + 加载失败 + + + ); +}; +export default ListLoadError; diff --git a/src/components/SearchBar/index.module.scss b/src/components/SearchBar/index.module.scss index fc822f1..cf5e343 100644 --- a/src/components/SearchBar/index.module.scss +++ b/src/components/SearchBar/index.module.scss @@ -5,9 +5,16 @@ --nutui-searchbar-input-text-color: #000000; --nutui-searchbar-input-padding: 0 0 0 10px; --nutui-searchbar-padding: 10px 0 0 0; + :global(.nut-searchbar-content) { box-shadow: 0 4px 48px #00000014; } + + .searchBarLeft { + display: flex; + align-items: center; + } + .searchBarRight { position: relative; width: 44px; @@ -18,14 +25,17 @@ display: flex; align-items: center; justify-content: center; + &.active { background-color: #000000; } } + .filterIcon { width: 20px; height: 20px; } + .filterCount { background-color: #000000; position: absolute; @@ -41,8 +51,9 @@ justify-content: center; font-size: 11px; } + .searchIcon { width: 20px; height: 20px; } -} +} \ No newline at end of file diff --git a/src/components/SearchBar/index.tsx b/src/components/SearchBar/index.tsx index e4f1fab..8181030 100644 --- a/src/components/SearchBar/index.tsx +++ b/src/components/SearchBar/index.tsx @@ -12,6 +12,7 @@ interface IProps { const SearchBarComponent = (props: IProps) => { const { handleFilterIcon, isSelect, filterCount, onChange } = props; + const handleChange = (value: string) => { onChange && onChange(value); }; @@ -19,9 +20,9 @@ const SearchBarComponent = (props: IProps) => { <> + - + } right={ { + const { loading, data = [], error, reload } = props; + // const { statusNavbarHeightInfo } = useGlobalState() || {}; + // const { totalHeight } = statusNavbarHeightInfo; + + const renderList = () => { + if (loading && data.length > 0) { + return ( + <> + {new Array(10).fill(0).map(() => { + return ; + })} + + ); + } + + if (error) { + return ; + } + + if (loading && data.length === 0) { + return null; + } + + return ( + <> + {data.map((match, index) => ( + + ))} + + ); + }; + + return ( + + {renderList()} + + ); +}; + +export default ListContainer; diff --git a/src/pages/list/FilterPopup.tsx b/src/pages/list/FilterPopup.tsx index 382d862..05af903 100644 --- a/src/pages/list/FilterPopup.tsx +++ b/src/pages/list/FilterPopup.tsx @@ -12,6 +12,8 @@ import { FilterPopupProps } from "../../../types/list/types"; import CourtType from "@/components/CourtType"; // 玩法 import GamePlayType from "@/components/GamePlayType"; +import { useDictionaryActions } from "@/store/dictionaryStore"; +import { useMemo } from "react"; const FilterPopup = (props: FilterPopupProps) => { const { @@ -27,7 +29,22 @@ const FilterPopup = (props: FilterPopupProps) => { } = props; const store = useListStore() || {}; - const { timeBubbleData, locationOptions, gamePlayOptions } = store; + const { getDictionaryValue } = useDictionaryActions() || {}; + const { timeBubbleData } = store; + + const handleOptions = (dictionaryValue: []) => { + return dictionaryValue?.map((item) => ({ label: item, value: item })) || []; + }; + + const courtType = getDictionaryValue("court_type") || []; + const locationOptions = useMemo(() => { + return courtType ? handleOptions(courtType) : []; + }, [courtType]); + + const gamePlay = getDictionaryValue("game_play") || []; + const gamePlayOptions = useMemo(() => { + return gamePlay ? handleOptions(gamePlay) : []; + }, [gamePlay]); const handleFilterChange = (name, value) => { onChange({ [name]: value }); diff --git a/src/pages/list/index.module.scss b/src/pages/list/index.module.scss index e1db7c9..7a5f4ac 100644 --- a/src/pages/list/index.module.scss +++ b/src/pages/list/index.module.scss @@ -3,20 +3,23 @@ .listTopSearchWrapper { padding: 0 15px; + position: sticky; + background: #fefefe; + z-index: 999; + } + + .isScroll { + border-bottom: 0.5px solid #0000000F; } .listTopFilterWrapper { display: flex; align-items: center; - margin-top: 5px; - margin-bottom: 10px; + padding-top: 10px; + padding-bottom: 10px; gap: 5px; } - .listContentWrapper { - padding: 0 5px; - } - .menuFilter { padding: 0; } diff --git a/src/pages/list/index.tsx b/src/pages/list/index.tsx index 3e2794e..8e7edb6 100644 --- a/src/pages/list/index.tsx +++ b/src/pages/list/index.tsx @@ -1,29 +1,22 @@ -import ListCard from "../../components/ListCard"; -import ListCardSkeleton from "../../components/ListCardSkeleton"; -import List from "../../components/List"; import Menu from "../../components/Menu"; import CityFilter from "../../components/CityFilter"; import SearchBar from "../../components/SearchBar"; import FilterPopup from "./FilterPopup"; import styles from "./index.module.scss"; import { useEffect } from "react"; -import Taro, { useReachBottom } from "@tarojs/taro"; +import Taro, { usePageScroll, useReachBottom } from "@tarojs/taro"; import { useListStore } from "@/store/listStore"; -import {useGlobalState} from '@/store/global' +import { useGlobalState } from "@/store/global"; import { View } from "@tarojs/components"; import CustomerNavBar from "@/components/CustomNavbar"; import GuideBar from "@/components/GuideBar"; -import { useDictionaryActions } from "@/store/dictionaryStore"; +import ListContainer from "@/container/listContainer"; const ListPage = () => { // 从 store 获取数据和方法 const store = useListStore() || {}; - const { statusNavbarHeightInfo } = useGlobalState() || {} - const { getDictionaryValue } = useDictionaryActions() || {}; - console.log('===getDictionaryValue', getDictionaryValue('court_type')); - // locationOptions 室内 - // game_play 玩法 + const { statusNavbarHeightInfo } = useGlobalState() || {}; const { isShowFilterPopup, error, @@ -31,7 +24,6 @@ const ListPage = () => { loading, fetchMatches, refreshMatches, - clearError, updateState, filterCount, updateFilterOptions, // 更新筛选条件 @@ -40,8 +32,15 @@ const ListPage = () => { distanceData, quickFilterData, distanceQuickFilter, + isScrollTop, } = store; + usePageScroll((res) => { + if (res?.scrollTop > 0 && !isScrollTop) { + updateState({ isScrollTop: true }); + } + }); + useReachBottom(() => { console.log("触底了"); // 调用 store 的加载更多方法 @@ -82,78 +81,6 @@ const ListPage = () => { }); }); - // 错误处理 - useEffect(() => { - if (error) { - Taro.showToast({ - title: error, - icon: "error", - duration: 2000, - }); - // 3秒后自动清除错误 - setTimeout(() => { - clearError(); - }, 3000); - } - }, [error, clearError]); - - // 加载状态显示 - if (loading && matches.length === 0) { - return ( -
-
加载中...
-
- 正在获取网球比赛数据 -
-
- ); - } - - // 错误状态显示 - if (error && matches.length === 0) { - return ( -
-
加载失败
-
- {error} -
- -
- ); - } - const toggleShowPopup = () => { updateState({ isShowFilterPopup: !isShowFilterPopup }); }; @@ -183,7 +110,14 @@ const ListPage = () => { - + 0} @@ -227,26 +161,16 @@ const ListPage = () => { - {/* 列表内容 */} - - {!loading && - matches.length > 0 && - matches.map((match, index) => ( - - ))} - - - {/* 空状态 */} - {loading && - matches.length === 0 && - new Array(10).fill(0).map(() => { - return ; - })} - + - + ); }; diff --git a/src/services/listApi.ts b/src/services/listApi.ts index eec41ca..de2be2e 100644 --- a/src/services/listApi.ts +++ b/src/services/listApi.ts @@ -63,25 +63,6 @@ const mockTennisMatches: TennisMatch[] = [ }, ]; -// 模拟数据变化 -const generateDynamicData = (): TennisMatch[] => { - Promise.resolve((res) => { - setTimeout(res, 3000); - }); - return mockTennisMatches.map((match) => ({ - ...match, - // 随机更新注册人数 - registeredCount: Math.min( - match.maxCount, - Math.max(0, match.registeredCount + Math.floor(Math.random() * 3) - 1) - ), - // 随机更新距离 - distance: `${(Math.random() * 5 + 1).toFixed(1)}km`, - // 随机更新时间 - dateTime: Math.random() > 0.5 ? match.dateTime : "今天下午3点 2小时", - })); -}; - /** * 获取网球比赛列表 * @param params 查询参数 @@ -116,47 +97,3 @@ export const refreshTennisMatches = async (): Promise => { } }; -/** - * 获取比赛详情 - * @param id 比赛ID - * @returns Promise - */ -export const getTennisMatchDetail = async ( - id: string -): Promise => { - try { - console.log("API调用: getTennisMatchDetail", id); - - // 模拟网络延迟 - await delay(600 + Math.random() * 400); - - // 模拟网络错误 - if (simulateNetworkError()) { - throw new Error("获取详情失败,请稍后重试"); - } - - const match = mockTennisMatches.find((m) => m.id === id); - - if (!match) { - throw new Error("比赛不存在"); - } - - console.log("API获取详情成功:", match.title); - return match; - } catch (error) { - console.error("API获取详情失败:", error); - throw error; - } -}; - -/** - * 模拟API统计信息 - */ -export const getApiStats = () => { - return { - totalCalls: 0, - successRate: 0.95, - averageResponseTime: 800, - lastCallTime: new Date().toISOString(), - }; -}; diff --git a/src/static/list/icon-load-error.svg b/src/static/list/icon-load-error.svg new file mode 100644 index 0000000..3b7ad03 --- /dev/null +++ b/src/static/list/icon-load-error.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/static/list/icon-paying-game.svg b/src/static/list/icon-paying-game.svg new file mode 100644 index 0000000..a312921 --- /dev/null +++ b/src/static/list/icon-paying-game.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/static/list/icon-reload.svg b/src/static/list/icon-reload.svg new file mode 100644 index 0000000..121dcc5 --- /dev/null +++ b/src/static/list/icon-reload.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/store/listStore.ts b/src/store/listStore.ts index 75454f6..2b767bd 100644 --- a/src/store/listStore.ts +++ b/src/store/listStore.ts @@ -1,6 +1,6 @@ import { create } from 'zustand' import { getTennisMatches } from '../services/listApi' -import {ListActions, IFilterOptions, ListState } from '../../types/list/types' +import { ListActions, IFilterOptions, ListState } from '../../types/list/types' // 完整的 Store 类型 type TennisStore = ListState & ListActions @@ -40,7 +40,7 @@ export const useListStore = create()((set, get) => ({ { id: 3, label: "10km", value: "10km" }, ], // 快捷筛选数据 - quickFilterData:[ + quickFilterData: [ { text: "默认排序", value: "0" }, { text: "好评排序", value: "1" }, { text: "销量排序", value: "2" }, @@ -75,6 +75,8 @@ export const useListStore = create()((set, get) => ({ ], // 球局数量 gamesNum: 124, + // 页面滚动距离顶部距离 是否大于0 + isScrollTop: false, // 获取比赛数据 fetchMatches: async (params) => { @@ -83,15 +85,42 @@ export const useListStore = create()((set, get) => ({ 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: rows || [], + matches: list || rows || [], loading: false, - // lastRefreshTime: new Date().toISOString() + gamesNum: count, }) } catch (error) { - + set({ + error, + matches: [], + loading: false, + }) } }, @@ -100,13 +129,14 @@ export const useListStore = create()((set, get) => ({ set({ loading: true, error: null }) try { - const matches = await getTennisMatches() + const resData = await getTennisMatches() || {}; + const { data = {}, code } = resData; + const { count, rows } = data; set({ - matches, + matches: rows, loading: false, lastRefreshTime: new Date().toISOString() }) - console.log('Store: 成功刷新网球比赛数据:', matches.length, '条') } catch (error) { } }, diff --git a/types/list/types.ts b/types/list/types.ts index 818dd14..e9aa406 100644 --- a/types/list/types.ts +++ b/types/list/types.ts @@ -1,6 +1,6 @@ // 网球比赛数据接口 export interface TennisMatch { - id: string + id: number title: string dateTime: string location: string @@ -39,6 +39,7 @@ export interface ListState { locationOptions: BubbleOption[] gamePlayOptions: BubbleOption[] gamesNum: number + isScrollTop: boolean } export interface ListState {