diff --git a/src/components/GeneralNavbar/index.module.scss b/src/components/GeneralNavbar/index.module.scss index aa3d236..f1f1cdb 100644 --- a/src/components/GeneralNavbar/index.module.scss +++ b/src/components/GeneralNavbar/index.module.scss @@ -2,7 +2,7 @@ position: fixed; top: 0; left: 0; - z-index: 999; + z-index: 9; width: 100%; background-color: #FAFAFA; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); diff --git a/src/publish_pages/publishBall/components/AiImportPopup/AiImportPopup.tsx b/src/publish_pages/publishBall/components/AiImportPopup/AiImportPopup.tsx index c6bb329..a4bf51c 100644 --- a/src/publish_pages/publishBall/components/AiImportPopup/AiImportPopup.tsx +++ b/src/publish_pages/publishBall/components/AiImportPopup/AiImportPopup.tsx @@ -6,6 +6,7 @@ import styles from './index.module.scss' import uploadFiles from '@/services/uploadFiles' import publishService from '@/services/publishService' import { usePublishBallActions } from '@/store/publishBallStore' +import { useKeyboardHeight } from '@/store/keyboardStore' import images from '@/config/images' export interface AiImportPopupProps { @@ -23,13 +24,14 @@ const AiImportPopup: React.FC = ({ const [uploadFailCount, setUploadFailCount] = useState(0) const [loading, setLoading] = useState(false) const [uploadLoading, setUploadLoading] = useState(false) - const [keyboardHeight, setKeyboardHeight] = useState(0) - const [isKeyboardVisible, setIsKeyboardVisible] = useState(false) const maxFailCount = 3 // 获取 actions(在组件顶层调用 Hook) const { setPublishData } = usePublishBallActions() + // 使用全局键盘状态 + const { keyboardHeight, isKeyboardVisible, addListener, initializeKeyboardListener } = useKeyboardHeight() + const textIdentification = async (text: string) => { setLoading(true) const res = await publishService.extract_tennis_activity({text}) @@ -51,8 +53,6 @@ const AiImportPopup: React.FC = ({ setUploadFailCount(0) setLoading(false) setUploadLoading(false) - setKeyboardHeight(0) - setIsKeyboardVisible(false) } const handlePasteAndRecognize = async () => { if (text) { @@ -109,31 +109,20 @@ const AiImportPopup: React.FC = ({ setText(e.detail.value) } - // 监听键盘高度变化,保持弹窗贴合底部 + // 使用全局键盘状态监听 useEffect(() => { - Taro.onKeyboardHeightChange?.((res: any) => { - const height = Number(res?.height || 0) - if (height > 0) { - setIsKeyboardVisible(true) - setKeyboardHeight(height) - } else { - setIsKeyboardVisible(false) - setKeyboardHeight(0) - } + // 初始化全局键盘监听器 + initializeKeyboardListener() + + // 添加本地监听器 + const removeListener = addListener((height, visible) => { + console.log('AiImportPopup 收到键盘变化:', height, visible) }) return () => { - // Taro 里 onKeyboardHeightChange 返回的不是取消函数,这里通过置零兜底 - setIsKeyboardVisible(false) - setKeyboardHeight(0) - // 微信小程序环境可调用 offKeyboardHeightChange,如存在则尝试注销 - // @ts-ignore - if (typeof Taro.offKeyboardHeightChange === 'function') { - // @ts-ignore - Taro.offKeyboardHeightChange() - } + removeListener() } - }, []) + }, [initializeKeyboardListener, addListener]) const handleImageRecognition = async () => { try { @@ -217,8 +206,8 @@ const AiImportPopup: React.FC = ({ className={styles.textArea} value={text} onInput={handleTextChange} - onFocus={() => setIsKeyboardVisible(true)} - onBlur={() => setIsKeyboardVisible(false)} + onFocus={() => {}} + onBlur={() => {}} placeholder="在此「粘贴识别」或输入文本,智能拆分球局时间、费用、地点和其他信息,并帮你智能生成球局标题" maxlength={100} showConfirmBar={false} diff --git a/src/publish_pages/publishBall/components/FormBasicInfo/FormBasicInfo.tsx b/src/publish_pages/publishBall/components/FormBasicInfo/FormBasicInfo.tsx index be08e55..72d2126 100644 --- a/src/publish_pages/publishBall/components/FormBasicInfo/FormBasicInfo.tsx +++ b/src/publish_pages/publishBall/components/FormBasicInfo/FormBasicInfo.tsx @@ -50,9 +50,9 @@ const FormBasicInfo: React.FC = ({ // 处理场馆选择 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 || {}; + const { address, name, venue_id, latitude, longitude, court_type, court_surface, description, description_tag, venue_image_list} = stadium || {}; onChange({...value, - venue_id: stadium?.id, + venue_id, location_name: name, location: address, latitude, diff --git a/src/publish_pages/publishBall/components/SelectStadium/StadiumDetail.tsx b/src/publish_pages/publishBall/components/SelectStadium/StadiumDetail.tsx index 92d6083..fbfa3b9 100644 --- a/src/publish_pages/publishBall/components/SelectStadium/StadiumDetail.tsx +++ b/src/publish_pages/publishBall/components/SelectStadium/StadiumDetail.tsx @@ -10,6 +10,7 @@ import './StadiumDetail.scss' export interface Stadium { id?: string + venue_id?: string name: string address?: string longitude?: number @@ -108,7 +109,8 @@ const StadiumDetail = forwardRef(({ description:{ description: '', description_tag: [] - } + }, + venue_id: stadium.id }) // 暴露方法给父组件 diff --git a/src/publish_pages/publishBall/index.module.scss b/src/publish_pages/publishBall/index.module.scss index 51b0a2a..2ac832d 100644 --- a/src/publish_pages/publishBall/index.module.scss +++ b/src/publish_pages/publishBall/index.module.scss @@ -1,5 +1,15 @@ @use '~@/scss/themeColor.scss' as theme; +.publish-ball-container{ + position: relative; + &.publish-ball-container-keyboard{ + position: absolute; + bottom: 0; + left: 0; + right: 0; + z-index: 9999; + } +} .publish-ball { padding-top: 0; min-height: 100vh; @@ -167,12 +177,8 @@ // 提交区域 .submit-section { - position: fixed; - bottom: 0; - left: 0; - right: 0; - padding: 16px; + padding: 16px; .submit-btn { width: 100%; color: white; @@ -260,5 +266,10 @@ } .publish-ball-navbar{ + position: fixed !important; + top: 0 !important; + left: 0 !important; + z-index: 9999 !important; + width: 100% !important; box-shadow: none!important; } \ No newline at end of file diff --git a/src/publish_pages/publishBall/index.tsx b/src/publish_pages/publishBall/index.tsx index a39804c..669a21c 100644 --- a/src/publish_pages/publishBall/index.tsx +++ b/src/publish_pages/publishBall/index.tsx @@ -1,6 +1,7 @@ -import React, { useState, useEffect } from 'react' +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' @@ -15,8 +16,9 @@ import GeneralNavbar from "@/components/GeneralNavbar" import images from '@/config/images' import { useUserInfo } from '@/store/userStore' import styles from './index.module.scss' -import dayjs from 'dayjs' import { usePublishBallData } from '@/store/publishBallStore' +import { useKeyboardHeight } from '@/store/keyboardStore' +import DetailService from "@/services/detailService"; const defaultFormData: PublishBallFormData = { title: '', @@ -64,12 +66,17 @@ const PublishBall: React.FC = () => { 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('发布') + const scrollViewRef = useRef(null) + // 删除确认弹窗状态 const [deleteConfirm, setDeleteConfirm] = useState<{ visible: boolean; @@ -171,17 +178,19 @@ const PublishBall: React.FC = () => { } const validateFormData = (formData: PublishBallFormData, isOnSubmit: boolean = false) => { - const { activityInfo, title, timeRange } = formData; - const { play_type, price, location_name } = activityInfo; - // if (!image_list?.length) { - // if (!isOnSubmit) { - // Taro.showToast({ - // title: `请上传活动封面`, - // icon: 'none' - // }) - // } - // return 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 (!title) { if (!isOnSubmit) { Taro.showToast({ @@ -222,6 +231,7 @@ const PublishBall: React.FC = () => { 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({ @@ -240,6 +250,33 @@ const PublishBall: React.FC = () => { } 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 @@ -252,16 +289,23 @@ const PublishBall: React.FC = () => { 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, timeRange, players, skill_level, image_list, wechat, ...rest } = formData[0]; + 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, @@ -270,25 +314,29 @@ const PublishBall: React.FC = () => { ...timeRange, max_players: max, min_players: min, - organizer_joined, + 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, + 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 = await PublishService.createPersonal(options); + 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: '发布成功', + 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=${(res as any).data?.id || 1}&from=publish&autoShare=1` + url: `/game_pages/detail/index?id=${id || 1}&from=publish&autoShare=1` }) } else { Taro.showToast({ @@ -310,7 +358,7 @@ const PublishBall: React.FC = () => { return } const options = formData.map((item) => { - const { activityInfo, descriptionInfo, timeRange, players, skill_level, ...rest } = item; + const { activityInfo, descriptionInfo, timeRange, players, skill_level, is_substitute_supported, id, ...rest } = item; const { min, max, organizer_joined } = players; return { ...rest, @@ -319,24 +367,28 @@ const PublishBall: React.FC = () => { ...timeRange, max_players: max, min_players: min, - organizer_joined, + organizer_joined: organizer_joined === true ? 1 : 0, skill_level_min: skill_level[0], skill_level_max: skill_level[1], - image_list: item.image_list.map(img => img.url) + is_substitute_supported: is_substitute_supported ? '1' : '0', + image_list: item.image_list.map(img => img.url), + ...(republish === '0' ? { id } : {}), } }) - const res = await PublishService.create_play_pmoothlys({rows: options}); + 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: '发布成功', + 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=${(res as any).data?.[0]?.id || 1}&from=publish&autoShare=1` + url: `/game_pages/detail/index?id=${id || 1}&from=publish&autoShare=1` }) } else { Taro.showToast({ @@ -346,12 +398,18 @@ const PublishBall: React.FC = () => { } } } - const mergeWithDefault = (data: any): PublishBallFormData => { - const userPhone = (userInfo as any)?.phone || '' - const { start_time, end_time, play_type, price, + + 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 + 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; @@ -365,9 +423,25 @@ const PublishBall: React.FC = () => { 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, - ...data, + title, + ...(is_substitute_supported === '0' ? { is_substitute_supported: false } : {}), + ...(publish_id ? { id: publish_id } : {}), timeRange: { ...defaultFormData.timeRange, start_time, @@ -384,13 +458,15 @@ const PublishBall: React.FC = () => { ...(description ? { description } : {}), ...(description_tag ? { description_tag } : {}), }, - ...(skill_level_max && skill_level_min ? { skill_level: [skill_level_min, skill_level_max] } : {}), - ...(max_players && min_players ? { players: { min: min_players, max: max_players, organizer_joined: true } } : {}), - wechat: { ...defaultFormData.wechat, default_wechat_contact: userPhone } + ...(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') { @@ -412,8 +488,7 @@ const PublishBall: React.FC = () => { setOptionsConfig(newFormSchema) } const initFormData = () => { - const currentInstance = Taro.getCurrentInstance() - const params = currentInstance.router?.params + const params = getParams() const userPhone = (userInfo as any)?.phone || '' if (params?.type) { const type = params.type as ActivityType @@ -429,8 +504,6 @@ const PublishBall: React.FC = () => { } } else if (type === 'ai') { // 从 Store 注入 AI 生成的表单 JSON - - if (publishAiData && Array.isArray(publishAiData) && publishAiData.length > 0) { Taro.showToast({ title: '智能识别成功,请完善剩余信息', @@ -453,7 +526,35 @@ const PublishBall: React.FC = () => { } } } + 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) } @@ -471,10 +572,28 @@ const PublishBall: React.FC = () => { initFormData() }, []) +// 使用全局键盘状态监听 +useEffect(() => { + // 初始化全局键盘监听器 + initializeKeyboardListener() + + // 添加本地监听器 + const removeListener = addListener((height, visible) => { + console.log('PublishBall 收到键盘变化:', height, visible) + + }) + + return () => { + removeListener() + } +}, [initializeKeyboardListener, addListener]) + + console.log(isKeyboardVisible, 'isKeyboardVisible'); + console.log(keyboardHeight, 'keyboardHeight'); return ( - + - + {/* 活动类型切换 */} {/* { /> */} - + { formData.map((item, index) => ( diff --git a/src/services/publishService.ts b/src/services/publishService.ts index 634c9aa..5041260 100644 --- a/src/services/publishService.ts +++ b/src/services/publishService.ts @@ -154,6 +154,12 @@ class PublishService { showToast: false, }) } + async gamesUpdate(data: PublishBallData): Promise> { + return httpService.post('/games/update', data, { + showLoading: true, + loadingText: '发布中...' + }) + } async getPictures(req) { const { type, tag, otherReq = {} } = req if (type === 'history') { diff --git a/src/store/keyboardStore.ts b/src/store/keyboardStore.ts new file mode 100644 index 0000000..e5bf20b --- /dev/null +++ b/src/store/keyboardStore.ts @@ -0,0 +1,98 @@ +import { create } from 'zustand' +import Taro from '@tarojs/taro' + +interface KeyboardState { + keyboardHeight: number + isKeyboardVisible: boolean + listeners: Set<(height: number, visible: boolean) => void> + isInitialized: boolean +} + +interface KeyboardActions { + setKeyboardHeight: (height: number) => void + setKeyboardVisible: (visible: boolean) => void + addListener: (listener: (height: number, visible: boolean) => void) => () => void + initializeKeyboardListener: () => void + cleanup: () => void +} + +type KeyboardStore = KeyboardState & KeyboardActions + +export const useKeyboardStore = create((set, get) => ({ + keyboardHeight: 0, + isKeyboardVisible: false, + listeners: new Set(), + isInitialized: false, + + setKeyboardHeight: (height: number) => { + set({ keyboardHeight: height }) + const { listeners } = get() + listeners.forEach(listener => listener(height, get().isKeyboardVisible)) + }, + + setKeyboardVisible: (visible: boolean) => { + set({ isKeyboardVisible: visible }) + const { listeners } = get() + listeners.forEach(listener => listener(get().keyboardHeight, visible)) + }, + + addListener: (listener: (height: number, visible: boolean) => void) => { + const { listeners } = get() + listeners.add(listener) + + // 返回取消监听的函数 + return () => { + listeners.delete(listener) + } + }, + + initializeKeyboardListener: () => { + const { isInitialized } = get() + if (isInitialized) return + + console.log('初始化全局键盘监听器') + + Taro.onKeyboardHeightChange?.((res: any) => { + const height = Number(res?.height || 0) + console.log('全局键盘高度变化:', height) + + const store = get() + if (height > 0) { + store.setKeyboardVisible(true) + store.setKeyboardHeight(height) + } else { + store.setKeyboardVisible(false) + store.setKeyboardHeight(0) + } + }) + + set({ isInitialized: true }) + }, + + cleanup: () => { + console.log('清理全局键盘监听器') + // @ts-ignore + if (typeof Taro.offKeyboardHeightChange === 'function') { + // @ts-ignore + Taro.offKeyboardHeightChange() + } + set({ + isInitialized: false, + keyboardHeight: 0, + isKeyboardVisible: false, + listeners: new Set() + }) + } +})) + +// 导出便捷的 hooks +export const useKeyboardHeight = () => { + const { keyboardHeight, isKeyboardVisible, addListener, initializeKeyboardListener } = useKeyboardStore() + + return { + keyboardHeight, + isKeyboardVisible, + addListener, + initializeKeyboardListener + } +} diff --git a/src/store/publishBallStore.ts b/src/store/publishBallStore.ts index 0f01836..8becffe 100644 --- a/src/store/publishBallStore.ts +++ b/src/store/publishBallStore.ts @@ -1,17 +1,21 @@ import { create } from "zustand"; -import { PublishBallFormData } from "../../types/publishBall"; + +type PublishData = { + game_type: string; + [key: string]: any; +} interface PublishBallState { // 待注入到发布页面的表单数据(支持单场或多场) - publishData: PublishBallFormData | PublishBallFormData[] | null; + publishData: PublishData[] | null; // 赋值/覆盖整份数据 setPublishData: ( - data: PublishBallFormData | PublishBallFormData[] | null + data: PublishData[] | null ) => void; // 读取当前数据 - getPublishData: () => PublishBallFormData | PublishBallFormData[] | null; + getPublishData: () => PublishData[] | null; // 清空 clearPublishData: () => void; diff --git a/types/publishBall.ts b/types/publishBall.ts index d1673a2..d638a2d 100644 --- a/types/publishBall.ts +++ b/types/publishBall.ts @@ -35,5 +35,6 @@ export interface PublishBallFormData { is_wechat_contact: boolean // 是否需要微信联系 default_wechat_contact: string // 默认微信联系 wechat_contact: string // 微信联系 - } + }, + [key: string]: any } \ No newline at end of file