feat: 生成分享图

This commit is contained in:
2025-10-15 11:20:36 +08:00
parent 56fd71f266
commit 77e50731a3
30 changed files with 2756 additions and 2641 deletions

View File

@@ -0,0 +1,239 @@
.detail-page-content-organizer-recommend-games {
padding: 24px 15px 10px;
.organizer-title {
overflow: hidden;
padding-bottom: 6px;
height: 24px;
color: #fff;
font-feature-settings: "liga" off, "clig" off;
text-overflow: ellipsis;
font-family: "PingFang SC";
font-size: 16px;
font-style: normal;
font-weight: 600;
line-height: 24px; /* 150% */
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.organizer-avatar-name {
display: flex;
align-items: center;
padding: 16px 0 0;
align-items: center;
gap: 8px;
justify-content: flex-start;
&-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
&-message {
display: flex;
flex-direction: column;
gap: 4px;
&-name {
color: rgba(255, 255, 255, 0.85);
font-size: 13px;
font-style: normal;
font-weight: 500;
line-height: 24px; /* 184.615% */
}
&-stats {
display: flex;
align-items: center;
gap: 5px;
color: rgba(255, 255, 255, 0.6);
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px; /* 133.333% */
letter-spacing: 0.06px;
&-separator {
width: 1px;
height: 10px;
color: rgba(255, 255, 255, 0.2);
}
}
}
.organizer-actions {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
.organizer-actions-follow,
.organizer-actions-comment {
display: flex;
height: 32px;
box-sizing: border-box;
align-items: center;
gap: 4px;
border-radius: 999px;
// border: 0.5px solid rgba(255, 255, 255, 0.10);
background: rgba(255, 255, 255, 0.16);
box-shadow: 0 4px 48px 0 rgba(0, 0, 0, 0.08);
& > image {
width: 16px;
height: 16px;
}
}
.organizer-actions-follow {
padding: 8px 12px 8px;
&-text {
color: #fff;
font-size: 13px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 153.846% */
letter-spacing: -0.23px;
}
}
.organizer-actions-comment {
padding: 8px 10px;
}
}
}
.organizer-recommend-games {
padding-top: 20px;
.organizer-recommend-games-title {
overflow: hidden;
color: rgba(255, 255, 255, 0.65);
font-family: "PingFang SC";
font-size: 15px;
font-style: normal;
font-weight: 500;
line-height: 24px; /* 160% */
display: flex;
align-items: center;
gap: 2px;
align-self: stretch;
&-arrow {
width: 12px;
height: 12px;
}
}
.recommend-games-list {
padding: 10px 0;
display: flex;
gap: 8px;
flex-wrap: nowrap;
&-content {
display: flex;
gap: 8px;
flex-wrap: nowrap;
.recommend-games-list-item {
width: 246px;
height: 122px;
display: flex;
flex-direction: column;
gap: 6px;
flex: 0 0 auto;
border-radius: 20px;
border: 1px solid rgba(33, 178, 0, 0.2);
background: rgba(255, 255, 255, 0.16);
padding: 12px 0 12px 15px;
box-sizing: border-box;
&-title {
display: flex;
align-items: center;
height: 24px;
gap: 2px;
overflow: hidden;
color: rgba(255, 255, 255, 0.85);
text-overflow: ellipsis;
font-size: 16px;
font-style: normal;
font-weight: 600;
line-height: 24px; /* 150% */
&-arrow {
width: 12px;
height: 12px;
}
}
&-time-range {
overflow: hidden;
color: rgba(255, 255, 255, 0.45);
text-overflow: ellipsis;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 150% */
}
&-location-venue-distance {
display: flex;
align-items: center;
gap: 2px;
overflow: hidden;
color: rgba(255, 255, 255, 0.45);
font-feature-settings: "liga" off, "clig" off;
text-overflow: ellipsis;
font-family: "PingFang SC";
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 150% */
}
&-addon {
display: flex;
align-items: center;
gap: 4px;
&-avatar {
width: 20px;
height: 20px;
border-radius: 50%;
object-fit: cover;
}
&-message {
display: flex;
gap: 4px;
&-applications,
&-level-requirements,
&-play-type {
color: rgba(255, 255, 255, 0.85);
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 181.818% */
letter-spacing: -0.23px;
display: flex;
height: 20px;
padding: 6px 8px;
box-sizing: border-box;
align-items: center;
gap: 4px;
border-radius: 999px;
// border: 0.5px solid rgba(0, 0, 0, 0.16);
background: rgba(255, 255, 255, 0.12);
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,276 @@
import Taro from "@tarojs/taro";
import dayjs from "dayjs";
import { View, Text, Image, ScrollView } from "@tarojs/components";
import { calculateDistance } from "@/utils";
import { useUserInfo } from "@/store/userStore";
import * as LoginService from "@/services/loginService";
import img from "@/config/images";
import { navto } from "../../utils/helper";
import styles from "./index.module.scss";
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,
};
});
}
export default 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={styles["detail-page-content-organizer-recommend-games"]}>
{/* orgnizer title */}
<View className={styles["organizer-title"]}>
<Text></Text>
</View>
{/* organizer avatar and name */}
<View className={styles["organizer-avatar-name"]}>
<Image
className={styles["organizer-avatar-name-avatar"]}
src={avatar_url}
mode="aspectFill"
onClick={handleViewUserInfo.bind(null, id)}
/>
<View className={styles["organizer-avatar-name-message"]}>
<Text className={styles["organizer-avatar-name-message-name"]}>
{nickname}
</Text>
<View className={styles["organizer-avatar-name-message-stats"]}>
<Text> {hosted_games_count} </Text>
<View
className={
styles["organizer-avatar-name-message-stats-separator"]
}
/>
<Text>NTRP {ntrp_level || "初学者"}</Text>
</View>
</View>
<View className={styles["organizer-actions"]}>
{my_id === id ? (
""
) : (
<View
className={styles["organizer-actions-follow"]}
onClick={toggleFollow.bind(null, is_following)}
>
{is_following ? (
<Text className={styles["organizer-actions-follow-text"]}>
</Text>
) : (
<>
<Image
className={styles["organizer-actions-follow-icon"]}
src={img.ICON_DETAIL_APPLICATION_ADD}
/>
<Text className={styles["organizer-actions-follow-text"]}>
</Text>
</>
)}
</View>
)}
<View
className={styles["organizer-actions-comment"]}
onClick={() => handleAddComment()}
>
<Image
className={styles["organizer-actions-comment-icon"]}
src={img.ICON_DETAIL_COMMENT}
/>
</View>
</View>
</View>
{/* recommend games by organizer */}
{recommendGames.length > 0 && (
<View className={styles["organizer-recommend-games"]}>
<View
className={styles["organizer-recommend-games-title"]}
onClick={handleViewUserInfo.bind(null, id)}
>
<Text>TA的更多活动</Text>
<Image
className={styles["organizer-recommend-games-title-arrow"]}
src={img.ICON_DETAIL_ARROW_RIGHT}
/>
</View>
<ScrollView className={styles["recommend-games-list"]} scrollX>
<View className={styles["recommend-games-list-content"]}>
{recommendGames.map((game, index) => (
<View
key={index}
className={styles["recommend-games-list-item"]}
onClick={handleViewGame.bind(null, game.id)}
>
{/* game title */}
<View className={styles["recommend-games-list-item-title"]}>
<Text>{game.title}</Text>
<Image
className={
styles["recommend-games-list-item-title-arrow"]
}
src={img.ICON_DETAIL_ARROW_RIGHT}
/>
</View>
{/* game time and range */}
<View
className={styles["recommend-games-list-item-time-range"]}
>
<Text>{game.time}</Text>
<Text>{game.timeLength}</Text>
</View>
{/* game location、vunue、distance */}
<View
className={
styles[
"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={styles["recommend-games-list-item-addon"]}>
<Image
className={
styles["recommend-games-list-item-addon-avatar"]
}
mode="aspectFill"
src={game.avatar}
onClick={(e) => {
e.stopPropagation();
handleViewUserInfo(id);
}}
/>
<View
className={
styles["recommend-games-list-item-addon-message"]
}
>
<View
className={
styles[
"recommend-games-list-item-addon-message-applications"
]
}
>
<Text>
{game.checkedApplications}/
{game.applications}
</Text>
</View>
<View
className={
styles[
"recommend-games-list-item-addon-message-level-requirements"
]
}
>
<Text>{game.levelRequirements}</Text>
</View>
<View
className={
styles[
"recommend-games-list-item-addon-message-play-type"
]
}
>
<Text>{game.playType}</Text>
</View>
</View>
</View>
</View>
))}
</View>
</ScrollView>
</View>
)}
</View>
);
}