551 lines
21 KiB
TypeScript
551 lines
21 KiB
TypeScript
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 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">3月25日 周一</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 + 1) * 108 + (participants.length) * 8 - 30}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">
|
||
<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
|