feat: 实现 Agent Park AI 项目导航网站核心功能

完成 Next.js 14+ 全栈应用的 MVP 实现,包含以下功能:

## 项目设置
- 初始化 Next.js 14+ 项目,配置 TypeScript 严格模式
- 配置 Tailwind CSS、ESLint、Prettier 代码质量工具
- 集成 shadcn/ui 组件库和 next-intl 国际化方案

## 数据层
- 配置 PostgreSQL + Prisma ORM
- 定义 Project、Tag、ExternalLink 数据模型
- 实现种子数据脚本(5个AI项目)

## 用户故事 1:浏览和搜索 AI 项目
- 实现首页,展示热门标签云和精选项目
- 实现项目列表页,支持搜索和标签筛选
- 创建 TagCloud、ProjectCard、ProjectList、SearchBar 组件
- 实现 ISR 缓存策略优化性能

## 用户故事 2:查看项目详细信息
- 实现项目详情页,显示完整信息
- 创建 ExternalLinkCard 组件展示外部链接
- 实现安全的 target="_blank" 外链跳转

## 用户故事 4:数据更新和管理
- 实现 webhook API 端点接收 n8n 数据推送
- 实现 API Key 身份验证
- 支持部分成功模式的批量数据处理
- 完善的错误处理和日志记录

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-25 15:12:09 +08:00
co-authored by Claude
parent 6d40d4e8e6
commit ba1fab65c6
34 changed files with 8225 additions and 0 deletions
+102
View File
@@ -0,0 +1,102 @@
import { prisma } from '@/lib/prisma'
export async function getProjects(options?: {
search?: string
tag?: string
status?: 'ACTIVE' | 'ARCHIVED'
page?: number
limit?: number
}) {
const {
search,
tag,
status = 'ACTIVE',
page = 1,
limit = 20,
} = options || {}
const where: any = {
status,
}
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ nameEn: { contains: search, mode: 'insensitive' } },
{ description: { contains: search, mode: 'insensitive' } },
{ descriptionEn: { contains: search, mode: 'insensitive' } },
]
}
if (tag) {
where.tags = {
some: {
slug: tag,
},
}
}
const [projects, total] = await Promise.all([
prisma.project.findMany({
where,
include: {
tags: true,
links: true,
},
orderBy: {
createdAt: 'desc',
},
skip: (page - 1) * limit,
take: limit,
}),
prisma.project.count({ where }),
])
return {
projects,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
}
}
export async function getProjectBySlug(slug: string) {
return prisma.project.findUnique({
where: { slug },
include: {
tags: true,
links: true,
},
})
}
export async function getAllTags() {
return prisma.tag.findMany({
include: {
_count: {
select: { projects: true },
},
},
orderBy: {
name: 'asc',
},
})
}
export async function getTagsWithProjectCounts() {
const tags = await prisma.tag.findMany({
include: {
_count: {
select: { projects: true },
},
},
orderBy: {
name: 'asc',
},
})
return tags.filter(tag => tag._count.projects > 0)
}
+11
View File
@@ -0,0 +1,11 @@
import { useSearchParams } from 'next/navigation'
export function useSearch() {
const searchParams = useSearchParams()
return {
search: searchParams.get('search') || '',
tag: searchParams.get('tag') || '',
page: Number(searchParams.get('page')) || 1,
}
}