Files
mini-programs/src/publish_pages/publishBall/index.tsx
2025-09-26 23:20:29 +08:00

576 lines
18 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 } from 'react'
import { View, Text, Button, Image } from '@tarojs/components'
import { Checkbox } from '@nutui/nutui-react-taro'
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 dayjs from 'dayjs'
import { usePublishBallData } from '@/store/publishBallStore'
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 [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 [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 } = formData;
const { play_type, price, location_name } = activityInfo;
// if (!image_list?.length) {
// if (!isOnSubmit) {
// Taro.showToast({
// title: `请上传活动封面`,
// icon: 'none'
// })
// }
// return false
// }
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)
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
}
}
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 handleSubmit = async () => {
// 基础验证
console.log(formData, 'formData');
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 { min, max, organizer_joined } = players;
const options = {
...rest,
...activityInfo,
...descriptionInfo,
...timeRange,
max_players: max,
min_players: min,
organizer_joined,
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,
wechat_contact: wechat.wechat_contact || wechat.default_wechat_contact,
}
const res = await PublishService.createPersonal(options);
if (res.code === 0 && res.data) {
Taro.showToast({
title: '发布成功',
icon: 'success'
})
delay(1000)
// 如果是个人球局,则跳转到详情页,并自动分享
// 如果是畅打,则跳转第一个球局详情页,并自动分享 @刘杰
Taro.navigateTo({
// @ts-expect-error: id
url: `/game_pages/detail/index?id=${(res as any).data?.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, ...rest } = item;
const { min, max, organizer_joined } = players;
return {
...rest,
...activityInfo,
...descriptionInfo,
...timeRange,
max_players: max,
min_players: min,
organizer_joined,
skill_level_min: skill_level[0],
skill_level_max: skill_level[1],
image_list: item.image_list.map(img => img.url)
}
})
const res = await PublishService.create_play_pmoothlys({rows: options});
if (res.code === 0 && res.data) {
Taro.showToast({
title: '发布成功',
icon: 'success'
})
delay(1000)
// 如果是个人球局,则跳转到详情页,并自动分享
// 如果是畅打,则跳转第一个球局详情页,并自动分享 @刘杰
Taro.navigateTo({
// @ts-expect-error: id
url: `/game_pages/detail/index?id=${(res as any).data?.[0]?.id || 1}&from=publish&autoShare=1`
})
} else {
Taro.showToast({
title: res.message,
icon: 'none'
})
}
}
}
const mergeWithDefault = (data: any): PublishBallFormData => {
const userPhone = (userInfo as any)?.phone || ''
const { start_time, end_time, play_type, price,
description, description_tag, max_players, min_players, skill_level_max, skill_level_min,
venueDtl
} = data;
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
}
}
return {
...defaultFormData,
...data,
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 } : {}),
},
...(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 }
}
}
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 currentInstance = Taro.getCurrentInstance()
const params = currentInstance.router?.params
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')
}
}
}
}
const onCheckedChange = (checked: boolean) => {
setChecked(checked)
}
useEffect(() => {
const isValid = validateOnSubmit()
if (!isValid) {
setIsSubmitDisabled(true)
} else {
setIsSubmitDisabled(false)
}
console.log(formData, 'formData');
}, [formData])
useEffect(() => {
initFormData()
}, [])
return (
<View>
<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)` }}>
{
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}
/>
</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/publishBall/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)