重构充值订单管理:更新OrderList接口以支持灵活的数据类型,增强订单状态处理,并改进订单记录的UI交互。实现付款连续逻辑,并通过更好的状态表示来优化订单详细信息呈现。
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
328
Cunkebao/src/pages/mobile/mine/recharge/order/detail/index.tsx
Normal file
328
Cunkebao/src/pages/mobile/mine/recharge/order/detail/index.tsx
Normal 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;
|
||||
@@ -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 (
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -44,7 +44,7 @@ const AppRouter: React.FC = () => (
|
||||
}}
|
||||
>
|
||||
<AppRoutes />
|
||||
<FloatingVideoHelp />
|
||||
{/* <FloatingVideoHelp /> */}
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
|
||||
@@ -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 />,
|
||||
|
||||
Reference in New Issue
Block a user