160 lines
6.0 KiB
TypeScript
160 lines
6.0 KiB
TypeScript
'use client';
|
||
|
||
import { useState } from 'react';
|
||
import { useRouter } from 'next/navigation';
|
||
import { useOrders, useCancelOrder } from '@/hooks/use-orders';
|
||
import { Order, OrderStatusType } from '@/lib/order-api';
|
||
|
||
const STATUS_OPTIONS: { label: string; value?: OrderStatusType }[] = [
|
||
{ label: '全部' },
|
||
{ label: '待支付', value: 'PENDING' },
|
||
{ label: '已支付', value: 'PAID' },
|
||
{ label: '已发货', value: 'SHIPPED' },
|
||
{ label: '已完成', value: 'COMPLETED' },
|
||
{ label: '已取消', value: 'CANCELLED' },
|
||
{ label: '已退款', value: 'REFUNDED' },
|
||
];
|
||
|
||
const STATUS_LABEL_MAP: Record<OrderStatusType, string> = {
|
||
PENDING: '待支付',
|
||
PAID: '已支付',
|
||
SHIPPED: '已发货',
|
||
COMPLETED: '已完成',
|
||
CANCELLED: '已取消',
|
||
REFUNDED: '已退款',
|
||
};
|
||
|
||
const STATUS_STYLE_MAP: Record<OrderStatusType, string> = {
|
||
PENDING: 'bg-yellow-100 text-yellow-800',
|
||
PAID: 'bg-blue-100 text-blue-800',
|
||
SHIPPED: 'bg-purple-100 text-purple-800',
|
||
COMPLETED: 'bg-green-100 text-green-800',
|
||
CANCELLED: 'bg-gray-100 text-gray-800',
|
||
REFUNDED: 'bg-red-100 text-red-800',
|
||
};
|
||
|
||
export default function OrdersPage() {
|
||
const router = useRouter();
|
||
const [page, setPage] = useState(1);
|
||
const [statusFilter, setStatusFilter] = useState<OrderStatusType | undefined>();
|
||
const limit = 20;
|
||
|
||
const { data: ordersData, isLoading } = useOrders(page, limit, statusFilter);
|
||
const cancelOrder = useCancelOrder();
|
||
|
||
const orders = ordersData?.data?.orders || [];
|
||
const pagination = ordersData?.data?.pagination;
|
||
|
||
const handleCancel = async (id: string) => {
|
||
if (!confirm('确定要取消该订单吗?')) return;
|
||
try {
|
||
await cancelOrder.mutateAsync(id);
|
||
} catch {
|
||
alert('取消订单失败');
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="p-6 space-y-6">
|
||
<h1 className="text-2xl font-bold">订单管理</h1>
|
||
|
||
<div className="flex space-x-2">
|
||
{STATUS_OPTIONS.map((opt) => (
|
||
<button
|
||
key={opt.label}
|
||
onClick={() => {
|
||
setStatusFilter(opt.value);
|
||
setPage(1);
|
||
}}
|
||
className={`px-3 py-1.5 text-sm rounded-md border ${
|
||
statusFilter === opt.value
|
||
? 'bg-blue-600 text-white border-blue-600'
|
||
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
|
||
}`}
|
||
>
|
||
{opt.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{isLoading ? (
|
||
<div className="text-center py-8 text-gray-500">加载中...</div>
|
||
) : orders.length === 0 ? (
|
||
<div className="text-center py-8 text-gray-500">暂无订单</div>
|
||
) : (
|
||
<div className="border rounded-lg overflow-hidden">
|
||
<table className="w-full">
|
||
<thead className="bg-gray-50">
|
||
<tr>
|
||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-600">订单号</th>
|
||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-600">金额</th>
|
||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-600">状态</th>
|
||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-600">创建时间</th>
|
||
<th className="px-4 py-3 text-right text-sm font-medium text-gray-600">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y">
|
||
{orders.map((order: Order) => (
|
||
<tr
|
||
key={order.id}
|
||
className="hover:bg-gray-50 cursor-pointer"
|
||
onClick={() => router.push(`/orders/${order.id}`)}
|
||
>
|
||
<td className="px-4 py-3 text-sm font-medium">{order.orderNo}</td>
|
||
<td className="px-4 py-3 text-sm">¥{order.totalAmount.toFixed(2)}</td>
|
||
<td className="px-4 py-3">
|
||
<span className={`inline-flex px-2 py-1 text-xs rounded-full ${STATUS_STYLE_MAP[order.status as OrderStatusType] || 'bg-gray-100 text-gray-800'}`}>
|
||
{STATUS_LABEL_MAP[order.status as OrderStatusType] || order.status}
|
||
</span>
|
||
</td>
|
||
<td className="px-4 py-3 text-sm text-gray-500">
|
||
{new Date(order.createdAt).toLocaleString()}
|
||
</td>
|
||
<td className="px-4 py-3 text-right" onClick={(e) => e.stopPropagation()}>
|
||
{(order.status === 'PENDING' || order.status === 'PAID') && (
|
||
<button
|
||
onClick={() => handleCancel(order.id)}
|
||
disabled={cancelOrder.isPending}
|
||
className="text-sm text-red-600 hover:underline disabled:opacity-50"
|
||
>
|
||
取消订单
|
||
</button>
|
||
)}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
|
||
{pagination && pagination.totalPages > 1 && (
|
||
<div className="flex justify-between items-center">
|
||
<p className="text-sm text-gray-500">
|
||
第 {(pagination.page - 1) * pagination.limit + 1} - {Math.min(pagination.page * pagination.limit, pagination.total)} 条,共 {pagination.total} 条
|
||
</p>
|
||
<div className="flex space-x-2">
|
||
<button
|
||
onClick={() => setPage(page - 1)}
|
||
disabled={page === 1}
|
||
className="px-3 py-1 border rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
上一页
|
||
</button>
|
||
<span className="px-3 py-1">
|
||
第 {pagination.page} / {pagination.totalPages} 页
|
||
</span>
|
||
<button
|
||
onClick={() => setPage(page + 1)}
|
||
disabled={page === pagination.totalPages}
|
||
className="px-3 py-1 border rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
下一页
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|