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

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

View File

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

View File

@@ -1,50 +1,49 @@
import { Component, ReactNode } from 'react'
import './nutui-theme.scss'
import './app.scss'
import { useDictionaryStore } from './store/dictionaryStore'
import { useGlobalStore } from './store/global'
import { Component, ReactNode } from "react";
import "./nutui-theme.scss";
import "./app.scss";
import "qweather-icons/font/qweather-icons.css";
import { useDictionaryStore } from "./store/dictionaryStore";
import { useGlobalStore } from "./store/global";
// import { getNavbarHeight } from "@/utils/getNavbarHeight";
interface AppProps {
children: ReactNode
children: ReactNode;
}
class App extends Component<AppProps> {
onLaunch() {
console.log('小程序启动,初始化逻辑写这里')
console.log("小程序启动,初始化逻辑写这里");
}
componentDidMount() {
// 初始化字典数据
this.initDictionaryData()
this.getNavBarHeight()
this.initDictionaryData();
this.getNavBarHeight();
// this.getLocation()
}
componentDidShow() { }
componentDidShow() {}
componentDidHide() { }
componentDidHide() {}
// 初始化字典数据
private async initDictionaryData() {
try {
const { fetchDictionary } = useDictionaryStore.getState()
await fetchDictionary()
const { fetchDictionary } = useDictionaryStore.getState();
await fetchDictionary();
} catch (error) {
console.error('初始化字典数据失败:', error)
console.error("初始化字典数据失败:", error);
}
}
// 获取导航高度
getNavBarHeight = () => {
const { getNavbarHeightInfo } = useGlobalStore.getState()
getNavbarHeightInfo()
}
const { getNavbarHeightInfo } = useGlobalStore.getState();
getNavbarHeightInfo();
};
// 获取位置信息
// 获取位置信息
// getLocation = () => {
// const { getCurrentLocationInfo } = useGlobalStore.getState()
// getCurrentLocationInfo()
@@ -52,8 +51,8 @@ class App extends Component<AppProps> {
render() {
// 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 Taro from '@tarojs/taro'
import { check_login_status } from '@/services/loginService'
import React, { useEffect, useState } from "react";
import { View } from "@tarojs/components";
import Taro from "@tarojs/taro";
import { check_login_status } from "@/services/loginService";
export function getCurrentFullPath(): string {
const pages = Taro.getCurrentPages();
@@ -31,18 +30,30 @@ export default function withAuth<P extends object>(
const is_login = check_login_status();
setAuthed(is_login);
if (!is_login) {
const currentPage = getCurrentFullPath();
// Taro.redirectTo({
// url: `/pages/login/index/index${
// currentPage ? `?redirect=${encodeURIComponent(currentPage)}` : ''
// }`,
// })
}
// if (!is_login) {
// const currentPage = getCurrentFullPath();
// Taro.redirectTo({
// url: `/pages/login/index/index${
// currentPage ? `?redirect=${encodeURIComponent(currentPage)}` : ""
// }`,
// });
// }
}, []);
// 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} />;

View File

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

View File

@@ -1,5 +1,16 @@
@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 {
padding: 20px;
box-sizing: border-box;
@@ -193,7 +204,9 @@
align-items: center;
align-self: stretch;
color: #000;
font-feature-settings: 'liga' off, 'clig' off;
font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
@@ -205,7 +218,7 @@
.summaryList {
border-radius: 12px;
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);
.summaryItem {
@@ -218,8 +231,10 @@
.title {
width: 120px;
display: inline-block;
color: rgba(60, 60, 67, 0.60);
font-feature-settings: 'liga' off, 'clig' off;
color: rgba(60, 60, 67, 0.6);
font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
@@ -229,7 +244,9 @@
.content {
color: #000;
font-feature-settings: 'liga' off, 'clig' off;
font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
@@ -248,7 +265,9 @@
align-items: center;
align-self: stretch;
color: #000;
font-feature-settings: 'liga' off, 'clig' off;
font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
@@ -260,7 +279,7 @@
.policyList {
border-radius: 12px;
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);
.policyItem {
@@ -269,7 +288,9 @@
align-items: center;
color: #000;
text-align: center;
font-feature-settings: 'liga' off, 'clig' off;
font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC";
font-size: 12px;
font-style: normal;
@@ -280,7 +301,9 @@
&:nth-child(1) {
color: #000;
text-align: center;
font-feature-settings: 'liga' off, 'clig' off;
font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
@@ -289,7 +312,8 @@
border: none;
}
.time, .rule {
.time,
.rule {
width: 50%;
padding: 10px 12px;
}
@@ -314,7 +338,9 @@
align-items: center;
align-self: stretch;
color: #000;
font-feature-settings: 'liga' off, 'clig' off;
font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
@@ -324,7 +350,7 @@
}
.content {
color: rgba(22, 24, 35, 0.60);
color: rgba(22, 24, 35, 0.6);
font-family: "PingFang SC";
font-size: 14px;
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 dayjs from "dayjs";
import { delay } from "@/utils";
import orderService, { GameOrderRes } from "@/services/orderService";
import detailService, { GameDetail } from "@/services/detailService";
import orderService, {
GameOrderRes,
OrderStatus,
} from "@/services/orderService";
import detailService, { GameData } from "@/services/detailService";
import { withAuth } from "@/components";
import { calculateDistance, getCurrentLocation } from "@/utils";
import img from "@/config/images";
@@ -111,7 +114,7 @@ function GameInfo(props) {
}
function OrderMsg(props) {
const { detail, orderInfo } = props;
const { detail, checkOrderInfo } = props;
const {
start_time,
end_time,
@@ -120,10 +123,10 @@ function OrderMsg(props) {
wechat_contact,
price,
} = detail;
const { order_info: { registrant_nickname } = {} } = orderInfo
const { order_info: { registrant_nickname } = {} } = checkOrderInfo;
const startTime = dayjs(start_time);
const endTime = dayjs(end_time);
const startYear = startTime.format('YYYY')
const startYear = startTime.format("YYYY");
const startMonth = startTime.format("M");
const startDay = startTime.format("D");
const startDate = `${startYear}${startMonth}${startDay}`;
@@ -157,32 +160,38 @@ function OrderMsg(props) {
</View>
{/* 订单信息摘要 */}
<View className={styles.summaryList}>
{
summary.map((item, index) => (<View key={index} className={styles.summaryItem}>
{summary.map((item, index) => (
<View key={index} className={styles.summaryItem}>
<Text className={styles.title}>{item.title}</Text>
<Text className={styles.content}>{item.content}</Text>
</View>))
}
</View>
))}
</View>
</View>
);
}
function RefundPolicy(props) {
const { orderInfo } = props
const { refund_policy = [] } = orderInfo
const policyList = [{
time: '申请退款时间',
rule: '退款规则',
}, ...refund_policy.map((item, index) => {
const [, theTime] = item.application_time.split('undefined ')
const theTimeObj = dayjs(theTime)
const year = theTimeObj.format('YYYY')
const month = theTimeObj.format('M')
const day = theTimeObj.format('D')
const time = theTimeObj.format('HH:MM')
return { time: `${year}${month}${day}${time}${index === 0 ? '前' : '后'}`, rule: item.refund_rule }
})]
const { checkOrderInfo } = props;
const { refund_policy = [] } = checkOrderInfo;
const policyList = [
{
time: "申请退款时间",
rule: "退款规则",
},
...refund_policy.map((item, index) => {
const [, theTime] = item.application_time.split("undefined ");
const theTimeObj = dayjs(theTime);
const year = theTimeObj.format("YYYY");
const month = theTimeObj.format("M");
const day = theTimeObj.format("D");
const time = theTimeObj.format("HH:MM");
return {
time: `${year}${month}${day}${time}${index === 0 ? "前" : "后"}`,
rule: item.refund_rule,
};
}),
];
return (
<View className={styles.refundPolicy}>
<View className={styles.moduleTitle}>
@@ -190,12 +199,12 @@ function RefundPolicy(props) {
</View>
{/* 订单信息摘要 */}
<View className={styles.policyList}>
{
policyList.map((item, index) => (<View key={index} className={styles.policyItem}>
{policyList.map((item, index) => (
<View key={index} className={styles.policyItem}>
<View className={styles.time}>{item.time}</View>
<View className={styles.rule}>{item.rule}</View>
</View>))
}
</View>
))}
</View>
</View>
);
@@ -212,22 +221,43 @@ function Disclaimer() {
const OrderCheck = () => {
const { params } = useRouter();
const { id, gameId } = params;
const [detail, setDetail] = useState<GameDetail | {}>({});
const { id: stringId, gameId: stringGameId } = params;
const [id, gameId] = [Number(stringId), Number(stringGameId)];
const [detail, setDetail] = useState<GameData | {}>({});
const [location, setLocation] = useState<number[]>([0, 0]);
const [orderInfo, setOrderInfo] = useState<GameOrderRes | {}>({})
const [checkOrderInfo, setCheckOrderInfo] = useState<GameOrderRes | {}>({});
const [orderDetail, setOrderDetail] = useState({});
useDidShow(async () => {
const res = await detailService.getDetail(Number(gameId));
const orderRes = await orderService.getOrderInfo(Number(gameId))
setOrderInfo(orderRes.data)
console.log(res);
if (res.code === 0) {
setDetail(res.data);
let gameDetail = {};
if (id) {
const res = await orderService.getOrderDetail(id);
if (res.code === 0) {
setOrderDetail(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();
setLocation([location.latitude, location.longitude]);
});
}
//TODO: get order msg from id
const handlePay = async () => {
@@ -235,56 +265,102 @@ const OrderCheck = () => {
title: "支付中...",
mask: true,
});
const res = await orderService.createOrder(Number(gameId));
if (res.code === 0) {
const { payment_required, payment_params } = res.data;
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",
});
},
});
let wxPayRes: any = {};
try {
if (orderDetail.game_info?.id) {
const res = await orderService.getUnpaidOrder(orderDetail.game_info.id);
if (res.code === 0) {
wxPayRes = {
...res.data,
payment_required: res.data.has_unpaid_order,
};
}
} else {
const res = await orderService.createOrder(detail.id);
if (res.code === 0) {
wxPayRes = res.data;
}
}
} 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 (
<View className={styles.container}>
{/* Game Date and Address */}
<GameInfo detail={detail} currentLocation={location} />
{/* Order message */}
<OrderMsg detail={detail} orderInfo={orderInfo} />
<OrderMsg detail={detail} checkOrderInfo={checkOrderInfo} />
{/* Refund policy */}
<RefundPolicy orderInfo={orderInfo} />
<RefundPolicy checkOrderInfo={checkOrderInfo} />
{/* 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 File

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

View File

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

View File

@@ -1,47 +1,120 @@
import httpService from './httpService'
import type { ApiResponse } from './httpService'
import httpService from "./httpService";
import type { ApiResponse } from "./httpService";
// 用户接口
export interface GameDetail {
id: number,
title: string,
venue_id: number,
creator_id: number,
game_date: string,
start_time: string,
end_time: string,
max_participants: number,
current_participants: number,
ntrp_level: string,
play_style: string,
description: string,
status: string,
created_at: string,
updated_at: string,
interface VenueImage {
id: string;
url: string;
}
interface Weather {
fxDate: string;
tempMax: string;
tempMin: string;
iconDay: string;
textDay: string;
iconNight: string;
textNight: string;
humidity: 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 {
NOT_STARTED = 0, // 未开始
IN_PROGRESS = 1, //进行中
FINISHED = 2 //已结束
NOT_STARTED = 0, // 未开始
IN_PROGRESS = 1, //进行中
FINISHED = 2, //已结束
}
// 响应接口
export interface Response {
code: string
message: string
data: GameDetail
export interface UpdateLocationRes {
latitude: number;
longitude: number;
country: string;
province: string;
city: string;
district: string;
}
// 发布球局类
class GameDetailService {
// 用户登录
async getDetail(id: number): Promise<ApiResponse<Response>> {
return httpService.post('/games/detail', { id }, {
async getDetail(id: number): Promise<ApiResponse<GameData>> {
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,
})
});
}
}
// 导出认证服务实例
export default new GameDetailService()
export default new GameDetailService();

View File

@@ -1,7 +1,7 @@
import Taro from "@tarojs/taro";
import httpService, { ApiResponse } from "./httpService";
import tokenManager from '../utils/tokenManager';
import { useUser } from '@/store/userStore';
import tokenManager from "../utils/tokenManager";
import { useUser } from "@/store/userStore";
// 微信用户信息接口
export interface WechatUserInfo {
@@ -49,7 +49,7 @@ export interface UserStats {
export interface PhoneLoginParams {
phone: string;
verification_code: string;
user_code: string
user_code: string;
}
export interface UserInfoType {
@@ -105,7 +105,7 @@ export const wechat_auth_login = async (
try {
await useUser.getState().fetchUserInfo();
} catch (error) {
console.error('更新用户信息到 store 失败:', error);
console.error("更新用户信息到 store 失败:", error);
}
return {
@@ -158,7 +158,7 @@ export const phone_auth_login = async (
try {
await useUser.getState().fetchUserInfo();
} catch (error) {
console.error('更新用户信息到 store 失败:', error);
console.error("更新用户信息到 store 失败:", error);
}
return {
@@ -410,4 +410,40 @@ export const updateUserPhone = async (payload: ChangePhoneParams) => {
console.error("更新用户手机号失败:", 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 type { ApiResponse } from './httpService'
import { requestPayment } from '@tarojs/taro'
import httpService from "./httpService";
import type { ApiResponse } from "./httpService";
export interface SignType {
/** 仅在微信支付 v2 版本接口适用 */
MD5
MD5;
/** 仅在微信支付 v2 版本接口适用 */
'HMAC-SHA256'
"HMAC-SHA256";
/** 仅在微信支付 v3 版本接口适用 */
RSA
RSA;
}
export enum OrderStatus {
PENDING = 0,
PAID,
FINISHED,
}
export interface PayMentParams {
order_id: number,
order_no: string,
status: number,
appId: string,
timeStamp: string,
nonceStr: string,
package: string,
signType: keyof SignType,
paySign: string
order_id: number;
order_no: string;
status: number;
appId: string;
timeStamp: string;
nonceStr: string;
package: string;
signType: keyof SignType;
paySign: string;
}
// 用户接口
export interface OrderResponse {
participant_id: number,
payment_required: boolean,
payment_params: PayMentParams
participant_id: number;
payment_required: boolean;
payment_params: PayMentParams;
}
export interface OrderInfo {
time: string
address: string
registrant_nickname: string
registrant_phone: string
cost: string
time: string;
address: string;
registrant_nickname: string;
registrant_phone: string;
cost: string;
}
export interface RefundPolicy {
application_time: string
refund_rule: string
application_time: string;
refund_rule: string;
}
export interface GameStatus {
current_players: number
max_players: number
is_full: boolean
can_join: boolean
current_players: number;
max_players: number;
is_full: boolean;
can_join: boolean;
}
export interface GameDetails {
game_id: number
game_title: string
game_description: string
game_id: number;
game_title: string;
game_description: string;
}
export interface GameOrderRes {
order_info: OrderInfo
refund_policy: RefundPolicy[]
notice: string
game_status: GameStatus
game_details: GameDetails
order_info: OrderInfo;
refund_policy: RefundPolicy[];
notice: string;
game_status: GameStatus;
game_details: GameDetails;
}
// 发布球局类
class OrderService {
// 查询订单列表
async getOrderList() {
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,
})
return httpService.post("/user/orders", {}, { showLoading: true });
}
async getOrderInfo(game_id: number): Promise<ApiResponse<GameOrderRes>> {
return httpService.post('/payment/check_order', { game_id }, {
showLoading: true,
})
// 获取订单详情
async getOrderDetail(order_id: number): Promise<ApiResponse<any>> {
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"
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:
version "2.1.0"
resolved "https://registry.npmmirror.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"