From 58bacb3a47c750779d4f2adfc4d183401be15c04 Mon Sep 17 00:00:00 2001 From: juguohong Date: Sun, 24 Aug 2025 19:58:00 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=97=E8=A1=A8=E9=AA=A8=E6=9E=B6=E5=B1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/index.ts | 2 +- src/app.config.ts | 12 +- src/components/GamePlayType/index.module.scss | 0 src/components/GamePlayType/index.tsx | 28 ++ .../{ListItem => ListCard}/index.scss | 28 +- .../{ListItem => ListCard}/index.tsx | 64 ++--- src/components/ListCardSkeleton/index.scss | 264 ++++++++++++++++++ src/components/ListCardSkeleton/index.tsx | 57 ++++ src/pages/list/FilterPopup.tsx | 9 +- src/pages/list/index.tsx | 59 +--- src/services/listApi.ts | 227 ++++++--------- src/store/listStore.ts | 15 +- types/list/types.ts | 16 ++ 13 files changed, 532 insertions(+), 249 deletions(-) create mode 100644 src/components/GamePlayType/index.module.scss create mode 100644 src/components/GamePlayType/index.tsx rename src/components/{ListItem => ListCard}/index.scss (88%) rename src/components/{ListItem => ListCard}/index.tsx (66%) create mode 100644 src/components/ListCardSkeleton/index.scss create mode 100644 src/components/ListCardSkeleton/index.tsx diff --git a/config/index.ts b/config/index.ts index c77a458..9e16ab3 100644 --- a/config/index.ts +++ b/config/index.ts @@ -2,7 +2,7 @@ import { defineConfig, type UserConfigExport } from '@tarojs/cli' import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin' import devConfig from './dev' import prodConfig from './prod' -import vitePluginImp from 'vite-plugin-imp' +// import vitePluginImp from 'vite-plugin-imp' import path from 'path' // https://taro-docs.jd.com/docs/next/config#defineconfig-辅助函数 diff --git a/src/app.config.ts b/src/app.config.ts index 7c74bbe..7b1fcad 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -1,12 +1,12 @@ export default defineAppConfig({ pages: [ - 'pages/login/index/index', - 'pages/login/verification/index', - 'pages/login/terms/index', - // 'pages/publishBall/index', + 'pages/list/index', + 'pages/publishBall/index', + // 'pages/login/index/index', + // 'pages/login/verification/index', + // 'pages/login/terms/index', // 'pages/mapDisplay/index', - // 'pages/list/index', - 'pages/index/index' + // 'pages/index/index' ], window: { backgroundTextStyle: 'light', diff --git a/src/components/GamePlayType/index.module.scss b/src/components/GamePlayType/index.module.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/components/GamePlayType/index.tsx b/src/components/GamePlayType/index.tsx new file mode 100644 index 0000000..d426d8b --- /dev/null +++ b/src/components/GamePlayType/index.tsx @@ -0,0 +1,28 @@ +import PopupGameplay from "../../pages/publishBall/components/PopupGameplay"; +import { View, Text, Image } from "@tarojs/components"; +import TitleComponent from "@/components/Title"; +import img from "@/config/images"; + +const GamePlayType = () => { + return ( + + } /> + { + console.log("onClose"); + }} + onConfirm={() => { + console.log("onConfirm"); + }} + visible={false} + options={[ + { label: "不限", value: "不限" }, + { label: "单打", value: "单打" }, + { label: "双打", value: "双打" }, + { label: "拉球", value: "拉球" }, + ]} + /> + + ); +}; +export default GamePlayType; diff --git a/src/components/ListItem/index.scss b/src/components/ListCard/index.scss similarity index 88% rename from src/components/ListItem/index.scss rename to src/components/ListCard/index.scss index 3fc1920..23ed8af 100644 --- a/src/components/ListItem/index.scss +++ b/src/components/ListCard/index.scss @@ -1,16 +1,17 @@ .list-item { display: flex; - padding: 16px; + padding: 12px 15px; background: #ffffff; border-radius: 20px; border: 0.5px solid #f0f0f0; + justify-content: space-between; } .content { flex: 1; display: flex; flex-direction: column; - gap: 6px; + width: calc(100% - 122px); } .titleWrapper { @@ -23,19 +24,40 @@ font-weight: 600; color: #000000; line-height: 24px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .title-right-arrow { width: 16px; height: 16px; + flex-shrink: 0; } -.date-time, .location { + display: flex; + align-items: center; +} + +.location-position { + max-width: 66%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.location-text { + display: block; +} + +.date-time { font-size: 12px; color: #3C3C4399; font-weight: 400; line-height: 18px; + margin-top: 6px; + margin-bottom: 4px; } .bottom-info { diff --git a/src/components/ListItem/index.tsx b/src/components/ListCard/index.tsx similarity index 66% rename from src/components/ListItem/index.tsx rename to src/components/ListCard/index.tsx index 46a6539..56bbc42 100644 --- a/src/components/ListItem/index.tsx +++ b/src/components/ListCard/index.tsx @@ -1,20 +1,10 @@ import { View, Text, Image } from "@tarojs/components"; import img from "../../config/images"; +import { ListCardProps } from "../../../types/list/types"; import "./index.scss"; +// import SkeletonComponent from "../../components/Skeleton"; -interface ListItemProps { - title: string; - dateTime: string; - location: string; - distance: string; - registeredCount: number; - maxCount: number; - skillLevel: string; - matchType: string; - images: string[]; -} - -const ListItem: React.FC = ({ +const ListCard: React.FC = ({ title, dateTime, location, @@ -24,7 +14,12 @@ const ListItem: React.FC = ({ skillLevel, matchType, images, + shinei, }) => { + const renderItemImage = (src: string) => { + return ; + }; + // 根据图片数量决定展示样式 const renderImages = () => { if (images.length === 0) return null; @@ -33,7 +28,8 @@ const ListItem: React.FC = ({ return ( - + {/* */} + {renderItemImage(images[0])} ); @@ -43,10 +39,12 @@ const ListItem: React.FC = ({ return ( - + {/* */} + {renderItemImage(images[0])} - + {/* */} + {renderItemImage(images[1])} ); @@ -55,19 +53,13 @@ const ListItem: React.FC = ({ // 3张或更多图片 return ( - - - - - - - - - + {renderItemImage(images[0])} + {renderItemImage(images[1])} + {renderItemImage(images[2])} ); }; - + console.log("===ttt", !title); return ( {/* 左侧内容区域 */} @@ -83,12 +75,20 @@ const ListItem: React.FC = ({ {/* 时间信息 */} - {dateTime} - {/* 地点和距离 */} - - {location}・{distance} - + + {dateTime} + + + {/* 地点,室内外,距离 */} + + + {location} + + {shinei && `・${shinei}`} + {distance && `・${distance}`} + + {/* 底部信息行:头像组、报名人数、技能等级、比赛类型 */} @@ -131,4 +131,4 @@ const ListItem: React.FC = ({ ); }; -export default ListItem; +export default ListCard; diff --git a/src/components/ListCardSkeleton/index.scss b/src/components/ListCardSkeleton/index.scss new file mode 100644 index 0000000..e642055 --- /dev/null +++ b/src/components/ListCardSkeleton/index.scss @@ -0,0 +1,264 @@ +.list-item { + display: flex; + padding: 12px 15px; + background: #ffffff; + border-radius: 20px; + border: 0.5px solid #f0f0f0; + justify-content: space-between; + --nutui-skeleton-line-height: 24px; + --nutui-skeleton-line-border-radius: 24px; + + .nut-skeleton-block { + margin: 0; + } +} + +.content { + flex: 1; + display: flex; + flex-direction: column; + width: calc(100% - 122px); +} + +.titleWrapper { + display: flex; + align-items: center; +} + +.title { + font-size: 16px; + font-weight: 600; + color: #000000; + line-height: 24px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.title-right-arrow { + width: 16px; + height: 16px; + flex-shrink: 0; +} + +.location { + display: flex; + align-items: center; +} + +.location-position { + max-width: 66%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.location-text { + display: block; +} + +.date-time { + font-size: 12px; + color: #3C3C4399; + font-weight: 400; + line-height: 18px; + margin-top: 6px; + margin-bottom: 4px; +} + +.bottom-info { + display: flex; + align-items: center; + margin-top: 4px; + column-gap: 4px; +} + +.left-section { + display: flex; + align-items: center; + gap: 8px; +} + +.avatar-group { + display: flex; + align-items: center; +} + +.avatar { + width: 20px; + height: 20px; + border-radius: 50%; + background: #e0e0e0; + border: 2px solid #ffffff; + margin-left: -8px; + overflow: hidden; + box-sizing: border-box; + + .avatar-image { + width: 100%; + height: 100%; + object-fit: cover; + } + + &:first-child { + margin-left: 0; + z-index: 3; + } + + &:nth-child(2) { + z-index: 2; + } + + &:nth-child(3) { + z-index: 1; + } +} + +.registration-text { + font-size: 12px; + color: #999999; +} + +.tags { + display: flex; + gap: 4px; +} + +.tag { + box-sizing: border-box; + padding: 0 6px; + border: 0.5px solid #00000029; + height: 20px; + border-radius: 20px; + min-width: 38px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: #000000; + font-size: 11px; +} + +.tag-text-max { + color: #666666; +} + +.image-section { + width: 100px; + height: 100px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + flex-basis: 100px; + flex-grow: 0; + flex-shrink: 0; + + .image-container { + width: 100%; + height: 100%; + border: 1.5px solid #ffffff; + border-radius: 10px; + box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2); + overflow: hidden; + position: absolute; + box-sizing: border-box; + + .nut-skeleton, + .nut-skeleton-content, + .nut-skeleton-block { + width: 100%; + height: 100%; + } + + .nut-skeleton-block { + margin: 0; + border-radius: unset; + } + + .image { + border-radius: 10px; + } + } +} + +.single-image { + position: relative; + width: 88px; + height: 88px; + + .image-container { + width: 88px; + height: 88px; + transform: rotate(-10deg); + } +} + +.double-image { + width: 100%; + height: 100%; + position: relative; + + .image-container { + width: 60%; + height: 60%; + position: absolute; + overflow: hidden; + top: 20%; + + &:first-child { + z-index: 2; + transform: translateX(4px) rotate(-10deg); + } + + &:last-child { + right: 0; + z-index: 1; + transform: translateX(-4px) rotate(10deg); + } + } +} + +.triple-image { + width: 100%; + height: 100%; + position: relative; + + .image-container { + position: absolute; + overflow: hidden; + + &:nth-child(1) { + bottom: 0; + left: 0; + width: 55px; + height: 55px; + z-index: 3; + transform: translateX(4px) rotate(-10deg); + } + + &:nth-child(2) { + bottom: 10px; + right: 0; + width: 55px; + height: 55px; + z-index: 2; + transform: rotate(3deg); + } + + &:nth-child(3) { + top: 5%; + left: 50%; + width: 100rpx; + height: 100rpx; + z-index: 1; + transform: translateX(-50%); + } + } +} + +.image { + width: 100%; + height: 100%; + object-fit: cover; +} \ No newline at end of file diff --git a/src/components/ListCardSkeleton/index.tsx b/src/components/ListCardSkeleton/index.tsx new file mode 100644 index 0000000..d19bf56 --- /dev/null +++ b/src/components/ListCardSkeleton/index.tsx @@ -0,0 +1,57 @@ +import { View } from "@tarojs/components"; +import { Skeleton } from "@nutui/nutui-react-taro"; +import "./index.scss"; + +const ListCard = () => { + return ( + + {/* 左侧内容区域 */} + + {/* 标题 */} + + + + + {/* 时间信息 */} + + + + + + {/* 地点,室内外,距离 */} + + + + + + {/* 底部信息行:头像组、报名人数、技能等级、比赛类型 */} + + + + {Array.from({ length: 3 }).map((_, index) => ( + + + + ))} + + + + + + + + + + {/* 右侧图片区域 */} + + + + + + + + + ); +}; + +export default ListCard; diff --git a/src/pages/list/FilterPopup.tsx b/src/pages/list/FilterPopup.tsx index 9af9136..8beea3a 100644 --- a/src/pages/list/FilterPopup.tsx +++ b/src/pages/list/FilterPopup.tsx @@ -2,12 +2,13 @@ import { Popup } from "@nutui/nutui-react-taro"; import Range from "../../components/Range"; import Bubble from "../../components/Bubble"; import styles from "./filterPopup.module.scss"; -import TitleComponent from "src/components/Title"; +import TitleComponent from "@/components/Title"; import { Button } from "@nutui/nutui-react-taro"; import { Image } from "@tarojs/components"; import img from "../../config/images"; import { useListStore } from "src/store/listStore"; -import {FilterPopupProps} from '../../../types/list/types' +import { FilterPopupProps } from "../../../types/list/types"; +import GamePlayType from "@/components/GamePlayType"; const FilterPopup = (props: FilterPopupProps) => { const { @@ -20,7 +21,7 @@ const FilterPopup = (props: FilterPopupProps) => { visible, onClose, } = props; - + const store = useListStore() || {}; const { timeBubbleData, locationOptions } = store; @@ -82,6 +83,8 @@ const FilterPopup = (props: FilterPopupProps) => { name="site" /> + {/* 玩法 */} + {/* 按钮 */}
-
- )} + {loading && + new Array(10).fill(0).map(() => { + return ; + })}
); diff --git a/src/services/listApi.ts b/src/services/listApi.ts index a937558..77ebd65 100644 --- a/src/services/listApi.ts +++ b/src/services/listApi.ts @@ -1,93 +1,85 @@ -import { TennisMatch } from '../store/listStore' +import { TennisMatch } from "../store/listStore"; // 模拟网络延迟 -const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); // 模拟API响应格式 interface ApiResponse { - code: number - message: string - data: T - timestamp: number + code: number; + message: string; + data: T; + timestamp: number; } - // 模拟网球比赛数据 const mockTennisMatches: TennisMatch[] = [ { - id: '1', - title: '周一晚场浦东新区单打约球', - dateTime: '明天(周五)下午5点 2小时', - location: '仁恒河滨花园网球场・室外', - distance: '3.5km', + id: "1", + title: "周一晚场浦东新区单打约球", + dateTime: "明天(周五)下午5点 2小时", + location: "仁恒河滨花园网球场", + distance: "3.5km", + shinei: "室内", registeredCount: 3, maxCount: 4, - skillLevel: '2.0 至 2.5', - matchType: '双打', + 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' - ] + "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', + id: "2", + title: "浦东新区单打约球", + dateTime: "明天(周五)下午5点 2小时", + location: "仁恒河滨花园网球场", + distance: "3.5km", + shinei: "室外", registeredCount: 2, maxCount: 4, - skillLevel: '2.0 至 2.5', - matchType: '双打', + 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' - ] + "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', + id: "3", + title: "黄浦区双打约球", + dateTime: "7月20日(周日)下午6点 2小时", + location: "仁恒河滨花园网球场", + distance: "3.5km", registeredCount: 3, maxCount: 4, - skillLevel: '2.0 至 2.5', - matchType: '双打', + 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-1554068865-24cecd4e34b8?w=200&h=200&fit=crop&crop=center", + ], + }, +]; // 模拟数据变化 const generateDynamicData = (): TennisMatch[] => { - return mockTennisMatches.map(match => ({ + Promise.resolve((res) => { + setTimeout(res, 3000); + }); + return mockTennisMatches.map((match) => ({ ...match, // 随机更新注册人数 registeredCount: Math.min( - match.maxCount, + 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小时' - })) -} - -// 模拟网络错误 -const simulateNetworkError = (): boolean => { - // 10% 概率模拟网络错误 - return Math.random() < 0.1 -} - -// 模拟网络超时 -const simulateTimeout = (): boolean => { - // 5% 概率模拟超时 - return Math.random() < 0.05 -} + dateTime: Math.random() > 0.5 ? match.dateTime : "今天下午3点 2小时", + })); +}; /** * 获取网球比赛列表 @@ -95,59 +87,21 @@ const simulateTimeout = (): boolean => { * @returns Promise */ export const getTennisMatches = async (params?: { - page?: number - pageSize?: number - location?: string - skillLevel?: string + page?: number; + pageSize?: number; + location?: string; + skillLevel?: string; }): Promise => { try { - console.log('API调用: getTennisMatches', params) - - // 模拟网络延迟 (800-1500ms) - const delayTime = 800 + Math.random() * 700 - await delay(delayTime) - - // 模拟网络错误 - if (simulateNetworkError()) { - throw new Error('网络连接失败,请检查网络设置') - } - - // 模拟超时 - if (simulateTimeout()) { - throw new Error('请求超时,请稍后重试') - } - // 生成动态数据 - const matches = generateDynamicData() - - // 模拟分页 - if (params?.page && params?.pageSize) { - const start = (params.page - 1) * params.pageSize - const end = start + params.pageSize - return matches.slice(start, end) - } - - // 模拟筛选 - if (params?.location) { - return matches.filter(match => - match.location.includes(params.location!) - ) - } - - if (params?.skillLevel) { - return matches.filter(match => - match.skillLevel.includes(params.skillLevel!) - ) - } - - console.log('API响应成功:', matches.length, '条数据') - return matches - + const matches = generateDynamicData(); + + return matches; } catch (error) { - console.error('API调用失败:', error) - throw error + console.error("API调用失败:", error); + throw error; } -} +}; /** * 刷新网球比赛数据 @@ -155,60 +109,47 @@ export const getTennisMatches = async (params?: { */ export const refreshTennisMatches = async (): Promise => { try { - console.log('API调用: refreshTennisMatches') - - // 模拟刷新延迟 (500-1000ms) - const delayTime = 500 + Math.random() * 500 - await delay(delayTime) - - // 模拟网络错误 - if (simulateNetworkError()) { - throw new Error('刷新失败,请稍后重试') - } - // 生成新的动态数据 - const matches = generateDynamicData() - - console.log('API刷新成功:', matches.length, '条数据') - return matches - + const matches = generateDynamicData(); + return matches; } catch (error) { - console.error('API刷新失败:', error) - throw error + console.error("API刷新失败:", error); + throw error; } -} +}; /** * 获取比赛详情 * @param id 比赛ID * @returns Promise */ -export const getTennisMatchDetail = async (id: string): Promise => { +export const getTennisMatchDetail = async ( + id: string +): Promise => { try { - console.log('API调用: getTennisMatchDetail', id) - + console.log("API调用: getTennisMatchDetail", id); + // 模拟网络延迟 - await delay(600 + Math.random() * 400) - + await delay(600 + Math.random() * 400); + // 模拟网络错误 if (simulateNetworkError()) { - throw new Error('获取详情失败,请稍后重试') + throw new Error("获取详情失败,请稍后重试"); } - - const match = mockTennisMatches.find(m => m.id === id) - + + const match = mockTennisMatches.find((m) => m.id === id); + if (!match) { - throw new Error('比赛不存在') + throw new Error("比赛不存在"); } - - console.log('API获取详情成功:', match.title) - return match - + + console.log("API获取详情成功:", match.title); + return match; } catch (error) { - console.error('API获取详情失败:', error) - throw error + console.error("API获取详情失败:", error); + throw error; } -} +}; /** * 模拟API统计信息 @@ -218,6 +159,6 @@ export const getApiStats = () => { totalCalls: 0, successRate: 0.95, averageResponseTime: 800, - lastCallTime: new Date().toISOString() - } -} + lastCallTime: new Date().toISOString(), + }; +}; diff --git a/src/store/listStore.ts b/src/store/listStore.ts index 9a5ec9a..b5eb210 100644 --- a/src/store/listStore.ts +++ b/src/store/listStore.ts @@ -77,14 +77,9 @@ export const useListStore = create()((set, get) => ({ loading: false, lastRefreshTime: new Date().toISOString() }) - console.log('Store: 成功获取网球比赛数据:', matches.length, '条') + } catch (error) { - // const errorMessage = error instanceof Error ? error.message : '未知错误' - // set({ - // error: errorMessage, - // loading: false - // }) - // console.error('Store: 获取网球比赛数据失败:', errorMessage) + } }, @@ -101,12 +96,6 @@ export const useListStore = create()((set, get) => ({ }) console.log('Store: 成功刷新网球比赛数据:', matches.length, '条') } catch (error) { - // const errorMessage = error instanceof Error ? error.message : '未知错误' - // set({ - // error: errorMessage, - // loading: false - // }) - // console.error('Store: 刷新网球比赛数据失败:', errorMessage) } }, diff --git a/types/list/types.ts b/types/list/types.ts index 2fc8d71..789164c 100644 --- a/types/list/types.ts +++ b/types/list/types.ts @@ -10,6 +10,7 @@ export interface TennisMatch { skillLevel: string matchType: string images: string[] + shinei: string } export interface IFilterOptions { location: string @@ -138,4 +139,19 @@ export interface FilterPopupProps { onClear: () => void; visible: boolean; onClose: () => void; +} + +// 列表卡片 +export interface ListCardProps { + title: string; + dateTime: string; + location: string; + distance: string; + registeredCount: number; + maxCount: number; + skillLevel: string; + matchType: string; + images: string[]; + shinei: string; + showSkeleton?: boolean; } \ No newline at end of file