Files
mini-programs/src/order_pages/orderDetail/index.tsx
2025-09-16 14:34:21 +08:00

573 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);