重构充值订单管理:更新OrderList接口以支持灵活的数据类型,增强订单状态处理,并改进订单记录的UI交互。实现付款连续逻辑,并通过更好的状态表示来优化订单详细信息呈现。

This commit is contained in:
超级老白兔
2025-11-19 10:04:32 +08:00
parent 5da3bc3000
commit bb2b681ae0
11 changed files with 1042 additions and 452 deletions

View File

@@ -75,7 +75,7 @@ export interface OrderListParams {
[property: string]: any; [property: string]: any;
} }
interface OrderList { export interface OrderList {
id?: number; id?: number;
mchId?: number; mchId?: number;
companyId?: number; companyId?: number;
@@ -84,22 +84,24 @@ interface OrderList {
status?: number; status?: number;
goodsId?: number; goodsId?: number;
goodsName?: string; goodsName?: string;
goodsSpecs?: { goodsSpecs?:
| {
id: number; id: number;
name: string; name: string;
price: number; price: number;
tokens: number; tokens: number;
}; }
| string;
money?: number; money?: number;
orderNo?: string; orderNo?: string;
ip?: string; ip?: string;
nonceStr?: string; nonceStr?: string;
createTime?: string; createTime?: string | number;
payType?: number; payType?: number;
payTime?: string; payTime?: string | number;
payInfo?: any; payInfo?: any;
deleteTime?: string; deleteTime?: string | number;
tokens?: string; tokens?: string | number;
statusText?: string; statusText?: string;
orderTypeText?: string; orderTypeText?: string;
payTypeText?: string; payTypeText?: string;

View File

@@ -258,6 +258,18 @@
border-radius: 12px; border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
border: 1px solid #f0f0f0; 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 { .recordHeader {

View File

@@ -12,17 +12,74 @@ import {
import NavCommon from "@/components/NavCommon"; import NavCommon from "@/components/NavCommon";
import Layout from "@/components/Layout/Layout"; import Layout from "@/components/Layout/Layout";
import { getStatistics, getOrderList } from "./api"; import { getStatistics, getOrderList } from "./api";
import type { Statistics } from "./api"; import type { Statistics, OrderList } from "./api";
import { Pagination } from "antd"; import { Pagination } from "antd";
type OrderRecordView = { type TagColor = NonNullable<React.ComponentProps<typeof Tag>["color"]>;
type GoodsSpecs =
| {
id: number; id: number;
type: string; name: string;
status: string; price: number;
amount: number; // 元 tokens: number;
power: number; }
description: string; | undefined;
createTime: string;
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 = () => { const PowerManagement: React.FC = () => {
@@ -30,7 +87,7 @@ const PowerManagement: React.FC = () => {
const [activeTab, setActiveTab] = useState("overview"); const [activeTab, setActiveTab] = useState("overview");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [stats, setStats] = useState<Statistics | null>(null); const [stats, setStats] = useState<Statistics | null>(null);
const [records, setRecords] = useState<OrderRecordView[]>([]); const [records, setRecords] = useState<OrderList[]>([]);
const [filterType, setFilterType] = useState<string>("all"); const [filterType, setFilterType] = useState<string>("all");
const [filterStatus, setFilterStatus] = useState<string>("all"); const [filterStatus, setFilterStatus] = useState<string>("all");
const [filterTypeVisible, setFilterTypeVisible] = useState(false); const [filterTypeVisible, setFilterTypeVisible] = useState(false);
@@ -50,11 +107,19 @@ const PowerManagement: React.FC = () => {
const statusOptions = [ const statusOptions = [
{ label: "全部状态", value: "all" }, { label: "全部状态", value: "all" },
{ label: "已完成", value: "completed" }, { label: "待支付", value: "pending", requestValue: "0" },
{ label: "进行中", value: "processing" }, { label: "已支付", value: "paid", requestValue: "1" },
{ label: "已取消", value: "cancelled" }, { label: "已取消", value: "cancelled", requestValue: "2" },
{ label: "已退款", value: "refunded", requestValue: "3" },
]; ];
const statusMeta: Record<number, { label: string; color: TagColor }> = {
0: { label: "待支付", color: "warning" },
1: { label: "已支付", color: "success" },
2: { label: "已取消", color: "default" },
3: { label: "已退款", color: "primary" },
};
useEffect(() => { useEffect(() => {
fetchStats(); fetchStats();
}, []); }, []);
@@ -81,35 +146,20 @@ const PowerManagement: React.FC = () => {
setLoading(true); setLoading(true);
try { try {
const reqPage = customPage !== undefined ? customPage : page; const reqPage = customPage !== undefined ? customPage : page;
// 映射状态到订单状态0待支付 1已支付 2已取消 3已退款 const statusRequestValue = statusOptions.find(
const statusMap: Record<string, string | undefined> = { opt => opt.value === filterStatus,
all: undefined, )?.requestValue;
completed: "1",
processing: "0",
cancelled: "2",
};
const res = await getOrderList({ const res = await getOrderList({
page: String(reqPage), page: String(reqPage),
limit: String(pageSize), limit: String(pageSize),
orderType: "1", orderType: "1",
status: statusMap[filterStatus], status: statusRequestValue,
}); });
setRecords(res.list || []);
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);
setTotal(Number(res.total || 0)); setTotal(Number(res.total || 0));
} catch (error) { } catch (error) {
console.error("获取消费记录失败:", error); console.error("获取订单记录失败:", error);
Toast.show({ content: "获取消费记录失败", position: "top" }); Toast.show({ content: "获取订单记录失败", position: "top" });
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -225,7 +275,7 @@ const PowerManagement: React.FC = () => {
</div> </div>
); );
// 渲染消费记录Tab // 渲染订单记录Tab
const renderRecords = () => ( const renderRecords = () => (
<div className={style.recordsContent}> <div className={style.recordsContent}>
{/* 筛选器 */} {/* 筛选器 */}
@@ -273,42 +323,72 @@ const PowerManagement: React.FC = () => {
</Picker> </Picker>
</div> </div>
{/* 消费记录列表 */} {/* 订单记录列表 */}
<div className={style.recordList}> <div className={style.recordList}>
{loading && records.length === 0 ? ( {loading && records.length === 0 ? (
<div className={style.loadingContainer}> <div className={style.loadingContainer}>
<div className={style.loadingText}>...</div> <div className={style.loadingText}>...</div>
</div> </div>
) : records.length > 0 ? ( ) : records.length > 0 ? (
records.map(record => ( records.map(record => {
<Card key={record.id} className={style.recordItem}> 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 (
<Card
key={record.id ?? record.orderNo}
className={style.recordItem}
onClick={() =>
record.orderNo &&
navigate(`/recharge/order/${record.orderNo}`)
}
>
<div className={style.recordHeader}> <div className={style.recordHeader}>
<div className={style.recordLeft}> <div className={style.recordLeft}>
<div className={style.recordType}>{record.type}</div> <div className={style.recordType}>
<Tag {record.goodsName || "算力充值"}
color={record.status === "已完成" ? "success" : "primary"} </div>
className={style.recordStatus} <Tag color={tagColor} className={style.recordStatus}>
> {tagLabel}
{record.status}
</Tag> </Tag>
</div> </div>
<div className={style.recordRight}> <div className={style.recordRight}>
<div className={style.recordAmount}> <div className={style.recordAmount}>
-¥{record.amount.toFixed(1)} -¥{amount.toFixed(2)}
</div> </div>
<div className={style.recordPower}> <div className={style.recordPower}>
{formatNumber(record.power)} {formatNumber(power)}
</div> </div>
</div> </div>
</div> </div>
<div className={style.recordDesc}>{record.description}</div> <div className={style.recordDesc}>{description}</div>
<div className={style.recordTime}>{record.createTime}</div> <div className={style.recordTime}>{createTime}</div>
</Card> </Card>
)) );
})
) : ( ) : (
<div className={style.emptyRecords}> <div className={style.emptyRecords}>
<div className={style.emptyIcon}>📋</div> <div className={style.emptyIcon}>📋</div>
<div className={style.emptyText}></div> <div className={style.emptyText}></div>
</div> </div>
)} )}
</div> </div>
@@ -334,7 +414,7 @@ const PowerManagement: React.FC = () => {
className={style.powerTabs} className={style.powerTabs}
> >
<Tabs.Tab title="概览" key="overview" /> <Tabs.Tab title="概览" key="overview" />
<Tabs.Tab title="消费记录" key="records" /> <Tabs.Tab title="订单记录" key="records" />
</Tabs> </Tabs>
</> </>
} }

View File

@@ -1,197 +1,37 @@
import { import {
RechargeOrdersResponse,
RechargeOrderDetail, RechargeOrderDetail,
RechargeOrderParams, RechargeOrderParams,
GetRechargeOrderDetailParams,
} from "./data"; } from "./data";
import request from "@/api/request";
// 模拟数据
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));
// 获取充值记录列表 // 获取充值记录列表
export async function getRechargeOrders( export async function getRechargeOrders(params: RechargeOrderParams) {
params: RechargeOrderParams, return request("/v1/tokens/orderList", params, "GET");
): Promise<RechargeOrdersResponse> {
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 getRechargeOrderDetail( export async function getRechargeOrderDetail(
id: string, params: GetRechargeOrderDetailParams,
): Promise<RechargeOrderDetail> { ): Promise<RechargeOrderDetail> {
await delay(500); return request("/v1/tokens/queryOrder", params, "GET");
const order = mockOrders.find(o => o.id === id);
if (!order) {
throw new Error("订单不存在");
} }
return { export interface ContinuePayParams {
...order, orderNo: string;
paymentChannel: [property: string]: any;
order.paymentMethod === "wechat"
? "微信支付"
: order.paymentMethod === "alipay"
? "支付宝"
: "银行转账",
transactionId: `TX${order.orderNo}`,
};
} }
// 取消充值订单 export interface ContinuePayResponse {
export async function cancelRechargeOrder(id: string): Promise<void> { code_url?: string;
await delay(1000); codeUrl?: string;
payUrl?: string;
const orderIndex = mockOrders.findIndex(o => o.id === id); [property: string]: any;
if (orderIndex === -1) {
throw new Error("订单不存在");
} }
if (mockOrders[orderIndex].status !== "pending") { // 继续支付
throw new Error("只能取消处理中的订单"); export function continuePay(
} params: ContinuePayParams,
): Promise<ContinuePayResponse> {
// 模拟更新订单状态 return request("/v1/tokens/pay", params, "POST");
(mockOrders[orderIndex] as any).status = "cancelled";
}
// 申请退款
export async function refundRechargeOrder(
id: string,
reason: string,
): Promise<void> {
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;
} }

View File

@@ -1,40 +1,62 @@
// 充值记录类型定义 // 充值记录类型定义
export interface RechargeOrder { export interface RechargeOrder {
id: string; id?: number | string;
orderNo: string; orderNo?: string;
amount: number; money?: number;
paymentMethod: string; amount?: number;
status: "success" | "pending" | "failed" | "cancelled"; paymentMethod?: string;
createTime: string; paymentChannel?: string;
payTime?: string; status?: number | string;
statusText?: string;
orderType?: number;
orderTypeText?: string;
createTime?: string | number;
payTime?: string | number;
description?: string; description?: string;
goodsName?: string;
goodsSpecs?:
| {
id: number;
name: string;
price: number;
tokens: number;
}
| string;
remark?: string; remark?: string;
operator?: string; operator?: string;
balance?: number; balance?: number;
tokens?: number | string;
payType?: number;
payTypeText?: string;
transactionId?: string;
} }
// API响应类型 // API响应类型
export interface RechargeOrdersResponse { export interface RechargeOrdersResponse {
list: RechargeOrder[]; list: RechargeOrder[];
total: number; total?: number;
page: number; page?: number;
limit: number; limit?: number;
} }
// 充值记录详情 // 充值记录详情
export interface RechargeOrderDetail extends RechargeOrder { export interface RechargeOrderDetail extends RechargeOrder {
paymentChannel?: string;
transactionId?: string;
refundAmount?: number; refundAmount?: number;
refundTime?: string; refundTime?: string | number;
refundReason?: string; refundReason?: string;
} }
// 查询参数 // 查询参数
export interface RechargeOrderParams { export interface RechargeOrderParams {
page: number; page?: number | string;
limit: number; limit?: number | string;
status?: string; status?: number | string;
startTime?: string; startTime?: string;
endTime?: string; endTime?: string;
[property: string]: any;
}
export interface GetRechargeOrderDetailParams {
orderNo: string;
[property: string]: any;
} }

View File

@@ -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;
}

View File

@@ -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<StatusCode, StatusMeta> = {
1: {
title: "支付成功",
description: "订单已完成支付",
amountPrefix: "已支付",
color: "#00b578",
icon: <CheckCircleOutline className={style.statusIcon} color="#00b578" />,
},
0: {
title: "待支付",
description: "请尽快完成支付,以免订单失效",
amountPrefix: "待支付",
color: "#faad14",
icon: <ClockCircleOutline className={style.statusIcon} color="#faad14" />,
},
4: {
title: "支付失败",
description: "支付未成功,可重新发起支付",
amountPrefix: "需支付",
color: "#ff4d4f",
icon: <CloseCircleOutline className={style.statusIcon} color="#ff4d4f" />,
},
2: {
title: "订单已取消",
description: "该订单已取消,如需继续请重新创建订单",
amountPrefix: "订单金额",
color: "#86909c",
icon: (
<ExclamationCircleOutline className={style.statusIcon} color="#86909c" />
),
},
3: {
title: "订单已退款",
description: "订单款项已退回,请注意查收",
amountPrefix: "退款金额",
color: "#1677ff",
icon: (
<ExclamationCircleOutline className={style.statusIcon} color="#1677ff" />
),
},
};
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<string, StatusCode> = {
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<RechargeOrderDetail | null>(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<StatusMeta>(() => {
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 (
<div className={style.refundBlock}>
<div className={style.refundTitle}>退</div>
<div>退¥{centsToYuan(detail.refundAmount).toFixed(2)}</div>
{detail.refundTime ? (
<div>退{formatDateTime(detail.refundTime)}</div>
) : null}
{detail.refundReason ? (
<div>退{detail.refundReason}</div>
) : null}
</div>
);
};
return (
<Layout
header={<NavCommon title="订单详情" />}
loading={loading && !detail}
footer={
<div className={style.actions}>
<Button className={style.invoiceBtn} onClick={handleApplyInvoice}>
</Button>
<Button className={style.backBtn} onClick={handleBack}>
</Button>
</div>
}
>
<div className={style.detailPage}>
{loading && !detail ? (
<div className={style.loadingWrapper}>
<SpinLoading color="primary" />
</div>
) : !detail ? (
<div className={style.emptyWrapper}></div>
) : (
<>
<div className={style.statusCard}>
{meta.icon}
<div className={style.statusTitle}>{meta.title}</div>
<div className={style.statusDesc}>{meta.description}</div>
<div
className={style.amountHighlight}
style={{ color: meta.color }}
>
{meta.amountPrefix} ¥
{centsToYuan(detail.money ?? detail.amount ?? 0).toFixed(2)}
</div>
</div>
<div className={style.section}>
<div className={style.sectionTitle}></div>
<div className={style.sectionList}>
<div className={style.row}>
<span className={style.label}></span>
<span className={style.value}>
{detail.orderNo || "-"}
<span
className={style.copyBtn}
onClick={() => handleCopy(detail.orderNo)}
>
<CopyOutlined />
</span>
</span>
</div>
<div className={style.row}>
<span className={style.label}></span>
<span className={style.value}>
{detail.description || detail.goodsName || "算力充值"}
</span>
</div>
<div className={style.row}>
<span className={style.label}></span>
<span className={style.value}>
¥
{centsToYuan(detail.money ?? detail.amount ?? 0).toFixed(2)}
</span>
</div>
<div className={style.row}>
<span className={style.label}></span>
<span className={style.value}>
{formatDateTime(detail.createTime)}
</span>
</div>
<div className={style.row}>
<span className={style.label}></span>
<span className={style.value}>
{formatDateTime(detail.payTime)}
</span>
</div>
{detail.balance !== undefined ? (
<div className={style.row}>
<span className={style.label}></span>
<span className={style.value}>
¥{centsToYuan(detail.balance).toFixed(2)}
</span>
</div>
) : null}
</div>
{renderRefundInfo()}
</div>
<div className={style.section}>
<div className={style.sectionTitle}></div>
<div className={style.sectionList}>
<div className={style.row}>
<span className={style.label}></span>
<span className={style.value}>
<span className={style.tagGroup}>
<Tag color="primary" fill="outline">
{detail.payTypeText ||
detail.paymentChannel ||
detail.paymentMethod ||
"其他支付"}
</Tag>
</span>
</span>
</div>
<div className={style.row}>
<span className={style.label}></span>
<span className={style.value}>{detail.id || "-"}</span>
</div>
{detail.remark ? (
<div className={style.row}>
<span className={style.label}></span>
<span className={style.value}>{detail.remark}</span>
</div>
) : null}
</div>
</div>
</>
)}
</div>
</Layout>
);
};
export default RechargeOrderDetailPage;

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from "react"; import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Card, SpinLoading, Empty, Toast, Dialog } from "antd-mobile"; import { Card, SpinLoading, Empty, Toast, Dialog } from "antd-mobile";
import { import {
@@ -10,14 +10,110 @@ import {
} from "@ant-design/icons"; } from "@ant-design/icons";
import NavCommon from "@/components/NavCommon"; import NavCommon from "@/components/NavCommon";
import Layout from "@/components/Layout/Layout"; import Layout from "@/components/Layout/Layout";
import { import { getRechargeOrders, continuePay } from "./api";
getRechargeOrders,
cancelRechargeOrder,
refundRechargeOrder,
} from "./api";
import { RechargeOrder } from "./data"; import { RechargeOrder } from "./data";
import style from "./index.module.scss"; 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<string, StatusCode> = {
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 RechargeOrders: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [orders, setOrders] = useState<RechargeOrder[]>([]); const [orders, setOrders] = useState<RechargeOrder[]>([]);
@@ -25,6 +121,7 @@ const RechargeOrders: React.FC = () => {
const [hasMore, setHasMore] = useState(true); const [hasMore, setHasMore] = useState(true);
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [statusFilter, setStatusFilter] = useState<string>("all"); const [statusFilter, setStatusFilter] = useState<string>("all");
const [payingOrderNo, setPayingOrderNo] = useState<string | null>(null);
const loadOrders = async (reset = false) => { const loadOrders = async (reset = false) => {
setLoading(true); setLoading(true);
@@ -33,6 +130,7 @@ const RechargeOrders: React.FC = () => {
const params = { const params = {
page: currentPage, page: currentPage,
limit: 20, limit: 20,
orderType: 1,
...(statusFilter !== "all" && { status: statusFilter }), ...(statusFilter !== "all" && { status: statusFilter }),
}; };
@@ -53,6 +151,7 @@ const RechargeOrders: React.FC = () => {
// 初始化加载 // 初始化加载
useEffect(() => { useEffect(() => {
loadOrders(true); loadOrders(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
// 筛选条件变化时重新加载 // 筛选条件变化时重新加载
@@ -63,36 +162,6 @@ const RechargeOrders: React.FC = () => {
loadOrders(true); 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) => { const getPaymentMethodIcon = (method: string) => {
switch (method.toLowerCase()) { switch (method.toLowerCase()) {
case "wechat": case "wechat":
@@ -119,92 +188,188 @@ const RechargeOrders: React.FC = () => {
} }
}; };
const formatTime = (timeStr: string) => { const handleViewDetail = (order: RechargeOrder) => {
const date = new Date(timeStr); const identifier = order.orderNo || order.id;
const now = new Date(); if (!identifier) {
const diff = now.getTime() - date.getTime(); Toast.show({
const days = Math.floor(diff / (1000 * 60 * 60 * 24)); content: "无法打开订单详情",
position: "top",
if (days === 0) {
return date.toLocaleTimeString("zh-CN", {
hour: "2-digit",
minute: "2-digit",
}); });
} else if (days === 1) { return;
return ( }
"昨天 " + navigate(`/recharge/order/${identifier}`);
date.toLocaleTimeString("zh-CN", { };
hour: "2-digit",
minute: "2-digit", const openPayDialog = (
}) order: RechargeOrder,
options: { codeUrl?: string; payUrl?: string },
) => {
const { codeUrl, payUrl } = options;
if (codeUrl) {
Dialog.show({
content: (
<div style={{ textAlign: "center", padding: "20px" }}>
<div
style={{
marginBottom: "16px",
fontSize: "16px",
fontWeight: 500,
}}
>
使
</div>
<img
src={codeUrl}
alt="支付二维码"
style={{ width: "220px", height: "220px", margin: "0 auto" }}
/>
<div
style={{
marginTop: "16px",
color: "#666",
fontSize: "14px",
}}
>
{centsToYuan(order.money ?? order.amount ?? 0).toFixed(2)}
</div>
</div>
),
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(
<button
key="continue"
className={`${style["action-btn"]} ${style["primary"]}`}
onClick={() => handleContinuePay(order)}
disabled={isPaying}
>
{isPaying ? "处理中..." : "继续支付"}
</button>,
); );
} else if (days < 7) {
return `${days}天前`;
} else {
return date.toLocaleDateString("zh-CN");
} }
};
const handleCancelOrder = async (orderId: string) => { if (statusCode === 4) {
const result = await Dialog.confirm({ actions.push(
content: "确定要取消这个充值订单吗?", <button
confirmText: "确定取消", key="retry"
cancelText: "再想想", className={`${style["action-btn"]} ${style["primary"]}`}
}); onClick={() => navigate("/recharge")}
>
if (result) {
try { </button>,
await cancelRechargeOrder(orderId); );
Toast.show({ content: "订单已取消", position: "top" });
loadOrders(true);
} catch (error) {
console.error("取消订单失败:", error);
Toast.show({ content: "取消失败,请重试", position: "top" });
} }
}
};
const handleRefundOrder = async (orderId: string) => { if (statusCode === 1 || statusCode === 3 || statusCode === 2) {
const result = await Dialog.confirm({ actions.push(
content: "确定要申请退款吗退款将在1-3个工作日内处理。", <button
confirmText: "申请退款", key="purchase-again"
cancelText: "取消", className={`${style["action-btn"]} ${style["secondary"]}`}
}); onClick={() => navigate("/recharge")}
>
if (result) {
try { </button>,
await refundRechargeOrder(orderId, "用户主动申请退款"); );
Toast.show({ content: "退款申请已提交", position: "top" });
loadOrders(true);
} catch (error) {
console.error("申请退款失败:", error);
Toast.show({ content: "申请失败,请重试", position: "top" });
} }
}
};
const renderOrderItem = (order: RechargeOrder) => ( actions.push(
<button
key="detail"
className={`${style["action-btn"]} ${style["secondary"]}`}
onClick={() => handleViewDetail(order)}
>
</button>,
);
return (
<Card key={order.id} className={style["order-card"]}> <Card key={order.id} className={style["order-card"]}>
<div className={style["order-header"]}> <div className={style["order-header"]}>
<div className={style["order-info"]}> <div className={style["order-info"]}>
<div className={style["order-no"]}>{order.orderNo}</div> <div className={style["order-no"]}>
{order.orderNo || "-"}
</div>
<div className={style["order-time"]}> <div className={style["order-time"]}>
<ClockCircleOutlined style={{ fontSize: 12 }} /> <ClockCircleOutlined style={{ fontSize: 12 }} />
{formatTime(order.createTime)} {formatTimestamp(order.createTime)}
</div> </div>
</div> </div>
<div className={style["order-amount"]}> <div className={style["order-amount"]}>
<div className={style["amount-text"]}> <div className={style["amount-text"]}>{amount.toFixed(2)}</div>
{order.amount.toFixed(2)}
</div>
<div <div
className={style["status-tag"]} className={style["status-tag"]}
style={{ style={{
backgroundColor: `${getStatusColor(order.status)}20`, backgroundColor: statusBgColor,
color: getStatusColor(order.status), color: statusMeta?.color || "#666",
}} }}
> >
{getStatusText(order.status)} {statusMeta?.label || "未知状态"}
</div> </div>
</div> </div>
</div> </div>
@@ -214,25 +379,29 @@ const RechargeOrders: React.FC = () => {
<div <div
className={style["method-icon"]} className={style["method-icon"]}
style={{ style={{
backgroundColor: getPaymentMethodColor(order.paymentMethod), backgroundColor: getPaymentMethodColor(paymentMethodKey),
}} }}
> >
{getPaymentMethodIcon(order.paymentMethod)} {getPaymentMethodIcon(paymentMethod)}
</div> </div>
<div className={style["method-text"]}>{order.paymentMethod}</div> <div className={style["method-text"]}>{paymentMethod}</div>
</div> </div>
{order.description && ( {(order.description || order.remark) && (
<div className={style["detail-row"]}> <div className={style["detail-row"]}>
<span className={style["label"]}></span> <span className={style["label"]}></span>
<span className={style["value"]}>{order.description}</span> <span className={style["value"]}>
{order.description || order.remark}
</span>
</div> </div>
)} )}
{order.payTime && ( {order.payTime && (
<div className={style["detail-row"]}> <div className={style["detail-row"]}>
<span className={style["label"]}></span> <span className={style["label"]}></span>
<span className={style["value"]}>{formatTime(order.payTime)}</span> <span className={style["value"]}>
{formatTimestamp(order.payTime)}
</span>
</div> </div>
)} )}
@@ -243,7 +412,7 @@ const RechargeOrders: React.FC = () => {
)} )}
</div> </div>
{order.status === "pending" && ( {/* {order.status === "pending" && (
<div className={style["order-actions"]}> <div className={style["order-actions"]}>
<button <button
className={`${style["action-btn"]} ${style["danger"]}`} className={`${style["action-btn"]} ${style["danger"]}`}
@@ -252,44 +421,21 @@ const RechargeOrders: React.FC = () => {
取消订单 取消订单
</button> </button>
</div> </div>
)} )} */}
{order.status === "success" && ( {actions.length > 0 && (
<div className={style["order-actions"]}> <div className={style["order-actions"]}>{actions}</div>
<button
className={`${style["action-btn"]} ${style["secondary"]}`}
onClick={() => navigate(`/recharge/order/${order.id}`)}
>
</button>
<button
className={`${style["action-btn"]} ${style["primary"]}`}
onClick={() => handleRefundOrder(order.id)}
>
退
</button>
</div>
)}
{order.status === "failed" && (
<div className={style["order-actions"]}>
<button
className={`${style["action-btn"]} ${style["primary"]}`}
onClick={() => navigate("/recharge")}
>
</button>
</div>
)} )}
</Card> </Card>
); );
};
const filterTabs = [ const filterTabs = [
{ key: "all", label: "全部" }, { key: "all", label: "全部" },
{ key: "success", label: "成功" }, { key: "1", label: "成功" },
{ key: "pending", label: "处理中" }, { key: "0", label: "待支付" },
{ key: "failed", label: "失败" }, { key: "2", label: "已取消" },
{ key: "cancelled", label: "已取消" }, { key: "3", label: "已退款" },
]; ];
return ( return (

View File

@@ -197,7 +197,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
// 下载模板 // 下载模板
const handleDownloadTemplate = () => { const handleDownloadTemplate = () => {
const template = 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 blob = new Blob([template], { type: "text/csv" });
const url = window.URL.createObjectURL(blob); const url = window.URL.createObjectURL(blob);
const a = document.createElement("a"); const a = document.createElement("a");

View File

@@ -44,7 +44,7 @@ const AppRouter: React.FC = () => (
}} }}
> >
<AppRoutes /> <AppRoutes />
<FloatingVideoHelp /> {/* <FloatingVideoHelp /> */}
</BrowserRouter> </BrowserRouter>
); );

View File

@@ -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 WechatAccountDetail from "@/pages/mobile/mine/wechat-accounts/detail/index";
import Recharge from "@/pages/mobile/mine/recharge/index"; import Recharge from "@/pages/mobile/mine/recharge/index";
import RechargeOrder from "@/pages/mobile/mine/recharge/order/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 BuyPower from "@/pages/mobile/mine/recharge/buy-power";
import UsageRecords from "@/pages/mobile/mine/recharge/usage-records"; import UsageRecords from "@/pages/mobile/mine/recharge/usage-records";
import Setting from "@/pages/mobile/mine/setting/index"; import Setting from "@/pages/mobile/mine/setting/index";
@@ -76,6 +77,11 @@ const routes = [
element: <RechargeOrder />, element: <RechargeOrder />,
auth: true, auth: true,
}, },
{
path: "/recharge/order/:id",
element: <RechargeOrderDetail />,
auth: true,
},
{ {
path: "/recharge/buy-power", path: "/recharge/buy-power",
element: <BuyPower />, element: <BuyPower />,