import React, { useState, useEffect, useRef } from 'react' import { View, Text, Button, Image } from '@tarojs/components' import { Checkbox } from '@nutui/nutui-react-taro' import dayjs from 'dayjs' import Taro from '@tarojs/taro' import { type ActivityType } from '../../components/ActivityTypeSwitch' import CommonDialog from '../../components/CommonDialog' import { withAuth } from '@/components' import PublishForm from './publishForm' import { FormFieldConfig, publishBallFormSchema } from '../../config/formSchema/publishBallFormSchema'; import { PublishBallFormData } from '../../../types/publishBall'; import PublishService from '@/services/publishService'; import { getNextHourTime, getEndTime, delay } from '@/utils'; import { useGlobalState } from "@/store/global" import GeneralNavbar from "@/components/GeneralNavbar" import images from '@/config/images' import { useUserInfo } from '@/store/userStore' import styles from './index.module.scss' import { usePublishBallData } from '@/store/publishBallStore' import { useKeyboardHeight } from '@/store/keyboardStore' import DetailService from "@/services/detailService"; const defaultFormData: PublishBallFormData = { title: '', image_list: [], 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: { min: 1, max: 1, organizer_joined: true }, skill_level: [1.0, 5.0], descriptionInfo: { description: '', description_tag: [], }, is_substitute_supported: true, wechat: { is_wechat_contact: true, wechat_contact: '', default_wechat_contact: '' } } const PublishBall: React.FC = () => { const [activityType, setActivityType] = useState('individual') const [isSubmitDisabled, setIsSubmitDisabled] = useState(false) const userInfo = useUserInfo(); const publishAiData = usePublishBallData() const { statusNavbarHeightInfo } = useGlobalState(); // 使用全局键盘状态 const { keyboardHeight, isKeyboardVisible, addListener, initializeKeyboardListener } = useKeyboardHeight() // 获取页面参数并设置导航标题 const [optionsConfig, setOptionsConfig] = useState(publishBallFormSchema) console.log(userInfo, 'userInfo'); const [formData, setFormData] = useState([defaultFormData]) const [checked, setChecked] = useState(true) const [titleBar, setTitleBar] = useState('发布') // 控制是否响应全局键盘(由具体输入框 focus/blur 控制) const [shouldReactToKeyboard, setShouldReactToKeyboard] = useState(false) // 删除确认弹窗状态 const [deleteConfirm, setDeleteConfirm] = useState<{ visible: boolean; index: number; }>({ visible: false, index: -1 }) // 更新表单数据 const updateFormData = (key: keyof PublishBallFormData, value: any, index: number) => { console.log(key, value, index, 'key, value, index'); setFormData(prev => { const newData = [...prev] newData[index] = { ...newData[index], [key]: value } console.log(newData, 'newData'); return newData }) } // 检查相邻两组数据是否相同 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 = () => { // 检查最后两组数据是否相同 if (checkAdjacentDataSame(formData)) { Taro.showToast({ title: '信息不可与前序场完全一致', icon: 'none' }) return } const newStartTime = getNextHourTime() setFormData(prev => [...prev, { ...defaultFormData, title: '', timeRange: { start_time: newStartTime, end_time: getEndTime(newStartTime) } }]) } // 复制上一场数据 const handleCopyPrevious = (index: number) => { if (index > 0) { setFormData(prev => { const newData = [...prev] newData[index] = { ...newData[index - 1] } return newData }) Taro.showToast({ title: '复制上一场填入', icon: 'success' }) } } // 删除确认弹窗 const showDeleteConfirm = (index: number) => { setDeleteConfirm({ visible: true, index }) } // 关闭删除确认弹窗 const closeDeleteConfirm = () => { setDeleteConfirm({ visible: false, index: -1 }) } // 确认删除 const confirmDelete = () => { if (deleteConfirm.index >= 0) { setFormData(prev => prev.filter((_, index) => index !== deleteConfirm.index)) closeDeleteConfirm() Taro.showToast({ title: '已删除该场次', icon: 'success' }) } } const validateFormData = (formData: PublishBallFormData, isOnSubmit: boolean = false) => { const { activityInfo, title, timeRange, image_list, players, current_players } = formData; const { play_type, price, location_name } = activityInfo; const { max } = players; if (!image_list?.length && activityType === 'group') { if (!isOnSubmit) { Taro.showToast({ title: `请上传活动封面`, icon: 'none' }) } return false } // 判断图片是否上传完成 if (image_list?.length > 0) { const uploadInProgress = image_list.some((item) => item.url.startsWith("http://tmp/") ); if (uploadInProgress) { Taro.showToast({ title: `封面图片上传中...`, icon: "none", }); return; } } if (!title) { if (!isOnSubmit) { Taro.showToast({ title: `请输入活动标题`, icon: 'none' }) } return false } if (!price || (typeof price === 'number' && price <= 0) || (typeof price === 'string' && !price.trim())) { if (!isOnSubmit) { Taro.showToast({ title: `请输入费用`, icon: 'none' }) } return false } if (!play_type || !play_type.trim()) { if (!isOnSubmit) { Taro.showToast({ title: `请选择玩法类型`, icon: 'none' }) } return false } if (!location_name || !location_name.trim()) { if (!isOnSubmit) { Taro.showToast({ title: `请选择场地`, icon: 'none' }) } return false } // 时间范围校验:结束时间需晚于开始时间,且至少间隔30分钟(支持跨天) if (timeRange?.start_time && timeRange?.end_time) { const start = dayjs(timeRange.start_time) const end = dayjs(timeRange.end_time) const currentTime = dayjs() if (!end.isAfter(start)) { if (!isOnSubmit) { Taro.showToast({ title: `结束时间需晚于开始时间`, icon: 'none' }) } return false } if (end.isBefore(start.add(30, 'minute'))) { if (!isOnSubmit) { Taro.showToast({ title: `时间间隔至少30分钟`, icon: 'none' }) } return false } if (start.isBefore(currentTime)) { if (!isOnSubmit) { Taro.showToast({ title: `开始时间需晚于当前时间`, icon: 'none' }) } return false } if (end.isBefore(currentTime)) { if (!isOnSubmit) { Taro.showToast({ title: `结束时间需晚于当前时间`, icon: 'none' }) } return false } } if (current_players && (current_players > max)) { if (!isOnSubmit) { Taro.showToast({ title: `最大人数不能小于当前参与人数${current_players}`, 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 true } const getParams = () => { const currentInstance = Taro.getCurrentInstance() const params = currentInstance.router?.params return params } // 提交表单 const handleSubmit = async () => { // 基础验证 console.log(formData, 'formData'); const params = getParams() const { republish } = params || {}; if (activityType === 'individual') { const isValid = validateFormData(formData[0]) if (!isValid) { return } const { activityInfo, descriptionInfo,is_substitute_supported, timeRange, players, skill_level, image_list, wechat, id, ...rest } = formData[0]; const { min, max, organizer_joined } = players; const options = { ...rest, ...activityInfo, ...descriptionInfo, ...timeRange, max_players: max, min_players: min, organizer_joined: organizer_joined === true ? 1 : 0, skill_level_min: skill_level[0], skill_level_max: skill_level[1], image_list: image_list.map(item => item.url), is_wechat_contact: wechat.is_wechat_contact ? 1 : 0, wechat_contact: wechat.wechat_contact || wechat.default_wechat_contact, is_substitute_supported: is_substitute_supported ? '1' : '0', ...(republish === '0' ? { id } : {}), } const res = republish === '0' ? await PublishService.gamesUpdate(options) : await PublishService.createPersonal(options); const successText = republish === '0' ? '更新成功' : '发布成功'; if (res.code === 0 && res.data) { Taro.showToast({ title: successText, icon: 'success' }) delay(1000) // 如果是个人球局,则跳转到详情页,并自动分享 // 如果是畅打,则跳转第一个球局详情页,并自动分享 @刘杰 const id = (res as any).data?.id; Taro.navigateTo({ // @ts-expect-error: id url: `/game_pages/detail/index?id=${id || 1}&from=publish&autoShare=1` }) } else { Taro.showToast({ title: res.message, icon: 'none' }) } } if (activityType === 'group') { const isValid = formData.every(item => validateFormData(item)) if (!isValid) { return } if (checkAdjacentDataSame(formData)) { Taro.showToast({ title: '信息不可与前序场完全一致', icon: 'none' }) return } const options = formData.map((item) => { const { activityInfo, descriptionInfo, timeRange, players, skill_level, is_substitute_supported, id, ...rest } = item; const { min, max, organizer_joined } = players; return { ...rest, ...activityInfo, ...descriptionInfo, ...timeRange, max_players: max, min_players: min, organizer_joined: organizer_joined === true ? 1 : 0, skill_level_min: skill_level[0], skill_level_max: skill_level[1], is_substitute_supported: is_substitute_supported ? '1' : '0', image_list: item.image_list.map(img => img.url), ...(republish === '0' ? { id } : {}), } }) const successText = republish === '0' ? '更新成功' : '发布成功'; const res = republish === '0' ? await PublishService.gamesUpdate(options[0]) : await PublishService.create_play_pmoothlys({rows: options}); if (res.code === 0 && res.data) { Taro.showToast({ title: successText, icon: 'success' }) delay(1000) // 如果是个人球局,则跳转到详情页,并自动分享 // 如果是畅打,则跳转第一个球局详情页,并自动分享 @刘杰 const id = republish === '0' ? (res as any).data?.id : (res as any).data?.[0]?.id; Taro.navigateTo({ // @ts-expect-error: id url: `/game_pages/detail/index?id=${id || 1}&from=publish&autoShare=1` }) } else { Taro.showToast({ title: res.message, icon: 'none' }) } } } const mergeWithDefault = (data: any, isDetail: boolean = false): PublishBallFormData => { // ai导入与详情数据处理 const { start_time, end_time, play_type, price, venue_id, location_name, location, latitude, longitude, court_type, court_surface, venue_description_tag, venue_description, venue_image_list, description, description_tag, max_players, min_players, skill_level_max, skill_level_min, venueDtl, wechat_contact, image_list, id: publish_id, is_wechat_contact, is_substitute_supported, title, current_players, organizer_joined } = data; const level_max = skill_level_max ? Number(skill_level_max) : 5.0; const level_min = skill_level_min ? Number(skill_level_min) : 1.0; const userPhone = wechat_contact || (userInfo as any)?.phone || '' let activityInfo = {}; if (venueDtl) { const { latitude, longitude,venue_type, surface_type, facilities, name, id } = venueDtl; activityInfo = { latitude, longitude, court_type: venue_type, court_surface: surface_type, venue_description: facilities, location_name: name, venue_id: id } } if (isDetail) { activityInfo = { venue_id, location_name, location, latitude, longitude, court_type, court_surface, venue_description_tag, venue_description, venue_image_list } } return { ...defaultFormData, title, ...(is_substitute_supported === '0' ? { is_substitute_supported: false } : {}), ...(publish_id ? { id: publish_id } : {}), timeRange: { ...defaultFormData.timeRange, start_time, end_time, }, activityInfo: { ...defaultFormData.activityInfo, ...(play_type ? { play_type } : {}), ...((price) ? { price } : {}), ...activityInfo }, descriptionInfo: { ...defaultFormData.descriptionInfo, ...(description ? { description } : {}), ...(description_tag ? { description_tag } : {}), }, ...(level_max && level_min ? { skill_level: [level_min, level_max] } : {}), ...(max_players && min_players ? { players: { min: min_players, max: max_players, organizer_joined: organizer_joined === 0 ? false : true } } : {}), wechat: { ...defaultFormData.wechat, default_wechat_contact: userPhone, is_wechat_contact: is_wechat_contact === 0 ? false : true}, image_list: image_list?.map(item => ({ url: item, id: item })) || [], ...(current_players ? { current_players } : {}), } } const formatConfig = () => { const newFormSchema = publishBallFormSchema.reduce((acc, item) => { if (item.prop === 'wechat') { 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) } const initFormData = () => { const params = getParams() const userPhone = (userInfo as any)?.phone || '' if (params?.type) { const type = params.type as ActivityType if (type === 'individual' || type === 'group') { setActivityType(type) if (type === 'group') { formatConfig() setFormData([defaultFormData]) setTitleBar('发布畅打活动') } else { setTitleBar('发布') setFormData([{...defaultFormData, wechat: { ...defaultFormData.wechat, default_wechat_contact: userPhone } }]) } } else if (type === 'ai') { // 从 Store 注入 AI 生成的表单 JSON if (publishAiData && Array.isArray(publishAiData) && publishAiData.length > 0) { Taro.showToast({ title: '智能识别成功,请完善剩余信息', icon: 'none' }) const merged = publishAiData.map(item => mergeWithDefault(item)) setFormData(merged.length ? merged : [defaultFormData]) if (merged.length === 1) { setTitleBar('发布') setActivityType('individual') } else { formatConfig() setTitleBar('发布畅打活动') setActivityType('group') } } else { setFormData([defaultFormData]) setTitleBar('发布') setActivityType('individual') } } } if (params?.gameId) { getGameDetail(params.gameId) } } const getGameDetail = async (gameId) => { if (!gameId) return; try { const res = await DetailService.getDetail(Number(gameId)); if (res.code === 0) { const merged = mergeWithDefault(res.data, true) setFormData([merged]) if (res.data.game_type === '个人球局') { setTitleBar('发布') setActivityType('individual') } else { setTitleBar('发布畅打活动') setActivityType('group') } } } catch (e) { Taro.showToast({ title: e.message, icon: 'none' }) } }; const onCheckedChange = (checked: boolean) => { setChecked(checked) } useEffect(() => { const isValid = validateOnSubmit() if (!isValid) { setIsSubmitDisabled(true) } else { setIsSubmitDisabled(false) } console.log(formData, 'formData'); }, [formData]) useEffect(() => { initFormData() }, []) // 使用全局键盘状态监听 useEffect(() => { // 初始化全局键盘监听器 initializeKeyboardListener() // 添加本地监听器 const removeListener = addListener((height, visible) => { console.log('PublishBall 收到键盘变化:', height, visible) // 这里只记录或用于其他逻辑,布局是否响应交由 shouldReactToKeyboard 决定 }) return () => { removeListener() } }, [initializeKeyboardListener, addListener]) const handleAnyInputFocus = (item: FormFieldConfig, e: any) => { const { prop } = item if (prop === 'descriptionInfo') { setShouldReactToKeyboard(true) } } const handleAnyInputBlur = () => { setShouldReactToKeyboard(false) } return ( {/* 活动类型切换 */} {/* */} { formData.map((item, index) => ( {/* 场次标题行 */} {activityType === 'group' && index > 0 && ( 第{index + 1}场 showDeleteConfirm(index)} > {index > 0 && ( handleCopyPrevious(index)} > 复制上一场 )} )} updateFormData(key, value, index)} optionsConfig={optionsConfig} onAnyInputFocus={handleAnyInputFocus} onAnyInputBlur={handleAnyInputBlur} /> )) } { activityType === 'group' && ( 再添加一场 ) } {/* 删除确认弹窗 */} {/* 完成按钮 */} { activityType === 'individual' && ( 点击确定发布约球,即表示已经同意条款 Taro.navigateTo({url: '/publish_pages/footballRules/index'})}>《约球规则》 ) } { activityType === 'group' && ( 已认证 徐汇爱打球官方球场,请严格遵守签约协议 ) } ) } export default withAuth(PublishBall)