页面开发

本文档将指导您如何在 Codofly Template 中使用 Next.js 15 的 App Router 创建和管理页面。

Next.js App Router 基础

App 目录结构

Next.js 15 使用 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 - 页面组件,定义路由的 UI
  • layout.tsx - 布局组件,包装子页面
  • loading.tsx - 加载状态 UI
  • error.tsx - 错误边界 UI
  • not-found.tsx - 404 页面 UI
  • route.ts - API 路由处理器

创建新页面

基础页面创建

  1. app 目录下创建新文件夹(路由名称)
  2. 在文件夹内创建 page.tsx 文件
示例:创建产品页面
app/products/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>
  );
}

带参数的页面

使用方括号 [] 创建动态路由:
app/products/[id]/page.tsx
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>
  );
}

页面布局

根布局

根布局是必需的,包装所有页面:
app/layout.tsx
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>
  );
}

嵌套布局

为特定路由段创建专用布局:
app/dashboard/layout.tsx
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>
  );
}

动态路由

单个参数路由

app/users/[id]/page.tsx
interface Props {
  params: { id: string };
}

export default function UserPage({ params }: Props) {
  return <div>用户 ID: {params.id}</div>;
}

多个参数路由

app/users/[id]/posts/[postId]/page.tsx
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] 捕获多个路由段:
app/docs/[...slug]/page.tsx
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 等所有路径。

页面元数据

静态元数据

app/about/page.tsx
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>;
}

动态元数据

app/blog/[slug]/page.tsx
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>;
}

国际化页面

设置国际化路由

在 Codofly Template 中,国际化通过 [locale] 路由实现:
app/[locale]/page.tsx
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>
  );
}

国际化布局

app/[locale]/layout.tsx
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>
  );
}

多语言元数据

app/[locale]/about/page.tsx
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>;
}

最佳实践

1. 页面组件结构

// 推荐的页面组件结构
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>
  );
}

2. 错误处理

app/products/error.tsx
'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>
  );
}

3. 加载状态

app/products/loading.tsx
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>
  );
}

总结

通过 Next.js App Router,您可以:
  • 使用文件系统路由快速创建页面
  • 通过嵌套布局实现复杂的页面结构
  • 利用动态路由处理参数化页面
  • 通过元数据 API 优化 SEO
  • 实现完整的国际化支持
掌握这些概念后,您就能在 Codofly Template 中高效地创建和管理页面了。