重构充值订单管理:更新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;
}
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;

View File

@@ -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 {

View File

@@ -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<React.ComponentProps<typeof Tag>["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<Statistics | null>(null);
const [records, setRecords] = useState<OrderRecordView[]>([]);
const [records, setRecords] = useState<OrderList[]>([]);
const [filterType, setFilterType] = useState<string>("all");
const [filterStatus, setFilterStatus] = useState<string>("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<number, { label: string; color: TagColor }> = {
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<string, string | undefined> = {
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 = () => {
</div>
);
// 渲染消费记录Tab
// 渲染订单记录Tab
const renderRecords = () => (
<div className={style.recordsContent}>
{/* 筛选器 */}
@@ -273,42 +323,72 @@ const PowerManagement: React.FC = () => {
</Picker>
</div>
{/* 消费记录列表 */}
{/* 订单记录列表 */}
<div className={style.recordList}>
{loading && records.length === 0 ? (
<div className={style.loadingContainer}>
<div className={style.loadingText}>...</div>
</div>
) : records.length > 0 ? (
records.map(record => (
<Card key={record.id} className={style.recordItem}>
<div className={style.recordHeader}>
<div className={style.recordLeft}>
<div className={style.recordType}>{record.type}</div>
<Tag
color={record.status === "已完成" ? "success" : "primary"}
className={style.recordStatus}
>
{record.status}
</Tag>
</div>
<div className={style.recordRight}>
<div className={style.recordAmount}>
-¥{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 (
<Card
key={record.id ?? record.orderNo}
className={style.recordItem}
onClick={() =>
record.orderNo &&
navigate(`/recharge/order/${record.orderNo}`)
}
>
<div className={style.recordHeader}>
<div className={style.recordLeft}>
<div className={style.recordType}>
{record.goodsName || "算力充值"}
</div>
<Tag color={tagColor} className={style.recordStatus}>
{tagLabel}
</Tag>
</div>
<div className={style.recordPower}>
{formatNumber(record.power)}
<div className={style.recordRight}>
<div className={style.recordAmount}>
-¥{amount.toFixed(2)}
</div>
<div className={style.recordPower}>
{formatNumber(power)}
</div>
</div>
</div>
</div>
<div className={style.recordDesc}>{record.description}</div>
<div className={style.recordTime}>{record.createTime}</div>
</Card>
))
<div className={style.recordDesc}>{description}</div>
<div className={style.recordTime}>{createTime}</div>
</Card>
);
})
) : (
<div className={style.emptyRecords}>
<div className={style.emptyIcon}>📋</div>
<div className={style.emptyText}></div>
<div className={style.emptyText}></div>
</div>
)}
</div>
@@ -334,7 +414,7 @@ const PowerManagement: React.FC = () => {
className={style.powerTabs}
>
<Tabs.Tab title="概览" key="overview" />
<Tabs.Tab title="消费记录" key="records" />
<Tabs.Tab title="订单记录" key="records" />
</Tabs>
</>
}

View File

@@ -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<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 getRechargeOrders(params: RechargeOrderParams) {
return request("/v1/tokens/orderList", params, "GET");
}
// 获取充值记录详情
export async function getRechargeOrderDetail(
id: string,
params: GetRechargeOrderDetailParams,
): Promise<RechargeOrderDetail> {
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<void> {
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<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;
export interface ContinuePayResponse {
code_url?: string;
codeUrl?: string;
payUrl?: string;
[property: string]: any;
}
// 继续支付
export function continuePay(
params: ContinuePayParams,
): Promise<ContinuePayResponse> {
return request("/v1/tokens/pay", params, "POST");
}

View File

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

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 { 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<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 navigate = useNavigate();
const [orders, setOrders] = useState<RechargeOrder[]>([]);
@@ -25,6 +121,7 @@ const RechargeOrders: React.FC = () => {
const [hasMore, setHasMore] = useState(true);
const [page, setPage] = useState(1);
const [statusFilter, setStatusFilter] = useState<string>("all");
const [payingOrderNo, setPayingOrderNo] = useState<string | null>(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: (
<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) => {
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(
<button
key="retry"
className={`${style["action-btn"]} ${style["primary"]}`}
onClick={() => navigate("/recharge")}
>
</button>,
);
}
};
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(
<button
key="purchase-again"
className={`${style["action-btn"]} ${style["secondary"]}`}
onClick={() => navigate("/recharge")}
>
</button>,
);
}
};
const renderOrderItem = (order: RechargeOrder) => (
<Card key={order.id} className={style["order-card"]}>
<div className={style["order-header"]}>
<div className={style["order-info"]}>
<div className={style["order-no"]}>{order.orderNo}</div>
<div className={style["order-time"]}>
<ClockCircleOutlined style={{ fontSize: 12 }} />
{formatTime(order.createTime)}
actions.push(
<button
key="detail"
className={`${style["action-btn"]} ${style["secondary"]}`}
onClick={() => handleViewDetail(order)}
>
</button>,
);
return (
<Card key={order.id} className={style["order-card"]}>
<div className={style["order-header"]}>
<div className={style["order-info"]}>
<div className={style["order-no"]}>
{order.orderNo || "-"}
</div>
<div className={style["order-time"]}>
<ClockCircleOutlined style={{ fontSize: 12 }} />
{formatTimestamp(order.createTime)}
</div>
</div>
<div className={style["order-amount"]}>
<div className={style["amount-text"]}>{amount.toFixed(2)}</div>
<div
className={style["status-tag"]}
style={{
backgroundColor: statusBgColor,
color: statusMeta?.color || "#666",
}}
>
{statusMeta?.label || "未知状态"}
</div>
</div>
</div>
<div className={style["order-amount"]}>
<div className={style["amount-text"]}>
{order.amount.toFixed(2)}
</div>
<div
className={style["status-tag"]}
style={{
backgroundColor: `${getStatusColor(order.status)}20`,
color: getStatusColor(order.status),
}}
>
{getStatusText(order.status)}
</div>
</div>
</div>
<div className={style["order-details"]}>
<div className={style["payment-method"]}>
<div
className={style["method-icon"]}
style={{
backgroundColor: getPaymentMethodColor(order.paymentMethod),
}}
>
{getPaymentMethodIcon(order.paymentMethod)}
<div className={style["order-details"]}>
<div className={style["payment-method"]}>
<div
className={style["method-icon"]}
style={{
backgroundColor: getPaymentMethodColor(paymentMethodKey),
}}
>
{getPaymentMethodIcon(paymentMethod)}
</div>
<div className={style["method-text"]}>{paymentMethod}</div>
</div>
<div className={style["method-text"]}>{order.paymentMethod}</div>
{(order.description || order.remark) && (
<div className={style["detail-row"]}>
<span className={style["label"]}></span>
<span className={style["value"]}>
{order.description || order.remark}
</span>
</div>
)}
{order.payTime && (
<div className={style["detail-row"]}>
<span className={style["label"]}></span>
<span className={style["value"]}>
{formatTimestamp(order.payTime)}
</span>
</div>
)}
{order.balance !== undefined && (
<div className={style["balance-info"]}>
: {order.balance.toFixed(2)}
</div>
)}
</div>
{order.description && (
<div className={style["detail-row"]}>
<span className={style["label"]}></span>
<span className={style["value"]}>{order.description}</span>
</div>
)}
{order.payTime && (
<div className={style["detail-row"]}>
<span className={style["label"]}></span>
<span className={style["value"]}>{formatTime(order.payTime)}</span>
</div>
)}
{order.balance !== undefined && (
<div className={style["balance-info"]}>
: {order.balance.toFixed(2)}
</div>
)}
</div>
{order.status === "pending" && (
{/* {order.status === "pending" && (
<div className={style["order-actions"]}>
<button
className={`${style["action-btn"]} ${style["danger"]}`}
@@ -252,44 +421,21 @@ const RechargeOrders: React.FC = () => {
取消订单
</button>
</div>
)}
)} */}
{order.status === "success" && (
<div className={style["order-actions"]}>
<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>
);
{actions.length > 0 && (
<div className={style["order-actions"]}>{actions}</div>
)}
</Card>
);
};
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 (

View File

@@ -197,7 +197,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
// 下载模板
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");

View File

@@ -44,7 +44,7 @@ const AppRouter: React.FC = () => (
}}
>
<AppRoutes />
<FloatingVideoHelp />
{/* <FloatingVideoHelp /> */}
</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 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: <RechargeOrder />,
auth: true,
},
{
path: "/recharge/order/:id",
element: <RechargeOrderDetail />,
auth: true,
},
{
path: "/recharge/buy-power",
element: <BuyPower />,