Files
wzdj/app/admin/orders/client.tsx

275 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useState, useEffect } from "react"
import Link from "next/link"
import {
Search,
MoreVertical,
Eye,
RefreshCw,
CheckCircle,
XCircle,
Clock,
Package,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { getOrders, updateOrderStatus, type AdminOrder } from "@/lib/api/admin"
const typeLabels: Record<string, string> = {
product: "商品",
service: "服务",
recharge: "充值",
hotel: "酒店",
cafe: "网咖",
course: "课程",
pawn: "典当",
pointCard: "点卡",
}
export default function AdminOrdersClient() {
const [orders, setOrders] = useState<AdminOrder[]>([])
const [searchTerm, setSearchTerm] = useState("")
const [statusFilter, setStatusFilter] = useState("all")
const [typeFilter, setTypeFilter] = useState("all")
const [loading, setLoading] = useState(true)
useEffect(() => {
loadOrders()
}, [])
const loadOrders = async () => {
setLoading(true)
try {
const list = await getOrders()
setOrders(list)
} catch (error) {
console.error("Failed to load orders:", error)
}
setLoading(false)
}
const filteredOrders = orders.filter((order) => {
const matchesSearch =
order.orderNo?.toLowerCase().includes(searchTerm.toLowerCase()) ||
order.itemName?.toLowerCase().includes(searchTerm.toLowerCase())
const matchesStatus = statusFilter === "all" || order.status === statusFilter
const matchesType = typeFilter === "all" || order.type === typeFilter
return matchesSearch && matchesStatus && matchesType
})
const updateOrderStatusHandler = async (order: AdminOrder, status: string) => {
try {
await updateOrderStatus(order.id, order.sourceTable, status)
await loadOrders()
} catch (e) {
console.error("update order status error:", e)
alert("更新失败")
}
}
const getStatusBadge = (status: string) => {
switch (status) {
case "completed":
case "paid":
case "delivered":
return <Badge className="bg-emerald-500/20 text-emerald-400"></Badge>
case "pending":
return <Badge className="bg-amber-500/20 text-amber-400"></Badge>
case "cancelled":
case "rejected":
return <Badge className="bg-red-500/20 text-red-400"></Badge>
case "refunded":
return <Badge className="bg-purple-500/20 text-purple-400">退</Badge>
default:
return <Badge className="bg-slate-100 text-slate-600">{status}</Badge>
}
}
return (
<div className="p-4 lg:p-6 max-w-[1600px] mx-auto">
<div className="mb-6">
<h1 className="text-2xl font-bold text-slate-800"></h1>
<p className="text-sm text-slate-500 mt-1">///// MongoDB</p>
</div>
<Card className="admin-glass rounded-2xl overflow-hidden mb-6 border border-slate-200/80 shadow-lg">
<CardContent className="p-4">
<div className="flex flex-wrap gap-4">
<div className="relative flex-1 min-w-[200px]">
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" />
<Input
placeholder="搜索订单号/商品名..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="bg-white/90 border-slate-200 text-slate-800 pl-10"
/>
</div>
<Select value={typeFilter} onValueChange={setTypeFilter}>
<SelectTrigger className="w-[130px] border-slate-200 text-slate-800">
<SelectValue placeholder="订单类型" />
</SelectTrigger>
<SelectContent className="bg-white border-slate-200">
<SelectItem value="all"></SelectItem>
{Object.entries(typeLabels).map(([v, l]) => (
<SelectItem key={v} value={v}>{l}</SelectItem>
))}
</SelectContent>
</Select>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[130px] border-slate-200 text-slate-800">
<SelectValue placeholder="订单状态" />
</SelectTrigger>
<SelectContent className="bg-white border-slate-200">
<SelectItem value="all"></SelectItem>
<SelectItem value="pending"></SelectItem>
<SelectItem value="completed"></SelectItem>
<SelectItem value="paid"></SelectItem>
<SelectItem value="cancelled"></SelectItem>
<SelectItem value="refunded">退</SelectItem>
</SelectContent>
</Select>
<Button variant="outline" onClick={loadOrders} className="border-slate-200 text-slate-600 hover:bg-slate-50">
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
</div>
</CardContent>
</Card>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<Card className="admin-glass rounded-2xl overflow-hidden border border-slate-200/80 shadow-lg">
<CardContent className="p-4 text-center">
<p className="text-2xl font-bold text-slate-800">{orders.length}</p>
<p className="text-slate-500 text-sm"></p>
</CardContent>
</Card>
<Card className="admin-glass rounded-2xl overflow-hidden border border-slate-200/80 shadow-lg">
<CardContent className="p-4 text-center">
<p className="text-2xl font-bold text-amber-600">
{orders.filter((o) => o.status === "pending").length}
</p>
<p className="text-slate-500 text-sm"></p>
</CardContent>
</Card>
<Card className="admin-glass rounded-2xl overflow-hidden border border-slate-200/80 shadow-lg">
<CardContent className="p-4 text-center">
<p className="text-2xl font-bold text-emerald-600">
{orders.filter((o) => ["completed", "paid", "delivered"].includes(o.status)).length}
</p>
<p className="text-slate-500 text-sm"></p>
</CardContent>
</Card>
<Card className="admin-glass rounded-2xl overflow-hidden border border-slate-200/80 shadow-lg">
<CardContent className="p-4 text-center">
<p className="text-2xl font-bold text-indigo-600">
{orders.reduce((sum, o) => sum + o.amount, 0).toLocaleString()}
</p>
<p className="text-slate-500 text-sm"></p>
</CardContent>
</Card>
</div>
<Card className="admin-glass rounded-2xl overflow-hidden border border-slate-200/80 shadow-lg">
<CardHeader>
<CardTitle className="text-slate-800"> ({filteredOrders.length})</CardTitle>
</CardHeader>
<CardContent>
{loading ? (
<div className="text-center py-8 text-slate-500">...</div>
) : filteredOrders.length === 0 ? (
<div className="text-center py-8 text-slate-500"></div>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-slate-200">
<th className="text-left text-slate-600 text-sm font-medium py-3 px-4"></th>
<th className="text-left text-slate-600 text-sm font-medium py-3 px-4"></th>
<th className="text-left text-slate-600 text-sm font-medium py-3 px-4"></th>
<th className="text-left text-slate-600 text-sm font-medium py-3 px-4"></th>
<th className="text-left text-slate-600 text-sm font-medium py-3 px-4"></th>
<th className="text-left text-slate-600 text-sm font-medium py-3 px-4"></th>
<th className="text-left text-slate-600 text-sm font-medium py-3 px-4"></th>
</tr>
</thead>
<tbody>
{filteredOrders.map((order) => (
<tr key={`${order.sourceTable}-${order.id}`} className="border-b border-slate-100 hover:bg-slate-50/80">
<td className="py-3 px-4 text-indigo-600 font-mono text-sm">{order.orderNo}</td>
<td className="py-3 px-4">
<div className="flex items-center gap-2">
<Package className="w-4 h-4 text-slate-500" />
<span className="text-slate-800">{order.itemName}</span>
</div>
</td>
<td className="py-3 px-4">
<Badge variant="outline" className="border-slate-200 text-slate-600">
{typeLabels[order.type] ?? order.type}
</Badge>
</td>
<td className="py-3 px-4 text-amber-600 font-medium">{order.amount} </td>
<td className="py-3 px-4">{getStatusBadge(order.status)}</td>
<td className="py-3 px-4 text-slate-500 text-sm">
{order.createdAt ? new Date(order.createdAt).toLocaleString("zh-CN") : "-"}
</td>
<td className="py-3 px-4">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="text-slate-500">
<MoreVertical className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="bg-white border-slate-200">
<DropdownMenuItem asChild className="text-slate-700">
<Link href={`/admin/orders/${order.id}?sourceTable=${order.sourceTable}`}>
<Eye className="w-4 h-4 mr-2" />
</Link>
</DropdownMenuItem>
{order.status === "pending" && (
<>
<DropdownMenuItem
className="text-emerald-600"
onClick={() => updateOrderStatusHandler(order, "completed")}
>
<CheckCircle className="w-4 h-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem
className="text-red-600"
onClick={() => updateOrderStatusHandler(order, "cancelled")}
>
<XCircle className="w-4 h-4 mr-2" />
</DropdownMenuItem>
</>
)}
{["completed", "paid"].includes(order.status) && (
<DropdownMenuItem
className="text-purple-600"
onClick={() => updateOrderStatusHandler(order, "refunded")}
>
<Clock className="w-4 h-4 mr-2" />
退
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
</div>
)
}