feat: 球局详情完善 & 订单详情完善
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user