Files
wzdj/old/app/admin/points/client.tsx
2026-04-20 14:56:28 +08:00

221 lines
10 KiB
TypeScript
Raw Permalink 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, RefreshCw, CreditCard, ExternalLink, Settings, Package, ChevronRight, 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 PointCard = {
id: string
game: string
name: string
faceValue?: number
price?: number
stock?: number
sales?: number
deliveryType?: string
status?: string
}
export default function AdminPointsClient() {
const [cards, setCards] = useState<PointCard[]>([])
const [ordersCount, setOrdersCount] = useState(0)
const [searchTerm, setSearchTerm] = useState("")
const [loading, setLoading] = useState(true)
const [syncing, setSyncing] = useState(false)
const [syncMsg, setSyncMsg] = useState("")
useEffect(() => {
loadData()
}, [])
const loadData = async () => {
setLoading(true)
try {
const [cardsRes, ordersRes] = await Promise.all([
fetch("/api/admin/points/cards"),
fetch("/api/admin/points/orders"),
])
const cardsJson = await cardsRes.json()
const ordersJson = await ordersRes.json()
if (cardsJson.success && Array.isArray(cardsJson.data)) setCards(cardsJson.data)
else setCards([])
if (ordersJson.success && Array.isArray(ordersJson.data)) setOrdersCount(ordersJson.data.length)
else setOrdersCount(0)
} catch (e) {
console.error(e)
setCards([])
setOrdersCount(0)
}
setLoading(false)
}
const syncFulu = async () => {
setSyncing(true)
setSyncMsg("")
try {
const res = await fetch("/api/admin/points/sync-fulu", { method: "POST" })
const json = await res.json()
setSyncMsg(json.message ?? json.error ?? (json.success ? "已请求同步" : "失败"))
if (json.success) loadData()
} catch (e) {
setSyncMsg("请求失败")
}
setSyncing(false)
}
const filtered = cards.filter(
(x) =>
(x.game ?? "").toLowerCase().includes(searchTerm.toLowerCase()) ||
(x.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 gamePointCards / pointCardOrders</p>
<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 items-center">
<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 hover:bg-slate-50">
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
<Button variant="outline" onClick={syncFulu} disabled={syncing} className="border-amber-200 text-amber-600 hover:bg-amber-50">
<Download className="w-4 h-4 mr-2" />
{syncing ? "同步中..." : "从福禄同步"}
</Button>
<Link href="/admin/points/channels">
<Button variant="outline" className="border-slate-200 text-slate-600 hover:bg-slate-50">
<Settings className="w-4 h-4 mr-2" />
</Button>
</Link>
<Link href="/mall/points" target="_blank" rel="noopener noreferrer">
<Button variant="outline" className="border-indigo-200 text-indigo-600 hover:bg-indigo-50">
<ExternalLink className="w-4 h-4 mr-2" />
</Button>
</Link>
</div>
{syncMsg && <p className="text-sm text-slate-600 mt-2">{syncMsg}</p>}
</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">{cards.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-sky-600">{ordersCount}</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">{cards.filter((c) => c.status === "active").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">
{cards.reduce((s, c) => s + (c.sales ?? 0), 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"> ({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"> FULU_APP_KEY / FULU_APP_SECRET</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 w-20"></th>
</tr>
</thead>
<tbody>
{filtered.map((c) => (
<tr key={c.id} className="border-b border-slate-100 hover:bg-slate-50/80">
<td className="py-3 px-4">
<div className="flex items-center gap-2">
<CreditCard className="w-5 h-5 text-slate-500" />
<div>
<Link href={`/admin/points/${c.id}`} className="text-slate-800 font-medium hover:text-indigo-600">{c.name}</Link>
<p className="text-slate-500 text-xs">{c.game}</p>
</div>
</div>
</td>
<td className="py-3 px-4 text-sky-600">{c.faceValue ?? 0} </td>
<td className="py-3 px-4 text-amber-600">¥{c.price ?? 0}</td>
<td className="py-3 px-4 text-slate-600">{c.stock ?? 0} / {(c.sales ?? 0).toLocaleString()}</td>
<td className="py-3 px-4">
<Badge variant="outline" className="border-slate-200 text-slate-600">
{c.deliveryType === "instant" ? "自动发货" : "人工"}
</Badge>
</td>
<td className="py-3 px-4">
<Badge className={c.status === "active" ? "bg-emerald-100 text-emerald-700" : c.status === "soldout" ? "bg-amber-100 text-amber-700" : "bg-slate-100 text-slate-600"}>
{c.status === "active" ? "在售" : c.status === "soldout" ? "售罄" : "下架"}
</Badge>
</td>
<td className="py-3 px-4">
<Link href={`/admin/points/${c.id}`}>
<Button variant="ghost" size="sm" className="text-slate-500 hover:text-slate-800"><ChevronRight className="w-4 h-4" /></Button>
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
<Card className="admin-glass rounded-2xl overflow-hidden border border-slate-200/80 shadow-lg mt-6">
<CardHeader>
<CardTitle className="text-slate-800 text-base"></CardTitle>
</CardHeader>
<CardContent className="text-slate-600 text-sm space-y-2">
<p>· <strong></strong>/Q API https://docs.open.fulu.com ,配置 FULU_APP_KEY / FULU_APP_SECRET 后可使用「从福禄同步」拉取商品。</p>
<p>· pointCardOrders</p>
</CardContent>
</Card>
</div>
)
}