修改发布增加拖动

This commit is contained in:
筱野
2025-08-31 20:19:10 +08:00
parent 721a9fab9d
commit f5aff7ce73
16 changed files with 399 additions and 115 deletions

View File

@@ -1,11 +1,13 @@
export default defineAppConfig({ export default defineAppConfig({
pages: [ pages: [
'pages/list/index', 'pages/list/index',
'pages/publishBall/index',
// 'pages/userInfo/myself/index', // 'pages/userInfo/myself/index',
'pages/login/index/index', 'pages/login/index/index',
'pages/login/verification/index', 'pages/login/verification/index',
'pages/login/terms/index', 'pages/login/terms/index',
'pages/publishBall/index',
// 'pages/mapDisplay/index', // 'pages/mapDisplay/index',
'pages/detail/index', 'pages/detail/index',
'pages/message/index', 'pages/message/index',

View File

@@ -1,4 +1,4 @@
import React from 'react' import React, { useRef, useState } from 'react'
import { View, Text } from '@tarojs/components' import { View, Text } from '@tarojs/components'
import { Popup, Button } from '@nutui/nutui-react-taro' import { Popup, Button } from '@nutui/nutui-react-taro'
import styles from './index.module.scss' import styles from './index.module.scss'
@@ -19,6 +19,7 @@ export interface CommonPopupProps {
children?: React.ReactNode children?: React.ReactNode
className?: string className?: string
style?: React.CSSProperties style?: React.CSSProperties
enableDragToClose?: boolean
} }
const CommonPopup: React.FC<CommonPopupProps> = ({ const CommonPopup: React.FC<CommonPopupProps> = ({
@@ -36,8 +37,14 @@ const CommonPopup: React.FC<CommonPopupProps> = ({
round = true, round = true,
zIndex, zIndex,
style, style,
children children,
enableDragToClose = true
}) => { }) => {
const [dragOffset, setDragOffset] = useState(0)
const [isDragging, setIsDragging] = useState(false)
const touchStartY = useRef(0)
const popupRef = useRef<HTMLDivElement>(null)
const handleCancel = () => { const handleCancel = () => {
if (onCancel) { if (onCancel) {
onCancel() onCancel()
@@ -46,6 +53,39 @@ const CommonPopup: React.FC<CommonPopupProps> = ({
} }
} }
const handleTouchStart = (e: any) => {
if (!enableDragToClose) return
touchStartY.current = e.touches[0].clientY
setIsDragging(true)
}
const handleTouchMove = (e: any) => {
if (!enableDragToClose || !isDragging) return
const currentY = e.touches[0].clientY
const deltaY = currentY - touchStartY.current
// 只允许向下拖动,限制最大拖动距离
if (deltaY > 0) {
setDragOffset(Math.min(deltaY, 200))
}
}
const handleTouchEnd = () => {
if (!enableDragToClose || !isDragging) return
setIsDragging(false)
// 如果拖动距离超过阈值,关闭弹窗
if (dragOffset > 100) {
onClose()
}
// 重置拖动偏移
setDragOffset(0)
}
return ( return (
<Popup <Popup
visible={visible} visible={visible}
@@ -54,8 +94,27 @@ const CommonPopup: React.FC<CommonPopupProps> = ({
closeable={false} closeable={false}
onClose={onClose} onClose={onClose}
className={`${styles['common-popup']} ${className ? className : ''}`} className={`${styles['common-popup']} ${className ? className : ''}`}
style={{ zIndex: zIndex ? zIndex : undefined, ...style }} style={{
zIndex: zIndex ? zIndex : undefined,
...style
}}
> >
{enableDragToClose && (
<View className={styles['common-popup__drag-handle-container']}>
<View
className={styles['common-popup__drag-handle']}
style={{
transform: `translateX(-50%) translateY(${dragOffset * 0.3}px)`,
opacity: isDragging ? 0.8 : 1,
transition: isDragging ? 'none' : 'all 0.3s ease-out'
}}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
/>
</View>
)}
{showHeader && ( {showHeader && (
<View className={styles['common-popup__header']}> <View className={styles['common-popup__header']}>
{typeof title === 'string' ? <Text className={styles['common-popup__title']}>{title}</Text> : title} {typeof title === 'string' ? <Text className={styles['common-popup__title']}>{title}</Text> : title}

View File

@@ -1,6 +1,25 @@
@use '~@/scss/themeColor.scss' as theme; @use '~@/scss/themeColor.scss' as theme;
.common-popup { .common-popup {
.common-popup__drag-handle-container {
position: position;
}
.common-popup__drag-handle {
position: absolute;
top: 6px;
left: 50%;
width: 32px;
height: 4px;
background-color: rgba(22, 24, 35, 0.20);
border-radius: 2px;
z-index: 10;
cursor: pointer;
transition: background-color 0.2s ease;
&:active {
background-color: #9ca3af;
}
}
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
max-height: calc(100vh - 10px); max-height: calc(100vh - 10px);

View File

@@ -33,8 +33,8 @@
.info-popover { .info-popover {
position: absolute; position: absolute;
bottom: 22px; bottom: 22px;
left: -65px; left: -92px;
width: 130px; width: 184px;
padding:12px; padding:12px;
background: rgba(57, 59, 68, 0.90); background: rgba(57, 59, 68, 0.90);
color: #fff; color: #fff;
@@ -51,7 +51,7 @@
content: ''; content: '';
position: absolute; position: absolute;
bottom: -6px; bottom: -6px;
left: 68px; /* 对齐图标宽12px可按需微调 */ left: 94px; /* 对齐图标宽12px可按需微调 */
width: 0; width: 0;
height: 0; height: 0;
border-left: 6px solid transparent; border-left: 6px solid transparent;

View File

@@ -61,9 +61,7 @@ const HourMinutePicker: React.FC<HourMinutePickerProps> = ({
return ( return (
<View className={styles['hour-minute-picker-popup']}> <View className={styles['hour-minute-picker-popup']}>
{/* 拖拽手柄 */} {/* 拖拽手柄 */}
<View className={styles['drag-handle']} />
{/* 时间选择器 */} {/* 时间选择器 */}
<View className={styles['picker-container']}> <View className={styles['picker-container']}>
{/* 多列选择器 */} {/* 多列选择器 */}

View File

@@ -8,14 +8,6 @@
box-sizing: border-box; box-sizing: border-box;
} }
.drag-handle {
width: 40px;
height: 4px;
background-color: #e0e0e0;
border-radius: 2px;
margin: 0 auto 20px;
}
.picker-container { .picker-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -6,16 +6,30 @@ import { InputNumber } from '@nutui/nutui-react-taro'
interface NumberIntervalProps { interface NumberIntervalProps {
value: [number, number] value: [number, number]
onChange: (value: [number, number]) => void onChange: (value: [number, number]) => void
min: number
max: number
} }
const NumberInterval: React.FC<NumberIntervalProps> = ({ const NumberInterval: React.FC<NumberIntervalProps> = ({
value, value,
onChange onChange,
min,
max
}) => { }) => {
const [minParticipants, maxParticipants] = value || [1, 4] const [minParticipants, maxParticipants] = value || [1, 1]
const handleChange = (value: [number | string, number | string]) => { const handleChange = (value: [number | string, number | string]) => {
onChange([Number(value[0]), Number(value[1])]) const newMin = Number(value[0])
const newMax = Number(value[1])
// 确保最少人数不能大于最多人数
if (newMin > newMax) {
return
}
onChange([newMin, newMax])
} }
return ( return (
<View className='participants-control-section'> <View className='participants-control-section'>
<View className='participant-control'> <View className='participant-control'>
@@ -24,7 +38,7 @@ const NumberInterval: React.FC<NumberIntervalProps> = ({
<InputNumber <InputNumber
className="format-width" className="format-width"
defaultValue={minParticipants} defaultValue={minParticipants}
min={minParticipants} min={min}
max={maxParticipants} max={maxParticipants}
onChange={(value) => handleChange([value, maxParticipants])} onChange={(value) => handleChange([value, maxParticipants])}
formatter={(value) => `${value}`} formatter={(value) => `${value}`}
@@ -37,9 +51,9 @@ const NumberInterval: React.FC<NumberIntervalProps> = ({
<InputNumber <InputNumber
className="format-width" className="format-width"
defaultValue={maxParticipants} defaultValue={maxParticipants}
onChange={(value) => handleChange([value, maxParticipants])} onChange={(value) => handleChange([minParticipants, value])}
min={minParticipants} min={minParticipants}
max={maxParticipants} max={max}
formatter={(value) => `${value}`} formatter={(value) => `${value}`}
/> />
</View> </View>

View File

@@ -181,7 +181,7 @@
flex-shrink: 0; flex-shrink: 0;
overflow: hidden; overflow: hidden;
&.rotated { &.rotated {
transform: rotate(45deg); transform: rotate(-90deg);
} }
} }

View File

@@ -19,7 +19,7 @@ const TextareaTag: React.FC<TextareaTagProps> = ({
value, value,
onChange, onChange,
placeholder = '请输入', placeholder = '请输入',
maxLength = 500, maxLength = 1000,
options = [] options = []
}) => { }) => {
// 处理文本输入变化 // 处理文本输入变化

View File

@@ -50,7 +50,8 @@ export const publishBallFormSchema: FormFieldConfig[] = [
placeholder: '请选择活动类型', placeholder: '请选择活动类型',
required: true, required: true,
props: { props: {
maxCount: 9 maxCount: 9,
source: ['album', 'history', 'preset']
} }
}, },
{ {
@@ -121,7 +122,8 @@ export const publishBallFormSchema: FormFieldConfig[] = [
defaultValue: 1, defaultValue: 1,
props: { props: {
showSummary: true, showSummary: true,
summary: '最少1人最多4人', min: 1,
max: 20,
} }
}, },
@@ -146,6 +148,9 @@ export const publishBallFormSchema: FormFieldConfig[] = [
type: FieldType.TEXTAREATAG, type: FieldType.TEXTAREATAG,
placeholder: '补充性别偏好、特殊要求和注意事项等信息', placeholder: '补充性别偏好、特殊要求和注意事项等信息',
required: true, required: true,
props: {
maxLength: 1000,
},
options:[ options:[
{ label: '仅限男生', value: '仅限男生' }, { label: '仅限男生', value: '仅限男生' },
{ label: '仅限女生', value: '仅限女生' }, { label: '仅限女生', value: '仅限女生' },

View File

@@ -1,5 +1,5 @@
import React, { useState, useCallback, useEffect } from 'react' import React, { useState, useCallback, useEffect } from 'react'
import { View, Text, Input, Image, Picker } from '@tarojs/components' import { View, Text, Input, Image } from '@tarojs/components'
import PopupGameplay from '../PopupGameplay' import PopupGameplay from '../PopupGameplay'
import img from '@/config/images'; import img from '@/config/images';
import { FormFieldConfig } from '@/config/formSchema/publishBallFormSchema'; import { FormFieldConfig } from '@/config/formSchema/publishBallFormSchema';
@@ -65,10 +65,52 @@ const FormBasicInfo: React.FC<FormBasicInfoProps> = ({
}) })
setShowStadiumSelector(false) setShowStadiumSelector(false)
} }
const handleChange = useCallback((key: string, costValue: any) => {
// 价格输入限制¥0.009999.99
console.log(costValue, 'valuevalue');
const handleChange = useCallback((key: string, value: any) => { if (key === children[0]?.prop) {
onChange({...value, [key]: value}) // 允许清空
}, [onChange]) if (costValue === '') {
onChange({...value, [key]: ''});
return;
}
// 只允许数字和一个小数点
const filteredValue = costValue.replace(/[^\d.]/g, '');
// 确保只有一个小数点
const parts = filteredValue.split('.');
if (parts.length > 2) {
return; // 不更新,保持原值
}
// 限制小数点后最多2位
if (parts.length === 2 && parts[1].length > 2) {
return; // 不更新,保持原值
}
const numValue = parseFloat(filteredValue);
if (isNaN(numValue)) {
onChange({...value, [key]: ''});
return;
}
if (numValue < 0) {
onChange({...value, [key]: '0'});
return;
}
if (numValue > 9999.99) {
onChange({...value, [key]: '9999.99'});
return;
}
// 使用过滤后的值
onChange({...value, [key]: filteredValue});
return;
}
onChange({...value, [key]: costValue})
}, [onChange, children])
useEffect(() => { useEffect(() => {
if (children.length > 2) { if (children.length > 2) {
@@ -76,6 +118,10 @@ const FormBasicInfo: React.FC<FormBasicInfoProps> = ({
setPlayGame(options) setPlayGame(options)
} }
}, [children]) }, [children])
useEffect(() => {
console.log(value, 'valuevalue');
}, [value])
const renderChildren = () => { const renderChildren = () => {
return children.map((child: any, index: number) => { return children.map((child: any, index: number) => {
return <View className='form-item'> return <View className='form-item'>
@@ -91,6 +137,7 @@ const FormBasicInfo: React.FC<FormBasicInfoProps> = ({
placeholder='请输入' placeholder='请输入'
placeholderClass='title-placeholder' placeholderClass='title-placeholder'
type='digit' type='digit'
maxlength={7}
value={value[child.prop]} value={value[child.prop]}
onInput={(e) => handleChange(child.prop, e.detail.value)} onInput={(e) => handleChange(child.prop, e.detail.value)}
/> />

View File

@@ -1,5 +1,5 @@
import React, { useState } from 'react' import React, { useCallback, useState } from 'react'
import { View, Text } from '@tarojs/components' import { View, Text, Input } from '@tarojs/components'
import { Checkbox } from '@nutui/nutui-react-taro' import { Checkbox } from '@nutui/nutui-react-taro'
import styles from './index.module.scss' import styles from './index.module.scss'
interface FormSwitchProps { interface FormSwitchProps {
@@ -10,7 +10,14 @@ interface FormSwitchProps {
} }
const FormSwitch: React.FC<FormSwitchProps> = ({ value, onChange, subTitle, wechatId }) => { const FormSwitch: React.FC<FormSwitchProps> = ({ value, onChange, subTitle, wechatId }) => {
const [editWechat, setEditWechat] = useState(false)
const editWechatId = () => {
}
const setWechatId = useCallback((e: any) => {
const value = e.target.value
onChange(value)
}, [])
return ( return (
<> <>
<View className={styles['wechat-contact-section']}> <View className={styles['wechat-contact-section']}>
@@ -28,7 +35,14 @@ const FormSwitch: React.FC<FormSwitchProps> = ({ value, onChange, subTitle, wech
wechatId && ( wechatId && (
<View className={styles['wechat-contact-id']}> <View className={styles['wechat-contact-id']}>
<Text className={styles['wechat-contact-text']}>: {wechatId.replace(/(\d{3})(\d{4})(\d{4})/, '$1 $2 $3')}</Text> <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 className={styles['wechat-contact-edit']} onClick={editWechatId}></View>
</View>
)
}
{
editWechat && (
<View className={styles['wechat-contact-edit']}>
<Input value={wechatId} onInput={setWechatId} />
</View> </View>
) )
} }

View File

@@ -184,6 +184,9 @@
border: 1px solid rgba(0, 0, 0, 0.06); border: 1px solid rgba(0, 0, 0, 0.06);
background: #000; background: #000;
box-shadow: 0 8px 64px 0 rgba(0, 0, 0, 0.10); box-shadow: 0 8px 64px 0 rgba(0, 0, 0, 0.10);
&.submit-btn-disabled {
color: rgba(255, 255, 255, 0.30);
}
} }
.submit-tip { .submit-tip {
@@ -192,12 +195,21 @@
color: #999; color: #999;
line-height: 1.4; line-height: 1.4;
display: flex; display: flex;
justify-content: center; justify-content: center;
padding: 12px 0; padding: 12px 0;
align-items: center;
.link { .link {
color: #007AFF; color: #007AFF;
} }
} }
.submit-checkbox {
width: 11px;
height: 11px;
:global(.nut-icon-Checked){
background: rgba(22, 24, 35, 0.75)!important;
}
}
} }
// 加载状态遮罩保持原样 // 加载状态遮罩保持原样

View File

@@ -1,11 +1,11 @@
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { View, Text, Button, Image } from '@tarojs/components' import { View, Text, Button, Image } from '@tarojs/components'
import { Checkbox } from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro' import Taro from '@tarojs/taro'
import { type ActivityType } from '../../components/ActivityTypeSwitch' import { type ActivityType } from '../../components/ActivityTypeSwitch'
// import ActivityTypeSwitch, { type ActivityType } from '../../components/ActivityTypeSwitch'
import CommonDialog from '../../components/CommonDialog' import CommonDialog from '../../components/CommonDialog'
import PublishForm from './publishForm' import PublishForm from './publishForm'
import { publishBallFormSchema } from '../../config/formSchema/publishBallFormSchema'; import { FormFieldConfig, 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, delay } from '@/utils'; import { getNextHourTime, getEndTime, delay } from '@/utils';
@@ -14,7 +14,7 @@ import styles from './index.module.scss'
const defaultFormData: PublishBallFormData = { const defaultFormData: PublishBallFormData = {
title: '', title: '',
image_list: ['https://static-o.oss-cn-shenzhen.aliyuncs.com/images/tpbj/tpss10.jpg'], image_list: [],
timeRange: { timeRange: {
start_time: getNextHourTime(), start_time: getNextHourTime(),
end_time: getEndTime(getNextHourTime()) end_time: getEndTime(getNextHourTime())
@@ -33,7 +33,7 @@ const defaultFormData: PublishBallFormData = {
venue_description: '', venue_description: '',
venue_image_list: [], venue_image_list: [],
}, },
players: [1, 4], players: [1, 1],
skill_level: [1.0, 5.0], skill_level: [1.0, 5.0],
descriptionInfo: { descriptionInfo: {
description: '', description: '',
@@ -46,32 +46,13 @@ const defaultFormData: PublishBallFormData = {
const PublishBall: React.FC = () => { const PublishBall: React.FC = () => {
const [activityType, setActivityType] = useState<ActivityType>('individual') const [activityType, setActivityType] = useState<ActivityType>('individual')
const [isSubmitDisabled, setIsSubmitDisabled] = useState(false)
// 获取页面参数并设置导航标题 // 获取页面参数并设置导航标题
useEffect(() => { const [optionsConfig, setOptionsConfig] = useState<FormFieldConfig[]>(publishBallFormSchema)
const currentInstance = Taro.getCurrentInstance()
const params = currentInstance.router?.params
if (params?.type) {
const type = params.type as ActivityType
if (type === 'individual' || type === 'group') {
setActivityType(type)
// 根据type设置导航标题
if (type === 'group') {
Taro.setNavigationBarTitle({
title: '发布畅打活动'
})
} else {
Taro.setNavigationBarTitle({
title: '发布'
})
}
}
handleActivityTypeChange(type)
}
}, [])
const [formData, setFormData] = useState<PublishBallFormData[]>([ const [formData, setFormData] = useState<PublishBallFormData[]>([
defaultFormData defaultFormData
]) ])
const [checked, setChecked] = useState(true)
// 删除确认弹窗状态 // 删除确认弹窗状态
const [deleteConfirm, setDeleteConfirm] = useState<{ const [deleteConfirm, setDeleteConfirm] = useState<{
@@ -103,7 +84,29 @@ const PublishBall: React.FC = () => {
} }
} }
// 检查相邻两组数据是否相同
const checkAdjacentDataSame = (formDataArray: PublishBallFormData[]) => {
if (formDataArray.length < 2) return false
const lastIndex = formDataArray.length - 1
const secondLastIndex = formDataArray.length - 2
const lastData = formDataArray[lastIndex]
const secondLastData = formDataArray[secondLastIndex]
// 比较关键字段是否相同
return (JSON.stringify(lastData) === JSON.stringify(secondLastData))
}
const handleAdd = () => { const handleAdd = () => {
// 检查最后两组数据是否相同
if (checkAdjacentDataSame(formData)) {
Taro.showToast({
title: '信息不可与前序场完全一致',
icon: 'none'
})
return
}
const newStartTime = getNextHourTime() const newStartTime = getNextHourTime()
setFormData(prev => [...prev, { setFormData(prev => [...prev, {
...defaultFormData, ...defaultFormData,
@@ -114,6 +117,7 @@ const PublishBall: React.FC = () => {
} }
}]) }])
} }
// 复制上一场数据 // 复制上一场数据
const handleCopyPrevious = (index: number) => { const handleCopyPrevious = (index: number) => {
@@ -124,7 +128,7 @@ const PublishBall: React.FC = () => {
return newData return newData
}) })
Taro.showToast({ Taro.showToast({
title: '复制上一场数据', title: '复制上一场填入',
icon: 'success' icon: 'success'
}) })
} }
@@ -158,42 +162,59 @@ const PublishBall: React.FC = () => {
} }
} }
const validateFormData = (formData: PublishBallFormData) => { const validateFormData = (formData: PublishBallFormData, isOnSubmit: boolean = false) => {
const { activityInfo, image_list, title } = formData; const { activityInfo, image_list, title } = formData;
const { play_type, price, location_name } = activityInfo; const { play_type, price, location_name } = activityInfo;
if (!image_list?.length) { if (!image_list?.length) {
Taro.showToast({ if (!isOnSubmit) {
title: `请上传活动封面`, Taro.showToast({
icon: 'none' title: `请上传活动封面`,
}) icon: 'none'
})
}
return false return false
} }
if (!title) { if (!title) {
Taro.showToast({ if (!isOnSubmit) {
title: `请输入活动标题`, Taro.showToast({
icon: 'none' title: `请输入活动标题`,
}) icon: 'none'
})
}
return false return false
} }
if (!price || (typeof price === 'number' && price <= 0) || (typeof price === 'string' && !price.trim())) { if (!price || (typeof price === 'number' && price <= 0) || (typeof price === 'string' && !price.trim())) {
Taro.showToast({ if (!isOnSubmit) {
title: `请输入费用`, Taro.showToast({
icon: 'none' title: `请输入费用`,
}) icon: 'none'
})
}
return false return false
} }
if (!play_type || !play_type.trim()) { if (!play_type || !play_type.trim()) {
Taro.showToast({ if (!isOnSubmit) {
title: `请选择玩法类型`, Taro.showToast({
icon: 'none' title: `请选择玩法类型`,
}) icon: 'none'
})
}
return false return false
} }
if (!location_name || !location_name.trim()) { if (!location_name || !location_name.trim()) {
Taro.showToast({ if (!isOnSubmit) {
title: `请选择场地`, Taro.showToast({
icon: 'none' title: `请选择场地`,
}) icon: 'none'
})
}
return false
}
return true
}
const validateOnSubmit = () => {
const isValid = activityType === 'individual' ? validateFormData(formData[0], true) : formData.every(item => validateFormData(item, true))
if (!isValid) {
return false return false
} }
return true return true
@@ -208,7 +229,7 @@ const PublishBall: React.FC = () => {
if (!isValid) { if (!isValid) {
return return
} }
const { activityInfo, descriptionInfo, timeRange, players, skill_level, ...rest } = formData[0]; const { activityInfo, descriptionInfo, timeRange, players, skill_level,image_list, ...rest } = formData[0];
const options = { const options = {
...rest, ...rest,
...activityInfo, ...activityInfo,
@@ -217,7 +238,8 @@ const PublishBall: React.FC = () => {
max_players: players[1], max_players: players[1],
current_players: players[0], current_players: players[0],
skill_level_min: skill_level[0], skill_level_min: skill_level[0],
skill_level_max: skill_level[1] skill_level_max: skill_level[1],
image_list: image_list.map(item => item.url)
} }
const res = await PublishService.createPersonal(options); const res = await PublishService.createPersonal(options);
if (res.code === 0 && res.data) { if (res.code === 0 && res.data) {
@@ -244,9 +266,16 @@ const PublishBall: React.FC = () => {
if (!isValid) { if (!isValid) {
return return
} }
formData.forEach(async (item) => { if (checkAdjacentDataSame(formData)) {
Taro.showToast({
title: '信息不可与前序场完全一致',
icon: 'none'
})
return
}
const options = formData.map((item) => {
const { activityInfo, descriptionInfo, timeRange, players, skill_level, ...rest } = item; const { activityInfo, descriptionInfo, timeRange, players, skill_level, ...rest } = item;
const options = { return {
...rest, ...rest,
...activityInfo, ...activityInfo,
...descriptionInfo, ...descriptionInfo,
@@ -254,21 +283,89 @@ const PublishBall: React.FC = () => {
max_players: players[1], max_players: players[1],
current_players: players[0], current_players: players[0],
skill_level_min: skill_level[0], skill_level_min: skill_level[0],
skill_level_max: skill_level[1] skill_level_max: skill_level[1],
} image_list: item.image_list.map(img => img.url)
const res = await PublishService.create_play_pmoothly(options);
if (res.code === 0 && res.data) {
Taro.showToast({
title: '发布成功',
icon: 'success'
})
} }
}) })
const res = await PublishService.create_play_pmoothlys({rows: options});
if (res.code === 0 && res.data) {
Taro.showToast({
title: '发布成功',
icon: 'success'
})
delay(1000)
// 如果是个人球局,则跳转到详情页,并自动分享
// 如果是畅打,则跳转第一个球局详情页,并自动分享 @刘杰
Taro.navigateTo({
// @ts-expect-error: id
url: `/pages/detail/index?id=${res.data?.[0].id || 1}&from=publish&autoShare=1`
})
} else {
Taro.showToast({
title: res.message,
icon: 'none'
})
}
} }
} }
const initFormData = () => {
const currentInstance = Taro.getCurrentInstance()
const params = currentInstance.router?.params
if (params?.type) {
const type = params.type as ActivityType
if (type === 'individual' || type === 'group') {
setActivityType(type)
if (type === 'group') {
const newFormSchema = publishBallFormSchema.reduce((acc, item) => {
if (item.prop === 'is_wechat_contact') {
return acc
}
if (item.prop === 'image_list') {
if (item.props) {
item.props.source = ['album', 'history']
}
}
if (item.prop === 'players') {
if (item.props) {
item.props.max = 100
}
}
acc.push(item)
return acc
}, [] as FormFieldConfig[])
setOptionsConfig(newFormSchema)
setFormData([defaultFormData])
}
// 根据type设置导航标题
if (type === 'group') {
Taro.setNavigationBarTitle({
title: '发布畅打活动'
})
} else {
Taro.setNavigationBarTitle({
title: '发布'
})
}
}
handleActivityTypeChange(type)
}
}
const onCheckedChange = (checked: boolean) => {
setChecked(checked)
}
useEffect(() => { useEffect(() => {
const isValid = validateOnSubmit()
if (!isValid) {
setIsSubmitDisabled(true)
} else {
setIsSubmitDisabled(false)
}
console.log(formData, 'formData'); console.log(formData, 'formData');
}, [formData])
useEffect(() => {
initFormData()
}, []) }, [])
return ( return (
@@ -314,7 +411,7 @@ const PublishBall: React.FC = () => {
<PublishForm <PublishForm
formData={item} formData={item}
onChange={(key, value) => updateFormData(key, value, index)} onChange={(key, value) => updateFormData(key, value, index)}
optionsConfig={publishBallFormSchema} optionsConfig={optionsConfig}
/> />
</View> </View>
)) ))
@@ -339,16 +436,31 @@ const PublishBall: React.FC = () => {
contentTitle="确认移除该场次?" contentTitle="确认移除该场次?"
contentDesc="该操作不可恢复" contentDesc="该操作不可恢复"
/> />
{/* 完成按钮 */} {/* 完成按钮 */}
<View className={styles['submit-section']}> <View className={styles['submit-section']}>
<Button className={styles['submit-btn']} onClick={handleSubmit}> <Button className={`${styles['submit-btn']} ${isSubmitDisabled ? styles['submit-btn-disabled'] : ''}`} onClick={handleSubmit}>
</Button> </Button>
<Text className={styles['submit-tip']}> {
activityType === 'individual' && (
<Text className={styles['link']}></Text> <Text className={styles['submit-tip']}>
</Text>
<Text className={styles['link']}></Text>
</Text>
)
}
{
activityType === 'group' && (
<View className={styles['submit-tip']}>
<Checkbox
className={styles['submit-checkbox']}
checked={checked}
onChange={onCheckedChange}
/>
</View>
)
}
</View> </View>
</View> </View>
) )

View File

@@ -31,13 +31,16 @@ const PublishForm: React.FC<{
// 字典数据相关 // 字典数据相关
const { getDictionaryValue } = useDictionaryActions() const { getDictionaryValue } = useDictionaryActions()
useEffect(() => {
setCoverImages(formData.image_list)
}, [formData.image_list])
// 处理封面图片变化 // 处理封面图片变化
const handleCoverImagesChange = (fn: (images: CoverImage[]) => CoverImage[]) => { const handleCoverImagesChange = (fn: (images: CoverImage[]) => CoverImage[]) => {
if (fn instanceof Function) { const newImages = fn instanceof Function ? fn(coverImages) : fn
setCoverImages(fn(coverImages)) setCoverImages(newImages)
} else { onChange('image_list', newImages)
setCoverImages(fn)
}
} }
// 更新表单数据 // 更新表单数据
@@ -113,12 +116,19 @@ const PublishForm: React.FC<{
return ''; return '';
} }
const getPlayersText = (players: [number, number]) => {
const [min, max] = players
return `最少${min}人,最多${max}`
}
const renderSummary = (item: FormFieldConfig) => { const renderSummary = (item: FormFieldConfig) => {
if (item.props?.showSummary) { if (item.props?.showSummary) {
if (item.prop === 'skill_level') { if (item.prop === 'skill_level') {
return <Text className={styles['section-summary']}>{getNTRPText(formData.skill_level)}</Text> return <Text className={styles['section-summary']}>{getNTRPText(formData.skill_level)}</Text>
} }
return <Text className={styles['section-summary']}>{item.props?.summary}</Text> if (item.prop === 'players') {
return <Text className={styles['section-summary']}>{getPlayersText(formData.players)}</Text>
}
} }
return null return null
} }

View File

@@ -25,8 +25,8 @@ export interface PublishBallData {
skill_level_max: number // 水平要求(NTRP) skill_level_max: number // 水平要求(NTRP)
description: string // 备注 description: string // 备注
description_tag: string[] // 备注标签 description_tag: string[] // 备注标签
is_substitute_supported: boolean // 是否支持替补 is_substitute_supported?: boolean // 是否支持替补
is_wechat_contact: boolean // 是否需要微信联系 is_wechat_contact?: boolean // 是否需要微信联系
wechat_contact?: string // 微信联系 wechat_contact?: string // 微信联系
} }
@@ -123,8 +123,8 @@ class PublishService {
showLoading: false }) showLoading: false })
} }
// 畅打发布 // 畅打发布
async create_play_pmoothly(data: PublishBallData): Promise<ApiResponse<Response>> { async create_play_pmoothlys(data: {rows: PublishBallData[]}): Promise<ApiResponse<Response>> {
return httpService.post('/games/create_play_pmoothly', data, { return httpService.post('/games/create_play_pmoothlys', data, {
showLoading: true, showLoading: true,
loadingText: '发布中...' loadingText: '发布中...'
}) })