列表骨架屏

This commit is contained in:
juguohong
2025-08-24 19:58:00 +08:00
parent cda1a4b7cc
commit 58bacb3a47
13 changed files with 532 additions and 249 deletions

View File

@@ -2,7 +2,7 @@ import { defineConfig, type UserConfigExport } from '@tarojs/cli'
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin' import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin'
import devConfig from './dev' import devConfig from './dev'
import prodConfig from './prod' import prodConfig from './prod'
import vitePluginImp from 'vite-plugin-imp' // import vitePluginImp from 'vite-plugin-imp'
import path from 'path' import path from 'path'
// https://taro-docs.jd.com/docs/next/config#defineconfig-辅助函数 // https://taro-docs.jd.com/docs/next/config#defineconfig-辅助函数

View File

@@ -1,12 +1,12 @@
export default defineAppConfig({ export default defineAppConfig({
pages: [ pages: [
'pages/login/index/index', 'pages/list/index',
'pages/login/verification/index', 'pages/publishBall/index',
'pages/login/terms/index', // 'pages/login/index/index',
// 'pages/publishBall/index', // 'pages/login/verification/index',
// 'pages/login/terms/index',
// 'pages/mapDisplay/index', // 'pages/mapDisplay/index',
// 'pages/list/index', // 'pages/index/index'
'pages/index/index'
], ],
window: { window: {
backgroundTextStyle: 'light', backgroundTextStyle: 'light',

View File

@@ -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 (
<View>
<TitleComponent title="玩法" icon={<Image src={img.ICON_SITE} />} />
<PopupGameplay
onClose={() => {
console.log("onClose");
}}
onConfirm={() => {
console.log("onConfirm");
}}
visible={false}
options={[
{ label: "不限", value: "不限" },
{ label: "单打", value: "单打" },
{ label: "双打", value: "双打" },
{ label: "拉球", value: "拉球" },
]}
/>
</View>
);
};
export default GamePlayType;

View File

@@ -1,16 +1,17 @@
.list-item { .list-item {
display: flex; display: flex;
padding: 16px; padding: 12px 15px;
background: #ffffff; background: #ffffff;
border-radius: 20px; border-radius: 20px;
border: 0.5px solid #f0f0f0; border: 0.5px solid #f0f0f0;
justify-content: space-between;
} }
.content { .content {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 6px; width: calc(100% - 122px);
} }
.titleWrapper { .titleWrapper {
@@ -23,19 +24,40 @@
font-weight: 600; font-weight: 600;
color: #000000; color: #000000;
line-height: 24px; line-height: 24px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.title-right-arrow { .title-right-arrow {
width: 16px; width: 16px;
height: 16px; height: 16px;
flex-shrink: 0;
} }
.date-time,
.location { .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; font-size: 12px;
color: #3C3C4399; color: #3C3C4399;
font-weight: 400; font-weight: 400;
line-height: 18px; line-height: 18px;
margin-top: 6px;
margin-bottom: 4px;
} }
.bottom-info { .bottom-info {

View File

@@ -1,20 +1,10 @@
import { View, Text, Image } from "@tarojs/components"; import { View, Text, Image } from "@tarojs/components";
import img from "../../config/images"; import img from "../../config/images";
import { ListCardProps } from "../../../types/list/types";
import "./index.scss"; import "./index.scss";
// import SkeletonComponent from "../../components/Skeleton";
interface ListItemProps { const ListCard: React.FC<ListCardProps> = ({
title: string;
dateTime: string;
location: string;
distance: string;
registeredCount: number;
maxCount: number;
skillLevel: string;
matchType: string;
images: string[];
}
const ListItem: React.FC<ListItemProps> = ({
title, title,
dateTime, dateTime,
location, location,
@@ -24,7 +14,12 @@ const ListItem: React.FC<ListItemProps> = ({
skillLevel, skillLevel,
matchType, matchType,
images, images,
shinei,
}) => { }) => {
const renderItemImage = (src: string) => {
return <Image src={src} className="image" mode="aspectFill" />;
};
// 根据图片数量决定展示样式 // 根据图片数量决定展示样式
const renderImages = () => { const renderImages = () => {
if (images.length === 0) return null; if (images.length === 0) return null;
@@ -33,7 +28,8 @@ const ListItem: React.FC<ListItemProps> = ({
return ( return (
<View className="single-image"> <View className="single-image">
<View className="image-container"> <View className="image-container">
<Image src={images[0]} className="image" mode="aspectFill" /> {/* <Image src={images[0]} className="image" mode="aspectFill" /> */}
{renderItemImage(images[0])}
</View> </View>
</View> </View>
); );
@@ -43,10 +39,12 @@ const ListItem: React.FC<ListItemProps> = ({
return ( return (
<View className="double-image"> <View className="double-image">
<View className="image-container"> <View className="image-container">
<Image src={images[0]} className="image" mode="aspectFill" /> {/* <Image src={images[0]} className="image" mode="aspectFill" /> */}
{renderItemImage(images[0])}
</View> </View>
<View className="image-container"> <View className="image-container">
<Image src={images[1]} className="image" mode="aspectFill" /> {/* <Image src={images[1]} className="image" mode="aspectFill" /> */}
{renderItemImage(images[1])}
</View> </View>
</View> </View>
); );
@@ -55,19 +53,13 @@ const ListItem: React.FC<ListItemProps> = ({
// 3张或更多图片 // 3张或更多图片
return ( return (
<View className="triple-image"> <View className="triple-image">
<View className="image-container"> <View className="image-container">{renderItemImage(images[0])}</View>
<Image src={images[0]} className="image" mode="aspectFill" /> <View className="image-container">{renderItemImage(images[1])}</View>
</View> <View className="image-container">{renderItemImage(images[2])}</View>
<View className="image-container">
<Image src={images[1]} className="image" mode="aspectFill" />
</View>
<View className="image-container">
<Image src={images[2]} className="image" mode="aspectFill" />
</View>
</View> </View>
); );
}; };
console.log("===ttt", !title);
return ( return (
<View className="list-item"> <View className="list-item">
{/* 左侧内容区域 */} {/* 左侧内容区域 */}
@@ -83,12 +75,20 @@ const ListItem: React.FC<ListItemProps> = ({
</View> </View>
{/* 时间信息 */} {/* 时间信息 */}
<Text className="date-time">{dateTime}</Text>
{/* 地点和距离 */} <View className="date-time">
<Text className="location"> <Text>{dateTime}</Text>
{location}{distance} </View>
</Text>
{/* 地点,室内外,距离 */}
<View className="location">
<Text className="location-text location-position">{location}</Text>
<Text className="location-text location-time-distance">
{shinei && `${shinei}`}
{distance && `${distance}`}
</Text>
</View>
{/* 底部信息行:头像组、报名人数、技能等级、比赛类型 */} {/* 底部信息行:头像组、报名人数、技能等级、比赛类型 */}
<View className="bottom-info"> <View className="bottom-info">
@@ -131,4 +131,4 @@ const ListItem: React.FC<ListItemProps> = ({
); );
}; };
export default ListItem; export default ListCard;

View File

@@ -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;
}

View File

@@ -0,0 +1,57 @@
import { View } from "@tarojs/components";
import { Skeleton } from "@nutui/nutui-react-taro";
import "./index.scss";
const ListCard = () => {
return (
<View className="list-item">
{/* 左侧内容区域 */}
<View className="content">
{/* 标题 */}
<View className="titleWrapper">
<Skeleton visible={false} style={{ width: "180px" }} />
</View>
{/* 时间信息 */}
<View className="date-time">
<Skeleton visible={false} style={{ width: "88px", }} />
</View>
{/* 地点,室内外,距离 */}
<View className="location">
<Skeleton visible={false} style={{ width: "60px", }} />
</View>
{/* 底部信息行:头像组、报名人数、技能等级、比赛类型 */}
<View className="bottom-info">
<View className="left-section">
<View className="avatar-group">
{Array.from({ length: 3 }).map((_, index) => (
<View key={index} className="avatar">
<Skeleton visible={false} style={{ width: "20px", height: '0' }} />
</View>
))}
</View>
</View>
<View className="tags">
<Skeleton visible={false} style={{ width: "64px" }} />
</View>
</View>
</View>
{/* 右侧图片区域 */}
<View className="image-section">
<View className="single-image">
<View className="image-container">
<Skeleton visible={false} />
</View>
</View>
</View>
</View>
);
};
export default ListCard;

View File

@@ -2,12 +2,13 @@ import { Popup } from "@nutui/nutui-react-taro";
import Range from "../../components/Range"; import Range from "../../components/Range";
import Bubble from "../../components/Bubble"; import Bubble from "../../components/Bubble";
import styles from "./filterPopup.module.scss"; 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 { Button } from "@nutui/nutui-react-taro";
import { Image } from "@tarojs/components"; import { Image } from "@tarojs/components";
import img from "../../config/images"; import img from "../../config/images";
import { useListStore } from "src/store/listStore"; 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 FilterPopup = (props: FilterPopupProps) => {
const { const {
@@ -82,6 +83,8 @@ const FilterPopup = (props: FilterPopupProps) => {
name="site" name="site"
/> />
</div> </div>
{/* 玩法 */}
<GamePlayType />
{/* 按钮 */} {/* 按钮 */}
<div className={styles.filterPopupBtnWrapper}> <div className={styles.filterPopupBtnWrapper}>
<Button <Button

View File

@@ -1,4 +1,5 @@
import ListItem from "../../components/ListItem"; import ListCard from "../../components/ListCard";
import ListCardSkeleton from "../../components/ListCardSkeleton";
import List from "../../components/List"; import List from "../../components/List";
import Menu from "../../components/Menu"; import Menu from "../../components/Menu";
import CityFilter from "../../components/CityFilter"; import CityFilter from "../../components/CityFilter";
@@ -9,7 +10,6 @@ import { useEffect } from "react";
import Taro from "@tarojs/taro"; import Taro from "@tarojs/taro";
import { useListStore } from "../../store/listStore"; import { useListStore } from "../../store/listStore";
import { View } from "@tarojs/components"; import { View } from "@tarojs/components";
import "./index.scss";
const ListPage = () => { const ListPage = () => {
// 从 store 获取数据和方法 // 从 store 获取数据和方法
@@ -82,21 +82,6 @@ const ListPage = () => {
} }
}, [error, clearError]); }, [error, clearError]);
// 格式化时间显示
const formatRefreshTime = (timeString: string | null) => {
if (!timeString) return "";
const date = new Date(timeString);
const now = new Date();
const diff = now.getTime() - date.getTime();
const minutes = Math.floor(diff / 60000);
if (minutes < 1) return "刚刚";
if (minutes < 60) return `${minutes}分钟前`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}小时前`;
return date.toLocaleDateString();
};
// 加载状态显示 // 加载状态显示
if (loading && matches.length === 0) { if (loading && matches.length === 0) {
return ( return (
@@ -230,40 +215,18 @@ const ListPage = () => {
<View className={styles.listContentWrapper}> <View className={styles.listContentWrapper}>
{/* 列表内容 */} {/* 列表内容 */}
<List> <List>
{matches.map((match, index) => ( {!loading &&
<ListItem key={match.id || index} {...match} /> matches.length > 0 &&
))} matches.map((match, index) => (
<ListCard key={match.id || index} {...match} />
))}
</List> </List>
{/* 空状态 */} {/* 空状态 */}
{!loading && matches.length === 0 && ( {loading &&
<div new Array(10).fill(0).map(() => {
style={{ return <ListCardSkeleton />;
display: "flex", })}
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
height: "200px",
fontSize: "14px",
color: "#999",
}}
>
<div style={{ marginBottom: "10px" }}></div>
<button
onClick={() => fetchMatches()}
style={{
padding: "8px 16px",
fontSize: "12px",
color: "#fff",
backgroundColor: "#007aff",
border: "none",
borderRadius: "4px",
}}
>
</button>
</div>
)}
</View> </View>
</View> </View>
); );

View File

@@ -1,69 +1,73 @@
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响应格式 // 模拟API响应格式
interface ApiResponse<T> { interface ApiResponse<T> {
code: number code: number;
message: string message: string;
data: T data: T;
timestamp: number timestamp: number;
} }
// 模拟网球比赛数据 // 模拟网球比赛数据
const mockTennisMatches: TennisMatch[] = [ const mockTennisMatches: TennisMatch[] = [
{ {
id: '1', id: "1",
title: '周一晚场浦东新区单打约球', title: "周一晚场浦东新区单打约球",
dateTime: '明天(周五)下午5点 2小时', dateTime: "明天(周五)下午5点 2小时",
location: '仁恒河滨花园网球场・室外', location: "仁恒河滨花园网球场",
distance: '3.5km', distance: "3.5km",
shinei: "室内",
registeredCount: 3, registeredCount: 3,
maxCount: 4, maxCount: 4,
skillLevel: '2.0 至 2.5', skillLevel: "2.0 至 2.5",
matchType: '双打', matchType: "双打",
images: [ 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",
'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?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-1551698618-1dfe5d97d256?w=200&h=200&fit=crop&crop=center",
] ],
}, },
{ {
id: '2', id: "2",
title: '浦东新区单打约球', title: "浦东新区单打约球",
dateTime: '明天(周五)下午5点 2小时', dateTime: "明天(周五)下午5点 2小时",
location: '仁恒河滨花园网球场・室外', location: "仁恒河滨花园网球场",
distance: '3.5km', distance: "3.5km",
shinei: "室外",
registeredCount: 2, registeredCount: 2,
maxCount: 4, maxCount: 4,
skillLevel: '2.0 至 2.5', skillLevel: "2.0 至 2.5",
matchType: '双打', matchType: "双打",
images: [ images: [
'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?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-1551698618-1dfe5d97d256?w=200&h=200&fit=crop&crop=center",
] ],
}, },
{ {
id: '3', id: "3",
title: '黄浦区双打约球', title: "黄浦区双打约球",
dateTime: '7月20日(周日)下午6点 2小时', dateTime: "7月20日(周日)下午6点 2小时",
location: '仁恒河滨花园网球场・室外', location: "仁恒河滨花园网球场",
distance: '3.5km', distance: "3.5km",
registeredCount: 3, registeredCount: 3,
maxCount: 4, maxCount: 4,
skillLevel: '2.0 至 2.5', skillLevel: "2.0 至 2.5",
matchType: '双打', matchType: "双打",
images: [ 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[] => { const generateDynamicData = (): TennisMatch[] => {
return mockTennisMatches.map(match => ({ Promise.resolve((res) => {
setTimeout(res, 3000);
});
return mockTennisMatches.map((match) => ({
...match, ...match,
// 随机更新注册人数 // 随机更新注册人数
registeredCount: Math.min( registeredCount: Math.min(
@@ -73,21 +77,9 @@ const generateDynamicData = (): TennisMatch[] => {
// 随机更新距离 // 随机更新距离
distance: `${(Math.random() * 5 + 1).toFixed(1)}km`, distance: `${(Math.random() * 5 + 1).toFixed(1)}km`,
// 随机更新时间 // 随机更新时间
dateTime: Math.random() > 0.5 ? match.dateTime : '今天下午3点 2小时' 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
}
/** /**
* 获取网球比赛列表 * 获取网球比赛列表
@@ -95,59 +87,21 @@ const simulateTimeout = (): boolean => {
* @returns Promise<TennisMatch[]> * @returns Promise<TennisMatch[]>
*/ */
export const getTennisMatches = async (params?: { export const getTennisMatches = async (params?: {
page?: number page?: number;
pageSize?: number pageSize?: number;
location?: string location?: string;
skillLevel?: string skillLevel?: string;
}): Promise<TennisMatch[]> => { }): Promise<TennisMatch[]> => {
try { 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() 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
return matches;
} catch (error) { } catch (error) {
console.error('API调用失败:', error) console.error("API调用失败:", error);
throw error throw error;
} }
} };
/** /**
* 刷新网球比赛数据 * 刷新网球比赛数据
@@ -155,60 +109,47 @@ export const getTennisMatches = async (params?: {
*/ */
export const refreshTennisMatches = async (): Promise<TennisMatch[]> => { export const refreshTennisMatches = async (): Promise<TennisMatch[]> => {
try { try {
console.log('API调用: refreshTennisMatches')
// 模拟刷新延迟 (500-1000ms)
const delayTime = 500 + Math.random() * 500
await delay(delayTime)
// 模拟网络错误
if (simulateNetworkError()) {
throw new Error('刷新失败,请稍后重试')
}
// 生成新的动态数据 // 生成新的动态数据
const matches = generateDynamicData() const matches = generateDynamicData();
return matches;
console.log('API刷新成功:', matches.length, '条数据')
return matches
} catch (error) { } catch (error) {
console.error('API刷新失败:', error) console.error("API刷新失败:", error);
throw error throw error;
} }
} };
/** /**
* 获取比赛详情 * 获取比赛详情
* @param id 比赛ID * @param id 比赛ID
* @returns Promise<TennisMatch | null> * @returns Promise<TennisMatch | null>
*/ */
export const getTennisMatchDetail = async (id: string): Promise<TennisMatch | null> => { export const getTennisMatchDetail = async (
id: string
): Promise<TennisMatch | null> => {
try { 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()) { 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) { if (!match) {
throw new Error('比赛不存在') throw new Error("比赛不存在");
} }
console.log('API获取详情成功:', match.title) console.log("API获取详情成功:", match.title);
return match return match;
} catch (error) { } catch (error) {
console.error('API获取详情失败:', error) console.error("API获取详情失败:", error);
throw error throw error;
} }
} };
/** /**
* 模拟API统计信息 * 模拟API统计信息
@@ -218,6 +159,6 @@ export const getApiStats = () => {
totalCalls: 0, totalCalls: 0,
successRate: 0.95, successRate: 0.95,
averageResponseTime: 800, averageResponseTime: 800,
lastCallTime: new Date().toISOString() lastCallTime: new Date().toISOString(),
} };
} };

View File

@@ -77,14 +77,9 @@ export const useListStore = create<TennisStore>()((set, get) => ({
loading: false, loading: false,
lastRefreshTime: new Date().toISOString() lastRefreshTime: new Date().toISOString()
}) })
console.log('Store: 成功获取网球比赛数据:', matches.length, '条')
} catch (error) { } 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<TennisStore>()((set, get) => ({
}) })
console.log('Store: 成功刷新网球比赛数据:', matches.length, '条') console.log('Store: 成功刷新网球比赛数据:', matches.length, '条')
} catch (error) { } catch (error) {
// const errorMessage = error instanceof Error ? error.message : '未知错误'
// set({
// error: errorMessage,
// loading: false
// })
// console.error('Store: 刷新网球比赛数据失败:', errorMessage)
} }
}, },

View File

@@ -10,6 +10,7 @@ export interface TennisMatch {
skillLevel: string skillLevel: string
matchType: string matchType: string
images: string[] images: string[]
shinei: string
} }
export interface IFilterOptions { export interface IFilterOptions {
location: string location: string
@@ -139,3 +140,18 @@ export interface FilterPopupProps {
visible: boolean; visible: boolean;
onClose: () => void; 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;
}