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

908 lines
25 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,
);
const [formData, setFormData] = useState<PublishBallFormData[]>([
defaultFormData,
]);
const [checked, setChecked] = useState(true);
const [publishLoading, setPublishLoading] = useState(false);
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,
) => {
setFormData((prev) => {
const newData = [...prev];
newData[index] = { ...newData[index], [key]: value };
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,
descriptionInfo,
} = formData;
const { play_type, price, location_name } = activityInfo;
const { description } = descriptionInfo;
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 (title.length > 20) {
if (!isOnSubmit) {
Taro.showToast({
title: `标题最多可输入20个字`,
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;
}
if (description?.length > 200) {
if (!isOnSubmit) {
Taro.showToast({
title: `补充要求最多可输入200个字`,
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 () => {
// 基础验证
const params = getParams();
const { republish } = params || {};
if (activityType === "individual") {
const isValid = validateFormData(formData[0]);
if (!isValid || publishLoading) {
return;
}
setPublishLoading(true);
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;
// 如果是编辑,就返回,否则就是新发布
if (republish === "0") {
Taro.navigateBack();
} else {
// 使用 redirectTo 替换当前页面,避免返回时回到发布页面
Taro.redirectTo({
// @ts-expect-error: id
url: `/game_pages/detail/index?id=${
id || 1
}&from=publish&autoShare=1`,
});
}
} else {
Taro.showToast({
title: res.message,
icon: "none",
});
setPublishLoading(false);
}
}
if (activityType === "group") {
const isValid = formData.every((item) => validateFormData(item));
if (!isValid || publishLoading) {
return;
}
setPublishLoading(true);
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;
// 使用 redirectTo 替换当前页面,避免返回时回到发布页面
Taro.redirectTo({
// @ts-expect-error: id
url: `/game_pages/detail/index?id=${
id || 1
}&from=publish&autoShare=1`,
});
} else {
Taro.showToast({
title: res.message,
icon: "none",
});
setPublishLoading(false);
}
}
};
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);
}
}, [formData]);
useEffect(() => {
initFormData();
}, []);
// 使用全局键盘状态监听
useEffect(() => {
// 初始化全局键盘监听器
initializeKeyboardListener();
// 添加本地监听器
const removeListener = addListener(() => {
// 布局是否响应交由 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}
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 + 140
}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);