feat: 分包
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
@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;
|
||||
&.selected{
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
.arrow{
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-left: 4px;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import React, { useState, useCallback, useEffect } from 'react'
|
||||
import { View, Text, Input, Image } from '@tarojs/components'
|
||||
import PopupGameplay from '../PopupGameplay'
|
||||
import img from '@/config/images';
|
||||
import { FormFieldConfig } from '@/config/formSchema/publishBallFormSchema';
|
||||
import SelectStadium from '../SelectStadium/SelectStadium'
|
||||
import { Stadium } from '../SelectStadium/StadiumDetail'
|
||||
import './FormBasicInfo.scss'
|
||||
|
||||
type PlayGame = {
|
||||
play_type: string // 玩法类型
|
||||
price: number | string // 价格
|
||||
venue_id?: number | null // 场地id
|
||||
location_name?: string // 场地名称
|
||||
location?: string // 场地地址
|
||||
latitude?: string // 纬度
|
||||
longitude?: string // 经度
|
||||
court_type?: string // 场地类型 1: 室内 2: 室外
|
||||
court_surface?: string // 场地表面 1: 硬地 2: 红土 3: 草地
|
||||
venue_description_tag?: Array<string>[] // 场地描述标签
|
||||
venue_description?: string // 场地描述
|
||||
venue_image_list?: Array<string>[] // 场地图片
|
||||
}
|
||||
interface FormBasicInfoProps {
|
||||
value: PlayGame
|
||||
onChange: (value: any) => void
|
||||
children: FormFieldConfig[]
|
||||
}
|
||||
|
||||
const FormBasicInfo: React.FC<FormBasicInfoProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
children
|
||||
}) => {
|
||||
const [gameplayVisible, setGameplayVisible] = useState(false)
|
||||
const [showStadiumSelector, setShowStadiumSelector] = useState(false)
|
||||
const [playGame, setPlayGame] = useState<{label: string, value: string }[]>([])
|
||||
const handleGameplaySelect = () => {
|
||||
setGameplayVisible(true)
|
||||
}
|
||||
|
||||
const handleGameplayConfirm = (selectedGameplay: string) => {
|
||||
onChange({...value, [children[2].prop]: selectedGameplay})
|
||||
setGameplayVisible(false)
|
||||
}
|
||||
|
||||
const handleGameplayClose = () => {
|
||||
setGameplayVisible(false)
|
||||
}
|
||||
// 处理场馆选择
|
||||
const handleStadiumSelect = (stadium: Stadium | null) => {
|
||||
console.log(stadium,'stadiumstadium');
|
||||
const { address, name, latitude, longitude, court_type, court_surface, description, description_tag, venue_image_list} = stadium || {};
|
||||
onChange({...value,
|
||||
venue_id: stadium?.id,
|
||||
location_name: name,
|
||||
location: address,
|
||||
latitude,
|
||||
longitude,
|
||||
court_type,
|
||||
court_surface,
|
||||
venue_description: description,
|
||||
venue_description_tag: description_tag,
|
||||
venue_image_list
|
||||
})
|
||||
setShowStadiumSelector(false)
|
||||
}
|
||||
|
||||
const handleChange = useCallback((key: string, costValue: any) => {
|
||||
// 价格输入限制:¥0.00–9999.99
|
||||
console.log(costValue, 'valuevalue');
|
||||
|
||||
if (key === children[0]?.prop) {
|
||||
// 允许清空
|
||||
if (costValue === '') {
|
||||
onChange({...value, [key]: ''});
|
||||
return;
|
||||
}
|
||||
|
||||
// 只允许数字和一个小数点
|
||||
const filteredValue = costValue.replace(/[^\d.]/g, '');
|
||||
|
||||
// 确保只有一个小数点
|
||||
const parts = filteredValue.split('.');
|
||||
if (parts.length > 2) {
|
||||
return; // 不更新,保持原值
|
||||
}
|
||||
|
||||
// 限制小数点后最多2位
|
||||
if (parts.length === 2 && parts[1].length > 2) {
|
||||
return; // 不更新,保持原值
|
||||
}
|
||||
|
||||
const numValue = parseFloat(filteredValue);
|
||||
if (isNaN(numValue)) {
|
||||
onChange({...value, [key]: ''});
|
||||
return;
|
||||
}
|
||||
if (numValue < 0) {
|
||||
onChange({...value, [key]: '0'});
|
||||
return;
|
||||
}
|
||||
if (numValue > 9999.99) {
|
||||
onChange({...value, [key]: '9999.99'});
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用过滤后的值
|
||||
onChange({...value, [key]: filteredValue});
|
||||
return;
|
||||
}
|
||||
onChange({...value, [key]: costValue})
|
||||
}, [onChange, children])
|
||||
|
||||
useEffect(() => {
|
||||
if (children.length > 2) {
|
||||
const options = children[2]?.options || [];
|
||||
setPlayGame(options)
|
||||
}
|
||||
}, [children])
|
||||
|
||||
useEffect(() => {
|
||||
console.log(value, 'valuevalue');
|
||||
}, [value])
|
||||
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'
|
||||
maxlength={7}
|
||||
value={value[child.prop]}
|
||||
onInput={(e) => handleChange(child.prop, 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={() => setShowStadiumSelector(true)}>
|
||||
<Text className={`right-text ${value[child.prop] ? 'selected' : ''}`}>
|
||||
{value[child.prop] ? value[child.prop] : '请选择'}
|
||||
</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' onClick={handleGameplaySelect}>
|
||||
<Text className={`right-text ${value[child.prop] ? 'selected' : ''}`}>
|
||||
{value[child.prop] ? value[child.prop] : '请选择'}
|
||||
</Text>
|
||||
<Image src={img.ICON_ARROW_RIGHT} className='arrow'/>
|
||||
</View>
|
||||
</View>)
|
||||
}
|
||||
</View>
|
||||
})
|
||||
}
|
||||
return (
|
||||
<View className='form-basic-info'>
|
||||
{/* 费用 */}
|
||||
{renderChildren()}
|
||||
{/* 玩法选择弹窗 */}
|
||||
<PopupGameplay
|
||||
visible={gameplayVisible}
|
||||
onClose={handleGameplayClose}
|
||||
onConfirm={handleGameplayConfirm}
|
||||
value={value[children[2].prop]}
|
||||
options={playGame}
|
||||
/>
|
||||
{/* 场馆选择弹窗 */}
|
||||
<SelectStadium
|
||||
visible={showStadiumSelector}
|
||||
onClose={() => setShowStadiumSelector(false)}
|
||||
onConfirm={handleStadiumSelect}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default FormBasicInfo
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './FormBasicInfo'
|
||||
@@ -0,0 +1,34 @@
|
||||
.optionsList {
|
||||
padding: 26px 15px 16px 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.optionItem {
|
||||
display: flex;
|
||||
min-height: 40px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex: 1 0 0;
|
||||
border-radius: 999px;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.12);
|
||||
background: #FFF;
|
||||
}
|
||||
|
||||
|
||||
.optionItem.selected {
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.06);
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
background: #000;
|
||||
.optionText {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.optionText {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { CommonPopup } from '@/components'
|
||||
import styles from './PopupGameplay.module.scss'
|
||||
|
||||
interface PopupGameplayProps {
|
||||
visible: boolean
|
||||
onClose: () => void
|
||||
onConfirm: (value: string) => void
|
||||
value?: string
|
||||
options?: { label: string, value: string }[]
|
||||
}
|
||||
|
||||
export default function PopupGameplay({ visible, onClose, onConfirm, value = '不限', options = [] }: PopupGameplayProps) {
|
||||
const [selectedOption, setSelectedOption] = useState(value)
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && value) {
|
||||
setSelectedOption(value)
|
||||
}
|
||||
}, [visible, value])
|
||||
|
||||
const handleOptionSelect = (option: string) => {
|
||||
setSelectedOption(option)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
onClose()
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
onConfirm(selectedOption)
|
||||
}
|
||||
|
||||
return (
|
||||
<CommonPopup
|
||||
visible={visible}
|
||||
onClose={handleClose}
|
||||
showHeader={false}
|
||||
onConfirm={handleConfirm}
|
||||
confirmText='确定'
|
||||
cancelText='取消'
|
||||
>
|
||||
<View className={styles.optionsList}>
|
||||
{options.map((option) => (
|
||||
<View
|
||||
key={option.value}
|
||||
className={`${styles.optionItem} ${selectedOption === option.value ? styles.selected : ''}`}
|
||||
onClick={() => handleOptionSelect(option.value)}
|
||||
>
|
||||
<Text className={styles.optionText}>{option.label}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</CommonPopup>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './PopupGameplay'
|
||||
@@ -0,0 +1,94 @@
|
||||
# 球馆选择流程说明
|
||||
|
||||
## 🎯 完整流程
|
||||
|
||||
### 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. **数据同步**:地图选择的球场会自动添加到球馆列表
|
||||
118
src/publish_pages/publishBall/components/SelectStadium/README.md
Normal file
118
src/publish_pages/publishBall/components/SelectStadium/README.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# 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 组件现在只包含场地配置选项,去掉了头部、提醒和活动封面部分
|
||||
@@ -0,0 +1,262 @@
|
||||
|
||||
|
||||
.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-item-loading{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
.loading-icon{
|
||||
color: #666;
|
||||
font-size: 30px;
|
||||
.nut-loading-icon{
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.nut-loading-text{
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 场馆列表
|
||||
.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;
|
||||
|
||||
.highlight-text {
|
||||
color: #007AFF;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
.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;
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
import React, { useState, useRef, useEffect } from 'react'
|
||||
import { View, Text, Input, ScrollView, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { Loading } from '@nutui/nutui-react-taro'
|
||||
import StadiumDetail, { StadiumDetailRef } from './StadiumDetail'
|
||||
import { CommonPopup } from '@/components'
|
||||
import { getLocation } from '@/utils/locationUtils'
|
||||
import PublishService from '@/services/publishService'
|
||||
import images from '@/config/images'
|
||||
import './SelectStadium.scss'
|
||||
|
||||
export interface Stadium {
|
||||
id?: string
|
||||
name: string
|
||||
address?: string
|
||||
distance_km?: number | null | undefined
|
||||
longitude?: number
|
||||
latitude?: number
|
||||
}
|
||||
|
||||
interface SelectStadiumProps {
|
||||
visible: boolean
|
||||
onClose: () => void
|
||||
onConfirm: (stadium: Stadium | null) => void
|
||||
}
|
||||
|
||||
|
||||
const SelectStadium: React.FC<SelectStadiumProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
onConfirm
|
||||
}) => {
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
const [selectedStadium, setSelectedStadium] = useState<Stadium | null>(null)
|
||||
const [showDetail, setShowDetail] = useState(false)
|
||||
const stadiumDetailRef = useRef<StadiumDetailRef>(null)
|
||||
const [stadiumList, setStadiumList] = useState<Stadium[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const initData = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const location = await getLocation()
|
||||
if (location.latitude && location.longitude) {
|
||||
const res = await PublishService.getStadiumList({
|
||||
seachOption: {
|
||||
latitude: location.latitude,
|
||||
longitude: location.longitude
|
||||
}
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
setStadiumList(res.data.rows || [])
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取场馆列表失败:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
initData()
|
||||
}
|
||||
}, [visible])
|
||||
|
||||
if (!visible) return null
|
||||
|
||||
// 过滤场馆列表
|
||||
const filteredStadiums = stadiumList.filter(stadium =>
|
||||
stadium.name.toLowerCase().includes(searchValue.toLowerCase())
|
||||
)
|
||||
|
||||
// 处理场馆选择
|
||||
const handleStadiumSelect = (stadium: Stadium) => {
|
||||
setSelectedStadium(stadium)
|
||||
setShowDetail(true)
|
||||
}
|
||||
|
||||
const calculateDistance = (stadium: Stadium) => {
|
||||
const distance_km = stadium.distance_km
|
||||
if (!distance_km) return ''
|
||||
if (distance_km && distance_km > 1) {
|
||||
return distance_km.toFixed(1) + 'km'
|
||||
}
|
||||
return (distance_km * 1000).toFixed(0) + 'm'
|
||||
}
|
||||
|
||||
|
||||
// 处理搜索框输入
|
||||
const handleSearchInput = (e: any) => {
|
||||
setSearchValue(e.detail.value)
|
||||
}
|
||||
|
||||
// 处理地图选择位置
|
||||
const handleMapLocation = () => {
|
||||
Taro.chooseLocation({
|
||||
success: (res) => {
|
||||
setSelectedStadium({
|
||||
name: res.name,
|
||||
address: res.address,
|
||||
longitude: res.longitude,
|
||||
latitude: res.latitude
|
||||
})
|
||||
setShowDetail(true)
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('选择位置失败:', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 处理确认
|
||||
const handleConfirm = () => {
|
||||
if (stadiumDetailRef.current) {
|
||||
const formData = stadiumDetailRef.current.getFormData()
|
||||
console.log('获取球馆表单数据:', formData)
|
||||
const { description, ...rest } = formData
|
||||
onConfirm({ ...rest, ...description })
|
||||
setSelectedStadium(null)
|
||||
setSearchValue('')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 处理取消
|
||||
const handleCancel = () => {
|
||||
onClose()
|
||||
setShowDetail(false)
|
||||
setSelectedStadium(null)
|
||||
setSearchValue('')
|
||||
}
|
||||
|
||||
const handleItemLocation = (stadium: Stadium) => {
|
||||
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) => {
|
||||
if (!searchValue) return text
|
||||
return text.replace(
|
||||
new RegExp(searchValue, 'gi'),
|
||||
`<span class="highlight-text">${searchValue}</span>`
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// 如果显示详情页面
|
||||
if (showDetail && selectedStadium) {
|
||||
return (
|
||||
<CommonPopup
|
||||
visible={visible}
|
||||
onClose={handleCancel}
|
||||
cancelText="返回"
|
||||
confirmText="确认"
|
||||
className="select-stadium-popup"
|
||||
onCancel={handleCancel}
|
||||
onConfirm={handleConfirm}
|
||||
position="bottom"
|
||||
round
|
||||
>
|
||||
<StadiumDetail
|
||||
ref={stadiumDetailRef}
|
||||
stadium={selectedStadium}
|
||||
/>
|
||||
</CommonPopup>
|
||||
)
|
||||
}
|
||||
|
||||
// 显示球馆列表
|
||||
return (
|
||||
<CommonPopup
|
||||
visible={visible}
|
||||
hideFooter
|
||||
onClose={handleCancel}
|
||||
cancelText="返回"
|
||||
confirmText="完成"
|
||||
className="select-stadium-popup"
|
||||
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>
|
||||
{
|
||||
loading ? (
|
||||
<View className='stadium-item-loading'>
|
||||
<Loading type="circular" className='loading-icon'>加载中</Loading>
|
||||
</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
|
||||
className='stadium-distance'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleItemLocation(stadium)
|
||||
}}
|
||||
>
|
||||
{calculateDistance(stadium)} ·
|
||||
</Text>
|
||||
<Text
|
||||
className='stadium-address-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 map-search-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 className='map-search-text'>腾讯地图</Text>
|
||||
<Image src={images.ICON_ARRORW_SMALL} className='stadium-map-icon' />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
{/* 场馆列表 */}
|
||||
|
||||
</View>
|
||||
</CommonPopup>
|
||||
)
|
||||
}
|
||||
|
||||
export default SelectStadium
|
||||
@@ -0,0 +1,192 @@
|
||||
.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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
import React, { useState, useCallback, forwardRef, useImperativeHandle } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import images from '@/config/images'
|
||||
import TextareaTag from '@/components/TextareaTag'
|
||||
// import CoverImageUpload, { type CoverImage } from '@/components/ImageUpload'
|
||||
import UploadCover, { type CoverImageValue } from '@/components/UploadCover'
|
||||
import { useDictionaryActions } from '@/store/dictionaryStore'
|
||||
import './StadiumDetail.scss'
|
||||
|
||||
export interface Stadium {
|
||||
id?: string
|
||||
name: string
|
||||
address?: string
|
||||
longitude?: number
|
||||
latitude?: number
|
||||
distance_km?: number | null
|
||||
court_type?: string
|
||||
court_surface?: string
|
||||
description?: string
|
||||
description_tag?: string[]
|
||||
venue_image_list?: CoverImageValue[]
|
||||
}
|
||||
|
||||
interface StadiumDetailProps {
|
||||
stadium: Stadium
|
||||
}
|
||||
|
||||
// 定义暴露给父组件的方法接口
|
||||
export interface StadiumDetailRef {
|
||||
getFormData: () => any
|
||||
setFormData: (data: any) => void
|
||||
}
|
||||
|
||||
|
||||
// 公共的标题组件
|
||||
const SectionTitle: React.FC<{ title: string,prop: string }> = ({ title, prop }) => {
|
||||
if (prop === 'venue_image_list') {
|
||||
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 = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
|
||||
stadium,
|
||||
}, ref) => {
|
||||
const { getDictionaryValue } = useDictionaryActions()
|
||||
const court_type = getDictionaryValue('court_type') || []
|
||||
const court_surface = getDictionaryValue('court_surface') || []
|
||||
const supplementary_information = getDictionaryValue('supplementary_information') || []
|
||||
const stadiumInfo = [
|
||||
{
|
||||
label: '场地类型',
|
||||
options: court_type,
|
||||
prop: 'court_type',
|
||||
type: 'tags'
|
||||
},
|
||||
{
|
||||
label: '地面材质',
|
||||
options: court_surface,
|
||||
prop: 'court_surface',
|
||||
type: 'tags'
|
||||
},
|
||||
{
|
||||
label: '场地信息补充',
|
||||
options: supplementary_information,
|
||||
prop: 'description',
|
||||
type: 'textareaTag'
|
||||
},
|
||||
{
|
||||
label: '场地预定截图',
|
||||
options: ['有其他场地信息可备注'],
|
||||
prop: 'venue_image_list',
|
||||
type: 'image'
|
||||
}
|
||||
]
|
||||
const [formData, setFormData] = useState({
|
||||
name: stadium.name,
|
||||
address: stadium.address,
|
||||
latitude: stadium.latitude,
|
||||
longitude: stadium.longitude,
|
||||
istance: stadium.distance_km,
|
||||
court_type: court_type[0] || '',
|
||||
court_surface: court_surface[0] || '',
|
||||
additionalInfo: '',
|
||||
venue_image_list: [] as CoverImageValue[],
|
||||
description:{
|
||||
description: '',
|
||||
description_tag: []
|
||||
}
|
||||
})
|
||||
|
||||
// 暴露方法给父组件
|
||||
useImperativeHandle(ref, () => ({
|
||||
getFormData: () => formData,
|
||||
setFormData: (data: any) => setFormData(data)
|
||||
}), [formData, stadium])
|
||||
|
||||
const calculateDistance = (distance_km: number | null) => {
|
||||
if (!distance_km) return ''
|
||||
if (distance_km && distance_km > 1) {
|
||||
return distance_km.toFixed(1) + 'km'
|
||||
}
|
||||
return (distance_km * 1000).toFixed(0) + 'm'
|
||||
}
|
||||
|
||||
|
||||
const handleMapLocation = () => {
|
||||
Taro.chooseLocation({
|
||||
success: (res) => {
|
||||
console.log(res,'resres');
|
||||
setFormData({
|
||||
...formData,
|
||||
name: res.name,
|
||||
address: res.address,
|
||||
latitude: res.longitude,
|
||||
longitude: res.latitude,
|
||||
istance: null
|
||||
})
|
||||
},
|
||||
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.court_type
|
||||
if (label === '地面材质') return formData.court_surface
|
||||
return ''
|
||||
}, [formData.court_type, formData.court_surface])
|
||||
|
||||
|
||||
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.name}</View>
|
||||
<View className='stadium-address'>
|
||||
<Text>{calculateDistance(formData.istance || null)} · </Text>
|
||||
<Text>{formData.address}</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[item.prop]}
|
||||
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}>
|
||||
<UploadCover
|
||||
value={formData[item.prop]}
|
||||
onChange={(value) => {
|
||||
console.log(value, 'value')
|
||||
if (value instanceof Function) {
|
||||
const newValue = value(formData[item.prop])
|
||||
console.log(newValue, 'newValue')
|
||||
updateFormData(item.prop, newValue)
|
||||
} else {
|
||||
updateFormData(item.prop, value)
|
||||
}
|
||||
}}
|
||||
maxCount={9}
|
||||
source={['album', 'history']}
|
||||
align='left'
|
||||
tag="screenshot"
|
||||
/>
|
||||
</SectionContainer>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
})}
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
export default StadiumDetail
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as SelectStadium } from './SelectStadium'
|
||||
export { default as StadiumDetail } from './StadiumDetail'
|
||||
export type { Stadium } from './SelectStadium'
|
||||
@@ -0,0 +1,68 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { View, Text, Input } from '@tarojs/components'
|
||||
import { Checkbox } from '@nutui/nutui-react-taro'
|
||||
import styles from './index.module.scss'
|
||||
interface FormSwitchProps {
|
||||
value: boolean
|
||||
onChange: (checked: boolean) => void
|
||||
subTitle: string
|
||||
wechatId?: string
|
||||
}
|
||||
|
||||
const FormSwitch: React.FC<FormSwitchProps> = ({ value, onChange, subTitle, wechatId }) => {
|
||||
const [editWechat, setEditWechat] = useState(false)
|
||||
const [wechatIdValue, setWechatIdValue] = useState('')
|
||||
const [wechat, setWechat] = useState(wechatId)
|
||||
const editWechatId = () => {
|
||||
setEditWechat(true)
|
||||
}
|
||||
const setWechatId = useCallback((e: any) => {
|
||||
const value = e.target.value
|
||||
onChange && onChange(value)
|
||||
setWechatIdValue(value)
|
||||
}, [onChange])
|
||||
|
||||
const fillWithPhone = () => {
|
||||
if (wechat) {
|
||||
setWechatIdValue(wechat)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<View className={styles['wechat-contact-section']}>
|
||||
<View className={styles['wechat-contact-item']}>
|
||||
<Checkbox
|
||||
className={styles['wechat-contact-checkbox']}
|
||||
checked={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<View className={styles['wechat-contact-content']}>
|
||||
<Text className={styles['wechat-contact-text']}>{subTitle}</Text>
|
||||
</View>
|
||||
</View>
|
||||
{
|
||||
!editWechat && wechatId && (
|
||||
<View className={styles['wechat-contact-id']}>
|
||||
<Text className={styles['wechat-contact-text']}>微信号: {wechatId.replace(/(\d{3})(\d{4})(\d{4})/, '$1 $2 $3')}</Text>
|
||||
<View className={styles['wechat-contact-edit']} onClick={editWechatId}>修改</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
{
|
||||
editWechat && (
|
||||
<View className={styles['wechat-contact-id']}>
|
||||
<View className={styles['wechat-contact-edit-input']}>
|
||||
<Input value={wechatIdValue} onInput={setWechatId} placeholder='请输入正确微信号' />
|
||||
</View>
|
||||
<View className={styles['wechat-contact-edit']} onClick={fillWithPhone}>手机号填入:{wechat}</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FormSwitch
|
||||
@@ -0,0 +1,100 @@
|
||||
@use '~@/scss/themeColor.scss' as theme;
|
||||
|
||||
.wechat-contact-section {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
.wechat-contact-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
.wechat-contact-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.wechat-contact-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-left: 4px;
|
||||
position: relative;
|
||||
.info-img{
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
.info-popover {
|
||||
position: absolute;
|
||||
bottom: 22px;
|
||||
left: -65px;
|
||||
width: 130px;
|
||||
padding:12px;
|
||||
background: rgba(57, 59, 68, 0.90);
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
z-index: 1001;
|
||||
white-space: normal;
|
||||
word-break: normal;
|
||||
overflow-wrap: break-word;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
.info-popover::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -6px;
|
||||
left: 68px; /* 对齐图标(宽12px),可按需微调 */
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 6px solid rgba(57, 59, 68, 0.90);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wechat-contact-checkbox {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
.wechat-contact-id {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding-top: 4px;
|
||||
.wechat-contact-text {
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
font-weight: normal;
|
||||
line-height: 24px;
|
||||
}
|
||||
.wechat-contact-edit {
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
display: flex;
|
||||
padding: 2px 6px;
|
||||
align-items: center;
|
||||
font-weight: normal;
|
||||
border-radius: 100px;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.wechat-contact-edit-input {
|
||||
max-width: 200px;
|
||||
font-size: 12px;
|
||||
.input-placeholder{
|
||||
color: theme.$textarea-placeholder-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './WechatSwitch'
|
||||
Reference in New Issue
Block a user