feat: 订单模块基本完成

This commit is contained in:
2025-09-16 14:34:11 +08:00
parent 4a00c7f1d8
commit a045b39580
13 changed files with 799 additions and 288 deletions

View File

@@ -1,26 +1,9 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { View } from "@tarojs/components"; import { View } from "@tarojs/components";
import Taro from "@tarojs/taro"; import Taro from "@tarojs/taro";
import { getCurrentFullPath } from '@/utils';
import { check_login_status } from "@/services/loginService"; import { check_login_status } from "@/services/loginService";
export function getCurrentFullPath(): string {
const pages = Taro.getCurrentPages();
const currentPage = pages.at(-1);
if (currentPage) {
console.log(currentPage, "currentPage get");
const route = currentPage.route;
const options = currentPage.options || {};
const query = Object.keys(options)
.map((key) => `${key}=${options[key]}`)
.join("&");
return query ? `/${route}?${query}` : `/${route}`;
}
return "";
}
export default function withAuth<P extends object>( export default function withAuth<P extends object>(
WrappedComponent: React.ComponentType<P>, WrappedComponent: React.ComponentType<P>,
) { ) {

View File

@@ -16,6 +16,7 @@ import EditModal from "./EditModal/index";
import withAuth from "./Auth"; import withAuth from "./Auth";
import { CustomPicker, PopupPicker } from "./Picker"; import { CustomPicker, PopupPicker } from "./Picker";
import NTRPEvaluatePopup from "./NTRPEvaluatePopup"; import NTRPEvaluatePopup from "./NTRPEvaluatePopup";
import RefundPopup from "./refundPopup";
export { export {
ActivityTypeSwitch, ActivityTypeSwitch,
@@ -37,4 +38,5 @@ export {
CustomPicker, CustomPicker,
PopupPicker, PopupPicker,
NTRPEvaluatePopup, NTRPEvaluatePopup,
RefundPopup,
}; };

View File

@@ -0,0 +1,132 @@
.refundPolicy {
padding-top: 20px;
// .moduleTitle {
// display: flex;
// padding: 15px 0 8px;
// justify-content: space-between;
// align-items: center;
// align-self: stretch;
// color: #000;
// font-feature-settings:
// "liga" off,
// "clig" off;
// font-family: "PingFang SC";
// font-size: 14px;
// font-style: normal;
// font-weight: 600;
// line-height: 20px;
// letter-spacing: -0.23px;
// }
.specTips {
padding-bottom: 20px;
color: rgba(60, 60, 67, 0.60);
text-align: center;
font-feature-settings: 'liga' off, 'clig' off;
font-family: "PingFang SC";
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 18px;
}
.policyList {
border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.06);
background: #fff;
box-shadow: 0 4px 36px 0 rgba(0, 0, 0, 0.06);
.policyItem {
display: flex;
justify-content: space-around;
align-items: center;
color: #000;
text-align: center;
font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC";
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px;
border-top: 1px solid rgba(0, 0, 0, 0.06);
&:nth-child(1) {
color: #000;
text-align: center;
font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
font-weight: 600;
line-height: 20px;
border: none;
}
.time,
.rule {
width: 50%;
padding: 10px 12px;
}
.rule {
border-left: 1px solid rgba(0, 0, 0, 0.06);
}
}
}
}
.container {
padding: 0 15px 40px;
.header {
padding: 24px 15px 0;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
.title {
color: #000;
text-align: center;
font-feature-settings: 'liga' off, 'clig' off;
font-family: "PingFang SC";
font-size: 16px;
font-style: normal;
font-weight: 600;
line-height: 20px;
margin-left: auto;
}
.closeIcon {
margin-left: auto;
width: 20px;
height: 20px;
}
}
.action {
display: flex;
align-items: center;
justify-content: center;
margin-top: 20px;
padding: 2px 6px;
height: 52px;
border-radius: 16px;
border: 1px solid rgba(0, 0, 0, 0.06);
box-shadow: 0 8px 64px 0 rgba(0, 0, 0, 0.10);
backdrop-filter: blur(16px);
color: #fff;
background-color: #000;
font-feature-settings: 'liga' off, 'clig' off;
font-family: "PingFang SC";
font-size: 16px;
font-style: normal;
font-weight: 600;
line-height: 20px;
letter-spacing: -0.23px;
}
}

View File

@@ -0,0 +1,139 @@
import React, { useState, useRef, forwardRef, useImperativeHandle } from 'react';
import { View, Text, Button, Image } from '@tarojs/components'
import Taro from '@tarojs/taro';
import dayjs from 'dayjs'
import { CommonPopup } from '@/components';
import orderService from '@/services/orderService';
import styles from './index.module.scss'
import closeIcon from '@/static/order/orderListClose.svg'
function genRefundNotice (refund_policy) {
if (refund_policy.length === 0) {
return {}
}
const now = dayjs()
const deadlines = refund_policy.map(item => dayjs(item.deadline_formatted))
let matchPolicyIndex = deadlines.findIndex(d => d.isAfter(now))
if (matchPolicyIndex === -1) {
matchPolicyIndex = refund_policy.length - 1
}
const { deadline_formatted, price, refund_rate } = refund_policy[matchPolicyIndex]
if (refund_rate === 1) {
return { refundPrice: price, notice: `本次可全额退款, ¥${price} 将原路退回,请查收` }
} else if (refund_rate === 0) {
return { refundPrice: 0, notice: `当前退出不可退款,后续流程未明确,@麻真瑜` }
}
const refundPrice = price * refund_rate
const leftHours = dayjs(deadline_formatted).diff(dayjs(), 'hour')
return { refundPrice, notice: `距活动开始已不足${leftHours}h当前退出您需扣除${price - refundPrice}` }
}
function renderCancelContent(checkOrderInfo) {
const { refund_policy = [] } = checkOrderInfo;
const policyList = [
{
time: "申请退款时间",
rule: "退款规则",
},
...refund_policy.map((item) => {
return {
time: item.application_time,
rule: item.refund_rule,
};
}),
];
const { notice } = genRefundNotice(refund_policy)
return (
<View className={styles.refundPolicy}>
{/* <View className={styles.moduleTitle}>
<Text>退款政策</Text>
</View> */}
{<View className={styles.specTips}>{notice}</View>}
{/* 订单信息摘要 */}
<View className={styles.policyList}>
{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>
);
}
export type RefundRef = {
show: (item: any, callback: (result: boolean) => void) => void
}
export default forwardRef<RefundRef>(function RefundPopup(_props, ref) {
const [visible, setVisible] = useState(false)
const [checkOrderInfo, setCheckOrderInfo] = useState({})
const [orderData, setOrderData] = useState({})
const onDown = useRef<((result: boolean) => void) | null>(null)
useImperativeHandle(ref, () => ({
show: onShow,
}))
async function onShow (orderItem, onFinish: (result: boolean) => void) {
const {
game_info,
} = orderItem
onDown.current = onFinish
setOrderData(orderItem)
const res = await orderService.getCheckOrderInfo(game_info.id);
setCheckOrderInfo(res.data);
setVisible(true)
}
function onClose () {
setVisible(false)
onDown.current?.(false)
}
async function handleConfirmQuit () {
const { order_no, amount } = orderData
try {
const refundRes = await orderService.applicateRefund({
order_no,
refund_amount: amount,
refund_reason: "用户主动退款",
});
if (refundRes.code !== 0) {
throw new Error(refundRes.message);
}
Taro.showToast({
title: "退出成功",
icon: "none",
})
onDown.current?.(true)
} catch (e) {
Taro.showToast({
title: e.message,
icon: "error",
});
} finally {
onClose()
}
}
return (
<CommonPopup
visible={visible}
onClose={onClose}
// title="退出活动"
enableDragToClose={false}
zIndex={1001}
hideFooter
>
<View className={styles.container}>
<View className={styles.header}>
<Text className={styles.title}>退</Text>
<Image className={styles.closeIcon} src={closeIcon} onClick={onClose} />
</View>
{renderCancelContent(checkOrderInfo)}
<Button className={styles.action} onClick={handleConfirmQuit}>退</Button>
</View>
</CommonPopup>
)
})

View File

@@ -229,7 +229,34 @@
.gameInfoActions { .gameInfoActions {
min-height: 12px; min-height: 12px;
padding: 0 12px;
border-top: 0.5px solid rgba(0, 0, 0, 0.06); border-top: 0.5px solid rgba(0, 0, 0, 0.06);
display: flex;
align-items: center;
justify-content: flex-start;
gap: 10px;
& > .button {
margin: 12px 0;
padding: 4px 10px;
height: 28px;
border-radius: 999px;
border: 0.5px solid rgba(0, 0, 0, 0.06);
color: #000;
font-size: 12px;
font-style: normal;
font-weight: 600;
line-height: 20px;
letter-spacing: -0.23px;
&:first-child {
background: #000;
color: #fff;
&.payNow {
background-color: #ff3b30;
}
}
}
} }
} }
@@ -432,3 +459,47 @@
font-weight: 600; font-weight: 600;
line-height: normal; line-height: normal;
} }
.dialogFooter {
// width: 100%;
width: calc(100% + 1px);
height: 44px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: flex-end;
position: absolute;
// margin: 0 -24px -24px;
bottom: 0;
left: 0;
border-top: 1px solid rgba(0, 0, 0, 0.06);
border-bottom-left-radius: 16px;
border-bottom-right-radius: 16px;
overflow: hidden;
& > .cancel, & > .confirm {
padding: 12px 10px;
height: 44px;
width: 50%;
text-align: center;
// border: 0.5px solid rgba(0, 0, 0, 0.06);
color: #000;
font-size: 16px;
font-style: normal;
font-weight: 600;
line-height: 20px;
&:last-child {
background: #000;
color: #fff;
}
}
& > .cancel {
border-radius: 0;
}
& > .confirm {
border-radius: 0;
}
}

View File

@@ -1,31 +1,80 @@
import React, { useState } from "react"; import React, { useState, useRef } from "react";
import { View, Text, Button, Image } from "@tarojs/components"; import { View, Text, Button, Image } from "@tarojs/components";
import { Dialog } from "@nutui/nutui-react-taro";
import Taro, { useDidShow, useRouter } from "@tarojs/taro"; import Taro, { useDidShow, useRouter } from "@tarojs/taro";
import dayjs from "dayjs"; import dayjs from "dayjs";
import classnames from "classnames";
import orderService, { import orderService, {
CancelType,
GameOrderRes, GameOrderRes,
OrderStatus, OrderStatus,
RefundStatus,
} from "@/services/orderService"; } from "@/services/orderService";
import { import {
payOrder, payOrder,
delay, delay,
calculateDistance, calculateDistance,
getCurrentLocation, getCurrentLocation,
getOrderStatus,
generateOrderActions,
reloadPage,
} from "@/utils"; } from "@/utils";
import detailService, { GameData } from "@/services/detailService"; import detailService, { GameData } from "@/services/detailService";
import { withAuth } from "@/components"; import { withAuth, RefundPopup } from "@/components";
import img from "@/config/images"; import img from "@/config/images";
import { DECLAIMER } from "./config"; import { DECLAIMER } from "./config";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
dayjs.locale("zh-cn"); dayjs.locale("zh-cn");
const refundTextMap = new Map([
[RefundStatus.NONE, "已支付"],
[RefundStatus.PENDING, "退款中"],
[RefundStatus.SUCCESS, "已退款"],
]);
const gameNoticeMap = new Map([
[
"pending",
{ title: "球局暂未开始", content: "球局开始前2小时我们将通过短信通知你" },
],
[
"pendinging",
{
title: "球局即将开始,请按时抵达球局",
content: "球局开始前2小时我们将通过短信通知你",
},
],
["progress", { title: "球局已开始", content: "友谊第一,比赛第二" }],
["finish", { title: "球局已结束", content: "" }],
]);
function genGameNotice(order_status, start_time) {
const startTime = dayjs(start_time);
let key = "";
if (order_status === OrderStatus.FINISHED) {
key = "finish";
}
const leftHour = startTime.diff(dayjs(), "hour");
const start = startTime.isBefore(dayjs());
if (start) {
key = "progress";
} else if (leftHour > 2) {
key = "pending";
} else if (leftHour < 2) {
key = "pendinging";
}
return gameNoticeMap.get(key) || {};
}
function GameInfo(props) { function GameInfo(props) {
const { detail, currentLocation, orderDetail } = props; const { detail, currentLocation, orderDetail } = props;
const { order_status } = orderDetail; const { order_status, refund_status } = orderDetail;
const { latitude, longitude, location, location_name, start_time, end_time } = const { latitude, longitude, location, location_name, start_time, end_time } =
detail || {}; detail || {};
const refundRef = useRef(null);
const openMap = () => { const openMap = () => {
Taro.openLocation({ Taro.openLocation({
latitude, // 纬度(必填) latitude, // 纬度(必填)
@@ -52,14 +101,129 @@ function GameInfo(props) {
const startDate = `${startMonth}${startDay}${theDayOfWeek}`; const startDate = `${startMonth}${startDay}${theDayOfWeek}`;
const gameRange = `${startTime.format("HH:mm")} - ${endTime.format("HH:mm")}`; const gameRange = `${startTime.format("HH:mm")} - ${endTime.format("HH:mm")}`;
const orderStatus = getOrderStatus(orderDetail);
const gameNotice = genGameNotice(order_status, start_time);
function handleViewGame(gameId) {
Taro.navigateTo({
url: `/game_pages/detail/index?id=${gameId}&from=orderList`,
});
}
async function handleDeleteOrder(item) {
const { order_id } = item;
// TODO删除订单,刷新这一页,然后后面的全清除掉
const onCancel = () => {
Dialog.close("detailCancelOrder");
};
const onConfirm = async () => {
try {
const deleteRes = await orderService.deleteOrder({
order_id,
});
if (deleteRes.code !== 0) {
throw new Error(deleteRes.message);
}
Taro.showToast({
title: "删除成功",
icon: "none",
});
delay(2000);
Taro.redirectTo({ url: "/order_pages/orderList/index" });
} catch (e) {
Taro.showToast({
title: e.message,
icon: "error",
});
} finally {
Dialog.close("detailCancelOrder");
}
};
Dialog.open("detailCancelOrder", {
title: "确定删除订单吗?",
content: "删除订单后,您将无法恢复订单。请确认是否继续取消?",
footer: (
<View className={styles.dialogFooter}>
<Button className={styles.cancel} onClick={onCancel}>
</Button>
<Button className={styles.confirm} type="primary" onClick={onConfirm}>
</Button>
</View>
),
onConfirm,
onCancel,
});
}
async function handleCancelOrder(item) {
const { order_no } = item;
const onCancel = () => {
Dialog.close("detailCancelOrder");
};
const onConfirm = async () => {
try {
const cancelRes = await orderService.cancelUnpaidOrder({
order_no,
cancel_reason: "用户主动取消",
});
if (cancelRes.code !== 0) {
throw new Error(cancelRes.message);
}
reloadPage();
Taro.showToast({
title: "取消成功",
icon: "none",
});
} catch (e) {
Taro.showToast({
title: e.message,
icon: "error",
});
} finally {
Dialog.close("detailCancelOrder");
}
};
Dialog.open("detailCancelOrder", {
title: "确定取消订单吗?",
content: "取消订单后,您将无法恢复订单。请确认是否继续取消?",
footer: (
<View className={styles.dialogFooter}>
<Button className={styles.cancel} onClick={onCancel}>
</Button>
<Button className={styles.confirm} type="primary" onClick={onConfirm}>
</Button>
</View>
),
onConfirm,
onCancel,
});
}
function handleQuit(item) {
if (refundRef.current) {
refundRef.current.show(item, (result) => {
if (result) {
reloadPage();
}
});
}
}
return ( return (
<View className={styles.gameInfoContainer}> <View className={styles.gameInfoContainer}>
{Boolean(order_status) && order_status !== OrderStatus.PENDING && ( {["progress", "expired"].includes(orderStatus) && (
<> <>
<View className={styles.paidInfo}> ¥ 90</View> <View className={styles.paidInfo}>
{refundTextMap.get(refund_status)} ¥ 90
</View>
<View className={styles.gameStatus}> <View className={styles.gameStatus}>
<Text className={styles.statusText}></Text> <Text className={styles.statusText}>{gameNotice.title}</Text>
<Text>2</Text> {gameNotice.content && <Text>{gameNotice.content}</Text>}
</View> </View>
</> </>
)} )}
@@ -122,13 +286,36 @@ function GameInfo(props) {
</View> </View>
</View> </View>
{/* Action bar */} {/* Action bar */}
<View className={styles.gameInfoActions}></View> <View className={styles.gameInfoActions}>
{orderDetail.order_id
? generateOrderActions(
orderDetail,
{
handleDeleteOrder,
handleCancelOrder,
handleQuit,
handlePayNow: () => {},
handleViewGame,
},
"detail"
)?.map((obj) => (
<Button
className={classnames(styles.button, styles[obj.className])}
onClick={obj.action}
>
{obj.text}
</Button>
))
: ""}
</View>
<Dialog id="detailCancelOrder" />
<RefundPopup ref={refundRef} />
</View> </View>
); );
} }
function OrderMsg(props) { function OrderMsg(props) {
const { detail, checkOrderInfo } = props; const { detail, orderDetail, checkOrderInfo } = props;
const { const {
start_time, start_time,
end_time, end_time,
@@ -137,7 +324,8 @@ function OrderMsg(props) {
wechat_contact, wechat_contact,
price, price,
} = detail; } = detail;
const { order_info: { registrant_nickname, registrant_phone } = {} } = checkOrderInfo; const { order_no } = orderDetail;
const { order_info: { registrant_phone } = {} } = 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");
@@ -159,18 +347,30 @@ function OrderMsg(props) {
</View> </View>
), ),
}, },
{
title: "报名人昵称",
content: registrant_nickname,
},
{ {
title: "报名人电话", title: "报名人电话",
content: registrant_phone, content: registrant_phone,
}, },
{
title: "组织人微信号",
content: wechat_contact,
},
{
title: "组织人电话",
content: wechat_contact,
},
{ {
title: "费用", title: "费用",
content: `${price} 元 / 人`, content: `${price} 元 / 人`,
}, },
...(order_no
? [
{
title: "订单号",
content: order_no,
},
]
: []),
]; ];
return ( return (
<View className={styles.orderSummary}> <View className={styles.orderSummary}>
@@ -199,8 +399,12 @@ function RefundPolicy(props) {
rule: "退款规则", rule: "退款规则",
}, },
...refund_policy.map((item, index) => { ...refund_policy.map((item, index) => {
const isLast = index === refund_policy.length - 1 const isLast = index === refund_policy.length - 1;
const theTimeObj = dayjs(isLast ? refund_policy.at(-2).deadline_formatted : item.deadline_formatted); const theTimeObj = dayjs(
isLast
? refund_policy.at(-2).deadline_formatted
: item.deadline_formatted
);
const year = theTimeObj.format("YYYY"); const year = theTimeObj.format("YYYY");
const month = theTimeObj.format("M"); const month = theTimeObj.format("M");
const day = theTimeObj.format("D"); const day = theTimeObj.format("D");
@@ -332,6 +536,9 @@ const OrderCheck = () => {
</View> </View>
); );
} }
const { order_status, cancel_type } = orderDetail;
return ( return (
<View className={styles.container}> <View className={styles.container}>
{/* Game Date and Address */} {/* Game Date and Address */}
@@ -341,14 +548,20 @@ const OrderCheck = () => {
currentLocation={location} currentLocation={location}
/> />
{/* Order message */} {/* Order message */}
<OrderMsg detail={detail} checkOrderInfo={checkOrderInfo} /> <OrderMsg
detail={detail}
orderDetail={orderDetail}
checkOrderInfo={checkOrderInfo}
/>
{/* Refund policy */} {/* Refund policy */}
<RefundPolicy checkOrderInfo={checkOrderInfo} /> <RefundPolicy checkOrderInfo={checkOrderInfo} />
{/* Disclaimer */} {/* Disclaimer */}
<Disclaimer /> <Disclaimer />
{(!id || orderDetail.order_status === OrderStatus.PENDING) && ( {(!id ||
(order_status === OrderStatus.PENDING &&
cancel_type === CancelType.NONE)) && (
<Button className={styles.payButton} onClick={handlePay}> <Button className={styles.payButton} onClick={handlePay}>
{orderDetail.order_status === OrderStatus.PENDING ? "继续" : "确认"} {order_status === OrderStatus.PENDING ? "继续" : "确认"}
</Button> </Button>
)} )}

View File

@@ -311,74 +311,6 @@
} }
} }
.refundPolicy {
.moduleTitle {
display: flex;
padding: 15px 0 8px;
justify-content: space-between;
align-items: center;
align-self: stretch;
color: #000;
font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
font-weight: 600;
line-height: 20px;
letter-spacing: -0.23px;
}
.policyList {
border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.06);
background: #fff;
box-shadow: 0 4px 36px 0 rgba(0, 0, 0, 0.06);
.policyItem {
display: flex;
justify-content: space-around;
align-items: center;
color: #000;
text-align: center;
font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC";
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px;
border-top: 1px solid rgba(0, 0, 0, 0.06);
&:nth-child(1) {
color: #000;
text-align: center;
font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
font-weight: 600;
line-height: 20px;
border: none;
}
.time,
.rule {
width: 50%;
padding: 10px 12px;
}
.rule {
border-left: 1px solid rgba(0, 0, 0, 0.06);
}
}
}
}
.dialogFooter { .dialogFooter {
// width: 100%; // width: 100%;
width: calc(100% + 1px); width: calc(100% + 1px);

View File

@@ -1,14 +1,14 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect, useRef } from "react";
import { View, Text, Button, Image, ScrollView } from "@tarojs/components"; import { View, Text, Button, Image, ScrollView } from "@tarojs/components";
import Taro, { useDidShow } from "@tarojs/taro"; import Taro, { useDidShow } from "@tarojs/taro";
import { Avatar, Dialog } from "@nutui/nutui-react-taro"; import { Avatar, Dialog } from "@nutui/nutui-react-taro";
import dayjs from "dayjs"; import dayjs from "dayjs";
import classnames from "classnames"; import classnames from "classnames";
import orderService, { OrderStatus, CancelType } from "@/services/orderService"; import orderService, { OrderStatus, CancelType } from "@/services/orderService";
import { withAuth } from "@/components"; import { withAuth, RefundPopup } from "@/components";
import { payOrder } from "@/utils"; import { payOrder, generateOrderActions } from "@/utils";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
// import orderListArrowRight from "@/static/order/orderListArrowRight.svg";
dayjs.locale("zh-cn"); dayjs.locale("zh-cn");
const PAGESIZE = 20; const PAGESIZE = 20;
@@ -45,11 +45,12 @@ function generateTimeMsg(game_info) {
return ( return (
<> <>
<Text> <Text>
{diffDay <= 2 {diffDay <= 2 && diffDay >= 0
? diffDayMap.get(diffDay) ? diffDayMap.get(diffDay)
: startTime.format("YYYY-MM-DD")} : startTime.format("YYYY-MM-DD")}
</Text> </Text>
<Text>({DayOfWeekMap.get(dayofWeek)})</Text> <Text>({DayOfWeekMap.get(dayofWeek)})</Text>
<Text>{startTime.format('ah')}</Text>
<Text>{gameLength}</Text> <Text>{gameLength}</Text>
</> </>
); );
@@ -58,6 +59,7 @@ function generateTimeMsg(game_info) {
const OrderList = () => { const OrderList = () => {
const [list, setList] = useState<any[][]>([]); const [list, setList] = useState<any[][]>([]);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const refundRef = useRef(null)
const end = list.length * PAGESIZE >= total; const end = list.length * PAGESIZE >= total;
@@ -118,48 +120,6 @@ const OrderList = () => {
}); });
} }
function renderCancelContent(checkOrderInfo) {
const { refund_policy = [] } = checkOrderInfo;
const policyList = [
{
time: "申请退款时间",
rule: "退款规则",
},
...refund_policy.map((item, index) => {
const isLast = index === refund_policy.length - 1;
const theTimeObj = dayjs(
isLast
? refund_policy.at(-2).deadline_formatted
: item.deadline_formatted
);
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} ${isLast ? "后" : "前"}`,
rule: item.refund_rule,
};
}),
];
return (
<View className={styles.refundPolicy}>
<View className={styles.moduleTitle}>
<Text>退</Text>
</View>
{/* 订单信息摘要 */}
<View className={styles.policyList}>
{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>
);
}
async function handleDeleteOrder(item) { async function handleDeleteOrder(item) {
const { id: order_id } = item const { id: order_id } = item
// TODO删除订单,刷新这一页,然后后面的全清除掉 // TODO删除订单,刷新这一页,然后后面的全清除掉
@@ -211,90 +171,65 @@ const OrderList = () => {
} }
async function handleCancelOrder(item) { async function handleCancelOrder(item) {
const { order_no, order_status, game_info, amount } = item; const { order_no } = item;
if (order_status === OrderStatus.PENDING) { const onCancel = () => {
const onCancel = () => { Dialog.close("cancelOrder");
Dialog.close("cancelOrder"); };
}; const onConfirm = async () => {
const onConfirm = async () => { try {
try { const cancelRes = await orderService.cancelUnpaidOrder({
const cancelRes = await orderService.cancelUnpaidOrder({ order_no,
order_no, cancel_reason: "用户主动取消",
cancel_reason: "用户主动取消", });
}); if (cancelRes.code !== 0) {
if (cancelRes.code !== 0) { throw new Error(cancelRes.message);
throw new Error(cancelRes.message);
}
getOrders(item.page, false);
Taro.showToast({
title: "取消成功",
icon: "none",
})
} catch (e) {
Taro.showToast({
title: e.message,
icon: "error",
});
} finally {
Dialog.close("cancelOrder");
} }
}; getOrders(item.page, false);
Dialog.open("cancelOrder", { Taro.showToast({
title: "确定取消订单吗?", title: "取消成功",
content: "取消订单后,您将无法恢复订单。请确认是否继续取消?", icon: "none",
footer: ( })
<View className={styles.dialogFooter}> } catch (e) {
<Button className={styles.cancel} onClick={onCancel}> Taro.showToast({
title: e.message,
</Button> icon: "error",
<Button });
className={styles.confirm} } finally {
type="primary" Dialog.close("cancelOrder");
onClick={onConfirm} }
> };
</Button>
</View>
),
onConfirm,
onCancel,
});
return;
}
const res = await orderService.getCheckOrderInfo(game_info.id);
Dialog.open("cancelOrder", { Dialog.open("cancelOrder", {
title: "确定取消订单吗?", title: "确定取消订单吗?",
content: renderCancelContent(res.data), content: "取消订单后,您将无法恢复订单。请确认是否继续取消?",
onConfirm: async () => { footer: (
try { <View className={styles.dialogFooter}>
const refundRes = await orderService.applicateRefund({ <Button className={styles.cancel} onClick={onCancel}>
order_no,
refund_amount: amount, </Button>
refund_reason: "用户主动退款", <Button
}); className={styles.confirm}
if (refundRes.code !== 0) { type="primary"
throw new Error(refundRes.message); onClick={onConfirm}
} >
getOrders(item.page, false);
Taro.showToast({ </Button>
title: "退出成功", </View>
icon: "none", ),
}) onConfirm,
} catch (e) { onCancel,
Taro.showToast({
title: e.message,
icon: "error",
});
} finally {
Dialog.close("cancelOrder");
}
},
onCancel: () => {
Dialog.close("cancelOrder");
},
}); });
} }
function handleQuit (item) {
if (refundRef.current) {
refundRef.current.show(item, (result) => {
if (result) {
getOrders(item.page)
}
})
}
}
function handleViewOrderDetail(orderId) { function handleViewOrderDetail(orderId) {
Taro.navigateTo({ Taro.navigateTo({
url: `/order_pages/orderDetail/index?id=${orderId}`, url: `/order_pages/orderDetail/index?id=${orderId}`,
@@ -314,19 +249,7 @@ const OrderList = () => {
> >
{/* <View className={styles.bg} /> */} {/* <View className={styles.bg} /> */}
{list.flat().map((item) => { {list.flat().map((item) => {
const unPay = item.order_status === OrderStatus.PENDING; const unPay = item.order_status === OrderStatus.PENDING && item.cancel_type === CancelType.NONE;
const expired =
item.order_status === OrderStatus.FINISHED ||
[CancelType.TIMEOUT, CancelType.USER].includes(item.cancel_type);
// const expiredTime = dayjs(item.expire_time).isSame(dayjs(), "day")
// ? dayjs(item.expire_time).format("HH:mm:ss")
// : dayjs(item.expire_time).format("YYYY-MM-DD HH:mm:ss");
const showCancel =
item.order_status === OrderStatus.PENDING &&
item.cancel_type === CancelType.NONE;
const showQuit =
item.order_status === OrderStatus.PAID &&
item.cancel_type === CancelType.NONE;
const { const {
game_info: { game_info: {
@@ -352,10 +275,10 @@ const OrderList = () => {
<View <View
className={classnames( className={classnames(
styles.payNum, styles.payNum,
styles[unPay && !expired ? "pending" : "paid"] styles[unPay ? "pending" : "paid"]
)} )}
> >
<Text>{unPay && !expired ? "待支付" : "已支付"}</Text> ¥{" "} <Text>{unPay ? "待支付" : "已支付"}</Text> ¥{" "}
<Text>{item.amount}</Text> <Text>{item.amount}</Text>
</View> </View>
</View> </View>
@@ -372,18 +295,48 @@ const OrderList = () => {
<View className={styles.gameOtherInfo}> <View className={styles.gameOtherInfo}>
{participants.length >= 0 ? ( {participants.length >= 0 ? (
<View className={styles.avatarCards}> <View className={styles.avatarCards}>
{/* participants */[{ user: { avatar_url: 'https://img.yzcdn.cn/vant/cat.jpeg', id: 1 } }, { user: { avatar_url: 'https://img.yzcdn.cn/vant/cat.jpeg', id: 2 } }, { user: { avatar_url: 'https://img.yzcdn.cn/vant/cat.jpeg', id: 3 } }].map((participant) => { {
const { /* participants */ [
user: { avatar_url, id }, {
} = participant; user: {
return <Image className={styles.avatar} mode="aspectFill" key={id} src={avatar_url} />; avatar_url: "https://img.yzcdn.cn/vant/cat.jpeg",
})} id: 1,
},
},
{
user: {
avatar_url: "https://img.yzcdn.cn/vant/cat.jpeg",
id: 2,
},
},
{
user: {
avatar_url: "https://img.yzcdn.cn/vant/cat.jpeg",
id: 3,
},
},
].map((participant) => {
const {
user: { avatar_url, id },
} = participant;
return (
<Image
className={styles.avatar}
mode="aspectFill"
key={id}
src={avatar_url}
/>
);
})
}
</View> </View>
) : ( ) : (
"" ""
)} )}
<View className={styles.participantProgress}> <View className={styles.participantProgress}>
<Text className={styles.current}> {current_players}</Text> <Text className={styles.current}>
{current_players}
</Text>
<Text>/</Text> <Text>/</Text>
<Text>{max_players}</Text> <Text>{max_players}</Text>
</View> </View>
@@ -400,45 +353,27 @@ const OrderList = () => {
<View className={styles.orderActions}> <View className={styles.orderActions}>
<View className={styles.extraActions}></View> <View className={styles.extraActions}></View>
<View className={styles.mainActions}> <View className={styles.mainActions}>
{expired && ( {generateOrderActions(
item,
{
handleDeleteOrder,
handleCancelOrder,
handleQuit,
handlePayNow,
handleViewGame,
},
"list"
)?.map((obj) => (
<Button <Button
className={classnames(styles.button, styles.cancelOrder)} className={classnames(
onClick={handleDeleteOrder.bind(null, item)} styles.button,
styles[obj.className]
)}
onClick={obj.action}
> >
{obj.text}
</Button> </Button>
)} ))}
{showCancel && (
<Button
className={classnames(styles.button, styles.cancelOrder)}
onClick={handleCancelOrder.bind(null, item)}
>
</Button>
)}
{showQuit && (
<Button
className={classnames(styles.button, styles.cancelOrder)}
onClick={handleCancelOrder.bind(null, item)}
>
退
</Button>
)}
{unPay && !expired ? (
<Button
className={classnames(styles.button, styles.payNow)}
onClick={handlePayNow.bind(null, item)}
>
</Button>
) : (
<Button
className={classnames(styles.button, styles.gameDetail)}
onClick={handleViewGame.bind(null, item.game_info.id)}
>
</Button>
)}
</View> </View>
</View> </View>
</View> </View>
@@ -447,6 +382,7 @@ const OrderList = () => {
{end && <View className={styles.endTips}></View>} {end && <View className={styles.endTips}></View>}
</ScrollView> </ScrollView>
<Dialog id="cancelOrder" /> <Dialog id="cancelOrder" />
<RefundPopup ref={refundRef} />
</View> </View>
); );
}; };

View File

@@ -22,6 +22,12 @@ export enum CancelType {
TIMEOUT, // 超时 TIMEOUT, // 超时
} }
export enum RefundStatus {
NONE = 0, // 无退款
PENDING, // 退款中
SUCCESS, // 已退款
}
export interface PayMentParams { export interface PayMentParams {
order_id: number; order_id: number;
order_no: string; order_no: string;

View File

@@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.83325 5.83325L14.1666 14.1666" stroke="#A0A0A0" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.83325 14.1666L14.1666 5.83325" stroke="#A0A0A0" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 367 B

View File

@@ -5,3 +5,5 @@ export * from "./processImage";
export * from "./timeUtils"; export * from "./timeUtils";
export * from "./tokenManager"; export * from "./tokenManager";
export * from "./order.pay"; export * from "./order.pay";
export * from './orderActions';
export * from './routeUtil';

68
src/utils/orderActions.ts Normal file
View File

@@ -0,0 +1,68 @@
import { OrderStatus, CancelType } from "@/services/orderService";
export function getOrderStatus(orderData) {
const { order_status, cancel_type } = orderData
const unPay = order_status === OrderStatus.PENDING && cancel_type === CancelType.NONE;
const expired =
order_status === OrderStatus.FINISHED ||
[CancelType.TIMEOUT, CancelType.USER].includes(cancel_type);
return unPay ? 'unpay' : expired ? 'expired' : 'progress'
}
// scene: list、detail
export function generateOrderActions(orderData, actions, scene) {
const { handleDeleteOrder, handleCancelOrder, handleQuit, handlePayNow, handleViewGame } = actions
const deleteOrder = {
text: '删除订单',
className: 'cancelOrder',
action: handleDeleteOrder.bind(null, orderData),
}
const cancelOrder = {
text: '取消订单',
className: 'cancelOrder',
action: handleCancelOrder.bind(null, orderData),
}
const quitGame = {
text: '退出活动',
className: 'cancelOrder',
action: handleQuit.bind(null, orderData),
}
const payNow = {
text: '立即支付',
className: 'payNow',
action: handlePayNow.bind(null, orderData),
}
const gameDetail = {
text: '球局详情',
className: 'gameDetail',
action: handleViewGame.bind(null, orderData.game_info.id),
}
const key = getOrderStatus(orderData)
if (scene === 'list') {
const actionMap = new Map([
['expired', [deleteOrder, gameDetail]],
['progress', [quitGame, gameDetail]],
['unpay', [cancelOrder, payNow]]
])
return actionMap.get(key)
}
if (scene === 'detail') {
const actionMap = new Map([
['expired', [gameDetail, deleteOrder]],
['progress', [gameDetail, quitGame]],
['unpay', [cancelOrder]]
])
return actionMap.get(key)
}
}

23
src/utils/routeUtil.ts Normal file
View File

@@ -0,0 +1,23 @@
import Taro from "@tarojs/taro";
export function getCurrentFullPath(): string {
const pages = Taro.getCurrentPages();
const currentPage = pages.at(-1);
if (currentPage) {
console.log(currentPage, "currentPage get");
const route = currentPage.route;
const options = currentPage.options || {};
const query = Object.keys(options)
.map((key) => `${key}=${options[key]}`)
.join("&");
return query ? `/${route}?${query}` : `/${route}`;
}
return "";
}
export function reloadPage() {
Taro.reLaunch({ url: getCurrentFullPath() })
}