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:
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user