232 lines
10 KiB
TypeScript
232 lines
10 KiB
TypeScript
"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>
|
||
)
|
||
}
|