2025.11.27.17.50

This commit is contained in:
RUI
2025-11-27 17:50:44 +08:00
commit 5dbb30b32c
111 changed files with 18320 additions and 0 deletions

View 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>
);
}