diff --git a/src/mod_user/orderDetail/index.module.scss b/src/mod_user/orderDetail/index.module.scss index 96eaaf7..3fe44c1 100644 --- a/src/mod_user/orderDetail/index.module.scss +++ b/src/mod_user/orderDetail/index.module.scss @@ -18,10 +18,53 @@ } .gameInfoContainer { + background-color: #fff; + border: 1px solid rgba(0, 0, 0, 0.06); + border-radius: 12px; + overflow: hidden; + + .paidInfo { + border-bottom: 0.5px solid rgba(0, 0, 0, 0.06); + padding: 8px 12px; + color: #000; + font-feature-settings: + "liga" off, + "clig" off; + font-family: "PingFang SC"; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 28px; + letter-spacing: -0.23px; + } + + .gameStatus { + padding: 12px 12px 8px; + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: flex-start; + + color: rgba(60, 60, 67, 0.6); + font-feature-settings: + "liga" off, + "clig" off; + font-family: "PingFang SC"; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 20px; + letter-spacing: -0.23px; + + .statusText { + color: #000; + font-size: 18px; + font-weight: 600; + line-height: 32px; + } + } + .gameInfo { - border-top-left-radius: 12px; - border-top-right-radius: 12px; - background-color: #fff; padding: 12px; box-sizing: border-box; display: flex; @@ -186,11 +229,7 @@ .gameInfoActions { min-height: 12px; - /* height: 12px; */ - background-color: #fff; border-top: 0.5px solid rgba(0, 0, 0, 0.06); - border-bottom-left-radius: 12px; - border-bottom-right-radius: 12px; } } diff --git a/src/mod_user/orderDetail/index.tsx b/src/mod_user/orderDetail/index.tsx index a327085..ec6659b 100644 --- a/src/mod_user/orderDetail/index.tsx +++ b/src/mod_user/orderDetail/index.tsx @@ -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"; @@ -17,7 +21,8 @@ import styles from "./index.module.scss"; dayjs.locale("zh-cn"); function GameInfo(props) { - const { detail, currentLocation } = props; + const { detail, currentLocation, orderDetail } = props; + const { order_status } = orderDetail; const { latitude, longitude, location, location_name, start_time, end_time } = detail || {}; @@ -49,6 +54,15 @@ function GameInfo(props) { return ( + {order_status !== OrderStatus.PENDING && ( + <> + 已支付 ¥ 90 + + 球局暂未开始 + 球局开始前2小时,我们将通过短信通知你 + + + )} {/* Date and Weather */} @@ -150,7 +164,7 @@ function OrderMsg(props) { }, { title: "费用", - content: `${price} 元`, + content: `${price} 元 / 人`, }, ]; return ( @@ -259,71 +273,43 @@ const OrderCheck = () => { 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, }); - 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; - } - } + 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: "支付调用失败", + title: error.message, 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) { @@ -344,23 +330,22 @@ const OrderCheck = () => { return ( {/* Game Date and Address */} - + {/* Order message */} {/* Refund policy */} {/* Disclaimer */} - {!id || - (orderDetail.order_status === OrderStatus.PENDING && ( - - ))} + {(!id || orderDetail.order_status === OrderStatus.PENDING) && ( + + )} ); }; diff --git a/src/mod_user/orderList/index.module.scss b/src/mod_user/orderList/index.module.scss index a41d76c..e42fc43 100644 --- a/src/mod_user/orderList/index.module.scss +++ b/src/mod_user/orderList/index.module.scss @@ -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 { + } + } } } -} \ No newline at end of file +} + +.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); + } + } + } +} diff --git a/src/mod_user/orderList/index.tsx b/src/mod_user/orderList/index.tsx index b72ccc3..5a7049d 100644 --- a/src/mod_user/orderList/index.tsx +++ b/src/mod_user/orderList/index.tsx @@ -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 ( + + + 退款政策 + + {/* 订单信息摘要 */} + + {policyList.map((item, index) => ( + + {item.time} + {item.rule} + + ))} + + + ); + } + + 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 ( - { - list.map(item => { - return ( - - - {item?.game_info?.title} - - - - + {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 ( + + + + + + Light + + {expired ? ( + "" + ) : ( + + + {unPay + ? `请在 ${expiredTime} 前支付` + : dayjs(item.pay_time).format("YYYY-MM-DD HH:mm:ss")} + + + {unPay ? "待支付" : "已支付"} ¥ {item.amount} + + + )} - ) - }) - } + + {item?.game_info?.title} + + + + + {showCancel && ( + + )} + {unPay && !expired && ( + + )} + + + + ); + })} + - ) -} + ); +}; -export default withAuth(OrderList) \ No newline at end of file +export default withAuth(OrderList); diff --git a/src/pages/detail/index.tsx b/src/pages/detail/index.tsx index 4122817..789085e 100644 --- a/src/pages/detail/index.tsx +++ b/src/pages/detail/index.tsx @@ -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 */} @@ -1014,7 +1015,7 @@ function Index() { }; async function fetchUserInfoById(user_id) { - const userDetailInfo = await LoginService.getUserInfoById(Number(user_id)); + const userDetailInfo = await LoginService.getUserInfoById(user_id); if (userDetailInfo.code === 0) { // console.log(userDetailInfo.data); setUserInfo(userDetailInfo.data); @@ -1072,7 +1073,7 @@ function Index() { {/* content */} {/* avatar and tags */} - + {/* title */} {detail.title} diff --git a/src/services/orderService.ts b/src/services/orderService.ts index 7272cd0..8b2646f 100644 --- a/src/services/orderService.ts +++ b/src/services/orderService.ts @@ -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> { + 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> { + return httpService.post( + "/payment/apply_refund", + { order_no, refund_reason, refund_amount }, + { + showLoading: true, + }, + ); + } } // 导出认证服务实例 diff --git a/src/static/order/orderListArrowRight.svg b/src/static/order/orderListArrowRight.svg new file mode 100644 index 0000000..01f71b4 --- /dev/null +++ b/src/static/order/orderListArrowRight.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/utils/index.ts b/src/utils/index.ts index 044bd88..af30b5d 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -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)) -} \ No newline at end of file +export * from "./getNavbarHeight"; +export * from "./genderUtils"; +export * from "./locationUtils"; +export * from "./processImage"; +export * from "./timeUtils"; +export * from "./tokenManager"; +export * from "./order.pay"; diff --git a/src/utils/order.pay.ts b/src/utils/order.pay.ts new file mode 100644 index 0000000..060fc7b --- /dev/null +++ b/src/utils/order.pay.ts @@ -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("支付失败")), + }); + }); +}