Merge branch 'feat/liujie'

This commit is contained in:
2025-09-07 20:40:04 +08:00
5 changed files with 415 additions and 320 deletions

View File

@@ -16,6 +16,8 @@
position: sticky;
top: 0;
z-index: 100;
overflow: hidden;
background-color: rgba(0, 0, 0, 0.2);
}
.detail-navigator {
@@ -63,12 +65,26 @@
left: 0;
top: 0;
background-size: cover;
filter: blur(40px);
transform: scale(1.5);
// filter: blur(40px);
// transform: scale(1.5);
z-index: -2;
width: calc(100% + 20px);
height: calc(100% + 20px);
margin: -10px;
// width: calc(100% + 20px);
// height: calc(100% + 20px);
// margin: -10px;
width: 100vw;
height: 100vh;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.80) 0%, rgba(0, 0, 0, 0.40) 100%);
backdrop-filter: blur(100px);
}
}
.detail-page-bg-text {
@@ -215,7 +231,6 @@
display: flex;
width: 48px;
height: 48px;
padding-bottom: 6px;
box-sizing: border-box;
flex-direction: column;
align-items: center;
@@ -225,6 +240,7 @@
background: rgba(255, 255, 255, 0.25);
overflow: hidden;
color: #FFF;
background: #536272;
.month {
width: 100%;
@@ -235,20 +251,21 @@
box-sizing: border-box;
justify-content: center;
align-items: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
// border-bottom: 1px solid rgba(255, 255, 255, 0.08);
background: #7B828B;
}
.day {
display: flex;
width: 48px;
padding-bottom: 6px;
height: 30px;
// padding-bottom: 6px;
box-sizing: border-box;
flex-direction: column;
align-items: center;
gap: 4px;
// border: 0.5px solid rgba(255, 255, 255, 0.08);
// background: rgba(255, 255, 255, 0.25);
// background-color: #536272;
}
}
@@ -334,7 +351,7 @@
align-items: center;
overflow: hidden;
// border: 0.5px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.25);
// background: rgba(255, 255, 255, 0.25);
&-image {
width: 20px;
@@ -399,7 +416,7 @@
}
}
&-detail {
&-venue {
padding: 24px 15px 0;
box-sizing: border-box;
@@ -408,7 +425,7 @@
height: 31px;
align-items: center;
justify-content: flex-start;
gap: 4px;
gap: 8px;
padding-bottom: 6px;
color: #FFF;
text-overflow: ellipsis;
@@ -419,9 +436,15 @@
line-height: 24px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
&-notice-icon {
width: 14px;
height: 14px;
.venue-reserve-status {
display: inline-flex;
justify-content: flex-start;
align-items: center;
gap: 4px;
.venue-reserve-screenshot {
width: 16px;
height: 16px;
}
}
}
@@ -461,6 +484,51 @@
line-height: 24px; /* 160% */
}
}
.venue-screenshot-title {
display: flex;
padding: 18px 20px 10px 20px;
align-items: center;
align-self: stretch;
font-family: "PingFang SC";
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 24px; /* 150% */
letter-spacing: -0.23px;
}
.venue-screenshot-scroll-view {
max-height: calc(100vh - 260px);
overflow-y: auto;
padding-bottom: 40px;
.venue-screenshot-image-list {
width: 100%;
padding: 0 16px;
box-sizing: border-box;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px 10px;
.venue-screenshot-image-item {
aspect-ratio: 1/1;
border-radius: 9px;
border: 1px solid rgba(0, 0, 0, 0.12);
box-sizing: border-box;
background: rgba(0, 0, 0, 0.06);
margin: 0;
position: relative;
.venue-screenshot-image-item-image {
width: 100%;
height: 100%;
border-radius: 9px;
margin: 0;
}
}
}
}
}
&-gameplay-requirements {
@@ -591,7 +659,7 @@
.participants-list-item {
display: flex;
width: 108px;
padding: 16px 12px 10px 12px;
padding: 16px 4px 10px 4px;
box-sizing: border-box;
flex-direction: column;
justify-content: center;
@@ -608,6 +676,7 @@
}
&-name {
width: 100%;
color: rgba(255, 255, 255, 0.85);
text-align: center;
font-feature-settings: 'liga' off, 'clig' off;
@@ -616,6 +685,9 @@
font-style: normal;
font-weight: 500;
line-height: 24px; /* 184.615% */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&-level {

View File

@@ -1,6 +1,6 @@
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 { Avatar, Popover, ImagePreview } from '@nutui/nutui-react-taro'
import Taro, { useRouter, useShareAppMessage, useShareTimeline, useDidShow } from '@tarojs/taro'
import dayjs from 'dayjs'
import 'dayjs/locale/zh-cn'
@@ -86,7 +86,6 @@ function StickyButton(props) {
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">
@@ -214,117 +213,196 @@ function GameInfo(props) {
)
}
function Index() {
// 使用Zustand store
// const userStats = useUserStats()
// const { incrementRequestCount, resetUserStats } = useUserActions()
// 场馆信息
function VenueInfo(props) {
const { detail } = props
const [visible, setVisible] = useState(false)
const { venue_description, venue_description_tag = [], venue_image_list = [] } = detail
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)
function showScreenShot() {
setVisible(true)
}
function onClose() {
setVisible(false)
}
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 */}`,
function previewImage(current_url) {
Taro.previewImage({
current: current_url,
urls: venue_image_list.map(c => c.url),
})
}
return (
<View className='detail-page-content-venue'>
{/* venue detail title and venue ordered status */}
<View className='venue-detail-title'>
<Text></Text>
{venue_image_list?.length > 0 ?
<>
<Text>·</Text>
<View className="venue-reserve-status" onClick={showScreenShot}>
<Text></Text>
<Image className="venue-reserve-screenshot" src={img.ICON_DETAIL_ARROW_RIGHT} />
</View>
</>
:
''
}
</View>
{/* venue detail content */}
<View className='venue-detail-content'>
{/* venue detail tags */}
<View className='venue-detail-content-tags'>
{insertDotInTags(venue_description_tag).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>{venue_description}</Text>
</View>
</View>
<CommonPopup
visible={visible}
onClose={onClose}
round
hideFooter
position='bottom'
zIndex={1001}
>
<View className="venue-screenshot-title"></View>
<ScrollView
scrollY
className="venue-screenshot-scroll-view"
>
<View className="venue-screenshot-image-list">
{venue_image_list.map(item => {
return (
<View className="venue-screenshot-image-item" onClick={previewImage.bind(null, item.url)}>
<Image className="venue-screenshot-image-item-image" src={item.url} />
</View>
)
})}
</View>
</ScrollView>
</CommonPopup>
</View>
)
}
const tags = [{
name: '🕙 急招',
icon: '',
}, {
name: '🔥 本周热门',
icon: '',
}, {
name: '🎉 新活动',
icon: '',
}, {
name: '官方组织',
icon: '',
}]
function genNTRPRequirementText(min, max) {
if (min && max) {
return `${min} - ${max} 之间`
} else if (max) {
return `${max} 以上`
}
return '没有要求'
}
// 玩法要求
function GamePlayAndRequirement(props) {
const { detail: { skill_level_min, skill_level_max, play_type, game_type } } = props
const detailTags = ['室内', '硬地', '2号场', '有停车场', '有淋浴间', '有更衣室']
const { title, longitude, latitude } = detail || {}
console.log(longitude, latitude, 2222)
const requirements = [{
const requirements = [
{
title: 'NTRP水平要求',
desc: '2.0 - 4.5 之间',
}, {
desc: genNTRPRequirementText(skill_level_min, skill_level_max),
},
{
title: '活动玩法',
desc: '双打',
}, {
desc: play_type || '-',
},
{
title: '人员构成',
desc: '个人球局 · 组织者参与活动',
}]
desc: game_type || '-',
}
]
return (
<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>
)
}
const participants = detail?.participants || []
// 参与者
function Participants(props) {
const { detail = {} } = props
const participants = detail.participants || []
const organizer_id = Number(detail.publisher_id)
return (
<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) => {
const { user: { avatar_url, nickname, level, id: participant_user_id } } = participant
const role = participant_user_id === organizer_id ? '组织者' : '参与者'
return (
<View key={participant.id} className='participants-list-item'>
<Avatar className='participants-list-item-avatar' src={avatar_url} />
<Text className='participants-list-item-name'>{nickname || '未知'}</Text>
<Text className='participants-list-item-level'>{level || '未知'}</Text>
<Text className='participants-list-item-role'>{role}</Text>
</View>
)
})}
</View>
</ScrollView>
</View>
</View>
)
}
const supplementalNotesTags = ['仅限男生', '装备自备', '其他']
function SupplementalNotes(props) {
const { detail: { description, description_tag = [] } } = props
return (
<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(description_tag).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>{description}</Text>
</View>
</View>
</View>
)
}
function OrganizerInfo(props) {
const recommendGames = [
{
title: '黄浦日场对拉',
@@ -366,200 +444,7 @@ function Index() {
playType: '双打',
},
]
function handleBack() {
const pages = Taro.getCurrentPages()
if (pages.length <= 1) {
Taro.redirectTo({
url: '/pages/list/index',
})
} else {
Taro.navigateBack()
}
}
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={handleBack}>
<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'>
@@ -635,6 +520,145 @@ function Index() {
</ScrollView>
</View>
</View>
)
}
function Index() {
const [detail, setDetail] = useState<any>({})
const { params } = useRouter()
const [currentLocation, setCurrentLocation] = useState<[number, number]>([0, 0])
const { id, from } = params
const { fetchUserInfo, updateUserInfo } = useUserActions()
const sharePopupRef = useRef<any>(null)
useDidShow(async () => {
await updateLocation()
await fetchUserInfo()
await fetchDetail()
})
const updateLocation = async () => {
try {
const location = await getCurrentLocation()
setCurrentLocation([location.latitude, location.longitude])
await updateUserInfo({ latitude: location.latitude, longitude: location.longitude })
} catch (error) {
console.error('用户位置更新失败', error)
}
}
const fetchDetail = async () => {
const res = await DetailService.getDetail(243/* Number(id) */)
if (res.code === 0) {
setDetail(res.data)
}
}
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: '',
}]
function handleBack() {
const pages = Taro.getCurrentPages()
if (pages.length <= 1) {
Taro.redirectTo({
url: '/pages/list/index',
})
} else {
Taro.navigateBack()
}
}
console.log('detail', detail)
const backgroundImage = detail?.image_list?.[0] ? { backgroundImage: `url(${detail?.image_list?.[0]})` } : {}
return (
<View className='detail-page'>
{/* custom navbar */}
<view className="custom-navbar">
<View className='detail-navigator'>
<View className='detail-navigator-back' onClick={handleBack}>
<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} />
{/* 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>
{/* 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'>{detail.title}</Text>
</View>
{/* Date and Place and weather */}
<GameInfo detail={detail} currentLocation={currentLocation} />
{/* detail */}
<VenueInfo detail={detail} />
{/* gameplay requirements */}
<GamePlayAndRequirement detail={detail} />
{/* participants */}
<Participants detail={detail} />
{/* supplemental notes */}
<SupplementalNotes detail={detail} />
{/* organizer and recommend games by organizer */}
<OrganizerInfo detail={detail} />
{/* sticky bottom action bar */}
<StickyButton handleShare={handleShare} handleJoinGame={handleJoinGame} detail={detail} />
{/* share popup */}

View File

@@ -45,7 +45,6 @@ const LoginPage: React.FC = () => {
setTimeout(() => {
if (redirect) {
console.log('redirect:', decodeURIComponent(redirect))
Taro.redirectTo({ url: decodeURIComponent(redirect) });
} else {
Taro.redirectTo({ url: '/pages/list/index' });

View File

@@ -211,12 +211,13 @@ class HttpService {
}
try {
console.log(this.buildHeaders(config), 1111);
const reqHeader = this.buildHeaders(config)
this.log('info', 'HTTP REQ HEADER: ', reqHeader)
const requestConfig = {
url: fullUrl,
method: method,
data: method !== 'GET' ? data : undefined,
header: this.buildHeaders(config),
header: reqHeader,
timeout: this.timeout
}

View File

@@ -34,7 +34,6 @@ export const useUser = create<UserState>()((set) => ({
},
updateUserInfo: async(userInfo: Partial<UserInfoType>) => {
const res = await updateUserProfile(userInfo)
console.log(res)
set({ user: res.data })
}
}))