2025.11.27.17.50
This commit is contained in:
232
src/pages/admin/orders/index.tsx
Normal file
232
src/pages/admin/orders/index.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import AdminLayout from '@/components/admin/AdminLayout';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Loader2, Search, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
import { zhCN } from 'date-fns/locale';
|
||||
|
||||
export default function OrderList() {
|
||||
const [orders, setOrders] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [pagination, setPagination] = useState({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0,
|
||||
pages: 0
|
||||
});
|
||||
const [filters, setFilters] = useState({
|
||||
status: 'all',
|
||||
search: ''
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchOrders();
|
||||
}, [pagination.page, filters]);
|
||||
|
||||
const fetchOrders = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const query = new URLSearchParams({
|
||||
page: pagination.page.toString(),
|
||||
limit: pagination.limit.toString(),
|
||||
status: filters.status,
|
||||
search: filters.search
|
||||
});
|
||||
|
||||
const res = await fetch(`/api/admin/orders?${query}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setOrders(data.orders);
|
||||
setPagination(data.pagination);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch orders', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setPagination(prev => ({ ...prev, page: 1 }));
|
||||
fetchOrders(); // Trigger fetch immediately or let useEffect handle it if search state changed
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'paid':
|
||||
return <Badge className="bg-green-500">已支付</Badge>;
|
||||
case 'pending':
|
||||
return <Badge variant="outline" className="text-yellow-600 border-yellow-600">待支付</Badge>;
|
||||
default:
|
||||
return <Badge variant="outline">{status}</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
const getOrderTypeLabel = (type: string) => {
|
||||
const map: Record<string, string> = {
|
||||
'buy_membership': '购买会员',
|
||||
'buy_resource': '购买资源',
|
||||
'recharge_points': '充值积分'
|
||||
};
|
||||
return map[type] || type;
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminLayout>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold tracking-tight">订单管理</h1>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 bg-white p-4 rounded-lg border border-gray-100 shadow-sm">
|
||||
<div className="w-full sm:w-48">
|
||||
<Select
|
||||
value={filters.status}
|
||||
onValueChange={(value) => setFilters(prev => ({ ...prev, status: value, page: 1 }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="订单状态" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部状态</SelectItem>
|
||||
<SelectItem value="paid">已支付</SelectItem>
|
||||
<SelectItem value="pending">待支付</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<form onSubmit={handleSearch} className="flex-1 flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-500" />
|
||||
<Input
|
||||
placeholder="搜索订单号、用户名或邮箱..."
|
||||
className="pl-9"
|
||||
value={filters.search}
|
||||
onChange={(e) => setFilters(prev => ({ ...prev, search: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit">搜索</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="bg-white rounded-lg border border-gray-100 shadow-sm overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>订单号</TableHead>
|
||||
<TableHead>用户</TableHead>
|
||||
<TableHead>商品信息</TableHead>
|
||||
<TableHead>金额</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>支付方式</TableHead>
|
||||
<TableHead>创建时间</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="h-24 text-center">
|
||||
<div className="flex justify-center">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-primary" />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : orders.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="h-24 text-center text-gray-500">
|
||||
暂无订单数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
orders.map((order) => (
|
||||
<TableRow key={order._id}>
|
||||
<TableCell className="font-mono text-xs">{order.订单号}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-full bg-gray-100 overflow-hidden">
|
||||
<img
|
||||
src={order.用户ID?.头像 || '/images/default_avatar.png'}
|
||||
alt="Avatar"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium">{order.用户ID?.用户名 || '未知用户'}</span>
|
||||
<span className="text-xs text-gray-500">{order.用户ID?.邮箱}</span>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{getOrderTypeLabel(order.订单类型)}</span>
|
||||
<span className="text-xs text-gray-500">{order.商品快照?.标题}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="font-bold text-gray-900">
|
||||
¥{order.支付金额.toFixed(2)}
|
||||
</TableCell>
|
||||
<TableCell>{getStatusBadge(order.订单状态)}</TableCell>
|
||||
<TableCell>
|
||||
{order.支付方式 === 'alipay' && <span className="text-blue-500 text-sm">支付宝</span>}
|
||||
{order.支付方式 === 'wechat' && <span className="text-green-500 text-sm">微信支付</span>}
|
||||
{!order.支付方式 && <span className="text-gray-400 text-sm">-</span>}
|
||||
</TableCell>
|
||||
<TableCell className="text-gray-500 text-sm">
|
||||
{format(new Date(order.createdAt), 'yyyy-MM-dd HH:mm', { locale: zhCN })}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-gray-500">
|
||||
共 {pagination.total} 条记录,当前第 {pagination.page} / {pagination.pages} 页
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPagination(prev => ({ ...prev, page: prev.page - 1 }))}
|
||||
disabled={pagination.page <= 1}
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
上一页
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPagination(prev => ({ ...prev, page: prev.page + 1 }))}
|
||||
disabled={pagination.page >= pagination.pages}
|
||||
>
|
||||
下一页
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user