合并代码

This commit is contained in:
筱野
2025-08-30 22:28:02 +08:00
143 changed files with 6369 additions and 2072 deletions

View File

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

1034
src/pages/detail/index.scss Normal file

File diff suppressed because it is too large Load Diff

556
src/pages/detail/index.tsx Normal file
View File

@@ -0,0 +1,556 @@
import React, { useState, useEffect, useRef, useImperativeHandle, forwardRef } from 'react'
import { View, Text, Button, Swiper, SwiperItem, Image, Map, ScrollView } from '@tarojs/components'
import { Cell, Avatar, Progress, Popover } from '@nutui/nutui-react-taro'
import Taro, { useRouter, useShareAppMessage, useShareTimeline } from '@tarojs/taro'
// 导入API服务
import DetailService from '../../services/detailService'
import {
useUserStats,
useUserActions
} from '../../store/userStore'
import img from '../../config/images'
import { getTextColorOnImage } from '../../utils'
import './index.scss'
import { CommonPopup } from '@/components'
const images = [
'http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/1a35ebbf-2361-44da-b338-7608561d0b31.png',
'http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/cf5a82ba-90af-4138-a1b3-9119adcde9e0.png',
'http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/49d7cdf0-b03c-4a0f-91c6-e7778080cfcd.png'
]
function insertDotInTags(tags: string[]) {
return tags.join('-·-').split('-')
}
const SharePopup = forwardRef(({ id, from }: { id: string, from: string }, ref) => {
const [visible, setVisible] = useState(false)
useImperativeHandle(ref, () => ({
show: () => {
setVisible(true)
}
}))
function handleShareToWechat() {
useShareAppMessage(() => {
return {
title: '分享',
path: `/pages/detail/index?id=${id}&from=${from}`,
}
})
}
function handleShareToWechatMoments() {
useShareTimeline(() => {
return {
title: '分享',
path: `/pages/detail/index?id=${id}&from=${from}`,
}
})
}
function handleSaveToLocal() {
Taro.saveImageToPhotosAlbum({
filePath: images[0],
success: () => {
Taro.showToast({ title: '保存成功', icon: 'success' })
},
fail: () => {
Taro.showToast({ title: '保存失败', icon: 'none' })
},
})
}
return (
<CommonPopup
title="分享"
visible={visible}
onClose={() => { setVisible(false) }}
hideFooter
style={{ minHeight: '100px' }}
>
<View catchMove className='share-popup-content'>
<View onClick={handleShareToWechat}>
<Image src={img.ICON_DETAIL_SHARE} />
<Text></Text>
</View>
<View onClick={handleShareToWechatMoments}>
<Image src={img.ICON_DETAIL_SHARE} />
<Text></Text>
</View>
<View onClick={handleSaveToLocal}>
<Image src={img.ICON_DETAIL_SHARE} />
<Text></Text>
</View>
</View>
</CommonPopup>
)
})
function Index() {
// 使用Zustand store
// const userStats = useUserStats()
// const { incrementRequestCount, resetUserStats } = useUserActions()
const [current, setCurrent] = useState(0)
const [colors, setColors] = useState<string []>([])
const [detail, setDetail] = useState<any>(null)
const { params } = useRouter()
const { id, autoShare, from } = params
console.log('from', from)
// 本地状态管理
const [loading, setLoading] = useState(false)
const sharePopupRef = useRef<any>(null)
// 页面加载时获取数据
useEffect(() => {
fetchDetail()
calcBgMainColors()
}, [])
const fetchDetail = async () => {
const res = await DetailService.getDetail(Number(id))
if (res.code === 0) {
console.log(res.data)
setDetail(res.data)
}
}
const calcBgMainColors = async () => {
const textcolors: string[] = []
for (const index in images) {
const { textColor } = await getTextColorOnImage(images[index])
textcolors[index] = textColor
}
setColors(textcolors)
}
function handleShare() {
sharePopupRef.current.show()
}
const openMap = () => {
Taro.openLocation({
latitude: detail?.latitude, // 纬度(必填)
longitude: detail?.longitude, // 经度(必填)
name: '上海体育场', // 位置名(可选)
address: '上海市徐汇区肇嘉浜路128号', // 地址详情(可选)
scale: 15, // 地图缩放级别1-28
})
}
const handleJoinGame = () => {
Taro.navigateTo({
url: `/pages/orderCheck/index?id=${id}`,
})
}
const tags = [{
name: '🕙 急招',
icon: '',
}, {
name: '🔥 本周热门',
icon: '',
}, {
name: '🎉 新活动',
icon: '',
}, {
name: '官方组织',
icon: '',
}]
const detailTags = ['室内', '硬地', '2号场', '有停车场', '有淋浴间', '有更衣室']
const { title, longitude, latitude } = detail || {}
const requirements = [{
title: 'NTRP水平要求',
desc: '2.0 - 4.5 之间',
}, {
title: '活动玩法',
desc: '双打',
}, {
title: '人员构成',
desc: '个人球局 · 组织者参与活动',
}]
const participants = detail?.participants || []
const supplementalNotesTags = ['仅限男生', '装备自备', '其他']
const recommendGames = [
{
title: '黄浦日场对拉',
time: '2025-08-25 9:00',
timeLength: '2小时',
venue: '上海体育场',
veuneType: '室外',
distance: '1.2km',
avatar: 'https://img.yzcdn.cn/vant/cat.jpeg',
applications: 10,
checkedApplications: 3,
levelRequirements: 'NTRP 3.5',
playType: '双打',
},
{
title: '黄浦夜场对拉',
time: '2025-08-25 19:00',
timeLength: '2小时',
venue: '上海体育场',
veuneType: '室外',
distance: '1.2km',
avatar: 'https://img.yzcdn.cn/vant/cat.jpeg',
applications: 10,
checkedApplications: 3,
levelRequirements: 'NTRP 3.5',
playType: '双打',
},
{
title: '黄浦全天对拉',
time: '2025-08-25 9:00',
timeLength: '12小时',
venue: '上海体育场',
veuneType: '室外',
distance: '1.2km',
avatar: 'https://img.yzcdn.cn/vant/cat.jpeg',
applications: 10,
checkedApplications: 3,
levelRequirements: 'NTRP 3.5',
playType: '双打',
},
]
return (
<View className='detail-page'>
{/* custom navbar */}
<view className="custom-navbar">
<View className='detail-navigator'>
<View className='detail-navigator-back' onClick={() => { Taro.navigateBack() }}>
<Image className='detail-navigator-back-icon' src={img.ICON_ARROW_LEFT} />
</View>
<View className='detail-navigator-icon'>
<Image className='detail-navigator-logo-icon' src={img.ICON_LOGO_GO} />
</View>
</View>
</view>
<View className='detail-page-bg' style={{ backgroundImage: `url(${images[current]})` }} />
<View className='detail-page-bg-text' />
{/* swiper */}
<Swiper
className='detail-swiper'
indicatorDots={false}
circular
nextMargin="20px"
onChange={(e) => { setCurrent(e.detail.current) }}
>
{images.map((imageUrl, index) => (
<SwiperItem
key={index}
className='detail-swiper-item'
>
<Image
src={imageUrl}
mode="aspectFill"
className='detail-swiper-item-image'
style={{
transform: index !== current ? 'scale(0.8) translateX(-12%)' : 'scale(0.95)', // 前后图缩小
}}
/>
</SwiperItem>
))}
</Swiper>
{/* content */}
<View className='detail-page-content'>
{/* avatar and tags */}
<View className='detail-page-content-avatar-tags'>
<View className='detail-page-content-avatar-tags-avatar'>
{/* network image mock */}
<Image className='detail-page-content-avatar-tags-avatar-image' src="https://img.yzcdn.cn/vant/cat.jpeg" />
</View>
<View className='detail-page-content-avatar-tags-tags'>
{tags.map((tag, index) => (
<View key={index} className='detail-page-content-avatar-tags-tags-tag'>
{tag.icon && <Image src={tag.icon} />}
<Text>{tag.name}</Text>
</View>
))}
</View>
</View>
{/* title */}
<View className='detail-page-content-title'>
<Text className='detail-page-content-title-text'>{title}</Text>
</View>
{/* Date and Place and weather */}
<View className='detail-page-content-game-info'>
{/* Date and Weather */}
<View className='detail-page-content-game-info-date-weather'>
{/* Calendar and Date time */}
<View className='detail-page-content-game-info-date-weather-calendar-date'>
{/* Calendar */}
<View className='detail-page-content-game-info-date-weather-calendar-date-calendar'>
<View className="month">3</View>
<View className="day">25</View>
</View>
{/* Date time */}
<View className='detail-page-content-game-info-date-weather-calendar-date-date'>
<View className="date">325 </View>
<View className="venue-time">19:00-21:00 2</View>
</View>
</View>
{/* Weather */}
<View className='detail-page-content-game-info-date-weather-weather'>
{/* Weather icon */}
<View className='detail-page-content-game-info-date-weather-weather-icon'>
<Image className="weather-icon" src={img.ICON_WEATHER_SUN} />
</View>
{/* Weather text and temperature */}
<View className='detail-page-content-game-info-date-weather-weather-text-temperature'>
<Text>28 - 32</Text>
</View>
</View>
</View>
{/* Place */}
<View className='detail-page-content-game-info-place'>
{/* venue location message */}
<View className='location-message'>
{/* location icon */}
<View className='location-message-icon'>
<Image className='location-message-icon-image' src={img.ICON_DETAIL_MAP} />
</View>
{/* location message */}
<View className='location-message-text'>
{/* venue name and distance */}
<View className='location-message-text-name-distance' onClick={openMap}>
<Text></Text>
<Text>·</Text>
<Text>1.2km</Text>
<Image className='location-message-text-name-distance-arrow' src={img.ICON_DETAIL_ARROW_RIGHT} />
</View>
{/* venue address */}
<View className='location-message-text-address'>
<Text>128</Text>
</View>
</View>
</View>
{/* venue map */}
<View className='location-map'>
<Map
className='location-map-map'
longitude={longitude}
latitude={latitude}
onError={() => {}}
// hide business msg
showLocation
theme='dark'
/>
</View>
</View>
</View>
{/* detail */}
<View className='detail-page-content-detail'>
{/* venue detail title and venue ordered status */}
<View className='venue-detail-title'>
<Text></Text>
<Text>·</Text>
<Text></Text>
<Popover
title="场地预定截图"
description={<View>
<Image src="https://img.yzcdn.cn/vant/cat.jpeg" />
</View>}
location='top'
visible={false}
>
<Image className='venue-detail-title-notice-icon' src={img.ICON_DETAIL_NOTICE} />
</Popover>
</View>
{/* venue detail content */}
<View className='venue-detail-content'>
{/* venue detail tags */}
<View className='venue-detail-content-tags'>
{insertDotInTags(detailTags).map((tag, index) => (
<View key={index} className='venue-detail-content-tags-tag'>
<Text>{tag}</Text>
</View>
))}
</View>
{/* venue remarks */}
<View className='venue-detail-content-remarks'>
<Text>西</Text>
</View>
</View>
</View>
{/* gameplay requirements */}
<View className='detail-page-content-gameplay-requirements'>
{/* title */}
<View className="gameplay-requirements-title">
<Text></Text>
</View>
{/* requirements */}
<View className='gameplay-requirements'>
{requirements.map((item, index) => (
<View key={index} className='gameplay-requirements-item'>
<Text className='gameplay-requirements-item-title'>{item.title}</Text>
<Text className='gameplay-requirements-item-desc'>{item.desc}</Text>
</View>
))}
</View>
</View>
{/* participants */}
<View className='detail-page-content-participants'>
<View className='participants-title'>
<Text></Text>
<Text>·</Text>
<Text> 3</Text>
</View>
<View className='participants-list'>
{/* application */}
<View className='participants-list-application' onClick={() => { Taro.showToast({ title: 'To be continued', icon: 'none' }) }}>
<Image className='participants-list-application-icon' src={img.ICON_DETAIL_APPLICATION_ADD} />
<Text className='participants-list-application-text'></Text>
</View>
{/* participants list */}
<ScrollView className='participants-list-scroll' scrollX>
<View className='participants-list-scroll-content' style={{ width: `${participants.length * 103 + (participants.length - 1) * 8}px` }}>
{participants.map((participant) => (
<View key={participant.id} className='participants-list-item'>
{/* <Avatar className='participants-list-item-avatar' src={participant.user.avatar_url} /> */}
{/* network image mock random */}
<Avatar className='participants-list-item-avatar' src={`https://picsum.photos/800/600?random=${participant.id}`} />
<Text className='participants-list-item-name'>{participant.user.nickname || '未知'}</Text>
<Text className='participants-list-item-level'>{participant.level || '未知'}</Text>
<Text className='participants-list-item-role'>{participant.role || '参与者'}</Text>
</View>
))}
</View>
</ScrollView>
</View>
</View>
{/* supplemental notes */}
<View className='detail-page-content-supplemental-notes'>
<View className='supplemental-notes-title'>
<Text></Text>
</View>
<View className='supplemental-notes-content'>
{/* supplemental notes tags */}
<View className='supplemental-notes-content-tags'>
{insertDotInTags(supplementalNotesTags).map((tag, index) => (
<View key={index} className='supplemental-notes-content-tags-tag'>
<Text>{tag}</Text>
</View>
))}
</View>
{/* supplemental notes content */}
<View className='supplemental-notes-content-text'>
<Text>西西</Text>
</View>
</View>
</View>
{/* organizer and recommend games by organizer */}
<View className='detail-page-content-organizer-recommend-games'>
{/* orgnizer title */}
<View className='organizer-title'>
<Text></Text>
</View>
{/* organizer avatar and name */}
<View className='organizer-avatar-name'>
<Avatar className='organizer-avatar-name-avatar' src="https://img.yzcdn.cn/vant/cat.jpeg" />
<View className='organizer-avatar-name-message'>
<Text className='organizer-avatar-name-message-name'>Light</Text>
<View className='organizer-avatar-name-message-stats'>
<Text> 8 </Text>
<View className='organizer-avatar-name-message-stats-separator' />
<Text>NTRP 3.5</Text>
</View>
</View>
<View className="organizer-actions">
<View className="organizer-actions-follow">
<Image className='organizer-actions-follow-icon' src={img.ICON_DETAIL_APPLICATION_ADD} />
<Text className='organizer-actions-follow-text'></Text>
</View>
<View className="organizer-actions-comment">
<Image className='organizer-actions-comment-icon' src={img.ICON_DETAIL_COMMENT} />
</View>
</View>
</View>
{/* recommend games by organizer */}
<View className='organizer-recommend-games'>
<View className='organizer-recommend-games-title'>
<Text>TA的更多活动</Text>
<Image className='organizer-recommend-games-title-arrow' src={img.ICON_DETAIL_ARROW_RIGHT} />
</View>
<ScrollView className='recommend-games-list' scrollX>
<View className='recommend-games-list-content'>
{recommendGames.map((game, index) => (
<View key={index} className='recommend-games-list-item'>
{/* game title */}
<View className='recommend-games-list-item-title'>
<Text>{game.title}</Text>
<Image className='recommend-games-list-item-title-arrow' src={img.ICON_DETAIL_ARROW_RIGHT} />
</View>
{/* game time and range */}
<View className='recommend-games-list-item-time-range'>
<Text>{game.time}</Text>
<Text>{game.timeLength}</Text>
</View>
{/* game location、vunue、distance */}
<View className='recommend-games-list-item-location-venue-distance'>
<Text>{game.venue}</Text>
<Text>·</Text>
<Text>{game.veuneType}</Text>
<Text>·</Text>
<Text>{game.distance}</Text>
</View>
{/* organizer avatar、applications、level requirements、play type */}
<View className='recommend-games-list-item-addon'>
<Avatar className='recommend-games-list-item-addon-avatar' src={game.avatar} />
<View className='recommend-games-list-item-addon-message'>
<View className='recommend-games-list-item-addon-message-applications'>
<Text> {game.checkedApplications}/{game.applications}</Text>
</View>
<View className='recommend-games-list-item-addon-message-level-requirements'>
<Text>{game.levelRequirements}</Text>
</View>
<View className='recommend-games-list-item-addon-message-play-type'>
<Text>{game.playType}</Text>
</View>
</View>
</View>
</View>
))}
</View>
</ScrollView>
</View>
</View>
{/* sticky bottom action bar */}
<View className="sticky-bottom-bar">
<View className="sticky-bottom-bar-share-and-comment">
<View className='sticky-bottom-bar-share' onClick={handleShare}>
<Image className='sticky-bottom-bar-share-icon' src={img.ICON_DETAIL_SHARE} />
<Text className='sticky-bottom-bar-share-text'></Text>
</View>
<View className='sticky-bottom-bar-share-and-comment-separator' />
<View className='sticky-bottom-bar-comment' onClick={() => { Taro.showToast({ title: 'To be continued', icon: 'none' }) }}>
<Image className='sticky-bottom-bar-comment-icon' src={img.ICON_DETAIL_COMMENT_DARK} />
<Text className='sticky-bottom-bar-comment-text'>32</Text>
</View>
</View>
<View className="sticky-bottom-bar-join-game" onClick={handleJoinGame}>
<Text>🎾</Text>
<Text></Text>
<View className='game-price'>
<Text>¥ 65</Text>
</View>
</View>
</View>
{/* share popup */}
<SharePopup ref={sharePopupRef} id={id} from={from} />
</View>
</View>
)
}
export default Index

View File

@@ -1,3 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '首页'
})

View File

@@ -1 +0,0 @@

View File

@@ -1,261 +0,0 @@
.index-page {
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
.page-header {
text-align: center;
margin-bottom: 24px;
.page-title {
font-size: 28px;
font-weight: bold;
color: white;
display: block;
margin-bottom: 8px;
}
.page-subtitle {
font-size: 16px;
color: rgba(255, 255, 255, 0.8);
display: block;
}
}
.user-card {
background: white;
border-radius: 16px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
.user-header {
display: flex;
align-items: center;
gap: 16px;
.user-info {
flex: 1;
.username {
font-size: 20px;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 4px;
}
.user-level {
font-size: 14px;
color: #666;
display: block;
margin-bottom: 4px;
}
.join-date {
font-size: 12px;
color: #999;
display: block;
}
}
}
}
.stats-section {
background: white;
border-radius: 16px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
.section-title {
font-size: 18px;
font-weight: bold;
color: #333;
margin-bottom: 16px;
display: block;
}
:global {
.nut-cell {
background: #f8f9fa;
border-radius: 12px;
margin-bottom: 8px;
border: none;
&:last-child {
margin-bottom: 0;
}
.nut-cell__title {
font-weight: 500;
color: #555;
}
.nut-cell__value {
font-weight: bold;
color: #007bff;
}
}
}
}
.action-section {
background: white;
border-radius: 16px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
.section-title {
font-size: 18px;
font-weight: bold;
color: #333;
margin-bottom: 16px;
display: block;
}
.button-group {
display: flex;
flex-direction: column;
gap: 12px;
.custom-button {
border-radius: 12px;
font-weight: 500;
height: 48px;
border: none;
margin-bottom: 12px;
font-size: 16px;
color: white;
&.primary-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
&.success-btn {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
&.warning-btn {
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
}
&:disabled {
opacity: 0.6;
}
}
// 保留 NutUI 按钮样式(备用)
:global {
.nut-button {
border-radius: 12px;
font-weight: 500;
height: 48px;
border: none;
&--primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
&--success {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
&--warning {
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
}
&:disabled {
opacity: 0.6;
}
}
}
}
}
.loading-section {
background: white;
border-radius: 16px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
text-align: center;
.loading-text {
font-size: 16px;
color: #666;
margin-bottom: 12px;
display: block;
}
:global {
.nut-progress {
.nut-progress-outer {
background: #f0f0f0;
border-radius: 10px;
}
.nut-progress-inner {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 10px;
}
}
}
}
.tips-section {
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
.tips-title {
font-size: 18px;
font-weight: bold;
color: #333;
margin-bottom: 16px;
display: block;
}
.tips-content {
.tip-item {
font-size: 14px;
color: #666;
line-height: 1.6;
margin-bottom: 8px;
display: block;
&:last-child {
margin-bottom: 0;
}
}
}
}
}
// 响应式设计
@media (max-width: 768px) {
.index-page {
padding: 16px;
.page-header {
.page-title {
font-size: 24px;
}
.page-subtitle {
font-size: 14px;
}
}
.user-card,
.stats-section,
.action-section,
.loading-section,
.tips-section {
padding: 16px;
margin-bottom: 16px;
}
}
}

View File

@@ -1,46 +1,81 @@
import { Popup } from "@nutui/nutui-react-taro";
import Range from "../../components/Range";
import Bubble, { BubbleOption } from "../../components/Bubble";
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 CourtType from "@/components/CourtType";
// 玩法
import GamePlayType from "@/components/GamePlayType";
import { useDictionaryActions } from "@/store/dictionaryStore";
import { useMemo } from "react";
const timeOptions: BubbleOption[] = [
{ id: 1, label: "晨间 6:00-10:00", value: "morning" },
{ id: 2, label: "上午 10:00-12:00", value: "forenoon" },
{ id: 3, label: "中午 12:00-14:00", value: "noon" },
{ id: 4, label: "下午 14:00-18:00", value: "afternoon" },
{ id: 5, label: "晚上 18:00-22:00", value: "evening" },
{ id: 6, label: "夜间 22:00-24:00", value: "night" },
];
const FilterPopup = (props: FilterPopupProps) => {
const {
loading,
onCancel,
onConfirm,
onChange,
filterOptions,
onClear,
visible,
onClose,
statusNavbarHeigh,
} = props;
const locationOptions: BubbleOption[] = [
{ id: 1, label: "室内", value: "1" },
{ id: 2, label: "室外", value: "2" },
{ id: 3, label: "半室外", value: "3" },
];
const store = useListStore() || {};
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 });
};
const handleClearFilter = () => {
onClear();
onCancel();
};
const FilterPopup = () => {
return (
<>
<Popup
visible={true}
destroyOnClose
position="top"
round
closeOnOverlayClick={false}
onClose={() => {
// setShowTop(false)
}}
visible={visible}
onClose={onClose}
style={{ marginTop: statusNavbarHeigh + "px" }}
overlayStyle={{ marginTop: statusNavbarHeigh + "px" }}
>
<div className={styles.filterPopupWrapper}>
{/* 时间气泡选项 */}
<Bubble
options={timeOptions}
value={(value) => {}}
onChange={(value) => {}}
options={timeBubbleData}
value={filterOptions?.time}
onChange={handleFilterChange}
layout="grid"
size="small"
columns={3}
name="time"
/>
{/* 范围选择 */}
@@ -49,19 +84,58 @@ const FilterPopup = () => {
max={5.0}
step={0.5}
className={styles.filterPopupRange}
onChange={handleFilterChange}
value={filterOptions?.ntrp}
name="ntrp"
/>
{/* 场次气泡选项 */}
<div>
<TitleComponent title="场地类型" />
{/* <div>
<TitleComponent
title="场地类型"
icon={<Image src={img.ICON_SITE} />}
/>
<Bubble
options={locationOptions}
value={(value) => {}}
onChange={(value) => {}}
value={filterOptions?.site}
onChange={handleFilterChange}
layout="grid"
size="small"
columns={3}
name="site"
/>
</div> */}
{/* CourtType */}
<CourtType
onChange={handleFilterChange}
name="court_type"
options={locationOptions}
value={filterOptions?.site}
/>
{/* 玩法 */}
<GamePlayType
onChange={handleFilterChange}
name="game_play"
options={gamePlayOptions}
value={filterOptions?.game_play}
/>
{/* 按钮 */}
<div className={styles.filterPopupBtnWrapper}>
<Button
className={styles.btn}
type="default"
onClick={handleClearFilter}
>
</Button>
<Button
className={`${styles.btn} ${styles.confirm}`}
type="default"
loading={loading}
onClick={onConfirm}
>
332
</Button>
</div>
</div>
</Popup>

View File

@@ -1,8 +1,35 @@
.filterPopupWrapper {
position: relative;
$m18: 18px;
padding: $m18;
.filterPopupRange {
margin-top: $m18;
margin-bottom: $m18;
}
.filterPopupBtnWrapper {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
position: sticky;
bottom: 0;
background-color: #ffffff;
padding: 8px 0;
.btn {
flex: 1;
}
--nutui-button-border-width: 0.5px;
--nutui-button-default-border-color: #0000000F;
--nutui-button-border-radius: 16px;
--nutui-button-default-height: 44px;
--nutui-button-default-color: #000000;
.confirm {
--nutui-button-default-color: #ffffff;
--nutui-button-default-background-color: #000000;
}
}
}

View File

@@ -1,5 +1,6 @@
export default definePageConfig({
navigationBarTitleText: '',
enablePullDownRefresh: true,
backgroundTextStyle: 'dark'
backgroundTextStyle: 'dark',
navigationStyle: 'custom',
})

View File

@@ -0,0 +1,26 @@
.listPage {
background-color: #fafafa;
.listTopSearchWrapper {
padding: 0 15px;
position: sticky;
background: #fefefe;
z-index: 999;
}
.isScroll {
border-bottom: 0.5px solid #0000000F;
}
.listTopFilterWrapper {
display: flex;
align-items: center;
padding-top: 10px;
padding-bottom: 10px;
gap: 5px;
}
.menuFilter {
padding: 0;
}
}

View File

@@ -1,29 +1,51 @@
import ListItem from "../../components/ListItem";
import List from "../../components/List";
import Bubble from "../../components/Bubble/example";
import Range from "../../components/Range/example";
import Menu from "../../components/Menu/example";
import CityFilter from "../../components/CityFilter/example";
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 from "@tarojs/taro";
import {
useTennisMatches,
useTennisLoading,
useTennisError,
useTennisLastRefresh,
useTennisActions,
} from "../../store/listStore";
import "./index.scss";
import Taro, { usePageScroll, useReachBottom } from "@tarojs/taro";
import { useListStore } from "@/store/listStore";
import { useGlobalState } from "@/store/global";
import { View } from "@tarojs/components";
import CustomerNavBar from "@/components/CustomNavbar";
import GuideBar from "@/components/GuideBar";
import ListContainer from "@/container/listContainer";
const ListPage = () => {
// 从 store 获取数据和方法
const matches = useTennisMatches();
const loading = useTennisLoading();
const error = useTennisError();
const lastRefreshTime = useTennisLastRefresh();
const { fetchMatches, refreshMatches, clearError } = useTennisActions();
const store = useListStore() || {};
const { statusNavbarHeightInfo } = useGlobalState() || {};
const {
isShowFilterPopup,
error,
matches,
loading,
fetchMatches,
refreshMatches,
updateState,
filterCount,
updateFilterOptions, // 更新筛选条件
filterOptions,
clearFilterOptions,
distanceData,
quickFilterData,
distanceQuickFilter,
isScrollTop,
} = store;
usePageScroll((res) => {
if (res?.scrollTop > 0 && !isScrollTop) {
updateState({ isScrollTop: true });
}
});
useReachBottom(() => {
console.log("触底了");
// 调用 store 的加载更多方法
// loadMoreMatches();
});
useEffect(() => {
// 页面加载时获取数据
@@ -59,147 +81,97 @@ const ListPage = () => {
});
});
// 错误处理
useEffect(() => {
if (error) {
Taro.showToast({
title: error,
icon: "error",
duration: 2000,
});
// 3秒后自动清除错误
setTimeout(() => {
clearError();
}, 3000);
}
}, [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();
const toggleShowPopup = () => {
updateState({ isShowFilterPopup: !isShowFilterPopup });
};
// 加载状态显示
if (loading && matches.length === 0) {
return (
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
height: "200px",
fontSize: "14px",
color: "#999",
}}
>
<div style={{ marginBottom: "10px" }}>...</div>
<div style={{ fontSize: "12px", color: "#ccc" }}>
</div>
</div>
);
}
/**
* @description 更新筛选条件
* @param {Record<string, any>} params 筛选项
*/
const handleUpdateFilterOptions = (params: Record<string, any>) => {
updateFilterOptions(params);
};
// 错误状态显示
if (error && matches.length === 0) {
return (
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
height: "200px",
fontSize: "14px",
color: "#999",
}}
>
<div style={{ marginBottom: "10px" }}></div>
<div style={{ marginBottom: "15px", fontSize: "12px", color: "#ccc" }}>
{error}
</div>
<button
onClick={() => fetchMatches()}
style={{
padding: "8px 16px",
fontSize: "12px",
color: "#fff",
backgroundColor: "#007aff",
border: "none",
borderRadius: "4px",
}}
>
</button>
</div>
);
}
const handleSearchChange = () => {};
// 距离筛选
const handleDistanceOrQuickChange = (name, value) => {
updateState({
distanceQuickFilter: {
...distanceQuickFilter,
[name]: value,
},
});
};
return (
<div>
<SearchBar />
{/* 综合筛选 */}
<div>
<FilterPopup />
</div>
{/* 筛选 */}
<div>
{/* 全城筛选 */}
<CityFilter />
{/* 智能排序 */}
<Menu />
</div>
<>
<CustomerNavBar />
{/* 列表内容 */}
<List>
{matches.map((match, index) => (
<ListItem key={match.id || index} {...match} />
))}
</List>
{/* 空状态 */}
{!loading && matches.length === 0 && (
<div
<View className={styles.listPage}>
<View
className={`${styles.listTopSearchWrapper} ${
isScrollTop ? styles.isScroll : ""
}`}
style={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
height: "200px",
fontSize: "14px",
color: "#999",
top: statusNavbarHeightInfo?.totalHeight,
}}
>
<div style={{ marginBottom: "10px" }}></div>
<button
onClick={() => fetchMatches()}
style={{
padding: "8px 16px",
fontSize: "12px",
color: "#fff",
backgroundColor: "#007aff",
border: "none",
borderRadius: "4px",
}}
>
</button>
</div>
)}
</div>
<SearchBar
handleFilterIcon={toggleShowPopup}
isSelect={filterCount > 0}
filterCount={filterCount}
onChange={handleSearchChange}
/>
{/* 综合筛选 */}
{isShowFilterPopup && (
<div>
<FilterPopup
loading={loading}
onCancel={toggleShowPopup}
onConfirm={toggleShowPopup}
onChange={handleUpdateFilterOptions}
filterOptions={filterOptions}
onClear={clearFilterOptions}
visible={isShowFilterPopup}
onClose={toggleShowPopup}
statusNavbarHeigh={statusNavbarHeightInfo?.totalHeight}
/>
</div>
)}
{/* 筛选 */}
<div className={styles.listTopFilterWrapper}>
{/* 全城筛选 */}
<CityFilter
options={distanceData}
value={distanceQuickFilter?.distance}
wrapperClassName={styles.menuFilter}
onChange={handleDistanceOrQuickChange}
name="distance"
/>
{/* 智能排序 */}
<Menu
options={quickFilterData}
value={distanceQuickFilter?.quick}
onChange={handleDistanceOrQuickChange}
wrapperClassName={styles.menuFilter}
name="quick"
/>
</div>
</View>
{/* 列表内容 */}
<ListContainer
data={matches}
loading={loading}
error={error}
reload={refreshMatches}
/>
</View>
<GuideBar currentPage="list" />
</>
);
};

View File

@@ -370,7 +370,7 @@
font-family: 'PingFang SC';
font-weight: 400;
font-size: 16px;
color: #07C160;
color: #000000;
text-decoration: underline;
cursor: pointer;
transition: all 0.3s ease;

View File

@@ -10,7 +10,7 @@ const LoginPage: React.FC = () => {
const [show_terms_layer, set_show_terms_layer] = useState(false);
// 微信授权登录
const handle_wechat_login = async () => {
const handle_wechat_login = async (e: any) => {
if (!agree_terms) {
set_show_terms_layer(true);
Taro.showToast({
@@ -21,9 +21,20 @@ const LoginPage: React.FC = () => {
return;
}
// 检查是否获取到手机号
if (!e.detail || !e.detail.code) {
Taro.showToast({
title: '获取手机号失败,请重试',
icon: 'none',
duration: 2000
});
return;
}
set_is_loading(true);
try {
const response = await wechat_auth_login();
// 传递手机号code给登录服务
const response = await wechat_auth_login(e.detail.code);
if (response.success) {
save_login_state(response.token!, response.user_info!);
@@ -99,7 +110,7 @@ const LoginPage: React.FC = () => {
<View className="background_image">
<Image
className="bg_img"
src={require('../../../static/login/login_bg.png')}
src="http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/2e00dea1-8723-42fe-ae42-84fe38e9ac3f.png"
mode="aspectFill"
/>
<View className="bg_overlay"></View>
@@ -123,7 +134,8 @@ const LoginPage: React.FC = () => {
{/* 微信快捷登录 */}
<Button
className={`login_button wechat_button ${is_loading ? 'loading' : ''}`}
onClick={handle_wechat_login}
openType="getPhoneNumber"
onGetPhoneNumber={handle_wechat_login}
disabled={is_loading}
>
<View className="wechat_icon">
@@ -217,7 +229,7 @@ const LoginPage: React.FC = () => {
{agree_terms ? '已同意' : '同意并继续'}
</Button>
</View>
</View>
)}

View File

@@ -157,7 +157,7 @@
position: relative;
z-index: 5;
flex: 1;
padding: 0px 24px ;
box-sizing: border-box;
overflow-y: auto;
@@ -181,6 +181,7 @@
line-height: 1.43em;
color: #000000;
margin-bottom: 24px;
}
// 条款详细内容
@@ -192,6 +193,14 @@
color: #000000;
margin-bottom: 40px;
white-space: pre-line;
padding: 0px 24px ;
.terms_first_line,
span.terms_first_line {
font-weight: 500;
display: block;
margin-bottom: 16px;
}
}
// 底部按钮

View File

@@ -24,7 +24,7 @@ const TermsPage: React.FC = () => {
case 'terms':
setPageTitle('条款和条件');
setTermsTitle('《开场的条款和条件》');
setTermsContent(`欢迎使用本平台(以下简称"本平台")发布与参与网球活动。为保障您的权益,请您务必仔细阅读并理解以下服务条款。
setTermsContent(`<span class="terms_first_line">欢迎使用本平台(以下简称"本平台")发布与参与网球活动。为保障您的权益,请您务必仔细阅读并理解以下服务条款。</span>
一、服务内容
1. 本平台为用户提供活动发布、报名、聊天室沟通、活动提醒等服务。
@@ -70,7 +70,7 @@ const TermsPage: React.FC = () => {
case 'binding':
setPageTitle('微信号绑定协议');
setTermsTitle('《开场与微信号绑定协议》');
setTermsContent(`欢迎使用本平台(以下简称"本平台")的微信绑定服务。为保障您的权益,请您务必仔细阅读并理解以下协议内容。
setTermsContent(`<span class="terms_first_line">欢迎使用本平台(以下简称"本平台")的微信绑定服务。为保障您的权益,请您务必仔细阅读并理解以下协议内容。</span>
一、绑定服务说明
1. 本平台提供微信账号绑定服务,用户可通过微信快捷登录方式使用平台功能。
@@ -115,7 +115,7 @@ const TermsPage: React.FC = () => {
case 'privacy':
setPageTitle('隐私权政策');
setTermsTitle('《隐私权政策》');
setTermsContent(`本平台(以下简称"我们")非常重视用户的隐私保护,本隐私权政策说明了我们如何收集、使用、存储和保护您的个人信息。
setTermsContent(`<span class="terms_first_line">本平台(以下简称"我们")非常重视用户的隐私保护,本隐私权政策说明了我们如何收集、使用、存储和保护您的个人信息。</span>
一、信息收集
1. 注册信息:包括手机号码、微信账号、昵称、头像等基本信息。
@@ -190,13 +190,14 @@ const TermsPage: React.FC = () => {
</View>
{/* 条款详细内容 */}
<View className="terms_content">
{termsContent}
</View>
<View
className="terms_content"
dangerouslySetInnerHTML={{ __html: termsContent }}
/>
</ScrollView>
</View>
);
};

View File

@@ -0,0 +1,5 @@
export default definePageConfig({
navigationBarTitleText: '消息',
// navigationBarBackgroundColor: '#FAFAFA',
navigationStyle: 'custom',
})

View File

@@ -0,0 +1,80 @@
@use '~@/scss/images.scss' as img;
$--Backgrounds-Primary: '#fff';
.message-container {
width: 100%;
height: 100vh;
background: radial-gradient(227.15% 100% at 50% 0%, #EEFFDC 0%, #FFF 36.58%), var(--Backgrounds-Primary, #FFF);
// padding-top: 100px;
box-sizing: border-box;
.custom-navbar {
height: 56px; /* 通常与原生导航栏高度一致 */
display: flex;
align-items: center;
justify-content: flex-start;
// background-color: #fff;
color: #000;
padding-top: 44px; /* 适配状态栏 */
position: sticky;
top: 0;
z-index: 100;
.message-navigator {
position: relative;
left: 15px;
top: -2px;
width: 80px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
.message-navigator-avatar {
width: 32px;
height: 32px;
}
.message-navigator-title {
font-size: 16px;
font-weight: 500;
color: #000;
}
}
}
.message-content {
.message-content-list {
display: flex;
flex-direction: column;
padding: 10px 15px;
box-sizing: border-box;
gap: 12px;
.message-item {
padding: 10px;
// border: 1px solid rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
gap: 10px;
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2);
background-color: rgba(255, 255, 255, 0.5);
border-radius: 10px;
}
.message-item-title {
font-size: 16px;
font-weight: 500;
color: #000;
}
.message-item-content {
font-size: 14px;
color: #666;
}
}
}
}

View File

@@ -0,0 +1,43 @@
import React from 'react'
import { View, Text, ScrollView } from '@tarojs/components'
import { Avatar } from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
import GuideBar from '@/components/GuideBar'
// import img from '@/config/images'
import './index.scss'
const Personal = () => {
const messageList = Array(10).fill(0).map((_, index) => ({
id: index + 1,
title: `消息${index + 1}消息${index + 1}消息${index + 1}消息${index + 1}`,
content: Array(Math.round(Math.random() * 40)).fill(0).map((_, index) => `消息${index + 1}`).join(''),
}))
return (
<View className='message-container'>
<View className='custom-navbar'>
<View className='message-navigator'>
<Avatar className='message-navigator-avatar' src="https://img.yzcdn.cn/vant/cat.jpeg" />
<Text className='message-navigator-title'></Text>
</View>
</View>
<ScrollView scrollY className='message-content'>
<View className='message-content-list'>
{messageList.map((item) => (
<View className='message-item' key={item.id}>
<View className='message-item-title'>
<Text>{item.title}</Text>
</View>
<View className='message-item-content'>
<Text>{item.content}</Text>
</View>
</View>
))}
</View>
</ScrollView>
<GuideBar currentPage='message' />
</View>
)
}
export default Personal

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '订单确认',
navigationBarBackgroundColor: '#FAFAFA'
})

View File

@@ -0,0 +1 @@
@use '~@/scss/images.scss' as img;

View File

@@ -0,0 +1,12 @@
import React from 'react'
import { View, Text } from '@tarojs/components'
const OrderCheck = () => {
return (
<View>
<Text>OrderCheck</Text>
</View>
)
}
export default OrderCheck

View File

@@ -0,0 +1,5 @@
export default definePageConfig({
navigationBarTitleText: '个人中心',
// navigationBarBackgroundColor: '#FAFAFA',
navigationStyle: 'custom',
})

View File

@@ -0,0 +1,35 @@
@use '~@/scss/images.scss' as img;
$--Backgrounds-Primary: '#fff';
.personal-container {
width: 100%;
height: 100vh;
background: radial-gradient(227.15% 100% at 50% 0%, #EEFFDC 0%, #FFF 36.58%), var(--Backgrounds-Primary, #FFF);
padding-top: 100px;
box-sizing: border-box;
.personal-navigator {
position: fixed;
left: 10px;
top: 54px;
width: 32px;
height: 32px;
.personal-navigator-back {
width: 100%;
height: 100%;
}
}
.personal-content {
width: 100%;
height: calc(100vh - 300px);
display: flex;
justify-content: center;
align-items: center;
font-size: 32px;
font-weight: 500;
color: #000;
}
}

View File

@@ -0,0 +1,31 @@
import React from 'react'
import { View, Text, Image } from '@tarojs/components'
import Taro, { useRouter } from '@tarojs/taro'
import GuideBar from '@/components/GuideBar'
import img from '@/config/images'
import './index.scss'
const Personal = () => {
const { params } = useRouter()
const { id } = params
const handleBack = () => {
Taro.navigateBack()
}
return (
<View className='personal-container'>
{id && (
<View className='personal-navigator' onClick={handleBack}>
<Image className='personal-navigator-back' src={img.ICON_NAVIGATOR_BACK} />
</View>
)}
<View className='personal-content'>
<Text>Personal</Text>
</View>
<GuideBar currentPage='personal' />
</View>
)
}
export default Personal

View File

@@ -3,7 +3,8 @@ import Taro from '@tarojs/taro'
import { View, Text, Image } from '@tarojs/components'
import images from '@/config/images'
import TextareaTag from '@/components/TextareaTag'
import CoverImageUpload, { type CoverImage } from '@/components/ImageUpload'
// import CoverImageUpload, { type CoverImage } from '@/components/ImageUpload'
import UploadCover, { type CoverImageValue } from '@/components/UploadCover'
import { useDictionaryActions } from '@/store/dictionaryStore'
import './StadiumDetail.scss'
@@ -18,7 +19,7 @@ export interface Stadium {
court_surface?: string
description?: string
description_tag?: string[]
venue_image_list?: CoverImage[]
venue_image_list?: CoverImageValue[]
}
interface StadiumDetailProps {
@@ -103,7 +104,7 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
court_type: court_type[0] || '',
court_surface: court_surface[0] || '',
additionalInfo: '',
venue_image_list: [] as CoverImage[],
venue_image_list: [] as CoverImageValue[],
description:{
description: '',
description_tag: []
@@ -216,9 +217,21 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
if (item.type === 'image') {
return (
<SectionContainer key={item.label} title={item.label} prop={item.prop}>
<CoverImageUpload
images={formData[item.prop]}
onChange={(images) => updateFormData(item.prop, images)}
<UploadCover
value={formData[item.prop]}
onChange={(value) => {
console.log(value, 'value')
if (value instanceof Function) {
const newValue = value(formData[item.prop])
console.log(newValue, 'newValue')
updateFormData(item.prop, newValue)
} else {
updateFormData(item.prop, value)
}
}}
maxCount={9}
source={['album', 'history', 'preset']}
align='left'
/>
</SectionContainer>
)
@@ -230,4 +243,4 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
)
})
export default StadiumDetail
export default StadiumDetail

View File

@@ -8,40 +8,40 @@ import PublishForm from './publishForm'
import { publishBallFormSchema } from '../../config/formSchema/publishBallFormSchema';
import { PublishBallFormData } from '../../../types/publishBall';
import PublishService from '@/services/publishService';
import { getNextHourTime, getEndTime } from '@/utils/timeUtils';
import { getNextHourTime, getEndTime, delay } from '@/utils';
import images from '@/config/images'
import styles from './index.module.scss'
const defaultFormData: PublishBallFormData = {
title: '',
image_list: ['https://static-o.oss-cn-shenzhen.aliyuncs.com/images/tpbj/tpss10.jpg'],
title: '',
image_list: ['https://static-o.oss-cn-shenzhen.aliyuncs.com/images/tpbj/tpss10.jpg'],
timeRange: {
start_time: getNextHourTime(),
start_time: getNextHourTime(),
end_time: getEndTime(getNextHourTime())
},
activityInfo: {
activityInfo: {
play_type: '不限',
price: '',
venue_id: null,
venue_id: null,
location_name: '',
location: '',
latitude: '',
location: '',
latitude: '',
longitude: '',
court_type: '',
court_surface: '',
venue_description_tag: [],
venue_description: '',
venue_image_list: [],
court_type: '',
court_surface: '',
venue_description_tag: [],
venue_description: '',
venue_image_list: [],
},
players: [1, 4],
skill_level: [1.0, 5.0],
descriptionInfo: {
description: '',
description_tag: [],
description: '',
description_tag: [],
},
is_substitute_supported: true,
is_wechat_contact: true,
wechat_contact: '14223332214'
is_substitute_supported: true,
is_wechat_contact: true,
wechat_contact: '14223332214'
}
const PublishBall: React.FC = () => {
@@ -72,7 +72,7 @@ const PublishBall: React.FC = () => {
const [formData, setFormData] = useState<PublishBallFormData[]>([
defaultFormData
])
// 删除确认弹窗状态
const [deleteConfirm, setDeleteConfirm] = useState<{
visible: boolean;
@@ -225,6 +225,13 @@ const PublishBall: React.FC = () => {
title: '发布成功',
icon: 'success'
})
delay(1000)
// 如果是个人球局,则跳转到详情页,并自动分享
// 如果是畅打,则跳转第一个球局详情页,并自动分享 @刘杰
Taro.navigateTo({
// @ts-expect-error: id
url: `/pages/detail/index?id=${res.data.id || 1}&from=publish&autoShare=1`
})
} else {
Taro.showToast({
title: res.message,
@@ -268,12 +275,16 @@ const PublishBall: React.FC = () => {
<View className={styles['publish-ball']}>
{/* 活动类型切换 */}
<View className={styles['activity-type-switch']}>
<<<<<<< HEAD
{/* <ActivityTypeSwitch
=======
<ActivityTypeSwitch
>>>>>>> d92419f3c5648a67d1b6857d66d6c92a4bece034
value={activityType}
onChange={handleActivityTypeChange}
/> */}
</View>
<View className={styles['publish-ball__scroll']}>
{
formData.map((item, index) => (
@@ -283,19 +294,19 @@ const PublishBall: React.FC = () => {
<View className={styles['session-header']}>
<View className={styles['session-title']}>
{index + 1}
<View
className={styles['session-delete']}
<View
className={styles['session-delete']}
onClick={() => showDeleteConfirm(index)}
>
<Image src={images.ICON_DELETE} className={styles['session-delete-icon']} />
</View>
</View>
<View className={styles['session-actions']}>
{index > 0 && (
<View
className={styles['session-action-btn']}
<View
className={styles['session-action-btn']}
onClick={() => handleCopyPrevious(index)}
>
@@ -304,10 +315,10 @@ const PublishBall: React.FC = () => {
</View>
</View>
)}
<PublishForm
formData={item}
onChange={(key, value) => updateFormData(key, value, index)}
optionsConfig={publishBallFormSchema}
<PublishForm
formData={item}
onChange={(key, value) => updateFormData(key, value, index)}
optionsConfig={publishBallFormSchema}
/>
</View>
))
@@ -323,6 +334,7 @@ const PublishBall: React.FC = () => {
</View>
{/* 删除确认弹窗 */}
<<<<<<< HEAD
<CommonDialog
visible={deleteConfirm.visible}
cancelText="再想想"
@@ -332,6 +344,30 @@ const PublishBall: React.FC = () => {
contentTitle="确认移除该场次?"
contentDesc="该操作不可恢复"
/>
=======
{deleteConfirm.visible && (
<View className={styles['delete-modal']}>
<View className={styles['delete-modal__content']}>
<Text className={styles['delete-modal__title']}></Text>
<Text className={styles['delete-modal__desc']}></Text>
<View className={styles['delete-modal__actions']}>
<Button
className={styles['delete-modal__btn']}
onClick={closeDeleteConfirm}
>
</Button>
<Button
className={styles['delete-modal__btn']}
onClick={confirmDelete}
>
</Button>
</View>
</View>
</View>
)}
>>>>>>> d92419f3c5648a67d1b6857d66d6c92a4bece034
{/* 完成按钮 */}
<View className={styles['submit-section']}>

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react'
import { View, Text } from '@tarojs/components'
import { ImageUpload, Range, TimeSelector, TextareaTag, NumberInterval, TitleTextarea, FormSwitch } from '../../components'
import { ImageUpload, Range, TimeSelector, TextareaTag, NumberInterval, TitleTextarea, FormSwitch, UploadCover } from '../../components'
import FormBasicInfo from './components/FormBasicInfo'
import { type CoverImage } from '../../components/index.types'
import { FormFieldConfig, FieldType } from '../../config/formSchema/publishBallFormSchema'
@@ -22,18 +22,22 @@ const componentMap = {
[FieldType.WECHATCONTACT]: WechatSwitch,
}
const PublishForm: React.FC<{
formData: PublishBallFormData,
onChange: (key: keyof PublishBallFormData, value: any, index?: number) => void,
const PublishForm: React.FC<{
formData: PublishBallFormData,
onChange: (key: keyof PublishBallFormData, value: any, index?: number) => void,
optionsConfig: FormFieldConfig[] }> = ({ formData, onChange, optionsConfig }) => {
const [coverImages, setCoverImages] = useState<CoverImage[]>([])
// 字典数据相关
const { getDictionaryValue } = useDictionaryActions()
// 处理封面图片变化
const handleCoverImagesChange = (images: CoverImage[]) => {
setCoverImages(images)
const handleCoverImagesChange = (fn: (images: CoverImage[]) => CoverImage[]) => {
if (fn instanceof Function) {
setCoverImages(fn(coverImages))
} else {
setCoverImages(fn)
}
}
// 更新表单数据
@@ -70,7 +74,7 @@ const PublishForm: React.FC<{
})
}
}
// 如果是补充要求,从字典获取选项
if (item.prop === 'descriptionInfo') {
const descriptionOptions = getDictionaryOptions('publishing_requirements', [])
@@ -79,7 +83,7 @@ const PublishForm: React.FC<{
options: descriptionOptions
}
}
return item
})
}
@@ -121,7 +125,7 @@ const PublishForm: React.FC<{
// 获取动态表单配置
const dynamicConfig = getDynamicFormConfig()
@@ -140,8 +144,8 @@ const PublishForm: React.FC<{
}
if (item.type === FieldType.UPLOADIMAGE) {
/* 活动封面 */
return <ImageUpload
images={coverImages}
return <UploadCover
value={coverImages}
onChange={handleCoverImagesChange}
{...item.props}
/>
@@ -182,7 +186,7 @@ const PublishForm: React.FC<{
value={formData[item.prop]}
onChange={(value) => updateFormData(item.prop as keyof PublishBallFormData, value)}
{...optionProps}
placeholder={item.placeholder}
placeholder={item.placeholder}
/>
</View>
</View>

View File

@@ -0,0 +1,9 @@
export default {
navigationBarTitleText: '个人主页',
navigationBarBackgroundColor: '#FFFFFF',
navigationBarTextStyle: 'black',
backgroundColor: '#FAFAFA',
enablePullDownRefresh: false,
disableScroll: false,
navigationStyle: 'custom'
}

View File

@@ -0,0 +1,600 @@
// 个人页面样式
.myself_page {
min-height: 100vh;
background: radial-gradient(circle at 50% 0%, rgba(238, 255, 220, 1) 0%, rgba(255, 255, 255, 1) 37%);
position: relative;
overflow: hidden;
box-sizing: border-box;
}
// 主要内容区域
.main_content {
position: relative;
z-index: 5;
flex: 1;
margin-top: 0;
box-sizing: border-box;
overflow-y: auto;
padding: 15px 15px 15px;
// 用户信息区域
.user_info_section {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 16px;
// 基本信息
.basic_info {
display: flex;
align-items: center;
gap: 16px;
.avatar_container {
width: 64px;
height: 64px;
border-radius: 50%;
overflow: hidden;
box-shadow: 0px 8px 20px 0px rgba(0, 0, 0, 0.12), 0px 0px 1px 0px rgba(0, 0, 0, 0.2);
.avatar {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.info_container {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
.nickname {
font-family: 'PingFang SC';
font-weight: 600;
font-size: 20px;
line-height: 1.4em;
letter-spacing: 1.9%;
color: #000000;
}
.join_date {
font-family: 'PingFang SC';
font-weight: 400;
font-size: 14px;
line-height: 1.4em;
letter-spacing: 2.7%;
color: rgba(0, 0, 0, 0.35);
}
}
}
// 统计数据
.stats_section {
display: flex;
justify-content: space-between;
align-items: center;
gap: 24px;
.stats_container {
display: flex;
align-items: center;
gap: 20px;
.stat_item {
display: flex;
flex-direction: column;
align-items: center;
.stat_number {
font-family: 'PingFang SC';
font-weight: 600;
font-size: 18px;
line-height: 1.4em;
letter-spacing: 2.1%;
color: rgba(0, 0, 0, 0.85);
}
.stat_label {
font-family: 'PingFang SC';
font-weight: 500;
font-size: 12px;
line-height: 1.4em;
letter-spacing: 3.2%;
color: rgba(0, 0, 0, 0.35);
}
}
}
.action_buttons {
display: flex;
align-items: center;
gap: 12px;
.follow_button {
display: flex;
align-items: center;
gap: 4px;
padding: 12px 16px 12px 12px;
height: 40px;
background: #000000;
border: 0.5px solid rgba(0, 0, 0, 0.06);
border-radius: 999px;
cursor: pointer;
transition: all 0.3s ease;
&.following {
background: #FFFFFF;
color: #000000;
}
.button_icon {
width: 20px;
height: 20px;
}
.button_text {
font-family: 'PingFang SC';
font-weight: 600;
font-size: 14px;
line-height: 1.4em;
color: #FFFFFF;
.following & {
color: #000000;
}
}
}
.message_button {
width: 40px;
height: 40px;
background: #FFFFFF;
border: 0.5px solid rgba(0, 0, 0, 0.12);
border-radius: 999px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
.button_icon {
width: 18px;
height: 18px;
}
}
.edit_button {
min-width: 60px;
height: 40px;
background: #FFFFFF;
border: 0.5px solid rgba(0, 0, 0, 0.12);
border-radius: 999px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
padding: 0 12px;
.button_text {
font-family: 'PingFang SC';
font-weight: 500;
font-size: 14px;
line-height: 1.4em;
color: #000000;
}
}
.share_button {
min-width: 60px;
height: 40px;
background: #FFFFFF;
border: 0.5px solid rgba(0, 0, 0, 0.12);
border-radius: 999px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
padding: 0 12px;
margin: 0px !important;
.button_text {
font-family: 'PingFang SC';
font-weight: 500;
font-size: 14px;
line-height: 1.4em;
color: #000000;
}
}
}
}
// 标签和简介
.tags_bio_section {
display: flex;
flex-direction: column;
gap: 10px;
.tags_container {
display: flex;
gap: 8px;
flex-wrap: wrap;
.tag_item {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 8px;
height: 20px;
background: #FFFFFF;
border: 0.5px solid rgba(0, 0, 0, 0.16);
border-radius: 999px;
.tag_icon {
width: 12px;
height: 12px;
}
.tag_text {
font-family: 'PingFang SC';
font-weight: 500;
font-size: 11px;
line-height: 1.8em;
letter-spacing: -2.1%;
color: #000000;
}
}
}
.bio_text {
font-family: 'PingFang SC';
font-weight: 400;
font-size: 14px;
line-height: 1.571em;
color: rgba(0, 0, 0, 0.65);
white-space: pre-line;
}
}
// 球局订单和收藏功能
.quick_actions_section {
margin-bottom: 16px;
.action_card {
display: flex;
align-items: center;
background: #FFFFFF;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 12px;
box-shadow: 0px 4px 36px 0px rgba(0, 0, 0, 0.06);
overflow: hidden;
.action_content {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
padding: 20px 0;
cursor: pointer;
transition: background-color 0.3s ease;
&:hover {
background-color: rgba(0, 0, 0, 0.02);
}
.action_icon {
width: 20px;
height: 20px;
}
.action_text {
font-family: 'PingFang SC';
font-weight: 600;
font-size: 15px;
line-height: 1.4em;
color: #000000;
}
}
.action_divider {
width: 1px;
height: 16px;
background: rgba(0, 0, 0, 0.06);
}
}
}
}
// 球局类型标签页
.game_tabs_section {
margin-bottom: 16px;
.tab_container {
display: flex;
gap: 16px;
padding: 12px 15px;
.tab_item {
padding: 12px 0;
cursor: pointer;
transition: all 0.3s ease;
.tab_text {
font-family: 'PingFang SC';
font-weight: 600;
font-size: 20px;
line-height: 1.4em;
letter-spacing: 1.9%;
color: rgba(0, 0, 0, 0.85);
transition: color 0.3s ease;
}
&.active {
.tab_text {
color: #000000;
}
}
&:not(.active) {
.tab_text {
color: rgba(0, 0, 0, 0.2);
}
}
}
}
}
// 球局列表区域
.game_list_section {
.date_header {
display: flex;
align-items: center;
gap: 4px;
padding: 10px 15px;
margin-bottom: 16px;
.date_text {
font-family: 'PingFang SC';
font-weight: 600;
font-size: 14px;
line-height: 1.4em;
letter-spacing: 2.71%;
color: rgba(0, 0, 0, 0.85);
}
.separator {
font-family: 'PingFang SC';
font-weight: 400;
font-size: 18px;
line-height: 1.4em;
letter-spacing: 2.11%;
color: rgba(0, 0, 0, 0.35);
}
.weekday_text {
font-family: 'PingFang SC';
font-weight: 600;
font-size: 14px;
line-height: 1.4em;
letter-spacing: 2.71%;
color: rgba(0, 0, 0, 0.85);
}
}
// 球局卡片
.game_cards {
display: flex;
flex-direction: column;
gap: 5px;
padding: 0 5px 15px;
.game_card {
background: #FFFFFF;
border: 0.5px solid rgba(0, 0, 0, 0.08);
border-radius: 20px;
padding: 0 0 12px;
box-shadow: 0px 4px 36px 0px rgba(0, 0, 0, 0.06);
cursor: pointer;
transition: all 0.3s ease;
position: relative;
&:active {
transform: scale(0.98);
}
// 球局标题和类型
.game_header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 15px 0;
.game_title {
font-family: 'PingFang SC';
font-weight: 600;
font-size: 16px;
line-height: 1.5em;
color: #000000;
}
.game_type_icon {
width: 16px;
height: 16px;
.type_icon {
width: 100%;
height: 100%;
}
}
}
// 球局时间
.game_time {
padding: 6px 15px 0;
.time_text {
font-family: 'PingFang SC';
font-weight: 400;
font-size: 12px;
line-height: 1.5em;
color: rgba(60, 60, 67, 0.6);
}
}
// 球局地点和类型
.game_location {
display: flex;
align-items: center;
gap: 2px;
padding: 4px 15px 0;
.location_text,
.type_text,
.distance_text {
font-family: 'PingFang SC';
font-weight: 400;
font-size: 12px;
line-height: 1.5em;
color: rgba(60, 60, 67, 0.6);
}
.separator {
font-family: 'PingFang SC';
font-weight: 400;
font-size: 14px;
line-height: 1.3em;
color: rgba(60, 60, 67, 0.3);
}
}
// 球局图片
.game_images {
position: absolute;
top: 11px;
right: 5px;
width: 100px;
height: 100px;
box-shadow: 0px 4px 24px 0px rgba(0, 0, 0, 0.2);
.game_image {
position: absolute;
width: 56.44px;
height: 56.44px;
border-radius: 9px;
border: 1.5px solid #FFFFFF;
&:nth-child(1) {
top: 4.18px;
left: 19.18px;
}
&:nth-child(2) {
top: 26.5px;
left: 38px;
width: 61.86px;
height: 61.86px;
}
&:nth-child(3) {
top: 32.5px;
left: 0;
width: 62.04px;
height: 62.04px;
}
}
}
// 球局信息标签
.game_tags {
display: flex;
flex-direction: row;
gap: 6px;
padding: 8px 15px 0;
.participants_info {
display: flex;
gap: 4px;
.avatars {
display: flex;
align-items: center;
gap: -8px;
.participant_avatar {
width: 20px;
height: 20px;
border-radius: 50%;
border: 1px solid #FFFFFF;
}
}
.participants_count {
background: #FFFFFF;
border: 0.5px solid rgba(0, 0, 0, 0.16);
border-radius: 999px;
padding: 6px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
.count_text {
font-family: 'PingFang SC';
font-weight: 500;
font-size: 11px;
line-height: 1.8em;
letter-spacing: -2.1%;
color: #000000;
}
}
}
.game_info_tags {
display: flex;
gap: 4px;
.info_tag {
background: #FFFFFF;
border: 0.5px solid rgba(0, 0, 0, 0.16);
border-radius: 999px;
padding: 6px 8px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
.tag_text {
font-family: 'PingFang SC';
font-weight: 500;
font-size: 11px;
line-height: 1.8em;
letter-spacing: -2.1%;
color: #000000;
}
}
}
}
}
}
}
}
// 底部指示器
.home_indicator {
position: absolute;
bottom: 21px;
left: 50%;
transform: translateX(-50%);
width: 140px;
height: 5px;
background: #000000;
border-radius: 2.5px;
z-index: 10;
}

View File

@@ -0,0 +1,377 @@
import React, { useState, useEffect } from 'react';
import { View, Text, Image, ScrollView, Button } from '@tarojs/components';
import Taro from '@tarojs/taro';
import './index.scss';
// 用户信息接口
interface UserInfo {
id: string;
nickname: string;
avatar: string;
join_date: string;
stats: {
following: number;
friends: number;
hosted: number;
participated: number;
};
tags: string[];
bio: string;
location: string;
occupation: string;
ntrp_level: string;
}
// 球局记录接口
interface GameRecord {
id: string;
title: string;
date: string;
time: string;
duration: string;
location: string;
type: string;
distance: string;
participants: {
avatar: string;
nickname: string;
}[];
max_participants: number;
current_participants: number;
level_range: string;
game_type: string;
images: string[];
}
const MyselfPage: React.FC = () => {
// 获取页面参数
const instance = Taro.getCurrentInstance();
const user_id = instance.router?.params?.userid;
// 判断是否为当前用户
const is_current_user = !user_id;
// 模拟用户数据
const [user_info] = useState<UserInfo>({
id: '1',
nickname: '188的王晨',
avatar: require('../../../static/userInfo/default_avatar.svg'),
join_date: '2025年9月加入',
stats: {
following: 124,
friends: 24,
hosted: 7,
participated: 24
},
tags: ['上海黄浦', '互联网从业者', 'NTRP 4.0'],
bio: '网球入坑两年,偏好双打,正手进攻型选手\n平时在张江、世纪公园附近活动欢迎约球\n不卷分数但认真对待每一拍每一场球都想打得开心。有时候也会带相机来拍点照片📸',
location: '上海黄浦',
occupation: '互联网从业者',
ntrp_level: 'NTRP 4.0'
});
// 模拟球局数据
const [game_records] = useState<GameRecord[]>([
{
id: '1',
title: '女生轻松双打',
date: '明天(周五)',
time: '下午5点',
duration: '2小时',
location: '仁恒河滨花园网球场',
type: '室外',
distance: '3.5km',
participants: [
{ avatar: require('../../../static/userInfo/user1.svg'), nickname: '用户1' },
{ avatar: require('../../../static/userInfo/user2.svg'), nickname: '用户2' }
],
max_participants: 4,
current_participants: 2,
level_range: '2.0 至 2.5',
game_type: '双打',
images: [
require('../../../static/userInfo/game1.svg'),
require('../../../static/userInfo/game2.svg'),
require('../../../static/userInfo/game3.svg')
]
}
]);
// 关注状态
const [is_following, setIsFollowing] = useState(false);
// 当前激活的标签页
const [active_tab, setActiveTab] = useState<'hosted' | 'participated'>('hosted');
// 处理关注/取消关注
const handle_follow = () => {
setIsFollowing(!is_following);
Taro.showToast({
title: is_following ? '已取消关注' : '关注成功',
icon: 'success',
duration: 1500
});
};
// 处理分享
const handle_share = () => {
Taro.showShareMenu({
withShareTicket: true
});
};
// 处理返回
const handle_back = () => {
Taro.navigateBack();
};
// 处理编辑资料
const handle_edit_profile = () => {
Taro.navigateTo({
url: '/pages/userInfo/edit/index'
});
};
// 处理球局详情
const handle_game_detail = (game_id: string) => {
Taro.navigateTo({
url: `/pages/game/detail/index?id=${game_id}`
});
};
// 处理球局订单
const handle_game_orders = () => {
Taro.navigateTo({
url: '/pages/game/orders/index'
});
};
// 处理收藏
const handle_favorites = () => {
Taro.navigateTo({
url: '/pages/game/favorites/index'
});
};
return (
<View className="myself_page">
{/* 主要内容 */}
<ScrollView className="main_content" scrollY>
{/* 用户信息区域 */}
<View className="user_info_section">
{/* 头像和基本信息 */}
<View className="basic_info">
<View className="avatar_container">
<Image className="avatar" src={user_info.avatar} />
</View>
<View className="info_container">
<Text className="nickname">{user_info.nickname}</Text>
<Text className="join_date">{user_info.join_date}</Text>
</View>
</View>
{/* 统计数据 */}
<View className="stats_section">
<View className="stats_container">
<View className="stat_item">
<Text className="stat_number">{user_info.stats.following}</Text>
<Text className="stat_label"></Text>
</View>
<View className="stat_item">
<Text className="stat_number">{user_info.stats.friends}</Text>
<Text className="stat_label"></Text>
</View>
<View className="stat_item">
<Text className="stat_number">{user_info.stats.hosted}</Text>
<Text className="stat_label"></Text>
</View>
<View className="stat_item">
<Text className="stat_number">{user_info.stats.participated}</Text>
<Text className="stat_label"></Text>
</View>
</View>
<View className="action_buttons">
{/* 只有非当前用户才显示关注按钮 */}
{!is_current_user && (
<Button
className={`follow_button ${is_following ? 'following' : ''}`}
onClick={handle_follow}
>
<Image
className="button_icon"
src={require('../../../static/userInfo/plus.svg')}
/>
<Text className="button_text">
{is_following ? '已关注' : '关注'}
</Text>
</Button>
)}
{/* 只有非当前用户才显示消息按钮 */}
{!is_current_user && (
<Button className="message_button">
<Image
className="button_icon"
src={require('../../../static/userInfo/message.svg')}
/>
</Button>
)}
{/* 只有当前用户才显示编辑按钮 */}
{is_current_user && (
<Button className="edit_button" onClick={handle_edit_profile}>
<Text className="button_text"></Text>
</Button>
)}
{/* 只有当前用户才显示分享按钮 */}
{is_current_user && (
<Button className="share_button" onClick={handle_share}>
<Text className="button_text"></Text>
</Button>
)}
</View>
</View>
{/* 标签和简介 */}
<View className="tags_bio_section">
<View className="tags_container">
<View className="tag_item">
<Image
className="tag_icon"
src={require('../../../static/userInfo/location.svg')}
/>
<Text className="tag_text">{user_info.location}</Text>
</View>
<View className="tag_item">
<Text className="tag_text">{user_info.occupation}</Text>
</View>
<View className="tag_item">
<Text className="tag_text">{user_info.ntrp_level}</Text>
</View>
</View>
<Text className="bio_text">{user_info.bio}</Text>
</View>
{/* 球局订单和收藏功能 */}
<View className="quick_actions_section">
<View className="action_card">
<View className="action_content" onClick={handle_game_orders}>
<Image
className="action_icon"
src={require('../../../static/userInfo/tennis.svg')}
/>
<Text className="action_text"></Text>
</View>
<View className="action_divider"></View>
<View className="action_content" onClick={handle_favorites}>
<Image
className="action_icon"
src={require('../../../static/userInfo/tennis.svg')}
/>
<Text className="action_text"></Text>
</View>
</View>
</View>
</View>
{/* 球局类型标签页 */}
<View className="game_tabs_section">
<View className="tab_container">
<View className={`tab_item ${active_tab === 'hosted' ? 'active' : ''}`} onClick={() => setActiveTab('hosted')}>
<Text className="tab_text"></Text>
</View>
<View className={`tab_item ${active_tab === 'participated' ? 'active' : ''}`} onClick={() => setActiveTab('participated')}>
<Text className="tab_text"></Text>
</View>
</View>
</View>
{/* 球局列表 */}
<View className="game_list_section">
<View className="date_header">
<Text className="date_text">528</Text>
<Text className="separator">/</Text>
<Text className="weekday_text"></Text>
</View>
{/* 球局卡片 */}
<View className="game_cards">
{game_records.map((game) => (
<View
key={game.id}
className="game_card"
onClick={() => handle_game_detail(game.id)}
>
{/* 球局标题和类型 */}
<View className="game_header">
<Text className="game_title">{game.title}</Text>
<View className="game_type_icon">
<Image
className="type_icon"
src={require('../../../static/userInfo/tennis.svg')}
/>
</View>
</View>
{/* 球局时间 */}
<View className="game_time">
<Text className="time_text">
{game.date} {game.time} {game.duration}
</Text>
</View>
{/* 球局地点和类型 */}
<View className="game_location">
<Text className="location_text">{game.location}</Text>
<Text className="separator">·</Text>
<Text className="type_text">{game.type}</Text>
<Text className="separator">·</Text>
<Text className="distance_text">{game.distance}</Text>
</View>
{/* 球局图片 */}
<View className="game_images">
{game.images.map((image, index) => (
<Image
key={index}
className="game_image"
src={image}
/>
))}
</View>
{/* 球局信息标签 */}
<View className="game_tags">
<View className="participants_info">
<View className="avatars">
{game.participants.map((participant, index) => (
<Image
key={index}
className="participant_avatar"
src={participant.avatar}
/>
))}
</View>
<View className="participants_count">
<Text className="count_text">
{game.current_participants}/{game.max_participants}
</Text>
</View>
</View>
<View className="game_info_tags">
<View className="info_tag">
<Text className="tag_text">{game.level_range}</Text>
</View>
<View className="info_tag">
<Text className="tag_text">{game.game_type}</Text>
</View>
</View>
</View>
</View>
))}
</View>
</View>
</ScrollView>
</View>
);
};
export default MyselfPage;