基于Next.js的旅游预订网站前端源码全栈实战教程与部署指南

引言:为什么选择Next.js构建旅游预订网站?
在当今的Web开发领域,Next.js已经成为构建高性能、SEO友好的React应用的首选框架。对于旅游预订网站这样的复杂应用,Next.js提供了许多独特的优势。
旅游预订网站通常具有以下特点:
SEO要求高
:需要被搜索引擎良好索引,吸引自然流量
数据密集
:涉及大量的旅游产品、目的地、价格等数据
用户体验要求高
:需要快速加载、流畅交互
多用户角色
:游客、注册用户、管理员等
Next.js通过其混合渲染模式(SSR + SSG)完美解决了这些需求。例如,首页和目的地页面可以使用SSG预渲染,而用户个人中心则使用SSR动态渲染。
1. 项目初始化与技术栈选择
1.1 创建Next.js项目
首先,我们需要创建一个基于TypeScript的Next.js项目:
# 使用create-next-app创建项目
npx create-next-app@latest travel-booking-app –typescript –tailwind –eslint –app

# 进入项目目录
cd travel-booking-app

# 安装必要的依赖
npm install react-icons react-hot-toast zustand
npm install -D @types/react-datepicker react-datepicker
1.2 项目结构规划
一个典型的旅游预订网站应该具有以下目录结构:
travel-booking-app/
├── app/
│ ├── (main)/
│ │ ├── page.tsx # 首页
│ │ ├── destinations/ # 目的地页面
│ │ ├── tours/ # 旅游产品页面
│ │ └── about/
│ ├── (auth)/
│ │ ├── login/page.tsx
│ │ └── register/page.tsx
│ ├── (dashboard)/
│ │ ├── bookings/page.tsx # 用户预订管理
│ │ └── profile/page.tsx
│ ├── api/
│ │ ├── auth/
│ │ ├── bookings/
│ │ └── tours/
│ ├── layout.tsx
│ └── page.tsx
├── components/
│ ├── ui/ # 可复用UI组件
│ ├── layout/ # 布局组件
│ └── tour/ # 旅游相关组件
├── lib/
│ ├── database.ts # 数据库连接
│ └── utils.ts # 工具函数
├── types/
│ └── index.ts # TypeScript类型定义
├── public/ # 静态资源
└── styles/ # 全局样式
1.3 配置Tailwind CSS
在
tailwind.config.ts
中添加旅游网站的自定义主题:
import type { Config } from “tailwindcss”;

const config: Config = {
content: [
“./pages/**/*.{js,ts,jsx,tsx,mdx}”,
“./components/**/*.{js,ts,jsx,tsx,mdx}”,
“./app/**/*.{js,ts,jsx,tsx,mdx}”,
],
theme: {
extend: {
colors: {
primary: {
50: “#eff6ff”,
100: “#dbeafe”,
500: “#3b82f6”,
600: “#2563eb”,
700: “#1d4ed8”,
},
secondary: {
500: “#10b981”,
600: “#059669”,
},
},
fontFamily: {
sans: [“Inter”, “system-ui”, “sans-serif”],
},
},
},
plugins: [],
};
export default config;
2. 数据模型与TypeScript类型定义
2.1 核心数据类型
在
types/index.ts
中定义旅游网站的核心数据结构:
// 用户类型
export interface User {
id: string;
email: string;
name: string;
role: “user” | “admin”;
createdAt: Date;
updatedAt: Date;
}

// 旅游产品类型
export interface Tour {
id: string;
title: string;
description: string;
destination: string;
price: number;
duration: number; // 天数
images: string[];
highlights: string[];
itinerary: ItineraryItem[];
availableDates: Date[];
maxGroupSize: number;
difficulty: “easy” | “medium” | “hard”;
rating: number;
reviewsCount: number;
}

export interface ItineraryItem {
day: number;
title: string;
description: string;
activities: string[];
}

// 预订类型
export interface Booking {
id: string;
userId: string;
tourId: string;
tour: Tour; // 关联的旅游产品
date: Date;
participants: number;
totalPrice: number;
status: “pending” | “confirmed” | “cancelled”;
specialRequests?: string;
createdAt: Date;
}

// 搜索参数类型
export interface SearchParams {
destination?: string;
date?: Date;
minPrice?: number;
maxPrice?: number;
difficulty?: string;
duration?: number;
}
3. 数据库集成(使用Prisma + PostgreSQL)
3.1 安装和配置Prisma
# 安装Prisma
npm install -D prisma
npm install @prisma/client

# 初始化Prisma
npx prisma init
在
.env
文件中配置数据库连接:
DATABASE_URL=”postgresql://username:password@localhost:5432/travel_booking?schema=public”
JWT_SECRET=”your-super-secret-jwt-key”
3.2 Prisma Schema定义
在
prisma/schema.prisma
中定义数据模型:
// prisma/schema.prisma

generator client {
provider = “prisma-client-js”
}

datasource db {
provider = “postgresql”
url = env(“DATABASE_URL”)
}

model User {
id String @id @default(uuid())
email String @unique
name String
password String
role Role @default(USER)
bookings Booking[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@map(“users”)
}

model Tour {
id String @id @default(uuid())
title String
description String
destination String
price Float
duration Int
images String[]
highlights String[]
itinerary Json // 存储行程安排
availableDates DateTime[]
maxGroupSize Int
difficulty String
rating Float @default(0)
reviewsCount Int @default(0)
bookings Booking[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@map(“tours”)
}

model Booking {
id String @id @default(uuid())
userId String
tourId String
date DateTime
participants Int
totalPrice Float
status BookingStatus @default(PENDING)
specialRequests String?
user User @relation(fields: [userId], references: [id])
tour Tour @relation(fields: [tourId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@map(“bookings”)
}

enum Role {
USER
ADMIN
}

enum BookingStatus {
PENDING
CONFIRMED
CANCELLED
}
3.3 生成Prisma客户端和数据库迁移
# 生成Prisma客户端
npx prisma generate

# 创建数据库迁移
npx prisma migrate dev –name init

# 可选:填充测试数据
npx prisma db seed
3.4 创建数据库连接工具
在
lib/database.ts
中:
import { PrismaClient } from ‘@prisma/client’

const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}

export const prisma = globalForPrisma.prisma ?? new PrismaClient()

if (process.env.NODE_ENV !== ‘production’) globalForPrisma.prisma = prisma
4. 认证系统实现(JWT + NextAuth)
4.1 安装NextAuth
npm install next-auth @next-auth/prisma-adapter
4.2 配置NextAuth
创建
app/api/auth/[…nextauth]/route.ts
:
import NextAuth from “next-auth”
import CredentialsProvider from “next-auth/providers/credentials”
import { PrismaAdapter } from “@next-auth/prisma-adapter”
import { prisma } from “@/lib/database”
import bcrypt from “bcryptjs”

const handler = NextAuth({
adapter: PrismaAdapter(prisma),
providers: [
CredentialsProvider({
name: “credentials”,
credentials: {
email: { label: “Email”, type: “email” },
password: { label: “Password”, type: “password” }
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
throw new Error(“Email and password required”)
}

const user = await prisma.user.findUnique({
where: { email: credentials.email as string }
})

if (!user || !user.password) {
throw new Error(“User not found”)
}

const isValid = await bcrypt.compare(
credentials.password as string,
user.password
)

if (!isValid) {
throw new Error(“Invalid password”)
}

return {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
}
}
})
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.role = user.role
token.id = user.id
}
return token
},
async session({ session, token }) {
if (session.user) {
session.user.role = token.role
session.user.id = token.id as string
}
return session
}
},
pages: {
signIn: “/login”,
},
session: {
strategy: “jwt”,
},
secret: process.env.JWT_SECRET,
})

export { handler as GET, handler as POST }
4.3 创建注册API
创建
app/api/auth/register/route.ts
:
import { NextRequest, NextResponse } from “next/server”
import bcrypt from “bcryptjs”
import { prisma } from “@/lib/database”

export async function POST(request: NextRequest) {
try {
const { email, name, password } = await request.json()

// 验证输入
if (!email || !name || !password) {
return NextResponse.json(
{ error: “Missing required fields” },
{ status: 400 }
)
}

// 检查用户是否已存在
const existingUser = await prisma.user.findUnique({
where: { email }
})

if (existingUser) {
return NextResponse.json(
{ error: “User already exists” },
{ status: 400 }
)
}

// 哈希密码
const hashedPassword = await bcrypt.hash(password, 12)

// 创建用户
const user = await prisma.user.create({
data: {
email,
name,
password: hashedPassword,
role: “USER”
}
})

// 返回用户信息(不包含密码)
const { password: _, …userWithoutPassword } = user

return NextResponse.json(
{ message: “User created successfully”, user: userWithoutPassword },
{ status: 201 }
)

} catch (error) {
console.error(“Registration error:”, error)
return NextResponse.json(
{ error: “Internal server error” },
{ status: 500 }
)
}
}
5. 核心页面开发
5.1 主页(SSG预渲染)
创建
app/(main)/page.tsx
:
import { prisma } from “@/lib/database”
import { Tour } from “@/types”
import Image from “next/image”
import Link from “next/link”
import { FaStar, FaClock, FaUsers } from “react-icons/fa”

// 使用SSG预渲染首页数据
export async function generateStaticParams() {
// 预取热门旅游产品
const tours = await prisma.tour.findMany({
take: 6,
orderBy: { rating: “desc” },
select: {
id: true,
title: true,
destination: true,
price: true,
images: true,
rating: true,
duration: true,
}
})
return tours.map(tour => ({ id: tour.id }))
}

export default async function HomePage() {
// 获取热门目的地和推荐旅游
const [featuredTours, topDestinations] = await Promise.all([
prisma.tour.findMany({
take: 6,
orderBy: { rating: “desc” },
include: {
_count: {
select: { bookings: true }
}
}
}),
prisma.tour.findMany({
take: 4,
distinct: [“destination”],
select: {
destination: true,
images: true,
}
})
])

return (

{/* Hero Section */}

Discover Your Next Adventure

Explore the world’s most amazing destinations with curated travel experiences



{/* Featured Tours */}

Featured Tours

{featuredTours.map((tour) => (

))}

{/* Top Destinations */}

Top Destinations

{topDestinations.map((dest, index) => (

{dest.destination}

{dest.destination}

))}

)
}

// Tour Card Component
function TourCard({ tour }: { tour: any }) {
return (

{tour.title}

${tour.price}


{tour.rating}
({tour._count.bookings} bookings)

{tour.title}

{tour.destination}


{tour.duration} days


{tour.maxGroupSize}


)
}
5.2 旅游产品详情页(SSR动态渲染)
创建
app/(main)/tours/[id]/page.tsx
:
import { prisma } from “@/lib/database”
import { notFound } from “next/navigation”
import Image from “next/image”
import { FaStar, FaClock, FaUsers, FaCalendar } from “react-icons/fa”
import { getServerSession } from “next-auth”
import { authOptions } from “@/app/api/auth/[…nextauth]/route”
import BookingForm from “@/components/BookingForm”

interface PageProps {
params: {
id: string
}
}

export default async function TourDetailPage({ params }: PageProps) {
const session = await getServerSession(authOptions)

const tour = await prisma.tour.findUnique({
where: { id: params.id },
include: {
_count: {
select: { bookings: true }
}
}
})

if (!tour) {
notFound()
}

// 转换JSON字段
const itinerary = tour.itinerary as any[]

return (

{/* Image Gallery */}

{tour.title}
{tour.images.slice(1, 5).map((img, index) => (

{`${tour.title}

))}

{/* Main Content */}

{tour.title}



{tour.rating} ({tour._count.bookings} reviews)


{tour.duration} days


Max {tour.maxGroupSize} people

{tour.description}

{/* Highlights */}

Tour Highlights

    {tour.highlights.map((highlight, index) => (

  • ✓
    {highlight}
  • ))}

{/* Itinerary */}

Itinerary

{itinerary.map((item, index) => (

Day {item.day}: {item.title}

{item.description}

    {item.activities.map((activity: string, i: number) => (


  • {activity}
  • ))}

))}

{/* Sidebar – Booking */}

${tour.price}
/ person

Available Dates:

{tour.availableDates.length} dates
Difficulty:

{tour.difficulty}

)
}
5.3 预订表单组件
创建
components/BookingForm.tsx
:
“use client”

import { useState } from “react”
import { useRouter } from “next/navigation”
import { toast } from “react-hot-toast”
import { FaCalendar, FaUsers } from “react-icons/fa”

interface BookingFormProps {
tour: {
id: string
price: number
maxGroupSize: number
availableDates: Date[]
}
user?: {
id: string
email: string
name: string
}
}

export default function BookingForm({ tour, user }: BookingFormProps) {
const router = useRouter()
const [loading, setLoading] = useState(false)
const [formData, setFormData] = useState({
date: “”,
participants: 1,
specialRequests: “”
})

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

try {
const response = await fetch(“/api/bookings”, {
method: “POST”,
headers: { “Content-Type”: “application/json” },
body: JSON.stringify({
tourId: tour.id,
date: formData.date,
participants: parseInt(formData.participants.toString()),
specialRequests: formData.specialRequests,
userId: user?.id
})
})

const data = await response.json()

if (!response.ok) {
throw new Error(data.error || “Failed to create booking”)
}

toast.success(“Booking created successfully!”)
router.push(“/dashboard/bookings”)
router.refresh()

} catch (error) {
toast.error(error instanceof Error ? error.message : “Booking failed”)
} finally {
setLoading(false)
}
}

const totalPrice = tour.price * formData.participants

if (!user) {
return (

Please sign in to book this tour

)
}

return (


setFormData({ …formData, date: e.target.value })}
min={new Date().toISOString().split(‘T’)[0]}
className=”w-full pl-10 pr-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500″
/>


setFormData({ …formData, participants: parseInt(e.target.value) })}
className=”w-full pl-10 pr-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500″
/>

Max {tour.maxGroupSize} people