> ## Documentation Index
> Fetch the complete documentation index at: https://codofly.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# 页面开发

> 学习如何在 Codofly Template 中使用 Next.js App Router 创建和管理页面

# 页面开发

本文档将指导您如何在 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 端点
```

### 路由规则

<Callout type="info">
  在 App Router 中，只有 `page.tsx` 文件会创建可访问的路由。其他特殊文件如 `layout.tsx`、`loading.tsx` 等有特定用途。
</Callout>

**特殊文件说明：**

* `page.tsx` - 页面组件，定义路由的 UI
* `layout.tsx` - 布局组件，包装子页面
* `loading.tsx` - 加载状态 UI
* `error.tsx` - 错误边界 UI
* `not-found.tsx` - 404 页面 UI
* `route.ts` - API 路由处理器

## 创建新页面

### 基础页面创建

1. 在 `app` 目录下创建新文件夹（路由名称）
2. 在文件夹内创建 `page.tsx` 文件

**示例：创建产品页面**

```typescript title="app/products/page.tsx" theme={null}
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>
  );
}
```

### 带参数的页面

使用方括号 `[]` 创建动态路由：

```typescript title="app/products/[id]/page.tsx" theme={null}
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>
  );
}
```

## 页面布局

### 根布局

根布局是必需的，包装所有页面：

```typescript title="app/layout.tsx" theme={null}
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>
  );
}
```

### 嵌套布局

为特定路由段创建专用布局：

```typescript title="app/dashboard/layout.tsx" theme={null}
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>
  );
}
```

## 动态路由

### 单个参数路由

```typescript title="app/users/[id]/page.tsx" theme={null}
interface Props {
  params: { id: string };
}

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

### 多个参数路由

```typescript title="app/users/[id]/posts/[postId]/page.tsx" theme={null}
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]` 捕获多个路由段：

```typescript title="app/docs/[...slug]/page.tsx" theme={null}
interface Props {
  params: { slug: string[] };
}

export default function DocsPage({ params }: Props) {
  const path = params.slug.join('/');
  return <div>文档路径: {path}</div>;
}
```

<Note>
  `[...slug]` 会匹配 `/docs/a`、`/docs/a/b`、`/docs/a/b/c` 等所有路径。
</Note>

## 页面元数据

### 静态元数据

```typescript title="app/about/page.tsx" theme={null}
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>;
}
```

### 动态元数据

```typescript title="app/blog/[slug]/page.tsx" theme={null}
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]` 路由实现：

```typescript title="app/[locale]/page.tsx" theme={null}
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>
  );
}
```

### 国际化布局

```typescript title="app/[locale]/layout.tsx" theme={null}
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>
  );
}
```

### 多语言元数据

```typescript title="app/[locale]/about/page.tsx" theme={null}
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. 页面组件结构

```typescript theme={null}
// 推荐的页面组件结构
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. 错误处理

```typescript title="app/products/error.tsx" theme={null}
'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. 加载状态

```typescript title="app/products/loading.tsx" theme={null}
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>
  );
}
```

<Callout type="warning">
  确保在使用客户端组件时添加 `'use client'` 指令，在使用服务器端功能时避免使用此指令。
</Callout>

## 总结

通过 Next.js App Router，您可以：

* 使用文件系统路由快速创建页面
* 通过嵌套布局实现复杂的页面结构
* 利用动态路由处理参数化页面
* 通过元数据 API 优化 SEO
* 实现完整的国际化支持

掌握这些概念后，您就能在 Codofly Template 中高效地创建和管理页面了。
