列表骨架屏
This commit is contained in:
@@ -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-辅助函数
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
0
src/components/GamePlayType/index.module.scss
Normal file
0
src/components/GamePlayType/index.module.scss
Normal file
28
src/components/GamePlayType/index.tsx
Normal file
28
src/components/GamePlayType/index.tsx
Normal 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;
|
||||||
@@ -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 {
|
||||||
@@ -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;
|
||||||
264
src/components/ListCardSkeleton/index.scss
Normal file
264
src/components/ListCardSkeleton/index.scss
Normal 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;
|
||||||
|
}
|
||||||
57
src/components/ListCardSkeleton/index.tsx
Normal file
57
src/components/ListCardSkeleton/index.tsx
Normal 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;
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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(),
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user