Files
2026-05-06 17:22:50 +08:00

330 lines
15 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.
import { prisma } from "@/app/lib/prisma"
import Link from "next/link"
function buildQueryString(params: Record<string, string>) {
const sp = new URLSearchParams()
Object.entries(params).forEach(([key, value]) => {
if (value) sp.append(key, value)
})
const qs = sp.toString()
return qs ? `?${qs}` : ""
}
export default async function AgentsPage({
searchParams,
}: {
searchParams: Promise<{ category?: string; q?: string; sort?: string; page?: string }>
}) {
const params = await searchParams
const { category, q, sort, page } = await searchParams
const categoryId = category ? parseInt(category) : undefined
const searchQuery = q || ""
const sortBy = sort || ""
const currentPage = page ? parseInt(page) : 1
const pageSize = 9
const where: any = {}
if (categoryId) where.categoryId = categoryId
if (searchQuery) {
where.OR = [
{ name: { contains: searchQuery } },
{ description: { contains: searchQuery } },
]
}
let orderBy: any = { createdAt: "desc" }
if (sortBy === "popular") orderBy = { usageCount: "desc" }
const totalAgents = await prisma.agent.count({ where })
const totalPages = Math.ceil(totalAgents / pageSize)
const agents = await prisma.agent.findMany({
where,
include: { category: true },
orderBy,
skip: (currentPage - 1) * pageSize,
take: pageSize,
})
const popularAgents = await prisma.agent.findMany({
include: { category: true },
orderBy: { usageCount: "desc" },
take: 6,
})
const categories = await prisma.category.findMany({
include: { _count: { select: { agents: true } } },
})
const totalCategories = await prisma.category.count()
const getPageUrl = (page: number) => {
const params: Record<string, string> = {}
if (categoryId) params.category = categoryId.toString()
if (searchQuery) params.q = searchQuery
if (sortBy) params.sort = sortBy
if (page > 1) params.page = page.toString()
return `/agents${buildQueryString(params)}`
}
return (
<div className="min-h-screen bg-gray-50">
<nav className="bg-white border-b border-gray-200">
<div className="max-w-6xl mx-auto px-6 py-3">
<div className="flex items-center justify-between">
<Link href="/" className="flex items-center gap-2">
<div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-sm">AI</span>
</div>
<span className="font-bold text-gray-900">广</span>
</Link>
<div className="flex items-center gap-6">
<Link href="/" className="text-gray-600 hover:text-blue-600 transition"></Link>
<Link href="/agents" className="text-blue-600 font-medium">广</Link>
<Link href="/news" className="text-gray-600 hover:text-blue-600 transition"></Link>
<Link href="/admin/login" className="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 transition">
</Link>
</div>
</div>
</div>
</nav>
<main className="pt-16">
<div className="max-w-6xl mx-auto px-6 py-8">
{/* 面包屑 */}
<div className="flex items-center gap-2 mb-6 text-sm">
<Link href="/" className="text-gray-500 hover:text-blue-600 transition"></Link>
<span className="text-gray-400">/</span>
<span className="text-gray-900 font-medium">广</span>
</div>
{/* 页面标题 */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">广</h1>
<p className="text-gray-500"> AI </p>
</div>
{/* 统计信息 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
<div className="bg-white rounded-2xl p-5 border border-gray-200 shadow-sm">
<div className="text-2xl font-bold text-blue-600 mb-1">{totalAgents}</div>
<div className="text-sm text-gray-500"></div>
</div>
<div className="bg-white rounded-2xl p-5 border border-gray-200 shadow-sm">
<div className="text-2xl font-bold text-green-600 mb-1">{totalCategories}</div>
<div className="text-sm text-gray-500"></div>
</div>
<div className="bg-white rounded-2xl p-5 border border-gray-200 shadow-sm">
<div className="text-2xl font-bold text-purple-600 mb-1">5000+</div>
<div className="text-sm text-gray-500"></div>
</div>
<div className="bg-white rounded-2xl p-5 border border-gray-200 shadow-sm">
<div className="text-2xl font-bold text-orange-600 mb-1">98%</div>
<div className="text-sm text-gray-500"></div>
</div>
</div>
{/* 搜索与筛选 */}
<div className="bg-white rounded-2xl border border-gray-200 p-6 mb-8 shadow-sm">
<h3 className="font-semibold text-gray-900 mb-4"></h3>
<form className="flex flex-wrap gap-4 items-center">
<div className="flex-1 min-w-64 relative">
<svg className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
<input
name="q"
type="text"
defaultValue={searchQuery}
placeholder="搜索智能体名称或简介..."
className="w-full pl-12 pr-4 py-3 border border-gray-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 transition"
/>
</div>
<select
name="category"
defaultValue={categoryId || ""}
className="px-4 py-3 border border-gray-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white"
>
<option value=""></option>
{categories.map((cat) => (
<option key={cat.id} value={cat.id}>{cat.name}</option>
))}
</select>
<select
name="sort"
defaultValue={sortBy}
className="px-4 py-3 border border-gray-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white"
>
<option value=""></option>
<option value="date"></option>
<option value="popular"></option>
</select>
<button type="submit" className="px-6 py-3 bg-blue-600 text-white rounded-xl text-sm hover:bg-blue-700 transition">
</button>
<Link href="/agents" className="px-4 py-3 bg-gray-100 text-gray-600 rounded-xl text-sm hover:bg-gray-200 transition">
</Link>
</form>
<div className="flex flex-wrap gap-2 mt-4 pt-4 border-t border-gray-100">
<span className="text-xs text-gray-400 self-center mr-2"></span>
<Link href="/agents" className={`px-3 py-1.5 rounded-full text-xs border border-gray-200 bg-white text-gray-600 hover:border-blue-400 hover:text-blue-600 transition-all ${!categoryId ? "bg-blue-50 border-blue-400 text-blue-600" : ""}`}>
</Link>
{categories.map((cat) => (
<Link
key={cat.id}
href={`/agents?category=${cat.id}`}
className={`px-3 py-1.5 rounded-full text-xs border border-gray-200 bg-white text-gray-600 hover:border-blue-400 hover:text-blue-600 transition-all ${categoryId === cat.id ? "bg-blue-50 border-blue-400 text-blue-600" : ""}`}
>
{cat.name}
</Link>
))}
</div>
</div>
{/* 热门推荐 */}
{popularAgents.length > 0 && (
<div className="mb-8">
<h2 className="text-xl font-bold text-gray-900 mb-4"></h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
{popularAgents.map((agent) => (
<Link
key={agent.id}
href={`/agents/${agent.slug}`}
className="bg-white rounded-2xl border border-gray-200 p-5 hover:border-blue-300 shadow-sm hover:shadow-md transition-all duration-300"
>
<div className="flex items-center gap-4 mb-4">
<div className="w-14 h-14 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center text-3xl flex-shrink-0">
{agent.icon || "🤖"}
</div>
<div className="min-w-0">
<h3 className="font-semibold text-gray-900 truncate">{agent.name}</h3>
{agent.category && (
<span className="inline-block mt-1 px-2 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-600">
{agent.category.name}
</span>
)}
</div>
</div>
<p className="text-gray-500 text-sm leading-relaxed mb-4">{agent.description}</p>
<div className="flex items-center justify-between text-xs text-gray-400">
<span>使 {agent.usageCount} </span>
<span className="text-blue-600 font-medium"> </span>
</div>
</Link>
))}
</div>
</div>
)}
{/* 智能体列表头部 */}
<div className="mb-6 flex items-center justify-between">
<h2 className="text-xl font-bold text-gray-900"></h2>
<span className="text-sm text-gray-500"> {totalAgents} </span>
</div>
{/* 智能体列表 */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{agents.map((agent) => (
<Link
key={agent.id}
href={`/agents/${agent.slug}`}
className="bg-white rounded-2xl border border-gray-200 p-5 hover:border-blue-300 shadow-sm hover:shadow-md transition-all duration-300"
>
<div className="flex items-center gap-4 mb-4">
<div className="w-14 h-14 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center text-3xl flex-shrink-0">
{agent.icon || "🤖"}
</div>
<div className="min-w-0">
<h3 className="font-semibold text-gray-900 truncate">{agent.name}</h3>
{agent.category && (
<span className="inline-block mt-1 px-2 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-600">
{agent.category.name}
</span>
)}
</div>
</div>
<p className="text-gray-500 text-sm leading-relaxed mb-4">{agent.description}</p>
<div className="flex items-center justify-between text-xs text-gray-400">
<span>使 {agent.usageCount} </span>
<span className={`font-medium ${agent.status === "active" ? "text-green-600" : "text-gray-600"}`}>
{agent.status === "active" ? "运行中" : agent.status}
</span>
</div>
</Link>
))}
</div>
{/* 空状态 */}
{agents.length === 0 && (
<div className="text-center py-24 bg-gray-50 rounded-2xl">
<div className="text-7xl mb-6">🔍</div>
<h3 className="text-xl font-medium text-gray-700 mb-3"></h3>
<p className="text-gray-400 mb-6 max-w-md mx-auto"></p>
<Link href="/agents" className="inline-block px-6 py-3 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 transition">
</Link>
</div>
)}
{/* 分页 */}
{totalPages > 1 && (
<div className="flex items-center justify-between mt-8 px-2">
<span className="text-sm text-gray-500">
{currentPage} {totalPages}
</span>
<div className="flex items-center gap-2">
<Link
href={getPageUrl(currentPage - 1)}
className={`px-4 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition ${currentPage <= 1 ? "opacity-40 cursor-not-allowed" : ""}`}
>
</Link>
<div className="flex items-center gap-1">
{Array.from({ length: totalPages }, (_, i) => i + 1).map((pageNum) => (
<Link
key={pageNum}
href={getPageUrl(pageNum)}
className={`w-8 h-8 flex items-center justify-center text-sm rounded-lg transition ${currentPage === pageNum ? "bg-blue-600 text-white" : "hover:bg-gray-50 text-gray-600"}`}
>
{pageNum}
</Link>
))}
</div>
<Link
href={getPageUrl(currentPage + 1)}
className={`px-4 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition ${currentPage >= totalPages ? "opacity-40 cursor-not-allowed" : ""}`}
>
</Link>
</div>
</div>
)}
</div>
</main>
{/* 底部 */}
<div className="border-t border-gray-200 py-8 mt-16">
<div className="max-w-6xl mx-auto px-6 flex flex-col md:flex-row items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="w-7 h-7 bg-blue-600 rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-xs">AI</span>
</div>
<span className="font-semibold text-gray-700 text-sm"></span>
</div>
<div className="text-gray-400 text-sm text-center">
© 2026 · AI 广
</div>
<div className="flex gap-4 text-xs text-gray-400">
<a className="hover:text-gray-600 cursor-pointer transition"></a>
<a className="hover:text-gray-600 cursor-pointer transition"></a>
<a className="hover:text-gray-600 cursor-pointer transition"></a>
</div>
</div>
</div>
</div>
)
}