page-layout-builder

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Page Layout Builder

页面布局构建器

Generate production-ready page layouts with routing, navigation, and state patterns.
生成可用于生产环境的页面布局,包含路由、导航和状态管理模式。

Core Workflow

核心工作流程

  1. Choose page type: Dashboard, auth, settings, CRUD, landing, etc.
  2. Setup routing: Create route files with proper structure
  3. Build layout: Header, sidebar, main content, footer
  4. Add navigation: Nav menus, breadcrumbs, tabs
  5. State placeholders: Data fetching, forms, modals
  6. Responsive design: Mobile-first with breakpoints
  7. Loading states: Skeletons and suspense boundaries
  1. 选择页面类型:仪表盘、认证页、设置页、CRUD页、落地页等。
  2. 配置路由:创建结构规范的路由文件
  3. 构建布局:页头、侧边栏、主内容区、页脚
  4. 添加导航:导航菜单、面包屑、标签页
  5. 状态占位符:数据获取、表单、模态框
  6. 响应式设计:移动端优先,适配断点
  7. 加载状态:骨架屏和Suspense边界

Common Page Patterns

常见页面模式

Dashboard Layout

仪表盘布局

typescript
// app/dashboard/layout.tsx
import { Sidebar } from "@/components/Sidebar";
import { Header } from "@/components/Header";

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex h-screen bg-gray-50">
      {/* Sidebar - Hidden on mobile, shown on desktop */}
      <Sidebar className="hidden lg:flex lg:w-64 lg:flex-col" />

      {/* Main Content Area */}
      <div className="flex flex-1 flex-col overflow-hidden">
        {/* Header */}
        <Header />

        {/* Page Content */}
        <main className="flex-1 overflow-y-auto p-4 md:p-6 lg:p-8">
          {children}
        </main>
      </div>
    </div>
  );
}
typescript
// app/dashboard/page.tsx
import { StatsCard } from "@/components/dashboard/StatsCard";
import { RecentActivity } from "@/components/dashboard/RecentActivity";
import { Chart } from "@/components/dashboard/Chart";

export default function DashboardPage() {
  return (
    <div className="space-y-6">
      <div>
        <h1 className="text-3xl font-bold">Dashboard</h1>
        <p className="text-gray-600">Welcome back! Here's your overview.</p>
      </div>

      {/* Stats Grid */}
      <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
        <StatsCard title="Total Users" value="2,543" change="+12%" trend="up" />
        <StatsCard title="Revenue" value="$45,231" change="+8%" trend="up" />
        <StatsCard
          title="Active Sessions"
          value="431"
          change="-3%"
          trend="down"
        />
        <StatsCard
          title="Conversion Rate"
          value="3.2%"
          change="+0.5%"
          trend="up"
        />
      </div>

      {/* Charts and Activity */}
      <div className="grid gap-6 md:grid-cols-2">
        <Chart title="Revenue Over Time" />
        <RecentActivity title="Recent Activity" />
      </div>
    </div>
  );
}
typescript
// app/dashboard/layout.tsx
import { Sidebar } from "@/components/Sidebar";
import { Header } from "@/components/Header";

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex h-screen bg-gray-50">
      {/* Sidebar - Hidden on mobile, shown on desktop */}
      <Sidebar className="hidden lg:flex lg:w-64 lg:flex-col" />

      {/* Main Content Area */}
      <div className="flex flex-1 flex-col overflow-hidden">
        {/* Header */}
        <Header />

        {/* Page Content */}
        <main className="flex-1 overflow-y-auto p-4 md:p-6 lg:p-8">
          {children}
        </main>
      </div>
    </div>
  );
}
typescript
// app/dashboard/page.tsx
import { StatsCard } from "@/components/dashboard/StatsCard";
import { RecentActivity } from "@/components/dashboard/RecentActivity";
import { Chart } from "@/components/dashboard/Chart";

export default function DashboardPage() {
  return (
    <div className="space-y-6">
      <div>
        <h1 className="text-3xl font-bold">Dashboard</h1>
        <p className="text-gray-600">Welcome back! Here's your overview.</p>
      </div>

      {/* Stats Grid */}
      <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
        <StatsCard title="Total Users" value="2,543" change="+12%" trend="up" />
        <StatsCard title="Revenue" value="$45,231" change="+8%" trend="up" />
        <StatsCard
          title="Active Sessions"
          value="431"
          change="-3%"
          trend="down"
        />
        <StatsCard
          title="Conversion Rate"
          value="3.2%"
          change="+0.5%"
          trend="up"
        />
      </div>

      {/* Charts and Activity */}
      <div className="grid gap-6 md:grid-cols-2">
        <Chart title="Revenue Over Time" />
        <RecentActivity title="Recent Activity" />
      </div>
    </div>
  );
}

Authentication Pages

认证页面

typescript
// app/(auth)/layout.tsx
export default function AuthLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex min-h-screen">
      {/* Left side - Branding (hidden on mobile) */}
      <div className="hidden lg:flex lg:w-1/2 lg:flex-col lg:justify-center lg:bg-primary-500 lg:p-12">
        <div className="text-white">
          <h1 className="text-4xl font-bold">Welcome to AppName</h1>
          <p className="mt-4 text-lg text-primary-100">
            The best platform for managing your workflow
          </p>
        </div>
      </div>

      {/* Right side - Auth form */}
      <div className="flex flex-1 flex-col justify-center px-4 py-12 sm:px-6 lg:px-20 xl:px-24">
        <div className="mx-auto w-full max-w-sm">{children}</div>
      </div>
    </div>
  );
}
typescript
// app/(auth)/login/page.tsx
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import Link from "next/link";

export default function LoginPage() {
  const router = useRouter();
  const [isLoading, setIsLoading] = useState(false);

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setIsLoading(true);

    // TODO: Implement authentication logic
    try {
      // await signIn(formData);
      router.push("/dashboard");
    } catch (error) {
      console.error("Login failed:", error);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div className="space-y-6">
      <div className="space-y-2 text-center">
        <h1 className="text-3xl font-bold">Sign In</h1>
        <p className="text-gray-600">
          Enter your credentials to access your account
        </p>
      </div>

      <form onSubmit={handleSubmit} className="space-y-4">
        <div className="space-y-2">
          <Label htmlFor="email">Email</Label>
          <Input
            id="email"
            type="email"
            placeholder="you@example.com"
            required
          />
        </div>

        <div className="space-y-2">
          <div className="flex items-center justify-between">
            <Label htmlFor="password">Password</Label>
            <Link
              href="/forgot-password"
              className="text-sm text-primary-600 hover:underline"
            >
              Forgot password?
            </Link>
          </div>
          <Input
            id="password"
            type="password"
            placeholder="••••••••"
            required
          />
        </div>

        <Button type="submit" className="w-full" isLoading={isLoading}>
          Sign In
        </Button>
      </form>

      <div className="text-center text-sm">
        Don't have an account?{" "}
        <Link href="/register" className="text-primary-600 hover:underline">
          Sign up
        </Link>
      </div>
    </div>
  );
}
typescript
// app/(auth)/layout.tsx
export default function AuthLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex min-h-screen">
      {/* Left side - Branding (hidden on mobile) */}
      <div className="hidden lg:flex lg:w-1/2 lg:flex-col lg:justify-center lg:bg-primary-500 lg:p-12">
        <div className="text-white">
          <h1 className="text-4xl font-bold">Welcome to AppName</h1>
          <p className="mt-4 text-lg text-primary-100">
            The best platform for managing your workflow
          </p>
        </div>
      </div>

      {/* Right side - Auth form */}
      <div className="flex flex-1 flex-col justify-center px-4 py-12 sm:px-6 lg:px-20 xl:px-24">
        <div className="mx-auto w-full max-w-sm">{children}</div>
      </div>
    </div>
  );
}
typescript
// app/(auth)/login/page.tsx
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import Link from "next/link";

export default function LoginPage() {
  const router = useRouter();
  const [isLoading, setIsLoading] = useState(false);

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setIsLoading(true);

    // TODO: Implement authentication logic
    try {
      // await signIn(formData);
      router.push("/dashboard");
    } catch (error) {
      console.error("Login failed:", error);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div className="space-y-6">
      <div className="space-y-2 text-center">
        <h1 className="text-3xl font-bold">Sign In</h1>
        <p className="text-gray-600">
          Enter your credentials to access your account
        </p>
      </div>

      <form onSubmit={handleSubmit} className="space-y-4">
        <div className="space-y-2">
          <Label htmlFor="email">Email</Label>
          <Input
            id="email"
            type="email"
            placeholder="you@example.com"
            required
          />
        </div>

        <div className="space-y-2">
          <div className="flex items-center justify-between">
            <Label htmlFor="password">Password</Label>
            <Link
              href="/forgot-password"
              className="text-sm text-primary-600 hover:underline"
            >
              Forgot password?
            </Link>
          </div>
          <Input
            id="password"
            type="password"
            placeholder="••••••••"
            required
          />
        </div>

        <Button type="submit" className="w-full" isLoading={isLoading}>
          Sign In
        </Button>
      </form>

      <div className="text-center text-sm">
        Don't have an account?{" "}
        <Link href="/register" className="text-primary-600 hover:underline">
          Sign up
        </Link>
      </div>
    </div>
  );
}

Settings/Profile Page

设置/个人资料页面

typescript
// app/settings/layout.tsx
import { SettingsSidebar } from "@/components/settings/SettingsSidebar";

export default function SettingsLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="container mx-auto py-6">
      <div className="mb-6">
        <h1 className="text-3xl font-bold">Settings</h1>
        <p className="text-gray-600">
          Manage your account settings and preferences
        </p>
      </div>

      <div className="flex flex-col gap-6 lg:flex-row">
        <SettingsSidebar className="lg:w-64" />
        <div className="flex-1">{children}</div>
      </div>
    </div>
  );
}
typescript
// app/settings/profile/page.tsx
"use client";

import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card } from "@/components/ui/card";

export default function ProfileSettingsPage() {
  const [isSaving, setIsSaving] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setIsSaving(true);

    // TODO: Save profile changes

    setIsSaving(false);
  };

  return (
    <div className="space-y-6">
      <Card className="p-6">
        <form onSubmit={handleSubmit} className="space-y-6">
          <div>
            <h2 className="text-xl font-semibold">Profile Information</h2>
            <p className="text-sm text-gray-600">
              Update your account's profile information and email address
            </p>
          </div>

          <div className="space-y-4">
            <div className="space-y-2">
              <Label htmlFor="name">Name</Label>
              <Input id="name" placeholder="Your name" />
            </div>

            <div className="space-y-2">
              <Label htmlFor="email">Email</Label>
              <Input id="email" type="email" placeholder="you@example.com" />
            </div>

            <div className="space-y-2">
              <Label htmlFor="bio">Bio</Label>
              <textarea
                id="bio"
                rows={4}
                className="w-full rounded-md border border-gray-300 p-3"
                placeholder="Tell us about yourself"
              />
            </div>
          </div>

          <div className="flex justify-end">
            <Button type="submit" isLoading={isSaving}>
              Save Changes
            </Button>
          </div>
        </form>
      </Card>
    </div>
  );
}
typescript
// app/settings/layout.tsx
import { SettingsSidebar } from "@/components/settings/SettingsSidebar";

export default function SettingsLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="container mx-auto py-6">
      <div className="mb-6">
        <h1 className="text-3xl font-bold">Settings</h1>
        <p className="text-gray-600">
          Manage your account settings and preferences
        </p>
      </div>

      <div className="flex flex-col gap-6 lg:flex-row">
        <SettingsSidebar className="lg:w-64" />
        <div className="flex-1">{children}</div>
      </div>
    </div>
  );
}
typescript
// app/settings/profile/page.tsx
"use client";

import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card } from "@/components/ui/card";

export default function ProfileSettingsPage() {
  const [isSaving, setIsSaving] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setIsSaving(true);

    // TODO: Save profile changes

    setIsSaving(false);
  };

  return (
    <div className="space-y-6">
      <Card className="p-6">
        <form onSubmit={handleSubmit} className="space-y-6">
          <div>
            <h2 className="text-xl font-semibold">Profile Information</h2>
            <p className="text-sm text-gray-600">
              Update your account's profile information and email address
            </p>
          </div>

          <div className="space-y-4">
            <div className="space-y-2">
              <Label htmlFor="name">Name</Label>
              <Input id="name" placeholder="Your name" />
            </div>

            <div className="space-y-2">
              <Label htmlFor="email">Email</Label>
              <Input id="email" type="email" placeholder="you@example.com" />
            </div>

            <div className="space-y-2">
              <Label htmlFor="bio">Bio</Label>
              <textarea
                id="bio"
                rows={4}
                className="w-full rounded-md border border-gray-300 p-3"
                placeholder="Tell us about yourself"
              />
            </div>
          </div>

          <div className="flex justify-end">
            <Button type="submit" isLoading={isSaving}>
              Save Changes
            </Button>
          </div>
        </form>
      </Card>
    </div>
  );
}

CRUD Page (List/Create/Edit/Delete)

CRUD页面(列表/创建/编辑/删除)

typescript
// app/users/page.tsx
"use client";

import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Table } from "@/components/ui/table";
import { CreateUserModal } from "@/components/users/CreateUserModal";
import { DeleteConfirmDialog } from "@/components/ui/DeleteConfirmDialog";

export default function UsersPage() {
  const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
  const [searchQuery, setSearchQuery] = useState("");

  return (
    <div className="space-y-6">
      {/* Header */}
      <div className="flex items-center justify-between">
        <div>
          <h1 className="text-3xl font-bold">Users</h1>
          <p className="text-gray-600">Manage your team members</p>
        </div>
        <Button onClick={() => setIsCreateModalOpen(true)}>Add User</Button>
      </div>

      {/* Filters */}
      <div className="flex gap-4">
        <Input
          placeholder="Search users..."
          value={searchQuery}
          onChange={(e) => setSearchQuery(e.target.value)}
          className="max-w-sm"
        />
      </div>

      {/* Table */}
      <div className="rounded-lg border">
        {/* TODO: Implement table with data */}
      </div>

      {/* Modals */}
      <CreateUserModal
        isOpen={isCreateModalOpen}
        onClose={() => setIsCreateModalOpen(false)}
      />
    </div>
  );
}
typescript
// app/users/page.tsx
"use client";

import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Table } from "@/components/ui/table";
import { CreateUserModal } from "@/components/users/CreateUserModal";
import { DeleteConfirmDialog } from "@/components/ui/DeleteConfirmDialog";

export default function UsersPage() {
  const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
  const [searchQuery, setSearchQuery] = useState("");

  return (
    <div className="space-y-6">
      {/* Header */}
      <div className="flex items-center justify-between">
        <div>
          <h1 className="text-3xl font-bold">Users</h1>
          <p className="text-gray-600">Manage your team members</p>
        </div>
        <Button onClick={() => setIsCreateModalOpen(true)}>Add User</Button>
      </div>

      {/* Filters */}
      <div className="flex gap-4">
        <Input
          placeholder="Search users..."
          value={searchQuery}
          onChange={(e) => setSearchQuery(e.target.value)}
          className="max-w-sm"
        />
      </div>

      {/* Table */}
      <div className="rounded-lg border">
        {/* TODO: Implement table with data */}
      </div>

      {/* Modals */}
      <CreateUserModal
        isOpen={isCreateModalOpen}
        onClose={() => setIsCreateModalOpen(false)}
      />
    </div>
  );
}

Navigation Components

导航组件

Sidebar Navigation

侧边栏导航

typescript
// components/Sidebar.tsx
"use client";

import Link from "next/link";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
import {
  HomeIcon,
  UsersIcon,
  SettingsIcon,
  ChartBarIcon,
} from "@/components/icons";

const navigation = [
  { name: "Dashboard", href: "/dashboard", icon: HomeIcon },
  { name: "Users", href: "/users", icon: UsersIcon },
  { name: "Analytics", href: "/analytics", icon: ChartBarIcon },
  { name: "Settings", href: "/settings", icon: SettingsIcon },
];

export function Sidebar({ className }: { className?: string }) {
  const pathname = usePathname();

  return (
    <aside className={cn("border-r bg-white", className)}>
      <div className="flex h-16 items-center px-6">
        <span className="text-xl font-bold">AppName</span>
      </div>

      <nav className="space-y-1 px-3">
        {navigation.map((item) => {
          const isActive = pathname === item.href;
          return (
            <Link
              key={item.name}
              href={item.href}
              className={cn(
                "flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors",
                isActive
                  ? "bg-primary-50 text-primary-600"
                  : "text-gray-700 hover:bg-gray-100"
              )}
            >
              <item.icon className="h-5 w-5" />
              {item.name}
            </Link>
          );
        })}
      </nav>
    </aside>
  );
}
typescript
// components/Sidebar.tsx
"use client";

import Link from "next/link";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
import {
  HomeIcon,
  UsersIcon,
  SettingsIcon,
  ChartBarIcon,
} from "@/components/icons";

const navigation = [
  { name: "Dashboard", href: "/dashboard", icon: HomeIcon },
  { name: "Users", href: "/users", icon: UsersIcon },
  { name: "Analytics", href: "/analytics", icon: ChartBarIcon },
  { name: "Settings", href: "/settings", icon: SettingsIcon },
];

export function Sidebar({ className }: { className?: string }) {
  const pathname = usePathname();

  return (
    <aside className={cn("border-r bg-white", className)}>
      <div className="flex h-16 items-center px-6">
        <span className="text-xl font-bold">AppName</span>
      </div>

      <nav className="space-y-1 px-3">
        {navigation.map((item) => {
          const isActive = pathname === item.href;
          return (
            <Link
              key={item.name}
              href={item.href}
              className={cn(
                "flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors",
                isActive
                  ? "bg-primary-50 text-primary-600"
                  : "text-gray-700 hover:bg-gray-100"
              )}
            >
              <item.icon className="h-5 w-5" />
              {item.name}
            </Link>
          );
        })}
      </nav>
    </aside>
  );
}

Header with User Menu

带用户菜单的页头

typescript
// components/Header.tsx
"use client";

import { Button } from "@/components/ui/button";
import { Avatar } from "@/components/ui/avatar";
import { DropdownMenu } from "@/components/ui/dropdown-menu";
import { BellIcon, MenuIcon } from "@/components/icons";

export function Header() {
  return (
    <header className="flex h-16 items-center justify-between border-b bg-white px-4 md:px-6">
      {/* Mobile menu button */}
      <Button variant="ghost" size="icon" className="lg:hidden">
        <MenuIcon className="h-6 w-6" />
      </Button>

      {/* Search (optional) */}
      <div className="flex-1 px-4">{/* Search component */}</div>

      {/* Right side actions */}
      <div className="flex items-center gap-4">
        <Button variant="ghost" size="icon">
          <BellIcon className="h-5 w-5" />
        </Button>

        <DropdownMenu>
          <Avatar src="/avatar.jpg" alt="User" />
        </DropdownMenu>
      </div>
    </header>
  );
}
typescript
// components/Header.tsx
"use client";

import { Button } from "@/components/ui/button";
import { Avatar } from "@/components/ui/avatar";
import { DropdownMenu } from "@/components/ui/dropdown-menu";
import { BellIcon, MenuIcon } from "@/components/icons";

export function Header() {
  return (
    <header className="flex h-16 items-center justify-between border-b bg-white px-4 md:px-6">
      {/* Mobile menu button */}
      <Button variant="ghost" size="icon" className="lg:hidden">
        <MenuIcon className="h-6 w-6" />
      </Button>

      {/* Search (optional) */}
      <div className="flex-1 px-4">{/* Search component */}</div>

      {/* Right side actions */}
      <div className="flex items-center gap-4">
        <Button variant="ghost" size="icon">
          <BellIcon className="h-5 w-5" />
        </Button>

        <DropdownMenu>
          <Avatar src="/avatar.jpg" alt="User" />
        </DropdownMenu>
      </div>
    </header>
  );
}

Routing Structure

路由结构

Next.js App Router

Next.js App Router

app/
├── (auth)/                 # Auth group (no dashboard layout)
│   ├── layout.tsx
│   ├── login/
│   │   └── page.tsx
│   ├── register/
│   │   └── page.tsx
│   └── forgot-password/
│       └── page.tsx
├── dashboard/             # Dashboard section
│   ├── layout.tsx
│   └── page.tsx
├── users/                 # CRUD section
│   ├── page.tsx           # List
│   ├── [id]/
│   │   ├── page.tsx       # Detail/Edit
│   │   └── loading.tsx
│   └── new/
│       └── page.tsx       # Create
├── settings/              # Settings section
│   ├── layout.tsx
│   ├── profile/
│   │   └── page.tsx
│   ├── security/
│   │   └── page.tsx
│   └── notifications/
│       └── page.tsx
└── layout.tsx             # Root layout
app/
├── (auth)/                 # Auth group (no dashboard layout)
│   ├── layout.tsx
│   ├── login/
│   │   └── page.tsx
│   ├── register/
│   │   └── page.tsx
│   └── forgot-password/
│       └── page.tsx
├── dashboard/             # Dashboard section
│   ├── layout.tsx
│   └── page.tsx
├── users/                 # CRUD section
│   ├── page.tsx           # List
│   ├── [id]/
│   │   ├── page.tsx       # Detail/Edit
│   │   └── loading.tsx
│   └── new/
│       └── page.tsx       # Create
├── settings/              # Settings section
│   ├── layout.tsx
│   ├── profile/
│   │   └── page.tsx
│   ├── security/
│   │   └── page.tsx
│   └── notifications/
│       └── page.tsx
└── layout.tsx             # Root layout

State Management Patterns

状态管理模式

Data Fetching Pattern

数据获取模式

typescript
// app/users/page.tsx
import { Suspense } from "react";
import { UsersTable } from "@/components/users/UsersTable";
import { UsersTableSkeleton } from "@/components/users/UsersTableSkeleton";

async function getUsers() {
  // Server-side data fetching
  const res = await fetch("https://api.example.com/users", {
    cache: "no-store",
  });
  return res.json();
}

export default async function UsersPage() {
  const users = await getUsers();

  return (
    <div className="space-y-6">
      <h1 className="text-3xl font-bold">Users</h1>

      <Suspense fallback={<UsersTableSkeleton />}>
        <UsersTable data={users} />
      </Suspense>
    </div>
  );
}
typescript
// app/users/page.tsx
import { Suspense } from "react";
import { UsersTable } from "@/components/users/UsersTable";
import { UsersTableSkeleton } from "@/components/users/UsersTableSkeleton";

async function getUsers() {
  // Server-side data fetching
  const res = await fetch("https://api.example.com/users", {
    cache: "no-store",
  });
  return res.json();
}

export default async function UsersPage() {
  const users = await getUsers();

  return (
    <div className="space-y-6">
      <h1 className="text-3xl font-bold">Users</h1>

      <Suspense fallback={<UsersTableSkeleton />}>
        <UsersTable data={users} />
      </Suspense>
    </div>
  );
}

Client-Side State

客户端状态

typescript
"use client";

import { useState, useEffect } from "react";
import { useUsers } from "@/hooks/useUsers";

export default function UsersPage() {
  const { users, isLoading, error } = useUsers();
  const [selectedUser, setSelectedUser] = useState(null);

  if (isLoading) return <LoadingState />;
  if (error) return <ErrorState error={error} />;

  return <div>{/* Page content */}</div>;
}
typescript
"use client";

import { useState, useEffect } from "react";
import { useUsers } from "@/hooks/useUsers";

export default function UsersPage() {
  const { users, isLoading, error } = useUsers();
  const [selectedUser, setSelectedUser] = useState(null);

  if (isLoading) return <LoadingState />;
  if (error) return <ErrorState error={error} />;

  return <div>{/* Page content */}</div>;
}

Loading States

加载状态

Skeleton Screens

骨架屏

typescript
// components/dashboard/DashboardSkeleton.tsx
export function DashboardSkeleton() {
  return (
    <div className="space-y-6 animate-pulse">
      <div className="h-8 w-48 bg-gray-200 rounded" />

      <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
        {Array.from({ length: 4 }).map((_, i) => (
          <div key={i} className="h-32 bg-gray-200 rounded-lg" />
        ))}
      </div>

      <div className="grid gap-6 md:grid-cols-2">
        <div className="h-64 bg-gray-200 rounded-lg" />
        <div className="h-64 bg-gray-200 rounded-lg" />
      </div>
    </div>
  );
}
typescript
// components/dashboard/DashboardSkeleton.tsx
export function DashboardSkeleton() {
  return (
    <div className="space-y-6 animate-pulse">
      <div className="h-8 w-48 bg-gray-200 rounded" />

      <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
        {Array.from({ length: 4 }).map((_, i) => (
          <div key={i} className="h-32 bg-gray-200 rounded-lg" />
        ))}
      </div>

      <div className="grid gap-6 md:grid-cols-2">
        <div className="h-64 bg-gray-200 rounded-lg" />
        <div className="h-64 bg-gray-200 rounded-lg" />
      </div>
    </div>
  );
}

Loading Component

加载组件

typescript
// app/dashboard/loading.tsx
import { DashboardSkeleton } from "@/components/dashboard/DashboardSkeleton";

export default function Loading() {
  return <DashboardSkeleton />;
}
typescript
// app/dashboard/loading.tsx
import { DashboardSkeleton } from "@/components/dashboard/DashboardSkeleton";

export default function Loading() {
  return <DashboardSkeleton />;
}

Responsive Patterns

响应式模式

Mobile Navigation

移动端导航

typescript
"use client";

import { useState } from "react";
import { Sheet } from "@/components/ui/sheet";

export function MobileNav() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <>
      <button onClick={() => setIsOpen(true)} className="lg:hidden">
        <MenuIcon />
      </button>

      <Sheet isOpen={isOpen} onClose={() => setIsOpen(false)}>
        <nav className="space-y-2 p-4">{/* Navigation items */}</nav>
      </Sheet>
    </>
  );
}
typescript
"use client";

import { useState } from "react";
import { Sheet } from "@/components/ui/sheet";

export function MobileNav() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <>
      <button onClick={() => setIsOpen(true)} className="lg:hidden">
        <MenuIcon />
      </button>

      <Sheet isOpen={isOpen} onClose={() => setIsOpen(false)}>
        <nav className="space-y-2 p-4">{/* Navigation items */}</nav>
      </Sheet>
    </>
  );
}

Best Practices

最佳实践

  1. Consistent layouts: Use layout files for shared structure
  2. Route groups: Organize related pages with (groupName)
  3. Loading states: Add loading.tsx for automatic suspense
  4. Error boundaries: Add error.tsx for error handling
  5. Mobile-first: Design for mobile, enhance for desktop
  6. Accessibility: Semantic HTML, ARIA labels, keyboard nav
  7. SEO: Use metadata, proper heading hierarchy
  8. Performance: Code splitting, lazy loading, optimized images
  1. 统一布局:使用layout文件实现共享结构
  2. 路由组:通过(groupName)组织相关页面
  3. 加载状态:添加loading.tsx自动启用Suspense
  4. 错误边界:添加error.tsx处理错误
  5. 移动端优先:先为移动端设计,再为桌面端增强
  6. 可访问性:语义化HTML、ARIA标签、键盘导航
  7. SEO优化:使用元数据、合理的标题层级
  8. 性能优化:代码分割、懒加载、图片优化

Output Checklist

输出检查清单

Every page layout should include:
  • Proper route structure with layout files
  • Responsive navigation (sidebar + mobile menu)
  • Header with actions
  • Main content area with proper spacing
  • Loading states (skeletons)
  • Empty states
  • Error boundaries
  • State management placeholders
  • Breadcrumbs or page headers
  • Mobile-responsive breakpoints
每个页面布局都应包含:
  • 带layout文件的规范路由结构
  • 响应式导航(侧边栏+移动端菜单)
  • 带操作项的页头
  • 间距规范的主内容区
  • 加载状态(骨架屏)
  • 空状态
  • 错误边界
  • 状态管理占位符
  • 面包屑或页面标题
  • 移动端响应断点