"use client"; import { useState, useEffect, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from "@/components/ui/dialog"; import { LazyImage } from "@/components/ui/LazyImage"; interface Tool { id: number; name: string; slug: string; categoryId: number; description: string; logoUrl: string | null; websiteUrl: string; pricingModel: string; pricingDetail: string | null; features: unknown; tags: unknown; viewCount: number; status: string; category: { id: number; name: string; slug: string } | null; createdAt: string; } interface Category { id: number; name: string; } const pricingLabel: Record = { free: "免费", freemium: "免费增值", paid: "付费", subscription: "订阅", }; const pricingColor: Record = { free: "border-emerald-200/60 bg-emerald-50 text-emerald-600", freemium: "border-blue-200/60 bg-blue-50 text-blue-600", paid: "border-amber-200/60 bg-amber-50 text-amber-600", subscription: "border-purple-200/60 bg-purple-50 text-purple-600", }; const statusLabel: Record = { draft: "草稿", published: "已发布", }; const statusColor: Record = { draft: "border-amber-200/60 bg-amber-50 text-amber-600", published: "border-emerald-200/60 bg-emerald-50 text-emerald-600", }; function TagInput({ value, onChange, placeholder, }: { value: string[]; onChange: (v: string[]) => void; placeholder: string; }) { const [input, setInput] = useState(""); const add = () => { const v = input.trim(); if (v && !value.includes(v)) onChange([...value, v]); setInput(""); }; const remove = (idx: number) => onChange(value.filter((_, i) => i !== idx)); return (
setInput(e.target.value)} onKeyDown={(e) => e.key === "Enter" && (e.preventDefault(), add())} placeholder={placeholder} className="h-10 flex-1 rounded-xl border border-border/50 bg-white px-3.5 text-sm text-foreground placeholder:text-muted-foreground/40 focus:border-primary/40 focus:bg-background focus:outline-none focus:ring-2 focus:ring-primary/15 transition-all duration-200" />
{value.map((tag, i) => ( {tag} ))}
); } const emptyForm = { name: "", description: "", websiteUrl: "", logoUrl: "", categoryId: "" as string, pricingModel: "freemium", pricingDetail: "", status: "draft", features: [] as string[], tags: [] as string[], }; export default function ToolsAdminPage() { const [tools, setTools] = useState([]); const [categories, setCategories] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [page, setPage] = useState(1); const [total, setTotal] = useState(0); const pageSize = 20; const [editTool, setEditTool] = useState(null); const [isNew, setIsNew] = useState(false); const [form, setForm] = useState(emptyForm); const [saving, setSaving] = useState(false); const [verifiedToolIds, setVerifiedToolIds] = useState>(new Set()); const fetchVerifiedTools = useCallback(async () => { try { const res = await fetch("/api/admin/tools/verify"); const data = await res.json(); if (data.verifiedTools) { const ids = new Set(); (data.verifiedTools as { toolId: number }[]).forEach((v) => ids.add(v.toolId)); setVerifiedToolIds(ids); } } catch { /* ignore */ } }, []); const fetchTools = useCallback(async () => { setError(""); try { const res = await fetch(`/api/admin/tools?page=${page}&pageSize=${pageSize}`); const data = await res.json(); setTools(data.tools || []); setTotal(data.total || 0); } catch { setError("加载工具列表失败,请稍后重试"); } finally { setLoading(false); } }, [page]); const fetchCategories = useCallback(async () => { try { const res = await fetch("/api/categories"); const data = await res.json(); const flat: Category[] = []; const walk = (list: { id: number; name: string; children?: unknown[] }[]) => { for (const c of list) { flat.push({ id: c.id, name: c.name }); if (Array.isArray(c.children)) walk(c.children as typeof list); } }; walk(data); setCategories(flat); } catch { console.error("Fetch categories failed"); } }, []); useEffect(() => { fetchTools(); fetchCategories(); fetchVerifiedTools(); }, [fetchTools, fetchCategories, fetchVerifiedTools]); const openEdit = (tool: Tool) => { setIsNew(false); setEditTool(tool); setForm({ name: tool.name, description: tool.description || "", websiteUrl: tool.websiteUrl || "", logoUrl: tool.logoUrl || "", categoryId: tool.categoryId?.toString() || "", pricingModel: tool.pricingModel || "freemium", pricingDetail: tool.pricingDetail || "", status: tool.status || "draft", features: Array.isArray(tool.features) ? (tool.features as string[]) : [], tags: Array.isArray(tool.tags) ? (tool.tags as string[]) : [], }); }; const openNew = () => { setIsNew(true); setEditTool({} as Tool); setForm(emptyForm); }; const handleSave = async () => { setSaving(true); try { const payload = { name: form.name, description: form.description, websiteUrl: form.websiteUrl, logoUrl: form.logoUrl, categoryId: form.categoryId ? parseInt(form.categoryId) : 1, pricingModel: form.pricingModel, pricingDetail: form.pricingDetail, features: form.features, tags: form.tags, status: form.status, }; if (isNew) { await fetch("/api/admin/tools", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); } else if (editTool?.id) { await fetch(`/api/admin/tools/${editTool.id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); } setEditTool(null); fetchTools(); } catch { console.error("Save failed"); } finally { setSaving(false); } }; const handleDelete = async (id: number) => { if (!confirm("确定删除该工具?")) return; try { await fetch(`/api/admin/tools/${id}`, { method: "DELETE" }); setTools((prev) => prev.filter((t) => t.id !== id)); } catch { console.error("Delete failed"); } }; const handleVerifyToggle = async (toolId: number) => { try { const res = await fetch("/api/admin/tools/verify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ toolId }), }); const data = await res.json(); if (data.success) { setVerifiedToolIds((prev) => { const next = new Set(prev); if (data.verified) { next.add(toolId); } else { next.delete(toolId); } return next; }); } } catch { console.error("Verify toggle failed"); } }; const publishedCount = tools.filter((t) => t.status === "published").length; const draftCount = tools.filter((t) => t.status === "draft").length; const totalViews = tools.reduce((sum, t) => sum + t.viewCount, 0); const totalPages = Math.ceil(total / pageSize); return (

已发布工具管理

管理所有已发布和草稿状态的AI工具

{publishedCount}

已发布

{draftCount}

草稿

{totalViews.toLocaleString()}

总浏览量

共 {total} 个工具
{error ? (

加载失败

{error}

) : loading ? (
{Array.from({ length: 6 }).map((_, i) => ( ))} {Array.from({ length: 5 }).map((_, i) => ( {Array.from({ length: 5 }).map((_, j) => ( ))} ))}
) : tools.length === 0 ? (

暂无工具

点击上方按钮添加第一个工具

) : (
{tools.map((tool) => ( ))}
工具 分类 定价 浏览量 状态 操作
{tool.logoUrl ? ( ) : (
{tool.name.charAt(0)}
)}

{tool.name}

{tool.websiteUrl}

{tool.category?.name || 未分类} {pricingLabel[tool.pricingModel] || tool.pricingModel} {tool.viewCount.toLocaleString()} {statusLabel[tool.status] || tool.status}
)} {totalPages > 1 && (
{page} / {totalPages}
)} !open && setEditTool(null)} > {isNew ? "添加新工具" : "编辑工具"}
setForm((f) => ({ ...f, name: e.target.value }))} className="h-11 w-full rounded-xl border border-border/50 bg-white px-4 text-sm text-foreground placeholder:text-muted-foreground/40 focus:border-primary/40 focus:bg-background focus:outline-none focus:ring-2 focus:ring-primary/15 transition-all duration-200" placeholder="输入工具名称" />