Merge remote-tracking branch 'origin/publish_hanxia'

This commit is contained in:
筱野
2025-09-28 22:56:50 +08:00
10 changed files with 311 additions and 81 deletions

View File

@@ -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);

View File

@@ -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<AiImportPopupProps> = ({
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<AiImportPopupProps> = ({
setUploadFailCount(0)
setLoading(false)
setUploadLoading(false)
setKeyboardHeight(0)
setIsKeyboardVisible(false)
}
const handlePasteAndRecognize = async () => {
if (text) {
@@ -109,31 +109,20 @@ const AiImportPopup: React.FC<AiImportPopupProps> = ({
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<AiImportPopupProps> = ({
className={styles.textArea}
value={text}
onInput={handleTextChange}
onFocus={() => setIsKeyboardVisible(true)}
onBlur={() => setIsKeyboardVisible(false)}
onFocus={() => {}}
onBlur={() => {}}
placeholder="在此「粘贴识别」或输入文本,智能拆分球局时间、费用、地点和其他信息,并帮你智能生成球局标题"
maxlength={100}
showConfirmBar={false}

View File

@@ -50,9 +50,9 @@ const FormBasicInfo: React.FC<FormBasicInfoProps> = ({
// 处理场馆选择
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,

View File

@@ -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<StadiumDetailRef, StadiumDetailProps>(({
description:{
description: '',
description_tag: []
}
},
venue_id: stadium.id
})
// 暴露方法给父组件

View File

@@ -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;
}

View File

@@ -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<FormFieldConfig[]>(publishBallFormSchema)
console.log(userInfo, 'userInfo');
const [formData, setFormData] = useState<PublishBallFormData[]>([defaultFormData])
const [checked, setChecked] = useState(true)
const [titleBar, setTitleBar] = useState('发布')
const scrollViewRef = useRef<any>(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 { activityInfo, title, timeRange, image_list, players, current_players } = formData;
const { play_type, price, location_name } = activityInfo;
// if (!image_list?.length) {
// if (!isOnSubmit) {
// Taro.showToast({
// title: `请上传活动封面`,
// icon: 'none'
// })
// }
// return false
// }
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,12 +458,14 @@ 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) => {
@@ -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,8 +572,26 @@ 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 (
<View>
<View className={`${styles['publish-ball-container']} ${isKeyboardVisible ? styles['publish-ball-container-keyboard'] : ''}`} style={{ bottom: isKeyboardVisible ? `${keyboardHeight - 124}px` : 0 }}>
<GeneralNavbar title={titleBar} backgroundColor="#FAFAFA" className={styles['publish-ball-navbar']} />
<View className={styles['publish-ball']} style={{ paddingTop: `${statusNavbarHeightInfo.totalHeight}px` }}>
{/* 活动类型切换 */}
@@ -483,7 +602,7 @@ const PublishBall: React.FC = () => {
/> */}
</View>
<View className={styles['publish-ball__scroll']} style={{ height: `calc(100vh - ${statusNavbarHeightInfo.totalHeight+120}px)` }}>
<View className={styles['publish-ball__scroll']} style={{ height: `calc(100vh - ${statusNavbarHeightInfo.totalHeight+120}px)`, overflow: 'auto' }}>
{
formData.map((item, index) => (
<View key={index}>

View File

@@ -154,6 +154,12 @@ class PublishService {
showToast: false,
})
}
async gamesUpdate(data: PublishBallData): Promise<ApiResponse<createGameData>> {
return httpService.post('/games/update', data, {
showLoading: true,
loadingText: '发布中...'
})
}
async getPictures(req) {
const { type, tag, otherReq = {} } = req
if (type === 'history') {

View File

@@ -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<KeyboardStore>((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
}
}

View File

@@ -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;

View File

@@ -35,5 +35,6 @@ export interface PublishBallFormData {
is_wechat_contact: boolean // 是否需要微信联系
default_wechat_contact: string // 默认微信联系
wechat_contact: string // 微信联系
}
},
[key: string]: any
}