发布球局

This commit is contained in:
筱野
2025-08-23 15:14:37 +08:00
parent e5176f4f5f
commit fb150617c6
34 changed files with 679 additions and 602 deletions

View 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;
}

View File

@@ -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;
}
}
}

View File

@@ -1,8 +1,7 @@
import React from 'react' import React from 'react'
import { View, Text, Image } from '@tarojs/components' import { View, Text, Image } from '@tarojs/components'
import images from '@/config/images' import images from '@/config/images'
import styles from './index.module.scss'
import './index.scss'
export type ActivityType = 'individual' | 'group' export type ActivityType = 'individual' | 'group'
@@ -13,22 +12,22 @@ interface ActivityTypeSwitchProps {
const ActivityTypeSwitch: React.FC<ActivityTypeSwitchProps> = ({ value, onChange }) => { const ActivityTypeSwitch: React.FC<ActivityTypeSwitchProps> = ({ value, onChange }) => {
return ( return (
<View className='activity-type-switch'> <View className={styles['activity-type-switch']}>
<View <View
className={`switch-tab ${value === 'individual' ? 'active' : ''}`} className={`${styles['switch-tab']} ${value === 'individual' ? styles.active : ''}`}
onClick={() => onChange('individual')} onClick={() => onChange('individual')}
> >
<View className='tab-icon'> <View className={styles['tab-icon']}>
<Image src={images.ICON_PERSONAL} className='icon-style' /> <Image src={images.ICON_PERSONAL} className={styles['icon-style']} />
</View> </View>
<Text className='tab-text'></Text> <Text className={styles['tab-text']}></Text>
</View> </View>
<View <View
className={`switch-tab ${value === 'group' ? 'active' : ''}`} className={`${styles['switch-tab']} ${value === 'group' ? styles.active : ''}`}
onClick={() => onChange('group')} onClick={() => onChange('group')}
> >
<Image src={images.ICON_CHANGDA} className='icon-style' /> <Image src={images.ICON_CHANGDA} className={styles['icon-style']} />
<Text className='tab-text'></Text> <Text className={styles['tab-text']}></Text>
</View> </View>
</View> </View>
) )

View File

@@ -43,7 +43,7 @@
color: #1f2329; color: #1f2329;
border: none; border: none;
width: 154px; width: 154px;
height: 36px; height: 44px;
border-radius: 12px; border-radius: 12px;
border: 0.5px solid rgba(0, 0, 0, 0.06); border: 0.5px solid rgba(0, 0, 0, 0.06);
background: #fff; background: #fff;
@@ -53,7 +53,7 @@
.common-popup__btn-confirm { .common-popup__btn-confirm {
/* 使用按钮组件的 primary 样式 */ /* 使用按钮组件的 primary 样式 */
width: 154px; width: 154px;
height: 36px; height: 44px;
border: 0.5px solid rgba(0, 0, 0, 0.06); border: 0.5px solid rgba(0, 0, 0, 0.06);
background: #000; background: #000;
border-radius: 12px; border-radius: 12px;

View File

@@ -1 +0,0 @@
export { default, type CoverImage } from './CoverImageUpload'

View File

@@ -25,7 +25,7 @@
width: 108px; width: 108px;
height: 108px; height: 108px;
border-radius: 12px; border-radius: 12px;
margin-right: 12px; margin-right: 6px;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
transition: all 0.3s ease; transition: all 0.3s ease;

View File

@@ -1,7 +1,7 @@
import React, { useMemo, useCallback } from 'react' import React, { useMemo, useCallback } from 'react'
import { View, Text, Image, ScrollView } from '@tarojs/components' import { View, Text, Image, ScrollView } from '@tarojs/components'
import Taro from '@tarojs/taro' import Taro from '@tarojs/taro'
import './CoverImageUpload.scss' import './ImageUpload.scss'
export interface CoverImage { export interface CoverImage {
id: string id: string
@@ -9,13 +9,13 @@ export interface CoverImage {
tempFilePath?: string tempFilePath?: string
} }
interface CoverImageUploadProps { interface ImageUploadProps {
images: CoverImage[] images: CoverImage[]
onChange: (images: CoverImage[]) => void onChange: (images: CoverImage[]) => void
maxCount?: number maxCount?: number
} }
const CoverImageUpload: React.FC<CoverImageUploadProps> = ({ const ImageUpload: React.FC<ImageUploadProps> = ({
images, images,
onChange, onChange,
maxCount = 9 maxCount = 9
@@ -88,4 +88,4 @@ const CoverImageUpload: React.FC<CoverImageUploadProps> = ({
) )
} }
export default CoverImageUpload export default ImageUpload

View File

@@ -0,0 +1 @@
export { default, type CoverImage } from './ImageUpload'

View File

@@ -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;
}
}
}
}
}

View File

@@ -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

View File

@@ -1 +0,0 @@
export { default, type NTRPRange, getNTRPRangeText } from './NTRPSlider'

View File

@@ -1,16 +1,16 @@
import React from 'react' import React from 'react'
import { View, Text, Button } from '@tarojs/components' import { View, Text, Button } from '@tarojs/components'
import './ParticipantsControl.scss' import './NumberInterval.scss'
import { InputNumber } from '@nutui/nutui-react-taro' import { InputNumber } from '@nutui/nutui-react-taro'
interface ParticipantsControlProps { interface NumberIntervalProps {
minParticipants: number minParticipants: number
maxParticipants: number maxParticipants: number
onMinParticipantsChange: (value: number) => void onMinParticipantsChange: (value: number) => void
onMaxParticipantsChange: (value: number) => void onMaxParticipantsChange: (value: number) => void
} }
const ParticipantsControl: React.FC<ParticipantsControlProps> = ({ const NumberInterval: React.FC<NumberIntervalProps> = ({
minParticipants, minParticipants,
maxParticipants, maxParticipants,
onMinParticipantsChange, onMinParticipantsChange,
@@ -46,4 +46,4 @@ const ParticipantsControl: React.FC<ParticipantsControlProps> = ({
) )
} }
export default ParticipantsControl export default NumberInterval

View File

@@ -0,0 +1 @@
export { default } from './NumberInterval'

View File

@@ -1,2 +0,0 @@
export { default } from './ParticipantsControl'
export type { ParticipantsControlProps } from './ParticipantsControl'

View File

@@ -117,6 +117,21 @@ const SelectStadium: React.FC<SelectStadiumProps> = ({
setSearchValue('') 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) => { const markSearchText = (text: string) => {
return text.replace(searchValue, `<span style="color: #007AFF;">${searchValue}</span>`) return text.replace(searchValue, `<span style="color: #007AFF;">${searchValue}</span>`)
@@ -215,9 +230,9 @@ const SelectStadium: React.FC<SelectStadiumProps> = ({
</View> </View>
<View className='stadium-item-right'> <View className='stadium-item-right'>
<View className='stadium-name' dangerouslySetInnerHTML={{ __html: markSearchText(stadium.name) }}></View> <View className='stadium-name' dangerouslySetInnerHTML={{ __html: markSearchText(stadium.name) }}></View>
<View className='stadium-address'> <View className='stadium-address' >
<Text>{stadium.istance} · </Text> <Text onClick={(e) => { e.stopPropagation(); handleItemLocation(stadium); }}>{stadium.istance} · </Text>
<Text>{stadium.address}</Text> <Text onClick={(e) => { e.stopPropagation(); handleItemLocation(stadium); }}>{stadium.address}</Text>
<Image src={images.ICON_ARRORW_SMALL} className='stadium-map-icon' /> <Image src={images.ICON_ARRORW_SMALL} className='stadium-map-icon' />
</View> </View>
</View> </View>

View File

@@ -57,77 +57,79 @@
// 场地类型 // 场地类型
.venue-type-section { .venue-type-section {
border-bottom: 1px solid #e5e5e5;
flex-shrink: 0; flex-shrink: 0;
.section-title { .section-title {
padding: 18px 20px 10px 20px; padding: 18px 20px 10px 20px;
font-size: 16px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: #333; color: #333;
margin-bottom: 12px;
display: block; 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;
}
}
} }
.option-buttons { .option-buttons {
display: flex; display: flex;
gap: 12px; gap: 16px;
padding: 0 15px; 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 { .option-btn {
padding: 8px 16px;
border-radius: 20px; border-radius: 20px;
border: 1px solid #e0e0e0; border: 1px solid #e0e0e0;
background: white; background: white;
cursor: pointer; cursor: pointer;
transition: all 0.2s; 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; display: flex;
gap: 12px; flex: 1;
justify-content: center;
.option-btn { align-items: center;
padding: 8px 16px; height: 40px;
border-radius: 20px; border-radius: 999px;
border: 1px solid #e0e0e0; border: 0.5px solid rgba(0, 0, 0, 0.12);
background: white; background: #FFF;
cursor: pointer; font-weight: 500;
transition: all 0.2s;
&.selected { &.selected {
background: #333; background: #000;
border-color: #333; border-color: #fff;
border-radius: 999px;
font-weight: 600;
border: 0.5px solid rgba(0, 0, 0, 0.06);
.option-text { .option-text {
color: white; color: white;
} }
@@ -141,36 +143,6 @@
} }
} }
// 场地信息补充
.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;
color: #333;
background: white;
height: 44px;
box-sizing: border-box;
&::placeholder {
color: #999;
}
}
}
// 底部按钮 // 底部按钮
.bottom-actions { .bottom-actions {

View File

@@ -1,14 +1,18 @@
import React, { useState, useCallback } from 'react' import React, { useState, useCallback } from 'react'
import Taro from '@tarojs/taro'
import { View, Text, Image } from '@tarojs/components' import { View, Text, Image } from '@tarojs/components'
import images from '@/config/images' import images from '@/config/images'
import './StadiumDetail.scss' import './StadiumDetail.scss'
import TextareaTag from '@/components/TextareaTag' import TextareaTag from '@/components/TextareaTag'
import CoverImageUpload, { type CoverImage } from '@/components/CoverImageUpload' import CoverImageUpload, { type CoverImage } from '@/components/ImageUpload'
export interface Stadium { export interface Stadium {
id?: string id?: string
name: string name: string
address?: string address?: string
longitude?: number
latitude?: number
istance?: string
} }
interface StadiumDetailProps { interface StadiumDetailProps {
@@ -21,56 +25,108 @@ const stadiumInfo = [
{ {
label: '场地类型', label: '场地类型',
options: ['室内', '室外', '室外雨棚'], options: ['室内', '室外', '室外雨棚'],
prop: 'venueType',
type: 'tags' type: 'tags'
}, },
{ {
label: '地面材质', label: '地面材质',
options: ['硬地', '红土', '草地'], options: ['硬地', '红土', '草地'],
prop: 'groundMaterial',
type: 'tags' type: 'tags'
}, },
{ {
label: '场地信息补充', label: '场地信息补充',
options: ['1号场', '2号场', '3号场', '4号场', '有空调', '6号场'], options: ['1号场', '2号场', '3号场', '4号场', '有空调', '6号场','6号场'],
prop: 'additionalInfo',
type: 'textareaTag' type: 'textareaTag'
}, },
{ {
label: '场地预定截图', label: '场地预定截图',
options: ['有其他场地信息可备注'], options: ['有其他场地信息可备注'],
prop: 'imagesList',
type: 'image' 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> = ({ const StadiumDetail: React.FC<StadiumDetailProps> = ({
stadium, stadium,
onBack,
onConfirm
}) => { }) => {
const [venueType, setVenueType] = useState('室内') const [formData, setFormData] = useState({
const [groundMaterial, setGroundMaterial] = useState('硬地') stadiumName: stadium.name,
const [additionalInfo, setAdditionalInfo] = useState('') stadiumAddress: stadium.address,
const [imagesList, setImagesList] = useState<CoverImage[]>([]) stadiumLongitude: stadium.longitude,
stadiumLatitude: stadium.latitude,
istance: stadium.istance,
venueType: '室内',
groundMaterial: '硬地',
additionalInfo: '',
imagesList: [] as CoverImage[]
})
const handleConfirm = () => {
onConfirm(stadium, venueType, groundMaterial, additionalInfo)
}
const handleMapLocation = () => { 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 updateFormData = useCallback((prop: string, value: any) => {
setFormData(prev => ({ ...prev, [prop]: value }))
}, [])
const getSelectedByLabel = useCallback((label: string) => { const getSelectedByLabel = useCallback((label: string) => {
if (label === '场地类型') return venueType if (label === '场地类型') return formData.venueType
if (label === '地面材质') return groundMaterial if (label === '地面材质') return formData.groundMaterial
return '' return ''
}, [venueType, groundMaterial]) }, [formData.venueType, formData.groundMaterial])
const setSelectedByLabel = useCallback((label: string, value: string) => {
if (label === '场地类型') {
setVenueType(value)
} else if (label === '地面材质') {
setGroundMaterial(value)
}
}, [])
console.log(stadium,'stadiumstadium'); console.log(stadium,'stadiumstadium');
return ( return (
@@ -84,9 +140,10 @@ const StadiumDetail: React.FC<StadiumDetailProps> = ({
<Image src={images.ICON_STADIUM} className='stadium-icon' /> <Image src={images.ICON_STADIUM} className='stadium-icon' />
</View> </View>
<View className='stadium-item-right'> <View className='stadium-item-right'>
<View className='stadium-name'>{stadium.name}</View> <View className='stadium-name'>{formData.stadiumName}</View>
<View className='stadium-address'> <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' /> <Image src={images.ICON_ARRORW_SMALL} className='stadium-map-icon' />
</View> </View>
</View> </View>
@@ -96,54 +153,43 @@ const StadiumDetail: React.FC<StadiumDetailProps> = ({
if (item.type === 'tags') { if (item.type === 'tags') {
const selected = getSelectedByLabel(item.label) const selected = getSelectedByLabel(item.label)
return ( return (
<View className='venue-type-section' key={item.label}> <SectionContainer key={item.label} title={item.label} prop={item.prop}>
<Text className='section-title'>{item.label}</Text>
<View className='option-buttons'>
{item.options.map((opt) => ( {item.options.map((opt) => (
<View <View
key={opt} key={opt}
className={`option-btn ${selected === opt ? 'selected' : ''}`} className={`option-btn ${selected === opt ? 'selected' : ''}`}
onClick={() => setSelectedByLabel(item.label, opt)} onClick={() => updateFormData(item.prop, opt)}
> >
<Text className='option-text'>{opt}</Text> <Text className='option-text'>{opt}</Text>
</View> </View>
))} ))}
</View> </SectionContainer>
</View>
) )
} }
if (item.type === 'textareaTag') { if (item.type === 'textareaTag') {
return ( return (
<View className='venue-type-section' key={item.label}> <SectionContainer key={item.label} title={item.label} prop={item.prop}>
<Text className='section-title'>{item.label}</Text> <View className='textarea-tag-container'>
<View className='option-buttons'>
<TextareaTag <TextareaTag
key={item.label} value={formData.additionalInfo}
value={additionalInfo} onChange={(value) => updateFormData(item.prop, value)}
onChange={setAdditionalInfo}
placeholder='有其他场地信息可备注' placeholder='有其他场地信息可备注'
options={(item.options || []).map((o) => ({ label: o, value: o }))} options={(item.options || []).map((o) => ({ label: o, value: o }))}
/> />
</View> </View>
</View> </SectionContainer>
) )
} }
if (item.type === 'image') { if (item.type === 'image') {
return ( return (
<View className='venue-type-section' key={item.label}> <SectionContainer key={item.label} title={item.label} prop={item.prop}>
<Text className='section-title'>{item.label}</Text>
<View className='option-buttons'>
<CoverImageUpload <CoverImageUpload
key={item.label} images={formData.imagesList}
images={imagesList} onChange={(images) => updateFormData(item.prop, images)}
onChange={setImagesList}
/> />
</View> </SectionContainer>
</View>
) )
} }

View File

@@ -4,6 +4,7 @@
border-radius: 16px; border-radius: 16px;
padding: 10px 16px; padding: 10px 16px;
width: 100%; width: 100%;
box-sizing: border-box;
.input-wrapper { .input-wrapper {
margin-top: 8px; margin-top: 8px;
.additional-input { .additional-input {
@@ -36,6 +37,7 @@
gap: 6px; gap: 6px;
.nut-checkbox{ .nut-checkbox{
margin-right: 0; margin-right: 0;
.nut-checkbox-button{ .nut-checkbox-button{
border: 1px solid theme.$primary-border-color; border: 1px solid theme.$primary-border-color;
color: theme.$primary-color; color: theme.$primary-color;
@@ -43,6 +45,7 @@
font-size: 12px; font-size: 12px;
padding: 2px 6px; padding: 2px 6px;
margin-right: 6px; margin-right: 6px;
margin-bottom: 6px;
} }
} }
} }

View File

@@ -1,6 +1,6 @@
import React from 'react' 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' import './index.scss'
interface TitleInputProps { interface TitleInputProps {
@@ -18,13 +18,14 @@ const TitleInput: React.FC<TitleInputProps> = ({
}) => { }) => {
return ( return (
<View className='title-input-wrapper'> <View className='title-input-wrapper'>
<Input <TextArea
className='title-input' className='title-input'
placeholder={placeholder} placeholder={placeholder}
placeholderClass='title-placeholder'
value={value} value={value}
onInput={(e) => onChange(e.detail.value)} onInput={(e) => onChange(e.detail.value)}
maxlength={maxLength} maxlength={maxLength}
autoSize={true}
placeholderClass='title-input-placeholder'
/> />
<View className='char-count'>{value.length}/{maxLength}</View> <View className='char-count'>{value.length}/{maxLength}</View>
</View> </View>

View File

@@ -1,35 +1,34 @@
.title-input-wrapper { .title-input-wrapper {
position: relative; position: relative;
width: 100%; width: 100%;
display: flex;
align-items: flex-start;
justify-content: space-around;
.title-input { .title-input {
width: 100%; width: 83%;
height: 44px; min-height: 44px;
padding: 0 16px; padding: 12px 16px;
border: 1px solid #e5e5e5;
border-radius: 8px; border-radius: 8px;
font-size: 16px; font-size: 16px;
line-height: 44px; line-height: 1.4;
background: #fff; background: #fff;
box-sizing: border-box; box-sizing: border-box;
&:focus { resize: none;
border-color: #007aff;
outline: none;
}
} }
.title-placeholder { // 使用 placeholderClass 来控制 placeholder 样式
color: #999; .title-input-placeholder {
font-size: 16px; color: rgba(60, 60, 67, 0.60) !important;
font-size: 16px !important;
font-weight: normal !important;
} }
.char-count { .char-count {
position: absolute;
right: 16px;
top: 50%;
transform: translateY(-50%);
font-size: 12px; font-size: 12px;
color: #999; color: #999;
pointer-events: none; pointer-events: none;
padding-top: 12px;
} }
} }

View File

@@ -1,10 +1,10 @@
import ActivityTypeSwitch from './ActivityTypeSwitch' import ActivityTypeSwitch from './ActivityTypeSwitch'
import TextareaTag from './TextareaTag' import TextareaTag from './TextareaTag'
import FormSwitch from './FormSwitch' import FormSwitch from './FormSwitch'
import CoverImageUpload from './CoverImageUpload' import ImageUpload from './ImageUpload'
import FormBasicInfo from './FormBasicInfo' import FormBasicInfo from './FormBasicInfo'
import Range from './Range' import Range from './Range'
import ParticipantsControl from './ParticipantsControl' import NumberInterval from './NumberInterval'
import { SelectStadium, StadiumDetail } from './SelectStadium' import { SelectStadium, StadiumDetail } from './SelectStadium'
import TimeSelector from './TimeSelector' import TimeSelector from './TimeSelector'
import TitleInput from './TitleInput' import TitleInput from './TitleInput'
@@ -14,10 +14,10 @@ export {
ActivityTypeSwitch, ActivityTypeSwitch,
TextareaTag, TextareaTag,
FormSwitch, FormSwitch,
CoverImageUpload, ImageUpload,
FormBasicInfo, FormBasicInfo,
Range, Range,
ParticipantsControl, NumberInterval,
SelectStadium, SelectStadium,
TimeSelector, TimeSelector,
TitleInput, TitleInput,

View File

@@ -1,6 +1,5 @@
import { getNTRPRangeText, type NTRPRange } from './NTRPSlider'
import { type TimeRange } from './TimeSelector' import { type TimeRange } from './TimeSelector'
import { type Stadium } from './SelectStadium' import { type Stadium } from './SelectStadium'
import { type ActivityType } from './ActivityTypeSwitch' import { type ActivityType } from './ActivityTypeSwitch'
import { type CoverImage } from './CoverImageUpload' import { type CoverImage } from './ImageUpload'
export type { NTRPRange, getNTRPRangeText, TimeRange, Stadium, ActivityType, CoverImage } export type { TimeRange, Stadium, ActivityType, CoverImage }

View File

@@ -7,7 +7,6 @@ export type EnvType = 'development' | 'test' | 'production'
export interface EnvConfig { export interface EnvConfig {
name: string name: string
apiBaseURL: string apiBaseURL: string
apiVersion: string
timeout: number timeout: number
enableLog: boolean enableLog: boolean
enableMock: boolean enableMock: boolean
@@ -18,8 +17,7 @@ const envConfigs: Record<EnvType, EnvConfig> = {
// 开发环境 // 开发环境
development: { development: {
name: '开发环境', name: '开发环境',
apiBaseURL: 'https://dev-api.playballtogether.com', apiBaseURL: 'https://sit.light120.com',
apiVersion: 'v1',
timeout: 15000, timeout: 15000,
enableLog: true, enableLog: true,
enableMock: true enableMock: true
@@ -28,8 +26,7 @@ const envConfigs: Record<EnvType, EnvConfig> = {
// 测试环境 // 测试环境
test: { test: {
name: '测试环境', name: '测试环境',
apiBaseURL: 'https://test-api.playballtogether.com', apiBaseURL: 'https://sit.light120.com',
apiVersion: 'v1',
timeout: 12000, timeout: 12000,
enableLog: true, enableLog: true,
enableMock: false enableMock: false
@@ -38,8 +35,7 @@ const envConfigs: Record<EnvType, EnvConfig> = {
// 生产环境 // 生产环境
production: { production: {
name: '生产环境', name: '生产环境',
apiBaseURL: 'https://api.playballtogether.com', apiBaseURL: 'https://sit.light120.com',
apiVersion: 'v1',
timeout: 10000, timeout: 10000,
enableLog: false, enableLog: false,
enableMock: false enableMock: false

View File

@@ -59,7 +59,7 @@ export const publishBallFormSchema: FormFieldConfig[] = [
placeholder: '好的标题更吸引人哦', placeholder: '好的标题更吸引人哦',
required: true, required: true,
props: { props: {
maxLength: 20 maxLength: 80
}, },
rules: [ rules: [
{ required: true, message: '请输入活动标题' }, { required: true, message: '请输入活动标题' },

View File

@@ -13,4 +13,8 @@ export default {
ICON_STADIUM: require('@/static/publishBall/icon-stadium.svg'), ICON_STADIUM: require('@/static/publishBall/icon-stadium.svg'),
ICON_ARRORW_SMALL: require('@/static/publishBall/icon-arrow-small.svg'), ICON_ARRORW_SMALL: require('@/static/publishBall/icon-arrow-small.svg'),
ICON_MAP_SEARCH: require('@/static/publishBall/icon-map-search.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')
} }

View File

@@ -0,0 +1 @@

View File

@@ -8,12 +8,28 @@
&__scroll { &__scroll {
height: calc(100vh - 120px); height: calc(100vh - 120px);
overflow: auto; overflow: auto;
padding: 4px 16px 0 16px; padding: 4px 16px 72px 16px;
box-sizing: border-box; box-sizing: border-box;
} }
&__content { &__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{ .activity-type-switch{
padding: 4px 16px 0 16px; 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; 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;
}
}
}
}
} }
// 旋转动画 // 旋转动画

View File

@@ -1,16 +1,18 @@
import React, { useState } from 'react' 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 Taro from '@tarojs/taro'
import ActivityTypeSwitch, { type ActivityType } from '../../components/ActivityTypeSwitch' import ActivityTypeSwitch, { type ActivityType } from '../../components/ActivityTypeSwitch'
import PublishForm from './publishForm' import PublishForm from './publishForm'
import { publishBallFormSchema } from '../../config/formSchema/publishBallFormSchema'; import { publishBallFormSchema } from '../../config/formSchema/publishBallFormSchema';
import { PublishBallFormData } from '../../../types/publishBall'; import { PublishBallFormData } from '../../../types/publishBall';
import PublishService from '@/services/publishService';
import styles from './index.module.scss' import styles from './index.module.scss'
import images from '@/config/images'
const PublishBall: React.FC = () => { const PublishBall: React.FC = () => {
const [formData, setFormData] = useState<PublishBallFormData>({ const [activityType, setActivityType] = useState<ActivityType>('individual')
activityType: 'individual', // 默认值 const [formData, setFormData] = useState<PublishBallFormData[]>([
{
title: '', title: '',
timeRange: { timeRange: {
startDate: '2025-11-23', startDate: '2025-11-23',
@@ -25,40 +27,116 @@ const PublishBall: React.FC = () => {
ntpLevel: [2.0, 4.0], ntpLevel: [2.0, 4.0],
additionalRequirements: '', additionalRequirements: '',
autoDegrade: false autoDegrade: false
}
])
// 删除确认弹窗状态
const [deleteConfirm, setDeleteConfirm] = useState<{
visible: boolean;
index: number;
}>({
visible: false,
index: -1
}) })
// 更新表单数据 // 更新表单数据
const updateFormData = (key: keyof PublishBallFormData, value: any) => { const updateFormData = (key: keyof PublishBallFormData, value: any, index: number) => {
setFormData(prev => ({ setFormData(prev => {
...prev, const newData = [...prev]
[key]: value newData[index] = { ...newData[index], [key]: value }
})) return newData
})
} }
// 处理活动类型变化 // 处理活动类型变化
const handleActivityTypeChange = (type: ActivityType) => { 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 () => { const handleSubmit = async () => {
// 基础验证 // 基础验证
if (!formData.title.trim()) {
Taro.showToast({
title: '请输入活动标题',
icon: 'none'
})
return
}
// TODO: 实现提交逻辑
// 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({ Taro.showToast({
title: '发布成功', title: '发布成功',
icon: 'success' icon: 'success'
@@ -70,15 +148,82 @@ const PublishBall: React.FC = () => {
{/* 活动类型切换 */} {/* 活动类型切换 */}
<View className={styles['activity-type-switch']}> <View className={styles['activity-type-switch']}>
<ActivityTypeSwitch <ActivityTypeSwitch
value={formData.activityType} value={activityType}
onChange={handleActivityTypeChange} onChange={handleActivityTypeChange}
/> />
</View> </View>
<View className={styles['publish-ball__scroll']}> <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> </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']}> <View className={styles['submit-section']}>

View File

@@ -2,7 +2,7 @@ import React, { useState } from 'react'
import { View, Text } from '@tarojs/components' import { View, Text } from '@tarojs/components'
import Taro from '@tarojs/taro' 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 { type Stadium, type CoverImage } from '../../components/index.types'
import { FormFieldConfig, FieldType } from '../../config/formSchema/publishBallFormSchema' import { FormFieldConfig, FieldType } from '../../config/formSchema/publishBallFormSchema'
import { PublishBallFormData } from '../../../types/publishBall'; import { PublishBallFormData } from '../../../types/publishBall';
@@ -15,15 +15,15 @@ const componentMap = {
[FieldType.TIMEINTERVAL]: TimeSelector, [FieldType.TIMEINTERVAL]: TimeSelector,
[FieldType.RANGE]: Range, [FieldType.RANGE]: Range,
[FieldType.TEXTAREATAG]: TextareaTag, [FieldType.TEXTAREATAG]: TextareaTag,
[FieldType.NUMBERINTERVAL]: ParticipantsControl, [FieldType.NUMBERINTERVAL]: NumberInterval,
[FieldType.UPLOADIMAGE]: CoverImageUpload, [FieldType.UPLOADIMAGE]: ImageUpload,
[FieldType.ACTIVITYINFO]: FormBasicInfo, [FieldType.ACTIVITYINFO]: FormBasicInfo,
[FieldType.CHECKBOX]: FormSwitch, [FieldType.CHECKBOX]: FormSwitch,
} }
const PublishForm: React.FC<{ const PublishForm: React.FC<{
formData: PublishBallFormData, formData: PublishBallFormData,
onChange: (key: keyof PublishBallFormData, value: any) => void, onChange: (key: keyof PublishBallFormData, value: any, index?: number) => void,
optionsConfig: FormFieldConfig[] }> = ({ formData, onChange, optionsConfig }) => { optionsConfig: FormFieldConfig[] }> = ({ formData, onChange, optionsConfig }) => {
const [coverImages, setCoverImages] = useState<CoverImage[]>([]) const [coverImages, setCoverImages] = useState<CoverImage[]>([])
const [showStadiumSelector, setShowStadiumSelector] = useState(false) const [showStadiumSelector, setShowStadiumSelector] = useState(false)
@@ -102,7 +102,7 @@ const PublishForm: React.FC<{
console.log(optionProps, item.label, formData[item.key]); console.log(optionProps, item.label, formData[item.key]);
if (item.type === FieldType.UPLOADIMAGE) { if (item.type === FieldType.UPLOADIMAGE) {
/* 活动封面 */ /* 活动封面 */
return <CoverImageUpload return <ImageUpload
images={coverImages} images={coverImages}
onChange={handleCoverImagesChange} onChange={handleCoverImagesChange}
{...item.props} {...item.props}

View File

@@ -41,7 +41,7 @@ class HttpService {
constructor() { constructor() {
// 使用环境配置 // 使用环境配置
this.baseURL = `${envConfig.apiBaseURL}/api/${envConfig.apiVersion}` this.baseURL = `${envConfig.apiBaseURL}/api/`
this.timeout = envConfig.timeout this.timeout = envConfig.timeout
this.enableLog = envConfig.enableLog this.enableLog = envConfig.enableLog
@@ -77,7 +77,6 @@ class HttpService {
private buildHeaders(config: RequestConfig): Record<string, string> { private buildHeaders(config: RequestConfig): Record<string, string> {
const headers: Record<string, string> = { const headers: Record<string, string> = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-Environment': envConfig.name, // 添加环境标识
...config.headers ...config.headers
} }
@@ -211,6 +210,7 @@ class HttpService {
} }
try { try {
console.log(this.buildHeaders(config), 1111);
const requestConfig = { const requestConfig = {
url: fullUrl, url: fullUrl,
method: method, method: method,

View 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()

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB