From 7dea93acde14ed1e1394fb794cef417fefff2afe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=9D=B0?= Date: Tue, 16 Sep 2025 17:30:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=90=83=E5=B1=80=E8=AF=A6=E6=83=85?= =?UTF-8?q?=E7=AE=A1=E7=90=86=20=E6=9C=AA=E5=AE=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GameManagePopup/index.module.scss | 27 ++ src/components/GameManagePopup/index.tsx | 145 ++++++++ src/components/NTRPEvaluatePopup/index.tsx | 2 +- .../UploadCover/upload-source-popup.scss | 5 +- .../UploadCover/upload-source-popup.tsx | 1 + src/components/index.ts | 2 + src/game_pages/detail/index.scss | 42 ++- src/game_pages/detail/index.tsx | 339 +++++++++++------- src/order_pages/orderList/index.tsx | 102 +++--- src/services/detailService.ts | 25 +- 10 files changed, 480 insertions(+), 210 deletions(-) create mode 100644 src/components/GameManagePopup/index.module.scss create mode 100644 src/components/GameManagePopup/index.tsx diff --git a/src/components/GameManagePopup/index.module.scss b/src/components/GameManagePopup/index.module.scss new file mode 100644 index 0000000..24fe1dc --- /dev/null +++ b/src/components/GameManagePopup/index.module.scss @@ -0,0 +1,27 @@ +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + padding-bottom: 40px; + + .button { + width: 100%; + padding: 20px 0; + display: flex; + justify-content: center; + align-items: center; + color: #000; + text-align: center; + font-feature-settings: 'liga' off, 'clig' off; + font-family: "PingFang SC"; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 20px; + + &:last-child { + border-top: 8px solid #f5f5f5; + } + } +} \ No newline at end of file diff --git a/src/components/GameManagePopup/index.tsx b/src/components/GameManagePopup/index.tsx new file mode 100644 index 0000000..12e6f34 --- /dev/null +++ b/src/components/GameManagePopup/index.tsx @@ -0,0 +1,145 @@ +import React, { + useState, + forwardRef, + useImperativeHandle, + useRef, +} from "react"; +import Taro from "@tarojs/taro"; +import { View } from "@tarojs/components"; +import CommonPopup from "../CommonPopup"; +import styles from "./index.module.scss"; +import detailService from "@/services/detailService"; +import { useUserInfo } from "@/store/userStore"; + +const CancelPopup = forwardRef((props, ref) => { + const [visible, setVisible] = useState(false); + const onFinish = useRef(null); + + useImperativeHandle(ref, () => ({ + show: (onAct) => { + onFinish.current = onAct; + setVisible(true); + }, + })); + + function onClose() { + setVisible(false); + } + return ( + <> + + + + + ); +}); + +export default forwardRef(function GameManagePopup(props, ref) { + const [visible, setVisible] = useState(false); + const [detail, setDetail] = useState({}); + const onStatusChange = useRef(null); + const cancelRef = useRef(null); + const userInfo = useUserInfo(); + + useImperativeHandle(ref, () => ({ + show: (gameDetail, onChange) => { + onStatusChange.current = onChange; + setDetail(gameDetail); + setVisible(true); + }, + })); + + function handleEditGame() { + Taro.navigateTo({ + url: `/publish_pages/publishBall/index?gameId=${detail.id}`, + }); + onClose() + } + + const handleRepubGame = handleEditGame; + + async function handleCancelGame() { + cancelRef.current.show(async (result) => { + if (result) { + try { + const res = await detailService.disbandGame({ + game_id: detail.id, + settle_reason: result, + }); + if (res.code === 0) { + Taro.showToast({ title: "活动取消成功" }); + onStatusChange.current?.(true); + } + } catch (e) { + Taro.showToast({ title: e.message, icon: "error" }); + } finally { + onClose(); + } + } + }); + } + + async function handleQuitGame() { + try { + const res = await detailService.organizerQuit({ + game_id: detail.id, + quit_reason: "组织者主动退出", + }); + if (res.code === 0) { + Taro.showToast({ title: "活动退出成功" }); + onStatusChange.current?.(true); + } + } catch (e) { + Taro.showToast({ title: e.message, icon: "error" }); + } finally { + onClose(); + } + } + + function onClose() { + setVisible(false); + } + + const hasJoin = (detail.participants || []).some(item => item.user.id === userInfo.id) + + return ( + <> + + + + 编辑活动 + + + 重新发布 + + + 取消活动 + + {!hasJoin && ( + + 退出活动 + + )} + + 取消 + + + + + + ); +}); diff --git a/src/components/NTRPEvaluatePopup/index.tsx b/src/components/NTRPEvaluatePopup/index.tsx index 724f678..2f9caa8 100644 --- a/src/components/NTRPEvaluatePopup/index.tsx +++ b/src/components/NTRPEvaluatePopup/index.tsx @@ -91,7 +91,7 @@ const NTRPEvaluatePopup = (props: NTRPEvaluatePopupProps, ref) => { - {showEntry && props.children} + {showEntry ? props.children : ''} ); }; diff --git a/src/components/UploadCover/upload-source-popup.scss b/src/components/UploadCover/upload-source-popup.scss index 999c934..bb8fbcd 100644 --- a/src/components/UploadCover/upload-source-popup.scss +++ b/src/components/UploadCover/upload-source-popup.scss @@ -31,7 +31,8 @@ } .upload-popup-scroll-view { - max-height: calc(100vh - 260px); + // max-height: calc(100vh - 260px); + height: 440px; overflow-y: auto; .upload-popup-image-list { @@ -124,7 +125,7 @@ display: flex; width: 100%; height: 62px; - padding: 8px 10px 10px 10px; + padding: 8px 10px 50px 10px; box-sizing: border-box; justify-content: center; align-items: flex-start; diff --git a/src/components/UploadCover/upload-source-popup.tsx b/src/components/UploadCover/upload-source-popup.tsx index ddd4004..8a832c0 100644 --- a/src/components/UploadCover/upload-source-popup.tsx +++ b/src/components/UploadCover/upload-source-popup.tsx @@ -121,6 +121,7 @@ export default forwardRef(function UploadImage(props: UploadImageProps, ref) { {images.length > 0 ? ( diff --git a/src/components/index.ts b/src/components/index.ts index abc1c4a..44bc01e 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -17,6 +17,7 @@ import withAuth from "./Auth"; import { CustomPicker, PopupPicker } from "./Picker"; import NTRPEvaluatePopup from "./NTRPEvaluatePopup"; import RefundPopup from "./refundPopup"; +import GameManagePopup from './GameManagePopup' export { ActivityTypeSwitch, @@ -39,4 +40,5 @@ export { PopupPicker, NTRPEvaluatePopup, RefundPopup, + GameManagePopup, }; diff --git a/src/game_pages/detail/index.scss b/src/game_pages/detail/index.scss index da114ba..d851af7 100644 --- a/src/game_pages/detail/index.scss +++ b/src/game_pages/detail/index.scss @@ -807,7 +807,7 @@ } &-organizer-recommend-games { - padding: 24px 15px 0; + padding: 24px 15px 10px; .organizer-title { overflow: hidden; @@ -1122,27 +1122,47 @@ } } - &-join-game { + .detail-main-action { display: flex; align-items: center; height: 52px; width: auto; - padding: 2px 6px; + // padding: 2px 6px; box-sizing: border-box; justify-content: center; gap: 12px; flex: 1 0 0; border-radius: 16px; - border: 1px solid rgba(0, 0, 0, 0.06); + // border: 1px solid rgba(0, 0, 0, 0.06); background: #fff; + overflow: hidden; - &-price { - font-family: "PoetsenOne"; - font-size: 28px; - font-weight: 400; - line-height: 24px; /* 114.286% */ - letter-spacing: -0.56px; - color: #000; + .sticky-bottom-bar-join-game { + margin-left: auto; + width: 151px; + display: flex; + align-items: center; + justify-content: center; + + &-price { + font-family: "PoetsenOne"; + font-size: 28px; + font-weight: 400; + line-height: 24px; /* 114.286% */ + letter-spacing: -0.56px; + color: #000; + } + } + + .game_manage { + width: 100px; + margin-left: auto; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: #000; + color: #fff; } } } diff --git a/src/game_pages/detail/index.tsx b/src/game_pages/detail/index.tsx index 29bc5bf..af9597c 100644 --- a/src/game_pages/detail/index.tsx +++ b/src/game_pages/detail/index.tsx @@ -16,7 +16,12 @@ import Taro, { import dayjs from "dayjs"; import "dayjs/locale/zh-cn"; // 导入API服务 -import { CommonPopup, withAuth, NTRPEvaluatePopup } from "@/components"; +import { + CommonPopup, + withAuth, + NTRPEvaluatePopup, + GameManagePopup, +} from "@/components"; import { EvaluateType, SceneType, @@ -210,7 +215,7 @@ const SharePopup = forwardRef( ); - }, + } ); function navto(url) { @@ -219,20 +224,18 @@ function navto(url) { }); } +function toast(message) { + Taro.showToast({ title: message, icon: "none" }); +} + // 底部操作栏 function StickyButton(props) { - const { handleShare, handleJoinGame, detail } = props; + const { handleShare, handleJoinGame, detail, onStatusChange } = props; const ntrpRef = useRef(null); - // const userInfo = useUserInfo(); - // const { id } = userInfo; - const { - id, - publisher_id, - match_status, - price, - user_action_status, - end_time, - } = detail || {}; + const { id, price, user_action_status, end_time, is_organizer } = + detail || {}; + + const gameManageRef = useRef(); function handleSelfEvaluate() { // TODO: 打开自评弹窗 @@ -240,13 +243,14 @@ function StickyButton(props) { } function generateTextAndAction( - user_action_status: null | { [key: string]: boolean }, + user_action_status: null | { [key: string]: boolean } ): undefined | { text: string | React.FC; action: () => void } { if (!user_action_status) { return; } + const displayPrice = is_organizer ? 0 : price; // user_action_status.can_assess = true; - user_action_status.can_join = true; + // user_action_status.can_join = true; const { can_assess, can_join, @@ -260,27 +264,27 @@ function StickyButton(props) { dayjs(end_time).isBefore(dayjs()) ) { return { - text: "球局已结束,查看其他球局", - action: navto.bind(null, "/game_pages/list/index"), + text: "活动已结束", + action: () => toast("活动已结束"), }; } if (waiting_start) { return { - text: "等待开始, 查看更多球局", - action: navto.bind(null, "/game_pages/list/index"), + text: () => ¥{displayPrice} 已加入, + action: () => toast("已加入"), }; } else if (is_substituting) { return { - text: "候补中,查看其他球局", - action: navto.bind(null, "/game_pages/list/index"), + text: () => ¥{displayPrice} 已加入候补, + action: () => toast("已加入候补"), }; } else if (can_pay) { return { - text: "继续支付", + text: () => ¥{price} 继续支付, action: async () => { const res = await OrderService.getUnpaidOrder(id); if (res.code === 0) { - Taro.navigateTo({ + navto({ url: `/order_pages/orderDetail/index?id=${res.data.order_info.order_id}`, }); } @@ -288,21 +292,13 @@ function StickyButton(props) { }; } else if (can_substitute) { return { - text: "立即候补", + text: () => ¥{displayPrice} 我要候补, action: handleJoinGame, }; } else if (can_join) { return { text: () => { - return ( - <> - 🎾 - 立即加入 - - ¥ {price} - - - ); + return ¥{displayPrice} 立即加入; }, action: handleJoinGame, }; @@ -315,7 +311,7 @@ function StickyButton(props) { scene={SceneType.DETAIL} displayCondition={DisplayConditionType.AUTO} > - NTRP自评 + ¥{displayPrice} 立即加入 ), action: handleSelfEvaluate, @@ -341,36 +337,49 @@ function StickyButton(props) { }; } - // const role = Number(publisher_id) === id ? "ownner" : "visitor"; - return ( - - - - - 分享 + <> + + + + + 分享 + + + { + Taro.showToast({ title: "To be continued", icon: "none" }); + }} + > + + 32 + - - { - Taro.showToast({ title: "To be continued", icon: "none" }); - }} - > - - 32 + + + + + {is_organizer && ( + { + gameManageRef.current.show(detail, onStatusChange); + }} + > + 管理 + + )} - - - - + + ); } @@ -612,10 +621,11 @@ function VenueInfo(props) { } function genNTRPRequirementText(min, max) { + console.log(min, max, "ntrp"); if (min && max && min !== max) { return `${min} - ${max} 之间`; - } else if (max === 1) { - return "没有要求"; + } else if (max === "1") { + return "无要求"; } else if (max) { return `${max} 以上`; } @@ -675,10 +685,9 @@ function Participants(props) { user_action_status; const showApplicationEntry = [can_pay, can_substitute, is_substituting, waiting_start].every( - (item) => !item, + (item) => !item ) && can_join; const leftCount = max_participants - participant_count; - const organizer_id = Number(detail.publisher_id); return ( @@ -694,7 +703,6 @@ function Participants(props) { className="participants-list-application" onClick={() => { handleJoinGame(); - // Taro.showToast({ title: "To be continued", icon: "none" }); }} > {participants.map((participant) => { const { + is_organizer, user: { avatar_url, nickname, @@ -723,15 +734,17 @@ function Participants(props) { id: participant_user_id, }, } = participant; - const role = - participant_user_id === organizer_id ? "组织者" : "参与者"; + const role = is_organizer ? "组织者" : "参与者"; return ( {nickname || "未知"} @@ -813,7 +826,12 @@ function genRecommendGames(games, location, avatar) { avatar, applications: max_players, checkedApplications: current_players, - levelRequirements: skill_level_max !== skill_level_min ? `${skill_level_min || '-'}至${skill_level_max || '-'}` : skill_level_min === 1 ? '无要求' : `${skill_level_min}以上`, + levelRequirements: + skill_level_max !== skill_level_min + ? `${skill_level_min || "-"}至${skill_level_max || "-"}` + : skill_level_min === "1" + ? "无要求" + : `${skill_level_min}以上`, playType: play_type, }; }); @@ -862,9 +880,9 @@ function OrganizerInfo(props) { }; function handleViewGame(gameId) { - Taro.navigateTo({ - url: `/game_pages/detail/index?id=${gameId}&from=current` - }) + navto({ + url: `/game_pages/detail/index?id=${gameId}&from=current`, + }); } return ( @@ -875,7 +893,12 @@ function OrganizerInfo(props) { {/* organizer avatar and name */} - + {nickname} @@ -914,66 +937,79 @@ function OrganizerInfo(props) { {/* recommend games by organizer */} - - - TA的更多活动 - - - - - {recommendGames.map((game, index) => ( - - {/* game title */} - - {game.title} - - - {/* game time and range */} - - {game.time} - {game.timeLength} - - {/* game location、vunue、distance */} - - {game.venue} - · - {game.venueType} - · - {game.distance} - - {/* organizer avatar、applications、level requirements、play type */} - - { e.stopPropagation(); handleViewUserInfo(id) }} - /> - - - - 报名人数 {game.checkedApplications}/{game.applications} - - - - {game.levelRequirements} - - - {game.playType} + {recommendGames.length > 0 && ( + + + TA的更多活动 + + + + + {recommendGames.map((game, index) => ( + + {/* game title */} + + {game.title} + + + {/* game time and range */} + + {game.time} + {game.timeLength} + + {/* game location、vunue、distance */} + + {game.venue} + · + {game.venueType} + · + {game.distance} + + {/* organizer avatar、applications、level requirements、play type */} + + { + e.stopPropagation(); + handleViewUserInfo(id); + }} + /> + + + + 报名人数 {game.checkedApplications}/ + {game.applications} + + + + {game.levelRequirements} + + + {game.playType} + - - ))} - - - + ))} + + + + )} ); } @@ -987,6 +1023,9 @@ function Index() { const { id, from } = params; const [userInfo, setUserInfo] = useState({}); // 组织者的userInfo const { fetchUserInfo } = useUserActions(); // 获取登录用户的userInfo + const myInfo = useUserInfo(); + + const isMyOwn = userInfo.id === myInfo.id; const sharePopupRef = useRef(null); @@ -1031,7 +1070,6 @@ function Index() { async function fetchUserInfoById(user_id) { const userDetailInfo = await LoginService.getUserInfoById(user_id); if (userDetailInfo.code === 0) { - // console.log(userDetailInfo.data); setUserInfo(userDetailInfo.data); } } @@ -1040,12 +1078,26 @@ function Index() { sharePopupRef.current.show(); } - const handleJoinGame = () => { - Taro.navigateTo({ + const handleJoinGame = async () => { + if (isMyOwn) { + const res = await DetailService.organizerJoin(Number(id)); + if (res.code === 0) { + toast("加入成功"); + fetchDetail(); + } + return; + } + navto({ url: `/order_pages/orderDetail/index?gameId=${id}`, }); }; + function onStatusChange(result) { + if (result) { + fetchDetail(); + } + } + function handleBack() { const pages = Taro.getCurrentPages(); if (pages.length <= 1) { @@ -1058,9 +1110,9 @@ function Index() { } function handleViewUserInfo(userId) { - Taro.navigateTo({ - url: `/user_pages/other/index?userid=${userId}` - }) + navto({ + url: `/user_pages/other/index?userid=${userId}`, + }); } console.log("detail", detail); @@ -1093,7 +1145,11 @@ function Index() { {/* content */} {/* avatar and tags */} - + {/* title */} {detail.title} @@ -1105,7 +1161,11 @@ function Index() { {/* gameplay requirements */} {/* participants */} - + {/* supplemental notes */} {/* organizer and recommend games by organizer */} @@ -1121,6 +1181,7 @@ function Index() { handleShare={handleShare} handleJoinGame={handleJoinGame} detail={detail} + onStatusChange={onStatusChange} /> {/* share popup */} ({DayOfWeekMap.get(dayofWeek)}) - {startTime.format('ah')}点 + {startTime.format("ah")}点 {gameLength} ); @@ -59,7 +59,7 @@ function generateTimeMsg(game_info) { const OrderList = () => { const [list, setList] = useState([]); const [total, setTotal] = useState(0); - const refundRef = useRef(null) + const refundRef = useRef(null); const end = list.length * PAGESIZE >= total; @@ -78,7 +78,7 @@ const OrderList = () => { setTotal(res.data.count); setList((prev) => { const newList = [...prev]; - const index = page - 1 + const index = page - 1; newList.splice( index, clear ? newList.length - index : 1, @@ -121,53 +121,49 @@ const OrderList = () => { } async function handleDeleteOrder(item) { - const { id: order_id } = item + const { id: order_id } = item; // TODO:删除订单,刷新这一页,然后后面的全清除掉 const onCancel = () => { - Dialog.close("cancelOrder"); - }; - const onConfirm = async () => { - try { - const deleteRes = await orderService.deleteOrder({ - order_id, - }); - if (deleteRes.code !== 0) { - throw new Error(deleteRes.message); - } - getOrders(item.page); - Taro.showToast({ - title: "删除成功", - icon: "none", - }) - } catch (e) { - Taro.showToast({ - title: e.message, - icon: "error", - }); - } finally { - Dialog.close("cancelOrder"); + Dialog.close("cancelOrder"); + }; + const onConfirm = async () => { + try { + const deleteRes = await orderService.deleteOrder({ + order_id, + }); + if (deleteRes.code !== 0) { + throw new Error(deleteRes.message); } - }; + getOrders(item.page); + Taro.showToast({ + title: "删除成功", + icon: "none", + }); + } catch (e) { + Taro.showToast({ + title: e.message, + icon: "error", + }); + } finally { + Dialog.close("cancelOrder"); + } + }; Dialog.open("cancelOrder", { title: "确定删除订单吗?", content: "删除订单后,您将无法恢复订单。请确认是否继续取消?", - footer: ( - - - - - ), - onConfirm, - onCancel, - }) + footer: ( + + + + + ), + onConfirm, + onCancel, + }); } async function handleCancelOrder(item) { @@ -188,7 +184,7 @@ const OrderList = () => { Taro.showToast({ title: "取消成功", icon: "none", - }) + }); } catch (e) { Taro.showToast({ title: e.message, @@ -206,11 +202,7 @@ const OrderList = () => { - @@ -220,13 +212,13 @@ const OrderList = () => { }); } - function handleQuit (item) { + function handleQuit(item) { if (refundRef.current) { refundRef.current.show(item, (result) => { if (result) { - getOrders(item.page) + getOrders(item.page); } - }) + }); } } @@ -249,7 +241,9 @@ const OrderList = () => { > {/* */} {list.flat().map((item) => { - const unPay = item.order_status === OrderStatus.PENDING && item.cancel_type === CancelType.NONE; + const unPay = + item.order_status === OrderStatus.PENDING && + item.cancel_type === CancelType.NONE; const { game_info: { diff --git a/src/services/detailService.ts b/src/services/detailService.ts index 8d03adf..2b8cf62 100644 --- a/src/services/detailService.ts +++ b/src/services/detailService.ts @@ -92,10 +92,8 @@ export interface UpdateLocationRes { city: string; district: string; } - -// 发布球局类 class GameDetailService { - // 用户登录 + // 查询球局详情 async getDetail(id: number): Promise> { return httpService.post( "/games/detail", @@ -114,6 +112,27 @@ class GameDetailService { showLoading: true, }); } + + // 组织者加入球局 + async organizerJoin(game_id: number): Promise> { + return httpService.post("/games/organizer_join", { game_id }, { + showLoading: true, + }); + } + + // 组织者退出球局 + async organizerQuit(req: { game_id: number, quit_reason: string }): Promise> { + return httpService.post("/games/organizer_quit", req, { + showLoading: true, + }); + } + + // 组织者解散球局 + async disbandGame(req: { game_id: number, settle_reason: string }): Promise> { + return httpService.post("/games/settle_game", req, { + showLoading: true, + }); + } } // 导出认证服务实例