Files
mini-programs/src/game_pages/detail/index.tsx
2025-10-01 09:36:33 +08:00

1315 lines
38 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,
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 (
<View className="detail-page-content-avatar-tags">
<View className="detail-page-content-avatar-tags-avatar">
{/* network image mock */}
<Image
className="detail-page-content-avatar-tags-avatar-image"
mode="aspectFill"
src={avatar_url}
onClick={handleViewUserInfo.bind(null, id)}
/>
</View>
<View className="detail-page-content-avatar-tags-tags">
{tags.map((tag, index) => (
<View
key={index}
className="detail-page-content-avatar-tags-tags-tag"
>
{tag.icon && <Image src={tag.icon} />}
<Text>{tag.name}</Text>
</View>
))}
</View>
</View>
);
}
type CourselItemType = {
url: string;
width: number;
height: number;
};
function Coursel(props) {
const { detail } = props;
const [list, setList] = useState<CourselItemType[]>([]);
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 (
<View className="detail-swiper-container">
<View
className="detail-swiper-scroll-container"
style={{ width: listWidth + "px" }}
>
{list.map((item, index) => {
return (
<View className="detail-swiper-item" key={index}>
<Image
src={item.url}
mode="aspectFill"
className="detail-swiper-item-image"
style={{ width: item.width + "px", height: item.height + "px" }}
/>
</View>
);
})}
</View>
</View>
);
}
// 分享弹窗
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 (
<CommonPopup
title="分享"
visible={visible}
onClose={() => {
setVisible(false);
}}
hideFooter
style={{ minHeight: "100px" }}
>
<View className={styles.shareContainer}>
<View catchMove className={styles.title}>
</View>
<View className={styles.shareItems}>
<Button
className={classnames(styles.button, styles.share)}
openType="share"
>
</Button>
<Button
className={classnames(styles.button, styles.save)}
onClick={handleSaveToLocal}
>
</Button>
</View>
</View>
</CommonPopup>
);
}
);
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: () => <Text>¥{displayPrice} </Text>,
action: () => toast("已加入"),
};
} else if (is_substituting) {
return {
text: () => <Text>¥{displayPrice} </Text>,
action: () => toast("已加入候补"),
};
} else if (can_pay) {
return {
text: () => <Text>¥{price} </Text>,
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: () => <Text>¥{displayPrice} </Text>,
action: handleJoinGame,
};
} else if (can_join) {
return {
text: () => {
return <Text>¥{displayPrice} </Text>;
},
action: handleJoinGame,
};
} else if (can_assess) {
return {
text: () => (
<NTRPEvaluatePopup
ref={ntrpRef}
types={[EvaluateType.EDIT, EvaluateType.EVALUATE]}
scene={SceneType.DETAIL}
displayCondition={DisplayConditionType.AUTO}
showGuide={false}
>
<Text>¥{displayPrice} </Text>
</NTRPEvaluatePopup>
),
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>{text as string}</Text>;
};
}
return (
<>
<View className="sticky-bottom-bar">
<View className="sticky-bottom-bar-share-and-comment">
<View className="sticky-bottom-bar-share" onClick={handleShare}>
<Image
className="sticky-bottom-bar-share-icon"
src={img.ICON_DETAIL_SHARE}
/>
<Text className="sticky-bottom-bar-share-text"></Text>
</View>
<View className="sticky-bottom-bar-share-and-comment-separator" />
<View
className="sticky-bottom-bar-comment"
onClick={() => {
// Taro.showToast({ title: "To be continued", icon: "none" });
handleAddComment();
}}
>
<Image
className="sticky-bottom-bar-comment-icon"
src={img.ICON_DETAIL_COMMENT_DARK}
/>
<Text className="sticky-bottom-bar-comment-text">
{commentCount > 0 ? commentCount : "评论"}
</Text>
</View>
</View>
<View
className={classnames(
"detail-main-action",
available ? "" : "disabled"
)}
>
<View
style={is_organizer ? {} : { margin: "auto" }}
className="sticky-bottom-bar-join-game"
onClick={action}
>
<ActionText />
</View>
{is_organizer && (
<View
className="game_manage"
onClick={() => {
gameManageRef.current.show(detail, onStatusChange);
}}
>
</View>
)}
</View>
</View>
<GameManagePopup ref={gameManageRef} />
</>
);
}
// 球局信息
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 (
<View className="detail-page-content-game-info">
{/* Date and Weather */}
<View className="detail-page-content-game-info-date-weather">
{/* Calendar and Date time */}
<View className="detail-page-content-game-info-date-weather-calendar-date">
{/* Calendar */}
<View className="detail-page-content-game-info-date-weather-calendar-date-calendar">
<View className="month">{startMonth}</View>
<View className="day">{startDay}</View>
</View>
{/* Date time */}
<View className="detail-page-content-game-info-date-weather-calendar-date-date">
<View className="date">{startDate}</View>
<View className="venue-time">
{gameRange} {game_length}
</View>
</View>
</View>
{/* Weather */}
<View className="detail-page-content-game-info-date-weather-weather">
{/* Weather icon */}
<View className="detail-page-content-game-info-date-weather-weather-icon">
{/*<Image className="weather-icon" src={img.ICON_WEATHER_SUN} />*/}
<i className={`qi-${iconDay}`}></i>
</View>
{/* Weather text and temperature */}
<View className="detail-page-content-game-info-date-weather-weather-text-temperature">
{tempMin && tempMax && (
<Text>
{tempMin} - {tempMax}
</Text>
)}
</View>
</View>
</View>
{/* Place */}
<View className="detail-page-content-game-info-place">
{/* venue location message */}
<View className="location-message">
{/* location icon */}
<View className="location-message-icon">
<Image
className="location-message-icon-image"
src={img.ICON_DETAIL_MAP}
/>
</View>
{/* location message */}
<View className="location-message-text">
{/* venue name and distance */}
{distance ? (
<View
className="location-message-text-name-distance"
onClick={openMap}
>
<Text>{location_name || "-"}</Text>
<Text>·</Text>
<Text>{distance.toFixed(1)}km</Text>
<Image
className="location-message-text-name-distance-arrow"
src={img.ICON_DETAIL_ARROW_RIGHT}
/>
</View>
) : (
""
)}
{/* venue address */}
<View className="location-message-text-address">
<Text>{location || "-"}</Text>
</View>
</View>
</View>
{/* venue map */}
<View className="location-map">
{longitude && latitude && (
<Map
className="location-map-map"
longitude={c_longitude}
latitude={c_latitude}
markers={[
{
id: 1,
latitude,
longitude,
iconPath: require("@/static/detail/icon-stark.svg"),
width: 16,
height: 16,
},
]}
includePoints={[
{ latitude, longitude },
{ latitude: c_latitude, longitude: c_longitude },
]}
includePadding={{ left: 50, right: 50, top: 50, bottom: 50 }}
onError={() => {}}
// hide business msg
showLocation
theme="dark"
/>
)}
</View>
</View>
</View>
);
}
// 场馆信息
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 (
<View className="detail-page-content-venue">
{/* venue detail title and venue ordered status */}
<View className="venue-detail-title">
<Text></Text>
{venue_image_list?.length > 0 ? (
<>
<Text>·</Text>
<View className="venue-reserve-status" onClick={showScreenShot}>
<Text></Text>
<Image
className="venue-reserve-screenshot"
src={img.ICON_DETAIL_ARROW_RIGHT}
/>
</View>
</>
) : (
""
)}
</View>
{/* venue detail content */}
<View className="venue-detail-content">
{/* venue detail tags */}
<View className="venue-detail-content-tags">
{insertDotInTags(venue_description_tag).map((tag, index) => (
<View key={index} className="venue-detail-content-tags-tag">
<Text>{tag}</Text>
</View>
))}
</View>
{/* venue remarks */}
<View className="venue-detail-content-remarks">
<Text>{venue_description}</Text>
</View>
</View>
<CommonPopup
visible={visible}
onClose={onClose}
round
hideFooter
position="bottom"
zIndex={1001}
>
<View className="venue-screenshot-title"></View>
<ScrollView scrollY className="venue-screenshot-scroll-view">
<View className="venue-screenshot-image-list">
{venue_image_list?.length > 0 &&
venue_image_list.map((item) => {
return (
<View
className="venue-screenshot-image-item"
onClick={previewImage.bind(null, item.url)}
>
<Image
className="venue-screenshot-image-item-image"
mode="aspectFill"
src={item.url}
/>
</View>
);
})}
</View>
</ScrollView>
</CommonPopup>
</View>
);
}
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 (
<View className="detail-page-content-gameplay-requirements">
{/* title */}
<View className="gameplay-requirements-title">
<Text></Text>
</View>
{/* requirements */}
<View className="gameplay-requirements">
{requirements.map((item, index) => (
<View key={index} className="gameplay-requirements-item">
<Text className="gameplay-requirements-item-title">
{item.title}
</Text>
<Text className="gameplay-requirements-item-desc">{item.desc}</Text>
</View>
))}
</View>
</View>
);
}
// 参与者
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 (
<View className="detail-page-content-participants">
<View className="participants-title">
<Text></Text>
<Text>·</Text>
<Text>{leftCount > 0 ? `剩余空位 ${leftCount}` : "已满员"}</Text>
</View>
{participant_count > 0 || showApplicationEntry ? (
<View className="participants-list">
{/* application */}
{showApplicationEntry && (
<View
className="participants-list-application"
onClick={() => {
handleJoinGame();
}}
>
<Image
className="participants-list-application-icon"
src={img.ICON_DETAIL_APPLICATION_ADD}
/>
<Text className="participants-list-application-text">
</Text>
</View>
)}
{/* participants list */}
<ScrollView className="participants-list-scroll" scrollX>
<View
className="participants-list-scroll-content"
style={{
width: `${
participants.length * 103 + (participants.length - 1) * 8
}px`,
}}
>
{participants.map((participant) => {
const {
is_organizer,
user: {
avatar_url,
nickname,
level,
id: participant_user_id,
},
} = participant;
const role = is_organizer ? "组织者" : "参与者";
return (
<View key={participant.id} className="participants-list-item">
<Image
className="participants-list-item-avatar"
mode="aspectFill"
src={avatar_url}
onClick={handleViewUserInfo.bind(
null,
participant_user_id
)}
/>
<Text className="participants-list-item-name">
{nickname || "未知"}
</Text>
<Text className="participants-list-item-level">
{level || "未知"}
</Text>
<Text className="participants-list-item-role">{role}</Text>
</View>
);
})}
</View>
</ScrollView>
</View>
) : (
""
)}
</View>
);
}
function SupplementalNotes(props) {
const {
detail: { description, description_tag },
} = props;
return (
<View className="detail-page-content-supplemental-notes">
<View className="supplemental-notes-title">
<Text></Text>
</View>
<View className="supplemental-notes-content">
{/* supplemental notes tags */}
<View className="supplemental-notes-content-tags">
{insertDotInTags(description_tag || []).map((tag, index) => (
<View key={index} className="supplemental-notes-content-tags-tag">
<Text>{tag}</Text>
</View>
))}
</View>
{/* supplemental notes content */}
<View className="supplemental-notes-content-text">
<Text>{description}</Text>
</View>
</View>
</View>
);
}
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 (
<View className="detail-page-content-organizer-recommend-games">
{/* orgnizer title */}
<View className="organizer-title">
<Text></Text>
</View>
{/* organizer avatar and name */}
<View className="organizer-avatar-name">
<Image
className="organizer-avatar-name-avatar"
src={avatar_url}
mode="aspectFill"
onClick={handleViewUserInfo.bind(null, id)}
/>
<View className="organizer-avatar-name-message">
<Text className="organizer-avatar-name-message-name">{nickname}</Text>
<View className="organizer-avatar-name-message-stats">
<Text> {hosted_games_count} </Text>
<View className="organizer-avatar-name-message-stats-separator" />
<Text>NTRP {ntrp_level || "初学者"}</Text>
</View>
</View>
<View className="organizer-actions">
{my_id === id ? (
""
) : (
<View
className="organizer-actions-follow"
onClick={toggleFollow.bind(null, is_following)}
>
{is_following ? (
<Text className="organizer-actions-follow-text"></Text>
) : (
<>
<Image
className="organizer-actions-follow-icon"
src={img.ICON_DETAIL_APPLICATION_ADD}
/>
<Text className="organizer-actions-follow-text"></Text>
</>
)}
</View>
)}
<View
className="organizer-actions-comment"
onClick={() => handleAddComment()}
>
<Image
className="organizer-actions-comment-icon"
src={img.ICON_DETAIL_COMMENT}
/>
</View>
</View>
</View>
{/* recommend games by organizer */}
{recommendGames.length > 0 && (
<View className="organizer-recommend-games">
<View
className="organizer-recommend-games-title"
onClick={handleViewUserInfo.bind(null, id)}
>
<Text>TA的更多活动</Text>
<Image
className="organizer-recommend-games-title-arrow"
src={img.ICON_DETAIL_ARROW_RIGHT}
/>
</View>
<ScrollView className="recommend-games-list" scrollX>
<View className="recommend-games-list-content">
{recommendGames.map((game, index) => (
<View
key={index}
className="recommend-games-list-item"
onClick={handleViewGame.bind(null, game.id)}
>
{/* game title */}
<View className="recommend-games-list-item-title">
<Text>{game.title}</Text>
<Image
className="recommend-games-list-item-title-arrow"
src={img.ICON_DETAIL_ARROW_RIGHT}
/>
</View>
{/* game time and range */}
<View className="recommend-games-list-item-time-range">
<Text>{game.time}</Text>
<Text>{game.timeLength}</Text>
</View>
{/* game location、vunue、distance */}
<View className="recommend-games-list-item-location-venue-distance">
<Text>{game.venue}</Text>
<Text>·</Text>
<Text>{game.venueType}</Text>
<Text>·</Text>
<Text>{game.distance}</Text>
</View>
{/* organizer avatar、applications、level requirements、play type */}
<View className="recommend-games-list-item-addon">
<Image
className="recommend-games-list-item-addon-avatar"
mode="aspectFill"
src={game.avatar}
onClick={(e) => {
e.stopPropagation();
handleViewUserInfo(id);
}}
/>
<View className="recommend-games-list-item-addon-message">
<View className="recommend-games-list-item-addon-message-applications">
<Text>
{game.checkedApplications}/
{game.applications}
</Text>
</View>
<View className="recommend-games-list-item-addon-message-level-requirements">
<Text>{game.levelRequirements}</Text>
</View>
<View className="recommend-games-list-item-addon-message-play-type">
<Text>{game.playType}</Text>
</View>
</View>
</View>
</View>
))}
</View>
</ScrollView>
</View>
)}
</View>
);
}
function Index() {
const [detail, setDetail] = useState<any>({});
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<any>(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 (
<View className="detail-page">
{/* custom navbar */}
<view className="custom-navbar">
<View className="detail-navigator">
<View className="detail-navigator-back" onClick={handleBack}>
<Image
className="detail-navigator-back-icon"
src={img.ICON_ARROW_LEFT}
/>
</View>
<View className="detail-navigator-icon">
<Image
className="detail-navigator-logo-icon"
src={img.ICON_LOGO_GO}
/>
</View>
</View>
</view>
<View className="detail-page-bg" style={backgroundImage} />
{/* swiper */}
<Coursel detail={detail} />
{/* content */}
<View className="detail-page-content">
{/* avatar and tags */}
<GameTags
detail={detail}
userInfo={userInfo}
handleViewUserInfo={handleViewUserInfo}
/>
{/* title */}
<View className="detail-page-content-title">
<Text className="detail-page-content-title-text">{detail.title}</Text>
</View>
{/* Date and Place and weather */}
<GameInfo detail={detail} currentLocation={currentLocation} />
{/* detail */}
<VenueInfo detail={detail} />
{/* gameplay requirements */}
<GamePlayAndRequirement detail={detail} />
{/* participants */}
<Participants
detail={detail}
handleJoinGame={handleJoinGame}
handleViewUserInfo={handleViewUserInfo}
/>
{/* supplemental notes */}
<SupplementalNotes detail={detail} />
{/* organizer and recommend games by organizer */}
<OrganizerInfo
detail={detail}
userInfo={userInfo}
currentLocation={currentLocation}
onUpdateUserInfo={onUpdateUserInfo}
handleViewUserInfo={handleViewUserInfo}
handleAddComment={() => {
commentRef.current && commentRef.current.addComment();
}}
/>
<Comments
ref={commentRef}
game_id={Number(id)}
publisher_id={Number(detail.publisher_id)}
/>
{/* sticky bottom action bar */}
<StickyButton
handleShare={handleShare}
handleJoinGame={handleJoinGame}
detail={detail}
onStatusChange={onStatusChange}
handleAddComment={() => {
commentRef.current && commentRef.current.addComment();
}}
getCommentCount={
commentRef.current && commentRef.current.getCommentCount
}
/>
{/* share popup */}
<SharePopup
ref={sharePopupRef}
id={id as string}
from={from as string}
/>
</View>
</View>
);
}
export default withAuth(Index);