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

232 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, Building2, ChevronRight, Plus } from "lucide-react"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
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 GuildRow = {
id: string
name: string
description?: string
memberCount?: number
maxMembers?: number
level?: number
totalIncome?: number
status?: string
tags?: string[]
createdAt?: string
streamerCount?: number
}
export default function AdminGuildsClient() {
const [guilds, setGuilds] = useState<GuildRow[]>([])
const [searchTerm, setSearchTerm] = useState("")
const [loading, setLoading] = useState(true)
const [addOpen, setAddOpen] = useState(false)
const [addName, setAddName] = useState("")
const [addDesc, setAddDesc] = useState("")
const [addSubmitting, setAddSubmitting] = useState(false)
useEffect(() => {
loadGuilds()
}, [])
const loadGuilds = async () => {
setLoading(true)
try {
const res = await fetch("/api/admin/guilds")
const json = await res.json()
if (json.success && Array.isArray(json.data)) setGuilds(json.data)
else setGuilds([])
} catch (e) {
console.error(e)
setGuilds([])
}
setLoading(false)
}
const handleAddGuild = async () => {
if (!addName.trim()) return
setAddSubmitting(true)
try {
const res = await fetch("/api/admin/guilds", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: addName.trim(), description: addDesc.trim() }),
})
const json = await res.json()
if (json.success) {
setAddOpen(false)
setAddName("")
setAddDesc("")
loadGuilds()
}
} finally {
setAddSubmitting(false)
}
}
const filtered = guilds.filter(
(g) =>
(g.name ?? "").toLowerCase().includes(searchTerm.toLowerCase()) ||
(g.tags ?? []).some((t: string) => t.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>
<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>
<Button variant="outline" onClick={loadGuilds} className="border-slate-200 text-slate-600 hover:bg-slate-50">
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
<Button onClick={() => setAddOpen(true)} className="bg-gradient-to-r from-indigo-500 to-purple-500 text-white shadow-md">
<Plus 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">{guilds.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">
{guilds.filter((g) => g.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-sky-600">
{guilds.reduce((s, g) => s + (g.streamerCount ?? 0), 0)}
</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">
¥{(guilds.reduce((s, g) => s + (g.totalIncome ?? 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"> MongoDB</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>
{filtered.map((g) => (
<tr key={g.id} className="border-b border-slate-100 hover:bg-slate-50/80">
<td className="py-3 px-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-slate-100 flex items-center justify-center">
<Building2 className="w-5 h-5 text-slate-500" />
</div>
<div>
<Link href={`/admin/guilds/${g.id}`} className="text-slate-800 font-medium hover:text-indigo-600">
{g.name}
</Link>
<p className="text-slate-500 text-xs truncate max-w-[200px]">{g.description}</p>
</div>
</div>
</td>
<td className="py-3 px-4 text-slate-700">{g.streamerCount ?? 0}</td>
<td className="py-3 px-4 text-slate-600">
{g.memberCount ?? 0} / {g.maxMembers ?? 0}
</td>
<td className="py-3 px-4">
<Badge variant="outline" className="border-indigo-200 text-indigo-700">Lv.{g.level ?? 0}</Badge>
</td>
<td className="py-3 px-4 text-amber-600">¥{(g.totalIncome ?? 0).toLocaleString()}</td>
<td className="py-3 px-4">
<Badge className={g.status === "active" ? "bg-emerald-100 text-emerald-700 border-emerald-200" : "bg-slate-100 text-slate-600"}>
{g.status === "active" ? "运营中" : g.status === "suspended" ? "暂停" : "已解散"}
</Badge>
</td>
<td className="py-3 px-4">
<Link href={`/admin/guilds/${g.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>
<Dialog open={addOpen} onOpenChange={setAddOpen}>
<DialogContent className="bg-white border-slate-200 text-slate-800">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="space-y-4 pt-2">
<div>
<label className="text-sm text-slate-600"></label>
<Input value={addName} onChange={(e) => setAddName(e.target.value)} placeholder="请输入名称" className="mt-1 border-slate-200" />
</div>
<div>
<label className="text-sm text-slate-600"></label>
<Input value={addDesc} onChange={(e) => setAddDesc(e.target.value)} placeholder="选填" className="mt-1 border-slate-200" />
</div>
<Button onClick={handleAddGuild} disabled={addSubmitting || !addName.trim()} className="w-full">
{addSubmitting ? "提交中..." : "创建"}
</Button>
</div>
</DialogContent>
</Dialog>
</div>
)
}