Files
mini-programs/src/pages/detail/index.tsx
2025-09-07 13:34:14 +08:00

635 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useRef, useImperativeHandle, forwardRef } from 'react'
import { View, Text, Image, Map, ScrollView } from '@tarojs/components'
import { Avatar, Popover } from '@nutui/nutui-react-taro'
import Taro, { useRouter, useShareAppMessage, useShareTimeline, useDidShow } from '@tarojs/taro'
import dayjs from 'dayjs'
import 'dayjs/locale/zh-cn'
// 导入API服务
import { CommonPopup, withAuth } from '@/components'
import DetailService, { MATCH_STATUS} from '@/services/detailService'
import { getCurrentLocation, calculateDistance } from '@/utils/locationUtils'
import {
useUserInfo,
useUserActions,
} from '@/store/userStore'
import img from '@/config/images'
// import { getTextColorOnImage } from '../../utils'
import './index.scss'
dayjs.locale('zh-cn')
// 将·作为连接符插入到标签文本之间
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=share`,
// }
// })
// }
// function handleShareToWechatMoments() {
// useShareTimeline(() => {
// return {
// title: '分享',
// path: `/pages/detail/index?id=${id}&from=share`,
// }
// })
// }
// 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>
</CommonPopup>
)
})
// 底部操作栏
function StickyButton(props) {
const { handleShare, handleJoinGame, detail } = props
const userInfo = useUserInfo()
const { id } = userInfo
const { publisher_id, match_status, price } = detail || {}
const role = Number(publisher_id) === id ? 'ownner' : 'visitor'
console.log(match_status, role)
return (
<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>¥ {price}</Text>
</View>
</View>
</View>
)
}
// 球局信息
function GameInfo(props) {
const { detail, currentLocation } = props
const { latitude, longitude, location, location_name, start_time, end_time } = detail || {}
const openMap = () => {
Taro.openLocation({
latitude, // 纬度(必填)
longitude, // 经度(必填)
name: location_name, // 位置名(可选)
address: location, // 地址详情(可选)
scale: 15, // 地图缩放级别1-28
})
}
const [c_latitude, c_longitude] = currentLocation
const distance = calculateDistance(c_latitude, c_longitude, latitude, longitude) / 1000
const startTime = dayjs(start_time)
const endTime = dayjs(end_time)
const game_length = endTime.diff(startTime, 'minutes') / 60
const startMonth = startTime.format('M')
const startDay = startTime.format('D')
const theDayOfWeek = startTime.format('dddd')
const startDate = `${startMonth}${startDay}${theDayOfWeek}`
const gameRange = `${startTime.format('HH:mm')} - ${endTime.format('HH:mm')}`
return (
<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">{startMonth}</View>
<View className="day">{startDay}</View>
</View>
{/* Date time */}
<View className='detail-page-content-game-info-date-weather-calendar-date-date'>
<View className="date">{startDate}</View>
<View className="venue-time">{gameRange} {game_length}</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>{location_name || '-'}</Text>
<Text>·</Text>
<Text>{distance.toFixed(1)}km</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>{location || '-'}</Text>
</View>
</View>
</View>
{/* venue map */}
<View className='location-map'>
{longitude && latitude && (
<Map
className='location-map-map'
longitude={latitude}
latitude={longitude}
markers={[{ id: 1, latitude: longitude, longitude: latitude, iconPath: require('@/static/detail/icon-stark.svg'), width: 16, height: 16 }]}
includePoints={[{ latitude: longitude, longitude: latitude }, { latitude: currentLocation[0], longitude: currentLocation[1] }]}
includePadding={{ left: 50, right: 50, top: 50, bottom: 50 }}
onError={() => {}}
// hide business msg
showLocation
theme='dark'
/>
)}
</View>
</View>
</View>
)
}
function Index() {
// 使用Zustand store
// const userStats = useUserStats()
// const { incrementRequestCount, resetUserStats } = useUserActions()
const [current, setCurrent] = useState(0)
// const [textColor, setTextColor] = useState<string []>([])
const [detail, setDetail] = useState<any>(null)
const { params } = useRouter()
const [currentLocation, setCurrentLocation] = useState<[number, number]>([0, 0])
const { id, autoShare, from } = params
const { fetchUserInfo, updateUserInfo } = useUserActions()
console.group('params')
console.log(params)
console.groupEnd()
// 本地状态管理
const [loading, setLoading] = useState(false)
const sharePopupRef = useRef<any>(null)
// 页面加载时获取数据
// useEffect(() => {
// fetchDetail()
// calcBgMainColors()
// }, [])
useDidShow(async () => {
await updateLocation()
await fetchUserInfo()
await fetchDetail()
// calcBgMainColors()
})
const updateLocation = async () => {
try {
const location = await getCurrentLocation()
setCurrentLocation([location.latitude, location.longitude])
await updateUserInfo({ latitude: location.latitude, longitude: location.longitude })
console.log('用户位置更新成功')
} catch (error) {
console.error('用户位置更新失败', error)
}
}
const fetchDetail = async () => {
const res = await DetailService.getDetail(243/* 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
// // }
// if (detail?.image_list?.length > 0) {
// const { textColor } = await getTextColorOnImage(detail.image_list[0])
// textcolors[0] = textColor
// }
// setColors(textcolors)
// }
function handleShare() {
sharePopupRef.current.show()
}
const handleJoinGame = () => {
Taro.navigateTo({
url: `/pages/orderCheck/index?gameId=${243/* id */}`,
})
}
const tags = [{
name: '🕙 急招',
icon: '',
}, {
name: '🔥 本周热门',
icon: '',
}, {
name: '🎉 新活动',
icon: '',
}, {
name: '官方组织',
icon: '',
}]
const detailTags = ['室内', '硬地', '2号场', '有停车场', '有淋浴间', '有更衣室']
const { title, longitude, latitude } = detail || {}
console.log(longitude, latitude, 2222)
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: '双打',
},
]
console.log('detail', detail)
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={detail?.image_list?.[0] ? { backgroundImage: `url(${detail?.image_list?.[0]})` } : {}} />
<View className='detail-page-bg-text' />
{/* swiper */}
<View className="detail-swiper-container">
<View className="detail-swiper-scroll-container">
{
detail?.image_list?.length > 0 && detail?.image_list.map((item, index) => {
return (
<View className='detail-swiper-item' key={index}>
<Image
src={item}
mode="aspectFill"
className='detail-swiper-item-image'
/>
</View>
)
})
}
</View>
</View>
{/* <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 */}
<GameInfo detail={detail} currentLocation={currentLocation} />
{/* 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 */}
<StickyButton handleShare={handleShare} handleJoinGame={handleJoinGame} detail={detail} />
{/* share popup */}
<SharePopup ref={sharePopupRef} id={id as string} from={from as string} />
</View>
</View>
)
}
export default withAuth(Index)