feat: 球局详情完善 & 订单详情完善

This commit is contained in:
2025-09-11 19:21:46 +08:00
parent c430ed407b
commit 03c2571dda
12 changed files with 856 additions and 423 deletions

View File

@@ -23,6 +23,8 @@ import {
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";
@@ -218,10 +220,16 @@ function navto(url) {
function StickyButton(props) {
const { handleShare, handleJoinGame, detail } = props;
const ntrpRef = useRef(null);
const userInfo = useUserInfo();
const { id } = userInfo;
const { publisher_id, match_status, price, user_action_status, end_time } =
detail || {};
// const userInfo = useUserInfo();
// const { id } = userInfo;
const {
id,
publisher_id,
match_status,
price,
user_action_status,
end_time,
} = detail || {};
function handleSelfEvaluate() {
// TODO: 打开自评弹窗
@@ -266,7 +274,14 @@ function StickyButton(props) {
} else if (can_pay) {
return {
text: "继续支付",
action: handleJoinGame,
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 {
@@ -323,7 +338,7 @@ function StickyButton(props) {
};
}
const role = Number(publisher_id) === id ? "ownner" : "visitor";
// const role = Number(publisher_id) === id ? "ownner" : "visitor";
return (
<View className="sticky-bottom-bar">
@@ -359,8 +374,17 @@ function StickyButton(props) {
// 球局信息
function GameInfo(props) {
const { detail, currentLocation } = props;
const { latitude, longitude, location, location_name, start_time, end_time } =
detail || {};
const {
latitude,
longitude,
location,
location_name,
start_time,
end_time,
weather = [{}],
} = detail || {};
const [{ iconDay, tempMax, tempMin }] = weather;
const openMap = () => {
Taro.openLocation({
@@ -374,7 +398,9 @@ function GameInfo(props) {
const [c_latitude, c_longitude] = currentLocation;
const distance =
calculateDistance(c_latitude, c_longitude, latitude, longitude) / 1000;
latitude && longitude
? calculateDistance(c_latitude, c_longitude, latitude, longitude) / 1000
: 0;
const startTime = dayjs(start_time);
const endTime = dayjs(end_time);
@@ -409,11 +435,16 @@ function GameInfo(props) {
<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} />
{/*<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">
<Text>28 - 32</Text>
{tempMin && tempMax && (
<Text>
{tempMin} - {tempMax}
</Text>
)}
</View>
</View>
</View>
@@ -431,18 +462,22 @@ function GameInfo(props) {
{/* location message */}
<View className="location-message-text">
{/* venue name and 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>
{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>
@@ -454,8 +489,8 @@ function GameInfo(props) {
{longitude && latitude && (
<Map
className="location-map-map"
longitude={longitude}
latitude={latitude}
longitude={c_longitude}
latitude={c_latitude}
markers={[
{
id: 1,
@@ -468,7 +503,7 @@ function GameInfo(props) {
]}
includePoints={[
{ latitude, longitude },
{ latitude: currentLocation[0], longitude: currentLocation[1] },
{ latitude: c_latitude, longitude: c_longitude },
]}
includePadding={{ left: 50, right: 50, top: 50, bottom: 50 }}
onError={() => {}}
@@ -580,7 +615,7 @@ function genNTRPRequirementText(min, max) {
} else if (max) {
return `${max} 以上`;
}
return '-'
return "-";
}
// 玩法要求
function GamePlayAndRequirement(props) {
@@ -627,65 +662,87 @@ function GamePlayAndRequirement(props) {
function Participants(props) {
const { detail = {}, handleJoinGame } = props;
const participants = detail.participants || [];
const { participant_count, max_participants, user_action_status = {} } = detail
const { can_join } = user_action_status
const leftCount = max_participants - participant_count
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 (
<View className="detail-page-content-participants">
<View className="participants-title">
<Text></Text>
<Text>·</Text>
<Text>{leftCount > 0 ? `剩余空位 ${leftCount}` : '已满员'}</Text>
</View>
<View className="participants-list">
{/* application */}
{can_join && <View
className="participants-list-application"
onClick={() => {
handleJoinGame()
// Taro.showToast({ title: "To be continued", icon: "none" });
}}
>
<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 {
user: { avatar_url, nickname, level, id: participant_user_id },
} = participant;
const role =
participant_user_id === organizer_id ? "组织者" : "参与者";
return (
<View key={participant.id} className="participants-list-item">
<Avatar
className="participants-list-item-avatar"
src={avatar_url}
/>
<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>
<Text>{leftCount > 0 ? `剩余空位 ${leftCount}` : "已满员"}</Text>
</View>
{participant_count > 0 || showApplicationEntry ? (
<View className="participants-list">
{/* application */}
{showApplicationEntry && (
<View
className="participants-list-application"
onClick={() => {
handleJoinGame();
// Taro.showToast({ title: "To be continued", icon: "none" });
}}
>
<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 {
user: {
avatar_url,
nickname,
level,
id: participant_user_id,
},
} = participant;
const role =
participant_user_id === organizer_id ? "组织者" : "参与者";
return (
<View key={participant.id} className="participants-list-item">
<Avatar
className="participants-list-item-avatar"
src={avatar_url}
/>
<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>
);
}
@@ -717,48 +774,86 @@ function SupplementalNotes(props) {
);
}
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 recommendGames = [
{
title: "黄浦日场对拉",
time: "2025-08-25 9:00",
timeLength: "2小时",
venue: "上海体育场",
veuneType: "室外",
distance: "1.2km",
avatar: "https://img.yzcdn.cn/vant/cat.jpeg",
applications: 10,
checkedApplications: 3,
levelRequirements: "NTRP 3.5",
playType: "双打",
},
{
title: "黄浦夜场对拉",
time: "2025-08-25 19:00",
timeLength: "2小时",
venue: "上海体育场",
veuneType: "室外",
distance: "1.2km",
avatar: "https://img.yzcdn.cn/vant/cat.jpeg",
applications: 10,
checkedApplications: 3,
levelRequirements: "NTRP 3.5",
playType: "双打",
},
{
title: "黄浦全天对拉",
time: "2025-08-25 9:00",
timeLength: "12小时",
venue: "上海体育场",
veuneType: "室外",
distance: "1.2km",
avatar: "https://img.yzcdn.cn/vant/cat.jpeg",
applications: 10,
checkedApplications: 3,
levelRequirements: "NTRP 3.5",
playType: "双打",
},
];
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 (
<View className="detail-page-content-organizer-recommend-games">
{/* orgnizer title */}
@@ -767,26 +862,36 @@ function OrganizerInfo(props) {
</View>
{/* organizer avatar and name */}
<View className="organizer-avatar-name">
<Avatar
className="organizer-avatar-name-avatar"
src="https://img.yzcdn.cn/vant/cat.jpeg"
/>
<Avatar className="organizer-avatar-name-avatar" src={avatar_url} />
<View className="organizer-avatar-name-message">
<Text className="organizer-avatar-name-message-name">Light</Text>
<Text className="organizer-avatar-name-message-name">{nickname}</Text>
<View className="organizer-avatar-name-message-stats">
<Text> 8 </Text>
<Text> {hosted_games_count} </Text>
<View className="organizer-avatar-name-message-stats-separator" />
<Text>NTRP 3.5</Text>
<Text>NTRP {ntrp_level || "初学者"}</Text>
</View>
</View>
<View className="organizer-actions">
<View className="organizer-actions-follow">
<Image
className="organizer-actions-follow-icon"
src={img.ICON_DETAIL_APPLICATION_ADD}
/>
<Text className="organizer-actions-follow-text"></Text>
</View>
{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">
<Image
className="organizer-actions-comment-icon"
@@ -797,7 +902,7 @@ function OrganizerInfo(props) {
</View>
{/* recommend games by organizer */}
<View className="organizer-recommend-games">
<View className="organizer-recommend-games-title">
<View className="organizer-recommend-games-title" onClick={() => {}}>
<Text>TA的更多活动</Text>
<Image
className="organizer-recommend-games-title-arrow"
@@ -825,7 +930,7 @@ function OrganizerInfo(props) {
<View className="recommend-games-list-item-location-venue-distance">
<Text>{game.venue}</Text>
<Text>·</Text>
<Text>{game.veuneType}</Text>
<Text>{game.venueType}</Text>
<Text>·</Text>
<Text>{game.distance}</Text>
</View>
@@ -865,26 +970,31 @@ function Index() {
0, 0,
]);
const { id, from } = params;
const { fetchUserInfo, updateUserInfo } = useUserActions();
const [userInfo, setUserInfo] = useState({}); // 组织者的userInfo
const { fetchUserInfo } = useUserActions(); // 获取登录用户的userInfo
const sharePopupRef = useRef<any>(null);
useDidShow(async () => {
await updateLocation();
await fetchUserInfo();
await fetchDetail();
// await fetchDetail();
});
const updateLocation = async () => {
try {
const location = await getCurrentLocation()
setCurrentLocation([location.latitude, location.longitude])
const { address, ...location } = await getCurrentLocation();
setCurrentLocation([location.latitude, location.longitude]);
// 使用 userStore 中的统一位置更新方法
await updateUserInfo({ latitude: location.latitude, longitude: location.longitude })
// await updateUserInfo({ latitude: location.latitude, longitude: location.longitude })
await DetailService.updateLocation({
latitude: Number(location.latitude),
longitude: Number(location.longitude),
});
// 位置更新后,重新获取详情页数据(因为距离等信息可能发生变化)
await fetchDetail()
await fetchDetail();
} catch (error) {
console.error("用户位置更新失败", error);
}
@@ -895,9 +1005,22 @@ function Index() {
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();
}
@@ -965,7 +1088,12 @@ function Index() {
{/* supplemental notes */}
<SupplementalNotes detail={detail} />
{/* organizer and recommend games by organizer */}
<OrganizerInfo detail={detail} />
<OrganizerInfo
detail={detail}
userInfo={userInfo}
currentLocation={currentLocation}
onUpdateUserInfo={onUpdateUserInfo}
/>
{/* sticky bottom action bar */}
<StickyButton
handleShare={handleShare}