573 lines
16 KiB
TypeScript
573 lines
16 KiB
TypeScript
import React, { useState, useRef } from "react";
|
||
import { View, Text, Button, Image } from "@tarojs/components";
|
||
import { Dialog } from "@nutui/nutui-react-taro";
|
||
import Taro, { useDidShow, useRouter } from "@tarojs/taro";
|
||
import dayjs from "dayjs";
|
||
import classnames from "classnames";
|
||
import orderService, {
|
||
CancelType,
|
||
GameOrderRes,
|
||
OrderStatus,
|
||
RefundStatus,
|
||
} from "@/services/orderService";
|
||
import {
|
||
payOrder,
|
||
delay,
|
||
calculateDistance,
|
||
getCurrentLocation,
|
||
getOrderStatus,
|
||
generateOrderActions,
|
||
reloadPage,
|
||
} from "@/utils";
|
||
import detailService, { GameData } from "@/services/detailService";
|
||
import { withAuth, RefundPopup } from "@/components";
|
||
import img from "@/config/images";
|
||
import { DECLAIMER } from "./config";
|
||
import styles from "./index.module.scss";
|
||
|
||
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) {
|
||
const { detail, currentLocation, orderDetail } = props;
|
||
const { order_status, refund_status } = orderDetail;
|
||
const { latitude, longitude, location, location_name, start_time, end_time } =
|
||
detail || {};
|
||
|
||
const refundRef = useRef(null);
|
||
|
||
const openMap = () => {
|
||
Taro.openLocation({
|
||
latitude, // 纬度(必填)
|
||
longitude, // 经度(必填)
|
||
name: location_name, // 位置名(可选)
|
||
address: location, // 地址详情(可选)
|
||
scale: 15, // 地图缩放级别(1-28)
|
||
});
|
||
};
|
||
|
||
const [c_latitude, c_longitude] = currentLocation;
|
||
const distance =
|
||
c_latitude + c_longitude === 0
|
||
? 0
|
||
: calculateDistance(c_latitude, c_longitude, latitude, longitude) / 1000;
|
||
|
||
const startTime = dayjs(start_time);
|
||
const endTime = dayjs(end_time);
|
||
const game_length = endTime.diff(startTime, "minutes") / 60;
|
||
|
||
const startMonth = startTime.format("M");
|
||
const startDay = startTime.format("D");
|
||
const theDayOfWeek = startTime.format("dddd");
|
||
const startDate = `${startMonth}月${startDay}日 ${theDayOfWeek}`;
|
||
const gameRange = `${startTime.format("HH:mm")} - ${endTime.format("HH:mm")}`;
|
||
|
||
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 (
|
||
<View className={styles.gameInfoContainer}>
|
||
{["progress", "expired"].includes(orderStatus) && (
|
||
<>
|
||
<View className={styles.paidInfo}>
|
||
{refundTextMap.get(refund_status)} ¥ 90
|
||
</View>
|
||
<View className={styles.gameStatus}>
|
||
<Text className={styles.statusText}>{gameNotice.title}</Text>
|
||
{gameNotice.content && <Text>{gameNotice.content}</Text>}
|
||
</View>
|
||
</>
|
||
)}
|
||
<View className={styles.gameInfo}>
|
||
{/* Date and Weather */}
|
||
<View className={styles.gameInfoDateWeather}>
|
||
{/* Calendar and Date time */}
|
||
<View className={styles.gameInfoDateWeatherCalendarDate}>
|
||
{/* Calendar */}
|
||
<View className={styles.gameInfoDateWeatherCalendarDateCalendar}>
|
||
<View className={styles.month}>{startMonth}月</View>
|
||
<View className={styles.day}>{startDay}</View>
|
||
</View>
|
||
{/* Date time */}
|
||
<View className={styles.gameInfoDateWeatherCalendarDateDate}>
|
||
<View className={styles.date}>{startDate}</View>
|
||
<View className={styles.venueTime}>
|
||
{gameRange} ({game_length}小时)
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
{/* Place */}
|
||
<View className={styles.gameInfoPlace}>
|
||
{/* venue location message */}
|
||
<View className={styles.locationMessage}>
|
||
{/* location icon */}
|
||
<View className={styles.locationMessageIcon}>
|
||
<Image
|
||
className={styles.locationMessageIconImage}
|
||
src="https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/43aab7e9-061e-4e3b-88c6-61c19b660b22.png"
|
||
/>
|
||
</View>
|
||
{/* location message */}
|
||
<View className={styles.locationMessageText}>
|
||
{/* venue name and distance */}
|
||
<View
|
||
className={styles.locationMessageTextNameDistance}
|
||
onClick={openMap}
|
||
>
|
||
<Text>{location_name || "-"}</Text>
|
||
{distance ? (
|
||
<>
|
||
<Text>·</Text>
|
||
<Text>{distance.toFixed(1)}km</Text>
|
||
</>
|
||
) : null}
|
||
|
||
<Image
|
||
className={styles.locationMessageTextNameDistanceArrow}
|
||
src={img.ICON_DETAIL_ARROW_RIGHT}
|
||
/>
|
||
</View>
|
||
{/* venue address */}
|
||
<View className={styles.locationMessageTextAddress}>
|
||
<Text>{location || "-"}</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
{/* Action bar */}
|
||
<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>
|
||
);
|
||
}
|
||
|
||
function OrderMsg(props) {
|
||
const { detail, orderDetail, checkOrderInfo } = props;
|
||
const {
|
||
start_time,
|
||
end_time,
|
||
location,
|
||
location_name,
|
||
wechat_contact,
|
||
price,
|
||
} = detail;
|
||
const { order_no } = orderDetail;
|
||
const { order_info: { registrant_phone } = {} } = checkOrderInfo;
|
||
const startTime = dayjs(start_time);
|
||
const endTime = dayjs(end_time);
|
||
const startYear = startTime.format("YYYY");
|
||
const startMonth = startTime.format("M");
|
||
const startDay = startTime.format("D");
|
||
const startDate = `${startYear}年${startMonth}月${startDay}日`;
|
||
const gameRange = `${startTime.format("HH:mm")} - ${endTime.format("HH:mm")}`;
|
||
const summary = [
|
||
{
|
||
title: "时间",
|
||
content: `${startDate} ${gameRange}`,
|
||
},
|
||
{
|
||
title: "地址",
|
||
content: (
|
||
<View className={styles.location}>
|
||
<Text>{location}</Text>
|
||
<Text>{location_name}</Text>
|
||
</View>
|
||
),
|
||
},
|
||
{
|
||
title: "报名人电话",
|
||
content: registrant_phone,
|
||
},
|
||
{
|
||
title: "组织人微信号",
|
||
content: wechat_contact,
|
||
},
|
||
{
|
||
title: "组织人电话",
|
||
content: wechat_contact,
|
||
},
|
||
{
|
||
title: "费用",
|
||
content: `${price} 元 / 人`,
|
||
},
|
||
...(order_no
|
||
? [
|
||
{
|
||
title: "订单号",
|
||
content: order_no,
|
||
},
|
||
]
|
||
: []),
|
||
];
|
||
return (
|
||
<View className={styles.orderSummary}>
|
||
<View className={styles.moduleTitle}>
|
||
<Text>确认订单信息</Text>
|
||
</View>
|
||
{/* 订单信息摘要 */}
|
||
<View className={styles.summaryList}>
|
||
{summary.map((item, index) => (
|
||
<View key={index} className={styles.summaryItem}>
|
||
<Text className={styles.title}>{item.title}</Text>
|
||
<View className={styles.content}>{item.content}</View>
|
||
</View>
|
||
))}
|
||
</View>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
function RefundPolicy(props) {
|
||
const { checkOrderInfo } = props;
|
||
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>
|
||
);
|
||
}
|
||
|
||
function Disclaimer() {
|
||
return (
|
||
<View className={styles.declaimer}>
|
||
<Text className={styles.title}>免责声明</Text>
|
||
<Text className={styles.content}>{DECLAIMER}</Text>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
const OrderCheck = () => {
|
||
const { params } = useRouter();
|
||
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 [checkOrderInfo, setCheckOrderInfo] = useState<GameOrderRes | {}>({});
|
||
const [orderDetail, setOrderDetail] = useState({});
|
||
|
||
useDidShow(async () => {
|
||
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]);
|
||
}
|
||
|
||
async function getPaymentParams() {
|
||
const unPaidRes = await orderService.getUnpaidOrder(detail.id);
|
||
if (unPaidRes.code === 0 && unPaidRes.data.has_unpaid_order) {
|
||
return unPaidRes.data.payment_params;
|
||
}
|
||
const createOrderRes = await orderService.createOrder(detail.id);
|
||
if (createOrderRes.code === 0) {
|
||
return createOrderRes.data.payment_params;
|
||
}
|
||
throw new Error("支付调用失败");
|
||
}
|
||
|
||
//TODO: get order msg from id
|
||
const handlePay = async () => {
|
||
Taro.showLoading({
|
||
title: "支付中...",
|
||
mask: true,
|
||
});
|
||
|
||
try {
|
||
const payment_params = await getPaymentParams();
|
||
await payOrder(payment_params);
|
||
Taro.hideLoading();
|
||
Taro.showToast({
|
||
title: "支付成功",
|
||
icon: "success",
|
||
});
|
||
await delay(1000);
|
||
Taro.navigateBack({
|
||
delta: 1,
|
||
});
|
||
} catch (error) {
|
||
Taro.hideLoading();
|
||
Taro.showToast({
|
||
title: error.message,
|
||
icon: "none",
|
||
});
|
||
}
|
||
};
|
||
if (!id && !gameId) {
|
||
return (
|
||
<View className={styles.errorTip}>
|
||
<Text>异常订单</Text>
|
||
<Button
|
||
type="warn"
|
||
onClick={() => {
|
||
Taro.redirectTo({ url: "/game_pages/list/index" });
|
||
}}
|
||
>
|
||
回到首页
|
||
</Button>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
const { order_status, cancel_type } = orderDetail;
|
||
|
||
return (
|
||
<View className={styles.container}>
|
||
{/* Game Date and Address */}
|
||
<GameInfo
|
||
detail={detail}
|
||
orderDetail={orderDetail}
|
||
currentLocation={location}
|
||
/>
|
||
{/* Order message */}
|
||
<OrderMsg
|
||
detail={detail}
|
||
orderDetail={orderDetail}
|
||
checkOrderInfo={checkOrderInfo}
|
||
/>
|
||
{/* Refund policy */}
|
||
<RefundPolicy checkOrderInfo={checkOrderInfo} />
|
||
{/* Disclaimer */}
|
||
<Disclaimer />
|
||
{(!id ||
|
||
(order_status === OrderStatus.PENDING &&
|
||
cancel_type === CancelType.NONE)) && (
|
||
<Button className={styles.payButton} onClick={handlePay}>
|
||
{order_status === OrderStatus.PENDING ? "继续" : "确认"}
|
||
支付
|
||
</Button>
|
||
)}
|
||
</View>
|
||
);
|
||
};
|
||
|
||
export default withAuth(OrderCheck);
|