2026-03-01 03:04:24 +08:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { useSearchParams } from 'next/navigation';
|
|
|
|
|
import { useState, useEffect, useCallback, Suspense } from 'react';
|
|
|
|
|
import OrderTable from '@/components/admin/OrderTable';
|
|
|
|
|
import OrderDetail from '@/components/admin/OrderDetail';
|
2026-03-01 20:12:32 +08:00
|
|
|
import PaginationBar from '@/components/PaginationBar';
|
2026-03-01 03:04:24 +08:00
|
|
|
|
2026-03-01 17:58:08 +08:00
|
|
|
interface AdminOrder {
|
|
|
|
|
id: string;
|
|
|
|
|
userId: number;
|
|
|
|
|
userName: string | null;
|
|
|
|
|
userEmail: string | null;
|
|
|
|
|
amount: number;
|
|
|
|
|
status: string;
|
|
|
|
|
paymentType: string;
|
|
|
|
|
createdAt: string;
|
|
|
|
|
paidAt: string | null;
|
|
|
|
|
completedAt: string | null;
|
|
|
|
|
failedReason: string | null;
|
|
|
|
|
expiresAt: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface AdminOrderDetail extends AdminOrder {
|
|
|
|
|
rechargeCode: string;
|
|
|
|
|
paymentTradeNo: string | null;
|
|
|
|
|
refundAmount: number | null;
|
|
|
|
|
refundReason: string | null;
|
|
|
|
|
refundAt: string | null;
|
|
|
|
|
forceRefund: boolean;
|
|
|
|
|
failedAt: string | null;
|
|
|
|
|
updatedAt: string;
|
|
|
|
|
clientIp: string | null;
|
2026-03-02 20:40:16 +08:00
|
|
|
srcHost: string | null;
|
|
|
|
|
srcUrl: string | null;
|
2026-03-01 17:58:08 +08:00
|
|
|
paymentSuccess?: boolean;
|
|
|
|
|
rechargeSuccess?: boolean;
|
|
|
|
|
rechargeStatus?: string;
|
|
|
|
|
auditLogs: { id: string; action: string; detail: string | null; operator: string | null; createdAt: string }[];
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-01 03:04:24 +08:00
|
|
|
function AdminContent() {
|
|
|
|
|
const searchParams = useSearchParams();
|
|
|
|
|
const token = searchParams.get('token');
|
|
|
|
|
|
2026-03-01 17:58:08 +08:00
|
|
|
const [orders, setOrders] = useState<AdminOrder[]>([]);
|
2026-03-01 03:04:24 +08:00
|
|
|
const [total, setTotal] = useState(0);
|
|
|
|
|
const [page, setPage] = useState(1);
|
2026-03-01 20:04:49 +08:00
|
|
|
const [pageSize, setPageSize] = useState(20);
|
2026-03-01 03:04:24 +08:00
|
|
|
const [totalPages, setTotalPages] = useState(1);
|
|
|
|
|
const [statusFilter, setStatusFilter] = useState('');
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
const [error, setError] = useState('');
|
|
|
|
|
|
2026-03-01 17:58:08 +08:00
|
|
|
const [detailOrder, setDetailOrder] = useState<AdminOrderDetail | null>(null);
|
2026-03-01 03:04:24 +08:00
|
|
|
|
|
|
|
|
const fetchOrders = useCallback(async () => {
|
|
|
|
|
if (!token) return;
|
|
|
|
|
setLoading(true);
|
|
|
|
|
try {
|
2026-03-01 20:04:49 +08:00
|
|
|
const params = new URLSearchParams({ token, page: String(page), page_size: String(pageSize) });
|
2026-03-01 03:04:24 +08:00
|
|
|
if (statusFilter) params.set('status', statusFilter);
|
|
|
|
|
|
|
|
|
|
const res = await fetch(`/api/admin/orders?${params}`);
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
if (res.status === 401) {
|
|
|
|
|
setError('管理员凭证无效');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
throw new Error('请求失败');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
setOrders(data.orders);
|
|
|
|
|
setTotal(data.total);
|
|
|
|
|
setTotalPages(data.total_pages);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
setError('加载订单列表失败');
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
2026-03-01 20:04:49 +08:00
|
|
|
}, [token, page, pageSize, statusFilter]);
|
2026-03-01 03:04:24 +08:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
fetchOrders();
|
|
|
|
|
}, [fetchOrders]);
|
|
|
|
|
|
|
|
|
|
if (!token) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex min-h-screen items-center justify-center">
|
|
|
|
|
<div className="text-red-500">缺少管理员凭证</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleRetry = async (orderId: string) => {
|
|
|
|
|
if (!confirm('确认重试充值?')) return;
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`/api/admin/orders/${orderId}/retry?token=${token}`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
});
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
fetchOrders();
|
|
|
|
|
} else {
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
setError(data.error || '重试失败');
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
setError('重试请求失败');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleCancel = async (orderId: string) => {
|
|
|
|
|
if (!confirm('确认取消该订单?')) return;
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`/api/admin/orders/${orderId}/cancel?token=${token}`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
});
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
fetchOrders();
|
|
|
|
|
} else {
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
setError(data.error || '取消失败');
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
setError('取消请求失败');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleViewDetail = async (orderId: string) => {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`/api/admin/orders/${orderId}?token=${token}`);
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
setDetailOrder(data);
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
setError('加载订单详情失败');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const statuses = ['', 'PENDING', 'PAID', 'RECHARGING', 'COMPLETED', 'EXPIRED', 'CANCELLED', 'FAILED', 'REFUNDED'];
|
|
|
|
|
const statusLabels: Record<string, string> = {
|
|
|
|
|
'': '全部',
|
|
|
|
|
PENDING: '待支付',
|
|
|
|
|
PAID: '已支付',
|
|
|
|
|
RECHARGING: '充值中',
|
|
|
|
|
COMPLETED: '已完成',
|
|
|
|
|
EXPIRED: '已超时',
|
|
|
|
|
CANCELLED: '已取消',
|
|
|
|
|
FAILED: '充值失败',
|
|
|
|
|
REFUNDED: '已退款',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="mx-auto min-h-screen max-w-6xl p-4">
|
2026-03-01 14:17:18 +08:00
|
|
|
<div className="mb-6 flex items-center justify-between">
|
|
|
|
|
<h1 className="text-2xl font-bold text-gray-900">Sub2ApiPay 订单管理</h1>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={fetchOrders}
|
|
|
|
|
className="rounded-lg border border-gray-300 px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-100"
|
|
|
|
|
>
|
|
|
|
|
刷新
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2026-03-01 03:04:24 +08:00
|
|
|
|
|
|
|
|
{error && (
|
|
|
|
|
<div className="mb-4 rounded-lg bg-red-50 p-3 text-sm text-red-600">
|
|
|
|
|
{error}
|
2026-03-01 17:58:08 +08:00
|
|
|
<button onClick={() => setError('')} className="ml-2 text-red-400 hover:text-red-600">
|
|
|
|
|
✕
|
|
|
|
|
</button>
|
2026-03-01 03:04:24 +08:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Filters */}
|
|
|
|
|
<div className="mb-4 flex flex-wrap gap-2">
|
|
|
|
|
{statuses.map((s) => (
|
|
|
|
|
<button
|
|
|
|
|
key={s}
|
2026-03-01 17:58:08 +08:00
|
|
|
onClick={() => {
|
|
|
|
|
setStatusFilter(s);
|
|
|
|
|
setPage(1);
|
|
|
|
|
}}
|
2026-03-01 03:04:24 +08:00
|
|
|
className={`rounded-full px-3 py-1 text-sm transition-colors ${
|
2026-03-01 17:58:08 +08:00
|
|
|
statusFilter === s ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
2026-03-01 03:04:24 +08:00
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{statusLabels[s]}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Table */}
|
|
|
|
|
<div className="rounded-xl bg-white shadow-sm">
|
|
|
|
|
{loading ? (
|
|
|
|
|
<div className="py-12 text-center text-gray-500">加载中...</div>
|
|
|
|
|
) : (
|
2026-03-01 17:58:08 +08:00
|
|
|
<OrderTable orders={orders} onRetry={handleRetry} onCancel={handleCancel} onViewDetail={handleViewDetail} />
|
2026-03-01 03:04:24 +08:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-01 20:12:32 +08:00
|
|
|
<PaginationBar
|
|
|
|
|
page={page}
|
|
|
|
|
totalPages={totalPages}
|
|
|
|
|
total={total}
|
|
|
|
|
pageSize={pageSize}
|
|
|
|
|
loading={loading}
|
|
|
|
|
onPageChange={(p) => setPage(p)}
|
|
|
|
|
onPageSizeChange={(s) => { setPageSize(s); setPage(1); }}
|
|
|
|
|
/>
|
2026-03-01 03:04:24 +08:00
|
|
|
|
|
|
|
|
{/* Order Detail */}
|
2026-03-01 17:58:08 +08:00
|
|
|
{detailOrder && <OrderDetail order={detailOrder} onClose={() => setDetailOrder(null)} />}
|
2026-03-01 03:04:24 +08:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function AdminPage() {
|
|
|
|
|
return (
|
2026-03-01 17:58:08 +08:00
|
|
|
<Suspense
|
|
|
|
|
fallback={
|
|
|
|
|
<div className="flex min-h-screen items-center justify-center">
|
|
|
|
|
<div className="text-gray-500">加载中...</div>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
>
|
2026-03-01 03:04:24 +08:00
|
|
|
<AdminContent />
|
|
|
|
|
</Suspense>
|
|
|
|
|
);
|
|
|
|
|
}
|