主要修复: - 安全性:API 密钥使用恒定时间比较防止时序攻击 - 安全性:URL 验证仅允许 http/https 协议,防止 XSS - 安全性:移除 Markdown 的 rehypeRaw 插件增强 XSS 防护 - 安全性:Webhook 错误信息脱敏,生产环境不暴露敏感数据 - 性能:项目详情页使用并行数据获取 - 性能:Webhook 批量查询标签,避免 N+1 查询问题 - 类型安全:移除 any 类型,使用 Prisma 类型注解 - 类型安全:为 useProjects 函数添加返回类型 - 代码质量:创建 slug 工具函数统一 slug 生成逻辑 - 代码质量:搜索字符串添加最小长度限制(2字符) Co-Authored-By: Claude <noreply@anthropic.com>
94 lines
3.1 KiB
TypeScript
94 lines
3.1 KiB
TypeScript
import { notFound } from 'next/navigation'
|
|
import { getTranslations } from 'next-intl/server'
|
|
import { getProjectBySlug, getProjects } from '@/hooks/useProjects'
|
|
import { ProjectDetail } from '@/components/project/ProjectDetail'
|
|
import { ProjectSidebar } from '@/components/project/ProjectSidebar'
|
|
import { RelatedProjects } from '@/components/project/RelatedProjects'
|
|
|
|
interface ProjectDetailPageProps {
|
|
params: Promise<{ locale: string; id: string }>
|
|
}
|
|
|
|
export default async function ProjectDetailPage({
|
|
params,
|
|
}: ProjectDetailPageProps) {
|
|
const resolvedParams = await params
|
|
const { locale, id } = resolvedParams
|
|
const tProject = await getTranslations('project')
|
|
|
|
// 并行获取项目和相关项目数据
|
|
const [project, relatedProjectsResult] = await Promise.all([
|
|
getProjectBySlug(id),
|
|
getProjects({ limit: 3 }),
|
|
])
|
|
|
|
if (!project) {
|
|
notFound()
|
|
}
|
|
|
|
const relatedProjects = relatedProjectsResult.projects
|
|
.filter(p => p.id !== project.id)
|
|
.filter(p => p.tags.some(t => project.tags.some(pt => pt.id === t.id)))
|
|
.slice(0, 3)
|
|
|
|
return (
|
|
<main className="min-h-screen bg-background-light dark:bg-background-dark text-text-light dark:text-text-dark font-body transition-colors duration-200">
|
|
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-12">
|
|
{/* Back Button */}
|
|
<div className="mb-8">
|
|
<a
|
|
className="inline-flex items-center text-sm font-display font-bold text-gray-500 hover:text-black dark:text-gray-400 dark:hover:text-white transition-colors group"
|
|
href={`/${locale}/projects`}
|
|
>
|
|
<span className="material-icons text-base mr-1 group-hover:-translate-x-1 transition-transform">
|
|
arrow_back
|
|
</span>
|
|
{tProject('backToProjects')}
|
|
</a>
|
|
</div>
|
|
|
|
{/* Main Grid Layout */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12">
|
|
{/* Main Content (8 columns) */}
|
|
<div className="lg:col-span-8">
|
|
<ProjectDetail project={project} locale={locale} />
|
|
</div>
|
|
|
|
{/* Sidebar (4 columns) */}
|
|
<aside className="lg:col-span-4">
|
|
<ProjectSidebar project={project} locale={locale} />
|
|
</aside>
|
|
</div>
|
|
|
|
{/* Related Projects Section */}
|
|
<RelatedProjects projects={relatedProjects} locale={locale} />
|
|
</div>
|
|
</main>
|
|
)
|
|
}
|
|
|
|
export async function generateMetadata({ params }: ProjectDetailPageProps) {
|
|
const resolvedParams = await params
|
|
const { id, locale } = resolvedParams
|
|
const tProject = await getTranslations('project')
|
|
|
|
const project = await getProjectBySlug(id)
|
|
|
|
if (!project) {
|
|
return {
|
|
title: tProject('notFound'),
|
|
}
|
|
}
|
|
|
|
const displayName = locale === 'en' && project.nameEn ? project.nameEn : project.name
|
|
const displayDescription =
|
|
locale === 'en' && project.descriptionEn ? project.descriptionEn : project.description
|
|
|
|
return {
|
|
title: `${displayName} - Agent Park`,
|
|
description: displayDescription,
|
|
}
|
|
}
|
|
|
|
export const revalidate = 300 // ISR: revalidate every 5 minutes
|