修改发布

This commit is contained in:
筱野
2025-08-23 21:39:46 +08:00
parent 8bc2fa8d97
commit c6f4f11259
29 changed files with 384 additions and 241 deletions

View File

@@ -1,89 +0,0 @@
@use '~@/scss/images.scss' as img;
@use '~@/scss/themeColor.scss' as theme;
// FormBasicInfo 组件样式
.form-basic-info{
background: white;
border-radius: 16px;
width: 100%;
// 费用项目
.form-item {
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
padding-left: 12px;
&:last-child{
.form-wrapper{
border-bottom: none;
}
}
.form-label {
display: flex;
align-items: center;
font-size: 14px;
padding-right: 14px;
.lable-icon {
width: 16px;
height: 16px;
}
text {
font-size: 16px;
color: #333;
font-weight: 500;
}
}
.form-wrapper{
display: flex;
justify-content: space-between;
flex: 1;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
align-items: center;
.form-item-label{
display: flex;
}
.form-right-wrapper{
display: flex;
padding-right: 12px;
height: 44px;
line-height: 44px;
align-items: center;
.title-placeholder{
font-size: 14px;
color: theme.$textarea-placeholder-color;
font-weight: 400;
}
.h5-input{
font-size: 14px;
color: #333;
font-weight: 500;
width: 50px;
text-align: right;
margin-right: 8px;
}
.unit{
font-size: 14px;
color: theme.$primary-color;
}
.right-text{
color: theme.$textarea-placeholder-color;
font-size: 14px;
padding-right: 8px;
width: 200px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
text-align: right;
}
.arrow{
width: 16px;
height: 16px;
margin-left: 4px;
}
}
}
}
}

View File

@@ -1,86 +0,0 @@
import React from 'react'
import { View, Text, Input, Image, Picker } from '@tarojs/components'
import { Stadium } from '../SelectStadium'
import img from '@/config/images';
import './FormBasicInfo.scss'
import { FormFieldConfig } from '@/config/formSchema/publishBallFormSchema';
interface FormBasicInfoProps {
fee: string
location: string
gameplay: string
selectedStadium: Stadium | null
onFeeChange: (value: string) => void
onLocationChange: (value: string) => void
onGameplayChange: (value: string) => void
onStadiumSelect: () => void
children: FormFieldConfig[]
}
const FormBasicInfo: React.FC<FormBasicInfoProps> = ({
fee,
location,
gameplay,
selectedStadium,
onFeeChange,
onLocationChange,
onGameplayChange,
onStadiumSelect,
children
}) => {
const renderChildren = () => {
return children.map((child: any, index: number) => {
return <View className='form-item'>
<View className='form-label'>
<Image className='lable-icon' src={img[child.iconType]} />
</View>
{
index === 0 && (<View className='form-wrapper'>
<Text className='form-item-label'>{child.label}</Text>
<View className='form-right-wrapper'>
<Input
className='fee-input'
placeholder='请输入'
placeholderClass='title-placeholder'
type='digit'
value={fee}
onInput={(e) => onFeeChange(e.detail.value)}
/>
<Text className='unit'>/</Text>
</View>
</View>)
}
{
index === 1 && (<View className='form-wrapper'>
<Text className='form-item-label'>{child.label}</Text>
<View className='form-right-wrapper' onClick={onStadiumSelect}>
<Text className={`right-text ${selectedStadium ? 'selected' : ''}`}>
{selectedStadium ? selectedStadium.name : '请选择'}
</Text>
<Image src={img.ICON_ARROW_RIGHT} className='arrow'/>
</View>
</View>)
}
{
index === 2 && ( <View className='form-wrapper'>
<Text className='form-item-label'>{child.label}</Text>
<View className='form-right-wrapper'>
<Text className={`right-text ${gameplay ? 'selected' : ''}`}>
{gameplay ? gameplay : '请选择'}
</Text>
<Image src={img.ICON_ARROW_RIGHT} className='arrow'/>
</View>
</View>)
}
</View>
})
}
return (
<View className='form-basic-info'>
{/* 费用 */}
{renderChildren()}
</View>
)
}
export default FormBasicInfo

View File

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

View File

@@ -1,94 +0,0 @@
# 球馆选择流程说明
## 🎯 完整流程
### 1. 初始状态 - 球馆列表
用户看到球馆选择弹窗,显示:
- 搜索框(可点击)
- 热门球场标题
- 球馆列表
- 底部取消/完成按钮
### 2. 点击搜索框
- 搜索框变为可点击状态
- 点击后跳转到地图选择页面
### 3. 地图选择页面
用户在地图页面可以:
- 查看地图,选择位置
- 在搜索框输入关键词搜索地点
- 从搜索结果中选择地点
- 点击"确定"按钮确认选择
### 4. 返回球馆详情
选择地点后:
- 自动跳转回球馆选择页面
- 显示球馆详情配置页面
- 新选择的球馆名称会显示在"已选球场"部分
### 5. 配置球馆详情
用户可以配置:
- 场地类型(室内/室外/室外雨棚)
- 地面材质(硬地/红土/草地)
- 场地信息补充(文本输入)
### 6. 完成选择
- 点击"完成"按钮
- 关闭弹窗,返回主页面
- 选中的球馆信息传递给父组件
## 🔄 状态管理
```typescript
// 主要状态
const [showDetail, setShowDetail] = useState(false) // 是否显示详情页
const [showMapSelector, setShowMapSelector] = useState(false) // 是否显示地图选择器
const [selectedStadium, setSelectedStadium] = useState<Stadium | null>(null) // 选中的球馆
```
## 📱 组件切换逻辑
```typescript
// 组件渲染优先级
if (showMapSelector) {
return <MapSelector /> // 1. 地图选择器
} else if (showDetail && selectedStadium) {
return <StadiumDetail /> // 2. 球馆详情
} else {
return <SelectStadium /> // 3. 球馆列表
}
```
## 🗺️ 地图集成
- 使用 Taro 的 `Map` 组件
- 支持地图标记和位置选择
- 集成搜索功能,支持关键词搜索
- 搜索结果包含地点名称、地址和距离信息
## 📋 数据传递
```typescript
// 从地图选择器传递到球馆详情
const handleMapLocationSelect = (location: Location) => {
const newStadium: Stadium = {
id: `map_${location.id}`,
name: location.name, // 地图选择的球场名称
address: location.address // 地图选择的球场地址
}
// 添加到球馆列表并选择
stadiumList.unshift(newStadium)
setSelectedStadium(newStadium)
setShowMapSelector(false)
setShowDetail(true)
}
```
## 🎨 用户体验
1. **无缝切换**:三个页面共享同一个弹窗容器
2. **状态保持**:选择的地点信息会正确传递
3. **视觉反馈**:选中状态有明确的视觉指示
4. **操作简单**:点击搜索即可进入地图选择
5. **数据同步**:地图选择的球场会自动添加到球馆列表

View File

@@ -1,118 +0,0 @@
# SelectStadium 球馆选择组件
这是一个球馆选择和详情的复合组件,包含两个主要功能:
1. 球馆列表选择
2. 球馆详情配置
## 功能特性
- 🏟️ 球馆搜索和选择
- 📱 响应式设计,适配移动端
- 🔄 无缝切换球馆列表和详情页面
- 🎯 支持场地类型、地面材质等配置
- 📝 场地信息补充
## 使用方法
### 基础用法
```tsx
import React, { useState } from 'react'
import { SelectStadium, Stadium } from './components/SelectStadium'
const App: React.FC = () => {
const [showSelector, setShowSelector] = useState(false)
const [selectedStadium, setSelectedStadium] = useState<Stadium | null>(null)
const handleStadiumSelect = (stadium: Stadium | null) => {
setSelectedStadium(stadium)
setShowSelector(false)
}
return (
<div>
<button onClick={() => setShowSelector(true)}>
</button>
<SelectStadium
visible={showSelector}
onClose={() => setShowSelector(false)}
onConfirm={handleStadiumSelect}
/>
</div>
)
}
```
## 组件结构
```
SelectStadium/
├── SelectStadium.tsx # 主组件,管理状态和切换逻辑
├── StadiumDetail.tsx # 球馆详情组件
├── SelectStadium.scss # 球馆列表样式
├── StadiumDetail.scss # 球馆详情样式
├── index.ts # 导出文件
└── README.md # 说明文档
```
## Props
### SelectStadium
| 属性 | 类型 | 必填 | 说明 |
|------|------|------|------|
| visible | boolean | 是 | 控制弹窗显示/隐藏 |
| onClose | () => void | 是 | 关闭弹窗回调 |
| onConfirm | (stadium: Stadium \| null) => void | 是 | 确认选择回调 |
### StadiumDetail
| 属性 | 类型 | 必填 | 说明 |
|------|------|------|------|
| stadium | Stadium | 是 | 选中的球馆信息 |
| onBack | () => void | 是 | 返回球馆列表回调 |
| onConfirm | (stadium, venueType, groundMaterial, additionalInfo) => void | 是 | 确认配置回调 |
## 数据接口
### Stadium
```typescript
interface Stadium {
id: string
name: string
address?: string
}
```
## 配置选项
### 场地类型
- 室内
- 室外
- 室外雨棚
### 地面材质
- 硬地
- 红土
- 草地
### 场地信息补充
- 文本输入框,支持用户自定义备注信息
## 样式定制
组件使用 SCSS 编写,可以通过修改以下文件来自定义样式:
- `SelectStadium.scss` - 球馆列表样式
- `StadiumDetail.scss` - 球馆详情样式
## 注意事项
1. 组件依赖 `@nutui/nutui-react-taro``Popup` 组件
2. 确保在 Taro 环境中使用
3. 组件内部管理状态,外部只需要控制 `visible` 属性
4. 球馆列表数据在组件内部硬编码,实际使用时可以通过 props 传入
5. StadiumDetail 组件现在只包含场地配置选项,去掉了头部、提醒和活动封面部分

View File

@@ -1,240 +0,0 @@
.select-stadium {
width: 100%;
height: calc(100vh - 10px);
background: #f5f5f5;
display: flex;
flex-direction: column;
overflow: hidden;
padding-bottom: env(safe-area-inset-bottom);
// 搜索区域
.search-section {
background: #f5f5f5;
padding: 26px 15px 0px 15px;
.search-wrapper {
display: flex;
align-items: center;
gap: 8px;
.search-bar {
font-size: 16px;
display: flex;
height: 44px;
padding: 0 12px;
align-items: center;
gap: 10px;
flex: 1;
border-radius: 999px;
border: 0.5px solid rgba(0, 0, 0, 0.06);
background: #FFF;
box-shadow: 0 4px 48px 0 rgba(0, 0, 0, 0.08);
.search-icon {
width: 20px;
height: 20px;
}
.clear-btn {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 50%;
cursor: pointer;
&:active {
background: rgba(0, 0, 0, 0.04);
}
.clear-icon {
width: 20px;
height: 20px;
}
}
}
.search-input {
flex: 1;
font-size: 16px;
color: #333;
border: none;
outline: none;
background: transparent;
.search-placeholder{
color: rgba(60, 60, 67, 0.60);
}
}
.map-btn {
display: flex;
height: 44px;
padding: 0 12px;
align-items: center;
gap: 2px;
border-radius: 999px;
border: 0.5px solid rgba(0, 0, 0, 0.06);
background: #FFF;
box-shadow: 0 4px 48px 0 rgba(0, 0, 0, 0.08);
box-sizing: border-box;
&:active {
background: #e0f0ff;
}
.map-icon {
width: 20px;
height: 20px;
}
.map-text {
font-size: 16px;
color: #000;
}
}
}
}
// 热门球场区域
.hot-section {
padding: 23px 20px 10px 20px;
.hot-header {
display: flex;
align-items: center;
.hot-title {
font-size: 16px;
font-weight: 600;
color: #000;
}
.hot-stadium-line{
height: 6px;
width: 1px;
background: rgba(22, 24, 35, 0.12);
margin: 0 12px;;
}
.booking-section {
display: flex;
align-items: center;
color: rgba(0, 0, 0, 0.50);
gap: 4px;
.booking-title {
font-size: 15px;
}
.booking-status {
display: flex;
padding: 2px 5px;
align-items: center;
gap: 4px;
border-radius: 999px;
border: 0.5px solid rgba(0, 0, 0, 0.16);
background: #FFF;
}
}
}
}
// 场馆列表
.stadium-list {
flex: 1;
width: auto;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
.stadium-item {
padding: 16px 20px;
display: flex;
align-items: center;
position: relative;
gap: 12px;
.stadium-item-left{
display: flex;
padding: 14px;
justify-content: center;
align-items: center;
border-radius: 12px;
border: 0.5px solid rgba(0, 0, 0, 0.08);
background: rgba(0, 0, 0, 0.06);
.stadium-icon{
width: 20px;
height: 20px;
}
}
.stadium-item-right{
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
.stadium-name{
font-size: 16px;
color: #000;
font-weight: 600;
line-height: 24px;
display: flex;
}
.stadium-address{
display: flex;
align-items: center;
gap: 4px;
color: rgba(0, 0, 0, 0.80);
font-size: 12px;
}
.stadium-map-icon{
width: 10px;
height: 10px;
}
}
}
}
// 底部按钮区域
.bottom-actions {
background: white;
padding: 16px;
padding-bottom: calc(16px + env(safe-area-inset-bottom));
border-top: 1px solid #e5e5e5;
flex-shrink: 0;
.action-buttons {
display: flex;
gap: 12px;
.cancel-btn,
.confirm-btn {
flex: 1;
height: 48px;
border-radius: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.cancel-btn {
background: #f5f5f5;
border: 1px solid #e0e0e0;
.cancel-text {
font-size: 16px;
color: #666;
font-weight: 500;
}
}
.confirm-btn {
background: #333;
.confirm-text {
font-size: 16px;
color: white;
font-weight: 500;
}
}
}
}
}
// 搜索框占位符样式
.search-input::placeholder {
color: #999;
}

View File

@@ -1,265 +0,0 @@
import React, { useState } from 'react'
import { View, Text, Input, ScrollView, Image } from '@tarojs/components'
import { Popup } from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
import StadiumDetail from './StadiumDetail'
import CommonPopup from '../CommonPopup'
import './SelectStadium.scss'
import images from '@/config/images'
export interface Stadium {
id?: string
name: string
address?: string
istance?: string
longitude?: number
latitude?: number
}
interface SelectStadiumProps {
visible: boolean
onClose: () => void
onConfirm: (stadium: Stadium | null) => void
}
const stadiumList: Stadium[] = [
{ id: '1', name: '静安网球馆', address: '浦东新区东园路18号', istance: '100米' , longitude: 121.4367, latitude: 31.2304},
{ id: '2', name: '芦湾体育馆', address: '浦东新区东园路18号', istance: '100米' , longitude: 121.4367, latitude: 31.2304 },
{ id: '3', name: '静安网球馆', address: '浦东新区东园路18号', istance: '100米' , longitude: 121.4367, latitude: 31.2304 },
{ id: '4', name: '徐汇游泳中心', address: '浦东新区东园路18号', istance: '100米' , longitude: 121.4367, latitude: 31.2304 },
{ id: '5', name: '汇龙新城小区', address: '浦东新区东园路18号', istance: '100米' , longitude: 121.4367, latitude: 31.2304 },
{ id: '6', name: '翠湖御苑小区', address: '浦东新区东园路18号', istance: '100米' , longitude: 121.4367, latitude: 31.2304 },
{ id: '7', name: '仁恒河滨花园网球场', address: '浦东新区东园路18号', istance: '100米' , longitude: 121.4367, latitude: 31.2304 },
{ id: '8', name: 'Our Tennis 东江球场', address: '浦东新区东园路18号', istance: '100米' , longitude: 121.4367, latitude: 31.2304 },
{ id: '9', name: '上海琦梦网球俱乐部', address: '浦东新区东园路18号', istance: '100米' , longitude: 121.4367, latitude: 31.2304 }
]
const SelectStadium: React.FC<SelectStadiumProps> = ({
visible,
onClose,
onConfirm
}) => {
const [searchValue, setSearchValue] = useState('')
const [selectedStadium, setSelectedStadium] = useState<Stadium | null>(null)
const [showDetail, setShowDetail] = useState(false)
if (!visible) return null
// 过滤场馆列表
const filteredStadiums = stadiumList.filter(stadium =>
stadium.name.toLowerCase().includes(searchValue.toLowerCase())
)
// 处理场馆选择
const handleStadiumSelect = (stadium: Stadium) => {
setSelectedStadium(stadium)
setShowDetail(true)
}
// 处理返回球馆列表
const handleBackToList = () => {
setShowDetail(false)
setSelectedStadium(null)
}
// 处理搜索框输入
const handleSearchInput = (e: any) => {
setSearchValue(e.detail.value)
}
// 处理地图选择位置
const handleMapLocation = () => {
Taro.chooseLocation({
success: (res) => {
console.log('选择位置成功:', res)
setSelectedStadium({
name: res.name,
address: res.address,
longitude: res.longitude,
latitude: res.latitude
})
setShowDetail(true)
},
fail: (err) => {
console.error('选择位置失败:', err)
Taro.showToast({
title: '位置选择失败',
icon: 'error'
})
}
})
}
// 处理确认
const handleConfirm = (stadium: Stadium, venueType: string, groundMaterial: string, additionalInfo: string) => {
// 这里可以处理球馆详情的信息
console.log('球馆详情:', { stadium, venueType, groundMaterial, additionalInfo })
onConfirm(stadium)
setShowDetail(false)
setSelectedStadium(null)
setSearchValue('')
}
// 处理球馆列表确认
const handleListConfirm = () => {
if (selectedStadium) {
onConfirm(selectedStadium)
setSelectedStadium(null)
setSearchValue('')
}
}
// 处理取消
const handleCancel = () => {
onClose()
setShowDetail(false)
setSelectedStadium(null)
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>`)
}
// 如果显示详情页面
if (showDetail && selectedStadium) {
return (
<CommonPopup
visible={visible}
onClose={handleCancel}
cancelText="返回"
confirmText="确认"
className="select-stadium-popup"
onCancel={handleCancel}
onConfirm={handleListConfirm}
position="bottom"
round
>
<StadiumDetail
stadium={selectedStadium}
onBack={handleBackToList}
onConfirm={handleConfirm}
/>
</CommonPopup>
)
}
// 显示球馆列表
return (
<CommonPopup
visible={visible}
hideFooter
onClose={handleCancel}
cancelText="返回"
confirmText="完成"
className="select-stadium-popup"
onCancel={handleCancel}
onConfirm={handleListConfirm}
position="bottom"
round
>
<View className='select-stadium'>
{/* 搜索框 */}
<View className='search-section'>
<View className='search-wrapper'>
<View className='search-bar'>
<Image src={images.ICON_SEARCH} className='search-icon' />
<Input
className='search-input'
placeholder='搜索'
placeholderClass='search-placeholder'
value={searchValue}
onInput={handleSearchInput}
/>
{searchValue && (
<View className='clear-btn' onClick={() => setSearchValue('')}>
<Image src={images.ICON_REMOVE} className='clear-icon' />
</View>
)}
</View>
{
!searchValue && (
<View className='map-btn' onClick={handleMapLocation}>
<Image src={images.ICON_MAP} className='map-icon' />
<Text className='map-text'></Text>
</View>
)
}
</View>
</View>
{/* 热门球场标题 */}
<View className='hot-section'>
<View className='hot-header'>
<Text className='hot-title'></Text>
<View className='hot-stadium-line'></View>
<View className='booking-section'>
<Text className='booking-title'></Text>
<Text className='booking-status'></Text>
</View>
</View>
</View>
{/* 场馆列表 */}
<ScrollView className='stadium-list' scrollY>
{filteredStadiums.map((stadium) => (
<View
key={stadium.id}
className={`stadium-item ${selectedStadium?.id === stadium.id ? 'selected' : ''}`}
onClick={() => handleStadiumSelect(stadium)}
>
<View className='stadium-item-left'>
<Image src={images.ICON_STADIUM} className='stadium-icon' />
</View>
<View className='stadium-item-right'>
<View className='stadium-name' dangerouslySetInnerHTML={{ __html: markSearchText(stadium.name) }}></View>
<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>
</View>
))}
{
searchValue && (<View
className={`stadium-item`}
onClick={() => handleMapLocation()}
>
<View className='stadium-item-left'>
<Image src={images.ICON_MAP_SEARCH} className='stadium-icon' />
</View>
<View className='stadium-item-right'>
<View className='stadium-name'></View>
<View className='stadium-address'>
<Text></Text>
<Image src={images.ICON_ARRORW_SMALL} className='stadium-map-icon' />
</View>
</View>
</View>)
}
</ScrollView>
</View>
</CommonPopup>
)
}
export default SelectStadium

View File

@@ -1,193 +0,0 @@
.stadium-detail {
width: 100%;
height: auto;
min-height: 60vh;
background: white;
display: flex;
flex-direction: column;
overflow: hidden;
padding-bottom: env(safe-area-inset-bottom);
// 已选球场
// 场馆列表
.stadium-item {
padding: 32px 20px 16px 20px;
display: flex;
align-items: center;
position: relative;
gap: 12px;
.stadium-item-left{
display: flex;
padding: 14px;
justify-content: center;
align-items: center;
border-radius: 12px;
border: 0.5px solid rgba(0, 0, 0, 0.08);
background: rgba(0, 0, 0, 0.06);
.stadium-icon{
width: 20px;
height: 20px;
}
}
.stadium-item-right{
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
.stadium-name{
font-size: 16px;
color: #000;
font-weight: 600;
line-height: 24px;
display: flex;
}
.stadium-address{
display: flex;
align-items: center;
gap: 4px;
color: rgba(0, 0, 0, 0.80);
font-size: 12px;
}
.stadium-map-icon{
width: 10px;
height: 10px;
}
}
}
// 场地类型
.venue-type-section {
flex-shrink: 0;
.section-title {
padding: 18px 20px 10px 20px;
font-size: 14px;
font-weight: 600;
color: #333;
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 {
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;
padding: 16px;
padding-bottom: calc(16px + env(safe-area-inset-bottom));
border-top: 1px solid #e5e5e5;
flex-shrink: 0;
margin-top: auto;
.action-buttons {
display: flex;
gap: 12px;
.cancel-btn,
.confirm-btn {
flex: 1;
height: 48px;
border-radius: 24px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.cancel-btn {
background: white;
border: 1px solid #e0e0e0;
.cancel-text {
font-size: 16px;
color: #666;
font-weight: 500;
}
}
.confirm-btn {
background: #333;
.confirm-text {
font-size: 16px;
color: white;
font-weight: 500;
}
}
}
}
}

View File

@@ -1,202 +0,0 @@
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/ImageUpload'
export interface Stadium {
id?: string
name: string
address?: string
longitude?: number
latitude?: number
istance?: string
}
interface StadiumDetailProps {
stadium: Stadium
onBack: () => void
onConfirm: (stadium: Stadium, venueType: string, groundMaterial: string, additionalInfo: string) => void
}
const stadiumInfo = [
{
label: '场地类型',
options: ['室内', '室外', '室外雨棚'],
prop: 'venueType',
type: 'tags'
},
{
label: '地面材质',
options: ['硬地', '红土', '草地'],
prop: 'groundMaterial',
type: 'tags'
},
{
label: '场地信息补充',
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,
}) => {
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 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) => {
if (label === '场地类型') return formData.venueType
if (label === '地面材质') return formData.groundMaterial
return ''
}, [formData.venueType, formData.groundMaterial])
console.log(stadium,'stadiumstadium');
return (
<View className='stadium-detail'>
{/* 已选球场 */}
<View
className={`stadium-item`}
onClick={() => handleMapLocation()}
>
<View className='stadium-item-left'>
<Image src={images.ICON_STADIUM} className='stadium-icon' />
</View>
<View className='stadium-item-right'>
<View className='stadium-name'>{formData.stadiumName}</View>
<View className='stadium-address'>
<Text>{formData.istance} · </Text>
<Text>{formData.stadiumAddress}</Text>
<Image src={images.ICON_ARRORW_SMALL} className='stadium-map-icon' />
</View>
</View>
</View>
{stadiumInfo.map((item) => {
if (item.type === 'tags') {
const selected = getSelectedByLabel(item.label)
return (
<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 (
<SectionContainer key={item.label} title={item.label} prop={item.prop}>
<View className='textarea-tag-container'>
<TextareaTag
value={formData.additionalInfo}
onChange={(value) => updateFormData(item.prop, value)}
placeholder='有其他场地信息可备注'
options={(item.options || []).map((o) => ({ label: o, value: o }))}
/>
</View>
</SectionContainer>
)
}
if (item.type === 'image') {
return (
<SectionContainer key={item.label} title={item.label} prop={item.prop}>
<CoverImageUpload
images={formData.imagesList}
onChange={(images) => updateFormData(item.prop, images)}
/>
</SectionContainer>
)
}
return null
})}
</View>
)
}
export default StadiumDetail

View File

@@ -1,3 +0,0 @@
export { default as SelectStadium } from './SelectStadium'
export { default as StadiumDetail } from './StadiumDetail'
export type { Stadium } from './SelectStadium'

View File

@@ -24,6 +24,7 @@ const TextareaTag: React.FC<TextareaTagProps> = ({
}) => {
// 处理输入框变化
const [tags, setTags] = useState<string[]>([])
console.log(value, 'options')
const handleInputChange = useCallback((e: any) => {
onChange(e.detail.value)
}, [onChange])
@@ -43,6 +44,7 @@ const TextareaTag: React.FC<TextareaTagProps> = ({
onChange(newValue)
}, [value, onChange])
console.log(options, 'options')
return (
<View className='textarea-tag'>
{/* 选择选项 */}

View File

@@ -1,11 +1,11 @@
import React from 'react'
import { View, Text, Picker } from '@tarojs/components'
import { getDate, getTime } from '@/utils/timeUtils'
import './TimeSelector.scss'
export interface TimeRange {
startDate: string
startTime: string
endTime: string
start_time: string
end_time: string
}
interface TimeSelectorProps {
@@ -15,40 +15,13 @@ interface TimeSelectorProps {
const TimeSelector: React.FC<TimeSelectorProps> = ({
value = {
startDate: '',
startTime: '',
endTime: ''
start_time: '',
end_time: ''
},
onChange
}) => {
// 格式化日期显示
const formatDate = (dateStr: string) => {
return dateStr.replace(/-/g, '年').replace(/-/g, '月') + '日'
}
// 处理开始日期变化
const handleStartDateChange = (e: any) => {
onChange({
...value,
startDate: e.detail.value
})
}
// 处理开始时间变化
const handleStartTimeChange = (e: any) => {
onChange({
...value,
startTime: e.detail.value
})
}
// 处理结束时间变化
const handleEndTimeChange = (e: any) => {
onChange({
...value,
endTime: e.detail.value
})
}
return (
<View className='time-selector'>
@@ -61,8 +34,8 @@ const TimeSelector: React.FC<TimeSelectorProps> = ({
<View className='time-content'>
<Text className='time-label'></Text>
<view className='time-text-wrapper'>
<Text className='time-text'>20251123</Text>
<Text className='time-text time-am'>8:00AM</Text>
<Text className='time-text'>{getDate(value.start_time)}</Text>
<Text className='time-text time-am'>{getTime(value.start_time)}</Text>
</view>
</View>
</View>
@@ -75,8 +48,7 @@ const TimeSelector: React.FC<TimeSelectorProps> = ({
<View className='time-content'>
<Text className='time-label'></Text>
<view className='time-text-wrapper'>
<Text className='time-text'>20251123</Text>
<Text className='time-text time-am'>8:00AM</Text>
<Text className='time-text time-am'>{getTime(value.end_time)}</Text>
</view>
</View>
</View>

View File

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

View File

@@ -3,14 +3,14 @@ import { View } from '@tarojs/components'
import { TextArea } from '@nutui/nutui-react-taro'
import './index.scss'
interface TitleInputProps {
interface TitleTextareaProps {
value: string
onChange: (value: string) => void
maxLength?: number
placeholder?: string
}
const TitleInput: React.FC<TitleInputProps> = ({
const TitleTextarea: React.FC<TitleTextareaProps> = ({
value,
onChange,
maxLength = 20,
@@ -32,4 +32,4 @@ const TitleInput: React.FC<TitleInputProps> = ({
)
}
export default TitleInput
export default TitleTextarea

View File

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

View File

@@ -2,12 +2,11 @@ import ActivityTypeSwitch from './ActivityTypeSwitch'
import TextareaTag from './TextareaTag'
import FormSwitch from './FormSwitch'
import ImageUpload from './ImageUpload'
import FormBasicInfo from './FormBasicInfo'
import Range from './Range'
import NumberInterval from './NumberInterval'
import { SelectStadium, StadiumDetail } from './SelectStadium'
import TimeSelector from './TimeSelector'
import TitleInput from './TitleInput'
import TitleTextarea from './TitleTextarea'
import CommonPopup from './CommonPopup'
export {
@@ -15,13 +14,10 @@ export {
TextareaTag,
FormSwitch,
ImageUpload,
FormBasicInfo,
Range,
NumberInterval,
SelectStadium,
TimeSelector,
TitleInput,
StadiumDetail,
TitleTextarea,
CommonPopup
}

View File

@@ -1,5 +1,4 @@
import { type TimeRange } from './TimeSelector'
import { type Stadium } from './SelectStadium'
import { type ActivityType } from './ActivityTypeSwitch'
import { type CoverImage } from './ImageUpload'
export type { TimeRange, Stadium, ActivityType, CoverImage }
export type { TimeRange, ActivityType, CoverImage }