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, ); const [formData, setFormData] = useState([ defaultFormData, ]); const [checked, setChecked] = useState(true); const [publishLoading, setPublishLoading] = useState(false); 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, ) => { setFormData((prev) => { const newData = [...prev]; newData[index] = { ...newData[index], [key]: value }; 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, descriptionInfo, } = formData; const { play_type, price, location_name } = activityInfo; const { description } = descriptionInfo; 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 (title.length > 20) { if (!isOnSubmit) { Taro.showToast({ title: `标题最多可输入20个字`, 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; } if (description?.length > 200) { if (!isOnSubmit) { Taro.showToast({ title: `补充要求最多可输入200个字`, 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 () => { // 基础验证 const params = getParams(); const { republish } = params || {}; if (activityType === "individual") { const isValid = validateFormData(formData[0]); if (!isValid || publishLoading) { return; } setPublishLoading(true); 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; // 如果是编辑,就返回,否则就是新发布 if (republish === "0") { Taro.navigateBack(); } else { // 使用 redirectTo 替换当前页面,避免返回时回到发布页面 Taro.redirectTo({ // @ts-expect-error: id url: `/game_pages/detail/index?id=${ id || 1 }&from=publish&autoShare=1`, }); } } else { Taro.showToast({ title: res.message, icon: "none", }); setPublishLoading(false); } } if (activityType === "group") { const isValid = formData.every((item) => validateFormData(item)); if (!isValid || publishLoading) { return; } setPublishLoading(true); 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; // 使用 redirectTo 替换当前页面,避免返回时回到发布页面 Taro.redirectTo({ // @ts-expect-error: id url: `/game_pages/detail/index?id=${ id || 1 }&from=publish&autoShare=1`, }); } else { Taro.showToast({ title: res.message, icon: "none", }); setPublishLoading(false); } } }; 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); } }, [formData]); useEffect(() => { initFormData(); }, []); // 使用全局键盘状态监听 useEffect(() => { // 初始化全局键盘监听器 initializeKeyboardListener(); // 添加本地监听器 const removeListener = addListener(() => { // 布局是否响应交由 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);