From 56fd71f2661fff2bda19ff2a6f756e1b1e3b17ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=9D=B0?= Date: Tue, 14 Oct 2025 15:12:04 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=AF=84=E8=AE=BA?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Comments/index.tsx | 29 +++++++++++++++-------------- src/services/commentServices.ts | 1 + 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/components/Comments/index.tsx b/src/components/Comments/index.tsx index 197e4b5..67c6c39 100644 --- a/src/components/Comments/index.tsx +++ b/src/components/Comments/index.tsx @@ -181,7 +181,7 @@ function CommentItem(props: { blink_id, } = props; const currentUserInfo = useUserInfo(); - const isGamePublisher = publisher_id === comment.user.id; + const isGamePublisher = publisher_id === currentUserInfo.id; const isCommentPublisher = currentUserInfo.id === comment.user.id; return ( {getRelativeDay(comment.create_time)} - 上海 + {comment.user.province} 回复 - {isGamePublisher || isCommentPublisher} - - handleDelete({ - parent_id: comment.parent_id, - id: comment.id, - }) - } - > - 删除 - + {(isGamePublisher || isCommentPublisher) && ( + + handleDelete({ + parent_id: comment.parent_id, + id: comment.id, + }) + } + > + 删除 + + )} {!isReplyComment(comment) && diff --git a/src/services/commentServices.ts b/src/services/commentServices.ts index 1c89fde..474107a 100644 --- a/src/services/commentServices.ts +++ b/src/services/commentServices.ts @@ -10,6 +10,7 @@ export interface UserInfo { id: number nickname: string avatar_url: string + province: string } export type BaseComment = { From 77e50731a3bbd7177aaf10b2dd995e1a307b0b64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=9D=B0?= Date: Wed, 15 Oct 2025 11:20:36 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E7=94=9F=E6=88=90=E5=88=86?= =?UTF-8?q?=E4=BA=AB=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.config.ts | 1 + .../components/Carousel/index.module.scss | 34 + .../detail/components/Carousel/index.tsx | 84 + .../components/GameInfo/index.module.scss | 201 +++ .../detail/components/GameInfo/index.tsx | 188 +++ .../GamePlayAndReq/index.module.scss | 54 + .../components/GamePlayAndReq/index.tsx | 46 + .../components/GameTags/index.module.scss | 44 + .../detail/components/GameTags/index.tsx | 49 + .../OrganizerInfo/index.module.scss | 239 +++ .../detail/components/OrganizerInfo/index.tsx | 276 ++++ .../components/Participants/index.module.scss | 132 ++ .../detail/components/Participants/index.tsx | 106 ++ .../SharePopup/index.module.scss} | 77 - .../detail/components/SharePopup/index.tsx | 165 ++ .../components/StickyBottom/index.module.scss | 124 ++ .../detail/components/StickyBottom/index.tsx | 253 +++ .../SupplementalNotes/index.module.scss | 55 + .../components/SupplementalNotes/index.tsx | 33 + .../components/VenueInfo/index.module.scss | 115 ++ .../detail/components/VenueInfo/index.tsx | 105 ++ src/game_pages/detail/index.module.scss | 111 ++ src/game_pages/detail/index.scss | 1138 ------------- src/game_pages/detail/index.tsx | 1458 +---------------- src/game_pages/detail/utils/helper.ts | 56 + src/game_pages/sharePoster/index.config.ts | 6 + src/game_pages/sharePoster/index.module.scss | 101 ++ src/game_pages/sharePoster/index.tsx | 134 ++ src/order_pages/orderList/index.tsx | 6 +- src/store/userStore.ts | 6 +- 30 files changed, 2756 insertions(+), 2641 deletions(-) create mode 100644 src/game_pages/detail/components/Carousel/index.module.scss create mode 100644 src/game_pages/detail/components/Carousel/index.tsx create mode 100644 src/game_pages/detail/components/GameInfo/index.module.scss create mode 100644 src/game_pages/detail/components/GameInfo/index.tsx create mode 100644 src/game_pages/detail/components/GamePlayAndReq/index.module.scss create mode 100644 src/game_pages/detail/components/GamePlayAndReq/index.tsx create mode 100644 src/game_pages/detail/components/GameTags/index.module.scss create mode 100644 src/game_pages/detail/components/GameTags/index.tsx create mode 100644 src/game_pages/detail/components/OrganizerInfo/index.module.scss create mode 100644 src/game_pages/detail/components/OrganizerInfo/index.tsx create mode 100644 src/game_pages/detail/components/Participants/index.module.scss create mode 100644 src/game_pages/detail/components/Participants/index.tsx rename src/game_pages/detail/{style.module.scss => components/SharePopup/index.module.scss} (61%) create mode 100644 src/game_pages/detail/components/SharePopup/index.tsx create mode 100644 src/game_pages/detail/components/StickyBottom/index.module.scss create mode 100644 src/game_pages/detail/components/StickyBottom/index.tsx create mode 100644 src/game_pages/detail/components/SupplementalNotes/index.module.scss create mode 100644 src/game_pages/detail/components/SupplementalNotes/index.tsx create mode 100644 src/game_pages/detail/components/VenueInfo/index.module.scss create mode 100644 src/game_pages/detail/components/VenueInfo/index.tsx create mode 100644 src/game_pages/detail/index.module.scss delete mode 100644 src/game_pages/detail/index.scss create mode 100644 src/game_pages/detail/utils/helper.ts create mode 100644 src/game_pages/sharePoster/index.config.ts create mode 100644 src/game_pages/sharePoster/index.module.scss create mode 100644 src/game_pages/sharePoster/index.tsx diff --git a/src/app.config.ts b/src/app.config.ts index 52281f8..5e46beb 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -9,6 +9,7 @@ export default defineAppConfig({ "game_pages/search/index", // 搜索页 "game_pages/searchResult/index", // 搜索结果页面 "game_pages/detail/index", // 球局详情页 + "game_pages/sharePoster/index", ], subPackages: [ diff --git a/src/game_pages/detail/components/Carousel/index.module.scss b/src/game_pages/detail/components/Carousel/index.module.scss new file mode 100644 index 0000000..72c52ec --- /dev/null +++ b/src/game_pages/detail/components/Carousel/index.module.scss @@ -0,0 +1,34 @@ +.detail-swiper-container { + height: 270px; + width: 100%; + padding: 15px 15px 0; + box-sizing: border-box; + overflow-x: scroll; + &::-webkit-scrollbar { + display: none; + width: 0; + height: 0; + color: transparent; /* 透明色 */ + } + + .detail-swiper-scroll-container { + display: flex; + height: 250px; + width: auto; + align-items: center; + justify-content: flex-start; + flex-wrap: nowrap; + gap: 12px; + + .detail-swiper-item { + flex: 0 0 auto; + height: 250px; + display: flex; + align-items: center; + &-image { + border-radius: 12px; + transition: transform 0.5s; + } + } + } +} diff --git a/src/game_pages/detail/components/Carousel/index.tsx b/src/game_pages/detail/components/Carousel/index.tsx new file mode 100644 index 0000000..a56de35 --- /dev/null +++ b/src/game_pages/detail/components/Carousel/index.tsx @@ -0,0 +1,84 @@ +import Taro from "@tarojs/taro"; +import { useEffect, useState } from "react"; +import { View, Image } from "@tarojs/components"; +import styles from "./index.module.scss"; + +type CarouselItemType = { + url: string; + width: number; + height: number; +}; +export default function Carousel(props) { + const { detail } = props; + const [list, setList] = useState([]); + const [listWidth, setListWidth] = useState(0); + const { image_list } = detail; + + async function getImagesMsg(imageList) { + const latest_list: CarouselItemType[] = []; + 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]); + + function previewImage(current_url) { + Taro.previewImage({ + current: current_url, + urls: list?.length > 0 ? list.map((c) => c.url) : [], + }); + } + + return ( + + + {list.map((item, index) => { + return ( + previewImage(item.url)} + > + + + ); + })} + + + ); +} diff --git a/src/game_pages/detail/components/GameInfo/index.module.scss b/src/game_pages/detail/components/GameInfo/index.module.scss new file mode 100644 index 0000000..b71dd6c --- /dev/null +++ b/src/game_pages/detail/components/GameInfo/index.module.scss @@ -0,0 +1,201 @@ +.detail-page-content-game-info { + &-date-weather { + padding: 20px 20px 0; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: space-between; + // gap: 12px; + + &-calendar-date { + width: 60%; + display: flex; + align-items: center; + gap: 16px; + + &-calendar { + display: flex; + width: 48px; + height: 48px; + box-sizing: border-box; + flex-direction: column; + align-items: center; + gap: 4px; + border-radius: 12px; + // border: 0.5px solid rgba(255, 255, 255, 0.08); + background: rgba(255, 255, 255, 0.25); + overflow: hidden; + color: #fff; + background: #536272; + + .month { + width: 100%; + height: 18px; + font-size: 10px; + display: flex; + padding: 1px auto; + box-sizing: border-box; + justify-content: center; + align-items: center; + // border-bottom: 1px solid rgba(255, 255, 255, 0.08); + background: #7b828b; + } + + .day { + display: flex; + width: 48px; + height: 30px; + // padding-bottom: 6px; + box-sizing: border-box; + flex-direction: column; + align-items: center; + // border: 0.5px solid rgba(255, 255, 255, 0.08); + // background: rgba(255, 255, 255, 0.25); + // background-color: #536272; + } + } + + &-date { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: space-evenly; + gap: 4px; + align-self: stretch; + color: #fff; + + .date { + color: #fff; + font-feature-settings: "liga" off, "clig" off; + font-family: "PingFang SC"; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 24px; /* 150% */ + } + + .venue-time { + color: rgba(255, 255, 255, 0.8); + font-feature-settings: "liga" off, "clig" off; + font-family: "PingFang SC"; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 166.667% */ + } + } + } + + &-weather { + display: flex; + align-items: flex-end; + flex-direction: column; + gap: 4px; + + &-icon { + width: 20px; + height: 20px; + color: rgba(255, 255, 255, 0.8); + } + &-text-temperature { + display: flex; + align-items: center; + gap: 12px; + color: rgba(255, 255, 255, 0.8); + font-feature-settings: "liga" off, "clig" off; + font-family: "PingFang SC"; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 166.667% */ + } + } + } + + &-place { + .location-message { + padding: 20px 20px 0; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: flex-start; + gap: 12px; + + &-icon { + width: 48px; + height: 48px; + border-radius: 12px; + padding: 14px; + box-sizing: border-box; + background: #4d5865; + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + // border: 0.5px solid rgba(255, 255, 255, 0.08); + // background: rgba(255, 255, 255, 0.25); + + &-image { + width: 20px; + height: 20px; + } + } + + &-text { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: space-evenly; + gap: 4px; + align-self: stretch; + + &-name-distance { + display: flex; + align-items: center; + gap: 4px; + color: #fff; + text-align: center; + font-feature-settings: "liga" off, "clig" off; + font-family: "PingFang SC"; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 24px; /* 150% */ + + &-arrow { + width: 16px; + height: 16px; + } + } + + &-address { + color: rgba(255, 255, 255, 0.8); + text-align: center; + font-feature-settings: "liga" off, "clig" off; + font-family: "PingFang SC"; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 166.667% */ + } + } + } + + .location-map { + width: 100%; + padding: 20px 20px 0; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + border-radius: 12px; + + &-map { + width: 100%; + height: 95px; + border-radius: 12px; + } + } + } +} diff --git a/src/game_pages/detail/components/GameInfo/index.tsx b/src/game_pages/detail/components/GameInfo/index.tsx new file mode 100644 index 0000000..5aea090 --- /dev/null +++ b/src/game_pages/detail/components/GameInfo/index.tsx @@ -0,0 +1,188 @@ +import Taro from "@tarojs/taro"; +import dayjs from "dayjs"; +import "dayjs/locale/zh-cn"; +import { calculateDistance } from "@/utils"; +import { View, Image, Text, Map } from "@tarojs/components"; +import img from "@/config/images"; +import styles from "./index.module.scss"; + +dayjs.locale("zh-cn"); + +// 球局信息 +export default 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" + /> + )} + + + + ); +} diff --git a/src/game_pages/detail/components/GamePlayAndReq/index.module.scss b/src/game_pages/detail/components/GamePlayAndReq/index.module.scss new file mode 100644 index 0000000..fe7c364 --- /dev/null +++ b/src/game_pages/detail/components/GamePlayAndReq/index.module.scss @@ -0,0 +1,54 @@ +.detail-page-content-gameplay-requirements { + padding: 24px 15px 0; + box-sizing: border-box; + + .gameplay-requirements-title { + overflow: hidden; + color: #fff; + height: 24px; + padding-bottom: 6px; + 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); + } + + .gameplay-requirements { + padding: 12px 0 0; + display: flex; + flex-direction: column; + gap: 12px; + + &-item { + display: flex; + justify-content: space-between; + align-items: center; + gap: 2px; + align-self: stretch; + + &-title { + color: rgba(255, 255, 255, 0.8); + font-feature-settings: "liga" off, "clig" off; + font-family: "PingFang SC"; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 24px; /* 171.429% */ + } + + &-desc { + color: #fff; + font-feature-settings: "liga" off, "clig" off; + font-family: "PingFang SC"; + font-size: 15px; + font-style: normal; + font-weight: 600; + line-height: 24px; /* 160% */ + } + } + } +} diff --git a/src/game_pages/detail/components/GamePlayAndReq/index.tsx b/src/game_pages/detail/components/GamePlayAndReq/index.tsx new file mode 100644 index 0000000..ae81fe2 --- /dev/null +++ b/src/game_pages/detail/components/GamePlayAndReq/index.tsx @@ -0,0 +1,46 @@ +import { View, Text } from "@tarojs/components"; +import { genNTRPRequirementText } from "../../utils/helper"; +import styles from "./index.module.scss"; + +// 玩法要求 +export default 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} + + + ))} + + + ); +} diff --git a/src/game_pages/detail/components/GameTags/index.module.scss b/src/game_pages/detail/components/GameTags/index.module.scss new file mode 100644 index 0000000..397759c --- /dev/null +++ b/src/game_pages/detail/components/GameTags/index.module.scss @@ -0,0 +1,44 @@ +.detail-page-content-avatar-tags { + padding: 20px 20px 0; + box-sizing: border-box; + display: flex; + align-items: center; + gap: 6px; + + &-avatar { + width: 28px; + height: 28px; + border-radius: 50%; + overflow: hidden; + + &-image { + width: 28px; + height: 28px; + } + } + + &-tags { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px; + + &-tag { + display: flex; + height: 28px; + padding: 6px 12px; + box-sizing: border-box; + align-items: center; + gap: 4px; + border-radius: 999px; + border: 0.5px solid rgba(255, 255, 255, 0.08); + background: rgba(255, 255, 255, 0.25); + color: #fff; + font-size: 13px; + font-weight: 500; + line-height: 20px; + letter-spacing: -0.23px; + text-align: center; + } + } +} diff --git a/src/game_pages/detail/components/GameTags/index.tsx b/src/game_pages/detail/components/GameTags/index.tsx new file mode 100644 index 0000000..ed04ab1 --- /dev/null +++ b/src/game_pages/detail/components/GameTags/index.tsx @@ -0,0 +1,49 @@ +import { View, Text, Image } from "@tarojs/components"; +import styles from "./index.module.scss"; + +export default function GameTags(props) { + const { userInfo, handleViewUserInfo } = props; + const { avatar_url, id } = userInfo; + const tags = [ + // { + // name: "🕙 急招", + // icon: "", + // }, + // { + // name: "🔥 本周热门", + // icon: "", + // }, + // { + // name: "🎉 新活动", + // icon: "", + // }, + // { + // name: "官方组织", + // icon: "", + // }, + ]; + return ( + + + {/* network image mock */} + + + + {tags.map((tag, index) => ( + + {tag.icon && } + {tag.name} + + ))} + + + ); +} diff --git a/src/game_pages/detail/components/OrganizerInfo/index.module.scss b/src/game_pages/detail/components/OrganizerInfo/index.module.scss new file mode 100644 index 0000000..bebb99d --- /dev/null +++ b/src/game_pages/detail/components/OrganizerInfo/index.module.scss @@ -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); + } + } + } + } + } + } + } +} diff --git a/src/game_pages/detail/components/OrganizerInfo/index.tsx b/src/game_pages/detail/components/OrganizerInfo/index.tsx new file mode 100644 index 0000000..a3a772a --- /dev/null +++ b/src/game_pages/detail/components/OrganizerInfo/index.tsx @@ -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 ( + + {/* orgnizer title */} + + 组织者 + + {/* organizer avatar and name */} + + + + + {nickname} + + + 已组织 {hosted_games_count} 次 + + NTRP {ntrp_level || "初学者"} + + + + {my_id === id ? ( + "" + ) : ( + + {is_following ? ( + + 取消关注 + + ) : ( + <> + + + 关注 + + + )} + + )} + handleAddComment()} + > + + + + + {/* recommend games by organizer */} + {recommendGames.length > 0 && ( + + + 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 */} + + { + e.stopPropagation(); + handleViewUserInfo(id); + }} + /> + + + + 报名人数 {game.checkedApplications}/ + {game.applications} + + + + {game.levelRequirements} + + + {game.playType} + + + + + ))} + + + + )} + + ); +} diff --git a/src/game_pages/detail/components/Participants/index.module.scss b/src/game_pages/detail/components/Participants/index.module.scss new file mode 100644 index 0000000..de9448d --- /dev/null +++ b/src/game_pages/detail/components/Participants/index.module.scss @@ -0,0 +1,132 @@ +.detail-page-content-participants { + padding: 24px 15px 0; + box-sizing: border-box; + + .participants-title { + display: flex; + padding-bottom: 6px; + align-items: center; + overflow: hidden; + 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); + } + + .participants-list { + padding: 10px 0 0; + height: 162px; + display: flex; + flex-direction: row; + gap: 8px; + + &-application { + display: flex; + width: 108px; + height: 162px; + padding: 16px 12px 10px 12px; + box-sizing: border-box; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 8px; + align-self: stretch; + border-radius: 20px; + border: 1px dashed rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.16); + flex: 0 0 auto; + + &-icon { + width: 28px; + height: 28px; + } + + &-text { + color: rgba(255, 255, 255, 0.6); + font-feature-settings: "liga" off, "clig" off; + font-family: "PingFang SC"; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 20px; /* 166.667% */ + } + } + + &-scroll { + flex: 0 0 auto; + width: calc(100% - 116px); + + &-content { + display: flex; + flex-direction: row; + gap: 8px; + height: 162px; + flex-wrap: nowrap; + + .participants-list-item { + display: flex; + width: 108px; + padding: 16px 4px 10px 4px; + box-sizing: border-box; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 4px; + border-radius: 20px; + border: 0.5px solid rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.16); + flex: 0 0 auto; + + .participants-list-item-avatar { + width: 60px; + height: 60px; + border-radius: 50%; + overflow: hidden; + } + + &-name { + width: 100%; + color: rgba(255, 255, 255, 0.85); + text-align: center; + font-feature-settings: "liga" off, "clig" off; + font-family: "PingFang SC"; + font-size: 13px; + font-style: normal; + font-weight: 500; + line-height: 24px; /* 184.615% */ + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &-level { + color: rgba(255, 255, 255, 0.45); + text-align: center; + font-feature-settings: "liga" off, "clig" off; + font-family: "PingFang SC"; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 166.667% */ + } + + &-role { + color: #fff; + text-align: center; + font-feature-settings: "liga" off, "clig" off; + font-family: "PingFang SC"; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 20px; /* 166.667% */ + } + } + } + } + } +} diff --git a/src/game_pages/detail/components/Participants/index.tsx b/src/game_pages/detail/components/Participants/index.tsx new file mode 100644 index 0000000..a761f77 --- /dev/null +++ b/src/game_pages/detail/components/Participants/index.tsx @@ -0,0 +1,106 @@ +import { View, Text, Image, ScrollView } from "@tarojs/components"; +import dayjs from "dayjs"; +import img from "@/config/images"; +import styles from "./index.module.scss"; + +// 参与者 +export default function Participants(props) { + const { detail = {}, handleJoinGame, handleViewUserInfo } = props; + const participants = detail.participants || []; + const { + participant_count, + max_participants, + user_action_status = {}, + start_time, + } = 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 && + dayjs(start_time).isAfter(dayjs()); + const leftCount = max_participants - participant_count; + return ( + + + 参与者 + · + {leftCount > 0 ? `剩余空位 ${leftCount}` : "已满员"} + + {participant_count > 0 || showApplicationEntry ? ( + + {/* application */} + {showApplicationEntry && ( + { + handleJoinGame(); + }} + > + + + 申请加入 + + + )} + {/* participants list */} + + + {participants.map((participant) => { + const { + is_organizer, + user: { + avatar_url, + nickname, + level, + id: participant_user_id, + }, + } = participant; + const role = is_organizer ? "组织者" : "参与者"; + return ( + + + + {nickname || "未知"} + + + {level || "未知"} + + + {role} + + + ); + })} + + + + ) : ( + "" + )} + + ); +} diff --git a/src/game_pages/detail/style.module.scss b/src/game_pages/detail/components/SharePopup/index.module.scss similarity index 61% rename from src/game_pages/detail/style.module.scss rename to src/game_pages/detail/components/SharePopup/index.module.scss index fce6dc2..6ac57ce 100644 --- a/src/game_pages/detail/style.module.scss +++ b/src/game_pages/detail/components/SharePopup/index.module.scss @@ -119,80 +119,3 @@ } } } - -.posterContainer { - background: linear-gradient(180deg, #fff 0%, #fafafa 100%), #fff; - padding: 20px; -} - -.posterWrap { - border-radius: 19.067px; - border: 1px solid rgba(0, 0, 0, 0.06); - background: linear-gradient(180deg, #bfffef 0%, #f2fffc 100%), #fff; - box-shadow: 0 6.933px 55.467px 0 rgba(0, 0, 0, 0.1); - overflow: hidden; - box-sizing: border-box; -} - -.sharePoster { - margin-top: 40px; - display: flex; - align-items: center; - justify-content: space-around; - - .shareItem { - display: flex; - flex-direction: column; - align-items: center; - justify-content: space-between; - gap: 12px; - color: rgba(0, 0, 0, 0.85); - font-feature-settings: "liga" off, "clig" off; - font-family: "PingFang SC"; - font-size: 14px; - font-style: normal; - font-weight: 600; - line-height: normal; - - background-color: #fff; - border: none; - padding: 0; - margin: 0; - line-height: normal; - font-size: inherit; - color: inherit; - - &:after { - border: none; - background: transparent; - } - - .icon { - width: 64px; - height: 64px; - border-radius: 50%; - background-color: #fff; - display: flex; - align-items: center; - justify-content: center; - border: 1px solid rgba(0, 0, 0, 0.06); - box-shadow: 0 8px 64px 0 rgba(0, 0, 0, 0.1); - - &.wechatIcon { - background-color: #07c160; - } - .download { - width: 28px; - height: 28px; - } - .wechat { - width: 36px; - height: 30px; - } - .timeline { - width: 32px; - height: 32px; - } - } - } -} diff --git a/src/game_pages/detail/components/SharePopup/index.tsx b/src/game_pages/detail/components/SharePopup/index.tsx new file mode 100644 index 0000000..ec607ed --- /dev/null +++ b/src/game_pages/detail/components/SharePopup/index.tsx @@ -0,0 +1,165 @@ +import { forwardRef, useState, useEffect, useImperativeHandle } from "react"; +import { View, Button, Image, Text } from "@tarojs/components"; +import Taro, { useShareAppMessage } from "@tarojs/taro"; +import dayjs from "dayjs"; +import "dayjs/locale/zh-cn"; +import classnames from "classnames"; +import { generateShareImage } from "@/utils"; +import DetailService from "@/services/detailService"; +import { CommonPopup } from "@/components"; +import DownloadIcon from "@/static/detail/download_icon.svg"; +import WechatLogo from "@/static/detail/wechat_icon.svg"; +// import WechatTimeline from "@/static/detail/wechat_timeline.svg"; +import LinkIcon from "@/static/detail/link.svg"; +import CrossIcon from "@/static/detail/cross.svg"; +import { genNTRPRequirementText, navto } from "../../utils/helper"; +import { DayOfWeekMap } from "../../config"; +import styles from "./index.module.scss"; + +dayjs.locale("zh-cn"); + +// 分享弹窗 +export default forwardRef(({ id, from, detail, userInfo }, ref) => { + const [visible, setVisible] = useState(false); + const [publishFlag, setPublishFlag] = useState(false); + // const posterRef = useRef(); + const { max_participants, participant_count } = detail || {}; + + useEffect(() => { + if (id) { + changeMessageType(); + } + }, [id]); + + async function changeMessageType() { + try { + const res = await DetailService.getActivityId({ + business_id: id, + business_type: "game", + is_private: false, + }); + if (res.code === 0) { + Taro.updateShareMenu({ + withShareTicket: false, // 是否需要返回 shareTicket + isUpdatableMessage: true, // 是否是动态消息(需要服务端配置过模版) + activityId: res.data.activity_id, // 动态消息的活动 id + }); + } + } catch (e) { + Taro.showToast({ title: e.message, icon: "none" }); + } + } + + useImperativeHandle(ref, () => ({ + show: (publish_flag = false) => { + setPublishFlag(publish_flag); + setVisible(true); + }, + })); + + useShareAppMessage(async (res) => { + const { + play_type, + skill_level_max, + skill_level_min, + start_time, + end_time, + location_name, + venue_image_list, + } = detail || {}; + const startTime = dayjs(start_time); + const endTime = dayjs(end_time); + const dayofWeek = DayOfWeekMap.get(startTime.day()); + const gameLength = `${endTime.diff(startTime, "hour")}小时`; + const url = await generateShareImage({ + userAvatar: userInfo.avatar_url, + userNickname: userInfo.nickname, + gameType: play_type, + skillLevel: `NTRP ${genNTRPRequirementText( + skill_level_min, + skill_level_max + )}`, + gameDate: `${startTime.format("M月D日")} (${dayofWeek})`, + gameTime: `${startTime.format("ah")}点 ${gameLength}`, + venueName: location_name, + venueImages: venue_image_list ? venue_image_list.map((c) => c.url) : [], + }); + // console.log(res, "res"); + return { + title: detail.title, + imageUrl: url || "https://img.yzcdn.cn/vant/cat.jpeg", + path: `/game_pages/detail/index?id=${id}&from=share`, + }; + }); + + async function handlePost() { + navto(`/game_pages/sharePoster/index?id=${detail.id}`); + setVisible(false); + } + + function onClose() { + setVisible(false); + setPublishFlag(false); + } + + return ( + <> + + + + {publishFlag ? ( + 球局发布成功 🎉 + ) : ( + 分享至 + )} + + + + + {publishFlag && ( + + + 还剩 + + {" "} + {max_participants - participant_count}{" "} + + 人加入完成组局, 去邀请好友加入吧~ + + + )} + + + + + + + + {/* */} + + ); +}); diff --git a/src/game_pages/detail/components/StickyBottom/index.module.scss b/src/game_pages/detail/components/StickyBottom/index.module.scss new file mode 100644 index 0000000..5239968 --- /dev/null +++ b/src/game_pages/detail/components/StickyBottom/index.module.scss @@ -0,0 +1,124 @@ +.sticky-bottom-bar { + position: sticky; + bottom: 0; + padding: 10px 10px 32px; + box-sizing: border-box; + width: 100%; + height: 92px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 6px; + + &-share-and-comment { + display: flex; + align-items: center; + height: 52px; + width: 120px; + box-sizing: border-box; + padding: 2px 20px; + justify-content: center; + gap: 16px; + border-radius: 16px; + border: 1px solid rgba(255, 255, 255, 0.06); + background: #fff; + + .sticky-bottom-bar-share { + display: flex; + align-items: center; + flex-direction: column; + gap: 4px; + + &-icon { + width: 16px; + height: 16px; + } + + &-text { + color: rgba(0, 0, 0, 0.85); + font-size: 10px; + font-style: normal; + font-weight: 500; + line-height: 16px; /* 160% */ + } + } + + &-separator { + width: 1px; + height: 24px; + background: rgba(0, 0, 0, 0.1); + } + + .sticky-bottom-bar-comment { + display: flex; + align-items: center; + flex-direction: column; + gap: 4px; + + &-icon { + width: 16px; + height: 16px; + } + + &-text { + color: rgba(0, 0, 0, 0.85); + font-size: 10px; + font-style: normal; + font-weight: 500; + line-height: 16px; /* 160% */ + } + } + } + + .detail-main-action { + display: flex; + align-items: center; + height: 52px; + width: auto; + // padding: 2px 6px; + box-sizing: border-box; + justify-content: center; + // gap: 12px; + flex: 1 0 0; + border-radius: 16px; + // border: 1px solid rgba(0, 0, 0, 0.06); + background: #fff; + overflow: hidden; + + &.disabled { + background-color: #b4b4b4; + color: rgba(60, 60, 67, 0.6); + pointer-events: none; + } + + .sticky-bottom-bar-join-game { + margin-left: auto; + // width: 151px; + display: flex; + align-items: center; + justify-content: center; + flex: 1; + + &-price { + font-family: "PoetsenOne"; + font-size: 28px; + font-weight: 400; + line-height: 24px; /* 114.286% */ + letter-spacing: -0.56px; + color: #000; + } + } + + .game_manage { + width: 100px; + margin-left: auto; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: #000; + color: #fff; + pointer-events: all; + } + } +} diff --git a/src/game_pages/detail/components/StickyBottom/index.tsx b/src/game_pages/detail/components/StickyBottom/index.tsx new file mode 100644 index 0000000..53e986c --- /dev/null +++ b/src/game_pages/detail/components/StickyBottom/index.tsx @@ -0,0 +1,253 @@ +import { useEffect, useRef, useState } from "react"; +import Taro from "@tarojs/taro"; +import classnames from "classnames"; +import dayjs from "dayjs"; +import { Text, View, Image } from "@tarojs/components"; +import OrderService from "@/services/orderService"; +import { EvaluateCallback, EvaluateScene } from "@/store/evaluateStore"; +import { MATCH_STATUS, IsSubstituteSupported } from "@/services/detailService"; +import { GameManagePopup, NTRPEvaluatePopup } from "@/components"; +import img from "@/config/images"; +import { toast, navto } from "../../utils/helper"; +import styles from "./index.module.scss"; + +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; +} + +// 底部操作栏 +export default function StickyButton(props) { + const { + handleShare, + handleJoinGame, + detail, + onStatusChange, + handleAddComment, + getCommentCount, + } = props; + const [commentCount, setCommentCount] = useState(0); + const ntrpRef = useRef<{ + show: (evaluateCallback: EvaluateCallback) => void; + }>({ show: () => {} }); + const { + id, + price, + user_action_status, + match_status, + start_time, + end_time, + is_organizer, + } = detail || {}; + + const gameManageRef = useRef(); + + function handleSelfEvaluate() { + ntrpRef?.current?.show({ + type: EvaluateScene.detail, + next: (flag) => { + if (flag) { + Taro.navigateTo({ + url: `/order_pages/orderDetail/index?gameId=${id}`, + }); + return; + } + Taro.redirectTo({ url: `/order_pages/orderDetail/index?gameId=${id}` }); + }, + onCancel: () => { + // Taro.redirectTo({ url: `/game_pages/detail/index?id=${id}` }); + Taro.navigateBack(); + }, + }); + } + + 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: () => ¥{displayPrice} 已加入, + action: () => toast("已加入"), + }; + } else if (is_substituting) { + return { + text: () => ¥{displayPrice} 已加入候补, + action: () => toast("已加入候补"), + }; + } else if (can_pay) { + return { + text: () => ¥{price} 继续支付, + 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: () => ¥{displayPrice} 我要候补, + action: handleJoinGame, + }; + } else if (can_join) { + return { + text: () => { + return ¥{displayPrice} 立即加入; + }, + action: handleJoinGame, + }; + } else if (can_assess) { + return { + text: () => ¥{displayPrice} 立即加入, + 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 as string}; + }; + } + + return ( + <> + + + handleShare()} + > + + 分享 + + + { + // Taro.showToast({ title: "To be continued", icon: "none" }); + handleAddComment(); + }} + > + + + {commentCount > 0 ? commentCount : "评论"} + + + + + + + + {is_organizer && ( + { + gameManageRef.current.show(detail, onStatusChange); + }} + > + 管理 + + )} + + + + + + ); +} diff --git a/src/game_pages/detail/components/SupplementalNotes/index.module.scss b/src/game_pages/detail/components/SupplementalNotes/index.module.scss new file mode 100644 index 0000000..89efbf8 --- /dev/null +++ b/src/game_pages/detail/components/SupplementalNotes/index.module.scss @@ -0,0 +1,55 @@ +.detail-page-content-supplemental-notes { + padding: 24px 15px 0; + + .supplemental-notes-title { + overflow: hidden; + padding-bottom: 7px; + 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); + } + + .supplemental-notes-content { + padding: 12px 0 0; + display: flex; + flex-direction: column; + gap: 6px; + + &-tags { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + + &-tag { + overflow: hidden; + color: #fff; + font-feature-settings: "liga" off, "clig" off; + text-overflow: ellipsis; + font-family: "PingFang SC"; + font-size: 15px; + font-style: normal; + font-weight: 500; + line-height: 24px; /* 160% */ + } + } + + &-text { + overflow: hidden; + color: rgba(255, 255, 255, 0.65); + font-feature-settings: "liga" off, "clig" off; + font-family: "PingFang SC"; + font-size: 15px; + font-style: normal; + font-weight: 400; + line-height: 24px; /* 160% */ + } + } +} diff --git a/src/game_pages/detail/components/SupplementalNotes/index.tsx b/src/game_pages/detail/components/SupplementalNotes/index.tsx new file mode 100644 index 0000000..08bcdf9 --- /dev/null +++ b/src/game_pages/detail/components/SupplementalNotes/index.tsx @@ -0,0 +1,33 @@ +import { Text, View } from "@tarojs/components"; +import styles from "./index.module.scss"; +import { insertDotInTags } from "../../utils/helper"; + +export default function SupplementalNotes(props) { + const { + detail: { description, description_tag }, + } = props; + return ( + + + 补充说明 + + + {/* supplemental notes tags */} + + {insertDotInTags(description_tag || []).map((tag, index) => ( + + {tag} + + ))} + + {/* supplemental notes content */} + + {description} + + + + ); +} diff --git a/src/game_pages/detail/components/VenueInfo/index.module.scss b/src/game_pages/detail/components/VenueInfo/index.module.scss new file mode 100644 index 0000000..292fa6d --- /dev/null +++ b/src/game_pages/detail/components/VenueInfo/index.module.scss @@ -0,0 +1,115 @@ +.detail-page-content-venue { + padding: 24px 15px 0; + box-sizing: border-box; + + .venue-detail-title { + display: flex; + height: 31px; + align-items: center; + justify-content: flex-start; + gap: 8px; + padding-bottom: 6px; + color: #fff; + text-overflow: ellipsis; + font-family: "PingFang SC"; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 24px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + + .venue-reserve-status { + display: inline-flex; + justify-content: flex-start; + align-items: center; + gap: 4px; + .venue-reserve-screenshot { + width: 16px; + height: 16px; + } + } + } + + .venue-detail-content { + padding: 16px 0 0; + box-sizing: border-box; + + &-tags { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + + &-tag { + overflow: hidden; + color: #fff; + font-feature-settings: "liga" off, "clig" off; + text-overflow: ellipsis; + font-family: "PingFang SC"; + font-size: 15px; + font-style: normal; + font-weight: 500; + line-height: 24px; /* 160% */ + } + } + + &-remarks { + overflow: hidden; + color: rgba(255, 255, 255, 0.65); + // font-feature-settings: 'liga' off, 'clig' off; + // text-overflow: ellipsis; + // white-space: nowrap; + font-family: "PingFang SC"; + font-size: 15px; + font-style: normal; + font-weight: 400; + line-height: 24px; /* 160% */ + } + } + + .venue-screenshot-title { + display: flex; + padding: 18px 20px 10px 20px; + align-items: center; + align-self: stretch; + font-family: "PingFang SC"; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 24px; /* 150% */ + letter-spacing: -0.23px; + } + + .venue-screenshot-scroll-view { + max-height: calc(100vh - 260px); + overflow-y: auto; + padding-bottom: 40px; + + .venue-screenshot-image-list { + width: 100%; + padding: 0 16px; + box-sizing: border-box; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 10px 10px; + + .venue-screenshot-image-item { + aspect-ratio: 1/1; + border-radius: 9px; + border: 1px solid rgba(0, 0, 0, 0.12); + box-sizing: border-box; + background: rgba(0, 0, 0, 0.06); + margin: 0; + position: relative; + + .venue-screenshot-image-item-image { + width: 100%; + height: 100%; + border-radius: 9px; + margin: 0; + object-fit: cover; + } + } + } + } +} diff --git a/src/game_pages/detail/components/VenueInfo/index.tsx b/src/game_pages/detail/components/VenueInfo/index.tsx new file mode 100644 index 0000000..2cb8533 --- /dev/null +++ b/src/game_pages/detail/components/VenueInfo/index.tsx @@ -0,0 +1,105 @@ +import { useState } from "react"; +import { View, Text, Image, ScrollView } from "@tarojs/components"; +import Taro from "@tarojs/taro"; +import { CommonPopup } from "@/components"; +import img from "@/config/images"; +import styles from "./index.module.scss"; +import { insertDotInTags } from "../../utils/helper"; + +// 场馆信息 +export default 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 ( + + {/* 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?.length > 0 && + venue_image_list.map((item) => { + return ( + + + + ); + })} + + + + + ); +} diff --git a/src/game_pages/detail/index.module.scss b/src/game_pages/detail/index.module.scss new file mode 100644 index 0000000..757116c --- /dev/null +++ b/src/game_pages/detail/index.module.scss @@ -0,0 +1,111 @@ +.detail-page { + width: 100%; + height: 100%; + // padding-bottom: env(safe-area-inset-bottom); + + .custom-navbar { + height: 56px; /* 通常与原生导航栏高度一致 */ + display: flex; + align-items: center; + justify-content: center; + // background-color: #fff; + color: #000; + padding-top: 44px; /* 适配状态栏 */ + position: sticky; + top: 0; + z-index: 100; + overflow: hidden; + background-color: rgba(0, 0, 0, 0.2); + } + + .detail-navigator { + height: 30px; + width: 80px; + border-radius: 15px; + position: absolute; + left: 12px; + border: 1px solid #888; + box-sizing: border-box; + color: #fff; + display: flex; + align-items: center; + background: rgba(0, 0, 0, 0.1); + + .detail-navigator-back { + border-right: 1px solid #444; + } + + .detail-navigator-back, + .detail-navigator-icon { + height: 20px; + width: 50%; + + display: flex; + justify-content: center; + + & > .detail-navigator-back-icon { + width: 20px; + height: 20px; + color: #fff; + } + + & > .detail-navigator-logo-icon { + width: 20px; + height: 20px; + color: #fff; + } + } + } + + .detail-page-bg { + width: 100%; + height: 100%; + position: fixed; + left: 0; + top: 0; + background-size: cover; + // filter: blur(40px); + // transform: scale(1.5); + z-index: -2; + // width: calc(100% + 20px); + // height: calc(100% + 20px); + // margin: -10px; + width: 100vw; + height: 100vh; + + &::after { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -1; + background: linear-gradient( + 180deg, + rgba(0, 0, 0, 0.8) 0%, + rgba(0, 0, 0, 0.4) 100% + ); + backdrop-filter: blur(100px); + } + } + + .detail-page-content { + &-title { + padding: 20px 20px 0; + box-sizing: border-box; + + &-text { + overflow: hidden; + color: #fff; + font-feature-settings: "liga" off, "clig" off; + text-overflow: ellipsis; + font-family: "PingFang SC"; + font-size: 22px; + font-style: normal; + font-weight: 600; + line-height: 32px; /* 145.455% */ + } + } + } +} diff --git a/src/game_pages/detail/index.scss b/src/game_pages/detail/index.scss deleted file mode 100644 index 1fff31c..0000000 --- a/src/game_pages/detail/index.scss +++ /dev/null @@ -1,1138 +0,0 @@ -@use "~@/scss/images.scss" as img; - -.detail-page { - width: 100%; - height: 100%; - // padding-bottom: env(safe-area-inset-bottom); - - .custom-navbar { - height: 56px; /* 通常与原生导航栏高度一致 */ - display: flex; - align-items: center; - justify-content: center; - // background-color: #fff; - color: #000; - padding-top: 44px; /* 适配状态栏 */ - position: sticky; - top: 0; - z-index: 100; - overflow: hidden; - background-color: rgba(0, 0, 0, 0.2); - } - - .detail-navigator { - height: 30px; - width: 80px; - border-radius: 15px; - position: absolute; - left: 12px; - border: 1px solid #888; - box-sizing: border-box; - color: #fff; - display: flex; - align-items: center; - background: rgba(0, 0, 0, 0.1); - - .detail-navigator-back { - border-right: 1px solid #444; - } - - .detail-navigator-back, - .detail-navigator-icon { - height: 20px; - width: 50%; - - display: flex; - justify-content: center; - - & > .detail-navigator-back-icon { - width: 20px; - height: 20px; - color: #fff; - } - - & > .detail-navigator-logo-icon { - width: 20px; - height: 20px; - color: #fff; - } - } - } - - .detail-page-bg { - width: 100%; - height: 100%; - position: fixed; - left: 0; - top: 0; - background-size: cover; - // filter: blur(40px); - // transform: scale(1.5); - z-index: -2; - // width: calc(100% + 20px); - // height: calc(100% + 20px); - // margin: -10px; - width: 100vw; - height: 100vh; - - &::after { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: -1; - background: linear-gradient( - 180deg, - rgba(0, 0, 0, 0.8) 0%, - rgba(0, 0, 0, 0.4) 100% - ); - backdrop-filter: blur(100px); - } - } - - .detail-swiper-container { - height: 270px; - width: 100%; - padding: 15px 15px 0; - box-sizing: border-box; - overflow-x: scroll; - &::-webkit-scrollbar { - display: none; - width: 0; - height: 0; - color: transparent; /* 透明色 */ - } - - .detail-swiper-scroll-container { - display: flex; - height: 250px; - width: auto; - align-items: center; - justify-content: flex-start; - flex-wrap: nowrap; - gap: 12px; - - .detail-swiper-item { - flex: 0 0 auto; - height: 250px; - display: flex; - align-items: center; - &-image { - border-radius: 12px; - transition: transform 0.5s; - } - } - } - } - - .detail-swiper { - height: 240px; - margin-top: 15px; - margin-left: 15px; - } - - .detail-swiper-item { - overflow: visible; - height: 100%; - - .detail-swiper-item-image { - width: 100%; - height: 100%; - border-radius: 12px; - transition: transform 0.5s; - } - } - - .detail-page-content { - &-avatar-tags { - padding: 20px 20px 0; - box-sizing: border-box; - display: flex; - align-items: center; - gap: 6px; - - &-avatar { - width: 28px; - height: 28px; - border-radius: 50%; - overflow: hidden; - - &-image { - width: 28px; - height: 28px; - } - } - - &-tags { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 6px; - - &-tag { - display: flex; - height: 28px; - padding: 6px 12px; - box-sizing: border-box; - align-items: center; - gap: 4px; - border-radius: 999px; - border: 0.5px solid rgba(255, 255, 255, 0.08); - background: rgba(255, 255, 255, 0.25); - color: #fff; - font-size: 13px; - font-weight: 500; - line-height: 20px; - letter-spacing: -0.23px; - text-align: center; - } - } - } - - &-title { - padding: 20px 20px 0; - box-sizing: border-box; - - &-text { - overflow: hidden; - color: #fff; - font-feature-settings: "liga" off, "clig" off; - text-overflow: ellipsis; - font-family: "PingFang SC"; - font-size: 22px; - font-style: normal; - font-weight: 600; - line-height: 32px; /* 145.455% */ - } - } - - &-game-info { - &-date-weather { - padding: 20px 20px 0; - box-sizing: border-box; - display: flex; - align-items: center; - justify-content: space-between; - // gap: 12px; - - &-calendar-date { - width: 60%; - display: flex; - align-items: center; - gap: 16px; - - &-calendar { - display: flex; - width: 48px; - height: 48px; - box-sizing: border-box; - flex-direction: column; - align-items: center; - gap: 4px; - border-radius: 12px; - // border: 0.5px solid rgba(255, 255, 255, 0.08); - background: rgba(255, 255, 255, 0.25); - overflow: hidden; - color: #fff; - background: #536272; - - .month { - width: 100%; - height: 18px; - font-size: 10px; - display: flex; - padding: 1px auto; - box-sizing: border-box; - justify-content: center; - align-items: center; - // border-bottom: 1px solid rgba(255, 255, 255, 0.08); - background: #7b828b; - } - - .day { - display: flex; - width: 48px; - height: 30px; - // padding-bottom: 6px; - box-sizing: border-box; - flex-direction: column; - align-items: center; - // border: 0.5px solid rgba(255, 255, 255, 0.08); - // background: rgba(255, 255, 255, 0.25); - // background-color: #536272; - } - } - - &-date { - display: flex; - flex-direction: column; - align-items: flex-start; - justify-content: space-evenly; - gap: 4px; - align-self: stretch; - color: #fff; - - .date { - color: #fff; - font-feature-settings: "liga" off, "clig" off; - font-family: "PingFang SC"; - font-size: 16px; - font-style: normal; - font-weight: 600; - line-height: 24px; /* 150% */ - } - - .venue-time { - color: rgba(255, 255, 255, 0.8); - font-feature-settings: "liga" off, "clig" off; - font-family: "PingFang SC"; - font-size: 12px; - font-style: normal; - font-weight: 400; - line-height: 20px; /* 166.667% */ - } - } - } - - &-weather { - display: flex; - align-items: flex-end; - flex-direction: column; - gap: 4px; - - &-icon { - width: 20px; - height: 20px; - color: rgba(255, 255, 255, 0.8); - } - &-text-temperature { - display: flex; - align-items: center; - gap: 12px; - color: rgba(255, 255, 255, 0.8); - font-feature-settings: "liga" off, "clig" off; - font-family: "PingFang SC"; - font-size: 12px; - font-style: normal; - font-weight: 400; - line-height: 20px; /* 166.667% */ - } - } - } - - &-place { - .location-message { - padding: 20px 20px 0; - box-sizing: border-box; - display: flex; - align-items: center; - justify-content: flex-start; - gap: 12px; - - &-icon { - width: 48px; - height: 48px; - border-radius: 12px; - padding: 14px; - box-sizing: border-box; - background: #4d5865; - display: flex; - justify-content: center; - align-items: center; - overflow: hidden; - // border: 0.5px solid rgba(255, 255, 255, 0.08); - // background: rgba(255, 255, 255, 0.25); - - &-image { - width: 20px; - height: 20px; - } - } - - &-text { - display: flex; - flex-direction: column; - align-items: flex-start; - justify-content: space-evenly; - gap: 4px; - align-self: stretch; - - &-name-distance { - display: flex; - align-items: center; - gap: 4px; - color: #fff; - text-align: center; - font-feature-settings: "liga" off, "clig" off; - font-family: "PingFang SC"; - font-size: 16px; - font-style: normal; - font-weight: 600; - line-height: 24px; /* 150% */ - - &-arrow { - width: 16px; - height: 16px; - } - } - - &-address { - color: rgba(255, 255, 255, 0.8); - text-align: center; - font-feature-settings: "liga" off, "clig" off; - font-family: "PingFang SC"; - font-size: 12px; - font-style: normal; - font-weight: 400; - line-height: 20px; /* 166.667% */ - } - } - } - - .location-map { - width: 100%; - padding: 20px 20px 0; - box-sizing: border-box; - display: flex; - align-items: center; - justify-content: center; - overflow: hidden; - border-radius: 12px; - - &-map { - width: 100%; - height: 95px; - border-radius: 12px; - } - } - } - } - - &-venue { - padding: 24px 15px 0; - box-sizing: border-box; - - .venue-detail-title { - display: flex; - height: 31px; - align-items: center; - justify-content: flex-start; - gap: 8px; - padding-bottom: 6px; - color: #fff; - text-overflow: ellipsis; - font-family: "PingFang SC"; - font-size: 16px; - font-style: normal; - font-weight: 600; - line-height: 24px; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); - - .venue-reserve-status { - display: inline-flex; - justify-content: flex-start; - align-items: center; - gap: 4px; - .venue-reserve-screenshot { - width: 16px; - height: 16px; - } - } - } - - .venue-detail-content { - padding: 16px 0 0; - box-sizing: border-box; - - &-tags { - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; - - &-tag { - overflow: hidden; - color: #fff; - font-feature-settings: "liga" off, "clig" off; - text-overflow: ellipsis; - font-family: "PingFang SC"; - font-size: 15px; - font-style: normal; - font-weight: 500; - line-height: 24px; /* 160% */ - } - } - - &-remarks { - overflow: hidden; - color: rgba(255, 255, 255, 0.65); - // font-feature-settings: 'liga' off, 'clig' off; - // text-overflow: ellipsis; - // white-space: nowrap; - font-family: "PingFang SC"; - font-size: 15px; - font-style: normal; - font-weight: 400; - line-height: 24px; /* 160% */ - } - } - - .venue-screenshot-title { - display: flex; - padding: 18px 20px 10px 20px; - align-items: center; - align-self: stretch; - font-family: "PingFang SC"; - font-size: 16px; - font-style: normal; - font-weight: 500; - line-height: 24px; /* 150% */ - letter-spacing: -0.23px; - } - - .venue-screenshot-scroll-view { - max-height: calc(100vh - 260px); - overflow-y: auto; - padding-bottom: 40px; - - .venue-screenshot-image-list { - width: 100%; - padding: 0 16px; - box-sizing: border-box; - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 10px 10px; - - .venue-screenshot-image-item { - aspect-ratio: 1/1; - border-radius: 9px; - border: 1px solid rgba(0, 0, 0, 0.12); - box-sizing: border-box; - background: rgba(0, 0, 0, 0.06); - margin: 0; - position: relative; - - .venue-screenshot-image-item-image { - width: 100%; - height: 100%; - border-radius: 9px; - margin: 0; - object-fit: cover; - } - } - } - } - } - - &-gameplay-requirements { - padding: 24px 15px 0; - box-sizing: border-box; - - .gameplay-requirements-title { - overflow: hidden; - color: #fff; - height: 24px; - padding-bottom: 6px; - 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); - } - - .gameplay-requirements { - padding: 12px 0 0; - display: flex; - flex-direction: column; - gap: 12px; - - &-item { - display: flex; - justify-content: space-between; - align-items: center; - gap: 2px; - align-self: stretch; - - &-title { - color: rgba(255, 255, 255, 0.8); - font-feature-settings: "liga" off, "clig" off; - font-family: "PingFang SC"; - font-size: 14px; - font-style: normal; - font-weight: 400; - line-height: 24px; /* 171.429% */ - } - - &-desc { - color: #fff; - font-feature-settings: "liga" off, "clig" off; - font-family: "PingFang SC"; - font-size: 15px; - font-style: normal; - font-weight: 600; - line-height: 24px; /* 160% */ - } - } - } - } - - &-participants { - padding: 24px 15px 0; - box-sizing: border-box; - - .participants-title { - display: flex; - padding-bottom: 6px; - align-items: center; - overflow: hidden; - 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); - } - - .participants-list { - padding: 10px 0 0; - height: 162px; - display: flex; - flex-direction: row; - gap: 8px; - - &-application { - display: flex; - width: 108px; - height: 162px; - padding: 16px 12px 10px 12px; - box-sizing: border-box; - flex-direction: column; - justify-content: center; - align-items: center; - gap: 8px; - align-self: stretch; - border-radius: 20px; - border: 1px dashed rgba(255, 255, 255, 0.2); - background: rgba(255, 255, 255, 0.16); - flex: 0 0 auto; - - &-icon { - width: 28px; - height: 28px; - } - - &-text { - color: rgba(255, 255, 255, 0.6); - font-feature-settings: "liga" off, "clig" off; - font-family: "PingFang SC"; - font-size: 12px; - font-style: normal; - font-weight: 500; - line-height: 20px; /* 166.667% */ - } - } - - &-scroll { - flex: 0 0 auto; - width: calc(100% - 116px); - - &-content { - display: flex; - flex-direction: row; - gap: 8px; - height: 162px; - flex-wrap: nowrap; - - .participants-list-item { - display: flex; - width: 108px; - padding: 16px 4px 10px 4px; - box-sizing: border-box; - flex-direction: column; - justify-content: center; - align-items: center; - gap: 4px; - border-radius: 20px; - border: 0.5px solid rgba(255, 255, 255, 0.2); - background: rgba(255, 255, 255, 0.16); - flex: 0 0 auto; - - .participants-list-item-avatar { - width: 60px; - height: 60px; - border-radius: 50%; - overflow: hidden; - } - - &-name { - width: 100%; - color: rgba(255, 255, 255, 0.85); - text-align: center; - font-feature-settings: "liga" off, "clig" off; - font-family: "PingFang SC"; - font-size: 13px; - font-style: normal; - font-weight: 500; - line-height: 24px; /* 184.615% */ - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - &-level { - color: rgba(255, 255, 255, 0.45); - text-align: center; - font-feature-settings: "liga" off, "clig" off; - font-family: "PingFang SC"; - font-size: 12px; - font-style: normal; - font-weight: 400; - line-height: 20px; /* 166.667% */ - } - - &-role { - color: #fff; - text-align: center; - font-feature-settings: "liga" off, "clig" off; - font-family: "PingFang SC"; - font-size: 12px; - font-style: normal; - font-weight: 500; - line-height: 20px; /* 166.667% */ - } - } - } - } - } - } - - &-supplemental-notes { - padding: 24px 15px 0; - - .supplemental-notes-title { - overflow: hidden; - padding-bottom: 7px; - 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); - } - - .supplemental-notes-content { - padding: 12px 0 0; - display: flex; - flex-direction: column; - gap: 6px; - - &-tags { - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; - - &-tag { - overflow: hidden; - color: #fff; - font-feature-settings: "liga" off, "clig" off; - text-overflow: ellipsis; - font-family: "PingFang SC"; - font-size: 15px; - font-style: normal; - font-weight: 500; - line-height: 24px; /* 160% */ - } - } - - &-text { - overflow: hidden; - color: rgba(255, 255, 255, 0.65); - font-feature-settings: "liga" off, "clig" off; - font-family: "PingFang SC"; - font-size: 15px; - font-style: normal; - font-weight: 400; - line-height: 24px; /* 160% */ - } - } - } - - &-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); - } - } - } - } - } - } - } - } - - .sticky-bottom-bar { - position: sticky; - bottom: 0; - padding: 10px 10px 32px; - box-sizing: border-box; - width: 100%; - height: 92px; - display: flex; - align-items: center; - justify-content: space-between; - gap: 6px; - - &-share-and-comment { - display: flex; - align-items: center; - height: 52px; - width: 120px; - box-sizing: border-box; - padding: 2px 20px; - justify-content: center; - gap: 16px; - border-radius: 16px; - border: 1px solid rgba(255, 255, 255, 0.06); - background: #fff; - - .sticky-bottom-bar-share { - display: flex; - align-items: center; - flex-direction: column; - gap: 4px; - - &-icon { - width: 16px; - height: 16px; - } - - &-text { - color: rgba(0, 0, 0, 0.85); - font-size: 10px; - font-style: normal; - font-weight: 500; - line-height: 16px; /* 160% */ - } - } - - &-separator { - width: 1px; - height: 24px; - background: rgba(0, 0, 0, 0.1); - } - - .sticky-bottom-bar-comment { - display: flex; - align-items: center; - flex-direction: column; - gap: 4px; - - &-icon { - width: 16px; - height: 16px; - } - - &-text { - color: rgba(0, 0, 0, 0.85); - font-size: 10px; - font-style: normal; - font-weight: 500; - line-height: 16px; /* 160% */ - } - } - } - - .detail-main-action { - display: flex; - align-items: center; - height: 52px; - width: auto; - // padding: 2px 6px; - box-sizing: border-box; - justify-content: center; - // gap: 12px; - flex: 1 0 0; - border-radius: 16px; - // border: 1px solid rgba(0, 0, 0, 0.06); - background: #fff; - overflow: hidden; - - &.disabled { - background-color: #b4b4b4; - color: rgba(60, 60, 67, 0.6); - pointer-events: none; - } - - .sticky-bottom-bar-join-game { - margin-left: auto; - // width: 151px; - display: flex; - align-items: center; - justify-content: center; - flex: 1; - - &-price { - font-family: "PoetsenOne"; - font-size: 28px; - font-weight: 400; - line-height: 24px; /* 114.286% */ - letter-spacing: -0.56px; - color: #000; - } - } - - .game_manage { - width: 100px; - margin-left: auto; - height: 100%; - display: flex; - align-items: center; - justify-content: center; - background: #000; - color: #fff; - pointer-events: all; - } - } - } - } -} diff --git a/src/game_pages/detail/index.tsx b/src/game_pages/detail/index.tsx index 93b5930..2e06275 100644 --- a/src/game_pages/detail/index.tsx +++ b/src/game_pages/detail/index.tsx @@ -1,1409 +1,26 @@ -import React, { - useState, - useEffect, - useRef, - useImperativeHandle, - forwardRef, -} from "react"; -import { View, Text, Image, Map, ScrollView, Button } from "@tarojs/components"; -import Taro, { - useRouter, - useShareAppMessage, - useShareTimeline, - useDidShow, - useLoad, -} from "@tarojs/taro"; -import classnames from "classnames"; -import dayjs from "dayjs"; -import "dayjs/locale/zh-cn"; +import { useState, useEffect, useRef } from "react"; +import { View, Text, Image } from "@tarojs/components"; +import Taro, { useRouter, useDidShow } from "@tarojs/taro"; // 导入API服务 -import { - CommonPopup, - withAuth, - NTRPEvaluatePopup, - GameManagePopup, - Comments, -} from "@/components"; -import { - generateShareImage, - generatePosterImage, - base64ToTempFilePath, - delay, -} from "@/utils"; -import DetailService, { - MATCH_STATUS, - IsSubstituteSupported, -} from "@/services/detailService"; +import { withAuth, Comments } from "@/components"; +import DetailService from "@/services/detailService"; import * as LoginService from "@/services/loginService"; -import OrderService from "@/services/orderService"; -import { getCurrentLocation, calculateDistance } from "@/utils/locationUtils"; +import { getCurrentLocation } from "@/utils/locationUtils"; import { useUserInfo, useUserActions } from "@/store/userStore"; -import { EvaluateCallback, EvaluateScene } from "@/store/evaluateStore"; -import img from "@/config/images"; -import DownloadIcon from "@/static/detail/download_icon.svg"; -import WechatLogo from "@/static/detail/wechat_icon.svg"; -// import WechatTimeline from "@/static/detail/wechat_timeline.svg"; -import LinkIcon from "@/static/detail/link.svg"; -import CrossIcon from "@/static/detail/cross.svg"; -import { DayOfWeekMap } from "./config"; -import styles from "./style.module.scss"; -import "./index.scss"; - -dayjs.locale("zh-cn"); - -const useSceneRedirect = (defaultPage: string) => { - useLoad((options) => { - if (options.scene) { - try { - const decoded = decodeURIComponent(options.scene || ""); - const params: Record = {}; - decoded.split("&").forEach((pair) => { - const [key, value] = pair.split("="); - if (key) params[key] = value ? decodeURIComponent(value) : ""; - }); - - // 拼接成 URL query - const query = Object.entries(params) - .map(([k, v]) => `${k}=${encodeURIComponent(v)}`) - .join("&"); - - Taro.redirectTo({ - url: query ? `/${defaultPage}?${query}` : `/${defaultPage}`, - }); - } catch (err) { - console.error("scene 解析失败:", err); - } - } - }); -}; - -// 将·作为连接符插入到标签文本之间 -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 ( - - - {/* 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(); - 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]); - - function previewImage(current_url) { - Taro.previewImage({ - current: current_url, - urls: list?.length > 0 ? list.map((c) => c.url) : [], - }); - } - - return ( - - - {list.map((item, index) => { - return ( - previewImage(item.url)} - > - - - ); - })} - - - ); -} - -// const PosterPopup = forwardRef((props, ref) => { -// const [visible, setVisible] = useState(false); -// const [posterData, setPosterData] = useState(); -// const posterRef = useRef(); -// useImperativeHandle(ref, () => ({ -// show: (detail, user) => { -// setVisible(true); -// const { -// play_type, -// skill_level_max, -// skill_level_min, -// image_list, -// title, -// start_time, -// end_time, -// location_name, -// } = detail; -// const { avatar_url, nickname } = user; -// const startTime = dayjs(start_time); -// const endTime = dayjs(end_time); -// const dayofWeek = DayOfWeekMap.get(startTime.day()); -// const gameLength = `${endTime.diff(startTime, "hour")}小时`; -// setPosterData({ -// playType: play_type, -// ntrp: `NTRP ${genNTRPRequirementText( -// skill_level_min, -// skill_level_max -// )}`, -// mainCoursal: -// image_list[0] || -// "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/0621b8cf-f7d6-43ad-b852-7dc39f29a782.png", -// nickname, -// avatarUrl: avatar_url, -// title, -// locationName: location_name, -// date: `${startTime.format("M月D日")} (${dayofWeek})`, -// time: `${startTime.format("ah")}点 ${gameLength}`, -// }); -// }, -// })); - -// useShareAppMessage(async () => { -// const tempFilePath = await posterRef.current.generateImage(); -// return { -// // title: detail.title, -// imageUrl: tempFilePath, -// path: `/game_pages/detail/index?id=${props.id}&from=share`, -// }; -// }); - -// useShareTimeline(async () => { -// const tempFilePath = await posterRef.current.generateImage(); -// return { -// title: "分享", -// imageUrl: tempFilePath, -// path: `/game_pages/detail/index?id=${props.id}&from=share`, -// }; -// }); - -// function onClose() { -// setVisible(false); -// setPosterData(undefined); -// Taro.updateShareMenu({ -// isUpdatableMessage: true, // 是否是动态消息(需要服务端配置过模版) -// }); -// } - -// async function handleShare() { -// const tempFilePath = await posterRef.current.generateImage(); -// Taro.showShareImageMenu({ -// path: tempFilePath, -// }); -// } -// return ( -// visible && ( -// -// -// -// {posterData && } -// -// -// -// -// -// -// -// -// ) -// ); -// }); - -// 分享弹窗 -const SharePopup = forwardRef(({ id, from, detail, userInfo }, ref) => { - const [visible, setVisible] = useState(false); - const [publishFlag, setPublishFlag] = useState(false); - // const posterRef = useRef(); - const { max_participants, participant_count } = detail || {}; - - useEffect(() => { - if (id) { - changeMessageType(); - } - }, [id]); - - async function changeMessageType() { - try { - const res = await DetailService.getActivityId({ - business_id: id, - business_type: "game", - is_private: false, - }); - if (res.code === 0) { - Taro.updateShareMenu({ - withShareTicket: false, // 是否需要返回 shareTicket - isUpdatableMessage: true, // 是否是动态消息(需要服务端配置过模版) - activityId: res.data.activity_id, // 动态消息的活动 id - }); - } - } catch (e) { - Taro.showToast({ title: e.message, icon: "none" }); - } - } - - useImperativeHandle(ref, () => ({ - show: (publish_flag = false) => { - setPublishFlag(publish_flag); - setVisible(true); - }, - })); - - useShareAppMessage(async (res) => { - const { - play_type, - skill_level_max, - skill_level_min, - start_time, - end_time, - location_name, - venue_image_list, - } = detail || {}; - const startTime = dayjs(start_time); - const endTime = dayjs(end_time); - const dayofWeek = DayOfWeekMap.get(startTime.day()); - const gameLength = `${endTime.diff(startTime, "hour")}小时`; - const url = await generateShareImage({ - userAvatar: userInfo.avatar_url, - userNickname: userInfo.nickname, - gameType: play_type, - skillLevel: `NTRP ${genNTRPRequirementText( - skill_level_min, - skill_level_max - )}`, - gameDate: `${startTime.format("M月D日")} (${dayofWeek})`, - gameTime: `${startTime.format("ah")}点 ${gameLength}`, - venueName: location_name, - venueImages: venue_image_list ? venue_image_list.map((c) => c.url) : [], - }); - // console.log(res, "res"); - return { - title: detail.title, - imageUrl: url || "https://img.yzcdn.cn/vant/cat.jpeg", - path: `/game_pages/detail/index?id=${id}&from=share`, - }; - }); - - async function handlePost() { - const { - id, - play_type, - skill_level_max, - skill_level_min, - start_time, - end_time, - location_name, - image_list, - title, - } = detail || {}; - const { avatar_url, nickname } = userInfo; - const startTime = dayjs(start_time); - const endTime = dayjs(end_time); - const dayofWeek = DayOfWeekMap.get(startTime.day()); - const gameLength = `${endTime.diff(startTime, "hour")}小时`; - Taro.showLoading({ title: "生成中..." }); - const qrCodeUrlRes = await DetailService.getQrCodeUrl({ - page: "game_pages/detail/index", - scene: `id=${id}`, - }); - const qrCodeUrl = await base64ToTempFilePath( - qrCodeUrlRes.data.qr_code_base64 - ); - console.log(qrCodeUrl, "qrCodeUrl"); - await delay(100); - const url = await generatePosterImage({ - playType: play_type, - ntrp: `NTRP ${genNTRPRequirementText(skill_level_min, skill_level_max)}`, - mainCoursal: - image_list[0] || - "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/0621b8cf-f7d6-43ad-b852-7dc39f29a782.png", - nickname, - avatarUrl: avatar_url, - title, - locationName: location_name, - date: `${startTime.format("M月D日")} (${dayofWeek})`, - time: `${startTime.format("ah")}点 ${gameLength}`, - qrCodeUrl, - }); - Taro.hideLoading(); - setVisible(false); - Taro.showShareImageMenu({ - path: url, - }); - } - - function onClose() { - setVisible(false); - setPublishFlag(false); - } - - return ( - <> - - - - {publishFlag ? ( - 球局发布成功 🎉 - ) : ( - 分享至 - )} - - - - - {publishFlag && ( - - - 还剩 - - {" "} - {max_participants - participant_count}{" "} - - 人加入完成组局, 去邀请好友加入吧~ - - - )} - - - - - - - - {/* */} - - ); -}); - -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<{ - show: (evaluateCallback: EvaluateCallback) => void; - }>({ show: () => {} }); - const { - id, - price, - user_action_status, - match_status, - start_time, - end_time, - is_organizer, - } = detail || {}; - - const gameManageRef = useRef(); - - function handleSelfEvaluate() { - ntrpRef?.current?.show({ - type: EvaluateScene.detail, - next: (flag) => { - if (flag) { - Taro.navigateTo({ - url: `/order_pages/orderDetail/index?gameId=${id}`, - }); - return; - } - Taro.redirectTo({ url: `/order_pages/orderDetail/index?gameId=${id}` }); - }, - onCancel: () => { - // Taro.redirectTo({ url: `/game_pages/detail/index?id=${id}` }); - Taro.navigateBack(); - }, - }); - } - - 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: () => ¥{displayPrice} 已加入, - action: () => toast("已加入"), - }; - } else if (is_substituting) { - return { - text: () => ¥{displayPrice} 已加入候补, - action: () => toast("已加入候补"), - }; - } else if (can_pay) { - return { - text: () => ¥{price} 继续支付, - 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: () => ¥{displayPrice} 我要候补, - action: handleJoinGame, - }; - } else if (can_join) { - return { - text: () => { - return ¥{displayPrice} 立即加入; - }, - action: handleJoinGame, - }; - } else if (can_assess) { - return { - text: () => ¥{displayPrice} 立即加入, - 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 as string}; - }; - } - - return ( - <> - - - handleShare()} - > - - 分享 - - - { - // Taro.showToast({ title: "To be continued", icon: "none" }); - handleAddComment(); - }} - > - - - {commentCount > 0 ? commentCount : "评论"} - - - - - - - - {is_organizer && ( - { - gameManageRef.current.show(detail, onStatusChange); - }} - > - 管理 - - )} - - - - - - ); -} - -// 球局信息 -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?.length > 0 ? 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?.length > 0 && - 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, handleViewUserInfo } = props; - const participants = detail.participants || []; - const { - participant_count, - max_participants, - user_action_status = {}, - start_time, - } = 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 && - dayjs(start_time).isAfter(dayjs()); - const leftCount = max_participants - participant_count; - return ( - - - 参与者 - · - {leftCount > 0 ? `剩余空位 ${leftCount}` : "已满员"} - - {participant_count > 0 || showApplicationEntry ? ( - - {/* application */} - {showApplicationEntry && ( - { - handleJoinGame(); - }} - > - - - 申请加入 - - - )} - {/* participants list */} - - - {participants.map((participant) => { - const { - is_organizer, - user: { - avatar_url, - nickname, - level, - id: participant_user_id, - }, - } = participant; - const role = is_organizer ? "组织者" : "参与者"; - 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: - 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 ( - - {/* orgnizer title */} - - 组织者 - - {/* organizer avatar and name */} - - - - {nickname} - - 已组织 {hosted_games_count} 次 - - NTRP {ntrp_level || "初学者"} - - - - {my_id === id ? ( - "" - ) : ( - - {is_following ? ( - 取消关注 - ) : ( - <> - - 关注 - - )} - - )} - handleAddComment()} - > - - - - - {/* recommend games by organizer */} - {recommendGames.length > 0 && ( - - - 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 */} - - { - e.stopPropagation(); - handleViewUserInfo(id); - }} - /> - - - - 报名人数 {game.checkedApplications}/ - {game.applications} - - - - {game.levelRequirements} - - - {game.playType} - - - - - ))} - - - - )} - - ); -} +import GameTags from "./components/GameTags"; +import Carousel from "./components/Carousel"; +import StickyBottom from "./components/StickyBottom"; +import GameInfo from "./components/GameInfo"; +import VenueInfo from "./components/VenueInfo"; +import GamePlayAndRequirement from "./components/GamePlayAndReq"; +import Participants from "./components/Participants"; +import SupplementalNotes from "./components/SupplementalNotes"; +import OrganizerInfo from "./components/OrganizerInfo"; +import SharePopup from "./components/SharePopup"; +import { navto, toast, useSceneRedirect } from "./utils/helper"; +import ArrowLeft from "@/static/detail/icon-arrow-left.svg"; +import Logo from "@/static/detail/icon-logo-go.svg"; +import styles from "./index.module.scss"; function Index() { const [detail, setDetail] = useState({}); @@ -1526,29 +143,32 @@ function Index() { : {}; return ( - + {/* custom navbar */} - - - + + + - + - + {/* swiper */} - + {/* content */} - + {/* avatar and tags */} {/* title */} - - {detail.title} + + + {detail.title} + {/* Date and Place and weather */} @@ -1591,7 +213,7 @@ function Index() { publisher_id={Number(detail.publisher_id)} /> {/* sticky bottom action bar */} - { + useLoad((options) => { + if (options.scene) { + try { + const decoded = decodeURIComponent(options.scene || ""); + const params: Record = {}; + decoded.split("&").forEach((pair) => { + const [key, value] = pair.split("="); + if (key) params[key] = value ? decodeURIComponent(value) : ""; + }); + + // 拼接成 URL query + const query = Object.entries(params) + .map(([k, v]) => `${k}=${encodeURIComponent(v)}`) + .join("&"); + + Taro.redirectTo({ + url: query ? `/${defaultPage}?${query}` : `/${defaultPage}`, + }); + } catch (err) { + console.error("scene 解析失败:", err); + } + } + }); +}; + +export function genNTRPRequirementText(min, max) { + if (min && max && min !== max) { + return `${min} - ${max} 之间`; + } else if (max === "1") { + return "无要求"; + } else if (max) { + return `${max} 以上`; + } + return "-"; +} \ No newline at end of file diff --git a/src/game_pages/sharePoster/index.config.ts b/src/game_pages/sharePoster/index.config.ts new file mode 100644 index 0000000..d57f38c --- /dev/null +++ b/src/game_pages/sharePoster/index.config.ts @@ -0,0 +1,6 @@ +export default definePageConfig({ + navigationBarTitleText: '生成分享图', + navigationStyle: 'custom', + enableShareAppMessage: true, + enableShareTimeline: true, +}) diff --git a/src/game_pages/sharePoster/index.module.scss b/src/game_pages/sharePoster/index.module.scss new file mode 100644 index 0000000..c334321 --- /dev/null +++ b/src/game_pages/sharePoster/index.module.scss @@ -0,0 +1,101 @@ +.navbar { + box-shadow: none; +} + +.posterContainer { + width: 100vw; + height: 100vh; + background: linear-gradient(180deg, #fff 0%, #fafafa 100%), #fff; + padding: 100px 20px 40px; + box-sizing: border-box; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + // gap: 20px; +} + +.posterWrap { + width: 100%; + border-radius: 19.067px; + // border: 1px solid rgba(0, 0, 0, 0.06); + // background: linear-gradient(180deg, #bfffef 0%, #f2fffc 100%), #fff; + // box-shadow: 0 6.933px 55.467px 0 rgba(0, 0, 0, 0.1); + // overflow: hidden; + box-sizing: border-box; + .imageContainer { + width: 100%; + padding: 0 20px; + box-sizing: border-box; + + .poster { + border-radius: 12px; + box-shadow: 0 6.933px 55.467px 0 rgba(0, 0, 0, 0.1); + width: 100%; + } + } +} + +.sharePoster { + width: 100%; + margin-top: 40px; + display: flex; + align-items: center; + justify-content: space-around; + + .shareItem { + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + gap: 12px; + color: rgba(0, 0, 0, 0.85); + font-feature-settings: "liga" off, "clig" off; + font-family: "PingFang SC"; + font-size: 14px; + font-style: normal; + font-weight: 600; + line-height: normal; + + background-color: #fff; + border: none; + padding: 0; + margin: 0; + line-height: normal; + font-size: inherit; + color: inherit; + + &:after { + border: none; + background: transparent; + } + + .icon { + width: 64px; + height: 64px; + border-radius: 50%; + background-color: #fff; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid rgba(0, 0, 0, 0.06); + box-shadow: 0 8px 64px 0 rgba(0, 0, 0, 0.1); + + &.wechatIcon { + background-color: #07c160; + } + .download { + width: 28px; + height: 28px; + } + .wechat { + width: 36px; + height: 30px; + } + .timeline { + width: 32px; + height: 32px; + } + } + } +} diff --git a/src/game_pages/sharePoster/index.tsx b/src/game_pages/sharePoster/index.tsx new file mode 100644 index 0000000..579a039 --- /dev/null +++ b/src/game_pages/sharePoster/index.tsx @@ -0,0 +1,134 @@ +// import React from "react"; +import { useState, useEffect } from "react"; +import { View, Image, Text, Button } from "@tarojs/components"; +import Taro, { useRouter } from "@tarojs/taro"; +import classnames from "classnames"; +import dayjs from "dayjs"; +import { generatePosterImage, base64ToTempFilePath, delay } from "@/utils"; +import { withAuth } from "@/components"; +import GeneralNavbar from "@/components/GeneralNavbar"; +import DetailService from "@/services/detailService"; +import DownloadIcon from "@/static/detail/download_icon.svg"; +import WechatLogo from "@/static/detail/wechat_icon.svg"; +import WechatTimeline from "@/static/detail/wechat_timeline.svg"; +import { useUserActions } from "@/store/userStore"; +import { DayOfWeekMap } from "../detail/config"; +import { genNTRPRequirementText } from "@/game_pages/detail/utils/helper"; +import styles from "./index.module.scss"; + +function SharePoster(props) { + const [url, setUrl] = useState(""); + const { fetchUserInfo } = useUserActions(); + const { id } = useRouter().params; + + useEffect(() => { + fetchDetail(); + }, []); + + async function fetchDetail() { + const res = await DetailService.getDetail(Number(id)); + handleGenPoster(res.data); + } + async function handleGenPoster(detail) { + const { + id, + play_type, + skill_level_max, + skill_level_min, + start_time, + end_time, + location_name, + image_list, + title, + } = detail || {}; + const userInfo = await fetchUserInfo(); + const { avatar_url, nickname } = userInfo; + const startTime = dayjs(start_time); + const endTime = dayjs(end_time); + const dayofWeek = DayOfWeekMap.get(startTime.day()); + const gameLength = `${endTime.diff(startTime, "hour")}小时`; + Taro.showLoading({ title: "生成中..." }); + const qrCodeUrlRes = await DetailService.getQrCodeUrl({ + page: "game_pages/detail/index", + scene: `id=${id}`, + }); + const qrCodeUrl = await base64ToTempFilePath( + qrCodeUrlRes.data.qr_code_base64 + ); + await delay(100); + const url = await generatePosterImage({ + playType: play_type, + ntrp: `NTRP ${genNTRPRequirementText(skill_level_min, skill_level_max)}`, + mainCoursal: + image_list[0] || + "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/0621b8cf-f7d6-43ad-b852-7dc39f29a782.png", + nickname, + avatarUrl: avatar_url, + title, + locationName: location_name, + date: `${startTime.format("M月D日")} (${dayofWeek})`, + time: `${startTime.format("ah")}点 ${gameLength}`, + qrCodeUrl, + }); + Taro.hideLoading(); + setUrl(url); + } + function handleShare() { + Taro.showShareImageMenu({ + path: url, + }); + } + + return ( + <> + + {url && ( + + + + + + + + + + + + + )} + + ); +} + +export default withAuth(SharePoster); diff --git a/src/order_pages/orderList/index.tsx b/src/order_pages/orderList/index.tsx index 1f6017e..3a6a303 100644 --- a/src/order_pages/orderList/index.tsx +++ b/src/order_pages/orderList/index.tsx @@ -16,17 +16,13 @@ import { withAuth, RefundPopup } from "@/components"; import { payOrder, generateOrderActions } from "@/utils"; import emptyContent from "@/static/emptyStatus/publish-empty.png"; import CustomerIcon from "@/static/order/customer.svg"; +import { insertDotInTags } from "@/game_pages/detail/utils/helper"; import styles from "./index.module.scss"; dayjs.locale("zh-cn"); const PAGESIZE = 100; -// 将·作为连接符插入到标签文本之间 -function insertDotInTags(tags: string[]) { - return tags.join("-·-").split("-"); -} - const diffDayMap = new Map([ [0, "今天"], [1, "明天"], diff --git a/src/store/userStore.ts b/src/store/userStore.ts index 53a04e2..70c4ca6 100644 --- a/src/store/userStore.ts +++ b/src/store/userStore.ts @@ -7,7 +7,7 @@ import { export interface UserState { user: UserInfoType | {}; - fetchUserInfo: () => Promise; + fetchUserInfo: () => Promise; updateUserInfo: (userInfo: Partial) => void; } @@ -16,9 +16,9 @@ export const useUser = create()((set) => ({ fetchUserInfo: async () => { try { const res = await fetchUserProfile(); - console.log(res); set({ user: res.data }); - } catch {} + return res.data + } catch { } }, updateUserInfo: async (userInfo: Partial) => { const res = await updateUserProfile(userInfo);