feat: 订单列表基本功能

This commit is contained in:
2025-09-11 22:51:11 +08:00
parent c86e20c316
commit 57613ec8fe
8 changed files with 504 additions and 79 deletions

View File

@@ -2,14 +2,18 @@ import React, { useState } from "react";
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,
OrderStatus,
} from "@/services/orderService";
import {
payOrder,
delay,
calculateDistance,
getCurrentLocation,
} from "@/utils";
import detailService, { GameData } from "@/services/detailService";
import { withAuth } from "@/components";
import { calculateDistance, getCurrentLocation } from "@/utils";
import img from "@/config/images";
import { DECLAIMER } from "./config";
import styles from "./index.module.scss";
@@ -290,21 +294,7 @@ const OrderCheck = () => {
try {
const payment_params = await getPaymentParams();
const {
timeStamp,
nonceStr,
package: package_,
signType,
paySign,
} = payment_params;
await Taro.requestPayment({
timeStamp,
nonceStr,
package: package_,
signType,
paySign,
success: async () => {
await payOrder(payment_params);
Taro.hideLoading();
Taro.showToast({
title: "支付成功",
@@ -314,15 +304,6 @@ const OrderCheck = () => {
Taro.navigateBack({
delta: 1,
});
},
fail: () => {
Taro.hideLoading();
Taro.showToast({
title: "支付失败",
icon: "none",
});
},
});
} catch (error) {
Taro.hideLoading();
Taro.showToast({

View File

@@ -1,4 +1,4 @@
@use '~@/scss/images.scss' as img;
@use "~@/scss/images.scss" as img;
.container {
padding: 12px;
@@ -18,6 +18,84 @@
height: 18px;
padding: 15px 15px 12px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
display: flex;
align-items: center;
justify-content: space-between;
.userInfo {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 6px;
.avatar {
width: 16px;
height: 16px;
}
.nickName {
display: contents;
.nickNameText {
color: #000;
font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC";
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 18px;
}
.arrowRight {
width: 8px;
height: 8px;
}
}
}
.paidInfo {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
.payTime {
font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC";
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px;
&.paid {
color: rgba(60, 60, 67, 0.6);
}
&.pending {
color: #000;
}
}
.payNum {
font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC";
font-size: 12px;
font-style: normal;
font-weight: 600;
line-height: 18px;
&.paid {
color: #000;
}
&.pending {
color: #ff3b30;
}
}
}
}
.gameInfo {
@@ -28,6 +106,112 @@
height: 28px;
padding: 12px 12px 15px;
border-top: 1px solid rgba(0, 0, 0, 0.06);
display: flex;
align-items: center;
justify-content: space-between;
.extraActions {
}
.mainActions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
& > .button {
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;
&:last-child {
background: #000;
color: #fff;
}
}
.cancelOrder {
}
.payNow {
}
}
}
}
}
.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);
}
}
}
}

View File

@@ -1,44 +1,241 @@
import React, { useState } from 'react'
import { View, Text, Button } from '@tarojs/components'
import Taro, { useDidShow } from '@tarojs/taro'
import { delay } from '@/utils'
import orderService from '@/services/orderService'
import { withAuth } from '@/components'
import styles from './index.module.scss'
import React, { useState } from "react";
import { View, Text, Button, Image } from "@tarojs/components";
import Taro, { useDidShow } from "@tarojs/taro";
import { Avatar, Dialog } from "@nutui/nutui-react-taro";
import dayjs from "dayjs";
import classnames from "classnames";
import orderService, { OrderStatus, CancelType } from "@/services/orderService";
import { withAuth } from "@/components";
import { payOrder } from "@/utils";
import styles from "./index.module.scss";
import orderListArrowRight from "@/static/order/orderListArrowRight.svg";
const OrderList = () => {
const [list, setList] = useState([])
const [list, setList] = useState([]);
useDidShow(async () => {
const res = await orderService.getOrderList()
console.log(res)
useDidShow(() => {
getOrders();
});
async function getOrders() {
const res = await orderService.getOrderList();
console.log(res);
if (res.code === 0) {
setList(res.data.rows)
setList(res.data.rows);
}
}
})
console.log(list, 'list')
async function handlePayNow(gameId) {
try {
const unPaidRes = await orderService.getUnpaidOrder(gameId);
if (unPaidRes.code === 0 && unPaidRes.data.has_unpaid_order) {
await payOrder(unPaidRes.data.payment_params);
getOrders();
} else {
throw new Error("支付调用失败");
}
} catch (e) {
console.log(e, 1111);
Taro.showToast({
title: e.message,
icon: "none",
});
}
}
function renderCancelContent(checkOrderInfo) {
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}>
<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 handleCancelOrder(item) {
const { order_no, order_status, game_info, amount } = item;
if (order_status === OrderStatus.PENDING) {
Dialog.open("cancelOrder", {
title: "确定取消订单吗?",
content: "取消订单后,您将无法恢复订单。请确认是否继续取消?",
onConfirm: async () => {
try {
const cancelRes = await orderService.cancelUnpaidOrder({
order_no,
cancel_reason: "用户主动取消",
});
if (cancelRes.code !== 0) {
throw new Error(cancelRes.message);
}
getOrders();
} catch (e) {
Taro.showToast({
title: e.message,
icon: "error",
});
} finally {
Dialog.close("cancelOrder");
}
},
onCancel: () => {
Dialog.close("cancelOrder");
},
});
return;
}
const res = await orderService.getCheckOrderInfo(game_info.id);
Dialog.open("cancelOrder", {
title: "确定取消订单吗?",
content: renderCancelContent(res.data),
onConfirm: async () => {
try {
const refundRes = await orderService.applicateRefund({
order_no,
refund_amount: amount,
refund_reason: "用户主动退款",
});
if (refundRes.code !== 0) {
throw new Error(refundRes.message);
}
getOrders();
} catch (e) {
Taro.showToast({
title: e.message,
icon: "error",
});
} finally {
Dialog.close("cancelOrder");
}
},
onCancel: () => {
Dialog.close("cancelOrder");
},
});
}
function handleViewOrderDetail(orderId) {
Taro.navigateTo({
url: `/mod_user/orderDetail/index?id=${orderId}`,
});
}
return (
<View className={styles.container}>
{
list.map(item => {
{list.map((item) => {
const unPay = item.order_status === OrderStatus.PENDING;
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.FINISHED &&
item.cancel_type === CancelType.NONE;
return (
<View key={item.id} className={styles.orderItem}>
<View className={styles.orderTitle}></View>
<View className={styles.gameInfo}>{item?.game_info?.title}</View>
<View className={styles.orderTitle}>
<View className={styles.userInfo}>
<Avatar
className={styles.avatar}
src="https://img.yzcdn.cn/vant/cat.jpeg"
/>
<View className={styles.nickName}>
<Text className={styles.nickNameText}>Light</Text>
<Image
className={styles.arrowRight}
src={orderListArrowRight}
/>
</View>
</View>
{expired ? (
""
) : (
<View className={styles.paidInfo}>
<Text
className={classnames(
styles.payTime,
styles[unPay ? "pending" : "paid"],
)}
>
{unPay
? `请在 ${expiredTime} 前支付`
: dayjs(item.pay_time).format("YYYY-MM-DD HH:mm:ss")}
</Text>
<Text
className={classnames(
styles.payNum,
styles[unPay ? "pending" : "paid"],
)}
>
{unPay ? "待支付" : "已支付"} ¥ {item.amount}
</Text>
</View>
)}
</View>
<View
className={styles.gameInfo}
onClick={handleViewOrderDetail.bind(null, item.id)}
>
{item?.game_info?.title}
</View>
<View className={styles.orderActions}>
<View></View>
<View>
<Button></Button>
<View className={styles.extraActions}></View>
<View className={styles.mainActions}>
{showCancel && (
<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.game_info.id)}
>
</Button>
)}
</View>
</View>
</View>
)
})
}
);
})}
<Dialog id="cancelOrder" />
</View>
)
}
);
};
export default withAuth(OrderList)
export default withAuth(OrderList);

View File

@@ -38,7 +38,8 @@ function insertDotInTags(tags: string[]) {
}
function GameTags(props) {
const { detail } = props;
const { userInfo } = props;
const { avatar_url } = userInfo;
const tags = [
{
name: "🕙 急招",
@@ -63,7 +64,7 @@ function GameTags(props) {
{/* network image mock */}
<Image
className="detail-page-content-avatar-tags-avatar-image"
src="https://img.yzcdn.cn/vant/cat.jpeg"
src={avatar_url}
/>
</View>
<View className="detail-page-content-avatar-tags-tags">
@@ -1072,7 +1073,7 @@ function Index() {
{/* content */}
<View className="detail-page-content">
{/* avatar and tags */}
<GameTags detail={detail} />
<GameTags detail={detail} userInfo={userInfo} />
{/* title */}
<View className="detail-page-content-title">
<Text className="detail-page-content-title-text">{detail.title}</Text>

View File

@@ -16,6 +16,12 @@ export enum OrderStatus {
FINISHED,
}
export enum CancelType {
NONE = 0, // 未取消
USER, // 用户主动取消
TIMEOUT, // 超时
}
export interface PayMentParams {
order_id: number;
order_no: string;
@@ -119,6 +125,42 @@ class OrderService {
},
);
}
// 取消未支付订单
async cancelUnpaidOrder({
order_no,
cancel_reason,
}: {
order_no: number;
cancel_reason: string;
}): Promise<ApiResponse<any>> {
return httpService.post(
"/payment/cancel_order",
{ order_no, cancel_reason },
{
showLoading: true,
},
);
}
// 申请退款
async applicateRefund({
order_no,
refund_reason,
refund_amount,
}: {
order_no: number;
refund_reason: string;
refund_amount: number;
}): Promise<ApiResponse<any>> {
return httpService.post(
"/payment/apply_refund",
{ order_no, refund_reason, refund_amount },
{
showLoading: true,
},
);
}
}
// 导出认证服务实例

View File

@@ -0,0 +1,3 @@
<svg width="5" height="8" viewBox="0 0 5 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.75 1L3.75 4L0.75 7" stroke="#3C3C43" stroke-opacity="0.6" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 241 B

View File

@@ -1,10 +1,7 @@
export * from './getNavbarHeight'
export * from './genderUtils'
export * from './locationUtils'
export * from './processImage'
export * from './timeUtils'
export * from './tokenManager'
export function delay(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms))
}
export * from "./getNavbarHeight";
export * from "./genderUtils";
export * from "./locationUtils";
export * from "./processImage";
export * from "./timeUtils";
export * from "./tokenManager";
export * from "./order.pay";

20
src/utils/order.pay.ts Normal file
View File

@@ -0,0 +1,20 @@
import Taro from "@tarojs/taro";
export function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export async function payOrder(params) {
const { timeStamp, nonceStr, package: _package, signType, paySign } = params;
return new Promise((resolve, reject) => {
Taro.requestPayment({
timeStamp,
nonceStr,
package: _package,
signType,
paySign,
success: resolve,
fail: reject.bind(null, new Error("支付失败")),
});
});
}