Files
mini-programs/src/publish_pages/publishBall/index.tsx

718 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<ActivityType>('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<FormFieldConfig[]>(publishBallFormSchema)
console.log(userInfo, 'userInfo');
const [formData, setFormData] = useState<PublishBallFormData[]>([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 (
<View className={`${styles['publish-ball-container']} ${isKeyboardVisible && shouldReactToKeyboard ? styles['publish-ball-container-keyboard'] : ''}`} style={{ bottom: isKeyboardVisible && shouldReactToKeyboard ? `${keyboardHeight - 124}px` : 0 }}>
<GeneralNavbar title={titleBar} backgroundColor="#FAFAFA" className={styles['publish-ball-navbar']} />
<View className={styles['publish-ball']} style={{ paddingTop: `${statusNavbarHeightInfo.totalHeight}px` }}>
{/* 活动类型切换 */}
<View className={styles['activity-type-switch']}>
{/* <ActivityTypeSwitch
value={activityType}
onChange={handleActivityTypeChange}
/> */}
</View>
<View className={styles['publish-ball__scroll']} style={{ height: `calc(100vh - ${statusNavbarHeightInfo.totalHeight+120}px)`, overflow: 'auto' }}>
{
formData.map((item, index) => (
<View key={index}>
{/* 场次标题行 */}
{activityType === 'group' && index > 0 && (
<View className={styles['session-header']}>
<View className={styles['session-title']}>
{index + 1}
<View
className={styles['session-delete']}
onClick={() => showDeleteConfirm(index)}
>
<Image src={images.ICON_DELETE} className={styles['session-delete-icon']} />
</View>
</View>
<View className={styles['session-actions']}>
{index > 0 && (
<View
className={styles['session-action-btn']}
onClick={() => handleCopyPrevious(index)}
>
</View>
)}
</View>
</View>
)}
<PublishForm
formData={item}
onChange={(key, value) => updateFormData(key, value, index)}
optionsConfig={optionsConfig}
onAnyInputFocus={handleAnyInputFocus}
onAnyInputBlur={handleAnyInputBlur}
/>
</View>
))
}
{
activityType === 'group' && (
<View className={styles['publish-ball__add']} onClick={handleAdd}>
<Image src={images.ICON_ADD} className={styles['publish-ball__add-icon']} />
</View>
)
}
</View>
{/* 删除确认弹窗 */}
<CommonDialog
visible={deleteConfirm.visible}
cancelText="再想想"
confirmText="确认移除"
onCancel={closeDeleteConfirm}
onConfirm={confirmDelete}
contentTitle="确认移除该场次?"
contentDesc="该操作不可恢复"
/>
{/* 完成按钮 */}
<View className={styles['submit-section']}>
<Button className={`${styles['submit-btn']} ${isSubmitDisabled ? styles['submit-btn-disabled'] : ''}`} onClick={handleSubmit}>
</Button>
{
activityType === 'individual' && (
<Text className={styles['submit-tip']}>
<Text className={styles['link']} onClick={() => Taro.navigateTo({url: '/publish_pages/footballRules/index'})}></Text>
</Text>
)
}
{
activityType === 'group' && (
<View className={styles['submit-tip']}>
<Checkbox
className={styles['submit-checkbox']}
checked={checked}
onChange={onCheckedChange}
/>
</View>
)
}
</View>
</View>
</View>
)
}
export default withAuth(PublishBall)