发布球局
This commit is contained in:
49
src/components/ActivityTypeSwitch/index.module.scss
Normal file
49
src/components/ActivityTypeSwitch/index.module.scss
Normal file
@@ -0,0 +1,49 @@
|
||||
.activity-type-switch {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
padding: 0 4px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
height: 40px;
|
||||
border-radius: 12px;
|
||||
padding: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.switch-tab {
|
||||
flex: 1;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #e5e5e5;
|
||||
color: #1890ff;
|
||||
opacity: 0.3;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.switch-tab.active {
|
||||
background: white;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
box-shadow: 0px 4px 48px 0px rgba(0, 0, 0, 0.08);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.icon-style {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
@use '~@/scss/themeColor.scss' as theme;
|
||||
|
||||
.activity-type-switch {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
padding: 0 4px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
height: 40px;
|
||||
border-radius: 12px;
|
||||
padding: 4px;
|
||||
overflow: hidden;
|
||||
.switch-tab {
|
||||
flex: 1;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #e5e5e5;
|
||||
color: theme.$primary-color;
|
||||
opacity: 0.3;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
.icon-style {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
&.active {
|
||||
background: white;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
box-shadow: 0px 4px 48px 0px rgba(0, 0, 0, 0.08);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from 'react'
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import images from '@/config/images'
|
||||
|
||||
import './index.scss'
|
||||
import styles from './index.module.scss'
|
||||
|
||||
export type ActivityType = 'individual' | 'group'
|
||||
|
||||
@@ -13,22 +12,22 @@ interface ActivityTypeSwitchProps {
|
||||
|
||||
const ActivityTypeSwitch: React.FC<ActivityTypeSwitchProps> = ({ value, onChange }) => {
|
||||
return (
|
||||
<View className='activity-type-switch'>
|
||||
<View className={styles['activity-type-switch']}>
|
||||
<View
|
||||
className={`switch-tab ${value === 'individual' ? 'active' : ''}`}
|
||||
className={`${styles['switch-tab']} ${value === 'individual' ? styles.active : ''}`}
|
||||
onClick={() => onChange('individual')}
|
||||
>
|
||||
<View className='tab-icon'>
|
||||
<Image src={images.ICON_PERSONAL} className='icon-style' />
|
||||
<View className={styles['tab-icon']}>
|
||||
<Image src={images.ICON_PERSONAL} className={styles['icon-style']} />
|
||||
</View>
|
||||
<Text className='tab-text'>个人约球</Text>
|
||||
<Text className={styles['tab-text']}>个人约球</Text>
|
||||
</View>
|
||||
<View
|
||||
className={`switch-tab ${value === 'group' ? 'active' : ''}`}
|
||||
className={`${styles['switch-tab']} ${value === 'group' ? styles.active : ''}`}
|
||||
onClick={() => onChange('group')}
|
||||
>
|
||||
<Image src={images.ICON_CHANGDA} className='icon-style' />
|
||||
<Text className='tab-text'>畅打活动</Text>
|
||||
<Image src={images.ICON_CHANGDA} className={styles['icon-style']} />
|
||||
<Text className={styles['tab-text']}>畅打活动</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
color: #1f2329;
|
||||
border: none;
|
||||
width: 154px;
|
||||
height: 36px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.06);
|
||||
background: #fff;
|
||||
@@ -53,7 +53,7 @@
|
||||
.common-popup__btn-confirm {
|
||||
/* 使用按钮组件的 primary 样式 */
|
||||
width: 154px;
|
||||
height: 36px;
|
||||
height: 44px;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.06);
|
||||
background: #000;
|
||||
border-radius: 12px;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { default, type CoverImage } from './CoverImageUpload'
|
||||
@@ -25,7 +25,7 @@
|
||||
width: 108px;
|
||||
height: 108px;
|
||||
border-radius: 12px;
|
||||
margin-right: 12px;
|
||||
margin-right: 6px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useMemo, useCallback } from 'react'
|
||||
import { View, Text, Image, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import './CoverImageUpload.scss'
|
||||
import './ImageUpload.scss'
|
||||
|
||||
export interface CoverImage {
|
||||
id: string
|
||||
@@ -9,13 +9,13 @@ export interface CoverImage {
|
||||
tempFilePath?: string
|
||||
}
|
||||
|
||||
interface CoverImageUploadProps {
|
||||
interface ImageUploadProps {
|
||||
images: CoverImage[]
|
||||
onChange: (images: CoverImage[]) => void
|
||||
maxCount?: number
|
||||
}
|
||||
|
||||
const CoverImageUpload: React.FC<CoverImageUploadProps> = ({
|
||||
const ImageUpload: React.FC<ImageUploadProps> = ({
|
||||
images,
|
||||
onChange,
|
||||
maxCount = 9
|
||||
@@ -88,4 +88,4 @@ const CoverImageUpload: React.FC<CoverImageUploadProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
export default CoverImageUpload
|
||||
export default ImageUpload
|
||||
1
src/components/ImageUpload/index.ts
Normal file
1
src/components/ImageUpload/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default, type CoverImage } from './ImageUpload'
|
||||
@@ -1,149 +0,0 @@
|
||||
.ntrp-slider {
|
||||
// 区域标题 - 灰色背景
|
||||
.section-title-wrapper {
|
||||
margin-bottom: 16px;
|
||||
padding: 0 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.section-summary {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
// NTRP控制区域 - 白色块
|
||||
.ntrp-control-section {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 20px 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.ntrp-slider-container {
|
||||
.ntrp-labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.ntrp-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.ntrp-slider-track {
|
||||
position: relative;
|
||||
height: 40px;
|
||||
margin: 0 12px;
|
||||
|
||||
.slider-bg {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.slider-range {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
height: 4px;
|
||||
background: #333;
|
||||
border-radius: 2px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.slider-thumb {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #333;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
z-index: 2;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.active {
|
||||
transform: translate(-50%, -50%) scale(1.2);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.thumb-value {
|
||||
position: absolute;
|
||||
top: -30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #333;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
&.active .thumb-value {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 暗色模式适配
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.ntrp-slider {
|
||||
.section-title-wrapper {
|
||||
.section-title {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.section-summary {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.ntrp-control-section {
|
||||
background: #2d2d2d;
|
||||
|
||||
.ntrp-labels .ntrp-label {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.slider-bg {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.slider-range {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.slider-thumb {
|
||||
background: #fff;
|
||||
border-color: #2d2d2d;
|
||||
|
||||
.thumb-value {
|
||||
background: #fff;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
import React, { useState, useCallback } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import './NTRPSlider.scss'
|
||||
|
||||
export interface NTRPRange {
|
||||
min: number
|
||||
max: number
|
||||
}
|
||||
|
||||
// 获取NTRP显示文本的工具函数
|
||||
export const getNTRPRangeText = (range: NTRPRange): string => {
|
||||
if (range.min === 2.0 && range.max === 4.0) {
|
||||
return '不限'
|
||||
}
|
||||
return `${range.min} - ${range.max}`
|
||||
}
|
||||
|
||||
interface NTRPSliderProps {
|
||||
value: NTRPRange
|
||||
onChange: (range: NTRPRange) => void
|
||||
title?: string
|
||||
showTitle?: boolean
|
||||
}
|
||||
|
||||
const NTRPSlider: React.FC<NTRPSliderProps> = ({
|
||||
value = {
|
||||
min: 1.0,
|
||||
max: 5.0
|
||||
},
|
||||
onChange,
|
||||
title = 'NTRP水平要求',
|
||||
showTitle = false
|
||||
}) => {
|
||||
const [activeThumb, setActiveThumb] = useState<'min' | 'max' | null>(null)
|
||||
|
||||
// 计算滑动条位置百分比
|
||||
const getSliderPercentage = useCallback((level: number) => {
|
||||
return ((level - 2.0) / 2.0) * 100
|
||||
}, [])
|
||||
|
||||
// 获取当前NTRP显示文本
|
||||
const currentRangeText = getNTRPRangeText(value)
|
||||
|
||||
const handleSliderTouchStart = useCallback((thumb: 'min' | 'max') => {
|
||||
setActiveThumb(thumb)
|
||||
}, [])
|
||||
|
||||
const handleSliderTouchMove = useCallback((e: any) => {
|
||||
if (!activeThumb) return
|
||||
|
||||
e.preventDefault()
|
||||
const query = Taro.createSelectorQuery()
|
||||
query.select('.ntrp-slider-track').boundingClientRect((rect: any) => {
|
||||
if (rect && !Array.isArray(rect)) {
|
||||
const touch = e.touches[0]
|
||||
const relativeX = touch.clientX - rect.left
|
||||
const percentage = Math.max(0, Math.min(1, relativeX / rect.width))
|
||||
const level = Number((2.0 + percentage * 2.0).toFixed(1))
|
||||
|
||||
if (activeThumb === 'min') {
|
||||
const newMin = Math.min(level, value.max - 0.1)
|
||||
onChange({ min: newMin, max: value.max })
|
||||
} else {
|
||||
const newMax = Math.max(level, value.min + 0.1)
|
||||
onChange({ min: value.min, max: newMax })
|
||||
}
|
||||
}
|
||||
}).exec()
|
||||
}, [activeThumb, value, onChange])
|
||||
|
||||
const handleSliderTouchEnd = useCallback(() => {
|
||||
setActiveThumb(null)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View className='ntrp-slider'>
|
||||
{showTitle && (
|
||||
<View className='section-title-wrapper'>
|
||||
<Text className='section-title'>{title}</Text>
|
||||
<Text className='section-summary'>{currentRangeText}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className='ntrp-control-section'>
|
||||
<View className='ntrp-slider-container'>
|
||||
<View className='ntrp-labels'>
|
||||
<Text className='ntrp-label'>2.0及以下</Text>
|
||||
<Text className='ntrp-label'>4.0及以上</Text>
|
||||
</View>
|
||||
|
||||
<View
|
||||
className='ntrp-slider-track'
|
||||
onTouchMove={handleSliderTouchMove}
|
||||
onTouchEnd={handleSliderTouchEnd}
|
||||
>
|
||||
{/* 背景轨道 */}
|
||||
<View className='slider-bg'></View>
|
||||
|
||||
{/* 选中区间 */}
|
||||
<View
|
||||
className='slider-range'
|
||||
style={{
|
||||
left: `${getSliderPercentage(value.min)}%`,
|
||||
width: `${getSliderPercentage(value.max) - getSliderPercentage(value.min)}%`
|
||||
}}
|
||||
></View>
|
||||
|
||||
{/* 最小值滑块 */}
|
||||
<View
|
||||
className={`slider-thumb ${activeThumb === 'min' ? 'active' : ''}`}
|
||||
style={{ left: `${getSliderPercentage(value.min)}%` }}
|
||||
onTouchStart={() => handleSliderTouchStart('min')}
|
||||
>
|
||||
<View className='thumb-value'>{value.min}</View>
|
||||
</View>
|
||||
|
||||
{/* 最大值滑块 */}
|
||||
<View
|
||||
className={`slider-thumb ${activeThumb === 'max' ? 'active' : ''}`}
|
||||
style={{ left: `${getSliderPercentage(value.max)}%` }}
|
||||
onTouchStart={() => handleSliderTouchStart('max')}
|
||||
>
|
||||
<View className='thumb-value'>{value.max}</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default NTRPSlider
|
||||
@@ -1 +0,0 @@
|
||||
export { default, type NTRPRange, getNTRPRangeText } from './NTRPSlider'
|
||||
@@ -1,16 +1,16 @@
|
||||
import React from 'react'
|
||||
import { View, Text, Button } from '@tarojs/components'
|
||||
import './ParticipantsControl.scss'
|
||||
import './NumberInterval.scss'
|
||||
import { InputNumber } from '@nutui/nutui-react-taro'
|
||||
|
||||
interface ParticipantsControlProps {
|
||||
interface NumberIntervalProps {
|
||||
minParticipants: number
|
||||
maxParticipants: number
|
||||
onMinParticipantsChange: (value: number) => void
|
||||
onMaxParticipantsChange: (value: number) => void
|
||||
}
|
||||
|
||||
const ParticipantsControl: React.FC<ParticipantsControlProps> = ({
|
||||
const NumberInterval: React.FC<NumberIntervalProps> = ({
|
||||
minParticipants,
|
||||
maxParticipants,
|
||||
onMinParticipantsChange,
|
||||
@@ -46,4 +46,4 @@ const ParticipantsControl: React.FC<ParticipantsControlProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
export default ParticipantsControl
|
||||
export default NumberInterval
|
||||
1
src/components/NumberInterval/index.ts
Normal file
1
src/components/NumberInterval/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './NumberInterval'
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default } from './ParticipantsControl'
|
||||
export type { ParticipantsControlProps } from './ParticipantsControl'
|
||||
@@ -117,6 +117,21 @@ const SelectStadium: React.FC<SelectStadiumProps> = ({
|
||||
setSearchValue('')
|
||||
}
|
||||
|
||||
const handleItemLocation = (stadium: Stadium) => {
|
||||
console.log(stadium,'stadiumstadium');
|
||||
if(stadium.latitude && stadium.longitude){
|
||||
Taro.openLocation({
|
||||
latitude: stadium.latitude,
|
||||
longitude: stadium.longitude,
|
||||
name: stadium.name,
|
||||
address: stadium.address,
|
||||
success: (res) => {
|
||||
console.log(res,'resres');
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const markSearchText = (text: string) => {
|
||||
return text.replace(searchValue, `<span style="color: #007AFF;">${searchValue}</span>`)
|
||||
@@ -215,9 +230,9 @@ const SelectStadium: React.FC<SelectStadiumProps> = ({
|
||||
</View>
|
||||
<View className='stadium-item-right'>
|
||||
<View className='stadium-name' dangerouslySetInnerHTML={{ __html: markSearchText(stadium.name) }}></View>
|
||||
<View className='stadium-address'>
|
||||
<Text>{stadium.istance} · </Text>
|
||||
<Text>{stadium.address}</Text>
|
||||
<View className='stadium-address' >
|
||||
<Text onClick={(e) => { e.stopPropagation(); handleItemLocation(stadium); }}>{stadium.istance} · </Text>
|
||||
<Text onClick={(e) => { e.stopPropagation(); handleItemLocation(stadium); }}>{stadium.address}</Text>
|
||||
<Image src={images.ICON_ARRORW_SMALL} className='stadium-map-icon' />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -57,121 +57,93 @@
|
||||
|
||||
// 场地类型
|
||||
.venue-type-section {
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
flex-shrink: 0;
|
||||
|
||||
.section-title {
|
||||
padding: 18px 20px 10px 20px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.option-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 0 15px;
|
||||
.option-btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #e0e0e0;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&.selected {
|
||||
background: #333;
|
||||
border-color: #333;
|
||||
|
||||
.option-text {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.option-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 地面材质
|
||||
.ground-material-section {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
flex-shrink: 0;
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.option-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
.option-btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #e0e0e0;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&.selected {
|
||||
background: #333;
|
||||
border-color: #333;
|
||||
|
||||
.option-text {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.option-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 场地信息补充
|
||||
.additional-info-section {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
flex-shrink: 0;
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.additional-input {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
background: white;
|
||||
height: 44px;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
.heart-wrapper{
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.heart-icon{
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
z-index: 1;
|
||||
}
|
||||
.icon-bg{
|
||||
border-radius: 1.6px;
|
||||
width: 165px;
|
||||
height: 17px;
|
||||
flex-shrink: 0;
|
||||
border: 0.5px solid rgba(238, 255, 135, 0.00);
|
||||
opacity: 0.4;
|
||||
background: linear-gradient(258deg, rgba(220, 250, 97, 0.00) 6.85%, rgba(228, 255, 59, 0.82) 91.69%);
|
||||
backdrop-filter: blur(1.25px);
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 4px;
|
||||
}
|
||||
.heart-text{
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.90);
|
||||
z-index: 2;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #999;
|
||||
.option-buttons {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 0 15px;
|
||||
.textarea-tag-container{
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
background: #FFF;
|
||||
box-shadow: 0 4px 36px 0 rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.option-btn {
|
||||
border-radius: 20px;
|
||||
border: 1px solid #e0e0e0;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
border-radius: 999px;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.12);
|
||||
background: #FFF;
|
||||
font-weight: 500;
|
||||
&.selected {
|
||||
background: #000;
|
||||
border-color: #fff;
|
||||
border-radius: 999px;
|
||||
font-weight: 600;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.06);
|
||||
.option-text {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.option-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 底部按钮
|
||||
.bottom-actions {
|
||||
background: white;
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import React, { useState, useCallback } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import images from '@/config/images'
|
||||
import './StadiumDetail.scss'
|
||||
import TextareaTag from '@/components/TextareaTag'
|
||||
import CoverImageUpload, { type CoverImage } from '@/components/CoverImageUpload'
|
||||
import CoverImageUpload, { type CoverImage } from '@/components/ImageUpload'
|
||||
|
||||
export interface Stadium {
|
||||
id?: string
|
||||
name: string
|
||||
address?: string
|
||||
longitude?: number
|
||||
latitude?: number
|
||||
istance?: string
|
||||
}
|
||||
|
||||
interface StadiumDetailProps {
|
||||
@@ -21,57 +25,109 @@ const stadiumInfo = [
|
||||
{
|
||||
label: '场地类型',
|
||||
options: ['室内', '室外', '室外雨棚'],
|
||||
prop: 'venueType',
|
||||
type: 'tags'
|
||||
},
|
||||
{
|
||||
label: '地面材质',
|
||||
options: ['硬地', '红土', '草地'],
|
||||
prop: 'groundMaterial',
|
||||
type: 'tags'
|
||||
},
|
||||
{
|
||||
label: '场地信息补充',
|
||||
options: ['1号场', '2号场', '3号场', '4号场', '有空调', '6号场'],
|
||||
options: ['1号场', '2号场', '3号场', '4号场', '有空调', '6号场','6号场'],
|
||||
prop: 'additionalInfo',
|
||||
type: 'textareaTag'
|
||||
},
|
||||
{
|
||||
label: '场地预定截图',
|
||||
options: ['有其他场地信息可备注'],
|
||||
prop: 'imagesList',
|
||||
type: 'image'
|
||||
}
|
||||
]
|
||||
|
||||
// 公共的标题组件
|
||||
const SectionTitle: React.FC<{ title: string,prop: string }> = ({ title, prop }) => {
|
||||
console.log(prop,'propprop');
|
||||
if (prop === 'imagesList') {
|
||||
return (
|
||||
<View className='section-title'>
|
||||
<Text>{title}</Text>
|
||||
<View className='heart-wrapper'>
|
||||
<Image src={images.ICON_HEART_CIRCLE} className='heart-icon' />
|
||||
<View className='icon-bg'></View>
|
||||
<Text className='heart-text'>添加截图,平台会优先推荐</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Text className='section-title'>{title}</Text>
|
||||
)
|
||||
}
|
||||
|
||||
// 公共的容器组件
|
||||
const SectionContainer: React.FC<{ title: string; children: React.ReactNode, prop: string }> = ({ title, children, prop }) => (
|
||||
<View className='venue-type-section'>
|
||||
<SectionTitle title={title} prop={prop}/>
|
||||
<View className='option-buttons'>
|
||||
{children}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
||||
const StadiumDetail: React.FC<StadiumDetailProps> = ({
|
||||
stadium,
|
||||
onBack,
|
||||
onConfirm
|
||||
}) => {
|
||||
const [venueType, setVenueType] = useState('室内')
|
||||
const [groundMaterial, setGroundMaterial] = useState('硬地')
|
||||
const [additionalInfo, setAdditionalInfo] = useState('')
|
||||
const [imagesList, setImagesList] = useState<CoverImage[]>([])
|
||||
const [formData, setFormData] = useState({
|
||||
stadiumName: stadium.name,
|
||||
stadiumAddress: stadium.address,
|
||||
stadiumLongitude: stadium.longitude,
|
||||
stadiumLatitude: stadium.latitude,
|
||||
istance: stadium.istance,
|
||||
venueType: '室内',
|
||||
groundMaterial: '硬地',
|
||||
additionalInfo: '',
|
||||
imagesList: [] as CoverImage[]
|
||||
})
|
||||
|
||||
|
||||
const handleConfirm = () => {
|
||||
onConfirm(stadium, venueType, groundMaterial, additionalInfo)
|
||||
}
|
||||
|
||||
const handleMapLocation = () => {
|
||||
|
||||
Taro.chooseLocation({
|
||||
success: (res) => {
|
||||
console.log(res,'resres');
|
||||
setFormData({
|
||||
...formData,
|
||||
stadiumName: res.name,
|
||||
stadiumAddress: res.address,
|
||||
stadiumLongitude: res.longitude,
|
||||
stadiumLatitude: res.latitude
|
||||
})
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('选择位置失败:', err)
|
||||
Taro.showToast({
|
||||
title: '位置选择失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getSelectedByLabel = useCallback((label: string) => {
|
||||
if (label === '场地类型') return venueType
|
||||
if (label === '地面材质') return groundMaterial
|
||||
return ''
|
||||
}, [venueType, groundMaterial])
|
||||
|
||||
const setSelectedByLabel = useCallback((label: string, value: string) => {
|
||||
if (label === '场地类型') {
|
||||
setVenueType(value)
|
||||
} else if (label === '地面材质') {
|
||||
setGroundMaterial(value)
|
||||
}
|
||||
const updateFormData = useCallback((prop: string, value: any) => {
|
||||
setFormData(prev => ({ ...prev, [prop]: value }))
|
||||
}, [])
|
||||
|
||||
const getSelectedByLabel = useCallback((label: string) => {
|
||||
if (label === '场地类型') return formData.venueType
|
||||
if (label === '地面材质') return formData.groundMaterial
|
||||
return ''
|
||||
}, [formData.venueType, formData.groundMaterial])
|
||||
|
||||
|
||||
console.log(stadium,'stadiumstadium');
|
||||
return (
|
||||
<View className='stadium-detail'>
|
||||
@@ -84,9 +140,10 @@ const StadiumDetail: React.FC<StadiumDetailProps> = ({
|
||||
<Image src={images.ICON_STADIUM} className='stadium-icon' />
|
||||
</View>
|
||||
<View className='stadium-item-right'>
|
||||
<View className='stadium-name'>{stadium.name}</View>
|
||||
<View className='stadium-name'>{formData.stadiumName}</View>
|
||||
<View className='stadium-address'>
|
||||
<Text>{stadium.address}</Text>
|
||||
<Text>{formData.istance} · </Text>
|
||||
<Text>{formData.stadiumAddress}</Text>
|
||||
<Image src={images.ICON_ARRORW_SMALL} className='stadium-map-icon' />
|
||||
</View>
|
||||
</View>
|
||||
@@ -96,54 +153,43 @@ const StadiumDetail: React.FC<StadiumDetailProps> = ({
|
||||
if (item.type === 'tags') {
|
||||
const selected = getSelectedByLabel(item.label)
|
||||
return (
|
||||
<View className='venue-type-section' key={item.label}>
|
||||
<Text className='section-title'>{item.label}</Text>
|
||||
<View className='option-buttons'>
|
||||
{item.options.map((opt) => (
|
||||
<View
|
||||
key={opt}
|
||||
className={`option-btn ${selected === opt ? 'selected' : ''}`}
|
||||
onClick={() => setSelectedByLabel(item.label, opt)}
|
||||
>
|
||||
<Text className='option-text'>{opt}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
<SectionContainer key={item.label} title={item.label} prop={item.prop}>
|
||||
{item.options.map((opt) => (
|
||||
<View
|
||||
key={opt}
|
||||
className={`option-btn ${selected === opt ? 'selected' : ''}`}
|
||||
onClick={() => updateFormData(item.prop, opt)}
|
||||
>
|
||||
<Text className='option-text'>{opt}</Text>
|
||||
</View>
|
||||
))}
|
||||
</SectionContainer>
|
||||
)
|
||||
}
|
||||
|
||||
if (item.type === 'textareaTag') {
|
||||
return (
|
||||
<View className='venue-type-section' key={item.label}>
|
||||
<Text className='section-title'>{item.label}</Text>
|
||||
<View className='option-buttons'>
|
||||
<SectionContainer key={item.label} title={item.label} prop={item.prop}>
|
||||
<View className='textarea-tag-container'>
|
||||
<TextareaTag
|
||||
key={item.label}
|
||||
value={additionalInfo}
|
||||
onChange={setAdditionalInfo}
|
||||
value={formData.additionalInfo}
|
||||
onChange={(value) => updateFormData(item.prop, value)}
|
||||
placeholder='有其他场地信息可备注'
|
||||
options={(item.options || []).map((o) => ({ label: o, value: o }))}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
</SectionContainer>
|
||||
)
|
||||
}
|
||||
|
||||
if (item.type === 'image') {
|
||||
return (
|
||||
<View className='venue-type-section' key={item.label}>
|
||||
<Text className='section-title'>{item.label}</Text>
|
||||
<View className='option-buttons'>
|
||||
<SectionContainer key={item.label} title={item.label} prop={item.prop}>
|
||||
<CoverImageUpload
|
||||
key={item.label}
|
||||
images={imagesList}
|
||||
onChange={setImagesList}
|
||||
images={formData.imagesList}
|
||||
onChange={(images) => updateFormData(item.prop, images)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
</SectionContainer>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
border-radius: 16px;
|
||||
padding: 10px 16px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
.input-wrapper {
|
||||
margin-top: 8px;
|
||||
.additional-input {
|
||||
@@ -36,6 +37,7 @@
|
||||
gap: 6px;
|
||||
.nut-checkbox{
|
||||
margin-right: 0;
|
||||
|
||||
.nut-checkbox-button{
|
||||
border: 1px solid theme.$primary-border-color;
|
||||
color: theme.$primary-color;
|
||||
@@ -43,6 +45,7 @@
|
||||
font-size: 12px;
|
||||
padding: 2px 6px;
|
||||
margin-right: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import { View, Input } from '@tarojs/components'
|
||||
|
||||
import { View } from '@tarojs/components'
|
||||
import { TextArea } from '@nutui/nutui-react-taro'
|
||||
import './index.scss'
|
||||
|
||||
interface TitleInputProps {
|
||||
@@ -18,13 +18,14 @@ const TitleInput: React.FC<TitleInputProps> = ({
|
||||
}) => {
|
||||
return (
|
||||
<View className='title-input-wrapper'>
|
||||
<Input
|
||||
<TextArea
|
||||
className='title-input'
|
||||
placeholder={placeholder}
|
||||
placeholderClass='title-placeholder'
|
||||
value={value}
|
||||
onInput={(e) => onChange(e.detail.value)}
|
||||
maxlength={maxLength}
|
||||
autoSize={true}
|
||||
placeholderClass='title-input-placeholder'
|
||||
/>
|
||||
<View className='char-count'>{value.length}/{maxLength}</View>
|
||||
</View>
|
||||
|
||||
@@ -1,35 +1,34 @@
|
||||
.title-input-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-around;
|
||||
.title-input {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
padding: 0 16px;
|
||||
border: 1px solid #e5e5e5;
|
||||
width: 83%;
|
||||
min-height: 44px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
line-height: 44px;
|
||||
line-height: 1.4;
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
&:focus {
|
||||
border-color: #007aff;
|
||||
outline: none;
|
||||
}
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.title-placeholder {
|
||||
color: #999;
|
||||
font-size: 16px;
|
||||
// 使用 placeholderClass 来控制 placeholder 样式
|
||||
.title-input-placeholder {
|
||||
color: rgba(60, 60, 67, 0.60) !important;
|
||||
font-size: 16px !important;
|
||||
font-weight: normal !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.char-count {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
pointer-events: none;
|
||||
padding-top: 12px;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import ActivityTypeSwitch from './ActivityTypeSwitch'
|
||||
import TextareaTag from './TextareaTag'
|
||||
import FormSwitch from './FormSwitch'
|
||||
import CoverImageUpload from './CoverImageUpload'
|
||||
import ImageUpload from './ImageUpload'
|
||||
import FormBasicInfo from './FormBasicInfo'
|
||||
import Range from './Range'
|
||||
import ParticipantsControl from './ParticipantsControl'
|
||||
import NumberInterval from './NumberInterval'
|
||||
import { SelectStadium, StadiumDetail } from './SelectStadium'
|
||||
import TimeSelector from './TimeSelector'
|
||||
import TitleInput from './TitleInput'
|
||||
@@ -14,10 +14,10 @@ export {
|
||||
ActivityTypeSwitch,
|
||||
TextareaTag,
|
||||
FormSwitch,
|
||||
CoverImageUpload,
|
||||
ImageUpload,
|
||||
FormBasicInfo,
|
||||
Range,
|
||||
ParticipantsControl,
|
||||
NumberInterval,
|
||||
SelectStadium,
|
||||
TimeSelector,
|
||||
TitleInput,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { getNTRPRangeText, type NTRPRange } from './NTRPSlider'
|
||||
import { type TimeRange } from './TimeSelector'
|
||||
import { type Stadium } from './SelectStadium'
|
||||
import { type ActivityType } from './ActivityTypeSwitch'
|
||||
import { type CoverImage } from './CoverImageUpload'
|
||||
export type { NTRPRange, getNTRPRangeText, TimeRange, Stadium, ActivityType, CoverImage }
|
||||
import { type CoverImage } from './ImageUpload'
|
||||
export type { TimeRange, Stadium, ActivityType, CoverImage }
|
||||
@@ -7,7 +7,6 @@ export type EnvType = 'development' | 'test' | 'production'
|
||||
export interface EnvConfig {
|
||||
name: string
|
||||
apiBaseURL: string
|
||||
apiVersion: string
|
||||
timeout: number
|
||||
enableLog: boolean
|
||||
enableMock: boolean
|
||||
@@ -18,8 +17,7 @@ const envConfigs: Record<EnvType, EnvConfig> = {
|
||||
// 开发环境
|
||||
development: {
|
||||
name: '开发环境',
|
||||
apiBaseURL: 'https://dev-api.playballtogether.com',
|
||||
apiVersion: 'v1',
|
||||
apiBaseURL: 'https://sit.light120.com',
|
||||
timeout: 15000,
|
||||
enableLog: true,
|
||||
enableMock: true
|
||||
@@ -28,8 +26,7 @@ const envConfigs: Record<EnvType, EnvConfig> = {
|
||||
// 测试环境
|
||||
test: {
|
||||
name: '测试环境',
|
||||
apiBaseURL: 'https://test-api.playballtogether.com',
|
||||
apiVersion: 'v1',
|
||||
apiBaseURL: 'https://sit.light120.com',
|
||||
timeout: 12000,
|
||||
enableLog: true,
|
||||
enableMock: false
|
||||
@@ -38,8 +35,7 @@ const envConfigs: Record<EnvType, EnvConfig> = {
|
||||
// 生产环境
|
||||
production: {
|
||||
name: '生产环境',
|
||||
apiBaseURL: 'https://api.playballtogether.com',
|
||||
apiVersion: 'v1',
|
||||
apiBaseURL: 'https://sit.light120.com',
|
||||
timeout: 10000,
|
||||
enableLog: false,
|
||||
enableMock: false
|
||||
|
||||
@@ -59,7 +59,7 @@ export const publishBallFormSchema: FormFieldConfig[] = [
|
||||
placeholder: '好的标题更吸引人哦',
|
||||
required: true,
|
||||
props: {
|
||||
maxLength: 20
|
||||
maxLength: 80
|
||||
},
|
||||
rules: [
|
||||
{ required: true, message: '请输入活动标题' },
|
||||
|
||||
@@ -13,4 +13,8 @@ export default {
|
||||
ICON_STADIUM: require('@/static/publishBall/icon-stadium.svg'),
|
||||
ICON_ARRORW_SMALL: require('@/static/publishBall/icon-arrow-small.svg'),
|
||||
ICON_MAP_SEARCH: require('@/static/publishBall/icon-map-search.svg'),
|
||||
ICON_HEART_CIRCLE: require('@/static/publishBall/icon-heartcircle.png'),
|
||||
ICON_ADD: require('@/static/publishBall/icon-add.svg'),
|
||||
ICON_COPY: require('@/static/publishBall/icon-arrow-right.svg'),
|
||||
ICON_DELETE: require('@/static/publishBall/icon-delete.svg')
|
||||
}
|
||||
1
src/pages/index/index.module.scss
Normal file
1
src/pages/index/index.module.scss
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -8,12 +8,28 @@
|
||||
&__scroll {
|
||||
height: calc(100vh - 120px);
|
||||
overflow: auto;
|
||||
padding: 4px 16px 0 16px;
|
||||
padding: 4px 16px 72px 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&__content {
|
||||
padding-bottom: 72px;
|
||||
}
|
||||
&__add{
|
||||
margin-top: 2px;
|
||||
border-radius: 12px;
|
||||
border: 2px dashed rgba(22, 24, 35, 0.12);
|
||||
display: flex;
|
||||
width: 343px;
|
||||
height: 60px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: rgba(60, 60, 67, 0.50);
|
||||
|
||||
&-icon{
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
.activity-type-switch{
|
||||
padding: 4px 16px 0 16px;
|
||||
@@ -22,6 +38,60 @@
|
||||
|
||||
}
|
||||
|
||||
// 场次标题行
|
||||
.session-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 22px 4px;
|
||||
|
||||
.session-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: theme.$primary-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
.session-delete {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
&-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.session-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.session-action-btn {
|
||||
border-radius: 8px;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.16);
|
||||
background: #000;
|
||||
box-shadow: 0 8px 64px 0 rgba(0, 0, 0, 0.10);
|
||||
backdrop-filter: blur(16px);
|
||||
display: flex;
|
||||
padding: 5px 8px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
|
||||
.action-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 标题区域 - 独立白色块
|
||||
@@ -162,6 +232,75 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 删除确认弹窗
|
||||
.delete-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
|
||||
&__content {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
margin: 0 32px;
|
||||
max-width: 320px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__title {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: theme.$primary-color;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: rgba(60, 60, 67, 0.6);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
.delete-modal__btn {
|
||||
flex: 1;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:first-child {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
color: rgba(60, 60, 67, 0.8);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
background: #FF3B30;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 旋转动画
|
||||
|
||||
@@ -1,64 +1,142 @@
|
||||
import React, { useState } from 'react'
|
||||
import { View, Text, Input, Button, Image, ScrollView, Picker } from '@tarojs/components'
|
||||
import { View, Text, Button, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import ActivityTypeSwitch, { type ActivityType } from '../../components/ActivityTypeSwitch'
|
||||
import PublishForm from './publishForm'
|
||||
import { publishBallFormSchema } from '../../config/formSchema/publishBallFormSchema';
|
||||
import { PublishBallFormData } from '../../../types/publishBall';
|
||||
import PublishService from '@/services/publishService';
|
||||
import styles from './index.module.scss'
|
||||
|
||||
import images from '@/config/images'
|
||||
|
||||
const PublishBall: React.FC = () => {
|
||||
const [formData, setFormData] = useState<PublishBallFormData>({
|
||||
activityType: 'individual', // 默认值
|
||||
title: '',
|
||||
timeRange: {
|
||||
startDate: '2025-11-23',
|
||||
startTime: '08:00',
|
||||
endTime: '10:00'
|
||||
},
|
||||
fee: '',
|
||||
location: '',
|
||||
gameplay: '',
|
||||
minParticipants: 1,
|
||||
maxParticipants: 4,
|
||||
ntpLevel: [2.0, 4.0],
|
||||
additionalRequirements: '',
|
||||
autoDegrade: false
|
||||
const [activityType, setActivityType] = useState<ActivityType>('individual')
|
||||
const [formData, setFormData] = useState<PublishBallFormData[]>([
|
||||
{
|
||||
title: '',
|
||||
timeRange: {
|
||||
startDate: '2025-11-23',
|
||||
startTime: '08:00',
|
||||
endTime: '10:00'
|
||||
},
|
||||
fee: '',
|
||||
location: '',
|
||||
gameplay: '',
|
||||
minParticipants: 1,
|
||||
maxParticipants: 4,
|
||||
ntpLevel: [2.0, 4.0],
|
||||
additionalRequirements: '',
|
||||
autoDegrade: false
|
||||
}
|
||||
])
|
||||
|
||||
// 删除确认弹窗状态
|
||||
const [deleteConfirm, setDeleteConfirm] = useState<{
|
||||
visible: boolean;
|
||||
index: number;
|
||||
}>({
|
||||
visible: false,
|
||||
index: -1
|
||||
})
|
||||
|
||||
|
||||
|
||||
// 更新表单数据
|
||||
const updateFormData = (key: keyof PublishBallFormData, value: any) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[key]: value
|
||||
}))
|
||||
const updateFormData = (key: keyof PublishBallFormData, value: any, index: number) => {
|
||||
setFormData(prev => {
|
||||
const newData = [...prev]
|
||||
newData[index] = { ...newData[index], [key]: value }
|
||||
return newData
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 处理活动类型变化
|
||||
const handleActivityTypeChange = (type: ActivityType) => {
|
||||
updateFormData('activityType', type)
|
||||
setActivityType(type)
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
setFormData(prev => [...prev, {
|
||||
title: '',
|
||||
timeRange: {
|
||||
startDate: '2025-11-23',
|
||||
startTime: '08:00',
|
||||
endTime: '10:00'
|
||||
},
|
||||
fee: '',
|
||||
location: '',
|
||||
gameplay: '',
|
||||
minParticipants: 1,
|
||||
maxParticipants: 4,
|
||||
ntpLevel: [2.0, 4.0],
|
||||
additionalRequirements: '',
|
||||
autoDegrade: false
|
||||
}])
|
||||
}
|
||||
|
||||
// 复制上一场数据
|
||||
const handleCopyPrevious = (index: number) => {
|
||||
if (index > 0) {
|
||||
setFormData(prev => {
|
||||
const newData = [...prev]
|
||||
newData[index] = { ...newData[index - 1] }
|
||||
return newData
|
||||
})
|
||||
Taro.showToast({
|
||||
title: '已复制上一场数据',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 删除确认弹窗
|
||||
const showDeleteConfirm = (index: number) => {
|
||||
setDeleteConfirm({
|
||||
visible: true,
|
||||
index
|
||||
})
|
||||
}
|
||||
|
||||
// 关闭删除确认弹窗
|
||||
const closeDeleteConfirm = () => {
|
||||
setDeleteConfirm({
|
||||
visible: false,
|
||||
index: -1
|
||||
})
|
||||
}
|
||||
|
||||
// 确认删除
|
||||
const confirmDelete = () => {
|
||||
if (deleteConfirm.index >= 0) {
|
||||
setFormData(prev => prev.filter((_, index) => index !== deleteConfirm.index))
|
||||
closeDeleteConfirm()
|
||||
Taro.showToast({
|
||||
title: '已删除该场次',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
// 基础验证
|
||||
if (!formData.title.trim()) {
|
||||
Taro.showToast({
|
||||
title: '请输入活动标题',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 实现提交逻辑
|
||||
|
||||
const res = await PublishService.createPersonal({
|
||||
"title": "周末网球约球",
|
||||
"venue_id": 1,
|
||||
"creator_id": 1,
|
||||
"game_date": "2024-06-15",
|
||||
"start_time": "14:00",
|
||||
"end_time": "16:00",
|
||||
"max_participants": 4,
|
||||
"current_participants": 2,
|
||||
"ntrp_level": "2.0-4.0",
|
||||
"play_style": "单打",
|
||||
"description": "周末约球,欢迎参加",
|
||||
})
|
||||
console.log(res);
|
||||
Taro.showToast({
|
||||
title: '发布成功',
|
||||
icon: 'success'
|
||||
@@ -70,15 +148,82 @@ const PublishBall: React.FC = () => {
|
||||
{/* 活动类型切换 */}
|
||||
<View className={styles['activity-type-switch']}>
|
||||
<ActivityTypeSwitch
|
||||
value={formData.activityType}
|
||||
value={activityType}
|
||||
onChange={handleActivityTypeChange}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className={styles['publish-ball__scroll']}>
|
||||
<PublishForm formData={formData} onChange={updateFormData} optionsConfig={publishBallFormSchema} />
|
||||
{
|
||||
formData.map((item, index) => (
|
||||
<View key={index}>
|
||||
{/* 场次标题行 */}
|
||||
{activityType === 'group' && index > 0 && (
|
||||
<View className={styles['session-header']}>
|
||||
<View className={styles['session-title']}>
|
||||
第{index + 1}场
|
||||
<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']}
|
||||
onClick={() => handleCopyPrevious(index)}
|
||||
>
|
||||
复制上一场
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
<PublishForm
|
||||
formData={item}
|
||||
onChange={(key, value) => updateFormData(key, value, index)}
|
||||
optionsConfig={publishBallFormSchema}
|
||||
/>
|
||||
</View>
|
||||
))
|
||||
}
|
||||
{
|
||||
activityType === 'group' && (
|
||||
<View className={styles['publish-ball__add']} onClick={handleAdd}>
|
||||
<Image src={images.ICON_ADD} className={styles['publish-ball__add-icon']} />
|
||||
再添加一场
|
||||
</View>
|
||||
)
|
||||
}
|
||||
</View>
|
||||
|
||||
{/* 删除确认弹窗 */}
|
||||
{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>
|
||||
)}
|
||||
|
||||
{/* 完成按钮 */}
|
||||
<View className={styles['submit-section']}>
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
|
||||
import Taro from '@tarojs/taro'
|
||||
import { CoverImageUpload, Range, TimeSelector, TextareaTag, SelectStadium, ParticipantsControl, TitleInput, FormBasicInfo, FormSwitch } from '../../components'
|
||||
import { ImageUpload, Range, TimeSelector, TextareaTag, SelectStadium, NumberInterval, TitleInput, FormBasicInfo, FormSwitch } from '../../components'
|
||||
import { type Stadium, type CoverImage } from '../../components/index.types'
|
||||
import { FormFieldConfig, FieldType } from '../../config/formSchema/publishBallFormSchema'
|
||||
import { PublishBallFormData } from '../../../types/publishBall';
|
||||
@@ -15,15 +15,15 @@ const componentMap = {
|
||||
[FieldType.TIMEINTERVAL]: TimeSelector,
|
||||
[FieldType.RANGE]: Range,
|
||||
[FieldType.TEXTAREATAG]: TextareaTag,
|
||||
[FieldType.NUMBERINTERVAL]: ParticipantsControl,
|
||||
[FieldType.UPLOADIMAGE]: CoverImageUpload,
|
||||
[FieldType.NUMBERINTERVAL]: NumberInterval,
|
||||
[FieldType.UPLOADIMAGE]: ImageUpload,
|
||||
[FieldType.ACTIVITYINFO]: FormBasicInfo,
|
||||
[FieldType.CHECKBOX]: FormSwitch,
|
||||
}
|
||||
|
||||
const PublishForm: React.FC<{
|
||||
formData: PublishBallFormData,
|
||||
onChange: (key: keyof PublishBallFormData, value: any) => void,
|
||||
onChange: (key: keyof PublishBallFormData, value: any, index?: number) => void,
|
||||
optionsConfig: FormFieldConfig[] }> = ({ formData, onChange, optionsConfig }) => {
|
||||
const [coverImages, setCoverImages] = useState<CoverImage[]>([])
|
||||
const [showStadiumSelector, setShowStadiumSelector] = useState(false)
|
||||
@@ -102,7 +102,7 @@ const PublishForm: React.FC<{
|
||||
console.log(optionProps, item.label, formData[item.key]);
|
||||
if (item.type === FieldType.UPLOADIMAGE) {
|
||||
/* 活动封面 */
|
||||
return <CoverImageUpload
|
||||
return <ImageUpload
|
||||
images={coverImages}
|
||||
onChange={handleCoverImagesChange}
|
||||
{...item.props}
|
||||
|
||||
@@ -41,7 +41,7 @@ class HttpService {
|
||||
|
||||
constructor() {
|
||||
// 使用环境配置
|
||||
this.baseURL = `${envConfig.apiBaseURL}/api/${envConfig.apiVersion}`
|
||||
this.baseURL = `${envConfig.apiBaseURL}/api/`
|
||||
this.timeout = envConfig.timeout
|
||||
this.enableLog = envConfig.enableLog
|
||||
|
||||
@@ -77,7 +77,6 @@ class HttpService {
|
||||
private buildHeaders(config: RequestConfig): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Environment': envConfig.name, // 添加环境标识
|
||||
...config.headers
|
||||
}
|
||||
|
||||
@@ -211,6 +210,7 @@ class HttpService {
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(this.buildHeaders(config), 1111);
|
||||
const requestConfig = {
|
||||
url: fullUrl,
|
||||
method: method,
|
||||
|
||||
38
src/services/publishService.ts
Normal file
38
src/services/publishService.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import httpService from './httpService'
|
||||
import type { ApiResponse } from './httpService'
|
||||
|
||||
// 用户接口
|
||||
export interface PublishBallData {
|
||||
title: string,
|
||||
venue_id: number,
|
||||
creator_id: number,
|
||||
game_date: string,
|
||||
start_time: string,
|
||||
end_time: string,
|
||||
max_participants: number,
|
||||
current_participants: number,
|
||||
ntrp_level: string,
|
||||
play_style: string,
|
||||
description: string,
|
||||
}
|
||||
|
||||
// 响应接口
|
||||
export interface Response {
|
||||
code: string
|
||||
message: string
|
||||
data: any
|
||||
}
|
||||
|
||||
// 发布球局类
|
||||
class PublishService {
|
||||
// 用户登录
|
||||
async createPersonal(data: PublishBallData): Promise<ApiResponse<Response>> {
|
||||
return httpService.post('/games/create', data, {
|
||||
showLoading: true,
|
||||
loadingText: '发布中...'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 导出认证服务实例
|
||||
export default new PublishService()
|
||||
5
src/static/publishBall/icon-delete.svg
Normal file
5
src/static/publishBall/icon-delete.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.26661 7.23309C7.26661 6.90172 6.99798 6.63309 6.66661 6.63309C6.33524 6.63309 6.06661 6.90172 6.06661 7.23309V11.8998C6.06661 12.2311 6.33524 12.4998 6.66661 12.4998C6.99798 12.4998 7.26661 12.2311 7.26661 11.8998V7.23309Z" fill="black"/>
|
||||
<path d="M9.33331 6.63309C9.66468 6.63309 9.93331 6.90172 9.93331 7.23309V11.8998C9.93331 12.2311 9.66468 12.4998 9.33331 12.4998C9.00194 12.4998 8.73331 12.2311 8.73331 11.8998V7.23309C8.73331 6.90172 9.00194 6.63309 9.33331 6.63309Z" fill="black"/>
|
||||
<path d="M4.97805 3.13887C5.08566 2.53447 5.6112 2.09424 6.2251 2.09424H9.77486C10.3888 2.09424 10.9143 2.53447 11.0219 3.13887L11.1979 4.12753L13.5 4.12753C13.8313 4.12753 14.1 4.39616 14.1 4.72753C14.1 5.0589 13.8313 5.32753 13.5 5.32753H12.7317L12.2463 13.3242C12.1844 14.3446 11.3388 15.1404 10.3165 15.1404H5.68341C4.66115 15.1404 3.81556 14.3446 3.75363 13.3242L3.26828 5.32753H2.49996C2.16859 5.32753 1.89996 5.0589 1.89996 4.72753C1.89996 4.39616 2.16859 4.12753 2.49996 4.12753H4.80202L4.97805 3.13887ZM9.97907 4.12753L9.8405 3.34922C9.83483 3.31741 9.80717 3.29424 9.77486 3.29424H6.2251C6.19279 3.29424 6.16513 3.31741 6.15947 3.34922L6.0209 4.12753L9.97907 4.12753ZM4.47083 5.33309L4.95142 13.2515C4.97491 13.6386 5.29565 13.9404 5.68341 13.9404H10.3165C10.7043 13.9404 11.025 13.6386 11.0485 13.2515L11.5291 5.33309H4.47083Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/static/publishBall/icon-heartcircle.png
Normal file
BIN
src/static/publishBall/icon-heartcircle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
Reference in New Issue
Block a user