学习如何在 Codofly Template 中使用 Next.js App Router 创建和管理页面
app
目录来定义路由结构,每个文件夹代表一个路由段:
app/
├── page.tsx # 首页 (/)
├── layout.tsx # 根布局
├── loading.tsx # 加载页面
├── error.tsx # 错误页面
├── not-found.tsx # 404 页面
├── dashboard/ # /dashboard
│ ├── page.tsx # 仪表盘页面
│ ├── layout.tsx # 仪表盘布局
│ └── settings/ # /dashboard/settings
│ └── page.tsx # 设置页面
├── [locale]/ # 国际化路由
│ ├── page.tsx # 本地化首页
│ └── about/ # /[locale]/about
│ └── page.tsx # 关于页面
└── api/ # API 路由
└── users/
└── route.ts # API 端点
page.tsx
- 页面组件,定义路由的 UIlayout.tsx
- 布局组件,包装子页面loading.tsx
- 加载状态 UIerror.tsx
- 错误边界 UInot-found.tsx
- 404 页面 UIroute.ts
- API 路由处理器app
目录下创建新文件夹(路由名称)page.tsx
文件import { Metadata } from 'next';
export const metadata: Metadata = {
title: '产品 - Codofly',
description: '探索 Codofly 的强大功能',
};
export default function ProductsPage() {
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
产品列表
</h1>
<div className="mt-6 grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{/* 产品列表内容 */}
</div>
</div>
);
}
[]
创建动态路由:
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
interface Props {
params: { id: string };
searchParams: { [key: string]: string | string[] | undefined };
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const product = await getProduct(params.id);
if (!product) {
return {
title: '产品未找到',
};
}
return {
title: `${product.name} - Codofly`,
description: product.description,
};
}
async function getProduct(id: string) {
// 从数据库或 API 获取产品信息
// 这里是示例代码
try {
const response = await fetch(`/api/products/${id}`);
if (!response.ok) return null;
return await response.json();
} catch {
return null;
}
}
export default async function ProductPage({ params }: Props) {
const product = await getProduct(params.id);
if (!product) {
notFound();
}
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
{product.name}
</h1>
<p className="mt-4 text-gray-600 dark:text-gray-300">
{product.description}
</p>
</div>
);
}
import { Inter } from 'next/font/google';
import { Providers } from '@/components/providers';
import { Navbar } from '@/components/navbar';
import { Footer } from '@/components/footer';
import './globals.css';
const inter = Inter({ subsets: ['latin'] });
export const metadata = {
title: {
default: 'Codofly Template',
template: '%s | Codofly',
},
description: 'AI SaaS 应用开发模板',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" suppressHydrationWarning>
<body className={inter.className}>
<Providers>
<div className="flex min-h-screen flex-col">
<Navbar />
<main className="flex-1">{children}</main>
<Footer />
</div>
</Providers>
</body>
</html>
);
}
import { Sidebar } from '@/components/dashboard/sidebar';
import { DashboardProvider } from '@/contexts/dashboard-context';
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<DashboardProvider>
<div className="flex h-screen bg-gray-100 dark:bg-gray-900">
<Sidebar />
<div className="flex flex-1 flex-col overflow-hidden">
<main className="flex-1 overflow-y-auto p-6">
{children}
</main>
</div>
</div>
</DashboardProvider>
);
}
interface Props {
params: { id: string };
}
export default function UserPage({ params }: Props) {
return <div>用户 ID: {params.id}</div>;
}
interface Props {
params: {
id: string;
postId: string;
};
}
export default function UserPostPage({ params }: Props) {
return (
<div>
<p>用户 ID: {params.id}</p>
<p>文章 ID: {params.postId}</p>
</div>
);
}
[...slug]
捕获多个路由段:
interface Props {
params: { slug: string[] };
}
export default function DocsPage({ params }: Props) {
const path = params.slug.join('/');
return <div>文档路径: {path}</div>;
}
[...slug]
会匹配 /docs/a
、/docs/a/b
、/docs/a/b/c
等所有路径。import { Metadata } from 'next';
export const metadata: Metadata = {
title: '关于我们',
description: 'Codofly 团队介绍',
keywords: ['AI', 'SaaS', '团队'],
openGraph: {
title: '关于我们 - Codofly',
description: 'Codofly 团队介绍',
images: ['/images/about-og.jpg'],
},
twitter: {
card: 'summary_large_image',
title: '关于我们 - Codofly',
description: 'Codofly 团队介绍',
images: ['/images/about-twitter.jpg'],
},
};
export default function AboutPage() {
return <div>关于我们页面内容</div>;
}
import { Metadata } from 'next';
interface Props {
params: { slug: string };
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const post = await getPost(params.slug);
if (!post) {
return {
title: '文章未找到',
};
}
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
type: 'article',
publishedTime: post.publishedAt,
authors: [post.author.name],
},
};
}
async function getPost(slug: string) {
// 获取文章数据的逻辑
}
export default async function BlogPostPage({ params }: Props) {
const post = await getPost(params.slug);
return <article>{/* 文章内容 */}</article>;
}
[locale]
路由实现:
import { setRequestLocale } from 'next-intl/server';
import { useTranslations } from 'next-intl';
interface Props {
params: { locale: string };
}
export default function HomePage({ params: { locale } }: Props) {
// 启用静态渲染
setRequestLocale(locale);
const t = useTranslations('HomePage');
return (
<div>
<h1>{t('title')}</h1>
<p>{t('description')}</p>
</div>
);
}
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
import { setRequestLocale } from 'next-intl/server';
interface Props {
children: React.ReactNode;
params: { locale: string };
}
export default async function LocaleLayout({
children,
params: { locale }
}: Props) {
// 启用静态渲染
setRequestLocale(locale);
const messages = await getMessages();
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
import { Metadata } from 'next';
import { getTranslations } from 'next-intl/server';
interface Props {
params: { locale: string };
}
export async function generateMetadata({ params: { locale } }: Props): Promise<Metadata> {
const t = await getTranslations({ locale, namespace: 'AboutPage' });
return {
title: t('meta.title'),
description: t('meta.description'),
};
}
export default function AboutPage({ params: { locale } }: Props) {
return <div>本地化的关于页面</div>;
}
// 推荐的页面组件结构
export default function ProductPage() {
return (
<div className="container mx-auto px-4 py-8">
{/* 页面头部 */}
<header className="mb-8">
<h1>页面标题</h1>
</header>
{/* 主要内容 */}
<main>
{/* 内容区域 */}
</main>
{/* 页面底部(如需要) */}
<footer className="mt-8">
{/* 页面相关操作 */}
</footer>
</div>
);
}
'use client';
import { useEffect } from 'react';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error(error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<h2 className="text-2xl font-bold mb-4">出错了!</h2>
<button
onClick={reset}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
重试
</button>
</div>
);
}
export default function Loading() {
return (
<div className="container mx-auto px-4 py-8">
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/4 mb-6"></div>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="h-48 bg-gray-200 rounded"></div>
))}
</div>
</div>
</div>
);
}