import React, { useState, useEffect, useRef, useImperativeHandle, forwardRef, } from "react"; import { View, Text, Image, Map, ScrollView } from "@tarojs/components"; import { Avatar } from "@nutui/nutui-react-taro"; import Taro, { useRouter, useShareAppMessage, useShareTimeline, useDidShow, } from "@tarojs/taro"; import dayjs from "dayjs"; import "dayjs/locale/zh-cn"; // 导入API服务 import { CommonPopup, withAuth, NTRPEvaluatePopup } from "@/components"; import { EvaluateType, SceneType, DisplayConditionType, } from "@/components/NTRPEvaluatePopup"; import DetailService, { MATCH_STATUS } 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 "./index.scss"; dayjs.locale("zh-cn"); // 将·作为连接符插入到标签文本之间 function insertDotInTags(tags: string[]) { return tags.join("-·-").split("-"); } function GameTags(props) { const { detail } = props; 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(); console.log(sys_info, "info"); 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); }, })); // function handleShareToWechat() { // useShareAppMessage(() => { // return { // title: '分享', // path: `/pages/detail/index?id=${id}&from=share`, // } // }) // } // function handleShareToWechatMoments() { // useShareTimeline(() => { // return { // title: '分享', // path: `/pages/detail/index?id=${id}&from=share`, // } // }) // } // function handleSaveToLocal() { // Taro.saveImageToPhotosAlbum({ // filePath: images[0], // 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 StickyButton(props) { const { handleShare, handleJoinGame, detail } = props; const ntrpRef = useRef(null); // const userInfo = useUserInfo(); // const { id } = userInfo; const { id, publisher_id, match_status, price, user_action_status, end_time, } = detail || {}; function handleSelfEvaluate() { // TODO: 打开自评弹窗 ntrpRef?.current?.show(); } function generateTextAndAction( user_action_status: null | { [key: string]: boolean }, ): undefined | { text: string | React.FC; action: () => void } { if (!user_action_status) { return; } // user_action_status.can_assess = true; user_action_status.can_join = true; const { can_assess, can_join, can_substitute, can_pay, is_substituting, waiting_start, } = user_action_status || {}; if ( Object.values(user_action_status).every((value) => !value) && dayjs(end_time).isBefore(dayjs()) ) { return { text: "球局已结束,查看其他球局", action: navto.bind(null, "/pages/list/index"), }; } if (waiting_start) { return { text: "等待开始, 查看更多球局", action: navto.bind(null, "/pages/list/index"), }; } else if (is_substituting) { return { text: "候补中,查看其他球局", action: navto.bind(null, "/pages/list/index"), }; } else if (can_pay) { return { text: "继续支付", action: async () => { const res = await OrderService.getUnpaidOrder(id); if (res.code === 0) { Taro.navigateTo({ url: `/mod_user/orderDetail/index?id=${res.data.order_info.order_id}`, }); } }, }; } else if (can_substitute) { return { text: "立即候补", action: handleJoinGame, }; } else if (can_join) { return { text: () => { return ( <> 🎾 立即加入 ¥ {price} ); }, action: handleJoinGame, }; } else if (can_assess) { return { text: () => ( NTRP自评 ), action: handleSelfEvaluate, }; } return { text: "球局无法加入", action: () => {}, }; } if (!user_action_status) { return ""; } const { text, action } = generateTextAndAction(user_action_status)!; let ActionText: React.FC | string = text; if (typeof ActionText === "string") { ActionText = () => { return {text as string}; }; } // const role = Number(publisher_id) === id ? "ownner" : "visitor"; return ( 分享 { Taro.showToast({ title: "To be continued", icon: "none" }); }} > 32 ); } // 球局信息 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.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.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 } = 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; const organizer_id = Number(detail.publisher_id); return ( 参与者 · {leftCount > 0 ? `剩余空位 ${leftCount}` : "已满员"} {participant_count > 0 || showApplicationEntry ? ( {/* application */} {showApplicationEntry && ( { handleJoinGame(); // Taro.showToast({ title: "To be continued", icon: "none" }); }} > 申请加入 )} {/* participants list */} {participants.map((participant) => { const { user: { avatar_url, nickname, level, id: participant_user_id, }, } = participant; const role = participant_user_id === organizer_id ? "组织者" : "参与者"; 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: `NTRP ${genNTRPRequirementText(skill_level_min, skill_level_max)}`, playType: play_type, }; }); } function OrganizerInfo(props) { const { userInfo, currentLocation: location, onUpdateUserInfo = () => {}, } = 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", }); } }; return ( {/* orgnizer title */} 组织者 {/* organizer avatar and name */} {nickname} 已组织 {hosted_games_count} 次 NTRP {ntrp_level || "初学者"} {my_id === id ? ( "" ) : ( {is_following ? ( 取消关注 ) : ( <> 关注 )} )} {/* 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 */} 报名人数 {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 sharePopupRef = useRef(null); 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; const res = await DetailService.getDetail(Number(id)); if (res.code === 0) { setDetail(res.data); fetchUserInfoById(res.data.publisher_id); } }; const onUpdateUserInfo = () => { fetchUserInfoById(detail.publisher_id); }; async function fetchUserInfoById(user_id) { const userDetailInfo = await LoginService.getUserInfoById(Number(user_id)); if (userDetailInfo.code === 0) { // console.log(userDetailInfo.data); setUserInfo(userDetailInfo.data); } } function handleShare() { sharePopupRef.current.show(); } const handleJoinGame = () => { Taro.navigateTo({ url: `/mod_user/orderDetail/index?gameId=${id}`, }); }; function handleBack() { const pages = Taro.getCurrentPages(); if (pages.length <= 1) { Taro.redirectTo({ url: "/pages/list/index", }); } else { Taro.navigateBack(); } } console.log("detail", detail); 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 */} {/* sticky bottom action bar */} {/* share popup */} ); } export default withAuth(Index);