import React, { useState, useEffect, useRef, useImperativeHandle, forwardRef, } from "react"; import { View, Text, Image, Map, ScrollView, Button } from "@tarojs/components"; // import { Avatar } from "@nutui/nutui-react-taro"; import Taro, { useRouter, useShareAppMessage, useShareTimeline, useDidShow, } from "@tarojs/taro"; import classnames from "classnames"; import dayjs from "dayjs"; import "dayjs/locale/zh-cn"; // 导入API服务 import { CommonPopup, withAuth, NTRPEvaluatePopup, GameManagePopup, Comments, } from "@/components"; import { EvaluateType, SceneType, DisplayConditionType, } from "@/components/NTRPEvaluatePopup"; import DetailService, { MATCH_STATUS, IsSubstituteSupported, } from "@/services/detailService"; import * as LoginService from "@/services/loginService"; import OrderService from "@/services/orderService"; import { getCurrentLocation, calculateDistance } from "@/utils/locationUtils"; import { useUserInfo, useUserActions } from "@/store/userStore"; import img from "@/config/images"; import styles from "./style.module.scss"; import "./index.scss"; dayjs.locale("zh-cn"); // 将·作为连接符插入到标签文本之间 function insertDotInTags(tags: string[]) { if (!tags) return []; return tags.join("-·-").split("-"); } function GameTags(props) { const { userInfo, handleViewUserInfo } = props; const { avatar_url, id } = userInfo; const tags = [ { name: "🕙 急招", icon: "", }, { name: "🔥 本周热门", icon: "", }, { name: "🎉 新活动", icon: "", }, { name: "官方组织", icon: "", }, ]; return ( {/* network image mock */} {tags.map((tag, index) => ( {tag.icon && } {tag.name} ))} ); } type CourselItemType = { url: string; width: number; height: number; }; function Coursel(props) { const { detail } = props; const [list, setList] = useState([]); const [listWidth, setListWidth] = useState(0); const { image_list } = detail; async function getImagesMsg(imageList) { const latest_list: CourselItemType[] = []; const sys_info = await Taro.getSystemInfo(); const max_width = sys_info.screenWidth - 30; const max_height = 240; const current_aspect_ratio = max_width / max_height; let container_width = 0; for (const imageUrl of imageList) { const { width, height } = await Taro.getImageInfo({ src: imageUrl }); if (width && height) { const aspect_ratio = width / height; const latest_w_h = { width, height }; if (aspect_ratio < current_aspect_ratio) { latest_w_h.width = max_height * aspect_ratio; latest_w_h.height = max_height; } else { latest_w_h.width = max_width; latest_w_h.height = max_width / aspect_ratio; } container_width += latest_w_h.width + 12; latest_list.push({ url: imageUrl, width: latest_w_h.width, height: latest_w_h.height, }); } } setList(latest_list); setListWidth(container_width); } useEffect(() => { getImagesMsg(image_list || []); }, [image_list]); return ( {list.map((item, index) => { return ( ); })} ); } // 分享弹窗 const SharePopup = forwardRef( ({ id, from }: { id: string; from: string }, ref) => { const [visible, setVisible] = useState(false); useImperativeHandle(ref, () => ({ show: () => { setVisible(true); }, })); useShareAppMessage((res) => { console.log(res, "res"); return { title: "分享", imageUrl: "https://img.yzcdn.cn/vant/cat.jpeg", path: `/game_pages/detail/index?id=${id}&from=share`, }; }); // function handleShareToWechatMoments() { // useShareTimeline(() => { // return { // title: '分享', // path: `/game_pages/detail/index?id=${id}&from=share`, // } // }) // } function handleSaveToLocal() { Taro.showToast({ title: "not yet", icon: "error" }); return; Taro.saveImageToPhotosAlbum({ filePath: "", success: () => { Taro.showToast({ title: "保存成功", icon: "success" }); }, fail: () => { Taro.showToast({ title: "保存失败", icon: "none" }); }, }); } return ( { setVisible(false); }} hideFooter style={{ minHeight: "100px" }} > 分享卡片 ); } ); function navto(url) { Taro.navigateTo({ url: url, }); } function toast(message) { Taro.showToast({ title: message, icon: "none" }); } function isFull(counts) { const { max_players, current_players, max_substitute_players, current_substitute_count, is_substitute_supported, } = counts; if (max_players === current_players) { return true; } else if (is_substitute_supported === IsSubstituteSupported.SUPPORT) { return max_substitute_players === current_substitute_count; } return false; } // 底部操作栏 function StickyButton(props) { const { handleShare, handleJoinGame, detail, onStatusChange, handleAddComment, getCommentCount, } = props; const [commentCount, setCommentCount] = useState(0); const ntrpRef = useRef(null); const { id, price, user_action_status, match_status, start_time, end_time, is_organizer, } = detail || {}; const gameManageRef = useRef(); function handleSelfEvaluate() { // TODO: 打开自评弹窗 ntrpRef?.current?.show(); } useEffect(() => { getCommentCount?.((count) => { setCommentCount(count); }); }, [getCommentCount]); function generateTextAndAction( user_action_status: null | { [key: string]: boolean } ): | undefined | { text: string | React.FC; action?: () => void; available?: boolean } { if (!user_action_status) { return; } const displayPrice = is_organizer ? 0 : price; user_action_status.can_assess = true; user_action_status.can_join = false; // console.log(user_action_status, "user_action"); const { can_assess, can_join, can_substitute, can_pay, is_substituting, waiting_start, } = user_action_status || {}; if (MATCH_STATUS.CANCELED === match_status) { return { text: "活动已取消", available: false, // action: () => toast("活动已取消"), }; } else if (dayjs(end_time).isBefore(dayjs())) { return { text: "活动已结束", available: false, // action: () => toast("活动已结束"), }; } else if (dayjs(start_time).isBefore(dayjs())) { return { text: "活动已开始", available: false, // action: () => toast("活动已开始"), }; } else if (isFull(detail)) { return { text: "活动已满员", available: false, // action: () => toast("活动已满员"), }; } if (waiting_start) { return { text: () => ¥{displayPrice} 已加入, action: () => toast("已加入"), }; } else if (is_substituting) { return { text: () => ¥{displayPrice} 已加入候补, action: () => toast("已加入候补"), }; } else if (can_pay) { return { text: () => ¥{price} 继续支付, action: async () => { const res = await OrderService.getUnpaidOrder(id); if (res.code === 0) { navto( `/order_pages/orderDetail/index?id=${res.data.order_info.order_id}` ); } }, }; } else if (can_substitute) { return { text: () => ¥{displayPrice} 我要候补, action: handleJoinGame, }; } else if (can_join) { return { text: () => { return ¥{displayPrice} 立即加入; }, action: handleJoinGame, }; } else if (can_assess) { return { text: () => ( ¥{displayPrice} 立即加入 ), action: handleSelfEvaluate, }; } return { text: "球局无法加入", available: false, }; } if (!user_action_status) { return ""; } const { text, available = true, action = () => {}, } = generateTextAndAction(user_action_status)!; let ActionText: React.FC | string = text; if (typeof ActionText === "string") { ActionText = () => { return {text as string}; }; } return ( <> 分享 { // Taro.showToast({ title: "To be continued", icon: "none" }); handleAddComment(); }} > {commentCount > 0 ? commentCount : "评论"} {is_organizer && ( { gameManageRef.current.show(detail, onStatusChange); }} > 管理 )} ); } // 球局信息 function GameInfo(props) { const { detail, currentLocation } = props; const { latitude, longitude, location, location_name, start_time, end_time, weather, } = detail || {}; const [{ iconDay, tempMax, tempMin }] = weather || [{}]; const openMap = () => { Taro.openLocation({ latitude, // 纬度(必填) longitude, // 经度(必填) name: location_name, // 位置名(可选) address: location, // 地址详情(可选) scale: 15, // 地图缩放级别(1-28) }); }; const [c_latitude, c_longitude] = currentLocation; const distance = latitude && longitude ? calculateDistance(c_latitude, c_longitude, latitude, longitude) / 1000 : 0; const startTime = dayjs(start_time); const endTime = dayjs(end_time); const game_length = endTime.diff(startTime, "minutes") / 60; const startMonth = startTime.format("M"); const startDay = startTime.format("D"); const theDayOfWeek = startTime.format("dddd"); const startDate = `${startMonth}月${startDay}日 ${theDayOfWeek}`; const gameRange = `${startTime.format("HH:mm")} - ${endTime.format("HH:mm")}`; return ( {/* Date and Weather */} {/* Calendar and Date time */} {/* Calendar */} {startMonth}月 {startDay} {/* Date time */} {startDate} {gameRange} ({game_length}小时) {/* Weather */} {/* Weather icon */} {/**/} {/* Weather text and temperature */} {tempMin && tempMax && ( {tempMin}℃ - {tempMax}℃ )} {/* Place */} {/* venue location message */} {/* location icon */} {/* location message */} {/* venue name and distance */} {distance ? ( {location_name || "-"} · {distance.toFixed(1)}km ) : ( "" )} {/* venue address */} {location || "-"} {/* venue map */} {longitude && latitude && ( {}} // hide business msg showLocation theme="dark" /> )} ); } // 场馆信息 function VenueInfo(props) { const { detail } = props; const [visible, setVisible] = useState(false); const { venue_description, venue_description_tag = [], venue_image_list = [], } = detail; function showScreenShot() { setVisible(true); } function onClose() { setVisible(false); } function previewImage(current_url) { Taro.previewImage({ current: current_url, urls: venue_image_list?.length > 0 ? venue_image_list.map((c) => c.url) : [], }); } return ( {/* venue detail title and venue ordered status */} 场馆详情 {venue_image_list?.length > 0 ? ( <> · 已订场 ) : ( "" )} {/* venue detail content */} {/* venue detail tags */} {insertDotInTags(venue_description_tag).map((tag, index) => ( {tag} ))} {/* venue remarks */} {venue_description} 预定截图 {venue_image_list?.length > 0 && venue_image_list.map((item) => { return ( ); })} ); } function genNTRPRequirementText(min, max) { if (min && max && min !== max) { return `${min} - ${max} 之间`; } else if (max === "1") { return "无要求"; } else if (max) { return `${max} 以上`; } return "-"; } // 玩法要求 function GamePlayAndRequirement(props) { const { detail: { skill_level_min, skill_level_max, play_type, game_type }, } = props; const requirements = [ { title: "NTRP水平要求", desc: genNTRPRequirementText(skill_level_min, skill_level_max), }, { title: "活动玩法", desc: play_type || "-", }, { title: "人员构成", desc: game_type || "-", }, ]; return ( {/* title */} 玩法要求 {/* requirements */} {requirements.map((item, index) => ( {item.title} {item.desc} ))} ); } // 参与者 function Participants(props) { const { detail = {}, handleJoinGame, handleViewUserInfo } = props; const participants = detail.participants || []; const { participant_count, max_participants, user_action_status = {}, } = detail; const { can_join, can_pay, can_substitute, is_substituting, waiting_start } = user_action_status; const showApplicationEntry = [can_pay, can_substitute, is_substituting, waiting_start].every( (item) => !item ) && can_join; const leftCount = max_participants - participant_count; return ( 参与者 · {leftCount > 0 ? `剩余空位 ${leftCount}` : "已满员"} {participant_count > 0 || showApplicationEntry ? ( {/* application */} {showApplicationEntry && ( { handleJoinGame(); }} > 申请加入 )} {/* participants list */} {participants.map((participant) => { const { is_organizer, user: { avatar_url, nickname, level, id: participant_user_id, }, } = participant; const role = is_organizer ? "组织者" : "参与者"; return ( {nickname || "未知"} {level || "未知"} {role} ); })} ) : ( "" )} ); } function SupplementalNotes(props) { const { detail: { description, description_tag }, } = props; return ( 补充说明 {/* supplemental notes tags */} {insertDotInTags(description_tag || []).map((tag, index) => ( {tag} ))} {/* supplemental notes content */} {description} ); } function genRecommendGames(games, location, avatar) { return games.map((item) => { const { id, title, start_time, end_time, court_type, location_name, current_players, max_players, latitude, longitude, skill_level_max, skill_level_min, play_type, } = item; const [c_latitude, c_longitude] = location; const distance = calculateDistance(c_latitude, c_longitude, latitude, longitude) / 1000; const startTime = dayjs(start_time); const endTime = dayjs(end_time); return { id, title, time: startTime.format("YYYY-MM-DD HH:MM"), timeLength: endTime.diff(startTime, "hour"), venue: location_name, venueType: court_type, distance: `${distance.toFixed(2)}km`, 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}以上`, playType: play_type, }; }); } function OrganizerInfo(props) { const { userInfo, currentLocation: location, onUpdateUserInfo = () => {}, handleViewUserInfo, handleAddComment, } = props; const { id, nickname, avatar_url, is_following, ntrp_level, stats: { hosted_games_count } = {}, ongoing_games = [], } = userInfo; const myInfo = useUserInfo(); const { id: my_id } = myInfo as LoginService.UserInfoType; const recommendGames = genRecommendGames(ongoing_games, location, avatar_url); const toggleFollow = async (follow) => { try { if (follow) { await LoginService.unFollowUser(id); } else { await LoginService.followUser(id); } onUpdateUserInfo(); Taro.showToast({ title: `${nickname} ${follow ? "已取消关注" : "已关注"}`, icon: "success", }); } catch (e) { Taro.showToast({ title: `${nickname} ${follow ? "取消关注失败" : "关注失败"}`, icon: "error", }); } }; function handleViewGame(gameId) { navto(`/game_pages/detail/index?id=${gameId}&from=current`); } return ( {/* orgnizer title */} 组织者 {/* organizer avatar and name */} {nickname} 已组织 {hosted_games_count} 次 NTRP {ntrp_level || "初学者"} {my_id === id ? ( "" ) : ( {is_following ? ( 取消关注 ) : ( <> 关注 )} )} handleAddComment()} > {/* recommend games by organizer */} {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} ))} )} ); } function Index() { const [detail, setDetail] = useState({}); const { params } = useRouter(); const [currentLocation, setCurrentLocation] = useState<[number, number]>([ 0, 0, ]); 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); const commentRef = useRef(); useDidShow(async () => { await updateLocation(); await fetchUserInfo(); // await fetchDetail(); }); const updateLocation = async () => { try { const { address, ...location } = await getCurrentLocation(); setCurrentLocation([location.latitude, location.longitude]); // 使用 userStore 中的统一位置更新方法 // await updateUserInfo({ latitude: location.latitude, longitude: location.longitude }) await DetailService.updateLocation({ latitude: Number(location.latitude), longitude: Number(location.longitude), }); // 位置更新后,重新获取详情页数据(因为距离等信息可能发生变化) await fetchDetail(); } catch (error) { console.error("用户位置更新失败", error); } }; const fetchDetail = async () => { if (!id) return; try { const res = await DetailService.getDetail(Number(id)); if (res.code === 0) { setDetail(res.data); fetchUserInfoById(res.data.publisher_id); } } catch (e) { if (e.message === "球局不存在") { handleBack(); } } }; const onUpdateUserInfo = () => { fetchUserInfoById(detail.publisher_id); }; async function fetchUserInfoById(user_id) { const userDetailInfo = await LoginService.getUserInfoById(user_id); if (userDetailInfo.code === 0) { setUserInfo(userDetailInfo.data); } } function handleShare() { sharePopupRef.current.show(); } const handleJoinGame = async () => { if (isMyOwn) { const res = await DetailService.organizerJoin(Number(id)); if (res.code === 0) { toast("加入成功"); fetchDetail(); } return; } navto(`/order_pages/orderDetail/index?gameId=${id}`); }; function onStatusChange(result) { if (result) { fetchDetail(); } } function handleBack() { const pages = Taro.getCurrentPages(); if (pages.length <= 1) { Taro.redirectTo({ url: "/game_pages/list/index", }); } else { Taro.navigateBack(); } } function handleViewUserInfo(userId) { navto(`/user_pages/other/index?userid=${userId}`); } const backgroundImage = detail?.image_list?.[0] ? { backgroundImage: `url(${detail?.image_list?.[0]})` } : {}; return ( {/* custom navbar */} {/* swiper */} {/* content */} {/* avatar and tags */} {/* title */} {detail.title} {/* Date and Place and weather */} {/* detail */} {/* gameplay requirements */} {/* participants */} {/* supplemental notes */} {/* organizer and recommend games by organizer */} { commentRef.current && commentRef.current.addComment(); }} /> {/* sticky bottom action bar */} { commentRef.current && commentRef.current.addComment(); }} getCommentCount={ commentRef.current && commentRef.current.getCommentCount } /> {/* share popup */} ); } export default withAuth(Index);