Files
wzdj/app/admin/content/client.tsx

332 lines
13 KiB
TypeScript
Raw 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, Trophy, Plus, Pencil, Trash2, Image as ImageIcon } 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"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
type ContentItem = {
id: string
type: string
title: string
link?: string
image?: string
sortOrder?: number
status?: string
createdAt?: string
}
const TYPE_LABELS: Record<string, string> = {
tag: "标签",
event: "赛事",
banner: "推荐位",
}
export default function AdminContentClient() {
const [items, setItems] = useState<ContentItem[]>([])
const [searchTerm, setSearchTerm] = useState("")
const [typeFilter, setTypeFilter] = useState<string>("")
const [loading, setLoading] = useState(true)
const [editOpen, setEditOpen] = useState(false)
const [editItem, setEditItem] = useState<ContentItem | null>(null)
const [formTitle, setFormTitle] = useState("")
const [formLink, setFormLink] = useState("")
const [formImage, setFormImage] = useState("")
const [formType, setFormType] = useState("tag")
const [formSortOrder, setFormSortOrder] = useState(0)
const [formStatus, setFormStatus] = useState("active")
const [submitting, setSubmitting] = useState(false)
useEffect(() => {
loadItems()
}, [typeFilter])
const loadItems = async () => {
setLoading(true)
try {
const q = typeFilter ? `?type=${typeFilter}` : ""
const res = await fetch(`/api/admin/content/items${q}`)
const json = await res.json()
if (json.success && Array.isArray(json.data)) setItems(json.data)
else setItems([])
} catch (e) {
console.error(e)
setItems([])
}
setLoading(false)
}
const openAdd = () => {
setEditItem(null)
setFormTitle("")
setFormLink("")
setFormImage("")
setFormType("tag")
setFormSortOrder(0)
setFormStatus("active")
setEditOpen(true)
}
const openEdit = (item: ContentItem) => {
setEditItem(item)
setFormTitle(item.title ?? "")
setFormLink(item.link ?? "")
setFormImage(item.image ?? "")
setFormType(item.type ?? "tag")
setFormSortOrder(item.sortOrder ?? 0)
setFormStatus(item.status ?? "active")
setEditOpen(true)
}
const handleSave = async () => {
setSubmitting(true)
try {
if (editItem?.id) {
const res = await fetch(`/api/admin/content/items/${editItem.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: formTitle,
link: formLink,
image: formImage,
type: formType,
sortOrder: formSortOrder,
status: formStatus,
}),
})
const json = await res.json()
if (json.success) {
setEditOpen(false)
loadItems()
}
} else {
const res = await fetch("/api/admin/content/items", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: formTitle,
link: formLink,
image: formImage,
type: formType,
sortOrder: formSortOrder,
status: formStatus,
}),
})
const json = await res.json()
if (json.success) {
setEditOpen(false)
loadItems()
}
}
} finally {
setSubmitting(false)
}
}
const handleDelete = async (id: string) => {
if (!confirm("确定删除?")) return
try {
const res = await fetch(`/api/admin/content/items/${id}`, { method: "DELETE" })
const json = await res.json()
if (json.success) loadItems()
} catch (e) {
console.error(e)
}
}
const filtered = items.filter(
(m) =>
(m.title ?? "").toLowerCase().includes(searchTerm.toLowerCase()) ||
(m.link ?? "").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 contentItems</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>
<div className="flex gap-2">
<Button
variant={typeFilter === "" ? "default" : "outline"}
size="sm"
onClick={() => setTypeFilter("")}
className={typeFilter === "" ? "bg-indigo-500" : "border-slate-200"}
>
</Button>
{(["tag", "event", "banner"] as const).map((t) => (
<Button
key={t}
variant={typeFilter === t ? "default" : "outline"}
size="sm"
onClick={() => setTypeFilter(t)}
className={typeFilter === t ? "bg-indigo-500" : "border-slate-200"}
>
{TYPE_LABELS[t]}
</Button>
))}
</div>
<Button variant="outline" onClick={loadItems} className="border-slate-200 text-slate-600">
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
<Button onClick={openAdd} className="bg-indigo-500 text-white hover:bg-indigo-600">
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
</CardContent>
</Card>
<div className="grid grid-cols-1 sm:grid-cols-3 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">{items.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">{items.filter((m) => m.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">{items.filter((m) => m.type === "event").length}</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 flex items-center gap-2">
<Trophy 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>
<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-32"></th>
</tr>
</thead>
<tbody>
{filtered.map((m) => (
<tr key={m.id} className="border-b border-slate-100 hover:bg-slate-50/80">
<td className="py-3 px-4">
<Badge variant="outline" className="border-indigo-200 text-indigo-700">
{TYPE_LABELS[m.type] ?? m.type}
</Badge>
</td>
<td className="py-3 px-4 text-slate-800 font-medium">{m.title}</td>
<td className="py-3 px-4 text-slate-600 text-sm truncate max-w-[200px]">
{m.link ? m.link : m.image ? <span className="flex items-center gap-1"><ImageIcon className="w-4 h-4" /> </span> : "—"}
</td>
<td className="py-3 px-4 text-slate-600">{m.sortOrder ?? 0}</td>
<td className="py-3 px-4">
<Badge className={m.status === "active" ? "bg-emerald-100 text-emerald-700" : "bg-slate-100 text-slate-600"}>
{m.status === "active" ? "上线" : "下线"}
</Badge>
</td>
<td className="py-3 px-4 flex gap-1">
<Button variant="ghost" size="sm" onClick={() => openEdit(m)} className="text-slate-600 hover:text-indigo-600">
<Pencil className="w-4 h-4" />
</Button>
<Button variant="ghost" size="sm" onClick={() => handleDelete(m.id)} className="text-slate-600 hover:text-red-600">
<Trash2 className="w-4 h-4" />
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
<Dialog open={editOpen} onOpenChange={setEditOpen}>
<DialogContent className="bg-white border-slate-200 text-slate-800 max-w-md">
<DialogHeader>
<DialogTitle>{editItem ? "编辑内容" : "新增内容"}</DialogTitle>
</DialogHeader>
<div className="space-y-4 pt-2">
<div>
<label className="text-sm text-slate-600"></label>
<select
value={formType}
onChange={(e) => setFormType(e.target.value)}
className="mt-1 w-full rounded-md border border-slate-200 px-3 py-2 text-slate-800"
>
<option value="tag"></option>
<option value="event"></option>
<option value="banner"></option>
</select>
</div>
<div>
<label className="text-sm text-slate-600"></label>
<Input value={formTitle} onChange={(e) => setFormTitle(e.target.value)} placeholder="标题" className="mt-1 border-slate-200" />
</div>
<div>
<label className="text-sm text-slate-600"></label>
<Input value={formLink} onChange={(e) => setFormLink(e.target.value)} placeholder="选填" className="mt-1 border-slate-200" />
</div>
<div>
<label className="text-sm text-slate-600"> URL</label>
<Input value={formImage} onChange={(e) => setFormImage(e.target.value)} placeholder="选填" className="mt-1 border-slate-200" />
</div>
<div>
<label className="text-sm text-slate-600"></label>
<Input type="number" value={formSortOrder} onChange={(e) => setFormSortOrder(Number(e.target.value) || 0)} className="mt-1 border-slate-200" />
</div>
<div>
<label className="text-sm text-slate-600"></label>
<select
value={formStatus}
onChange={(e) => setFormStatus(e.target.value)}
className="mt-1 w-full rounded-md border border-slate-200 px-3 py-2 text-slate-800"
>
<option value="active">线</option>
<option value="offline">线</option>
</select>
</div>
<Button onClick={handleSave} disabled={submitting || !formTitle.trim()} className="w-full">
{submitting ? "保存中..." : "保存"}
</Button>
</div>
</DialogContent>
</Dialog>
</div>
)
}