Merge branch 'feat/liujie'

This commit is contained in:
2025-09-11 19:23:30 +08:00
12 changed files with 856 additions and 423 deletions

View File

@@ -54,6 +54,7 @@
"@tarojs/shared": "4.1.5", "@tarojs/shared": "4.1.5",
"@tarojs/taro": "4.1.5", "@tarojs/taro": "4.1.5",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"qweather-icons": "^1.8.0",
"react": "^18.0.0", "react": "^18.0.0",
"react-dom": "^18.0.0", "react-dom": "^18.0.0",
"zustand": "^4.4.7" "zustand": "^4.4.7"

View File

@@ -1,50 +1,49 @@
import { Component, ReactNode } from 'react' import { Component, ReactNode } from "react";
import './nutui-theme.scss' import "./nutui-theme.scss";
import './app.scss' import "./app.scss";
import { useDictionaryStore } from './store/dictionaryStore' import "qweather-icons/font/qweather-icons.css";
import { useGlobalStore } from './store/global' import { useDictionaryStore } from "./store/dictionaryStore";
import { useGlobalStore } from "./store/global";
// import { getNavbarHeight } from "@/utils/getNavbarHeight"; // import { getNavbarHeight } from "@/utils/getNavbarHeight";
interface AppProps { interface AppProps {
children: ReactNode children: ReactNode;
} }
class App extends Component<AppProps> { class App extends Component<AppProps> {
onLaunch() { onLaunch() {
console.log('小程序启动,初始化逻辑写这里') console.log("小程序启动,初始化逻辑写这里");
} }
componentDidMount() { componentDidMount() {
// 初始化字典数据 // 初始化字典数据
this.initDictionaryData() this.initDictionaryData();
this.getNavBarHeight() this.getNavBarHeight();
// this.getLocation() // this.getLocation()
} }
componentDidShow() { } componentDidShow() {}
componentDidHide() { } componentDidHide() {}
// 初始化字典数据 // 初始化字典数据
private async initDictionaryData() { private async initDictionaryData() {
try { try {
const { fetchDictionary } = useDictionaryStore.getState() const { fetchDictionary } = useDictionaryStore.getState();
await fetchDictionary() await fetchDictionary();
} catch (error) { } catch (error) {
console.error('初始化字典数据失败:', error) console.error("初始化字典数据失败:", error);
} }
} }
// 获取导航高度 // 获取导航高度
getNavBarHeight = () => { getNavBarHeight = () => {
const { getNavbarHeightInfo } = useGlobalStore.getState() const { getNavbarHeightInfo } = useGlobalStore.getState();
getNavbarHeightInfo() getNavbarHeightInfo();
} };
// 获取位置信息 // 获取位置信息
// getLocation = () => { // getLocation = () => {
// const { getCurrentLocationInfo } = useGlobalStore.getState() // const { getCurrentLocationInfo } = useGlobalStore.getState()
// getCurrentLocationInfo() // getCurrentLocationInfo()
@@ -52,8 +51,8 @@ class App extends Component<AppProps> {
render() { render() {
// this.props.children 是将要会渲染的页面 // this.props.children 是将要会渲染的页面
return this.props.children return this.props.children;
} }
} }
export default App export default App;

View File

@@ -1,8 +1,7 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from "react";
import Taro from '@tarojs/taro' import { View } from "@tarojs/components";
import { check_login_status } from '@/services/loginService' import Taro from "@tarojs/taro";
import { check_login_status } from "@/services/loginService";
export function getCurrentFullPath(): string { export function getCurrentFullPath(): string {
const pages = Taro.getCurrentPages(); const pages = Taro.getCurrentPages();
@@ -31,18 +30,30 @@ export default function withAuth<P extends object>(
const is_login = check_login_status(); const is_login = check_login_status();
setAuthed(is_login); setAuthed(is_login);
if (!is_login) { // if (!is_login) {
const currentPage = getCurrentFullPath(); // const currentPage = getCurrentFullPath();
// Taro.redirectTo({ // Taro.redirectTo({
// url: `/pages/login/index/index${ // url: `/pages/login/index/index${
// currentPage ? `?redirect=${encodeURIComponent(currentPage)}` : '' // currentPage ? `?redirect=${encodeURIComponent(currentPage)}` : ""
// }`, // }`,
// }) // });
} // }
}, []); }, []);
// if (!authed) { // if (!authed) {
// return <View style={{ width: '100vh', height: '100vw', backgroundColor: 'white', position: 'fixed', top: 0, left: 0, zIndex: 999 }} /> // 空壳,避免 children 渲染出错 // return (
// <View
// style={{
// width: "100vh",
// height: "100vw",
// backgroundColor: "white",
// position: "fixed",
// top: 0,
// left: 0,
// zIndex: 999,
// }}
// />
// ); // 空壳,避免 children 渲染出错
// } // }
return <WrappedComponent {...props} />; return <WrappedComponent {...props} />;

View File

@@ -17,8 +17,8 @@ const envConfigs: Record<EnvType, EnvConfig> = {
// 开发环境 // 开发环境
development: { development: {
name: '开发环境', name: '开发环境',
// apiBaseURL: 'https://sit.light120.com', apiBaseURL: 'https://sit.light120.com',
apiBaseURL: 'http://localhost:9098', // apiBaseURL: 'http://localhost:9098',
timeout: 15000, timeout: 15000,
enableLog: true, enableLog: true,
enableMock: true enableMock: true

View File

@@ -1,5 +1,16 @@
@use "~@/scss/images.scss" as img; @use "~@/scss/images.scss" as img;
.errorTip {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: space-around;
flex-direction: column;
font-weight: 600;
font-size: 16px;
}
.container { .container {
padding: 20px; padding: 20px;
box-sizing: border-box; box-sizing: border-box;
@@ -193,7 +204,9 @@
align-items: center; align-items: center;
align-self: stretch; align-self: stretch;
color: #000; color: #000;
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 14px; font-size: 14px;
font-style: normal; font-style: normal;
@@ -205,7 +218,7 @@
.summaryList { .summaryList {
border-radius: 12px; border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.06); border: 1px solid rgba(0, 0, 0, 0.06);
background: #FFF; background: #fff;
box-shadow: 0 4px 36px 0 rgba(0, 0, 0, 0.06); box-shadow: 0 4px 36px 0 rgba(0, 0, 0, 0.06);
.summaryItem { .summaryItem {
@@ -218,8 +231,10 @@
.title { .title {
width: 120px; width: 120px;
display: inline-block; display: inline-block;
color: rgba(60, 60, 67, 0.60); color: rgba(60, 60, 67, 0.6);
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 14px; font-size: 14px;
font-style: normal; font-style: normal;
@@ -229,7 +244,9 @@
.content { .content {
color: #000; color: #000;
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 14px; font-size: 14px;
font-style: normal; font-style: normal;
@@ -248,7 +265,9 @@
align-items: center; align-items: center;
align-self: stretch; align-self: stretch;
color: #000; color: #000;
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 14px; font-size: 14px;
font-style: normal; font-style: normal;
@@ -260,7 +279,7 @@
.policyList { .policyList {
border-radius: 12px; border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.06); border: 1px solid rgba(0, 0, 0, 0.06);
background: #FFF; background: #fff;
box-shadow: 0 4px 36px 0 rgba(0, 0, 0, 0.06); box-shadow: 0 4px 36px 0 rgba(0, 0, 0, 0.06);
.policyItem { .policyItem {
@@ -269,7 +288,9 @@
align-items: center; align-items: center;
color: #000; color: #000;
text-align: center; text-align: center;
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 12px; font-size: 12px;
font-style: normal; font-style: normal;
@@ -280,7 +301,9 @@
&:nth-child(1) { &:nth-child(1) {
color: #000; color: #000;
text-align: center; text-align: center;
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 14px; font-size: 14px;
font-style: normal; font-style: normal;
@@ -289,7 +312,8 @@
border: none; border: none;
} }
.time, .rule { .time,
.rule {
width: 50%; width: 50%;
padding: 10px 12px; padding: 10px 12px;
} }
@@ -314,7 +338,9 @@
align-items: center; align-items: center;
align-self: stretch; align-self: stretch;
color: #000; color: #000;
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 14px; font-size: 14px;
font-style: normal; font-style: normal;
@@ -324,7 +350,7 @@
} }
.content { .content {
color: rgba(22, 24, 35, 0.60); color: rgba(22, 24, 35, 0.6);
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 14px; font-size: 14px;
font-style: normal; font-style: normal;

View File

@@ -3,8 +3,11 @@ import { View, Text, Button, Image } from "@tarojs/components";
import Taro, { useDidShow, useRouter } from "@tarojs/taro"; import Taro, { useDidShow, useRouter } from "@tarojs/taro";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { delay } from "@/utils"; import { delay } from "@/utils";
import orderService, { GameOrderRes } from "@/services/orderService"; import orderService, {
import detailService, { GameDetail } from "@/services/detailService"; GameOrderRes,
OrderStatus,
} from "@/services/orderService";
import detailService, { GameData } from "@/services/detailService";
import { withAuth } from "@/components"; import { withAuth } from "@/components";
import { calculateDistance, getCurrentLocation } from "@/utils"; import { calculateDistance, getCurrentLocation } from "@/utils";
import img from "@/config/images"; import img from "@/config/images";
@@ -111,7 +114,7 @@ function GameInfo(props) {
} }
function OrderMsg(props) { function OrderMsg(props) {
const { detail, orderInfo } = props; const { detail, checkOrderInfo } = props;
const { const {
start_time, start_time,
end_time, end_time,
@@ -120,10 +123,10 @@ function OrderMsg(props) {
wechat_contact, wechat_contact,
price, price,
} = detail; } = detail;
const { order_info: { registrant_nickname } = {} } = orderInfo const { order_info: { registrant_nickname } = {} } = checkOrderInfo;
const startTime = dayjs(start_time); const startTime = dayjs(start_time);
const endTime = dayjs(end_time); const endTime = dayjs(end_time);
const startYear = startTime.format('YYYY') const startYear = startTime.format("YYYY");
const startMonth = startTime.format("M"); const startMonth = startTime.format("M");
const startDay = startTime.format("D"); const startDay = startTime.format("D");
const startDate = `${startYear}${startMonth}${startDay}`; const startDate = `${startYear}${startMonth}${startDay}`;
@@ -157,32 +160,38 @@ function OrderMsg(props) {
</View> </View>
{/* 订单信息摘要 */} {/* 订单信息摘要 */}
<View className={styles.summaryList}> <View className={styles.summaryList}>
{ {summary.map((item, index) => (
summary.map((item, index) => (<View key={index} className={styles.summaryItem}> <View key={index} className={styles.summaryItem}>
<Text className={styles.title}>{item.title}</Text> <Text className={styles.title}>{item.title}</Text>
<Text className={styles.content}>{item.content}</Text> <Text className={styles.content}>{item.content}</Text>
</View>)) </View>
} ))}
</View> </View>
</View> </View>
); );
} }
function RefundPolicy(props) { function RefundPolicy(props) {
const { orderInfo } = props const { checkOrderInfo } = props;
const { refund_policy = [] } = orderInfo const { refund_policy = [] } = checkOrderInfo;
const policyList = [{ const policyList = [
time: '申请退款时间', {
rule: '退款规则', time: "申请退款时间",
}, ...refund_policy.map((item, index) => { rule: "退款规则",
const [, theTime] = item.application_time.split('undefined ') },
const theTimeObj = dayjs(theTime) ...refund_policy.map((item, index) => {
const year = theTimeObj.format('YYYY') const [, theTime] = item.application_time.split("undefined ");
const month = theTimeObj.format('M') const theTimeObj = dayjs(theTime);
const day = theTimeObj.format('D') const year = theTimeObj.format("YYYY");
const time = theTimeObj.format('HH:MM') const month = theTimeObj.format("M");
return { time: `${year}${month}${day}${time}${index === 0 ? '前' : '后'}`, rule: item.refund_rule } const day = theTimeObj.format("D");
})] const time = theTimeObj.format("HH:MM");
return {
time: `${year}${month}${day}${time}${index === 0 ? "前" : "后"}`,
rule: item.refund_rule,
};
}),
];
return ( return (
<View className={styles.refundPolicy}> <View className={styles.refundPolicy}>
<View className={styles.moduleTitle}> <View className={styles.moduleTitle}>
@@ -190,12 +199,12 @@ function RefundPolicy(props) {
</View> </View>
{/* 订单信息摘要 */} {/* 订单信息摘要 */}
<View className={styles.policyList}> <View className={styles.policyList}>
{ {policyList.map((item, index) => (
policyList.map((item, index) => (<View key={index} className={styles.policyItem}> <View key={index} className={styles.policyItem}>
<View className={styles.time}>{item.time}</View> <View className={styles.time}>{item.time}</View>
<View className={styles.rule}>{item.rule}</View> <View className={styles.rule}>{item.rule}</View>
</View>)) </View>
} ))}
</View> </View>
</View> </View>
); );
@@ -212,22 +221,43 @@ function Disclaimer() {
const OrderCheck = () => { const OrderCheck = () => {
const { params } = useRouter(); const { params } = useRouter();
const { id, gameId } = params; const { id: stringId, gameId: stringGameId } = params;
const [detail, setDetail] = useState<GameDetail | {}>({}); const [id, gameId] = [Number(stringId), Number(stringGameId)];
const [detail, setDetail] = useState<GameData | {}>({});
const [location, setLocation] = useState<number[]>([0, 0]); const [location, setLocation] = useState<number[]>([0, 0]);
const [orderInfo, setOrderInfo] = useState<GameOrderRes | {}>({}) const [checkOrderInfo, setCheckOrderInfo] = useState<GameOrderRes | {}>({});
const [orderDetail, setOrderDetail] = useState({});
useDidShow(async () => { useDidShow(async () => {
const res = await detailService.getDetail(Number(gameId)); let gameDetail = {};
const orderRes = await orderService.getOrderInfo(Number(gameId)) if (id) {
setOrderInfo(orderRes.data) const res = await orderService.getOrderDetail(id);
console.log(res); if (res.code === 0) {
if (res.code === 0) { setOrderDetail(res.data);
setDetail(res.data); gameDetail = res.data.game_info;
}
} else if (gameId) {
const res = await detailService.getDetail(gameId);
if (res.code === 0) {
gameDetail = res.data;
}
} }
if (gameDetail.id) {
setDetail(gameDetail);
onInit(gameDetail.id);
}
});
async function checkOrder(gid) {
const orderRes = await orderService.getCheckOrderInfo(gid);
setCheckOrderInfo(orderRes.data);
}
async function onInit(gid) {
checkOrder(gid);
const location = await getCurrentLocation(); const location = await getCurrentLocation();
setLocation([location.latitude, location.longitude]); setLocation([location.latitude, location.longitude]);
}); }
//TODO: get order msg from id //TODO: get order msg from id
const handlePay = async () => { const handlePay = async () => {
@@ -235,56 +265,102 @@ const OrderCheck = () => {
title: "支付中...", title: "支付中...",
mask: true, mask: true,
}); });
const res = await orderService.createOrder(Number(gameId)); let wxPayRes: any = {};
if (res.code === 0) { try {
const { payment_required, payment_params } = res.data; if (orderDetail.game_info?.id) {
if (payment_required) { const res = await orderService.getUnpaidOrder(orderDetail.game_info.id);
const { if (res.code === 0) {
timeStamp, wxPayRes = {
nonceStr, ...res.data,
package: package_, payment_required: res.data.has_unpaid_order,
signType, };
paySign, }
} = payment_params; } else {
await Taro.requestPayment({ const res = await orderService.createOrder(detail.id);
timeStamp, if (res.code === 0) {
nonceStr, wxPayRes = res.data;
package: package_, }
signType,
paySign,
success: async () => {
Taro.hideLoading();
Taro.showToast({
title: "支付成功",
icon: "success",
});
await delay(1000);
Taro.navigateBack({
delta: 1,
});
},
fail: () => {
Taro.hideLoading();
Taro.showToast({
title: "支付失败",
icon: "none",
});
},
});
} }
} catch (error) {
Taro.hideLoading();
Taro.showToast({
title: "支付调用失败",
icon: "none",
});
return;
}
const { payment_required, payment_params } = wxPayRes;
if (payment_required) {
const {
timeStamp,
nonceStr,
package: package_,
signType,
paySign,
} = payment_params;
await Taro.requestPayment({
timeStamp,
nonceStr,
package: package_,
signType,
paySign,
success: async () => {
Taro.hideLoading();
Taro.showToast({
title: "支付成功",
icon: "success",
});
await delay(1000);
Taro.navigateBack({
delta: 1,
});
},
fail: () => {
Taro.hideLoading();
Taro.showToast({
title: "支付失败",
icon: "none",
});
},
});
} }
}; };
if (!id && !gameId) {
return (
<View className={styles.errorTip}>
<Text></Text>
<Button
type="warn"
onClick={() => {
Taro.redirectTo({ url: "/pages/list/index" });
}}
>
</Button>
</View>
);
}
return ( return (
<View className={styles.container}> <View className={styles.container}>
{/* Game Date and Address */} {/* Game Date and Address */}
<GameInfo detail={detail} currentLocation={location} /> <GameInfo detail={detail} currentLocation={location} />
{/* Order message */} {/* Order message */}
<OrderMsg detail={detail} orderInfo={orderInfo} /> <OrderMsg detail={detail} checkOrderInfo={checkOrderInfo} />
{/* Refund policy */} {/* Refund policy */}
<RefundPolicy orderInfo={orderInfo} /> <RefundPolicy checkOrderInfo={checkOrderInfo} />
{/* Disclaimer */} {/* Disclaimer */}
<Disclaimer /> <Disclaimer />
<Button className={styles.payButton} type="primary" onClick={handlePay}></Button> {!id ||
(orderDetail.order_status === OrderStatus.PENDING && (
<Button
className={styles.payButton}
type="primary"
onClick={handlePay}
>
{orderDetail.order_status === OrderStatus.PENDING ? "继续" : ""}
</Button>
))}
</View> </View>
); );
}; };

View File

@@ -1,4 +1,4 @@
@use '~@/scss/images.scss' as img; @use "~@/scss/images.scss" as img;
.detail-page { .detail-page {
width: 100%; width: 100%;
@@ -31,13 +31,14 @@
color: #fff; color: #fff;
display: flex; display: flex;
align-items: center; align-items: center;
background: rgba(0, 0, 0, 0.10); background: rgba(0, 0, 0, 0.1);
.detail-navigator-back { .detail-navigator-back {
border-right: 1px solid #444; border-right: 1px solid #444;
} }
.detail-navigator-back, .detail-navigator-icon { .detail-navigator-back,
.detail-navigator-icon {
height: 20px; height: 20px;
width: 50%; width: 50%;
@@ -75,14 +76,18 @@
height: 100vh; height: 100vh;
&::after { &::after {
content: ''; content: "";
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: -1; z-index: -1;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.80) 0%, rgba(0, 0, 0, 0.40) 100%); background: linear-gradient(
180deg,
rgba(0, 0, 0, 0.8) 0%,
rgba(0, 0, 0, 0.4) 100%
);
backdrop-filter: blur(100px); backdrop-filter: blur(100px);
} }
} }
@@ -141,7 +146,6 @@
} }
.detail-page-content { .detail-page-content {
&-avatar-tags { &-avatar-tags {
padding: 20px 20px 0; padding: 20px 20px 0;
box-sizing: border-box; box-sizing: border-box;
@@ -194,8 +198,10 @@
&-text { &-text {
overflow: hidden; overflow: hidden;
color: #FFF; color: #fff;
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
text-overflow: ellipsis; text-overflow: ellipsis;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 22px; font-size: 22px;
@@ -232,7 +238,7 @@
// border: 0.5px solid rgba(255, 255, 255, 0.08); // border: 0.5px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.25); background: rgba(255, 255, 255, 0.25);
overflow: hidden; overflow: hidden;
color: #FFF; color: #fff;
background: #536272; background: #536272;
.month { .month {
@@ -245,7 +251,7 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
// border-bottom: 1px solid rgba(255, 255, 255, 0.08); // border-bottom: 1px solid rgba(255, 255, 255, 0.08);
background: #7B828B; background: #7b828b;
} }
.day { .day {
@@ -269,11 +275,13 @@
justify-content: space-evenly; justify-content: space-evenly;
gap: 4px; gap: 4px;
align-self: stretch; align-self: stretch;
color: #FFF; color: #fff;
.date { .date {
color: #FFF; color: #fff;
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 16px; font-size: 16px;
font-style: normal; font-style: normal;
@@ -282,8 +290,10 @@
} }
.venue-time { .venue-time {
color: rgba(255, 255, 255, 0.80); color: rgba(255, 255, 255, 0.8);
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 12px; font-size: 12px;
font-style: normal; font-style: normal;
@@ -302,18 +312,16 @@
&-icon { &-icon {
width: 20px; width: 20px;
height: 20px; height: 20px;
color: rgba(255, 255, 255, 0.8);
.weather-icon {
width: 20px;
height: 20px;
}
} }
&-text-temperature { &-text-temperature {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
color: rgba(255, 255, 255, 0.80); color: rgba(255, 255, 255, 0.8);
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 12px; font-size: 12px;
font-style: normal; font-style: normal;
@@ -338,7 +346,7 @@
border-radius: 12px; border-radius: 12px;
padding: 14px; padding: 14px;
box-sizing: border-box; box-sizing: border-box;
background: #4D5865; background: #4d5865;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@@ -364,9 +372,11 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
color: #FFF; color: #fff;
text-align: center; text-align: center;
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 16px; font-size: 16px;
font-style: normal; font-style: normal;
@@ -380,9 +390,11 @@
} }
&-address { &-address {
color: rgba(255, 255, 255, 0.80); color: rgba(255, 255, 255, 0.8);
text-align: center; text-align: center;
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 12px; font-size: 12px;
font-style: normal; font-style: normal;
@@ -422,7 +434,7 @@
justify-content: flex-start; justify-content: flex-start;
gap: 8px; gap: 8px;
padding-bottom: 6px; padding-bottom: 6px;
color: #FFF; color: #fff;
text-overflow: ellipsis; text-overflow: ellipsis;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 16px; font-size: 16px;
@@ -455,8 +467,10 @@
&-tag { &-tag {
overflow: hidden; overflow: hidden;
color: #FFF; color: #fff;
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
text-overflow: ellipsis; text-overflow: ellipsis;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 15px; font-size: 15px;
@@ -520,6 +534,7 @@
height: 100%; height: 100%;
border-radius: 9px; border-radius: 9px;
margin: 0; margin: 0;
object-fit: cover;
} }
} }
} }
@@ -532,10 +547,12 @@
.gameplay-requirements-title { .gameplay-requirements-title {
overflow: hidden; overflow: hidden;
color: #FFF; color: #fff;
height: 24px; height: 24px;
padding-bottom: 6px; padding-bottom: 6px;
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
text-overflow: ellipsis; text-overflow: ellipsis;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 16px; font-size: 16px;
@@ -559,8 +576,10 @@
align-self: stretch; align-self: stretch;
&-title { &-title {
color: rgba(255, 255, 255, 0.80); color: rgba(255, 255, 255, 0.8);
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 14px; font-size: 14px;
font-style: normal; font-style: normal;
@@ -569,8 +588,10 @@
} }
&-desc { &-desc {
color: #FFF; color: #fff;
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 15px; font-size: 15px;
font-style: normal; font-style: normal;
@@ -590,8 +611,10 @@
padding-bottom: 6px; padding-bottom: 6px;
align-items: center; align-items: center;
overflow: hidden; overflow: hidden;
color: #FFF; color: #fff;
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
text-overflow: ellipsis; text-overflow: ellipsis;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 16px; font-size: 16px;
@@ -620,7 +643,7 @@
gap: 8px; gap: 8px;
align-self: stretch; align-self: stretch;
border-radius: 20px; border-radius: 20px;
border: 1px dashed rgba(255, 255, 255, 0.20); border: 1px dashed rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.16); background: rgba(255, 255, 255, 0.16);
flex: 0 0 auto; flex: 0 0 auto;
@@ -630,8 +653,10 @@
} }
&-text { &-text {
color: rgba(255, 255, 255, 0.60); color: rgba(255, 255, 255, 0.6);
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 12px; font-size: 12px;
font-style: normal; font-style: normal;
@@ -661,7 +686,7 @@
align-items: center; align-items: center;
gap: 4px; gap: 4px;
border-radius: 20px; border-radius: 20px;
border: 0.5px solid rgba(255, 255, 255, 0.20); border: 0.5px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.16); background: rgba(255, 255, 255, 0.16);
flex: 0 0 auto; flex: 0 0 auto;
@@ -674,7 +699,9 @@
width: 100%; width: 100%;
color: rgba(255, 255, 255, 0.85); color: rgba(255, 255, 255, 0.85);
text-align: center; text-align: center;
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 13px; font-size: 13px;
font-style: normal; font-style: normal;
@@ -688,7 +715,9 @@
&-level { &-level {
color: rgba(255, 255, 255, 0.45); color: rgba(255, 255, 255, 0.45);
text-align: center; text-align: center;
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 12px; font-size: 12px;
font-style: normal; font-style: normal;
@@ -697,9 +726,11 @@
} }
&-role { &-role {
color: #FFF; color: #fff;
text-align: center; text-align: center;
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 12px; font-size: 12px;
font-style: normal; font-style: normal;
@@ -719,8 +750,10 @@
overflow: hidden; overflow: hidden;
padding-bottom: 7px; padding-bottom: 7px;
height: 24px; height: 24px;
color: #FFF; color: #fff;
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
text-overflow: ellipsis; text-overflow: ellipsis;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 16px; font-size: 16px;
@@ -744,8 +777,10 @@
&-tag { &-tag {
overflow: hidden; overflow: hidden;
color: #FFF; color: #fff;
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
text-overflow: ellipsis; text-overflow: ellipsis;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 15px; font-size: 15px;
@@ -758,7 +793,9 @@
&-text { &-text {
overflow: hidden; overflow: hidden;
color: rgba(255, 255, 255, 0.65); color: rgba(255, 255, 255, 0.65);
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 15px; font-size: 15px;
font-style: normal; font-style: normal;
@@ -775,8 +812,10 @@
overflow: hidden; overflow: hidden;
padding-bottom: 6px; padding-bottom: 6px;
height: 24px; height: 24px;
color: #FFF; color: #fff;
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
text-overflow: ellipsis; text-overflow: ellipsis;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 16px; font-size: 16px;
@@ -816,7 +855,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 5px; gap: 5px;
color: rgba(255, 255, 255, 0.60); color: rgba(255, 255, 255, 0.6);
font-size: 12px; font-size: 12px;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
@@ -826,7 +865,7 @@
&-separator { &-separator {
width: 1px; width: 1px;
height: 10px; height: 10px;
color: rgba(255, 255, 255, 0.20); color: rgba(255, 255, 255, 0.2);
} }
} }
} }
@@ -837,7 +876,8 @@
gap: 8px; gap: 8px;
margin-left: auto; margin-left: auto;
.organizer-actions-follow, .organizer-actions-comment { .organizer-actions-follow,
.organizer-actions-comment {
display: flex; display: flex;
height: 32px; height: 32px;
box-sizing: border-box; box-sizing: border-box;
@@ -857,7 +897,7 @@
.organizer-actions-follow { .organizer-actions-follow {
padding: 8px 12px 8px; padding: 8px 12px 8px;
&-text { &-text {
color: #FFF; color: #fff;
font-size: 13px; font-size: 13px;
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
@@ -913,7 +953,7 @@
gap: 6px; gap: 6px;
flex: 0 0 auto; flex: 0 0 auto;
border-radius: 20px; border-radius: 20px;
border: 1px solid rgba(33, 178, 0, 0.20); border: 1px solid rgba(33, 178, 0, 0.2);
background: rgba(255, 255, 255, 0.16); background: rgba(255, 255, 255, 0.16);
padding: 12px 0 12px 15px; padding: 12px 0 12px 15px;
box-sizing: border-box; box-sizing: border-box;
@@ -953,7 +993,9 @@
gap: 2px; gap: 2px;
overflow: hidden; overflow: hidden;
color: rgba(255, 255, 255, 0.45); color: rgba(255, 255, 255, 0.45);
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
text-overflow: ellipsis; text-overflow: ellipsis;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 12px; font-size: 12px;
@@ -976,7 +1018,9 @@
display: flex; display: flex;
gap: 4px; gap: 4px;
&-applications, &-level-requirements, &-play-type { &-applications,
&-level-requirements,
&-play-type {
color: rgba(255, 255, 255, 0.85); color: rgba(255, 255, 255, 0.85);
font-size: 11px; font-size: 11px;
font-style: normal; font-style: normal;
@@ -1024,7 +1068,7 @@
gap: 16px; gap: 16px;
border-radius: 16px; border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.06);
background: #FFF; background: #fff;
.sticky-bottom-bar-share { .sticky-bottom-bar-share {
display: flex; display: flex;
@@ -1049,7 +1093,7 @@
&-separator { &-separator {
width: 1px; width: 1px;
height: 24px; height: 24px;
background: rgba(0, 0, 0, 0.10); background: rgba(0, 0, 0, 0.1);
} }
.sticky-bottom-bar-comment { .sticky-bottom-bar-comment {
@@ -1085,7 +1129,7 @@
flex: 1 0 0; flex: 1 0 0;
border-radius: 16px; border-radius: 16px;
border: 1px solid rgba(0, 0, 0, 0.06); border: 1px solid rgba(0, 0, 0, 0.06);
background: #FFF; background: #fff;
&-price { &-price {
font-family: "PoetsenOne"; font-family: "PoetsenOne";
@@ -1130,4 +1174,3 @@
} }
} }
} }

View File

@@ -23,6 +23,8 @@ import {
DisplayConditionType, DisplayConditionType,
} from "@/components/NTRPEvaluatePopup"; } from "@/components/NTRPEvaluatePopup";
import DetailService, { MATCH_STATUS } from "@/services/detailService"; import DetailService, { MATCH_STATUS } from "@/services/detailService";
import * as LoginService from "@/services/loginService";
import OrderService from "@/services/orderService";
import { getCurrentLocation, calculateDistance } from "@/utils/locationUtils"; import { getCurrentLocation, calculateDistance } from "@/utils/locationUtils";
import { useUserInfo, useUserActions } from "@/store/userStore"; import { useUserInfo, useUserActions } from "@/store/userStore";
import img from "@/config/images"; import img from "@/config/images";
@@ -218,10 +220,16 @@ function navto(url) {
function StickyButton(props) { function StickyButton(props) {
const { handleShare, handleJoinGame, detail } = props; const { handleShare, handleJoinGame, detail } = props;
const ntrpRef = useRef(null); const ntrpRef = useRef(null);
const userInfo = useUserInfo(); // const userInfo = useUserInfo();
const { id } = userInfo; // const { id } = userInfo;
const { publisher_id, match_status, price, user_action_status, end_time } = const {
detail || {}; id,
publisher_id,
match_status,
price,
user_action_status,
end_time,
} = detail || {};
function handleSelfEvaluate() { function handleSelfEvaluate() {
// TODO: 打开自评弹窗 // TODO: 打开自评弹窗
@@ -266,7 +274,14 @@ function StickyButton(props) {
} else if (can_pay) { } else if (can_pay) {
return { return {
text: "继续支付", text: "继续支付",
action: handleJoinGame, action: async () => {
const res = await OrderService.getUnpaidOrder(id);
if (res.code === 0) {
Taro.navigateTo({
url: `/mod_user/orderDetail/index?id=${res.data.order_info.order_id}`,
});
}
},
}; };
} else if (can_substitute) { } else if (can_substitute) {
return { return {
@@ -323,7 +338,7 @@ function StickyButton(props) {
}; };
} }
const role = Number(publisher_id) === id ? "ownner" : "visitor"; // const role = Number(publisher_id) === id ? "ownner" : "visitor";
return ( return (
<View className="sticky-bottom-bar"> <View className="sticky-bottom-bar">
@@ -359,8 +374,17 @@ function StickyButton(props) {
// 球局信息 // 球局信息
function GameInfo(props) { function GameInfo(props) {
const { detail, currentLocation } = props; const { detail, currentLocation } = props;
const { latitude, longitude, location, location_name, start_time, end_time } = const {
detail || {}; latitude,
longitude,
location,
location_name,
start_time,
end_time,
weather = [{}],
} = detail || {};
const [{ iconDay, tempMax, tempMin }] = weather;
const openMap = () => { const openMap = () => {
Taro.openLocation({ Taro.openLocation({
@@ -374,7 +398,9 @@ function GameInfo(props) {
const [c_latitude, c_longitude] = currentLocation; const [c_latitude, c_longitude] = currentLocation;
const distance = const distance =
calculateDistance(c_latitude, c_longitude, latitude, longitude) / 1000; latitude && longitude
? calculateDistance(c_latitude, c_longitude, latitude, longitude) / 1000
: 0;
const startTime = dayjs(start_time); const startTime = dayjs(start_time);
const endTime = dayjs(end_time); const endTime = dayjs(end_time);
@@ -409,11 +435,16 @@ function GameInfo(props) {
<View className="detail-page-content-game-info-date-weather-weather"> <View className="detail-page-content-game-info-date-weather-weather">
{/* Weather icon */} {/* Weather icon */}
<View className="detail-page-content-game-info-date-weather-weather-icon"> <View className="detail-page-content-game-info-date-weather-weather-icon">
<Image className="weather-icon" src={img.ICON_WEATHER_SUN} /> {/*<Image className="weather-icon" src={img.ICON_WEATHER_SUN} />*/}
<i className={`qi-${iconDay}`}></i>
</View> </View>
{/* Weather text and temperature */} {/* Weather text and temperature */}
<View className="detail-page-content-game-info-date-weather-weather-text-temperature"> <View className="detail-page-content-game-info-date-weather-weather-text-temperature">
<Text>28 - 32</Text> {tempMin && tempMax && (
<Text>
{tempMin} - {tempMax}
</Text>
)}
</View> </View>
</View> </View>
</View> </View>
@@ -431,18 +462,22 @@ function GameInfo(props) {
{/* location message */} {/* location message */}
<View className="location-message-text"> <View className="location-message-text">
{/* venue name and distance */} {/* venue name and distance */}
<View {distance ? (
className="location-message-text-name-distance" <View
onClick={openMap} className="location-message-text-name-distance"
> onClick={openMap}
<Text>{location_name || "-"}</Text> >
<Text>·</Text> <Text>{location_name || "-"}</Text>
<Text>{distance.toFixed(1)}km</Text> <Text>·</Text>
<Image <Text>{distance.toFixed(1)}km</Text>
className="location-message-text-name-distance-arrow" <Image
src={img.ICON_DETAIL_ARROW_RIGHT} className="location-message-text-name-distance-arrow"
/> src={img.ICON_DETAIL_ARROW_RIGHT}
</View> />
</View>
) : (
""
)}
{/* venue address */} {/* venue address */}
<View className="location-message-text-address"> <View className="location-message-text-address">
<Text>{location || "-"}</Text> <Text>{location || "-"}</Text>
@@ -454,8 +489,8 @@ function GameInfo(props) {
{longitude && latitude && ( {longitude && latitude && (
<Map <Map
className="location-map-map" className="location-map-map"
longitude={longitude} longitude={c_longitude}
latitude={latitude} latitude={c_latitude}
markers={[ markers={[
{ {
id: 1, id: 1,
@@ -468,7 +503,7 @@ function GameInfo(props) {
]} ]}
includePoints={[ includePoints={[
{ latitude, longitude }, { latitude, longitude },
{ latitude: currentLocation[0], longitude: currentLocation[1] }, { latitude: c_latitude, longitude: c_longitude },
]} ]}
includePadding={{ left: 50, right: 50, top: 50, bottom: 50 }} includePadding={{ left: 50, right: 50, top: 50, bottom: 50 }}
onError={() => {}} onError={() => {}}
@@ -580,7 +615,7 @@ function genNTRPRequirementText(min, max) {
} else if (max) { } else if (max) {
return `${max} 以上`; return `${max} 以上`;
} }
return '-' return "-";
} }
// 玩法要求 // 玩法要求
function GamePlayAndRequirement(props) { function GamePlayAndRequirement(props) {
@@ -627,65 +662,87 @@ function GamePlayAndRequirement(props) {
function Participants(props) { function Participants(props) {
const { detail = {}, handleJoinGame } = props; const { detail = {}, handleJoinGame } = props;
const participants = detail.participants || []; const participants = detail.participants || [];
const { participant_count, max_participants, user_action_status = {} } = detail const {
const { can_join } = user_action_status participant_count,
const leftCount = max_participants - participant_count max_participants,
user_action_status = {},
} = detail;
const { can_join, can_pay, can_substitute, is_substituting, waiting_start } =
user_action_status;
const showApplicationEntry =
[can_pay, can_substitute, is_substituting, waiting_start].every(
(item) => !item,
) && can_join;
const leftCount = max_participants - participant_count;
const organizer_id = Number(detail.publisher_id); const organizer_id = Number(detail.publisher_id);
return ( return (
<View className="detail-page-content-participants"> <View className="detail-page-content-participants">
<View className="participants-title"> <View className="participants-title">
<Text></Text> <Text></Text>
<Text>·</Text> <Text>·</Text>
<Text>{leftCount > 0 ? `剩余空位 ${leftCount}` : '已满员'}</Text> <Text>{leftCount > 0 ? `剩余空位 ${leftCount}` : "已满员"}</Text>
</View>
<View className="participants-list">
{/* application */}
{can_join && <View
className="participants-list-application"
onClick={() => {
handleJoinGame()
// Taro.showToast({ title: "To be continued", icon: "none" });
}}
>
<Image
className="participants-list-application-icon"
src={img.ICON_DETAIL_APPLICATION_ADD}
/>
<Text className="participants-list-application-text"></Text>
</View>}
{/* participants list */}
<ScrollView className="participants-list-scroll" scrollX>
<View
className="participants-list-scroll-content"
style={{
width: `${participants.length * 103 + (participants.length - 1) * 8}px`,
}}
>
{participants.map((participant) => {
const {
user: { avatar_url, nickname, level, id: participant_user_id },
} = participant;
const role =
participant_user_id === organizer_id ? "组织者" : "参与者";
return (
<View key={participant.id} className="participants-list-item">
<Avatar
className="participants-list-item-avatar"
src={avatar_url}
/>
<Text className="participants-list-item-name">
{nickname || "未知"}
</Text>
<Text className="participants-list-item-level">
{level || "未知"}
</Text>
<Text className="participants-list-item-role">{role}</Text>
</View>
);
})}
</View>
</ScrollView>
</View> </View>
{participant_count > 0 || showApplicationEntry ? (
<View className="participants-list">
{/* application */}
{showApplicationEntry && (
<View
className="participants-list-application"
onClick={() => {
handleJoinGame();
// Taro.showToast({ title: "To be continued", icon: "none" });
}}
>
<Image
className="participants-list-application-icon"
src={img.ICON_DETAIL_APPLICATION_ADD}
/>
<Text className="participants-list-application-text">
</Text>
</View>
)}
{/* participants list */}
<ScrollView className="participants-list-scroll" scrollX>
<View
className="participants-list-scroll-content"
style={{
width: `${participants.length * 103 + (participants.length - 1) * 8}px`,
}}
>
{participants.map((participant) => {
const {
user: {
avatar_url,
nickname,
level,
id: participant_user_id,
},
} = participant;
const role =
participant_user_id === organizer_id ? "组织者" : "参与者";
return (
<View key={participant.id} className="participants-list-item">
<Avatar
className="participants-list-item-avatar"
src={avatar_url}
/>
<Text className="participants-list-item-name">
{nickname || "未知"}
</Text>
<Text className="participants-list-item-level">
{level || "未知"}
</Text>
<Text className="participants-list-item-role">{role}</Text>
</View>
);
})}
</View>
</ScrollView>
</View>
) : (
""
)}
</View> </View>
); );
} }
@@ -717,48 +774,86 @@ function SupplementalNotes(props) {
); );
} }
function genRecommendGames(games, location, avatar) {
return games.map((item) => {
const {
id,
title,
start_time,
end_time,
court_type,
location_name,
current_players,
max_players,
latitude,
longitude,
skill_level_max,
skill_level_min,
play_type,
} = item;
const [c_latitude, c_longitude] = location;
const distance =
calculateDistance(c_latitude, c_longitude, latitude, longitude) / 1000;
const startTime = dayjs(start_time);
const endTime = dayjs(end_time);
return {
id,
title,
time: startTime.format("YYYY-MM-DD HH:MM"),
timeLength: endTime.diff(startTime, "hour"),
venue: location_name,
venueType: court_type,
distance: `${distance.toFixed(2)}km`,
avatar,
applications: max_players,
checkedApplications: current_players,
levelRequirements: `NTRP ${genNTRPRequirementText(skill_level_min, skill_level_max)}`,
playType: play_type,
};
});
}
function OrganizerInfo(props) { function OrganizerInfo(props) {
const recommendGames = [ const {
{ userInfo,
title: "黄浦日场对拉", currentLocation: location,
time: "2025-08-25 9:00", onUpdateUserInfo = () => {},
timeLength: "2小时", } = props;
venue: "上海体育场", const {
veuneType: "室外", id,
distance: "1.2km", nickname,
avatar: "https://img.yzcdn.cn/vant/cat.jpeg", avatar_url,
applications: 10, is_following,
checkedApplications: 3, ntrp_level,
levelRequirements: "NTRP 3.5", stats: { hosted_games_count } = {},
playType: "双打", ongoing_games = [],
}, } = userInfo;
{
title: "黄浦夜场对拉", const myInfo = useUserInfo();
time: "2025-08-25 19:00", const { id: my_id } = myInfo as LoginService.UserInfoType;
timeLength: "2小时",
venue: "上海体育场", const recommendGames = genRecommendGames(ongoing_games, location, avatar_url);
veuneType: "室外",
distance: "1.2km", const toggleFollow = async (follow) => {
avatar: "https://img.yzcdn.cn/vant/cat.jpeg", try {
applications: 10, if (follow) {
checkedApplications: 3, await LoginService.unFollowUser(id);
levelRequirements: "NTRP 3.5", } else {
playType: "双打", await LoginService.followUser(id);
}, }
{ onUpdateUserInfo();
title: "黄浦全天对拉", Taro.showToast({
time: "2025-08-25 9:00", title: `${nickname} ${follow ? "已取消关注" : "已关注"}`,
timeLength: "12小时", icon: "success",
venue: "上海体育场", });
veuneType: "室外", } catch (e) {
distance: "1.2km", Taro.showToast({
avatar: "https://img.yzcdn.cn/vant/cat.jpeg", title: `${nickname} ${follow ? "取消关注失败" : "关注失败"}`,
applications: 10, icon: "error",
checkedApplications: 3, });
levelRequirements: "NTRP 3.5", }
playType: "双打", };
},
];
return ( return (
<View className="detail-page-content-organizer-recommend-games"> <View className="detail-page-content-organizer-recommend-games">
{/* orgnizer title */} {/* orgnizer title */}
@@ -767,26 +862,36 @@ function OrganizerInfo(props) {
</View> </View>
{/* organizer avatar and name */} {/* organizer avatar and name */}
<View className="organizer-avatar-name"> <View className="organizer-avatar-name">
<Avatar <Avatar className="organizer-avatar-name-avatar" src={avatar_url} />
className="organizer-avatar-name-avatar"
src="https://img.yzcdn.cn/vant/cat.jpeg"
/>
<View className="organizer-avatar-name-message"> <View className="organizer-avatar-name-message">
<Text className="organizer-avatar-name-message-name">Light</Text> <Text className="organizer-avatar-name-message-name">{nickname}</Text>
<View className="organizer-avatar-name-message-stats"> <View className="organizer-avatar-name-message-stats">
<Text> 8 </Text> <Text> {hosted_games_count} </Text>
<View className="organizer-avatar-name-message-stats-separator" /> <View className="organizer-avatar-name-message-stats-separator" />
<Text>NTRP 3.5</Text> <Text>NTRP {ntrp_level || "初学者"}</Text>
</View> </View>
</View> </View>
<View className="organizer-actions"> <View className="organizer-actions">
<View className="organizer-actions-follow"> {my_id === id ? (
<Image ""
className="organizer-actions-follow-icon" ) : (
src={img.ICON_DETAIL_APPLICATION_ADD} <View
/> className="organizer-actions-follow"
<Text className="organizer-actions-follow-text"></Text> onClick={toggleFollow.bind(null, is_following)}
</View> >
{is_following ? (
<Text className="organizer-actions-follow-text"></Text>
) : (
<>
<Image
className="organizer-actions-follow-icon"
src={img.ICON_DETAIL_APPLICATION_ADD}
/>
<Text className="organizer-actions-follow-text"></Text>
</>
)}
</View>
)}
<View className="organizer-actions-comment"> <View className="organizer-actions-comment">
<Image <Image
className="organizer-actions-comment-icon" className="organizer-actions-comment-icon"
@@ -797,7 +902,7 @@ function OrganizerInfo(props) {
</View> </View>
{/* recommend games by organizer */} {/* recommend games by organizer */}
<View className="organizer-recommend-games"> <View className="organizer-recommend-games">
<View className="organizer-recommend-games-title"> <View className="organizer-recommend-games-title" onClick={() => {}}>
<Text>TA的更多活动</Text> <Text>TA的更多活动</Text>
<Image <Image
className="organizer-recommend-games-title-arrow" className="organizer-recommend-games-title-arrow"
@@ -825,7 +930,7 @@ function OrganizerInfo(props) {
<View className="recommend-games-list-item-location-venue-distance"> <View className="recommend-games-list-item-location-venue-distance">
<Text>{game.venue}</Text> <Text>{game.venue}</Text>
<Text>·</Text> <Text>·</Text>
<Text>{game.veuneType}</Text> <Text>{game.venueType}</Text>
<Text>·</Text> <Text>·</Text>
<Text>{game.distance}</Text> <Text>{game.distance}</Text>
</View> </View>
@@ -865,26 +970,31 @@ function Index() {
0, 0, 0, 0,
]); ]);
const { id, from } = params; const { id, from } = params;
const { fetchUserInfo, updateUserInfo } = useUserActions(); const [userInfo, setUserInfo] = useState({}); // 组织者的userInfo
const { fetchUserInfo } = useUserActions(); // 获取登录用户的userInfo
const sharePopupRef = useRef<any>(null); const sharePopupRef = useRef<any>(null);
useDidShow(async () => { useDidShow(async () => {
await updateLocation(); await updateLocation();
await fetchUserInfo(); await fetchUserInfo();
await fetchDetail(); // await fetchDetail();
}); });
const updateLocation = async () => { const updateLocation = async () => {
try { try {
const location = await getCurrentLocation() const { address, ...location } = await getCurrentLocation();
setCurrentLocation([location.latitude, location.longitude]) setCurrentLocation([location.latitude, location.longitude]);
// 使用 userStore 中的统一位置更新方法 // 使用 userStore 中的统一位置更新方法
await updateUserInfo({ latitude: location.latitude, longitude: location.longitude }) // await updateUserInfo({ latitude: location.latitude, longitude: location.longitude })
await DetailService.updateLocation({
latitude: Number(location.latitude),
longitude: Number(location.longitude),
});
// 位置更新后,重新获取详情页数据(因为距离等信息可能发生变化) // 位置更新后,重新获取详情页数据(因为距离等信息可能发生变化)
await fetchDetail() await fetchDetail();
} catch (error) { } catch (error) {
console.error("用户位置更新失败", error); console.error("用户位置更新失败", error);
} }
@@ -895,9 +1005,22 @@ function Index() {
const res = await DetailService.getDetail(Number(id)); const res = await DetailService.getDetail(Number(id));
if (res.code === 0) { if (res.code === 0) {
setDetail(res.data); setDetail(res.data);
fetchUserInfoById(res.data.publisher_id);
} }
}; };
const onUpdateUserInfo = () => {
fetchUserInfoById(detail.publisher_id);
};
async function fetchUserInfoById(user_id) {
const userDetailInfo = await LoginService.getUserInfoById(Number(user_id));
if (userDetailInfo.code === 0) {
// console.log(userDetailInfo.data);
setUserInfo(userDetailInfo.data);
}
}
function handleShare() { function handleShare() {
sharePopupRef.current.show(); sharePopupRef.current.show();
} }
@@ -965,7 +1088,12 @@ function Index() {
{/* supplemental notes */} {/* supplemental notes */}
<SupplementalNotes detail={detail} /> <SupplementalNotes detail={detail} />
{/* organizer and recommend games by organizer */} {/* organizer and recommend games by organizer */}
<OrganizerInfo detail={detail} /> <OrganizerInfo
detail={detail}
userInfo={userInfo}
currentLocation={currentLocation}
onUpdateUserInfo={onUpdateUserInfo}
/>
{/* sticky bottom action bar */} {/* sticky bottom action bar */}
<StickyButton <StickyButton
handleShare={handleShare} handleShare={handleShare}

View File

@@ -1,47 +1,120 @@
import httpService from './httpService' import httpService from "./httpService";
import type { ApiResponse } from './httpService' import type { ApiResponse } from "./httpService";
// 用户接口 interface VenueImage {
export interface GameDetail { id: string;
id: number, url: string;
title: string, }
venue_id: number,
creator_id: number, interface Weather {
game_date: string, fxDate: string;
start_time: string, tempMax: string;
end_time: string, tempMin: string;
max_participants: number, iconDay: string;
current_participants: number, textDay: string;
ntrp_level: string, iconNight: string;
play_style: string, textNight: string;
description: string, humidity: string;
status: string, }
created_at: string,
updated_at: string, interface UserActionStatus {
can_assess: boolean;
can_join: boolean;
can_substitute: boolean;
can_pay: boolean;
waiting_start: boolean;
is_substituting: boolean;
}
export interface GameData {
image_list: string[];
description_tag: string[];
start_time: string;
end_time: string;
venue_description_tag: string[];
venue_image_list: VenueImage[];
remark_tag: string[];
create_time: string;
last_modify_time: string;
id: number;
title: string;
description: string;
game_type: string;
play_type: string;
publisher_id: string;
venue_id: string;
max_players: number;
current_players: number;
price: string;
price_mode: string;
court_type: string;
court_surface: string;
gender_limit: string;
skill_level_min: string;
skill_level_max: string;
is_urgent: string;
is_substitute_supported: string;
max_substitute_players: number;
current_substitute_count: number;
is_wechat_contact: number;
wechat_contact: string;
privacy_level: string;
member_visibility: string;
match_status: number;
venue_description: string;
location_name: string;
location: string;
latitude: number;
longitude: number;
deadline_hours: number;
remark: string;
venue_dtl: any | null;
formal_members: any[];
substitute_members: any[];
participants: any[];
participant_count: number;
max_participants: number;
weather: Weather[];
user_action_status: UserActionStatus;
} }
export enum MATCH_STATUS { export enum MATCH_STATUS {
NOT_STARTED = 0, // 未开始 NOT_STARTED = 0, // 未开始
IN_PROGRESS = 1, //进行中 IN_PROGRESS = 1, //进行中
FINISHED = 2 //已结束 FINISHED = 2, //已结束
} }
// 响应接口 export interface UpdateLocationRes {
export interface Response { latitude: number;
code: string longitude: number;
message: string country: string;
data: GameDetail province: string;
city: string;
district: string;
} }
// 发布球局类 // 发布球局类
class GameDetailService { class GameDetailService {
// 用户登录 // 用户登录
async getDetail(id: number): Promise<ApiResponse<Response>> { async getDetail(id: number): Promise<ApiResponse<GameData>> {
return httpService.post('/games/detail', { id }, { return httpService.post(
"/games/detail",
{ id },
{
showLoading: true,
},
);
}
async updateLocation(location: {
latitude: number;
longitude: number;
}): Promise<ApiResponse<UpdateLocationRes>> {
return httpService.post("/user/update_location", location, {
showLoading: true, showLoading: true,
}) });
} }
} }
// 导出认证服务实例 // 导出认证服务实例
export default new GameDetailService() export default new GameDetailService();

View File

@@ -1,7 +1,7 @@
import Taro from "@tarojs/taro"; import Taro from "@tarojs/taro";
import httpService, { ApiResponse } from "./httpService"; import httpService, { ApiResponse } from "./httpService";
import tokenManager from '../utils/tokenManager'; import tokenManager from "../utils/tokenManager";
import { useUser } from '@/store/userStore'; import { useUser } from "@/store/userStore";
// 微信用户信息接口 // 微信用户信息接口
export interface WechatUserInfo { export interface WechatUserInfo {
@@ -49,7 +49,7 @@ export interface UserStats {
export interface PhoneLoginParams { export interface PhoneLoginParams {
phone: string; phone: string;
verification_code: string; verification_code: string;
user_code: string user_code: string;
} }
export interface UserInfoType { export interface UserInfoType {
@@ -105,7 +105,7 @@ export const wechat_auth_login = async (
try { try {
await useUser.getState().fetchUserInfo(); await useUser.getState().fetchUserInfo();
} catch (error) { } catch (error) {
console.error('更新用户信息到 store 失败:', error); console.error("更新用户信息到 store 失败:", error);
} }
return { return {
@@ -158,7 +158,7 @@ export const phone_auth_login = async (
try { try {
await useUser.getState().fetchUserInfo(); await useUser.getState().fetchUserInfo();
} catch (error) { } catch (error) {
console.error('更新用户信息到 store 失败:', error); console.error("更新用户信息到 store 失败:", error);
} }
return { return {
@@ -410,4 +410,40 @@ export const updateUserPhone = async (payload: ChangePhoneParams) => {
console.error("更新用户手机号失败:", error); console.error("更新用户手机号失败:", error);
throw error; throw error;
} }
}
// 获取指定用户信息
export const getUserInfoById = async (id) => {
try {
const response = await httpService.post("/user/detail_by_id", { id });
return response;
} catch (error) {
console.error("获取用户信息失败:", error);
throw error;
}
};
// 关注用户
export const followUser = async (following_id) => {
try {
const response = await httpService.post("/wch_users/follow", {
following_id,
});
return response;
} catch (error) {
console.error("关注失败:", error);
throw error;
}
};
// 取消关注用户
export const unFollowUser = async (following_id) => {
try {
const response = await httpService.post("/wch_users/unfollow", {
following_id,
});
return response;
} catch (error) {
console.error("取消关注失败:", error);
throw error;
}
}; };

View File

@@ -1,90 +1,125 @@
import httpService from './httpService' import httpService from "./httpService";
import type { ApiResponse } from './httpService' import type { ApiResponse } from "./httpService";
import { requestPayment } from '@tarojs/taro'
export interface SignType { export interface SignType {
/** 仅在微信支付 v2 版本接口适用 */ /** 仅在微信支付 v2 版本接口适用 */
MD5 MD5;
/** 仅在微信支付 v2 版本接口适用 */ /** 仅在微信支付 v2 版本接口适用 */
'HMAC-SHA256' "HMAC-SHA256";
/** 仅在微信支付 v3 版本接口适用 */ /** 仅在微信支付 v3 版本接口适用 */
RSA RSA;
}
export enum OrderStatus {
PENDING = 0,
PAID,
FINISHED,
} }
export interface PayMentParams { export interface PayMentParams {
order_id: number, order_id: number;
order_no: string, order_no: string;
status: number, status: number;
appId: string, appId: string;
timeStamp: string, timeStamp: string;
nonceStr: string, nonceStr: string;
package: string, package: string;
signType: keyof SignType, signType: keyof SignType;
paySign: string paySign: string;
} }
// 用户接口 // 用户接口
export interface OrderResponse { export interface OrderResponse {
participant_id: number, participant_id: number;
payment_required: boolean, payment_required: boolean;
payment_params: PayMentParams payment_params: PayMentParams;
} }
export interface OrderInfo { export interface OrderInfo {
time: string time: string;
address: string address: string;
registrant_nickname: string registrant_nickname: string;
registrant_phone: string registrant_phone: string;
cost: string cost: string;
} }
export interface RefundPolicy { export interface RefundPolicy {
application_time: string application_time: string;
refund_rule: string refund_rule: string;
} }
export interface GameStatus { export interface GameStatus {
current_players: number current_players: number;
max_players: number max_players: number;
is_full: boolean is_full: boolean;
can_join: boolean can_join: boolean;
} }
export interface GameDetails { export interface GameDetails {
game_id: number game_id: number;
game_title: string game_title: string;
game_description: string game_description: string;
} }
export interface GameOrderRes { export interface GameOrderRes {
order_info: OrderInfo order_info: OrderInfo;
refund_policy: RefundPolicy[] refund_policy: RefundPolicy[];
notice: string notice: string;
game_status: GameStatus game_status: GameStatus;
game_details: GameDetails game_details: GameDetails;
} }
// 发布球局类 // 发布球局类
class OrderService { class OrderService {
// 查询订单列表 // 查询订单列表
async getOrderList() { async getOrderList() {
return httpService.post('/user/orders', {}, { showLoading: true }) return httpService.post("/user/orders", {}, { showLoading: true });
}
// 创建订单
async createOrder(game_id: number): Promise<ApiResponse<OrderResponse>> {
return httpService.post('/payment/create_order', { game_id }, {
showLoading: true,
})
} }
async getOrderInfo(game_id: number): Promise<ApiResponse<GameOrderRes>> { // 获取订单详情
return httpService.post('/payment/check_order', { game_id }, { async getOrderDetail(order_id: number): Promise<ApiResponse<any>> {
showLoading: true, return httpService.post(
}) "/payment/order_details",
{ order_id },
{
showLoading: true,
},
);
}
// 创建订单
async createOrder(game_id: number): Promise<ApiResponse<OrderResponse>> {
return httpService.post(
"/payment/create_order",
{ game_id },
{
showLoading: true,
},
);
}
// 检查订单信息
async getCheckOrderInfo(game_id: number): Promise<ApiResponse<GameOrderRes>> {
return httpService.post(
"/payment/check_order",
{ game_id },
{
showLoading: true,
},
);
}
// 获取未支付订单
async getUnpaidOrder(game_id: number): Promise<ApiResponse<any>> {
return httpService.post(
"/payment/get_unpaid_order",
{ game_id },
{
showLoading: true,
},
);
} }
} }
// 导出认证服务实例 // 导出认证服务实例
export default new OrderService() export default new OrderService();

View File

@@ -8333,6 +8333,11 @@ quick-lru@^4.0.1:
resolved "https://registry.npmmirror.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" resolved "https://registry.npmmirror.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
qweather-icons@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/qweather-icons/-/qweather-icons-1.8.0.tgz#25eb2714d68daf13c06032c082f720e6734c4ecb"
integrity sha512-Ti7KGrWXSV7e5HMpjPhDYua8hwC3t0nScNOSQ3kLPehsOja5cioZw8bcKi7jeAGBlVLrgAkLla5xVEYlH1s5Jw==
randombytes@^2.1.0: randombytes@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.npmmirror.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" resolved "https://registry.npmmirror.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"