From fb150617c668c3fd8926d82bae136b7303cbf115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AD=B1=E9=87=8E?= Date: Sat, 23 Aug 2025 15:14:37 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8F=91=E5=B8=83=E7=90=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ActivityTypeSwitch/index.module.scss | 49 ++++ src/components/ActivityTypeSwitch/index.scss | 50 ---- src/components/ActivityTypeSwitch/index.tsx | 19 +- src/components/CommonPopup/index.module.scss | 4 +- src/components/CoverImageUpload/index.ts | 1 - .../ImageUpload.scss} | 2 +- .../ImageUpload.tsx} | 8 +- src/components/ImageUpload/index.ts | 1 + src/components/NTRPSlider/NTRPSlider.scss | 149 ------------ src/components/NTRPSlider/NTRPSlider.tsx | 133 ----------- src/components/NTRPSlider/index.ts | 1 - .../NumberInterval.scss} | 0 .../NumberInterval.tsx} | 8 +- src/components/NumberInterval/index.ts | 1 + src/components/ParticipantsControl/index.ts | 2 - .../SelectStadium/SelectStadium.tsx | 21 +- .../SelectStadium/StadiumDetail.scss | 180 ++++++--------- .../SelectStadium/StadiumDetail.tsx | 160 ++++++++----- src/components/TextareaTag/TextareaTag.scss | 3 + src/components/TitleInput/TitleInput.tsx | 9 +- src/components/TitleInput/index.scss | 33 ++- src/components/index.ts | 8 +- src/components/index.types.ts | 5 +- src/config/env.ts | 10 +- .../formSchema/publishBallFormSchema.ts | 2 +- src/config/images.js | 4 + src/pages/index/index.module.scss | 1 + src/pages/publishBall/index.module.scss | 143 +++++++++++- src/pages/publishBall/index.tsx | 217 +++++++++++++++--- src/pages/publishBall/publishForm.tsx | 10 +- src/services/httpService.ts | 4 +- src/services/publishService.ts | 38 +++ src/static/publishBall/icon-delete.svg | 5 + src/static/publishBall/icon-heartcircle.png | Bin 0 -> 6810 bytes 34 files changed, 679 insertions(+), 602 deletions(-) create mode 100644 src/components/ActivityTypeSwitch/index.module.scss delete mode 100644 src/components/ActivityTypeSwitch/index.scss delete mode 100644 src/components/CoverImageUpload/index.ts rename src/components/{CoverImageUpload/CoverImageUpload.scss => ImageUpload/ImageUpload.scss} (98%) rename src/components/{CoverImageUpload/CoverImageUpload.tsx => ImageUpload/ImageUpload.tsx} (93%) create mode 100644 src/components/ImageUpload/index.ts delete mode 100644 src/components/NTRPSlider/NTRPSlider.scss delete mode 100644 src/components/NTRPSlider/NTRPSlider.tsx delete mode 100644 src/components/NTRPSlider/index.ts rename src/components/{ParticipantsControl/ParticipantsControl.scss => NumberInterval/NumberInterval.scss} (100%) rename src/components/{ParticipantsControl/ParticipantsControl.tsx => NumberInterval/NumberInterval.tsx} (87%) create mode 100644 src/components/NumberInterval/index.ts delete mode 100644 src/components/ParticipantsControl/index.ts create mode 100644 src/pages/index/index.module.scss create mode 100644 src/services/publishService.ts create mode 100644 src/static/publishBall/icon-delete.svg create mode 100644 src/static/publishBall/icon-heartcircle.png diff --git a/src/components/ActivityTypeSwitch/index.module.scss b/src/components/ActivityTypeSwitch/index.module.scss new file mode 100644 index 0000000..27c632c --- /dev/null +++ b/src/components/ActivityTypeSwitch/index.module.scss @@ -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; +} diff --git a/src/components/ActivityTypeSwitch/index.scss b/src/components/ActivityTypeSwitch/index.scss deleted file mode 100644 index c606d06..0000000 --- a/src/components/ActivityTypeSwitch/index.scss +++ /dev/null @@ -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; - } - - - } -} diff --git a/src/components/ActivityTypeSwitch/index.tsx b/src/components/ActivityTypeSwitch/index.tsx index 481215c..eb5c029 100644 --- a/src/components/ActivityTypeSwitch/index.tsx +++ b/src/components/ActivityTypeSwitch/index.tsx @@ -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 = ({ value, onChange }) => { return ( - + onChange('individual')} > - - + + - 个人约球 + 个人约球 onChange('group')} > - - 畅打活动 + + 畅打活动 ) diff --git a/src/components/CommonPopup/index.module.scss b/src/components/CommonPopup/index.module.scss index bc5b33b..fdfc417 100644 --- a/src/components/CommonPopup/index.module.scss +++ b/src/components/CommonPopup/index.module.scss @@ -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; diff --git a/src/components/CoverImageUpload/index.ts b/src/components/CoverImageUpload/index.ts deleted file mode 100644 index b5e0cf9..0000000 --- a/src/components/CoverImageUpload/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default, type CoverImage } from './CoverImageUpload' \ No newline at end of file diff --git a/src/components/CoverImageUpload/CoverImageUpload.scss b/src/components/ImageUpload/ImageUpload.scss similarity index 98% rename from src/components/CoverImageUpload/CoverImageUpload.scss rename to src/components/ImageUpload/ImageUpload.scss index 6c1d229..78e6bcf 100644 --- a/src/components/CoverImageUpload/CoverImageUpload.scss +++ b/src/components/ImageUpload/ImageUpload.scss @@ -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; diff --git a/src/components/CoverImageUpload/CoverImageUpload.tsx b/src/components/ImageUpload/ImageUpload.tsx similarity index 93% rename from src/components/CoverImageUpload/CoverImageUpload.tsx rename to src/components/ImageUpload/ImageUpload.tsx index b1efa51..88f6f06 100644 --- a/src/components/CoverImageUpload/CoverImageUpload.tsx +++ b/src/components/ImageUpload/ImageUpload.tsx @@ -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 = ({ +const ImageUpload: React.FC = ({ images, onChange, maxCount = 9 @@ -88,4 +88,4 @@ const CoverImageUpload: React.FC = ({ ) } -export default CoverImageUpload \ No newline at end of file +export default ImageUpload \ No newline at end of file diff --git a/src/components/ImageUpload/index.ts b/src/components/ImageUpload/index.ts new file mode 100644 index 0000000..369f620 --- /dev/null +++ b/src/components/ImageUpload/index.ts @@ -0,0 +1 @@ +export { default, type CoverImage } from './ImageUpload' \ No newline at end of file diff --git a/src/components/NTRPSlider/NTRPSlider.scss b/src/components/NTRPSlider/NTRPSlider.scss deleted file mode 100644 index ba28159..0000000 --- a/src/components/NTRPSlider/NTRPSlider.scss +++ /dev/null @@ -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; - } - } - } - } -} diff --git a/src/components/NTRPSlider/NTRPSlider.tsx b/src/components/NTRPSlider/NTRPSlider.tsx deleted file mode 100644 index ec09b41..0000000 --- a/src/components/NTRPSlider/NTRPSlider.tsx +++ /dev/null @@ -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 = ({ - 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 ( - - {showTitle && ( - - {title} - {currentRangeText} - - )} - - - - - 2.0及以下 - 4.0及以上 - - - - {/* 背景轨道 */} - - - {/* 选中区间 */} - - - {/* 最小值滑块 */} - handleSliderTouchStart('min')} - > - {value.min} - - - {/* 最大值滑块 */} - handleSliderTouchStart('max')} - > - {value.max} - - - - - - ) -} - -export default NTRPSlider diff --git a/src/components/NTRPSlider/index.ts b/src/components/NTRPSlider/index.ts deleted file mode 100644 index 5cd76d2..0000000 --- a/src/components/NTRPSlider/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default, type NTRPRange, getNTRPRangeText } from './NTRPSlider' diff --git a/src/components/ParticipantsControl/ParticipantsControl.scss b/src/components/NumberInterval/NumberInterval.scss similarity index 100% rename from src/components/ParticipantsControl/ParticipantsControl.scss rename to src/components/NumberInterval/NumberInterval.scss diff --git a/src/components/ParticipantsControl/ParticipantsControl.tsx b/src/components/NumberInterval/NumberInterval.tsx similarity index 87% rename from src/components/ParticipantsControl/ParticipantsControl.tsx rename to src/components/NumberInterval/NumberInterval.tsx index bf50099..2a8f829 100644 --- a/src/components/ParticipantsControl/ParticipantsControl.tsx +++ b/src/components/NumberInterval/NumberInterval.tsx @@ -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 = ({ +const NumberInterval: React.FC = ({ minParticipants, maxParticipants, onMinParticipantsChange, @@ -46,4 +46,4 @@ const ParticipantsControl: React.FC = ({ ) } -export default ParticipantsControl \ No newline at end of file +export default NumberInterval \ No newline at end of file diff --git a/src/components/NumberInterval/index.ts b/src/components/NumberInterval/index.ts new file mode 100644 index 0000000..2f8bed9 --- /dev/null +++ b/src/components/NumberInterval/index.ts @@ -0,0 +1 @@ +export { default } from './NumberInterval' diff --git a/src/components/ParticipantsControl/index.ts b/src/components/ParticipantsControl/index.ts deleted file mode 100644 index bd2824f..0000000 --- a/src/components/ParticipantsControl/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './ParticipantsControl' -export type { ParticipantsControlProps } from './ParticipantsControl' \ No newline at end of file diff --git a/src/components/SelectStadium/SelectStadium.tsx b/src/components/SelectStadium/SelectStadium.tsx index ed08bc6..119e760 100644 --- a/src/components/SelectStadium/SelectStadium.tsx +++ b/src/components/SelectStadium/SelectStadium.tsx @@ -117,6 +117,21 @@ const SelectStadium: React.FC = ({ 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, `${searchValue}`) @@ -215,9 +230,9 @@ const SelectStadium: React.FC = ({ - - {stadium.istance} · - {stadium.address} + + { e.stopPropagation(); handleItemLocation(stadium); }}>{stadium.istance} · + { e.stopPropagation(); handleItemLocation(stadium); }}>{stadium.address} diff --git a/src/components/SelectStadium/StadiumDetail.scss b/src/components/SelectStadium/StadiumDetail.scss index c366a0a..69bb8cd 100644 --- a/src/components/SelectStadium/StadiumDetail.scss +++ b/src/components/SelectStadium/StadiumDetail.scss @@ -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; diff --git a/src/components/SelectStadium/StadiumDetail.tsx b/src/components/SelectStadium/StadiumDetail.tsx index dc2e217..518711c 100644 --- a/src/components/SelectStadium/StadiumDetail.tsx +++ b/src/components/SelectStadium/StadiumDetail.tsx @@ -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 ( + + {title} + + + + 添加截图,平台会优先推荐 + + + ) + } + return ( + {title} + ) +} + +// 公共的容器组件 +const SectionContainer: React.FC<{ title: string; children: React.ReactNode, prop: string }> = ({ title, children, prop }) => ( + + + + {children} + + +) + const StadiumDetail: React.FC = ({ stadium, - onBack, - onConfirm }) => { - const [venueType, setVenueType] = useState('室内') - const [groundMaterial, setGroundMaterial] = useState('硬地') - const [additionalInfo, setAdditionalInfo] = useState('') - const [imagesList, setImagesList] = useState([]) + 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 ( @@ -84,9 +140,10 @@ const StadiumDetail: React.FC = ({ - {stadium.name} + {formData.stadiumName} - {stadium.address} + {formData.istance} · + {formData.stadiumAddress} @@ -96,54 +153,43 @@ const StadiumDetail: React.FC = ({ if (item.type === 'tags') { const selected = getSelectedByLabel(item.label) return ( - - {item.label} - - {item.options.map((opt) => ( - setSelectedByLabel(item.label, opt)} - > - {opt} - - ))} - - + + {item.options.map((opt) => ( + updateFormData(item.prop, opt)} + > + {opt} + + ))} + ) } if (item.type === 'textareaTag') { return ( - - {item.label} - + + updateFormData(item.prop, value)} placeholder='有其他场地信息可备注' options={(item.options || []).map((o) => ({ label: o, value: o }))} /> - - + ) } if (item.type === 'image') { return ( - - {item.label} - + updateFormData(item.prop, images)} /> - - - + ) } diff --git a/src/components/TextareaTag/TextareaTag.scss b/src/components/TextareaTag/TextareaTag.scss index 5bfb696..910ec70 100644 --- a/src/components/TextareaTag/TextareaTag.scss +++ b/src/components/TextareaTag/TextareaTag.scss @@ -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; } } } diff --git a/src/components/TitleInput/TitleInput.tsx b/src/components/TitleInput/TitleInput.tsx index 1d984b1..1a72cfc 100644 --- a/src/components/TitleInput/TitleInput.tsx +++ b/src/components/TitleInput/TitleInput.tsx @@ -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 = ({ }) => { return ( - onChange(e.detail.value)} maxlength={maxLength} + autoSize={true} + placeholderClass='title-input-placeholder' /> {value.length}/{maxLength} diff --git a/src/components/TitleInput/index.scss b/src/components/TitleInput/index.scss index 04a60c0..024d30e 100644 --- a/src/components/TitleInput/index.scss +++ b/src/components/TitleInput/index.scss @@ -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; } } \ No newline at end of file diff --git a/src/components/index.ts b/src/components/index.ts index 60d168e..cc5f816 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -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, diff --git a/src/components/index.types.ts b/src/components/index.types.ts index 6234305..61bcc79 100644 --- a/src/components/index.types.ts +++ b/src/components/index.types.ts @@ -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 } \ No newline at end of file +import { type CoverImage } from './ImageUpload' +export type { TimeRange, Stadium, ActivityType, CoverImage } \ No newline at end of file diff --git a/src/config/env.ts b/src/config/env.ts index 1d78051..104cf1b 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -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 = { // 开发环境 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 = { // 测试环境 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 = { // 生产环境 production: { name: '生产环境', - apiBaseURL: 'https://api.playballtogether.com', - apiVersion: 'v1', + apiBaseURL: 'https://sit.light120.com', timeout: 10000, enableLog: false, enableMock: false diff --git a/src/config/formSchema/publishBallFormSchema.ts b/src/config/formSchema/publishBallFormSchema.ts index b4224e7..02d5820 100644 --- a/src/config/formSchema/publishBallFormSchema.ts +++ b/src/config/formSchema/publishBallFormSchema.ts @@ -59,7 +59,7 @@ export const publishBallFormSchema: FormFieldConfig[] = [ placeholder: '好的标题更吸引人哦', required: true, props: { - maxLength: 20 + maxLength: 80 }, rules: [ { required: true, message: '请输入活动标题' }, diff --git a/src/config/images.js b/src/config/images.js index 027a8ee..c2ac918 100644 --- a/src/config/images.js +++ b/src/config/images.js @@ -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') } \ No newline at end of file diff --git a/src/pages/index/index.module.scss b/src/pages/index/index.module.scss new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/src/pages/index/index.module.scss @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/pages/publishBall/index.module.scss b/src/pages/publishBall/index.module.scss index 3dd5244..ffe1d1d 100644 --- a/src/pages/publishBall/index.module.scss +++ b/src/pages/publishBall/index.module.scss @@ -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; + } + } + } + } } // 旋转动画 diff --git a/src/pages/publishBall/index.tsx b/src/pages/publishBall/index.tsx index a9ed683..dc7e88d 100644 --- a/src/pages/publishBall/index.tsx +++ b/src/pages/publishBall/index.tsx @@ -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({ - 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('individual') + const [formData, setFormData] = useState([ + { + 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 = () => { {/* 活动类型切换 */} - + { + formData.map((item, index) => ( + + {/* 场次标题行 */} + {activityType === 'group' && index > 0 && ( + + + 第{index + 1}场 + showDeleteConfirm(index)} + > + + + + + + + {index > 0 && ( + handleCopyPrevious(index)} + > + 复制上一场 + + )} + + + )} + updateFormData(key, value, index)} + optionsConfig={publishBallFormSchema} + /> + + )) + } + { + activityType === 'group' && ( + + + 再添加一场 + + ) + } + {/* 删除确认弹窗 */} + {deleteConfirm.visible && ( + + + 确认移除该场次? + 该操作不可恢复 + + + + + + + )} {/* 完成按钮 */} diff --git a/src/pages/publishBall/publishForm.tsx b/src/pages/publishBall/publishForm.tsx index 4ee2722..cc7e1cc 100644 --- a/src/pages/publishBall/publishForm.tsx +++ b/src/pages/publishBall/publishForm.tsx @@ -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([]) 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 { const headers: Record = { '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, diff --git a/src/services/publishService.ts b/src/services/publishService.ts new file mode 100644 index 0000000..9e73c87 --- /dev/null +++ b/src/services/publishService.ts @@ -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> { + return httpService.post('/games/create', data, { + showLoading: true, + loadingText: '发布中...' + }) + } +} + +// 导出认证服务实例 +export default new PublishService() \ No newline at end of file diff --git a/src/static/publishBall/icon-delete.svg b/src/static/publishBall/icon-delete.svg new file mode 100644 index 0000000..10f7414 --- /dev/null +++ b/src/static/publishBall/icon-delete.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/static/publishBall/icon-heartcircle.png b/src/static/publishBall/icon-heartcircle.png new file mode 100644 index 0000000000000000000000000000000000000000..21d9ed498a78db705fef0b9629c40e7a7136c13d GIT binary patch literal 6810 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 IM6N<$g3*O48UO$Q literal 0 HcmV?d00001