From bb2b681ae07a2b04c042fef2f3eb2b065ac400f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Wed, 19 Nov 2025 10:04:32 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E5=85=85=E5=80=BC=E8=AE=A2?= =?UTF-8?q?=E5=8D=95=E7=AE=A1=E7=90=86=EF=BC=9A=E6=9B=B4=E6=96=B0OrderList?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E4=BB=A5=E6=94=AF=E6=8C=81=E7=81=B5=E6=B4=BB?= =?UTF-8?q?=E7=9A=84=E6=95=B0=E6=8D=AE=E7=B1=BB=E5=9E=8B=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=E8=AE=A2=E5=8D=95=E7=8A=B6=E6=80=81=E5=A4=84=E7=90=86?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E6=94=B9=E8=BF=9B=E8=AE=A2=E5=8D=95=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E7=9A=84UI=E4=BA=A4=E4=BA=92=E3=80=82=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E4=BB=98=E6=AC=BE=E8=BF=9E=E7=BB=AD=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E9=80=9A=E8=BF=87=E6=9B=B4=E5=A5=BD=E7=9A=84?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E8=A1=A8=E7=A4=BA=E6=9D=A5=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E8=AE=A2=E5=8D=95=E8=AF=A6=E7=BB=86=E4=BF=A1=E6=81=AF=E5=91=88?= =?UTF-8?q?=E7=8E=B0=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/mobile/mine/recharge/index/api.ts | 24 +- .../mine/recharge/index/index.module.scss | 12 + .../mobile/mine/recharge/index/index.tsx | 202 ++++--- .../pages/mobile/mine/recharge/order/api.ts | 202 +------ .../pages/mobile/mine/recharge/order/data.ts | 54 +- .../recharge/order/detail/index.module.scss | 154 ++++++ .../mine/recharge/order/detail/index.tsx | 328 +++++++++++ .../mobile/mine/recharge/order/index.tsx | 508 +++++++++++------- .../plan/new/steps/BasicSettings.tsx | 2 +- Cunkebao/src/router/index.tsx | 2 +- Cunkebao/src/router/module/mine.tsx | 6 + 11 files changed, 1042 insertions(+), 452 deletions(-) create mode 100644 Cunkebao/src/pages/mobile/mine/recharge/order/detail/index.module.scss create mode 100644 Cunkebao/src/pages/mobile/mine/recharge/order/detail/index.tsx diff --git a/Cunkebao/src/pages/mobile/mine/recharge/index/api.ts b/Cunkebao/src/pages/mobile/mine/recharge/index/api.ts index 6a870667..de13a1e0 100644 --- a/Cunkebao/src/pages/mobile/mine/recharge/index/api.ts +++ b/Cunkebao/src/pages/mobile/mine/recharge/index/api.ts @@ -75,7 +75,7 @@ export interface OrderListParams { [property: string]: any; } -interface OrderList { +export interface OrderList { id?: number; mchId?: number; companyId?: number; @@ -84,22 +84,24 @@ interface OrderList { status?: number; goodsId?: number; goodsName?: string; - goodsSpecs?: { - id: number; - name: string; - price: number; - tokens: number; - }; + goodsSpecs?: + | { + id: number; + name: string; + price: number; + tokens: number; + } + | string; money?: number; orderNo?: string; ip?: string; nonceStr?: string; - createTime?: string; + createTime?: string | number; payType?: number; - payTime?: string; + payTime?: string | number; payInfo?: any; - deleteTime?: string; - tokens?: string; + deleteTime?: string | number; + tokens?: string | number; statusText?: string; orderTypeText?: string; payTypeText?: string; diff --git a/Cunkebao/src/pages/mobile/mine/recharge/index/index.module.scss b/Cunkebao/src/pages/mobile/mine/recharge/index/index.module.scss index be4e8f82..99d1355e 100644 --- a/Cunkebao/src/pages/mobile/mine/recharge/index/index.module.scss +++ b/Cunkebao/src/pages/mobile/mine/recharge/index/index.module.scss @@ -258,6 +258,18 @@ border-radius: 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); border: 1px solid #f0f0f0; + cursor: pointer; + transition: + transform 0.2s ease, + box-shadow 0.2s ease; +} + +.recordItem:active { + transform: scale(0.98); +} + +.recordItem:hover { + box-shadow: 0 6px 18px rgba(22, 119, 255, 0.08); } .recordHeader { diff --git a/Cunkebao/src/pages/mobile/mine/recharge/index/index.tsx b/Cunkebao/src/pages/mobile/mine/recharge/index/index.tsx index d713b2f3..3f52a632 100644 --- a/Cunkebao/src/pages/mobile/mine/recharge/index/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/recharge/index/index.tsx @@ -12,17 +12,74 @@ import { import NavCommon from "@/components/NavCommon"; import Layout from "@/components/Layout/Layout"; import { getStatistics, getOrderList } from "./api"; -import type { Statistics } from "./api"; +import type { Statistics, OrderList } from "./api"; import { Pagination } from "antd"; -type OrderRecordView = { - id: number; - type: string; - status: string; - amount: number; // 元 - power: number; - description: string; - createTime: string; +type TagColor = NonNullable["color"]>; + +type GoodsSpecs = + | { + id: number; + name: string; + price: number; + tokens: number; + } + | undefined; + +const parseGoodsSpecs = (value: OrderList["goodsSpecs"]): GoodsSpecs => { + if (!value) return undefined; + if (typeof value === "string") { + try { + return JSON.parse(value); + } catch (error) { + console.warn("解析 goodsSpecs 失败:", error, value); + return undefined; + } + } + return value; +}; + +const formatTimestamp = (value?: number | string | null) => { + if (value === undefined || value === null) return ""; + if (typeof value === "string" && value.trim() === "") return ""; + + const numericValue = + typeof value === "number" ? value : Number.parseFloat(value); + + if (Number.isNaN(numericValue)) { + return String(value); + } + + const timestamp = + numericValue > 1e12 + ? numericValue + : numericValue > 1e10 + ? numericValue + : numericValue * 1000; + + const date = new Date(timestamp); + if (Number.isNaN(date.getTime())) { + return String(value); + } + return date.toLocaleString("zh-CN", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); +}; + +const centsToYuan = (value?: number | string | null) => { + if (value === undefined || value === null) return 0; + if (typeof value === "string" && value.trim() === "") return 0; + const num = Number(value); + if (!Number.isFinite(num)) return 0; + if (Number.isInteger(num)) { + return num / 100; + } + return num; }; const PowerManagement: React.FC = () => { @@ -30,7 +87,7 @@ const PowerManagement: React.FC = () => { const [activeTab, setActiveTab] = useState("overview"); const [loading, setLoading] = useState(false); const [stats, setStats] = useState(null); - const [records, setRecords] = useState([]); + const [records, setRecords] = useState([]); const [filterType, setFilterType] = useState("all"); const [filterStatus, setFilterStatus] = useState("all"); const [filterTypeVisible, setFilterTypeVisible] = useState(false); @@ -50,11 +107,19 @@ const PowerManagement: React.FC = () => { const statusOptions = [ { label: "全部状态", value: "all" }, - { label: "已完成", value: "completed" }, - { label: "进行中", value: "processing" }, - { label: "已取消", value: "cancelled" }, + { label: "待支付", value: "pending", requestValue: "0" }, + { label: "已支付", value: "paid", requestValue: "1" }, + { label: "已取消", value: "cancelled", requestValue: "2" }, + { label: "已退款", value: "refunded", requestValue: "3" }, ]; + const statusMeta: Record = { + 0: { label: "待支付", color: "warning" }, + 1: { label: "已支付", color: "success" }, + 2: { label: "已取消", color: "default" }, + 3: { label: "已退款", color: "primary" }, + }; + useEffect(() => { fetchStats(); }, []); @@ -81,35 +146,20 @@ const PowerManagement: React.FC = () => { setLoading(true); try { const reqPage = customPage !== undefined ? customPage : page; - // 映射状态到订单状态:0待支付 1已支付 2已取消 3已退款 - const statusMap: Record = { - all: undefined, - completed: "1", - processing: "0", - cancelled: "2", - }; - + const statusRequestValue = statusOptions.find( + opt => opt.value === filterStatus, + )?.requestValue; const res = await getOrderList({ page: String(reqPage), limit: String(pageSize), orderType: "1", - status: statusMap[filterStatus], + status: statusRequestValue, }); - - const list = (res.list || []).map((o: any) => ({ - id: o.id, - type: o.orderTypeText || o.goodsName || "充值订单", - status: o.statusText || "", - amount: typeof o.money === "number" ? o.money / 100 : 0, - power: Number(o.goodsSpecs?.tokens ?? o.tokens ?? 0), - description: o.goodsName || "", - createTime: o.createTime || "", - })); - setRecords(list); + setRecords(res.list || []); setTotal(Number(res.total || 0)); } catch (error) { - console.error("获取消费记录失败:", error); - Toast.show({ content: "获取消费记录失败", position: "top" }); + console.error("获取订单记录失败:", error); + Toast.show({ content: "获取订单记录失败", position: "top" }); } finally { setLoading(false); } @@ -225,7 +275,7 @@ const PowerManagement: React.FC = () => { ); - // 渲染消费记录Tab + // 渲染订单记录Tab const renderRecords = () => (
{/* 筛选器 */} @@ -273,42 +323,72 @@ const PowerManagement: React.FC = () => {
- {/* 消费记录列表 */} + {/* 订单记录列表 */}
{loading && records.length === 0 ? (
加载中...
) : records.length > 0 ? ( - records.map(record => ( - -
-
-
{record.type}
- - {record.status} - -
-
-
- -¥{record.amount.toFixed(1)} + records.map(record => { + const statusCode = + record.status !== undefined ? Number(record.status) : undefined; + const tagColor = + statusCode !== undefined + ? statusMeta[statusCode]?.color || "default" + : "default"; + const tagLabel = + record.statusText || + (statusCode !== undefined + ? statusMeta[statusCode]?.label || "未知状态" + : "未知状态"); + const goodsSpecs = parseGoodsSpecs(record.goodsSpecs); + const amount = centsToYuan(record.money); + const powerValue = Number(goodsSpecs?.tokens ?? record.tokens ?? 0); + const power = Number.isNaN(powerValue) ? 0 : powerValue; + const description = + record.orderTypeText || + goodsSpecs?.name || + record.goodsName || + ""; + const createTime = formatTimestamp(record.createTime); + + return ( + + record.orderNo && + navigate(`/recharge/order/${record.orderNo}`) + } + > +
+
+
+ {record.goodsName || "算力充值"} +
+ + {tagLabel} +
-
- {formatNumber(record.power)} 算力 +
+
+ -¥{amount.toFixed(2)} +
+
+ {formatNumber(power)} 算力 +
-
-
{record.description}
-
{record.createTime}
-
- )) +
{description}
+
{createTime}
+ + ); + }) ) : (
📋
-
暂无消费记录
+
暂无订单记录
)}
@@ -334,7 +414,7 @@ const PowerManagement: React.FC = () => { className={style.powerTabs} > - + } diff --git a/Cunkebao/src/pages/mobile/mine/recharge/order/api.ts b/Cunkebao/src/pages/mobile/mine/recharge/order/api.ts index 11d4573e..acf1b12d 100644 --- a/Cunkebao/src/pages/mobile/mine/recharge/order/api.ts +++ b/Cunkebao/src/pages/mobile/mine/recharge/order/api.ts @@ -1,197 +1,37 @@ import { - RechargeOrdersResponse, RechargeOrderDetail, RechargeOrderParams, + GetRechargeOrderDetailParams, } from "./data"; - -// 模拟数据 -const mockOrders = [ - { - id: "1", - orderNo: "RC20241201001", - amount: 100.0, - paymentMethod: "wechat", - status: "success" as const, - createTime: "2024-12-01T10:30:00Z", - payTime: "2024-12-01T10:32:15Z", - description: "账户充值", - balance: 150.0, - }, - { - id: "2", - orderNo: "RC20241201002", - amount: 200.0, - paymentMethod: "alipay", - status: "pending" as const, - createTime: "2024-12-01T14:20:00Z", - description: "账户充值", - balance: 350.0, - }, - { - id: "3", - orderNo: "RC20241130001", - amount: 50.0, - paymentMethod: "bank", - status: "success" as const, - createTime: "2024-11-30T09:15:00Z", - payTime: "2024-11-30T09:18:30Z", - description: "账户充值", - balance: 50.0, - }, - { - id: "4", - orderNo: "RC20241129001", - amount: 300.0, - paymentMethod: "wechat", - status: "failed" as const, - createTime: "2024-11-29T16:45:00Z", - description: "账户充值", - }, - { - id: "5", - orderNo: "RC20241128001", - amount: 150.0, - paymentMethod: "alipay", - status: "cancelled" as const, - createTime: "2024-11-28T11:20:00Z", - description: "账户充值", - }, - { - id: "6", - orderNo: "RC20241127001", - amount: 80.0, - paymentMethod: "wechat", - status: "success" as const, - createTime: "2024-11-27T13:10:00Z", - payTime: "2024-11-27T13:12:45Z", - description: "账户充值", - balance: 80.0, - }, - { - id: "7", - orderNo: "RC20241126001", - amount: 120.0, - paymentMethod: "bank", - status: "success" as const, - createTime: "2024-11-26T08:30:00Z", - payTime: "2024-11-26T08:33:20Z", - description: "账户充值", - balance: 120.0, - }, - { - id: "8", - orderNo: "RC20241125001", - amount: 250.0, - paymentMethod: "alipay", - status: "pending" as const, - createTime: "2024-11-25T15:45:00Z", - description: "账户充值", - balance: 370.0, - }, -]; - -// 模拟延迟 -const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); +import request from "@/api/request"; // 获取充值记录列表 -export async function getRechargeOrders( - params: RechargeOrderParams, -): Promise { - await delay(800); // 模拟网络延迟 - - let filteredOrders = [...mockOrders]; - - // 状态筛选 - if (params.status && params.status !== "all") { - filteredOrders = filteredOrders.filter( - order => order.status === params.status, - ); - } - - // 时间筛选 - if (params.startTime) { - filteredOrders = filteredOrders.filter( - order => new Date(order.createTime) >= new Date(params.startTime!), - ); - } - if (params.endTime) { - filteredOrders = filteredOrders.filter( - order => new Date(order.createTime) <= new Date(params.endTime!), - ); - } - - // 分页 - const startIndex = (params.page - 1) * params.limit; - const endIndex = startIndex + params.limit; - const paginatedOrders = filteredOrders.slice(startIndex, endIndex); - - return { - list: paginatedOrders, - total: filteredOrders.length, - page: params.page, - limit: params.limit, - }; +export async function getRechargeOrders(params: RechargeOrderParams) { + return request("/v1/tokens/orderList", params, "GET"); } // 获取充值记录详情 export async function getRechargeOrderDetail( - id: string, + params: GetRechargeOrderDetailParams, ): Promise { - await delay(500); - - const order = mockOrders.find(o => o.id === id); - if (!order) { - throw new Error("订单不存在"); - } - - return { - ...order, - paymentChannel: - order.paymentMethod === "wechat" - ? "微信支付" - : order.paymentMethod === "alipay" - ? "支付宝" - : "银行转账", - transactionId: `TX${order.orderNo}`, - }; + return request("/v1/tokens/queryOrder", params, "GET"); } -// 取消充值订单 -export async function cancelRechargeOrder(id: string): Promise { - await delay(1000); - - const orderIndex = mockOrders.findIndex(o => o.id === id); - if (orderIndex === -1) { - throw new Error("订单不存在"); - } - - if (mockOrders[orderIndex].status !== "pending") { - throw new Error("只能取消处理中的订单"); - } - - // 模拟更新订单状态 - (mockOrders[orderIndex] as any).status = "cancelled"; +export interface ContinuePayParams { + orderNo: string; + [property: string]: any; } -// 申请退款 -export async function refundRechargeOrder( - id: string, - reason: string, -): Promise { - await delay(1200); - - const orderIndex = mockOrders.findIndex(o => o.id === id); - if (orderIndex === -1) { - throw new Error("订单不存在"); - } - - if (mockOrders[orderIndex].status !== "success") { - throw new Error("只能对成功的订单申请退款"); - } - - // 模拟添加退款信息 - const order = mockOrders[orderIndex]; - (order as any).refundAmount = order.amount; - (order as any).refundTime = new Date().toISOString(); - (order as any).refundReason = reason; +export interface ContinuePayResponse { + code_url?: string; + codeUrl?: string; + payUrl?: string; + [property: string]: any; +} + +// 继续支付 +export function continuePay( + params: ContinuePayParams, +): Promise { + return request("/v1/tokens/pay", params, "POST"); } diff --git a/Cunkebao/src/pages/mobile/mine/recharge/order/data.ts b/Cunkebao/src/pages/mobile/mine/recharge/order/data.ts index 95b9a0e9..2fc56bbd 100644 --- a/Cunkebao/src/pages/mobile/mine/recharge/order/data.ts +++ b/Cunkebao/src/pages/mobile/mine/recharge/order/data.ts @@ -1,40 +1,62 @@ // 充值记录类型定义 export interface RechargeOrder { - id: string; - orderNo: string; - amount: number; - paymentMethod: string; - status: "success" | "pending" | "failed" | "cancelled"; - createTime: string; - payTime?: string; + id?: number | string; + orderNo?: string; + money?: number; + amount?: number; + paymentMethod?: string; + paymentChannel?: string; + status?: number | string; + statusText?: string; + orderType?: number; + orderTypeText?: string; + createTime?: string | number; + payTime?: string | number; description?: string; + goodsName?: string; + goodsSpecs?: + | { + id: number; + name: string; + price: number; + tokens: number; + } + | string; remark?: string; operator?: string; balance?: number; + tokens?: number | string; + payType?: number; + payTypeText?: string; + transactionId?: string; } // API响应类型 export interface RechargeOrdersResponse { list: RechargeOrder[]; - total: number; - page: number; - limit: number; + total?: number; + page?: number; + limit?: number; } // 充值记录详情 export interface RechargeOrderDetail extends RechargeOrder { - paymentChannel?: string; - transactionId?: string; refundAmount?: number; - refundTime?: string; + refundTime?: string | number; refundReason?: string; } // 查询参数 export interface RechargeOrderParams { - page: number; - limit: number; - status?: string; + page?: number | string; + limit?: number | string; + status?: number | string; startTime?: string; endTime?: string; + [property: string]: any; +} + +export interface GetRechargeOrderDetailParams { + orderNo: string; + [property: string]: any; } diff --git a/Cunkebao/src/pages/mobile/mine/recharge/order/detail/index.module.scss b/Cunkebao/src/pages/mobile/mine/recharge/order/detail/index.module.scss new file mode 100644 index 00000000..72f68cb2 --- /dev/null +++ b/Cunkebao/src/pages/mobile/mine/recharge/order/detail/index.module.scss @@ -0,0 +1,154 @@ +.detailPage { + padding: 16px 16px 80px; +} + +.statusCard { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 24px 16px; + border-radius: 16px; + background: #ffffff; + box-shadow: 0 10px 30px rgba(0, 95, 204, 0.06); + margin-bottom: 20px; + text-align: center; +} + +.statusIcon { + font-size: 56px; + margin-bottom: 12px; +} + +.statusTitle { + font-size: 20px; + font-weight: 600; + color: #1d2129; +} + +.statusDesc { + margin-top: 6px; + font-size: 14px; + color: #86909c; +} + +.amountHighlight { + margin-top: 12px; + font-size: 24px; + font-weight: 600; + color: #00b578; +} + +.section { + background: #ffffff; + border-radius: 16px; + padding: 20px 16px; + box-shadow: 0 6px 24px rgba(15, 54, 108, 0.05); + margin-bottom: 16px; +} + +.sectionTitle { + font-size: 16px; + font-weight: 600; + color: #1d2129; + margin-bottom: 12px; +} + +.sectionList { + display: flex; + flex-direction: column; + gap: 12px; +} + +.row { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.row:not(:last-child) { + padding-bottom: 12px; + border-bottom: 1px dashed #e5e6eb; +} + +.label { + font-size: 14px; + color: #86909c; + flex-shrink: 0; +} + +.value { + font-size: 14px; + color: #1d2129; + text-align: right; + word-break: break-all; +} + +.copyBtn { + margin-left: 8px; + font-size: 13px; + color: #1677ff; + cursor: pointer; +} + +.tagGroup { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + justify-content: flex-end; +} + +.actions { + position: fixed; + left: 0; + right: 0; + bottom: 0; + padding: 12px 16px 24px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, #ffffff 70%); + box-shadow: 0 -4px 20px rgba(20, 66, 125, 0.06); + display: flex; + gap: 12px; +} + +.invoiceBtn { + flex: 1; + border: 1px solid #1677ff; + color: #1677ff; + border-radius: 20px; +} + +.backBtn { + flex: 1; + background: linear-gradient(135deg, #1677ff 0%, #4096ff 100%); + color: #ffffff; + border-radius: 20px; +} + +.loadingWrapper { + display: flex; + align-items: center; + justify-content: center; + height: 200px; +} + +.emptyWrapper { + text-align: center; + color: #86909c; + padding: 40px 0; +} + +.refundBlock { + margin-top: 12px; + padding: 12px; + border-radius: 12px; + background: #f5f7ff; + color: #1d2129; + line-height: 1.6; +} + +.refundTitle { + font-weight: 600; + margin-bottom: 6px; +} diff --git a/Cunkebao/src/pages/mobile/mine/recharge/order/detail/index.tsx b/Cunkebao/src/pages/mobile/mine/recharge/order/detail/index.tsx new file mode 100644 index 00000000..bb8c64d1 --- /dev/null +++ b/Cunkebao/src/pages/mobile/mine/recharge/order/detail/index.tsx @@ -0,0 +1,328 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { Button, SpinLoading, Tag, Toast } from "antd-mobile"; +import { + CheckCircleOutline, + CloseCircleOutline, + ClockCircleOutline, + ExclamationCircleOutline, +} from "antd-mobile-icons"; +import { CopyOutlined } from "@ant-design/icons"; +import Layout from "@/components/Layout/Layout"; +import NavCommon from "@/components/NavCommon"; +import { getRechargeOrderDetail } from "../api"; +import type { RechargeOrderDetail } from "../data"; +import style from "./index.module.scss"; + +type StatusMeta = { + title: string; + description: string; + amountPrefix: string; + color: string; + icon: React.ReactNode; +}; + +type StatusCode = 0 | 1 | 2 | 3 | 4; + +const statusMetaMap: Record = { + 1: { + title: "支付成功", + description: "订单已完成支付", + amountPrefix: "已支付", + color: "#00b578", + icon: , + }, + 0: { + title: "待支付", + description: "请尽快完成支付,以免订单失效", + amountPrefix: "待支付", + color: "#faad14", + icon: , + }, + 4: { + title: "支付失败", + description: "支付未成功,可重新发起支付", + amountPrefix: "需支付", + color: "#ff4d4f", + icon: , + }, + 2: { + title: "订单已取消", + description: "该订单已取消,如需继续请重新创建订单", + amountPrefix: "订单金额", + color: "#86909c", + icon: ( + + ), + }, + 3: { + title: "订单已退款", + description: "订单款项已退回,请注意查收", + amountPrefix: "退款金额", + color: "#1677ff", + icon: ( + + ), + }, +}; + +const parseStatusCode = (status?: RechargeOrderDetail["status"]) => { + if (status === undefined || status === null) return undefined; + if (typeof status === "number") + return statusMetaMap[status] ? status : undefined; + const numeric = Number(status); + if (!Number.isNaN(numeric) && statusMetaMap[numeric as StatusCode]) { + return numeric as StatusCode; + } + const map: Record = { + success: 1, + pending: 0, + failed: 4, + cancelled: 2, + refunded: 3, + }; + return map[status] ?? undefined; +}; + +const formatDateTime = (value?: string | number | null) => { + if (value === undefined || value === null) return "-"; + if (typeof value === "string" && value.trim() === "") return "-"; + + const numericValue = + typeof value === "number" ? value : Number.parseFloat(value); + if (!Number.isFinite(numericValue)) { + return String(value); + } + + const timestamp = + numericValue > 1e12 + ? numericValue + : numericValue > 1e10 + ? numericValue + : numericValue * 1000; + + const date = new Date(timestamp); + if (Number.isNaN(date.getTime())) return String(value); + return date.toLocaleString("zh-CN", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); +}; + +const centsToYuan = (value?: number | string | null) => { + if (value === undefined || value === null) return 0; + if (typeof value === "string" && value.trim() === "") return 0; + const num = Number(value); + if (!Number.isFinite(num)) return 0; + if (Number.isInteger(num)) return num / 100; + return num; +}; + +const RechargeOrderDetailPage: React.FC = () => { + const navigate = useNavigate(); + const { id } = useParams<{ id: string }>(); + const [loading, setLoading] = useState(true); + const [detail, setDetail] = useState(null); + + useEffect(() => { + if (!id) { + Toast.show({ content: "缺少订单ID", position: "top" }); + navigate(-1); + return; + } + + const fetchDetail = async () => { + try { + setLoading(true); + const res = await getRechargeOrderDetail({ orderNo: id }); + setDetail(res); + } catch (error) { + console.error("获取订单详情失败:", error); + Toast.show({ content: "获取订单详情失败", position: "top" }); + } finally { + setLoading(false); + } + }; + + fetchDetail(); + }, [id, navigate]); + + const meta = useMemo(() => { + if (!detail) { + return statusMetaMap[0]; + } + const code = parseStatusCode(detail.status); + if (code !== undefined && statusMetaMap[code]) { + return statusMetaMap[code]; + } + return statusMetaMap[0]; + }, [detail]); + + const handleCopy = async (text?: string) => { + if (!text) return; + if (!navigator.clipboard) { + Toast.show({ content: "当前环境不支持复制", position: "top" }); + return; + } + try { + await navigator.clipboard.writeText(text); + Toast.show({ content: "复制成功", position: "top" }); + } catch (error) { + console.error("复制失败:", error); + Toast.show({ content: "复制失败,请手动复制", position: "top" }); + } + }; + + const handleApplyInvoice = () => { + Toast.show({ content: "发票功能即将上线,敬请期待", position: "top" }); + }; + + const handleBack = () => { + navigate("/recharge"); + }; + + const renderRefundInfo = () => { + if (!detail?.refundAmount) return null; + return ( +
+
退款信息
+
退款金额:¥{centsToYuan(detail.refundAmount).toFixed(2)}
+ {detail.refundTime ? ( +
退款时间:{formatDateTime(detail.refundTime)}
+ ) : null} + {detail.refundReason ? ( +
退款原因:{detail.refundReason}
+ ) : null} +
+ ); + }; + + return ( + } + loading={loading && !detail} + footer={ +
+ + +
+ } + > +
+ {loading && !detail ? ( +
+ +
+ ) : !detail ? ( +
未找到订单详情
+ ) : ( + <> +
+ {meta.icon} +
{meta.title}
+
{meta.description}
+
+ {meta.amountPrefix} ¥ + {centsToYuan(detail.money ?? detail.amount ?? 0).toFixed(2)} +
+
+ +
+
订单信息
+
+
+ 订单号 + + {detail.orderNo || "-"} + handleCopy(detail.orderNo)} + > + + + +
+
+ 套餐名称 + + {detail.description || detail.goodsName || "算力充值"} + +
+
+ 订单金额 + + ¥ + {centsToYuan(detail.money ?? detail.amount ?? 0).toFixed(2)} + +
+
+ 创建时间 + + {formatDateTime(detail.createTime)} + +
+
+ 支付时间 + + {formatDateTime(detail.payTime)} + +
+ {detail.balance !== undefined ? ( +
+ 充值后余额 + + ¥{centsToYuan(detail.balance).toFixed(2)} + +
+ ) : null} +
+ {renderRefundInfo()} +
+ +
+
支付信息
+
+
+ 支付方式 + + + + {detail.payTypeText || + detail.paymentChannel || + detail.paymentMethod || + "其他支付"} + + + +
+
+ 交易流水号 + {detail.id || "-"} +
+ {detail.remark ? ( +
+ 备注信息 + {detail.remark} +
+ ) : null} +
+
+ + )} +
+
+ ); +}; + +export default RechargeOrderDetailPage; diff --git a/Cunkebao/src/pages/mobile/mine/recharge/order/index.tsx b/Cunkebao/src/pages/mobile/mine/recharge/order/index.tsx index 7e5260e6..240589b6 100644 --- a/Cunkebao/src/pages/mobile/mine/recharge/order/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/recharge/order/index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import { Card, SpinLoading, Empty, Toast, Dialog } from "antd-mobile"; import { @@ -10,14 +10,110 @@ import { } from "@ant-design/icons"; import NavCommon from "@/components/NavCommon"; import Layout from "@/components/Layout/Layout"; -import { - getRechargeOrders, - cancelRechargeOrder, - refundRechargeOrder, -} from "./api"; +import { getRechargeOrders, continuePay } from "./api"; import { RechargeOrder } from "./data"; import style from "./index.module.scss"; +type StatusCode = 0 | 1 | 2 | 3 | 4; + +const STATUS_META: Record< + StatusCode, + { label: string; color: string; tagBgOpacity?: number } +> = { + 0: { label: "待支付", color: "#faad14" }, + 1: { label: "充值成功", color: "#52c41a" }, + 2: { label: "已取消", color: "#999999" }, + 3: { label: "已退款", color: "#1890ff" }, + 4: { label: "充值失败", color: "#ff4d4f" }, +}; + +const parseStatusCode = ( + status?: RechargeOrder["status"], +): StatusCode | undefined => { + if (status === undefined || status === null) return undefined; + + if (typeof status === "number") { + return STATUS_META[status as StatusCode] + ? (status as StatusCode) + : undefined; + } + + const numeric = Number(status); + if (!Number.isNaN(numeric) && STATUS_META[numeric as StatusCode]) { + return numeric as StatusCode; + } + + const stringMap: Record = { + success: 1, + pending: 0, + cancelled: 2, + refunded: 3, + failed: 4, + }; + + return stringMap[status] ?? undefined; +}; + +const formatTimestamp = (value?: string | number | null) => { + if (value === undefined || value === null) return "-"; + if (typeof value === "string" && value.trim() === "") return "-"; + + const numericValue = + typeof value === "number" ? value : Number.parseFloat(value); + if (!Number.isFinite(numericValue)) { + return String(value); + } + + const timestamp = + numericValue > 1e12 + ? numericValue + : numericValue > 1e10 + ? numericValue + : numericValue * 1000; + + const date = new Date(timestamp); + if (Number.isNaN(date.getTime())) { + return String(value); + } + + const now = new Date(); + const diff = now.getTime() - date.getTime(); + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + + if (days === 0) { + return date.toLocaleTimeString("zh-CN", { + hour: "2-digit", + minute: "2-digit", + }); + } + if (days === 1) { + return `昨天 ${date.toLocaleTimeString("zh-CN", { + hour: "2-digit", + minute: "2-digit", + })}`; + } + if (days < 7) { + return `${days}天前`; + } + return date.toLocaleDateString("zh-CN"); +}; + +const centsToYuan = (value?: number | string | null) => { + if (value === undefined || value === null) return 0; + if (typeof value === "string" && value.trim() === "") return 0; + const num = Number(value); + if (!Number.isFinite(num)) return 0; + if (Number.isInteger(num)) return num / 100; + return num; +}; + +const getPaymentMethodText = (order: RechargeOrder) => { + if (order.payTypeText) return order.payTypeText; + if (order.paymentChannel) return order.paymentChannel; + if (order.paymentMethod) return order.paymentMethod; + return "其他支付"; +}; + const RechargeOrders: React.FC = () => { const navigate = useNavigate(); const [orders, setOrders] = useState([]); @@ -25,6 +121,7 @@ const RechargeOrders: React.FC = () => { const [hasMore, setHasMore] = useState(true); const [page, setPage] = useState(1); const [statusFilter, setStatusFilter] = useState("all"); + const [payingOrderNo, setPayingOrderNo] = useState(null); const loadOrders = async (reset = false) => { setLoading(true); @@ -33,6 +130,7 @@ const RechargeOrders: React.FC = () => { const params = { page: currentPage, limit: 20, + orderType: 1, ...(statusFilter !== "all" && { status: statusFilter }), }; @@ -53,6 +151,7 @@ const RechargeOrders: React.FC = () => { // 初始化加载 useEffect(() => { loadOrders(true); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // 筛选条件变化时重新加载 @@ -63,36 +162,6 @@ const RechargeOrders: React.FC = () => { loadOrders(true); }; - const getStatusText = (status: string) => { - switch (status) { - case "success": - return "充值成功"; - case "pending": - return "处理中"; - case "failed": - return "充值失败"; - case "cancelled": - return "已取消"; - default: - return "未知状态"; - } - }; - - const getStatusColor = (status: string) => { - switch (status) { - case "success": - return "#52c41a"; - case "pending": - return "#faad14"; - case "failed": - return "#ff4d4f"; - case "cancelled": - return "#999"; - default: - return "#666"; - } - }; - const getPaymentMethodIcon = (method: string) => { switch (method.toLowerCase()) { case "wechat": @@ -119,131 +188,231 @@ const RechargeOrders: React.FC = () => { } }; - const formatTime = (timeStr: string) => { - const date = new Date(timeStr); - const now = new Date(); - const diff = now.getTime() - date.getTime(); - const days = Math.floor(diff / (1000 * 60 * 60 * 24)); - - if (days === 0) { - return date.toLocaleTimeString("zh-CN", { - hour: "2-digit", - minute: "2-digit", + const handleViewDetail = (order: RechargeOrder) => { + const identifier = order.orderNo || order.id; + if (!identifier) { + Toast.show({ + content: "无法打开订单详情", + position: "top", }); - } else if (days === 1) { - return ( - "昨天 " + - date.toLocaleTimeString("zh-CN", { - hour: "2-digit", - minute: "2-digit", - }) + return; + } + navigate(`/recharge/order/${identifier}`); + }; + + const openPayDialog = ( + order: RechargeOrder, + options: { codeUrl?: string; payUrl?: string }, + ) => { + const { codeUrl, payUrl } = options; + if (codeUrl) { + Dialog.show({ + content: ( +
+
+ 请使用微信扫码完成支付 +
+ 支付二维码 +
+ 支付金额:¥ + {centsToYuan(order.money ?? order.amount ?? 0).toFixed(2)} +
+
+ ), + closeOnMaskClick: true, + }); + return; + } + + if (payUrl) { + window.location.href = payUrl; + return; + } + + Toast.show({ + content: "暂未获取到支付信息,请稍后重试", + position: "top", + }); + }; + + const handleContinuePay = async (order: RechargeOrder) => { + if (!order.orderNo) { + Toast.show({ + content: "订单号缺失,无法继续支付", + position: "top", + }); + return; + } + + const orderNo = String(order.orderNo); + setPayingOrderNo(orderNo); + try { + const res = await continuePay({ orderNo }); + const codeUrl = res?.code_url || res?.codeUrl; + const payUrl = res?.payUrl; + if (!codeUrl && !payUrl) { + Toast.show({ + content: "未获取到支付链接,请稍后重试", + position: "top", + }); + return; + } + openPayDialog(order, { codeUrl, payUrl }); + } catch (error) { + console.error("继续支付失败:", error); + Toast.show({ + content: "继续支付失败,请重试", + position: "top", + }); + } finally { + setPayingOrderNo(prev => (prev === orderNo ? null : prev)); + } + }; + + const renderOrderItem = (order: RechargeOrder) => { + const statusCode = parseStatusCode(order.status); + const statusMeta = + statusCode !== undefined ? STATUS_META[statusCode] : undefined; + const paymentMethod = getPaymentMethodText(order); + const paymentMethodKey = paymentMethod.toLowerCase(); + const statusBgOpacity = statusMeta?.tagBgOpacity ?? 0.15; + const statusBgColor = statusMeta + ? `${statusMeta.color}${Math.round(statusBgOpacity * 255) + .toString(16) + .padStart(2, "0")}` + : "#66666626"; + const amount = centsToYuan(order.money ?? order.amount ?? 0) || 0; + const isPaying = payingOrderNo === order.orderNo; + const actions: React.ReactNode[] = []; + + if (statusCode === 0) { + actions.push( + , ); - } else if (days < 7) { - return `${days}天前`; - } else { - return date.toLocaleDateString("zh-CN"); } - }; - const handleCancelOrder = async (orderId: string) => { - const result = await Dialog.confirm({ - content: "确定要取消这个充值订单吗?", - confirmText: "确定取消", - cancelText: "再想想", - }); - - if (result) { - try { - await cancelRechargeOrder(orderId); - Toast.show({ content: "订单已取消", position: "top" }); - loadOrders(true); - } catch (error) { - console.error("取消订单失败:", error); - Toast.show({ content: "取消失败,请重试", position: "top" }); - } + if (statusCode === 4) { + actions.push( + , + ); } - }; - const handleRefundOrder = async (orderId: string) => { - const result = await Dialog.confirm({ - content: "确定要申请退款吗?退款将在1-3个工作日内处理。", - confirmText: "申请退款", - cancelText: "取消", - }); - - if (result) { - try { - await refundRechargeOrder(orderId, "用户主动申请退款"); - Toast.show({ content: "退款申请已提交", position: "top" }); - loadOrders(true); - } catch (error) { - console.error("申请退款失败:", error); - Toast.show({ content: "申请失败,请重试", position: "top" }); - } + if (statusCode === 1 || statusCode === 3 || statusCode === 2) { + actions.push( + , + ); } - }; - const renderOrderItem = (order: RechargeOrder) => ( - -
-
-
订单号:{order.orderNo}
-
- - {formatTime(order.createTime)} + actions.push( + , + ); + + return ( + +
+
+
+ 订单号:{order.orderNo || "-"} +
+
+ + {formatTimestamp(order.createTime)} +
+
+
+
¥{amount.toFixed(2)}
+
+ {statusMeta?.label || "未知状态"} +
-
-
- ¥{order.amount.toFixed(2)} -
-
- {getStatusText(order.status)} -
-
-
-
-
-
- {getPaymentMethodIcon(order.paymentMethod)} +
+
+
+ {getPaymentMethodIcon(paymentMethod)} +
+
{paymentMethod}
-
{order.paymentMethod}
+ + {(order.description || order.remark) && ( +
+ 备注 + + {order.description || order.remark} + +
+ )} + + {order.payTime && ( +
+ 支付时间 + + {formatTimestamp(order.payTime)} + +
+ )} + + {order.balance !== undefined && ( +
+ 充值后余额: ¥{order.balance.toFixed(2)} +
+ )}
- {order.description && ( -
- 备注 - {order.description} -
- )} - - {order.payTime && ( -
- 支付时间 - {formatTime(order.payTime)} -
- )} - - {order.balance !== undefined && ( -
- 充值后余额: ¥{order.balance.toFixed(2)} -
- )} -
- - {order.status === "pending" && ( + {/* {order.status === "pending" && (
- )} + )} */} - {order.status === "success" && ( -
- - -
- )} - - {order.status === "failed" && ( -
- -
- )} - - ); + {actions.length > 0 && ( +
{actions}
+ )} + + ); + }; const filterTabs = [ { key: "all", label: "全部" }, - { key: "success", label: "成功" }, - { key: "pending", label: "处理中" }, - { key: "failed", label: "失败" }, - { key: "cancelled", label: "已取消" }, + { key: "1", label: "成功" }, + { key: "0", label: "待支付" }, + { key: "2", label: "已取消" }, + { key: "3", label: "已退款" }, ]; return ( diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx b/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx index 7590f6fa..76d460c8 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx +++ b/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx @@ -197,7 +197,7 @@ const BasicSettings: React.FC = ({ // 下载模板 const handleDownloadTemplate = () => { const template = - "电话号码,微信号,来源,订单金额,下单日期\n13800138000,wxid_123,抖音,99.00,2024-03-03"; + "姓名/备注,电话号码,微信号,来源,订单金额,下单日期\n张三,13800138000,wxid_123,抖音,99.00,2024-03-03"; const blob = new Blob([template], { type: "text/csv" }); const url = window.URL.createObjectURL(blob); const a = document.createElement("a"); diff --git a/Cunkebao/src/router/index.tsx b/Cunkebao/src/router/index.tsx index d52ba041..fc9adb97 100644 --- a/Cunkebao/src/router/index.tsx +++ b/Cunkebao/src/router/index.tsx @@ -44,7 +44,7 @@ const AppRouter: React.FC = () => ( }} > - + {/* */} ); diff --git a/Cunkebao/src/router/module/mine.tsx b/Cunkebao/src/router/module/mine.tsx index bf2bc20c..58e9ec87 100644 --- a/Cunkebao/src/router/module/mine.tsx +++ b/Cunkebao/src/router/module/mine.tsx @@ -9,6 +9,7 @@ import WechatAccounts from "@/pages/mobile/mine/wechat-accounts/list/index"; import WechatAccountDetail from "@/pages/mobile/mine/wechat-accounts/detail/index"; import Recharge from "@/pages/mobile/mine/recharge/index"; import RechargeOrder from "@/pages/mobile/mine/recharge/order/index"; +import RechargeOrderDetail from "@/pages/mobile/mine/recharge/order/detail"; import BuyPower from "@/pages/mobile/mine/recharge/buy-power"; import UsageRecords from "@/pages/mobile/mine/recharge/usage-records"; import Setting from "@/pages/mobile/mine/setting/index"; @@ -76,6 +77,11 @@ const routes = [ element: , auth: true, }, + { + path: "/recharge/order/:id", + element: , + auth: true, + }, { path: "/recharge/buy-power", element: ,