From d61243c887540926d4275cb43efefb50266770e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AD=B1=E9=87=8E?= Date: Sun, 28 Sep 2025 22:52:45 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=8F=91=E5=B8=83=E7=90=83?= =?UTF-8?q?=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GeneralNavbar/index.module.scss | 2 +- .../AiImportPopup/AiImportPopup.tsx | 41 ++--- .../FormBasicInfo/FormBasicInfo.tsx | 4 +- .../SelectStadium/StadiumDetail.tsx | 1 + .../publishBall/index.module.scss | 21 ++- src/publish_pages/publishBall/index.tsx | 145 +++++++++++++----- src/store/keyboardStore.ts | 98 ++++++++++++ types/publishBall.ts | 3 +- 8 files changed, 245 insertions(+), 70 deletions(-) create mode 100644 src/store/keyboardStore.ts 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 035ef8d..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 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 b78251b..669a21c 100644 --- a/src/publish_pages/publishBall/index.tsx +++ b/src/publish_pages/publishBall/index.tsx @@ -1,4 +1,4 @@ -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' @@ -17,6 +17,7 @@ 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 = { @@ -65,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; @@ -172,8 +178,10 @@ const PublishBall: React.FC = () => { } const validateFormData = (formData: PublishBallFormData, isOnSubmit: boolean = false) => { - const { activityInfo, title, timeRange, image_list } = formData; - const { play_type, price, location_name } = activityInfo; + 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({ @@ -223,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({ @@ -241,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 @@ -269,7 +305,7 @@ const PublishBall: React.FC = () => { 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, @@ -278,12 +314,14 @@ 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 = republish === '0' ? await PublishService.gamesUpdate(options) : await PublishService.createPersonal(options); const successText = republish === '0' ? '更新成功' : '发布成功'; @@ -295,9 +333,10 @@ const PublishBall: React.FC = () => { 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({ @@ -319,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, @@ -328,10 +367,12 @@ 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 successText = republish === '0' ? '更新成功' : '发布成功'; @@ -344,9 +385,10 @@ const PublishBall: React.FC = () => { 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({ @@ -357,13 +399,17 @@ const PublishBall: React.FC = () => { } } - const mergeWithDefault = (data: any): PublishBallFormData => { - const userPhone = (userInfo as any)?.phone || '' + 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; @@ -377,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, @@ -389,16 +451,6 @@ const PublishBall: React.FC = () => { ...defaultFormData.activityInfo, ...(play_type ? { play_type } : {}), ...((price) ? { price } : {}), - ...(venue_id ? { venue_id } : {}), - ...(location_name ? { location_name } : {}), - ...(location ? { location } : {}), - ...(latitude ? { latitude } : {}), - ...(longitude ? { longitude } : {}), - ...(court_type ? { court_type } : {}), - ...(court_surface ? { court_surface } : {}), - ...(venue_description_tag ? { venue_description_tag } : {}), - ...(venue_description ? { venue_description } : {}), - ...(venue_image_list ? { venue_image_list } : {}), ...activityInfo }, descriptionInfo: { @@ -406,9 +458,12 @@ 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 } : {}), + } } @@ -483,9 +538,9 @@ const PublishBall: React.FC = () => { try { const res = await DetailService.getDetail(Number(gameId)); if (res.code === 0) { - const merged = mergeWithDefault(res.data) + const merged = mergeWithDefault(res.data, true) setFormData([merged]) - if (merged.activityInfo.play_type === '个人球局') { + if (res.data.game_type === '个人球局') { setTitleBar('发布') setActivityType('individual') } else { @@ -494,8 +549,10 @@ const PublishBall: React.FC = () => { } } } catch (e) { - if (e.message === '球局不存在') { - } + Taro.showToast({ + title: e.message, + icon: 'none' + }) } }; const onCheckedChange = (checked: boolean) => { @@ -515,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/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/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