185 lines
8.2 KiB
TypeScript
185 lines
8.2 KiB
TypeScript
"use client"
|
||
|
||
import { useState, useEffect } from "react"
|
||
import { Search, RefreshCw, Package, FileText, Download } from "lucide-react"
|
||
import { Input } from "@/components/ui/input"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||
import { Badge } from "@/components/ui/badge"
|
||
|
||
type Product = { id: string; game?: string; name?: string; rankFrom?: string; rankTo?: string; price?: number; status?: string }
|
||
type Order = { id: string; productId?: string; status?: string; amount?: number; createdAt?: string }
|
||
|
||
export default function AdminLevelingClient() {
|
||
const [products, setProducts] = useState<Product[]>([])
|
||
const [orders, setOrders] = useState<Order[]>([])
|
||
const [searchTerm, setSearchTerm] = useState("")
|
||
const [loading, setLoading] = useState(true)
|
||
const [syncing, setSyncing] = useState(false)
|
||
|
||
useEffect(() => {
|
||
loadData()
|
||
}, [])
|
||
|
||
const loadData = async () => {
|
||
setLoading(true)
|
||
try {
|
||
const [pRes, oRes] = await Promise.all([
|
||
fetch("/api/admin/leveling/products"),
|
||
fetch("/api/admin/leveling/orders"),
|
||
])
|
||
const pJson = await pRes.json()
|
||
const oJson = await oRes.json()
|
||
setProducts(pJson.success && Array.isArray(pJson.data) ? pJson.data : [])
|
||
setOrders(oJson.success && Array.isArray(oJson.data) ? oJson.data : [])
|
||
} catch (e) {
|
||
console.error(e)
|
||
setProducts([])
|
||
setOrders([])
|
||
}
|
||
setLoading(false)
|
||
}
|
||
|
||
const syncDailian = async () => {
|
||
setSyncing(true)
|
||
try {
|
||
await fetch("/api/admin/leveling/sync-dailian", { method: "POST" })
|
||
loadData()
|
||
} finally {
|
||
setSyncing(false)
|
||
}
|
||
}
|
||
|
||
const filtered = products.filter(
|
||
(p) =>
|
||
(p.game ?? "").toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||
(p.name ?? "").toLowerCase().includes(searchTerm.toLowerCase()),
|
||
)
|
||
|
||
return (
|
||
<div className="p-4 lg:p-6 max-w-[1600px] mx-auto">
|
||
<h1 className="text-xl lg:text-2xl font-bold text-slate-800 mb-6">代练管理</h1>
|
||
<p className="text-slate-500 text-sm mb-4">代练套餐与订单(MongoDB levelingProducts / levelingOrders),可对接代练通等平台</p>
|
||
|
||
<Card className="admin-glass rounded-2xl overflow-hidden mb-6 border border-slate-200/80 shadow-lg">
|
||
<CardContent className="p-4 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>
|
||
<Button variant="outline" onClick={loadData} className="border-slate-200 text-slate-600">
|
||
<RefreshCw className="w-4 h-4 mr-2" /> 刷新
|
||
</Button>
|
||
<Button variant="outline" onClick={syncDailian} disabled={syncing} className="border-indigo-200 text-indigo-600">
|
||
<Download className="w-4 h-4 mr-2" /> {syncing ? "同步中..." : "同步代练通"}
|
||
</Button>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
||
<Card className="admin-glass rounded-2xl border border-slate-200/80 shadow-lg">
|
||
<CardContent className="p-4 text-center">
|
||
<p className="text-2xl font-bold text-slate-800">{products.length}</p>
|
||
<p className="text-slate-500 text-sm">套餐数</p>
|
||
</CardContent>
|
||
</Card>
|
||
<Card className="admin-glass rounded-2xl border border-slate-200/80 shadow-lg">
|
||
<CardContent className="p-4 text-center">
|
||
<p className="text-2xl font-bold text-indigo-600">{orders.length}</p>
|
||
<p className="text-slate-500 text-sm">订单数</p>
|
||
</CardContent>
|
||
</Card>
|
||
<Card className="admin-glass rounded-2xl border border-slate-200/80 shadow-lg">
|
||
<CardContent className="p-4 text-center">
|
||
<p className="text-2xl font-bold text-emerald-600">{products.filter((p) => p.status === "active").length}</p>
|
||
<p className="text-slate-500 text-sm">在售套餐</p>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
<Card className="admin-glass rounded-2xl border border-slate-200/80 shadow-lg">
|
||
<CardHeader>
|
||
<CardTitle className="text-slate-800 flex items-center gap-2">
|
||
<Package className="w-5 h-5 text-indigo-500" />
|
||
套餐列表 ({filtered.length})
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{loading ? (
|
||
<div className="text-center py-8 text-slate-500">加载中...</div>
|
||
) : filtered.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>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{filtered.map((p) => (
|
||
<tr key={p.id} className="border-b border-slate-100 hover:bg-slate-50/80">
|
||
<td className="py-3 px-4 text-slate-800 font-medium">{p.name}</td>
|
||
<td className="py-3 px-4 text-slate-600">{p.rankFrom ?? "—"} → {p.rankTo ?? "—"}</td>
|
||
<td className="py-3 px-4 text-amber-600">¥{p.price ?? 0}</td>
|
||
<td className="py-3 px-4">
|
||
<Badge className={p.status === "active" ? "bg-emerald-100 text-emerald-700" : "bg-slate-100 text-slate-600"}>{p.status ?? "—"}</Badge>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card className="admin-glass rounded-2xl border border-slate-200/80 shadow-lg mt-6">
|
||
<CardHeader>
|
||
<CardTitle className="text-slate-800 flex items-center gap-2">
|
||
<FileText className="w-5 h-5 text-indigo-500" />
|
||
代练订单 ({orders.length})
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{orders.length === 0 ? (
|
||
<p className="text-slate-500 text-sm">暂无订单</p>
|
||
) : (
|
||
<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">订单ID</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>
|
||
{orders.map((o) => (
|
||
<tr key={o.id} className="border-b border-slate-100">
|
||
<td className="py-3 px-4 text-slate-800 font-mono text-sm">{o.id.slice(0, 8)}...</td>
|
||
<td className="py-3 px-4"><Badge className="bg-slate-100 text-slate-600">{o.status ?? "—"}</Badge></td>
|
||
<td className="py-3 px-4 text-amber-600">{o.amount ?? 0}</td>
|
||
<td className="py-3 px-4 text-slate-500 text-sm">{o.createdAt ? new Date(o.createdAt).toLocaleString("zh-CN") : "—"}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
)
|
||
}
|