275 lines
12 KiB
TypeScript
275 lines
12 KiB
TypeScript
"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>
|
||
)
|
||
}
|