mergecode
This commit is contained in:
@@ -53,6 +53,7 @@
|
|||||||
"@tarojs/runtime": "4.1.5",
|
"@tarojs/runtime": "4.1.5",
|
||||||
"@tarojs/shared": "4.1.5",
|
"@tarojs/shared": "4.1.5",
|
||||||
"@tarojs/taro": "4.1.5",
|
"@tarojs/taro": "4.1.5",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"qqmap-wx-jssdk": "^1.0.0",
|
"qqmap-wx-jssdk": "^1.0.0",
|
||||||
"react": "^18.0.0",
|
"react": "^18.0.0",
|
||||||
"react-dom": "^18.0.0",
|
"react-dom": "^18.0.0",
|
||||||
|
|||||||
16
src/app.ts
16
src/app.ts
@@ -1,18 +1,32 @@
|
|||||||
import { Component, ReactNode } from 'react'
|
import { Component, ReactNode } from 'react'
|
||||||
import './app.scss'
|
import './app.scss'
|
||||||
import './nutui-theme.scss'
|
import './nutui-theme.scss'
|
||||||
|
import { useDictionaryStore } from './store/dictionaryStore'
|
||||||
|
|
||||||
interface AppProps {
|
interface AppProps {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
class App extends Component<AppProps> {
|
class App extends Component<AppProps> {
|
||||||
componentDidMount() {}
|
componentDidMount() {
|
||||||
|
// 初始化字典数据
|
||||||
|
this.initDictionaryData()
|
||||||
|
}
|
||||||
|
|
||||||
componentDidShow() {}
|
componentDidShow() {}
|
||||||
|
|
||||||
componentDidHide() {}
|
componentDidHide() {}
|
||||||
|
|
||||||
|
// 初始化字典数据
|
||||||
|
private async initDictionaryData() {
|
||||||
|
try {
|
||||||
|
const { fetchDictionary } = useDictionaryStore.getState()
|
||||||
|
await fetchDictionary()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('初始化字典数据失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
// this.props.children 是将要会渲染的页面
|
// this.props.children 是将要会渲染的页面
|
||||||
return this.props.children
|
return this.props.children
|
||||||
|
|||||||
115
src/components/DateTimePicker/DateTimePicker.tsx
Normal file
115
src/components/DateTimePicker/DateTimePicker.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { View, Text } from '@tarojs/components'
|
||||||
|
import { Picker, Popup } from '@nutui/nutui-react-taro'
|
||||||
|
import styles from './index.module.scss'
|
||||||
|
|
||||||
|
export interface DateTimePickerProps {
|
||||||
|
visible: boolean
|
||||||
|
onClose: () => void
|
||||||
|
onConfirm: (year: number, month: number) => void
|
||||||
|
defaultYear?: number
|
||||||
|
defaultMonth?: number
|
||||||
|
minYear?: number
|
||||||
|
maxYear?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const DateTimePicker: React.FC<DateTimePickerProps> = ({
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
onConfirm,
|
||||||
|
defaultYear = new Date().getFullYear(),
|
||||||
|
defaultMonth = new Date().getMonth() + 1,
|
||||||
|
minYear = 2020,
|
||||||
|
maxYear = 2030
|
||||||
|
}) => {
|
||||||
|
const [selectedYear, setSelectedYear] = useState(defaultYear)
|
||||||
|
const [selectedMonth, setSelectedMonth] = useState(defaultMonth)
|
||||||
|
|
||||||
|
// 生成年份选项
|
||||||
|
const yearOptions = Array.from({ length: maxYear - minYear + 1 }, (_, index) => ({
|
||||||
|
text: `${minYear + index}年`,
|
||||||
|
value: minYear + index
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 生成月份选项
|
||||||
|
const monthOptions = Array.from({ length: 12 }, (_, index) => ({
|
||||||
|
text: `${index + 1}月`,
|
||||||
|
value: index + 1
|
||||||
|
}))
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible) {
|
||||||
|
setSelectedYear(defaultYear)
|
||||||
|
setSelectedMonth(defaultMonth)
|
||||||
|
}
|
||||||
|
}, [visible, defaultYear, defaultMonth])
|
||||||
|
|
||||||
|
const handleYearChange = (value: any) => {
|
||||||
|
setSelectedYear(value[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMonthChange = (value: any) => {
|
||||||
|
setSelectedMonth(value[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
onConfirm(selectedYear, selectedMonth)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popup
|
||||||
|
visible={visible}
|
||||||
|
position="bottom"
|
||||||
|
round
|
||||||
|
onClose={onClose}
|
||||||
|
className={styles['date-time-picker-popup']}
|
||||||
|
>
|
||||||
|
{/* 拖拽手柄 */}
|
||||||
|
<View className={styles['popup-handle']} />
|
||||||
|
|
||||||
|
{/* 时间选择器 */}
|
||||||
|
<View className={styles['picker-container']}>
|
||||||
|
<View className={styles['picker-columns']}>
|
||||||
|
{/* 年份选择 */}
|
||||||
|
<View className={styles['picker-column']}>
|
||||||
|
<Text className={styles['picker-label']}>年</Text>
|
||||||
|
<Picker
|
||||||
|
value={[selectedYear]}
|
||||||
|
options={yearOptions}
|
||||||
|
onChange={handleYearChange}
|
||||||
|
className={styles['year-picker']}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 月份选择 */}
|
||||||
|
<View className={styles['picker-column']}>
|
||||||
|
<Text className={styles['picker-label']}>月</Text>
|
||||||
|
<Picker
|
||||||
|
value={[selectedMonth]}
|
||||||
|
options={monthOptions}
|
||||||
|
onChange={handleMonthChange}
|
||||||
|
className={styles['month-picker']}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 操作按钮 */}
|
||||||
|
<View className={styles['action-buttons']}>
|
||||||
|
<View className={styles['cancel-btn']} onClick={handleCancel}>
|
||||||
|
<Text className={styles['cancel-text']}>取消</Text>
|
||||||
|
</View>
|
||||||
|
<View className={styles['confirm-btn']} onClick={handleConfirm}>
|
||||||
|
<Text className={styles['confirm-text']}>完成</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Popup>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DateTimePicker
|
||||||
67
src/components/DateTimePicker/README.md
Normal file
67
src/components/DateTimePicker/README.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# DateTimePicker 年月选择器
|
||||||
|
|
||||||
|
一个基于 NutUI 的年月切换弹窗组件,支持自定义年份范围和默认值。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- 🎯 年月分别选择,操作简单直观
|
||||||
|
- 🎨 遵循设计稿样式,美观易用
|
||||||
|
- 📱 支持移动端手势操作
|
||||||
|
- ⚙️ 可自定义年份范围
|
||||||
|
- <20><> 基于 NutUI 组件库,稳定可靠
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { DateTimePicker } from '@/components'
|
||||||
|
|
||||||
|
const MyComponent = () => {
|
||||||
|
const [visible, setVisible] = useState(false)
|
||||||
|
|
||||||
|
const handleConfirm = (year: number, month: number) => {
|
||||||
|
console.log('选择的年月:', year, month)
|
||||||
|
setVisible(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DateTimePicker
|
||||||
|
visible={visible}
|
||||||
|
onClose={() => setVisible(false)}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
defaultYear={2025}
|
||||||
|
defaultMonth={11}
|
||||||
|
minYear={2020}
|
||||||
|
maxYear={2030}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 参数
|
||||||
|
|
||||||
|
| 参数 | 类型 | 默认值 | 说明 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| visible | boolean | - | 是否显示弹窗 |
|
||||||
|
| onClose | () => void | - | 关闭弹窗的回调 |
|
||||||
|
| onConfirm | (year: number, month: number) => void | - | 确认选择的回调 |
|
||||||
|
| defaultYear | number | 当前年份 | 默认选中的年份 |
|
||||||
|
| defaultMonth | number | 当前月份 | 默认选中的月份 |
|
||||||
|
| minYear | number | 2020 | 可选择的最小年份 |
|
||||||
|
| maxYear | number | 2030 | 可选择的最大年份 |
|
||||||
|
|
||||||
|
## 样式定制
|
||||||
|
|
||||||
|
组件使用 CSS Modules,可以通过修改 `index.module.scss` 文件来自定义样式。
|
||||||
|
|
||||||
|
主要样式类:
|
||||||
|
- `.date-time-picker-popup` - 弹窗容器
|
||||||
|
- `.picker-columns` - 选择器列容器
|
||||||
|
- `.picker-column` - 单列选择器
|
||||||
|
- `.action-buttons` - 操作按钮区域
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 组件基于 NutUI 的 Picker 和 Popup 组件
|
||||||
|
2. 年份范围建议不要设置过大,以免影响性能
|
||||||
|
3. 月份固定为 1-12 月
|
||||||
|
4. 组件会自动处理边界情况
|
||||||
45
src/components/DateTimePicker/example.tsx
Normal file
45
src/components/DateTimePicker/example.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import { View, Button } from '@tarojs/components'
|
||||||
|
import DateTimePicker from './DateTimePicker'
|
||||||
|
|
||||||
|
const DateTimePickerExample: React.FC = () => {
|
||||||
|
const [visible, setVisible] = useState(false)
|
||||||
|
const [selectedDate, setSelectedDate] = useState('')
|
||||||
|
|
||||||
|
const handleOpen = () => {
|
||||||
|
setVisible(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setVisible(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirm = (year: number, month: number) => {
|
||||||
|
setSelectedDate(`${year}年${month}月`)
|
||||||
|
console.log('选择的日期:', year, month)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{ padding: '20px' }}>
|
||||||
|
<Button onClick={handleOpen}>选择年月</Button>
|
||||||
|
|
||||||
|
{selectedDate && (
|
||||||
|
<View style={{ marginTop: '20px', fontSize: '16px' }}>
|
||||||
|
已选择: {selectedDate}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DateTimePicker
|
||||||
|
visible={visible}
|
||||||
|
onClose={handleClose}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
defaultYear={2025}
|
||||||
|
defaultMonth={11}
|
||||||
|
minYear={2020}
|
||||||
|
maxYear={2030}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DateTimePickerExample
|
||||||
102
src/components/DateTimePicker/index.module.scss
Normal file
102
src/components/DateTimePicker/index.module.scss
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
.date-time-picker-popup {
|
||||||
|
:global(.nut-popup) {
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-handle {
|
||||||
|
width: 40px;
|
||||||
|
height: 4px;
|
||||||
|
background: #e5e5e5;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin: 12px auto 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-container {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-columns {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #999;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.year-picker,
|
||||||
|
.month-picker {
|
||||||
|
:global(.nut-picker) {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.nut-picker__content) {
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.nut-picker-item) {
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.nut-picker-item--selected) {
|
||||||
|
color: #000;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.nut-picker-item--disabled) {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
padding: 0 20px 20px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn,
|
||||||
|
.confirm-btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-text {
|
||||||
|
color: #666;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-btn {
|
||||||
|
background: #000;
|
||||||
|
border: 1px solid #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-text {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
2
src/components/DateTimePicker/index.ts
Normal file
2
src/components/DateTimePicker/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import DateTimePicker from './DateTimePicker'
|
||||||
|
export default DateTimePicker
|
||||||
@@ -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
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export { default } from './FormBasicInfo'
|
|
||||||
export type { FormBasicInfoProps } from './FormBasicInfo'
|
|
||||||
@@ -4,18 +4,18 @@ import './NumberInterval.scss'
|
|||||||
import { InputNumber } from '@nutui/nutui-react-taro'
|
import { InputNumber } from '@nutui/nutui-react-taro'
|
||||||
|
|
||||||
interface NumberIntervalProps {
|
interface NumberIntervalProps {
|
||||||
minParticipants: number
|
value: [number, number]
|
||||||
maxParticipants: number
|
onChange: (value: [number, number]) => void
|
||||||
onMinParticipantsChange: (value: number) => void
|
|
||||||
onMaxParticipantsChange: (value: number) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const NumberInterval: React.FC<NumberIntervalProps> = ({
|
const NumberInterval: React.FC<NumberIntervalProps> = ({
|
||||||
minParticipants,
|
value,
|
||||||
maxParticipants,
|
onChange
|
||||||
onMinParticipantsChange,
|
|
||||||
onMaxParticipantsChange
|
|
||||||
}) => {
|
}) => {
|
||||||
|
const [minParticipants, maxParticipants] = value || [1, 4]
|
||||||
|
const handleChange = (value: [number | string, number | string]) => {
|
||||||
|
onChange([Number(value[0]), Number(value[1])])
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<View className='participants-control-section'>
|
<View className='participants-control-section'>
|
||||||
<View className='participant-control'>
|
<View className='participant-control'>
|
||||||
@@ -23,9 +23,10 @@ const NumberInterval: React.FC<NumberIntervalProps> = ({
|
|||||||
<View className='control-buttons'>
|
<View className='control-buttons'>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
className="format-width"
|
className="format-width"
|
||||||
defaultValue={4}
|
defaultValue={minParticipants}
|
||||||
min={0}
|
min={minParticipants}
|
||||||
max={4}
|
max={maxParticipants}
|
||||||
|
onChange={(value) => handleChange([value, maxParticipants])}
|
||||||
formatter={(value) => `${value}人`}
|
formatter={(value) => `${value}人`}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@@ -35,9 +36,10 @@ const NumberInterval: React.FC<NumberIntervalProps> = ({
|
|||||||
<View className='control-buttons'>
|
<View className='control-buttons'>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
className="format-width"
|
className="format-width"
|
||||||
defaultValue={4}
|
defaultValue={maxParticipants}
|
||||||
min={0}
|
onChange={(value) => handleChange([value, maxParticipants])}
|
||||||
max={4}
|
min={minParticipants}
|
||||||
|
max={maxParticipants}
|
||||||
formatter={(value) => `${value}人`}
|
formatter={(value) => `${value}人`}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { Checkbox } from '@nutui/nutui-react-taro'
|
|||||||
import './TextareaTag.scss'
|
import './TextareaTag.scss'
|
||||||
|
|
||||||
interface TextareaTagProps {
|
interface TextareaTagProps {
|
||||||
value: string
|
value: { description: string, description_tag: string[] }
|
||||||
onChange: (value: string) => void
|
onChange: (value: { description: string, description_tag: string[] }) => void
|
||||||
title?: string
|
title?: string
|
||||||
showTitle?: boolean
|
showTitle?: boolean
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
@@ -22,27 +22,17 @@ const TextareaTag: React.FC<TextareaTagProps> = ({
|
|||||||
maxLength = 500,
|
maxLength = 500,
|
||||||
options = []
|
options = []
|
||||||
}) => {
|
}) => {
|
||||||
// 处理输入框变化
|
// 处理文本输入变化
|
||||||
const [tags, setTags] = useState<string[]>([])
|
const handleTextChange = useCallback((e: any) => {
|
||||||
const handleInputChange = useCallback((e: any) => {
|
onChange({...value, description: e.detail.value})
|
||||||
onChange(e.detail.value)
|
|
||||||
}, [onChange])
|
}, [onChange])
|
||||||
|
|
||||||
// 选择预设选项
|
// 处理标签选择变化
|
||||||
const handleSelectOption = useCallback((option: string) => {
|
const handleTagChange = useCallback((selectedTags: string[]) => {
|
||||||
let newValue = ''
|
onChange({...value, description_tag: selectedTags})
|
||||||
|
}, [onChange])
|
||||||
if (value) {
|
|
||||||
// 如果已有内容,用分号分隔添加
|
|
||||||
newValue = value + ';' + option
|
|
||||||
} else {
|
|
||||||
// 如果没有内容,直接添加
|
|
||||||
newValue = option
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(newValue)
|
|
||||||
}, [value, onChange])
|
|
||||||
|
|
||||||
|
console.log(options, 'options')
|
||||||
return (
|
return (
|
||||||
<View className='textarea-tag'>
|
<View className='textarea-tag'>
|
||||||
{/* 选择选项 */}
|
{/* 选择选项 */}
|
||||||
@@ -52,8 +42,8 @@ const TextareaTag: React.FC<TextareaTagProps> = ({
|
|||||||
<Checkbox.Group
|
<Checkbox.Group
|
||||||
labelPosition="left"
|
labelPosition="left"
|
||||||
direction="horizontal"
|
direction="horizontal"
|
||||||
value={tags}
|
value={value.description_tag}
|
||||||
onChange={(value) => setTags(value)}
|
onChange={handleTagChange}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
options?.map((option, index) => (
|
options?.map((option, index) => (
|
||||||
@@ -74,9 +64,9 @@ const TextareaTag: React.FC<TextareaTagProps> = ({
|
|||||||
<Textarea
|
<Textarea
|
||||||
className='additional-input'
|
className='additional-input'
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={value}
|
value={value.description}
|
||||||
placeholderClass='textarea-placeholder'
|
placeholderClass='textarea-placeholder'
|
||||||
onInput={handleInputChange}
|
onInput={handleTextChange}
|
||||||
maxlength={maxLength}
|
maxlength={maxLength}
|
||||||
autoHeight={false}
|
autoHeight={false}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import React from 'react'
|
import React, { useState } from 'react'
|
||||||
import { View, Text, Picker } from '@tarojs/components'
|
import { View, Text, } from '@tarojs/components'
|
||||||
|
import { getDate, getTime } from '@/utils/timeUtils'
|
||||||
|
import DateTimePicker from '@/components/DateTimePicker'
|
||||||
import './TimeSelector.scss'
|
import './TimeSelector.scss'
|
||||||
|
|
||||||
export interface TimeRange {
|
export interface TimeRange {
|
||||||
startDate: string
|
start_time: string
|
||||||
startTime: string
|
end_time: string
|
||||||
endTime: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TimeSelectorProps {
|
interface TimeSelectorProps {
|
||||||
@@ -15,41 +16,16 @@ interface TimeSelectorProps {
|
|||||||
|
|
||||||
const TimeSelector: React.FC<TimeSelectorProps> = ({
|
const TimeSelector: React.FC<TimeSelectorProps> = ({
|
||||||
value = {
|
value = {
|
||||||
startDate: '',
|
start_time: '',
|
||||||
startTime: '',
|
end_time: ''
|
||||||
endTime: ''
|
|
||||||
},
|
},
|
||||||
onChange
|
onChange
|
||||||
}) => {
|
}) => {
|
||||||
// 格式化日期显示
|
// 格式化日期显示
|
||||||
const formatDate = (dateStr: string) => {
|
const [visible, setVisible] = useState(false)
|
||||||
return dateStr.replace(/-/g, '年').replace(/-/g, '月') + '日'
|
const handleConfirm = (year: number, month: number) => {
|
||||||
|
console.log('选择的日期:', year, month)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理开始日期变化
|
|
||||||
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 (
|
return (
|
||||||
<View className='time-selector'>
|
<View className='time-selector'>
|
||||||
<View className='time-section'>
|
<View className='time-section'>
|
||||||
@@ -58,11 +34,11 @@ const TimeSelector: React.FC<TimeSelectorProps> = ({
|
|||||||
<View className='time-label'>
|
<View className='time-label'>
|
||||||
<View className='dot'></View>
|
<View className='dot'></View>
|
||||||
</View>
|
</View>
|
||||||
<View className='time-content'>
|
<View className='time-content' onClick={() => setVisible(true)}>
|
||||||
<Text className='time-label'>开始时间</Text>
|
<Text className='time-label'>开始时间</Text>
|
||||||
<view className='time-text-wrapper'>
|
<view className='time-text-wrapper'>
|
||||||
<Text className='time-text'>2025年11月23日</Text>
|
<Text className='time-text'>{getDate(value.start_time)}</Text>
|
||||||
<Text className='time-text time-am'>8:00 AM</Text>
|
<Text className='time-text time-am'>{getTime(value.start_time)}</Text>
|
||||||
</view>
|
</view>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -75,12 +51,20 @@ const TimeSelector: React.FC<TimeSelectorProps> = ({
|
|||||||
<View className='time-content'>
|
<View className='time-content'>
|
||||||
<Text className='time-label'>结束时间</Text>
|
<Text className='time-label'>结束时间</Text>
|
||||||
<view className='time-text-wrapper'>
|
<view className='time-text-wrapper'>
|
||||||
<Text className='time-text'>2025年11月23日</Text>
|
<Text className='time-text time-am'>{getTime(value.end_time)}</Text>
|
||||||
<Text className='time-text time-am'>8:00 AM</Text>
|
|
||||||
</view>
|
</view>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
<DateTimePicker
|
||||||
|
visible={visible}
|
||||||
|
onClose={() => setVisible(false)}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
defaultYear={2025}
|
||||||
|
defaultMonth={11}
|
||||||
|
minYear={2020}
|
||||||
|
maxYear={2030}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from './TitleInput'
|
|
||||||
@@ -3,14 +3,14 @@ import { View } from '@tarojs/components'
|
|||||||
import { TextArea } from '@nutui/nutui-react-taro'
|
import { TextArea } from '@nutui/nutui-react-taro'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
interface TitleInputProps {
|
interface TitleTextareaProps {
|
||||||
value: string
|
value: string
|
||||||
onChange: (value: string) => void
|
onChange: (value: string) => void
|
||||||
maxLength?: number
|
maxLength?: number
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const TitleInput: React.FC<TitleInputProps> = ({
|
const TitleTextarea: React.FC<TitleTextareaProps> = ({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
maxLength = 20,
|
maxLength = 20,
|
||||||
@@ -32,4 +32,4 @@ const TitleInput: React.FC<TitleInputProps> = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TitleInput
|
export default TitleTextarea
|
||||||
1
src/components/TitleTextarea/index.ts
Normal file
1
src/components/TitleTextarea/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './TitleTextarea'
|
||||||
@@ -2,26 +2,24 @@ import ActivityTypeSwitch from './ActivityTypeSwitch'
|
|||||||
import TextareaTag from './TextareaTag'
|
import TextareaTag from './TextareaTag'
|
||||||
import FormSwitch from './FormSwitch'
|
import FormSwitch from './FormSwitch'
|
||||||
import ImageUpload from './ImageUpload'
|
import ImageUpload from './ImageUpload'
|
||||||
import FormBasicInfo from './FormBasicInfo'
|
|
||||||
import Range from './Range'
|
import Range from './Range'
|
||||||
import NumberInterval from './NumberInterval'
|
import NumberInterval from './NumberInterval'
|
||||||
import { SelectStadium, StadiumDetail } from './SelectStadium'
|
|
||||||
import TimeSelector from './TimeSelector'
|
import TimeSelector from './TimeSelector'
|
||||||
import TitleInput from './TitleInput'
|
import TitleTextarea from './TitleTextarea'
|
||||||
import CommonPopup from './CommonPopup'
|
import CommonPopup from './CommonPopup'
|
||||||
|
import DateTimePicker from './DateTimePicker/DateTimePicker'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ActivityTypeSwitch,
|
ActivityTypeSwitch,
|
||||||
TextareaTag,
|
TextareaTag,
|
||||||
FormSwitch,
|
FormSwitch,
|
||||||
ImageUpload,
|
ImageUpload,
|
||||||
FormBasicInfo,
|
|
||||||
Range,
|
Range,
|
||||||
NumberInterval,
|
NumberInterval,
|
||||||
SelectStadium,
|
|
||||||
TimeSelector,
|
TimeSelector,
|
||||||
TitleInput,
|
TitleTextarea,
|
||||||
StadiumDetail,
|
CommonPopup,
|
||||||
CommonPopup
|
DateTimePicker
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { type TimeRange } from './TimeSelector'
|
import { type TimeRange } from './TimeSelector'
|
||||||
import { type Stadium } from './SelectStadium'
|
|
||||||
import { type ActivityType } from './ActivityTypeSwitch'
|
import { type ActivityType } from './ActivityTypeSwitch'
|
||||||
import { type CoverImage } from './ImageUpload'
|
import { type CoverImage } from './ImageUpload'
|
||||||
export type { TimeRange, Stadium, ActivityType, CoverImage }
|
export type { TimeRange, ActivityType, CoverImage }
|
||||||
@@ -15,12 +15,13 @@ export enum FieldType {
|
|||||||
NUMBERINTERVAL = 'numberinterval',
|
NUMBERINTERVAL = 'numberinterval',
|
||||||
RANGE = 'range',
|
RANGE = 'range',
|
||||||
TEXTAREATAG = 'textareaTag',
|
TEXTAREATAG = 'textareaTag',
|
||||||
ACTIVITYINFO = 'activityInfo'
|
ACTIVITYINFO = 'activityInfo',
|
||||||
|
WECHATCONTACT = 'wechatContact'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 表单字段配置接口
|
// 表单字段配置接口
|
||||||
export interface FormFieldConfig {
|
export interface FormFieldConfig {
|
||||||
key: string
|
prop: string
|
||||||
label: string
|
label: string
|
||||||
type: FieldType
|
type: FieldType
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
@@ -43,7 +44,7 @@ export interface FormFieldConfig {
|
|||||||
// 发布球局表单配置
|
// 发布球局表单配置
|
||||||
export const publishBallFormSchema: FormFieldConfig[] = [
|
export const publishBallFormSchema: FormFieldConfig[] = [
|
||||||
{
|
{
|
||||||
key: 'coverImages',
|
prop: 'image_list',
|
||||||
label: '活动封页',
|
label: '活动封页',
|
||||||
type: FieldType.UPLOADIMAGE,
|
type: FieldType.UPLOADIMAGE,
|
||||||
placeholder: '请选择活动类型',
|
placeholder: '请选择活动类型',
|
||||||
@@ -53,41 +54,31 @@ export const publishBallFormSchema: FormFieldConfig[] = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'title',
|
prop: 'title',
|
||||||
label: '',
|
label: '',
|
||||||
type: FieldType.TEXT,
|
type: FieldType.TEXT,
|
||||||
placeholder: '好的标题更吸引人哦',
|
placeholder: '好的标题更吸引人哦',
|
||||||
required: true,
|
required: true,
|
||||||
props: {
|
props: {
|
||||||
maxLength: 80
|
maxLength: 20
|
||||||
},
|
}
|
||||||
rules: [
|
|
||||||
{ required: true, message: '请输入活动标题' },
|
|
||||||
{ max: 20, message: '标题不能超过20个字符' }
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'date',
|
prop: 'timeRange',
|
||||||
label: '',
|
label: '',
|
||||||
type: FieldType.TIMEINTERVAL,
|
type: FieldType.TIMEINTERVAL,
|
||||||
placeholder: '请选择活动日期',
|
placeholder: '请选择活动日期',
|
||||||
required: true,
|
required: true
|
||||||
rules: [
|
|
||||||
{ required: true, message: '请选择活动日期' }
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'timeRange',
|
prop: 'activityInfo',
|
||||||
label: '活动信息',
|
label: '活动信息',
|
||||||
type: FieldType.ACTIVITYINFO,
|
type: FieldType.ACTIVITYINFO,
|
||||||
placeholder: '请选择活动时间',
|
placeholder: '请选择活动时间',
|
||||||
required: true,
|
required: true,
|
||||||
rules: [
|
|
||||||
{ required: true, message: '请选择活动时间' }
|
|
||||||
],
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
key: 'fee',
|
prop: 'price',
|
||||||
label: '费用',
|
label: '费用',
|
||||||
iconType: 'ICON_COST',
|
iconType: 'ICON_COST',
|
||||||
type: FieldType.NUMBER,
|
type: FieldType.NUMBER,
|
||||||
@@ -99,7 +90,7 @@ export const publishBallFormSchema: FormFieldConfig[] = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'location',
|
prop: 'location_name',
|
||||||
label: '地点',
|
label: '地点',
|
||||||
iconType: 'ICON_LOCATION',
|
iconType: 'ICON_LOCATION',
|
||||||
type: FieldType.LOCATION,
|
type: FieldType.LOCATION,
|
||||||
@@ -107,25 +98,23 @@ export const publishBallFormSchema: FormFieldConfig[] = [
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'sport',
|
prop: 'play_type',
|
||||||
label: '玩法',
|
label: '玩法',
|
||||||
iconType: 'ICON_GAMEPLAY',
|
iconType: 'ICON_GAMEPLAY',
|
||||||
type: FieldType.SELECT,
|
type: FieldType.SELECT,
|
||||||
placeholder: '请选择玩法',
|
placeholder: '请选择玩法',
|
||||||
required: true,
|
required: true,
|
||||||
options: [
|
options: [
|
||||||
{ label: '篮球', value: 'basketball' },
|
{ label: '不限', value: '不限' },
|
||||||
{ label: '足球', value: 'football' },
|
{ label: '单打', value: '单打' },
|
||||||
{ label: '羽毛球', value: 'badminton' },
|
{ label: '双打', value: '双打' },
|
||||||
{ label: '网球', value: 'tennis' },
|
{ label: '拉球', value: '拉球' }
|
||||||
{ label: '乒乓球', value: 'pingpong' },
|
|
||||||
{ label: '排球', value: 'volleyball' }
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'minParticipants',
|
prop: 'players',
|
||||||
label: '人数要求',
|
label: '人数要求',
|
||||||
type: FieldType.NUMBERINTERVAL,
|
type: FieldType.NUMBERINTERVAL,
|
||||||
placeholder: '请输入最少参与人数',
|
placeholder: '请输入最少参与人数',
|
||||||
@@ -133,15 +122,11 @@ export const publishBallFormSchema: FormFieldConfig[] = [
|
|||||||
props: {
|
props: {
|
||||||
showSummary: true,
|
showSummary: true,
|
||||||
summary: '最少1人,最多4人',
|
summary: '最少1人,最多4人',
|
||||||
},
|
}
|
||||||
rules: [
|
|
||||||
{ min: 1, message: '最少参与人数不能为0' },
|
|
||||||
{ max: 4, message: '最少参与人数不能超过100人' }
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
key: 'ntpLevel',
|
prop: 'skill_level',
|
||||||
label: 'NTRP 水平要求',
|
label: 'NTRP 水平要求',
|
||||||
type: FieldType.RANGE,
|
type: FieldType.RANGE,
|
||||||
placeholder: '请选择开始时间',
|
placeholder: '请选择开始时间',
|
||||||
@@ -156,22 +141,19 @@ export const publishBallFormSchema: FormFieldConfig[] = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'additionalRequirements',
|
prop: 'descriptionInfo',
|
||||||
label: '补充要求(选填)',
|
label: '补充要求(选填)',
|
||||||
type: FieldType.TEXTAREATAG,
|
type: FieldType.TEXTAREATAG,
|
||||||
placeholder: '补充性别偏好、特殊要求和注意事项等信息',
|
placeholder: '补充性别偏好、特殊要求和注意事项等信息',
|
||||||
required: true,
|
required: true,
|
||||||
options:[
|
options:[
|
||||||
{ label: '仅限男生', value: 'beginner' },
|
{ label: '仅限男生', value: '仅限男生' },
|
||||||
{ label: '仅限女生', value: 'intermediate' },
|
{ label: '仅限女生', value: '仅限女生' },
|
||||||
{ label: '性别不限', value: 'advanced' }
|
{ label: '性别不限', value: '性别不限' }
|
||||||
],
|
|
||||||
rules: [
|
|
||||||
{ max: 100, message: '补充要求不能超过100个字符' }
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'autoDegrade',
|
prop: 'is_substitute_supported',
|
||||||
label: '',
|
label: '',
|
||||||
type: FieldType.CHECKBOX,
|
type: FieldType.CHECKBOX,
|
||||||
placeholder: '开启自动候补逻辑',
|
placeholder: '开启自动候补逻辑',
|
||||||
@@ -180,9 +162,15 @@ export const publishBallFormSchema: FormFieldConfig[] = [
|
|||||||
subTitle: '开启自动候补逻辑',
|
subTitle: '开启自动候补逻辑',
|
||||||
showToast: true,
|
showToast: true,
|
||||||
description: '开启后,当活动人数不足时,系统会自动将活动状态改为“候补”,并通知用户。',
|
description: '开启后,当活动人数不足时,系统会自动将活动状态改为“候补”,并通知用户。',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
rules: [
|
{
|
||||||
{ required: true, message: '请选择开启自动候补逻辑' }
|
prop: 'is_wechat_contact',
|
||||||
]
|
label: '',
|
||||||
|
type: FieldType.WECHATCONTACT,
|
||||||
|
required: true,
|
||||||
|
props:{
|
||||||
|
subTitle: '允许球友微信联系我',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -75,6 +75,9 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
&.selected{
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.arrow{
|
.arrow{
|
||||||
width: 16px;
|
width: 16px;
|
||||||
148
src/pages/publishBall/components/FormBasicInfo/FormBasicInfo.tsx
Normal file
148
src/pages/publishBall/components/FormBasicInfo/FormBasicInfo.tsx
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import React, { useState, useCallback, useEffect } from 'react'
|
||||||
|
import { View, Text, Input, Image, Picker } 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, value: any) => {
|
||||||
|
onChange({...value, [key]: value})
|
||||||
|
}, [onChange])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (children.length > 2) {
|
||||||
|
const options = children[2]?.options || [];
|
||||||
|
setPlayGame(options)
|
||||||
|
}
|
||||||
|
}, [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={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
|
||||||
1
src/pages/publishBall/components/FormBasicInfo/index.ts
Normal file
1
src/pages/publishBall/components/FormBasicInfo/index.ts
Normal file
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
1
src/pages/publishBall/components/PopupGameplay/index.ts
Normal file
1
src/pages/publishBall/components/PopupGameplay/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './PopupGameplay'
|
||||||
@@ -134,7 +134,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.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 {
|
.stadium-list {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -172,6 +189,11 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
|
.highlight-text {
|
||||||
|
color: #007AFF;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.stadium-address{
|
.stadium-address{
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState, useRef, useEffect } from 'react'
|
||||||
import { View, Text, Input, ScrollView, Image } from '@tarojs/components'
|
import { View, Text, Input, ScrollView, Image } from '@tarojs/components'
|
||||||
import { Popup } from '@nutui/nutui-react-taro'
|
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import StadiumDetail from './StadiumDetail'
|
import { Loading } from '@nutui/nutui-react-taro'
|
||||||
import CommonPopup from '../CommonPopup'
|
import StadiumDetail, { StadiumDetailRef } from './StadiumDetail'
|
||||||
import './SelectStadium.scss'
|
import { CommonPopup } from '../../../../components'
|
||||||
|
import { getLocation } from '@/utils/locationUtils'
|
||||||
|
import PublishService from '@/services/publishService'
|
||||||
import images from '@/config/images'
|
import images from '@/config/images'
|
||||||
|
import './SelectStadium.scss'
|
||||||
|
|
||||||
export interface Stadium {
|
export interface Stadium {
|
||||||
id?: string
|
id?: string
|
||||||
@@ -22,17 +24,6 @@ interface SelectStadiumProps {
|
|||||||
onConfirm: (stadium: Stadium | null) => 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> = ({
|
const SelectStadium: React.FC<SelectStadiumProps> = ({
|
||||||
visible,
|
visible,
|
||||||
@@ -42,6 +33,37 @@ const SelectStadium: React.FC<SelectStadiumProps> = ({
|
|||||||
const [searchValue, setSearchValue] = useState('')
|
const [searchValue, setSearchValue] = useState('')
|
||||||
const [selectedStadium, setSelectedStadium] = useState<Stadium | null>(null)
|
const [selectedStadium, setSelectedStadium] = useState<Stadium | null>(null)
|
||||||
const [showDetail, setShowDetail] = useState(false)
|
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
|
if (!visible) return null
|
||||||
|
|
||||||
@@ -56,11 +78,6 @@ const SelectStadium: React.FC<SelectStadiumProps> = ({
|
|||||||
setShowDetail(true)
|
setShowDetail(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理返回球馆列表
|
|
||||||
const handleBackToList = () => {
|
|
||||||
setShowDetail(false)
|
|
||||||
setSelectedStadium(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理搜索框输入
|
// 处理搜索框输入
|
||||||
const handleSearchInput = (e: any) => {
|
const handleSearchInput = (e: any) => {
|
||||||
@@ -71,7 +88,6 @@ const SelectStadium: React.FC<SelectStadiumProps> = ({
|
|||||||
const handleMapLocation = () => {
|
const handleMapLocation = () => {
|
||||||
Taro.chooseLocation({
|
Taro.chooseLocation({
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
console.log('选择位置成功:', res)
|
|
||||||
setSelectedStadium({
|
setSelectedStadium({
|
||||||
name: res.name,
|
name: res.name,
|
||||||
address: res.address,
|
address: res.address,
|
||||||
@@ -82,32 +98,25 @@ const SelectStadium: React.FC<SelectStadiumProps> = ({
|
|||||||
},
|
},
|
||||||
fail: (err) => {
|
fail: (err) => {
|
||||||
console.error('选择位置失败:', err)
|
console.error('选择位置失败:', err)
|
||||||
Taro.showToast({
|
|
||||||
title: '位置选择失败',
|
|
||||||
icon: 'error'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理确认
|
// 处理确认
|
||||||
const handleConfirm = (stadium: Stadium, venueType: string, groundMaterial: string, additionalInfo: string) => {
|
const handleConfirm = () => {
|
||||||
// 这里可以处理球馆详情的信息
|
if (stadiumDetailRef.current) {
|
||||||
console.log('球馆详情:', { stadium, venueType, groundMaterial, additionalInfo })
|
const formData = stadiumDetailRef.current.getFormData()
|
||||||
onConfirm(stadium)
|
console.log('获取球馆表单数据:', formData)
|
||||||
setShowDetail(false)
|
const { description, ...rest } = formData
|
||||||
|
onConfirm({ ...rest, ...description })
|
||||||
setSelectedStadium(null)
|
setSelectedStadium(null)
|
||||||
setSearchValue('')
|
setSearchValue('')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 处理球馆列表确认
|
|
||||||
const handleListConfirm = () => {
|
|
||||||
if (selectedStadium) {
|
|
||||||
onConfirm(selectedStadium)
|
|
||||||
setSelectedStadium(null)
|
|
||||||
setSearchValue('')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理取消
|
// 处理取消
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
@@ -118,7 +127,6 @@ const SelectStadium: React.FC<SelectStadiumProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleItemLocation = (stadium: Stadium) => {
|
const handleItemLocation = (stadium: Stadium) => {
|
||||||
console.log(stadium,'stadiumstadium');
|
|
||||||
if (stadium.latitude && stadium.longitude) {
|
if (stadium.latitude && stadium.longitude) {
|
||||||
Taro.openLocation({
|
Taro.openLocation({
|
||||||
latitude: stadium.latitude,
|
latitude: stadium.latitude,
|
||||||
@@ -126,17 +134,23 @@ const SelectStadium: React.FC<SelectStadiumProps> = ({
|
|||||||
name: stadium.name,
|
name: stadium.name,
|
||||||
address: stadium.address,
|
address: stadium.address,
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
console.log(res,'resres');
|
console.log(res, 'resres')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const markSearchText = (text: string) => {
|
const markSearchText = (text: string) => {
|
||||||
return text.replace(searchValue, `<span style="color: #007AFF;">${searchValue}</span>`)
|
if (!searchValue) return text
|
||||||
|
return text.replace(
|
||||||
|
new RegExp(searchValue, 'gi'),
|
||||||
|
`<span class="highlight-text">${searchValue}</span>`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 如果显示详情页面
|
// 如果显示详情页面
|
||||||
if (showDetail && selectedStadium) {
|
if (showDetail && selectedStadium) {
|
||||||
return (
|
return (
|
||||||
@@ -147,14 +161,13 @@ const SelectStadium: React.FC<SelectStadiumProps> = ({
|
|||||||
confirmText="确认"
|
confirmText="确认"
|
||||||
className="select-stadium-popup"
|
className="select-stadium-popup"
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
onConfirm={handleListConfirm}
|
onConfirm={handleConfirm}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
round
|
round
|
||||||
>
|
>
|
||||||
<StadiumDetail
|
<StadiumDetail
|
||||||
|
ref={stadiumDetailRef}
|
||||||
stadium={selectedStadium}
|
stadium={selectedStadium}
|
||||||
onBack={handleBackToList}
|
|
||||||
onConfirm={handleConfirm}
|
|
||||||
/>
|
/>
|
||||||
</CommonPopup>
|
</CommonPopup>
|
||||||
)
|
)
|
||||||
@@ -169,8 +182,6 @@ const SelectStadium: React.FC<SelectStadiumProps> = ({
|
|||||||
cancelText="返回"
|
cancelText="返回"
|
||||||
confirmText="完成"
|
confirmText="完成"
|
||||||
className="select-stadium-popup"
|
className="select-stadium-popup"
|
||||||
onCancel={handleCancel}
|
|
||||||
onConfirm={handleListConfirm}
|
|
||||||
position="bottom"
|
position="bottom"
|
||||||
round
|
round
|
||||||
>
|
>
|
||||||
@@ -216,8 +227,12 @@ const SelectStadium: React.FC<SelectStadiumProps> = ({
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
{
|
||||||
{/* 场馆列表 */}
|
loading ? (
|
||||||
|
<View className='stadium-item-loading'>
|
||||||
|
<Loading type="circular" className='loading-icon'>加载中</Loading>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
<ScrollView className='stadium-list' scrollY>
|
<ScrollView className='stadium-list' scrollY>
|
||||||
{filteredStadiums.map((stadium) => (
|
{filteredStadiums.map((stadium) => (
|
||||||
<View
|
<View
|
||||||
@@ -231,32 +246,49 @@ const SelectStadium: React.FC<SelectStadiumProps> = ({
|
|||||||
<View className='stadium-item-right'>
|
<View className='stadium-item-right'>
|
||||||
<View className='stadium-name' dangerouslySetInnerHTML={{ __html: markSearchText(stadium.name) }}></View>
|
<View className='stadium-name' dangerouslySetInnerHTML={{ __html: markSearchText(stadium.name) }}></View>
|
||||||
<View className='stadium-address'>
|
<View className='stadium-address'>
|
||||||
<Text onClick={(e) => { e.stopPropagation(); handleItemLocation(stadium); }}>{stadium.istance} · </Text>
|
<Text
|
||||||
<Text onClick={(e) => { e.stopPropagation(); handleItemLocation(stadium); }}>{stadium.address}</Text>
|
className='stadium-distance'
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleItemLocation(stadium)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{stadium.istance} ·
|
||||||
|
</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' />
|
<Image src={images.ICON_ARRORW_SMALL} className='stadium-map-icon' />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
{
|
{searchValue && (
|
||||||
searchValue && (<View
|
<View className='stadium-item map-search-item' onClick={handleMapLocation}>
|
||||||
className={`stadium-item`}
|
|
||||||
onClick={() => handleMapLocation()}
|
|
||||||
>
|
|
||||||
<View className='stadium-item-left'>
|
<View className='stadium-item-left'>
|
||||||
<Image src={images.ICON_MAP_SEARCH} className='stadium-icon' />
|
<Image src={images.ICON_MAP_SEARCH} className='stadium-icon' />
|
||||||
</View>
|
</View>
|
||||||
<View className='stadium-item-right'>
|
<View className='stadium-item-right'>
|
||||||
<View className='stadium-name'>没有找到球场?去地图定位</View>
|
<View className='stadium-name'>没有找到球场?去地图定位</View>
|
||||||
<View className='stadium-address'>
|
<View className='stadium-address'>
|
||||||
<Text>腾讯地图</Text>
|
<Text className='map-search-text'>腾讯地图</Text>
|
||||||
<Image src={images.ICON_ARRORW_SMALL} className='stadium-map-icon' />
|
<Image src={images.ICON_ARRORW_SMALL} className='stadium-map-icon' />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>)
|
</View>
|
||||||
}
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{/* 场馆列表 */}
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
</CommonPopup>
|
</CommonPopup>
|
||||||
)
|
)
|
||||||
@@ -114,7 +114,6 @@
|
|||||||
border: 1px solid #e0e0e0;
|
border: 1px solid #e0e0e0;
|
||||||
background: white;
|
background: white;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import React, { useState, useCallback } from 'react'
|
import React, { useState, useCallback, forwardRef, useImperativeHandle } from 'react'
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import { View, Text, Image } from '@tarojs/components'
|
import { View, Text, Image } from '@tarojs/components'
|
||||||
import images from '@/config/images'
|
import images from '@/config/images'
|
||||||
import './StadiumDetail.scss'
|
|
||||||
import TextareaTag from '@/components/TextareaTag'
|
import TextareaTag from '@/components/TextareaTag'
|
||||||
import CoverImageUpload, { type CoverImage } from '@/components/ImageUpload'
|
import CoverImageUpload, { type CoverImage } from '@/components/ImageUpload'
|
||||||
|
import { useDictionaryActions } from '@/store/dictionaryStore'
|
||||||
|
import './StadiumDetail.scss'
|
||||||
|
|
||||||
export interface Stadium {
|
export interface Stadium {
|
||||||
id?: string
|
id?: string
|
||||||
@@ -13,45 +14,27 @@ export interface Stadium {
|
|||||||
longitude?: number
|
longitude?: number
|
||||||
latitude?: number
|
latitude?: number
|
||||||
istance?: string
|
istance?: string
|
||||||
|
court_type?: string
|
||||||
|
court_surface?: string
|
||||||
|
description?: string
|
||||||
|
description_tag?: string[]
|
||||||
|
venue_image_list?: CoverImage[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StadiumDetailProps {
|
interface StadiumDetailProps {
|
||||||
stadium: Stadium
|
stadium: Stadium
|
||||||
onBack: () => void
|
|
||||||
onConfirm: (stadium: Stadium, venueType: string, groundMaterial: string, additionalInfo: string) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const stadiumInfo = [
|
// 定义暴露给父组件的方法接口
|
||||||
{
|
export interface StadiumDetailRef {
|
||||||
label: '场地类型',
|
getFormData: () => any
|
||||||
options: ['室内', '室外', '室外雨棚'],
|
setFormData: (data: any) => void
|
||||||
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 }) => {
|
const SectionTitle: React.FC<{ title: string,prop: string }> = ({ title, prop }) => {
|
||||||
console.log(prop,'propprop');
|
if (prop === 'venue_image_list') {
|
||||||
if (prop === 'imagesList') {
|
|
||||||
return (
|
return (
|
||||||
<View className='section-title'>
|
<View className='section-title'>
|
||||||
<Text>{title}</Text>
|
<Text>{title}</Text>
|
||||||
@@ -78,21 +61,61 @@ const SectionContainer: React.FC<{ title: string; children: React.ReactNode, pro
|
|||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|
||||||
const StadiumDetail: React.FC<StadiumDetailProps> = ({
|
const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
|
||||||
stadium,
|
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({
|
const [formData, setFormData] = useState({
|
||||||
stadiumName: stadium.name,
|
name: stadium.name,
|
||||||
stadiumAddress: stadium.address,
|
address: stadium.address,
|
||||||
stadiumLongitude: stadium.longitude,
|
latitude: stadium.longitude,
|
||||||
stadiumLatitude: stadium.latitude,
|
longitude: stadium.latitude,
|
||||||
istance: stadium.istance,
|
istance: stadium.istance,
|
||||||
venueType: '室内',
|
court_type: court_type[0] || '',
|
||||||
groundMaterial: '硬地',
|
court_surface: court_surface[0] || '',
|
||||||
additionalInfo: '',
|
additionalInfo: '',
|
||||||
imagesList: [] as CoverImage[]
|
venue_image_list: [] as CoverImage[],
|
||||||
|
description:{
|
||||||
|
description: '',
|
||||||
|
description_tag: []
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 暴露方法给父组件
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
getFormData: () => formData,
|
||||||
|
setFormData: (data: any) => setFormData(data)
|
||||||
|
}), [formData, stadium])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleMapLocation = () => {
|
const handleMapLocation = () => {
|
||||||
@@ -101,10 +124,10 @@ const StadiumDetail: React.FC<StadiumDetailProps> = ({
|
|||||||
console.log(res,'resres');
|
console.log(res,'resres');
|
||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...formData,
|
||||||
stadiumName: res.name,
|
name: res.name,
|
||||||
stadiumAddress: res.address,
|
address: res.address,
|
||||||
stadiumLongitude: res.longitude,
|
latitude: res.longitude,
|
||||||
stadiumLatitude: res.latitude
|
longitude: res.latitude
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
fail: (err) => {
|
fail: (err) => {
|
||||||
@@ -122,10 +145,10 @@ const StadiumDetail: React.FC<StadiumDetailProps> = ({
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const getSelectedByLabel = useCallback((label: string) => {
|
const getSelectedByLabel = useCallback((label: string) => {
|
||||||
if (label === '场地类型') return formData.venueType
|
if (label === '场地类型') return formData.court_type
|
||||||
if (label === '地面材质') return formData.groundMaterial
|
if (label === '地面材质') return formData.court_surface
|
||||||
return ''
|
return ''
|
||||||
}, [formData.venueType, formData.groundMaterial])
|
}, [formData.court_type, formData.court_surface])
|
||||||
|
|
||||||
|
|
||||||
console.log(stadium,'stadiumstadium');
|
console.log(stadium,'stadiumstadium');
|
||||||
@@ -140,10 +163,10 @@ const StadiumDetail: React.FC<StadiumDetailProps> = ({
|
|||||||
<Image src={images.ICON_STADIUM} className='stadium-icon' />
|
<Image src={images.ICON_STADIUM} className='stadium-icon' />
|
||||||
</View>
|
</View>
|
||||||
<View className='stadium-item-right'>
|
<View className='stadium-item-right'>
|
||||||
<View className='stadium-name'>{formData.stadiumName}</View>
|
<View className='stadium-name'>{formData.name}</View>
|
||||||
<View className='stadium-address'>
|
<View className='stadium-address'>
|
||||||
<Text>{formData.istance} · </Text>
|
<Text>{formData.istance} · </Text>
|
||||||
<Text>{formData.stadiumAddress}</Text>
|
<Text>{formData.address}</Text>
|
||||||
<Image src={images.ICON_ARRORW_SMALL} className='stadium-map-icon' />
|
<Image src={images.ICON_ARRORW_SMALL} className='stadium-map-icon' />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -172,7 +195,7 @@ const StadiumDetail: React.FC<StadiumDetailProps> = ({
|
|||||||
<SectionContainer key={item.label} title={item.label} prop={item.prop}>
|
<SectionContainer key={item.label} title={item.label} prop={item.prop}>
|
||||||
<View className='textarea-tag-container'>
|
<View className='textarea-tag-container'>
|
||||||
<TextareaTag
|
<TextareaTag
|
||||||
value={formData.additionalInfo}
|
value={formData[item.prop]}
|
||||||
onChange={(value) => updateFormData(item.prop, value)}
|
onChange={(value) => updateFormData(item.prop, value)}
|
||||||
placeholder='有其他场地信息可备注'
|
placeholder='有其他场地信息可备注'
|
||||||
options={(item.options || []).map((o) => ({ label: o, value: o }))}
|
options={(item.options || []).map((o) => ({ label: o, value: o }))}
|
||||||
@@ -186,7 +209,7 @@ const StadiumDetail: React.FC<StadiumDetailProps> = ({
|
|||||||
return (
|
return (
|
||||||
<SectionContainer key={item.label} title={item.label} prop={item.prop}>
|
<SectionContainer key={item.label} title={item.label} prop={item.prop}>
|
||||||
<CoverImageUpload
|
<CoverImageUpload
|
||||||
images={formData.imagesList}
|
images={formData[item.prop]}
|
||||||
onChange={(images) => updateFormData(item.prop, images)}
|
onChange={(images) => updateFormData(item.prop, images)}
|
||||||
/>
|
/>
|
||||||
</SectionContainer>
|
</SectionContainer>
|
||||||
@@ -197,6 +220,6 @@ const StadiumDetail: React.FC<StadiumDetailProps> = ({
|
|||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
export default StadiumDetail
|
export default StadiumDetail
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import { View, Text } 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 }) => {
|
||||||
|
|
||||||
|
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>
|
||||||
|
{
|
||||||
|
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']}>修改</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FormSwitch
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
1
src/pages/publishBall/components/WechatSwitch/index.ts
Normal file
1
src/pages/publishBall/components/WechatSwitch/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './WechatSwitch'
|
||||||
@@ -34,9 +34,6 @@
|
|||||||
.activity-type-switch{
|
.activity-type-switch{
|
||||||
padding: 4px 16px 0 16px;
|
padding: 4px 16px 0 16px;
|
||||||
}
|
}
|
||||||
.publish-form{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// 场次标题行
|
// 场次标题行
|
||||||
.session-header {
|
.session-header {
|
||||||
|
|||||||
@@ -6,28 +6,46 @@ import PublishForm from './publishForm'
|
|||||||
import { publishBallFormSchema } from '../../config/formSchema/publishBallFormSchema';
|
import { publishBallFormSchema } from '../../config/formSchema/publishBallFormSchema';
|
||||||
import { PublishBallFormData } from '../../../types/publishBall';
|
import { PublishBallFormData } from '../../../types/publishBall';
|
||||||
import PublishService from '@/services/publishService';
|
import PublishService from '@/services/publishService';
|
||||||
|
import { getNextHourTime, getEndTime } from '@/utils/timeUtils';
|
||||||
import images from '@/config/images'
|
import images from '@/config/images'
|
||||||
import styles from './index.module.scss'
|
import styles from './index.module.scss'
|
||||||
|
|
||||||
|
const defaultFormData: PublishBallFormData = {
|
||||||
|
title: '',
|
||||||
|
image_list: ['https://static-o.oss-cn-shenzhen.aliyuncs.com/images/tpbj/tpss10.jpg'],
|
||||||
|
timeRange: {
|
||||||
|
start_time: getNextHourTime(),
|
||||||
|
end_time: getEndTime(getNextHourTime())
|
||||||
|
},
|
||||||
|
activityInfo: {
|
||||||
|
play_type: '不限',
|
||||||
|
price: '',
|
||||||
|
venue_id: null,
|
||||||
|
location_name: '',
|
||||||
|
location: '',
|
||||||
|
latitude: '',
|
||||||
|
longitude: '',
|
||||||
|
court_type: '',
|
||||||
|
court_surface: '',
|
||||||
|
venue_description_tag: [],
|
||||||
|
venue_description: '',
|
||||||
|
venue_image_list: [],
|
||||||
|
},
|
||||||
|
players: [1, 4],
|
||||||
|
skill_level: [1.0, 5.0],
|
||||||
|
descriptionInfo: {
|
||||||
|
description: '',
|
||||||
|
description_tag: [],
|
||||||
|
},
|
||||||
|
is_substitute_supported: true,
|
||||||
|
is_wechat_contact: true,
|
||||||
|
wechat_contact: '14223332214'
|
||||||
|
}
|
||||||
|
|
||||||
const PublishBall: React.FC = () => {
|
const PublishBall: React.FC = () => {
|
||||||
const [activityType, setActivityType] = useState<ActivityType>('individual')
|
const [activityType, setActivityType] = useState<ActivityType>('individual')
|
||||||
const [formData, setFormData] = useState<PublishBallFormData[]>([
|
const [formData, setFormData] = useState<PublishBallFormData[]>([
|
||||||
{
|
defaultFormData
|
||||||
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
|
|
||||||
}
|
|
||||||
])
|
])
|
||||||
|
|
||||||
// 删除确认弹窗状态
|
// 删除确认弹窗状态
|
||||||
@@ -41,9 +59,11 @@ const PublishBall: React.FC = () => {
|
|||||||
|
|
||||||
// 更新表单数据
|
// 更新表单数据
|
||||||
const updateFormData = (key: keyof PublishBallFormData, value: any, index: number) => {
|
const updateFormData = (key: keyof PublishBallFormData, value: any, index: number) => {
|
||||||
|
console.log(key, value, index, 'key, value, index');
|
||||||
setFormData(prev => {
|
setFormData(prev => {
|
||||||
const newData = [...prev]
|
const newData = [...prev]
|
||||||
newData[index] = { ...newData[index], [key]: value }
|
newData[index] = { ...newData[index], [key]: value }
|
||||||
|
console.log(newData, 'newData');
|
||||||
return newData
|
return newData
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -56,21 +76,12 @@ const PublishBall: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
|
const newStartTime = getNextHourTime()
|
||||||
setFormData(prev => [...prev, {
|
setFormData(prev => [...prev, {
|
||||||
|
...defaultFormData,
|
||||||
title: '',
|
title: '',
|
||||||
timeRange: {
|
start_time: newStartTime,
|
||||||
startDate: '2025-11-23',
|
end_time: getEndTime(newStartTime)
|
||||||
startTime: '08:00',
|
|
||||||
endTime: '10:00'
|
|
||||||
},
|
|
||||||
fee: '',
|
|
||||||
location: '',
|
|
||||||
gameplay: '',
|
|
||||||
minParticipants: 1,
|
|
||||||
maxParticipants: 4,
|
|
||||||
ntpLevel: [2.0, 4.0],
|
|
||||||
additionalRequirements: '',
|
|
||||||
autoDegrade: false
|
|
||||||
}])
|
}])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,30 +128,80 @@ const PublishBall: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const validateFormData = (formData: PublishBallFormData) => {
|
||||||
|
const { activityInfo, image_list, title } = formData;
|
||||||
|
const { play_type, price, location_name } = activityInfo;
|
||||||
|
if (!image_list.length) {
|
||||||
|
Taro.showToast({
|
||||||
|
title: `请上传活动封面`,
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!title) {
|
||||||
|
Taro.showToast({
|
||||||
|
title: `请输入活动标题`,
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!price) {
|
||||||
|
Taro.showToast({
|
||||||
|
title: `请输入费用`,
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!play_type) {
|
||||||
|
Taro.showToast({
|
||||||
|
title: `请选择玩法类型`,
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!location_name) {
|
||||||
|
Taro.showToast({
|
||||||
|
title: `请选择场地`,
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// 提交表单
|
// 提交表单
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
// 基础验证
|
// 基础验证
|
||||||
|
console.log(formData, 'formData');
|
||||||
// TODO: 实现提交逻辑
|
if (activityType === 'individual') {
|
||||||
const res = await PublishService.createPersonal({
|
const isValid = validateFormData(formData[0])
|
||||||
"title": "周末网球约球",
|
if (!isValid) {
|
||||||
"venue_id": 1,
|
return
|
||||||
"creator_id": 1,
|
}
|
||||||
"game_date": "2024-06-15",
|
const { activityInfo, descriptionInfo, timeRange, players, skill_level, ...rest } = formData[0];
|
||||||
"start_time": "14:00",
|
const options = {
|
||||||
"end_time": "16:00",
|
...rest,
|
||||||
"max_participants": 4,
|
...activityInfo,
|
||||||
"current_participants": 2,
|
...descriptionInfo,
|
||||||
"ntrp_level": "2.0-4.0",
|
...timeRange,
|
||||||
"play_style": "单打",
|
max_players: players[1],
|
||||||
"description": "周末约球,欢迎参加",
|
current_players: players[0],
|
||||||
})
|
skill_level_min: skill_level[0],
|
||||||
console.log(res);
|
skill_level_max: skill_level[1]
|
||||||
|
}
|
||||||
|
const res = await PublishService.createPersonal(options);
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: '发布成功',
|
title: '发布成功',
|
||||||
icon: 'success'
|
icon: 'success'
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
Taro.showToast({
|
||||||
|
title: res.message,
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { View, Text } from '@tarojs/components'
|
import { View, Text } from '@tarojs/components'
|
||||||
|
import { ImageUpload, Range, TimeSelector, TextareaTag, NumberInterval, TitleTextarea, FormSwitch } from '../../components'
|
||||||
import Taro from '@tarojs/taro'
|
import FormBasicInfo from './components/FormBasicInfo'
|
||||||
import { ImageUpload, Range, TimeSelector, TextareaTag, SelectStadium, NumberInterval, TitleInput, FormBasicInfo, FormSwitch } from '../../components'
|
import { type CoverImage } from '../../components/index.types'
|
||||||
import { type Stadium, type CoverImage } from '../../components/index.types'
|
|
||||||
import { FormFieldConfig, FieldType } from '../../config/formSchema/publishBallFormSchema'
|
import { FormFieldConfig, FieldType } from '../../config/formSchema/publishBallFormSchema'
|
||||||
import { PublishBallFormData } from '../../../types/publishBall';
|
import { PublishBallFormData } from '../../../types/publishBall';
|
||||||
|
import WechatSwitch from './components/WechatSwitch/WechatSwitch'
|
||||||
import styles from './index.module.scss'
|
import styles from './index.module.scss'
|
||||||
|
import { useDictionaryActions } from '../../store/dictionaryStore'
|
||||||
|
|
||||||
// 组件映射器
|
// 组件映射器
|
||||||
const componentMap = {
|
const componentMap = {
|
||||||
[FieldType.TEXT]: TitleInput,
|
[FieldType.TEXT]: TitleTextarea,
|
||||||
[FieldType.TIMEINTERVAL]: TimeSelector,
|
[FieldType.TIMEINTERVAL]: TimeSelector,
|
||||||
[FieldType.RANGE]: Range,
|
[FieldType.RANGE]: Range,
|
||||||
[FieldType.TEXTAREATAG]: TextareaTag,
|
[FieldType.TEXTAREATAG]: TextareaTag,
|
||||||
@@ -19,6 +19,7 @@ const componentMap = {
|
|||||||
[FieldType.UPLOADIMAGE]: ImageUpload,
|
[FieldType.UPLOADIMAGE]: ImageUpload,
|
||||||
[FieldType.ACTIVITYINFO]: FormBasicInfo,
|
[FieldType.ACTIVITYINFO]: FormBasicInfo,
|
||||||
[FieldType.CHECKBOX]: FormSwitch,
|
[FieldType.CHECKBOX]: FormSwitch,
|
||||||
|
[FieldType.WECHATCONTACT]: WechatSwitch,
|
||||||
}
|
}
|
||||||
|
|
||||||
const PublishForm: React.FC<{
|
const PublishForm: React.FC<{
|
||||||
@@ -26,8 +27,9 @@ const PublishForm: React.FC<{
|
|||||||
onChange: (key: keyof PublishBallFormData, value: any, index?: number) => void,
|
onChange: (key: keyof PublishBallFormData, value: any, index?: number) => void,
|
||||||
optionsConfig: FormFieldConfig[] }> = ({ formData, onChange, optionsConfig }) => {
|
optionsConfig: FormFieldConfig[] }> = ({ formData, onChange, optionsConfig }) => {
|
||||||
const [coverImages, setCoverImages] = useState<CoverImage[]>([])
|
const [coverImages, setCoverImages] = useState<CoverImage[]>([])
|
||||||
const [showStadiumSelector, setShowStadiumSelector] = useState(false)
|
|
||||||
const [selectedStadium, setSelectedStadium] = useState<Stadium | null>(null)
|
// 字典数据相关
|
||||||
|
const { getDictionaryValue } = useDictionaryActions()
|
||||||
|
|
||||||
// 处理封面图片变化
|
// 处理封面图片变化
|
||||||
const handleCoverImagesChange = (images: CoverImage[]) => {
|
const handleCoverImagesChange = (images: CoverImage[]) => {
|
||||||
@@ -39,15 +41,47 @@ const PublishForm: React.FC<{
|
|||||||
onChange(key, value)
|
onChange(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取字典选项
|
||||||
|
const getDictionaryOptions = (key: string, defaultValue: any[] = []) => {
|
||||||
// 处理场馆选择
|
const dictValue = getDictionaryValue(key, defaultValue)
|
||||||
const handleStadiumSelect = (stadium: Stadium | null) => {
|
if (Array.isArray(dictValue)) {
|
||||||
setSelectedStadium(stadium)
|
return dictValue.map(item => ({
|
||||||
if (stadium) {
|
label: item.label || item.name || item.value || item,
|
||||||
updateFormData('location', stadium.name)
|
value: item.value || item.id || item
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
setShowStadiumSelector(false)
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 动态生成表单配置,集成字典数据
|
||||||
|
const getDynamicFormConfig = (): FormFieldConfig[] => {
|
||||||
|
return optionsConfig.map(item => {
|
||||||
|
// 如果是玩法选择,从字典获取选项
|
||||||
|
if (item.prop === 'activityInfo' && item.children) {
|
||||||
|
const playTypeOptions = getDictionaryOptions('game_play', item.children.find(child => child.prop === 'play_type')?.options)
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
children: item.children.map(child => {
|
||||||
|
if (child.prop === 'play_type') {
|
||||||
|
return { ...child, options: playTypeOptions }
|
||||||
|
}
|
||||||
|
return child
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是补充要求,从字典获取选项
|
||||||
|
if (item.prop === 'descriptionInfo') {
|
||||||
|
const descriptionOptions = getDictionaryOptions('publishing_requirements', [])
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
options: descriptionOptions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return item
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderSummary = (item: FormFieldConfig) => {
|
const renderSummary = (item: FormFieldConfig) => {
|
||||||
@@ -59,47 +93,23 @@ const PublishForm: React.FC<{
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 提交表单
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
// 基础验证
|
|
||||||
if (!formData.title.trim()) {
|
|
||||||
Taro.showToast({
|
|
||||||
title: '请输入活动标题',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (coverImages.length === 0) {
|
|
||||||
Taro.showToast({
|
|
||||||
title: '请至少上传一张活动封面',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: 实现提交逻辑
|
// 获取动态表单配置
|
||||||
console.log('提交数据:', { coverImages, formData })
|
const dynamicConfig = getDynamicFormConfig()
|
||||||
|
|
||||||
Taro.showToast({
|
|
||||||
title: '发布成功',
|
|
||||||
icon: 'success'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className={styles['publish-form']}>
|
<View className={styles['publish-form']}>
|
||||||
<View className={styles['publish-ball__content']}>
|
<View className={styles['publish-ball__content']}>
|
||||||
{
|
{
|
||||||
optionsConfig.map((item) => {
|
dynamicConfig.map((item) => {
|
||||||
const Component = componentMap[item.type]
|
const Component = componentMap[item.type]
|
||||||
const optionProps = {
|
const optionProps = {
|
||||||
...item.props,
|
...item.props,
|
||||||
...(item.key === 'additionalRequirements' ? { options: item.options } : {}),
|
...(item.type === FieldType.TEXTAREATAG ? { options: item.options } : {}),
|
||||||
...(item.props?.className ? { className: styles[item.props.className] } : {})
|
...(item.props?.className ? { className: styles[item.props.className] } : {}),
|
||||||
|
...(item.type === FieldType.WECHATCONTACT ? { wechatId: formData.wechat_contact } : {})
|
||||||
}
|
}
|
||||||
console.log(item.props?.className)
|
|
||||||
console.log(optionProps, item.label, formData[item.key]);
|
|
||||||
if (item.type === FieldType.UPLOADIMAGE) {
|
if (item.type === FieldType.UPLOADIMAGE) {
|
||||||
/* 活动封面 */
|
/* 活动封面 */
|
||||||
return <ImageUpload
|
return <ImageUpload
|
||||||
@@ -120,15 +130,10 @@ const PublishForm: React.FC<{
|
|||||||
{/* 费用地点玩法区域 - 合并白色块 */}
|
{/* 费用地点玩法区域 - 合并白色块 */}
|
||||||
<View className={styles['bg-section']}>
|
<View className={styles['bg-section']}>
|
||||||
<FormBasicInfo
|
<FormBasicInfo
|
||||||
fee={formData.fee}
|
|
||||||
location={formData.location}
|
|
||||||
gameplay={formData.gameplay}
|
|
||||||
selectedStadium={selectedStadium}
|
|
||||||
children={item.children || []}
|
children={item.children || []}
|
||||||
onFeeChange={(value) => updateFormData('fee', value)}
|
value={formData[item.prop]}
|
||||||
onLocationChange={(value) => updateFormData('location', value)}
|
onChange={(value) => updateFormData(item.prop as keyof PublishBallFormData, value)}
|
||||||
onGameplayChange={(value) => updateFormData('gameplay', value)}
|
{...optionProps}
|
||||||
onStadiumSelect={() => setShowStadiumSelector(true)}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
@@ -146,8 +151,8 @@ const PublishForm: React.FC<{
|
|||||||
<View className={styles['bg-section']}>
|
<View className={styles['bg-section']}>
|
||||||
<Component
|
<Component
|
||||||
label={item.label}
|
label={item.label}
|
||||||
value={formData[item.key]}
|
value={formData[item.prop]}
|
||||||
onChange={(value) => updateFormData(item.key as keyof PublishBallFormData, value)}
|
onChange={(value) => updateFormData(item.prop as keyof PublishBallFormData, value)}
|
||||||
{...optionProps}
|
{...optionProps}
|
||||||
placeholder={item.placeholder}
|
placeholder={item.placeholder}
|
||||||
/>
|
/>
|
||||||
@@ -158,13 +163,6 @@ const PublishForm: React.FC<{
|
|||||||
}
|
}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 场馆选择弹窗 */}
|
|
||||||
<SelectStadium
|
|
||||||
visible={showStadiumSelector}
|
|
||||||
onClose={() => setShowStadiumSelector(false)}
|
|
||||||
onConfirm={handleStadiumSelect}
|
|
||||||
/>
|
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,13 @@ class CommonApiService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取字典数据
|
||||||
|
async getDictionaryManyKey(keys: string): Promise<ApiResponse<any>> {
|
||||||
|
return httpService.get('/parameter/many_key', { keys }, {
|
||||||
|
showLoading: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出通用API服务实例
|
// 导出通用API服务实例
|
||||||
|
|||||||
@@ -3,17 +3,31 @@ import type { ApiResponse } from './httpService'
|
|||||||
|
|
||||||
// 用户接口
|
// 用户接口
|
||||||
export interface PublishBallData {
|
export interface PublishBallData {
|
||||||
title: string,
|
title: string // 球局标题
|
||||||
venue_id: number,
|
image_list: Array<string>[] // 球局封面
|
||||||
creator_id: number,
|
|
||||||
game_date: string,
|
|
||||||
start_time: string,
|
start_time: string,
|
||||||
end_time: string,
|
end_time: string
|
||||||
max_participants: number,
|
play_type: string // 玩法类型
|
||||||
current_participants: number,
|
price: number | string // 价格
|
||||||
ntrp_level: string,
|
venue_id?: number | null // 场地id
|
||||||
play_style: string,
|
location_name?: string // 场地名称
|
||||||
description: 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>[] // 场地图片
|
||||||
|
max_players: number // 人数要求
|
||||||
|
current_players: number // 人数要求
|
||||||
|
skill_level_min: number // 水平要求(NTRP)
|
||||||
|
skill_level_max: number // 水平要求(NTRP)
|
||||||
|
description: string // 备注
|
||||||
|
description_tag: Array<string>[] // 备注标签
|
||||||
|
is_substitute_supported: boolean // 是否支持替补
|
||||||
|
is_wechat_contact: boolean // 是否需要微信联系
|
||||||
|
wechat_contact?: string // 微信联系
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应接口
|
// 响应接口
|
||||||
@@ -23,15 +37,35 @@ export interface Response {
|
|||||||
data: any
|
data: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 响应接口
|
||||||
|
export interface StadiumListResponse {
|
||||||
|
rows: Stadium[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Stadium {
|
||||||
|
id?: string
|
||||||
|
name: string
|
||||||
|
address?: string
|
||||||
|
istance?: string
|
||||||
|
longitude?: number
|
||||||
|
latitude?: number
|
||||||
|
}
|
||||||
|
|
||||||
// 发布球局类
|
// 发布球局类
|
||||||
class PublishService {
|
class PublishService {
|
||||||
// 用户登录
|
// 发布
|
||||||
async createPersonal(data: PublishBallData): Promise<ApiResponse<Response>> {
|
async createPersonal(data: PublishBallData): Promise<ApiResponse<Response>> {
|
||||||
return httpService.post('/games/create', data, {
|
return httpService.post('/games/create', data, {
|
||||||
showLoading: true,
|
showLoading: true,
|
||||||
loadingText: '发布中...'
|
loadingText: '发布中...'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取球场列表
|
||||||
|
async getStadiumList(data: { seachOption: { latitude: number, longitude: number }}): Promise<ApiResponse<StadiumListResponse>> {
|
||||||
|
return httpService.post('/venues/list', data, {
|
||||||
|
showLoading: false })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出认证服务实例
|
// 导出认证服务实例
|
||||||
|
|||||||
79
src/store/dictionaryStore.ts
Normal file
79
src/store/dictionaryStore.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { create } from 'zustand'
|
||||||
|
import commonApi from '../services/commonApi'
|
||||||
|
|
||||||
|
// 字典数据接口
|
||||||
|
export interface DictionaryData {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字典Store状态接口
|
||||||
|
interface DictionaryState {
|
||||||
|
dictionaryData: DictionaryData
|
||||||
|
isLoading: boolean
|
||||||
|
error: string | null
|
||||||
|
fetchDictionary: () => Promise<void>
|
||||||
|
getDictionaryValue: (key: string, defaultValue?: any) => any
|
||||||
|
clearDictionary: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建字典Store
|
||||||
|
export const useDictionaryStore = create<DictionaryState>()((set, get) => ({
|
||||||
|
// 初始状态
|
||||||
|
dictionaryData: {},
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
|
||||||
|
// 获取字典数据
|
||||||
|
fetchDictionary: async () => {
|
||||||
|
set({ isLoading: true, error: null })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const keys = 'publishing_requirements,court_type,court_surface,supplementary_information,game_play';
|
||||||
|
const response = await commonApi.getDictionaryManyKey(keys)
|
||||||
|
|
||||||
|
if (response.code === 0 && response.data) {
|
||||||
|
const dictionaryData = {};
|
||||||
|
keys.split(',').forEach(key => {
|
||||||
|
const list = response.data[key];
|
||||||
|
const listData = list.split('|');
|
||||||
|
dictionaryData[key] = listData;
|
||||||
|
})
|
||||||
|
set({
|
||||||
|
dictionaryData: dictionaryData || {},
|
||||||
|
isLoading: false
|
||||||
|
})
|
||||||
|
console.log('字典数据获取成功:', response.data)
|
||||||
|
} else {
|
||||||
|
throw new Error(response.message || '获取字典数据失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : '获取字典数据失败'
|
||||||
|
set({
|
||||||
|
error: errorMessage,
|
||||||
|
isLoading: false
|
||||||
|
})
|
||||||
|
console.error('获取字典数据失败:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取字典值
|
||||||
|
getDictionaryValue: (key: string, defaultValue?: any) => {
|
||||||
|
const { dictionaryData } = get()
|
||||||
|
return dictionaryData[key] !== undefined ? dictionaryData[key] : defaultValue
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清空字典数据
|
||||||
|
clearDictionary: () => {
|
||||||
|
set({ dictionaryData: {}, error: null })
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 导出hooks
|
||||||
|
export const useDictionaryData = () => useDictionaryStore((state) => state.dictionaryData)
|
||||||
|
export const useDictionaryLoading = () => useDictionaryStore((state) => state.isLoading)
|
||||||
|
export const useDictionaryError = () => useDictionaryStore((state) => state.error)
|
||||||
|
export const useDictionaryActions = () => useDictionaryStore((state) => ({
|
||||||
|
fetchDictionary: state.fetchDictionary,
|
||||||
|
getDictionaryValue: state.getDictionaryValue,
|
||||||
|
clearDictionary: state.clearDictionary
|
||||||
|
}))
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
|
|
||||||
|
export interface Location {
|
||||||
|
latitude: number
|
||||||
|
longitude: number
|
||||||
|
speed?: number
|
||||||
|
accuracy?: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface LocationInfo {
|
export interface LocationInfo {
|
||||||
latitude: number
|
latitude: number
|
||||||
longitude: number
|
longitude: number
|
||||||
@@ -56,6 +63,24 @@ export const chooseLocation = (): Promise<LocationInfo> => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getLocation = (): Promise<Location> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
Taro.getLocation({
|
||||||
|
success: (res) => {
|
||||||
|
resolve({
|
||||||
|
latitude: res.latitude,
|
||||||
|
longitude: res.longitude,
|
||||||
|
speed: res.speed,
|
||||||
|
accuracy: res.accuracy
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 逆地理编码(简化版本,实际项目中应该调用真实的地图服务API)
|
// 逆地理编码(简化版本,实际项目中应该调用真实的地图服务API)
|
||||||
export const reverseGeocode = (latitude: number, longitude: number): Promise<string> => {
|
export const reverseGeocode = (latitude: number, longitude: number): Promise<string> => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
|||||||
44
src/utils/timeUtils.ts
Normal file
44
src/utils/timeUtils.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取下一个整点时间
|
||||||
|
* @returns 格式为 YYYY-MM-DD HH:mm 的字符串
|
||||||
|
*/
|
||||||
|
export const getNextHourTime = (): string => {
|
||||||
|
const now = dayjs()
|
||||||
|
const nextHour = now.add(1, 'hour').startOf('hour')
|
||||||
|
return nextHour.format('YYYY-MM-DD HH:mm')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据开始时间计算结束时间(2小时后)
|
||||||
|
* @param startTime 开始时间,格式为 YYYY-MM-DD HH:mm
|
||||||
|
* @returns 格式为 YYYY-MM-DD HH:mm 的字符串
|
||||||
|
*/
|
||||||
|
export const getEndTime = (startTime: string): string => {
|
||||||
|
const startDateTime = dayjs(startTime)
|
||||||
|
const endDateTime = startDateTime.add(2, 'hour')
|
||||||
|
return endDateTime.format('YYYY-MM-DD HH:mm')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const getDate = (date: string): string => {
|
||||||
|
return dayjs(date).format('YYYY年MM月DD日')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTime = (time: string): string => {
|
||||||
|
const timeObj = dayjs(time)
|
||||||
|
const hour = timeObj.hour()
|
||||||
|
const minute = timeObj.minute()
|
||||||
|
|
||||||
|
// 判断是上午还是下午
|
||||||
|
const period = hour < 12 ? 'AM' : 'PM'
|
||||||
|
|
||||||
|
// 转换为12小时制
|
||||||
|
const hour12 = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour
|
||||||
|
|
||||||
|
// 格式化分钟,保证两位数
|
||||||
|
const minuteStr = minute.toString().padStart(2, '0')
|
||||||
|
|
||||||
|
return `${hour12}:${minuteStr} ${period}`
|
||||||
|
}
|
||||||
@@ -1,15 +1,32 @@
|
|||||||
import { TimeRange } from "@/components/index.types"
|
|
||||||
|
|
||||||
|
|
||||||
export interface PublishBallFormData {
|
export interface PublishBallFormData {
|
||||||
title: string
|
title: string // 球局标题
|
||||||
timeRange: TimeRange
|
image_list: Array<string>[] // 球局封面
|
||||||
fee: string
|
timeRange: {
|
||||||
location: string
|
start_time: string,
|
||||||
gameplay: string
|
end_time: string
|
||||||
minParticipants: number
|
}
|
||||||
maxParticipants: number
|
activityInfo: {
|
||||||
ntpLevel: number[]
|
play_type: string // 玩法类型
|
||||||
additionalRequirements: string
|
price: number | string // 价格
|
||||||
autoDegrade: boolean
|
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>[] // 场地图片
|
||||||
|
}
|
||||||
|
players: [number, number] // 人数要求
|
||||||
|
skill_level: [number, number] // 水平要求(NTRP)
|
||||||
|
descriptionInfo: {
|
||||||
|
description: string // 备注
|
||||||
|
description_tag: Array<string>[] // 备注标签
|
||||||
|
}
|
||||||
|
is_substitute_supported: boolean // 是否支持替补
|
||||||
|
is_wechat_contact: boolean // 是否需要微信联系
|
||||||
|
wechat_contact: string // 微信联系
|
||||||
}
|
}
|
||||||
@@ -4099,6 +4099,11 @@ data-view-byte-offset@^1.0.1:
|
|||||||
es-errors "^1.3.0"
|
es-errors "^1.3.0"
|
||||||
is-data-view "^1.0.1"
|
is-data-view "^1.0.1"
|
||||||
|
|
||||||
|
dayjs@^1.11.13:
|
||||||
|
version "1.11.13"
|
||||||
|
resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c"
|
||||||
|
integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==
|
||||||
|
|
||||||
debug@2.6.9:
|
debug@2.6.9:
|
||||||
version "2.6.9"
|
version "2.6.9"
|
||||||
resolved "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
resolved "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||||
|
|||||||
Reference in New Issue
Block a user