引言:为什么选择Next.js构建旅游预订平台

在当今数字化时代,旅游预订网站已经成为人们规划旅行不可或缺的工具。一个优秀的旅游预订平台不仅需要提供丰富的旅游资源和便捷的预订流程,更需要拥有现代化、响应式的用户界面来吸引和留住用户。Next.js作为React生态系统中最受欢迎的框架之一,为构建高性能、SEO友好的Web应用提供了完美的解决方案。

Next.js具有服务器端渲染(SSR)、静态站点生成(SSG)、API路由、内置路由等特性,这些特性对于旅游预订网站来说至关重要。服务器端渲染能够确保搜索引擎爬虫轻松抓取内容,提升网站在搜索结果中的排名;静态站点生成则可以为常访问的页面(如首页、热门目的地页面)提供极快的加载速度;API路由让开发者可以轻松构建后端接口,处理用户认证、支付集成等复杂功能。

此外,Next.js的生态系统中还有许多优秀的UI组件库和工具,如Tailwind CSS、Shadcn UI、Headless UI等,它们可以帮助开发者快速构建美观且功能强大的界面。结合TypeScript,还可以获得更好的类型安全和开发体验。

核心功能模块设计

一个完整的旅游预订网站通常包含以下核心功能模块:

1. 首页模块

首页是用户访问的第一个页面,需要展示网站的核心价值和主要功能。应该包含:

  • 搜索框:支持目的地、日期、人数等搜索条件
  • 热门目的地推荐:展示最受欢迎的旅游城市或景点
  • 特色旅游套餐:推荐高性价比的旅游产品
  • 用户评价:展示真实用户的反馈
  • 品牌介绍:建立信任感

2. 搜索与筛选模块

搜索功能是旅游预订网站的核心。需要支持:

  • 关键词搜索:支持模糊匹配目的地、景点名称
  • 高级筛选:价格范围、评分、星级、设施等
  • 排序功能:价格、评分、销量等
  • 地图集成:在地图上显示搜索结果

3. 详情页模块

详情页是转化的关键环节,需要展示:

  • 高清图片轮播
  • 详细信息:地址、设施、政策等
  • 日历选择器:显示价格日历
  • 预订表单:选择日期、人数、附加服务
  • 用户评价:详细评价内容
  • 相关推荐:相似产品推荐

4. 预订流程模块

预订流程需要简洁明了:

  • 确认订单:显示订单详情
  • 填写信息:旅客信息、联系方式
  • 支付集成:支持多种支付方式
  • 确认页面:显示预订成功信息

5. 用户中心模块

用户可以管理自己的预订:

  • 我的订单:查看历史订单
  • 收藏夹:管理收藏的产品
  • 个人信息:修改个人资料
  • 评价管理:管理已发表的评价

技术栈选择

为了快速搭建一个现代化的旅游预订网站,我们推荐以下技术栈:

  • 前端框架: Next.js 14+ (App Router)
  • 语言: TypeScript
  • 样式: Tailwind CSS + Shadcn UI
  • 状态管理: Zustand 或 React Context
  • 表单处理: React Hook Form + Zod
  • 数据获取: TanStack Query (React Query)
  • UI组件: Headless UI, Radix UI
  • 地图集成: Google Maps API 或 Mapbox
  • 支付集成: Stripe 或 PayPal
  • 认证: NextAuth.js
  • 数据库: Prisma + PostgreSQL (或 MongoDB)
  • 部署: Vercel

项目初始化与配置

1. 创建Next.js项目

npx create-next-app@latest travel-booking-app --typescript --tailwind --eslint --app --src-dir --import-alias "@/*" cd travel-booking-app 

2. 安装核心依赖

npm install @radix-ui/react-slot class-variance-authority clsx tailwind-merge lucide-react npm install -D @types/node @types/react @types/react-dom postcss tailwindcss npm install react-hook-form zod @hookform/resolvers npm install @tanstack/react-query npm install next-auth @next-auth/prisma-adapter npm install prisma npm install stripe 

3. 配置Tailwind CSS

tailwind.config.ts中添加自定义主题:

import type { Config } from "tailwindcss" const config: Config = { darkMode: ["class"], content: [ './pages/**/*.{ts,tsx}', './components/**/*.{ts,tsx}', './app/**/*.{ts,tsx}', './src/**/*.{ts,tsx}', ], theme: { container: { center: true, padding: "2rem", screens: { "2xl": "1400px", }, }, extend: { colors: { border: "hsl(var(--border))", input: "hsl(var(--input))", ring: "hsl(var(--ring))", background: "hsl(var(--background))", foreground: "hsl(var(--foreground))", primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))", }, secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))", }, destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))", }, muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))", }, accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))", }, popover: { DEFAULT: "hsl(var(--popover))", foreground: "hsl(var(--popover-foreground))", }, card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))", }, }, borderRadius: { lg: "var(--radius)", md: "calc(var(--radius) - 2px)", sm: "calc(var(--radius) - 4px)", }, keyframes: { "accordion-down": { from: { height: "0" }, to: { height: "var(--radix-accordion-content-height)" }, }, "accordion-up": { from: { height: "var(--radix-accordion-content-height)" }, to: { height: "0" }, }, }, animation: { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", }, }, }, plugins: [require("tailwindcss-animate")], } export default config 

4. 配置CSS变量

app/globals.css中添加:

@tailwind base; @tailwind components; @tailwind utilities; @layer base { :root { --background: 0 0% 100%; --foreground: 222.2 84% 4.9%; --card: 0 0% 100%; --card-foreground: 222.2 84% 4.9%; --popover: 0 0% 100%; --popover-foreground: 222.2 84% 4.9%; --primary: 221.2 83.2% 53.3%; --primary-foreground: 210 40% 98%; --secondary: 210 40% 96.1%; --secondary-foreground: 222.2 47.4% 11.2%; --muted: 210 40% 96.1%; --muted-foreground: 215.4 16.3% 46.9%; --accent: 210 40% 96.1%; --accent-foreground: 222.2 47.4% 11.2%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 210 40% 98%; --border: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%; --ring: 221.2 83.2% 53.3%; --radius: 0.5rem; } .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; --card: 222.2 84% 4.9%; --card-foreground: 210 40% 98%; --popover: 222.2 84% 4.9%; --popover-foreground: 210 40% 98%; --primary: 221.2 83.2% 53.3%; --primary-foreground: 210 40% 98%; --secondary: 217.2 32.6% 17.5%; --secondary-foreground: 210 40% 98%; --muted: 217.2 32.6% 17.5%; --muted-foreground: 215.2 16.3% 64.9%; --accent: 217.2 32.6% 17.5%; --accent-foreground: 210 40% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 40% 98%; --border: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%; --ring: 224.3 76.3% 48%; } } @layer base { * { @apply border-border; } body { @apply bg-background text-foreground; } } 

核心组件开发

1. 搜索框组件

搜索框是旅游网站最重要的组件之一。我们需要创建一个功能完整、用户体验良好的搜索组件。

// src/components/search/SearchBox.tsx 'use client'; import { useState } from 'react'; import { Calendar, MapPin, Users, Search } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Calendar as CalendarComponent } from '@/components/ui/calendar'; import { format } from 'date-fns'; import { cn } from '@/lib/utils'; interface SearchState { destination: string; checkIn: Date | null; checkOut: Date | null; guests: { adults: number; children: number; rooms: number; }; } export function SearchBox() { const [searchState, setSearchState] = useState<SearchState>({ destination: '', checkIn: null, checkOut: null, guests: { adults: 1, children: 0, rooms: 1, }, }); const [openCalendar, setOpenCalendar] = useState(false); const [openGuests, setOpenGuests] = useState(false); const updateGuests = (type: 'adults' | 'children' | 'rooms', value: number) => { setSearchState(prev => ({ ...prev, guests: { ...prev.guests, [type]: Math.max(1, value), }, })); }; const handleSearch = () => { // 这里可以添加搜索逻辑,跳转到搜索结果页面 const params = new URLSearchParams({ destination: searchState.destination, checkIn: searchState.checkIn ? format(searchState.checkIn, 'yyyy-MM-dd') : '', checkOut: searchState.checkOut ? format(searchState.checkOut, 'yyyy-MM-dd') : '', adults: searchState.guests.adults.toString(), children: searchState.guests.children.toString(), rooms: searchState.guests.rooms.toString(), }); console.log('Search params:', params.toString()); // router.push(`/search?${params}`); }; return ( <div className="w-full max-w-4xl mx-auto bg-white rounded-lg shadow-lg p-4"> <div className="grid grid-cols-1 md:grid-cols-4 gap-4"> {/* 目的地输入 */} <div className="relative"> <MapPin className="absolute left-3 top-3 h-4 w-4 text-gray-400" /> <input type="text" placeholder="目的地" className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" value={searchState.destination} onChange={(e) => setSearchState(prev => ({ ...prev, destination: e.target.value }))} /> </div> {/* 日期选择 */} <Popover open={openCalendar} onOpenChange={setOpenCalendar}> <PopoverTrigger asChild> <button className="flex items-center gap-2 w-full px-4 py-2 border border-gray-200 rounded-md hover:bg-gray-50"> <Calendar className="h-4 w-4 text-gray-400" /> <span className="text-sm text-gray-600"> {searchState.checkIn && searchState.checkOut ? `${format(searchState.checkIn, 'MM/dd')} - ${format(searchState.checkOut, 'MM/dd')}` : '入住 - 退房'} </span> </button> </PopoverTrigger> <PopoverContent className="w-auto p-0" align="start"> <div className="p-4 space-y-4"> <div className="flex gap-4"> <div> <label className="text-xs font-medium text-gray-500">入住日期</label> <CalendarComponent mode="single" selected={searchState.checkIn} onSelect={(date) => { setSearchState(prev => ({ ...prev, checkIn: date })); if (searchState.checkOut && date && searchState.checkOut <= date) { setSearchState(prev => ({ ...prev, checkOut: null })); } }} disabled={(date) => date < new Date()} /> </div> <div> <label className="text-xs font-medium text-gray-500">退房日期</label> <CalendarComponent mode="single" selected={searchState.checkOut} onSelect={(date) => setSearchState(prev => ({ ...prev, checkOut: date }))} disabled={(date) => !searchState.checkIn || date <= searchState.checkIn} /> </div> </div> <Button className="w-full" onClick={() => setOpenCalendar(false)} > 确认日期 </Button> </div> </PopoverContent> </Popover> {/* 人数选择 */} <Popover open={openGuests} onOpenChange={setOpenGuests}> <PopoverTrigger asChild> <button className="flex items-center gap-2 w-full px-4 py-2 border border-gray-200 rounded-md hover:bg-gray-50"> <Users className="h-4 w-4 text-gray-400" /> <span className="text-sm text-gray-600"> {searchState.guests.adults}成人{searchState.guests.children > 0 ? ` ${searchState.guests.children}儿童` : ''} · {searchState.guests.rooms}房间 </span> </button> </PopoverTrigger> <PopoverContent className="w-80" align="start"> <div className="space-y-4"> <div className="flex justify-between items-center"> <div> <div className="font-medium">成人</div> <div className="text-xs text-gray-500">13岁以上</div> </div> <div className="flex items-center gap-2"> <Button variant="outline" size="sm" className="h-8 w-8" onClick={() => updateGuests('adults', searchState.guests.adults - 1)} disabled={searchState.guests.adults <= 1} > - </Button> <span className="w-6 text-center">{searchState.guests.adults}</span> <Button variant="outline" size="sm" className="h-8 w-8" onClick={() => updateGuests('adults', searchState.guests.adults + 1)} > + </Button> </div> </div> <div className="flex justify-between items-center"> <div> <div className="font-medium">儿童</div> <div className="text-xs text-gray-500">0-12岁</div> </div> <div className="flex items-center gap-2"> <Button variant="outline" size="sm" className="h-8 w-8" onClick={() => updateGuests('children', searchState.guests.children - 1)} disabled={searchState.guests.children <= 0} > - </Button> <span className="w-6 text-center">{searchState.guests.children}</span> <Button variant="outline" size="sm" className="h-8 w-8" onClick={() => updateGuests('children', searchState.guests.children + 1)} > + </Button> </div> </div> <div className="flex justify-between items-center"> <div> <div className="font-medium">房间</div> </div> <div className="flex items-center gap-2"> <Button variant="outline" size="sm" className="h-8 w-8" onClick={() => updateGuests('rooms', searchState.guests.rooms - 1)} disabled={searchState.guests.rooms <= 1} > - </Button> <span className="w-6 text-center">{searchState.guests.rooms}</span> <Button variant="outline" size="sm" className="h-8 w-8" onClick={() => updateGuests('rooms', searchState.guests.rooms + 1)} > + </Button> </div> </div> <Button className="w-full" onClick={() => setOpenGuests(false)} > 确认人数 </Button> </div> </PopoverContent> </Popover> {/* 搜索按钮 */} <Button className="flex items-center justify-center gap-2" onClick={handleSearch} disabled={!searchState.destination} > <Search className="h-4 w-4" /> 搜索 </Button> </div> </div> ); } 

2. 热门目的地卡片组件

// src/components/destination/DestinationCard.tsx import Image from 'next/image'; import { Star, MapPin } from 'lucide-react'; import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; interface DestinationCardProps { id: string; name: string; location: string; image: string; rating: number; reviews: number; price: number; tags?: string[]; onClick?: () => void; } export function DestinationCard({ id, name, location, image, rating, reviews, price, tags = [], onClick, }: DestinationCardProps) { return ( <Card className="overflow-hidden transition-all hover:shadow-lg cursor-pointer" onClick={onClick} > <div className="relative h-48"> <Image src={image} alt={name} fill className="object-cover" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" /> <div className="absolute top-2 right-2 flex gap-1"> {tags.map((tag) => ( <Badge key={tag} variant="secondary" className="bg-white/90"> {tag} </Badge> ))} </div> </div> <CardContent className="p-4"> <div className="flex justify-between items-start mb-2"> <div> <h3 className="font-semibold text-lg">{name}</h3> <div className="flex items-center text-sm text-gray-500 mt-1"> <MapPin className="h-3 w-3 mr-1" /> {location} </div> </div> <div className="flex items-center gap-1 bg-green-50 px-2 py-1 rounded"> <Star className="h-3 w-3 fill-yellow-400 text-yellow-400" /> <span className="font-medium text-sm">{rating}</span> </div> </div> <div className="flex justify-between items-center mt-3"> <div className="text-sm text-gray-500"> {reviews} 条评价 </div> <div className="text-lg font-bold text-blue-600"> ¥{price} <span className="text-sm font-normal text-gray-500">/晚起</span> </div> </div> </CardContent> </Card> ); } 

3. 产品详情页组件

// src/components/product/ProductDetail.tsx 'use client'; import { useState } from 'react'; import Image from 'next/image'; import { Star, MapPin, Users, Bed, Wifi, Coffee, Utensils, ChevronLeft, ChevronRight } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Calendar } from '@/components/ui/calendar'; import { format } from 'date-fns'; import { cn } from '@/lib/utils'; interface ProductDetailProps { product: { id: string; name: string; description: string; images: string[]; location: string; rating: number; reviews: number; price: number; amenities: string[]; policies: string[]; rooms: { type: string; capacity: number; price: number; }[]; }; } export function ProductDetail({ product }: ProductDetailProps) { const [currentImage, setCurrentImage] = useState(0); const [checkIn, setCheckIn] = useState<Date | undefined>(); const [checkOut, setCheckOut] = useState<Date | undefined>(); const [selectedRoom, setSelectedRoom] = useState<string | null>(null); const nextImage = () => { setCurrentImage((prev) => (prev + 1) % product.images.length); }; const prevImage = () => { setCurrentImage((prev) => (prev - 1 + product.images.length) % product.images.length); }; const calculateTotalNights = () => { if (!checkIn || !checkOut) return 0; const diffTime = Math.abs(checkOut.getTime() - checkIn.getTime()); return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); }; const calculateTotalPrice = () => { const nights = calculateTotalNights(); const room = product.rooms.find(r => r.type === selectedRoom); if (!room || nights === 0) return 0; return nights * room.price; }; const handleBooking = () => { if (!checkIn || !checkOut || !selectedRoom) { alert('请选择日期和房型'); return; } // 这里可以添加预订逻辑 console.log('Booking:', { productId: product.id, checkIn, checkOut, room: selectedRoom, totalPrice: calculateTotalPrice(), }); }; return ( <div className="max-w-6xl mx-auto space-y-6"> {/* 头部信息 */} <div> <h1 className="text-3xl font-bold mb-2">{product.name}</h1> <div className="flex items-center gap-4 text-gray-600"> <div className="flex items-center gap-1"> <MapPin className="h-4 w-4" /> {product.location} </div> <div className="flex items-center gap-1"> <Star className="h-4 w-4 fill-yellow-400 text-yellow-400" /> <span className="font-medium">{product.rating}</span> <span className="text-sm">({product.reviews} 条评价)</span> </div> </div> </div> {/* 图片轮播 */} <div className="relative group"> <div className="relative h-96 rounded-lg overflow-hidden"> <Image src={product.images[currentImage]} alt={`${product.name} - ${currentImage + 1}`} fill className="object-cover" /> </div> <Button variant="outline" size="icon" className="absolute left-2 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity" onClick={prevImage} > <ChevronLeft className="h-4 w-4" /> </Button> <Button variant="outline" size="icon" className="absolute right-2 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity" onClick={nextImage} > <ChevronRight className="h-4 w-4" /> </Button> <div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2"> {product.images.map((_, index) => ( <div key={index} className={cn( "w-2 h-2 rounded-full transition-all", currentImage === index ? "bg-white w-4" : "bg-white/50" )} /> ))} </div> </div> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> {/* 左侧内容区 */} <div className="lg:col-span-2 space-y-6"> {/* 描述 */} <Card> <CardHeader> <CardTitle>描述</CardTitle> </CardHeader> <CardContent> <p className="text-gray-700 leading-relaxed">{product.description}</p> </CardContent> </Card> {/* 设施 */} <Card> <CardHeader> <CardTitle>设施</CardTitle> </CardHeader> <CardContent> <div className="grid grid-cols-2 gap-3"> {product.amenities.map((amenity) => ( <div key={amenity} className="flex items-center gap-2 text-sm"> {amenity === 'WiFi' && <Wifi className="h-4 w-4" />} {amenity === '早餐' && <Coffee className="h-4 w-4" />} {amenity === '餐厅' && <Utensils className="h-4 w-4" />} {amenity === '客房服务' && <Users className="h-4 w-4" />} <span>{amenity}</span> </div> ))} </div> </CardContent> </Card> {/* 政策 */} <Card> <CardHeader> <CardTitle>政策</CardTitle> </CardHeader> <CardContent> <ul className="list-disc list-inside space-y-1 text-sm text-gray-700"> {product.policies.map((policy, index) => ( <li key={index}>{policy}</li> ))} </ul> </CardContent> </Card> {/* 评价 */} <Card> <CardHeader> <CardTitle>评价</CardTitle> </CardHeader> <CardContent> <div className="space-y-4"> {/* 这里可以动态加载评价列表 */} <div className="border-b pb-4"> <div className="flex items-center gap-2 mb-2"> <div className="font-medium">张三</div> <div className="flex items-center"> {Array.from({ length: 5 }).map((_, i) => ( <Star key={i} className={cn( "h-3 w-3", i < 5 ? "fill-yellow-400 text-yellow-400" : "text-gray-300" )} /> ))} </div> </div> <p className="text-sm text-gray-700">设施完善,服务周到,位置便利,强烈推荐!</p> </div> </div> </CardContent> </Card> </div> {/* 右侧预订面板 */} <div className="lg:col-span-1"> <Card className="sticky top-4"> <CardHeader> <CardTitle>预订</CardTitle> </CardHeader> <CardContent className="space-y-4"> {/* 日期选择 */} <div className="space-y-2"> <label className="text-sm font-medium">日期</label> <div className="grid grid-cols-2 gap-2"> <div> <div className="text-xs text-gray-500 mb-1">入住</div> <Calendar mode="single" selected={checkIn} onSelect={setCheckIn} disabled={(date) => date < new Date()} className="rounded-md border" /> </div> <div> <div className="text-xs text-gray-500 mb-1">退房</div> <Calendar mode="single" selected={checkOut} onSelect={setCheckOut} disabled={(date) => !checkIn || date <= checkIn} className="rounded-md border" /> </div> </div> </div> {/* 房型选择 */} <div className="space-y-2"> <label className="text-sm font-medium">房型</label> <div className="space-y-2"> {product.rooms.map((room) => ( <button key={room.type} onClick={() => setSelectedRoom(room.type)} className={cn( "w-full p-3 border rounded-md text-left transition-all", selectedRoom === room.type ? "border-blue-500 bg-blue-50" : "border-gray-200 hover:border-gray-300" )} > <div className="flex justify-between items-center"> <div> <div className="font-medium">{room.type}</div> <div className="text-xs text-gray-500"> <Users className="inline h-3 w-3" /> {room.capacity}人 </div> </div> <div className="font-bold text-blue-600">¥{room.price}</div> </div> </button> ))} </div> </div> {/* 价格明细 */} {(checkIn && checkOut && selectedRoom) && ( <div className="space-y-2 border-t pt-4"> <div className="flex justify-between text-sm"> <span>价格 ({calculateTotalNights()}晚)</span> <span>¥{product.rooms.find(r => r.type === selectedRoom)?.price}</span> </div> <div className="flex justify-between text-sm"> <span>总价</span> <span className="font-bold text-lg">¥{calculateTotalPrice()}</span> </div> </div> )} <Button className="w-full" size="lg" onClick={handleBooking} disabled={!checkIn || !checkOut || !selectedRoom} > 立即预订 </Button> </CardContent> </Card> </div> </div> </div> ); } 

4. 预订流程组件

// src/components/booking/BookingWizard.tsx 'use client'; import { useState } from 'react'; import { Check, User, CreditCard, Calendar, CheckCircle } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { cn } from '@/lib/utils'; interface BookingData { guestInfo: { firstName: string; lastName: string; email: string; phone: string; }; paymentInfo: { cardNumber: string; expiry: string; cvv: string; }; bookingDetails: { checkIn: Date; checkOut: Date; room: string; guests: number; totalPrice: number; }; } export function BookingWizard() { const [step, setStep] = useState(1); const [bookingData, setBookingData] = useState<BookingData>({ guestInfo: { firstName: '', lastName: '', email: '', phone: '' }, paymentInfo: { cardNumber: '', expiry: '', cvv: '' }, bookingDetails: { checkIn: new Date(), checkOut: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), room: '标准间', guests: 2, totalPrice: 1200, }, }); const updateGuestInfo = (field: string, value: string) => { setBookingData(prev => ({ ...prev, guestInfo: { ...prev.guestInfo, [field]: value }, })); }; const updatePaymentInfo = (field: string, value: string) => { setBookingData(prev => ({ ...prev, paymentInfo: { ...prev.paymentInfo, [field]: value }, })); }; const handleNext = () => { if (step === 1) { const { firstName, lastName, email, phone } = bookingData.guestInfo; if (!firstName || !lastName || !email || !phone) { alert('请填写所有必填项'); return; } } else if (step === 2) { const { cardNumber, expiry, cvv } = bookingData.paymentInfo; if (!cardNumber || !expiry || !cvv) { alert('请填写所有支付信息'); return; } } setStep(step + 1); }; const handleConfirm = () => { // 模拟支付处理 setTimeout(() => { setStep(4); }, 1500); }; const steps = [ { id: 1, title: '旅客信息', icon: User }, { id: 2, title: '支付信息', icon: CreditCard }, { id: 3, title: '确认订单', icon: Calendar }, { id: 4, title: '完成', icon: CheckCircle }, ]; return ( <div className="max-w-2xl mx-auto space-y-6"> {/* 步骤指示器 */} <div className="flex justify-between items-center"> {steps.map((s, index) => ( <div key={s.id} className="flex items-center flex-1"> <div className="flex flex-col items-center flex-1"> <div className={cn( "w-10 h-10 rounded-full flex items-center justify-center mb-2 transition-all", step >= s.id ? "bg-blue-600 text-white" : "bg-gray-200 text-gray-500" )} > {step > s.id ? <Check className="h-5 w-5" /> : <s.icon className="h-5 w-5" />} </div> <span className={cn("text-xs font-medium", step >= s.id ? "text-blue-600" : "text-gray-500")}> {s.title} </span> </div> {index < steps.length - 1 && ( <div className={cn("h-0.5 flex-1 mx-2", step > s.id ? "bg-blue-600" : "bg-gray-200")} /> )} </div> ))} </div> {/* 步骤内容 */} <Card> <CardHeader> <CardTitle>{steps[step - 1].title}</CardTitle> </CardHeader> <CardContent> {step === 1 && ( <div className="space-y-4"> <div className="grid grid-cols-2 gap-4"> <div> <Label htmlFor="firstName">名字 *</Label> <Input id="firstName" value={bookingData.guestInfo.firstName} onChange={(e) => updateGuestInfo('firstName', e.target.value)} placeholder="请输入名字" /> </div> <div> <Label htmlFor="lastName">姓氏 *</Label> <Input id="lastName" value={bookingData.guestInfo.lastName} onChange={(e) => updateGuestInfo('lastName', e.target.value)} placeholder="请输入姓氏" /> </div> </div> <div> <Label htmlFor="email">邮箱 *</Label> <Input id="email" type="email" value={bookingData.guestInfo.email} onChange={(e) => updateGuestInfo('email', e.target.value)} placeholder="请输入邮箱地址" /> </div> <div> <Label htmlFor="phone">手机号 *</Label> <Input id="phone" type="tel" value={bookingData.guestInfo.phone} onChange={(e) => updateGuestInfo('phone', e.target.value)} placeholder="请输入手机号码" /> </div> </div> )} {step === 2 && ( <div className="space-y-4"> <div> <Label htmlFor="cardNumber">卡号 *</Label> <Input id="cardNumber" value={bookingData.paymentInfo.cardNumber} onChange={(e) => updatePaymentInfo('cardNumber', e.target.value)} placeholder="1234 5678 9012 3456" maxLength={19} /> </div> <div className="grid grid-cols-2 gap-4"> <div> <Label htmlFor="expiry">有效期 *</Label> <Input id="expiry" value={bookingData.paymentInfo.expiry} onChange={(e) => updatePaymentInfo('expiry', e.target.value)} placeholder="MM/YY" maxLength={5} /> </div> <div> <Label htmlFor="cvv">CVV *</Label> <Input id="cvv" value={bookingData.paymentInfo.cvv} onChange={(e) => updatePaymentInfo('cvv', e.target.value)} placeholder="123" maxLength={3} /> </div> </div> </div> )} {step === 3 && ( <div className="space-y-4"> <div className="bg-gray-50 p-4 rounded-md space-y-2"> <h3 className="font-semibold">订单详情</h3> <div className="flex justify-between text-sm"> <span>房型:</span> <span>{bookingData.bookingDetails.room}</span> </div> <div className="flex justify-between text-sm"> <span>入住:</span> <span>{format(bookingData.bookingDetails.checkIn, 'yyyy-MM-dd')}</span> </div> <div className="flex justify-between text-sm"> <span>退房:</span> <span>{format(bookingData.bookingDetails.checkOut, 'yyyy-MM-dd')}</span> </div> <div className="flex justify-between text-sm"> <span>人数:</span> <span>{bookingData.bookingDetails.guests}人</span> </div> <div className="flex justify-between font-bold text-lg border-t pt-2 mt-2"> <span>总价:</span> <span>¥{bookingData.bookingDetails.totalPrice}</span> </div> </div> <div className="bg-blue-50 p-4 rounded-md"> <h3 className="font-semibold text-blue-800 mb-2">旅客信息</h3> <div className="text-sm text-blue-700 space-y-1"> <div>{bookingData.guestInfo.firstName} {bookingData.guestInfo.lastName}</div> <div>{bookingData.guestInfo.email}</div> <div>{bookingData.guestInfo.phone}</div> </div> </div> </div> )} {step === 4 && ( <div className="text-center py-8"> <CheckCircle className="h-16 w-16 mx-auto text-green-500 mb-4" /> <h2 className="text-2xl font-bold mb-2">预订成功!</h2> <p className="text-gray-600 mb-6"> 您的预订已确认,确认邮件已发送至 {bookingData.guestInfo.email} </p> <div className="bg-gray-50 p-4 rounded-md inline-block text-left"> <div className="font-medium mb-2">预订编号: BK{Date.now()}</div> <div className="text-sm text-gray-600"> 请保存此编号用于后续查询 </div> </div> </div> )} </CardContent> </Card> {/* 按钮组 */} <div className="flex justify-between"> {step > 1 && step < 4 && ( <Button variant="outline" onClick={() => setStep(step - 1)}> 返回 </Button> )} {step < 3 && ( <Button onClick={handleNext} className="ml-auto"> 下一步 </Button> )} {step === 3 && ( <Button onClick={handleConfirm} className="ml-auto"> 确认支付 </Button> )} {step === 4 && ( <Button className="ml-auto" onClick={() => window.location.reload()}> 返回首页 </Button> )} </div> </div> ); } 

页面布局与路由结构

1. 主布局组件

// src/app/layout.tsx import type { Metadata } from 'next'; import { Inter } from 'next/font/google'; import './globals.css'; import { Header } from '@/components/layout/Header'; import { Footer } from '@/components/layout/Footer'; import { QueryProvider } from '@/providers/QueryProvider'; import { AuthProvider } from '@/providers/AuthProvider'; const inter = Inter({ subsets: ['latin'] }); export const metadata: Metadata = { title: 'TravelBooking - 在线旅游预订平台', description: '提供全球酒店、机票、度假套餐预订服务', }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="zh-CN"> <body className={inter.className}> <AuthProvider> <QueryProvider> <Header /> <main className="min-h-screen bg-gray-50"> <div className="container mx-auto px-4 py-8"> {children} </div> </main> <Footer /> </QueryProvider> </AuthProvider> </body> </html> ); } 

2. 头部导航组件

// src/components/layout/Header.tsx 'use client'; import { useState, useEffect } from 'react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; import { Search, User, Menu, X, LogIn } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { useAuth } from '@/hooks/useAuth'; export function Header() { const pathname = usePathname(); const [isScrolled, setIsScrolled] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const { user, logout } = useAuth(); useEffect(() => { const handleScroll = () => { setIsScrolled(window.scrollY > 10); }; window.addEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll); }, []); const navigation = [ { name: '首页', href: '/' }, { name: '酒店', href: '/hotels' }, { name: '机票', href: '/flights' }, { name: '度假套餐', href: '/packages' }, { name: '目的地', href: '/destinations' }, ]; return ( <header className={cn( "sticky top-0 z-50 transition-all", isScrolled ? "bg-white/95 backdrop-blur shadow-sm" : "bg-white" )} > <div className="container mx-auto px-4"> <div className="flex items-center justify-between h-16"> {/* Logo */} <Link href="/" className="flex items-center gap-2"> <div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center"> <span className="text-white font-bold">T</span> </div> <span className="text-xl font-bold text-gray-900">TravelBooking</span> </Link> {/* Desktop Navigation */} <nav className="hidden md:flex items-center gap-6"> {navigation.map((item) => ( <Link key={item.name} href={item.href} className={cn( "text-sm font-medium transition-colors hover:text-blue-600", pathname === item.href ? "text-blue-600" : "text-gray-700" )} > {item.name} </Link> ))} </nav> {/* Right Actions */} <div className="hidden md:flex items-center gap-3"> <Button variant="ghost" size="sm"> <Search className="h-4 w-4 mr-2" /> 搜索 </Button> {user ? ( <div className="flex items-center gap-2"> <span className="text-sm font-medium">{user.name}</span> <Button variant="ghost" size="sm" onClick={logout}> 退出 </Button> </div> ) : ( <Link href="/login"> <Button size="sm"> <LogIn className="h-4 w-4 mr-2" /> 登录 </Button> </Link> )} </div> {/* Mobile Menu Button */} <button className="md:hidden p-2" onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)} > {isMobileMenuOpen ? ( <X className="h-6 w-6" /> ) : ( <Menu className="h-6 w-6" /> )} </button> </div> {/* Mobile Menu */} {isMobileMenuOpen && ( <div className="md:hidden border-t"> <div className="py-4 space-y-3"> {navigation.map((item) => ( <Link key={item.name} href={item.href} className={cn( "block px-4 py-2 rounded-md text-sm font-medium", pathname === item.href ? "bg-blue-50 text-blue-600" : "text-gray-700 hover:bg-gray-50" )} onClick={() => setIsMobileMenuOpen(false)} > {item.name} </Link> ))} <div className="px-4 pt-3 border-t"> {user ? ( <div className="space-y-2"> <div className="text-sm font-medium">{user.name}</div> <Button variant="ghost" className="w-full justify-start" onClick={() => { logout(); setIsMobileMenuOpen(false); }} > 退出 </Button> </div> ) : ( <Link href="/login" onClick={() => setIsMobileMenuOpen(false)}> <Button className="w-full"> <LogIn className="h-4 w-4 mr-2" /> 登录 </Button> </Link> )} </div> </div> </div> )} </div> </header> ); } 

3. 页脚组件

// src/components/layout/Footer.tsx import Link from 'next/link'; import { Facebook, Twitter, Instagram, Mail, Phone, MapPin } from 'lucide-react'; export function Footer() { return ( <footer className="bg-gray-900 text-gray-300 mt-16"> <div className="container mx-auto px-4 py-12"> <div className="grid grid-cols-1 md:grid-cols-4 gap-8"> {/* 公司信息 */} <div> <div className="flex items-center gap-2 mb-4"> <div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center"> <span className="text-white font-bold">T</span> </div> <span className="text-xl font-bold text-white">TravelBooking</span> </div> <p className="text-sm text-gray-400 mb-4"> 提供全球酒店、机票、度假套餐预订服务,让您的旅行更轻松。 </p> <div className="flex gap-3"> <Link href="#" className="hover:text-white transition-colors"> <Facebook className="h-5 w-5" /> </Link> <Link href="#" className="hover:text-white transition-colors"> <Twitter className="h-5 w-5" /> </Link> <Link href="#" className="hover:text-white transition-colors"> <Instagram className="h-5 w-5" /> </Link> </div> </div> {/* 快速链接 */} <div> <h3 className="text-white font-semibold mb-4">快速链接</h3> <ul className="space-y-2 text-sm"> <li><Link href="/hotels" className="hover:text-white transition-colors">酒店预订</Link></li> <li><Link href="/flights" className="hover:text-white transition-colors">机票预订</Link></li> <li><Link href="/packages" className="hover:text-white transition-colors">度假套餐</Link></li> <li><Link href="/destinations" className="hover:text-white transition-colors">热门目的地</Link></li> <li><Link href="/deals" className="hover:text-white transition-colors">特惠活动</Link></li> </ul> </div> {/* 客户服务 */} <div> <h3 className="text-white font-semibold mb-4">客户服务</h3> <ul className="space-y-2 text-sm"> <li><Link href="/help" className="hover:text-white transition-colors">帮助中心</Link></li> <li><Link href="/contact" className="hover:text-white transition-colors">联系我们</Link></li> <li><Link href="/privacy" className="hover:text-white transition-colors">隐私政策</Link></li> <li><Link href="/terms" className="hover:text-white transition-colors">服务条款</Link></li> <li><Link href="/faq" className="hover:text-white transition-colors">常见问题</Link></li> </ul> </div> {/* 联系方式 */} <div> <h3 className="text-white font-semibold mb-4">联系我们</h3> <ul className="space-y-3 text-sm"> <li className="flex items-start gap-2"> <Phone className="h-4 w-4 mt-0.5 flex-shrink-0" /> <span>400-123-4567</span> </li> <li className="flex items-start gap-2"> <Mail className="h-4 w-4 mt-0.5 flex-shrink-0" /> <span>support@travelbooking.com</span> </li> <li className="flex items-start gap-2"> <MapPin className="h-4 w-4 mt-0.5 flex-shrink-0" /> <span>北京市朝阳区建国路88号</span> </li> </ul> </div> </div> <div className="border-t border-gray-800 mt-8 pt-8 text-center text-sm text-gray-500"> <p>&copy; {new Date().getFullYear()} TravelBooking. All rights reserved.</p> </div> </div> </footer> ); } 

4. 首页页面

// src/app/page.tsx import { SearchBox } from '@/components/search/SearchBox'; import { DestinationCard } from '@/components/destination/DestinationCard'; import { Button } from '@/components/ui/button'; import { ArrowRight, Star, Shield, Zap } from 'lucide-react'; // 模拟数据 const featuredDestinations = [ { id: '1', name: '东京半岛酒店', location: '日本东京', image: 'https://images.unsplash.com/photo-1551882547-ff40c63fe5fa?w=800', rating: 4.8, reviews: 1234, price: 2800, tags: ['豪华', '市中心'], }, { id: '2', name: '巴黎香榭丽舍酒店', location: '法国巴黎', image: 'https://images.unsplash.com/photo-1502602898657-3e91760cbb34?w=800', rating: 4.7, reviews: 892, price: 3200, tags: ['浪漫', '景观'], }, { id: '3', name: '纽约时代广场酒店', location: '美国纽约', image: 'https://images.unsplash.com/photo-1496417263034-38ec4f0d665a?w=800', rating: 4.6, reviews: 1567, price: 2400, tags: ['繁华', '便利'], }, { id: '4', name: '迪拜帆船酒店', location: '阿联酋迪拜', image: 'https://images.unsplash.com/photo-1512453979798-5ea266f8880c?w=800', rating: 4.9, reviews: 2341, price: 5800, tags: ['奢华', '地标'], }, ]; export default function Home() { return ( <div className="space-y-12"> {/* Hero Section */} <section className="text-center space-y-6"> <h1 className="text-4xl md:text-5xl font-bold text-gray-900"> 探索世界,轻松预订 </h1> <p className="text-xl text-gray-600 max-w-2xl mx-auto"> 全球超过100万家酒店,数万条航线,为您提供最优质的旅行预订服务 </p> <SearchBox /> </section> {/* 特色优势 */} <section className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="text-center p-6 bg-white rounded-lg shadow-sm"> <div className="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-3"> <Shield className="h-6 w-6 text-blue-600" /> </div> <h3 className="font-semibold mb-2">安全可靠</h3> <p className="text-sm text-gray-600">SSL加密支付,保障您的交易安全</p> </div> <div className="text-center p-6 bg-white rounded-lg shadow-sm"> <div className="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-3"> <Star className="h-6 w-6 text-green-600" /> </div> <h3 className="font-semibold mb-2">品质保证</h3> <p className="text-sm text-gray-600">精选优质酒店,真实用户评价</p> </div> <div className="text-center p-6 bg-white rounded-lg shadow-sm"> <div className="w-12 h-12 bg-orange-100 rounded-full flex items-center justify-center mx-auto mb-3"> <Zap className="h-6 w-6 text-orange-600" /> </div> <h3 className="font-semibold mb-2">极速响应</h3> <p className="text-sm text-gray-600">24/7客服支持,随时解决问题</p> </div> </section> {/* 热门目的地 */} <section> <div className="flex items-center justify-between mb-6"> <h2 className="text-2xl font-bold text-gray-900">热门目的地</h2> <Button variant="ghost"> 查看全部 <ArrowRight className="h-4 w-4 ml-2" /> </Button> </div> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> {featuredDestinations.map((destination) => ( <DestinationCard key={destination.id} {...destination} onClick={() => console.log(`Navigate to ${destination.id}`)} /> ))} </div> </section> {/* 特色推荐 */} <section className="bg-blue-50 rounded-lg p-8"> <div className="max-w-4xl mx-auto text-center space-y-4"> <h2 className="text-2xl font-bold text-gray-900">限时特惠</h2> <p className="text-gray-700"> 现在预订享受8折优惠,免费取消,包含早餐 </p> <Button size="lg" className="mt-4"> 查看特惠 <ArrowRight className="h-4 w-4 ml-2" /> </Button> </div> </section> </div> ); } 

数据管理与API集成

1. 状态管理(使用Zustand)

// src/store/useSearchStore.ts import { create } from 'zustand'; interface SearchState { destination: string; checkIn: Date | null; checkOut: Date | null; guests: { adults: number; children: number; rooms: number; }; setDestination: (destination: string) => void; setCheckIn: (date: Date | null) => void; setCheckOut: (date: Date | null) => void; updateGuests: (type: 'adults' | 'children' | 'rooms', value: number) => void; reset: () => void; } export const useSearchStore = create<SearchState>((set) => ({ destination: '', checkIn: null, checkOut: null, guests: { adults: 1, children: 0, rooms: 1, }, setDestination: (destination) => set({ destination }), setCheckIn: (checkIn) => set({ checkIn }), setCheckOut: (checkOut) => set({ checkOut }), updateGuests: (type, value) => set((state) => ({ guests: { ...state.guests, [type]: Math.max(1, value), }, })), reset: () => set({ destination: '', checkIn: null, checkOut: null, guests: { adults: 1, children: 0, rooms: 1, }, }), })); 

2. API客户端

// src/lib/api/client.ts import axios from 'axios'; const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api'; const apiClient = axios.create({ baseURL: API_BASE_URL, timeout: 10000, headers: { 'Content-Type': 'application/json', }, }); // 请求拦截器 apiClient.interceptors.request.use( (config) => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => Promise.reject(error) ); // 响应拦截器 apiClient.interceptors.response.use( (response) => response.data, (error) => { if (error.response?.status === 401) { localStorage.removeItem('token'); window.location.href = '/login'; } return Promise.reject(error); } ); export const api = { // 搜索酒店 searchHotels: (params: any) => apiClient.get('/hotels', { params }), // 获取酒店详情 getHotelDetail: (id: string) => apiClient.get(`/hotels/${id}`), // 创建预订 createBooking: (data: any) => apiClient.post('/bookings', data), // 获取用户订单 getUserBookings: () => apiClient.get('/bookings/me'), // 用户登录 login: (credentials: any) => apiClient.post('/auth/login', credentials), // 用户注册 register: (data: any) => apiClient.post('/auth/register', data), }; 

3. React Query集成

// src/providers/QueryProvider.tsx 'use client'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { useState } from 'react'; export function QueryProvider({ children }: { children: React.ReactNode }) { const [queryClient] = useState(() => new QueryClient({ defaultOptions: { queries: { staleTime: 5 * 60 * 1000, // 5分钟 cacheTime: 10 * 60 * 1000, // 10分钟 refetchOnWindowFocus: false, retry: 1, }, }, })); return ( <QueryClientProvider client={queryClient}> {children} </QueryClientProvider> ); } 

4. 自定义Hook示例

// src/hooks/useHotels.ts import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { api } from '@/lib/api/client'; export function useHotels(searchParams: any) { return useQuery({ queryKey: ['hotels', searchParams], queryFn: () => api.searchHotels(searchParams), enabled: !!searchParams.destination, }); } export function useHotelDetail(id: string) { return useQuery({ queryKey: ['hotel', id], queryFn: () => api.getHotelDetail(id), enabled: !!id, }); } export function useCreateBooking() { const queryClient = useQueryClient(); return useMutation({ mutationFn: (data: any) => api.createBooking(data), onSuccess: () => { queryClient.invalidateQueries(['bookings']); }, }); } export function useUserBookings() { return useQuery({ queryKey: ['bookings'], queryFn: () => api.getUserBookings(), }); } 

样式与主题定制

1. 响应式设计最佳实践

// src/components/ui/responsive-grid.tsx import { ReactNode } from 'react'; interface ResponsiveGridProps { children: ReactNode; minItemWidth?: string; gap?: string; } export function ResponsiveGrid({ children, minItemWidth = '280px', gap = '1.5rem' }: ResponsiveGridProps) { return ( <div className="grid" style={{ gridTemplateColumns: `repeat(auto-fit, minmax(${minItemWidth}, 1fr))`, gap: gap, }} > {children} </div> ); } 

2. 暗色模式支持

// src/components/theme-toggle.tsx 'use client'; import { Moon, Sun } from 'lucide-react'; import { useTheme } from 'next-themes'; import { Button } from '@/components/ui/button'; export function ThemeToggle() { const { theme, setTheme } = useTheme(); return ( <Button variant="ghost" size="icon" onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} > <Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> <Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> <span className="sr-only">切换主题</span> </Button> ); } 

性能优化策略

1. 图片优化

// src/components/optimized-image.tsx import Image from 'next/image'; interface OptimizedImageProps { src: string; alt: string; width?: number; height?: number; className?: string; priority?: boolean; } export function OptimizedImage({ src, alt, width, height, className, priority = false, }: OptimizedImageProps) { return ( <Image src={src} alt={alt} width={width} height={height} className={className} priority={priority} placeholder="blur" blurDataURL="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9IiNmMmYyZjIiLz48L3N2Zz4=" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" /> ); } 

2. 代码分割与懒加载

// src/components/lazy-components.tsx import dynamic from 'next/dynamic'; // 懒加载重型组件 export const LazyMap = dynamic( () => import('./map-component'), { loading: () => <div className="h-96 bg-gray-100 animate-pulse rounded-lg" />, ssr: false, } ); export const LazyCalendar = dynamic( () => import('./calendar-component'), { loading: () => <div className="h-64 bg-gray-100 animate-pulse rounded-lg" />, } ); 

部署与CI/CD

1. Vercel部署配置

// vercel.json { "buildCommand": "npm run build", "outputDirectory": ".next", "installCommand": "npm install", "devCommand": "npm run dev", "rewrites": [ { "source": "/api/:path*", "destination": "/api/:path*" } ] } 

2. GitHub Actions CI/CD

# .github/workflows/deploy.yml name: Deploy to Vercel on: push: branches: [main] pull_request: branches: [main] jobs: build-and-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: '18' cache: 'npm' - run: npm ci - run: npm run lint - run: npm run type-check - run: npm run build deploy: needs: build-and-test runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v3 - uses: amondnet/vercel-action@v25 with: vercel-token: ${{ secrets.VERCEL_TOKEN }} vercel-org-id: ${{ secrets.ORG_ID }} vercel-project-id: ${{ secrets.PROJECT_ID }} 

测试策略

1. 单元测试示例

// src/components/search/__tests__/SearchBox.test.tsx import { render, screen, fireEvent } from '@testing-library/react'; import { SearchBox } from '../SearchBox'; describe('SearchBox', () => { it('renders all input fields', () => { render(<SearchBox />); expect(screen.getByPlaceholderText('目的地')).toBeInTheDocument(); expect(screen.getByText('入住 - 退房')).toBeInTheDocument(); expect(screen.getByText('1成人 · 1房间')).toBeInTheDocument(); expect(screen.getByText('搜索')).toBeInTheDocument(); }); it('updates destination input', () => { render(<SearchBox />); const input = screen.getByPlaceholderText('目的地'); fireEvent.change(input, { target: { value: '东京' } }); expect(input).toHaveValue('东京'); }); it('disables search button when destination is empty', () => { render(<SearchBox />); const button = screen.getByText('搜索'); expect(button).toBeDisabled(); }); }); 

2. E2E测试示例

// e2e/search.spec.ts import { test, expect } from '@playwright/test'; test.describe('Search Flow', () => { test('should complete a search flow', async ({ page }) => { await page.goto('http://localhost:3000'); // Fill destination await page.fill('input[placeholder="目的地"]', '东京'); // Open calendar and select dates await page.click('text=入住 - 退房'); await page.waitForSelector('[role="dialog"]'); // Select check-in date (next week) const nextWeek = new Date(); nextWeek.setDate(nextWeek.getDate() + 7); const checkInDay = nextWeek.getDate(); await page.click(`text=${checkInDay}`); // Select check-out date (next week + 3 days) const checkOut = new Date(nextWeek); checkOut.setDate(checkOut.getDate() + 3); const checkOutDay = checkOut.getDate(); await page.click(`text=${checkOutDay}`); // Confirm dates await page.click('text=确认日期'); // Click search await page.click('text=搜索'); // Verify navigation to search results await expect(page).toHaveURL(/.*destination=东京/); }); }); 

安全最佳实践

1. 环境变量配置

# .env.local NEXT_PUBLIC_API_URL=https://api.travelbooking.com NEXTAUTH_SECRET=your-secret-key-here DATABASE_URL=postgresql://user:pass@localhost:5432/travelbooking STRIPE_SECRET_KEY=sk_test_... NEXTAUTH_URL=http://localhost:3000 

2. 输入验证

// src/lib/validators/booking.ts import { z } from 'zod'; export const bookingSchema = z.object({ destination: z.string().min(1, '目的地不能为空'), checkIn: z.date().min(new Date(), '入住日期必须是今天之后'), checkOut: z.date().min(new Date(), '退房日期必须是今天之后'), guests: z.object({ adults: z.number().min(1).max(10), children: z.number().min(0).max(10), rooms: z.number().min(1).max(5), }), }); export type BookingFormData = z.infer<typeof bookingSchema>; 

总结

通过使用Next.js构建旅游预订网站,我们可以充分利用其现代化的特性和丰富的生态系统来创建高性能、用户友好的应用。本文详细介绍了从项目初始化到核心组件开发、数据管理、样式定制、性能优化和部署的完整流程。

关键要点包括:

  1. 技术栈选择:Next.js + TypeScript + Tailwind CSS + Shadcn UI的组合提供了最佳的开发体验和性能
  2. 组件化架构:将UI拆分为可复用的组件,提高代码维护性
  3. 状态管理:使用Zustand进行全局状态管理,React Query处理服务器状态
  4. 响应式设计:确保在所有设备上都能提供良好的用户体验
  5. 性能优化:图片优化、代码分割、缓存策略等
  6. 安全性:输入验证、环境变量管理、认证保护

这个模板为快速搭建旅游预订平台提供了坚实的基础,开发者可以根据具体需求进行扩展和定制,添加更多功能如评论系统、推荐算法、多语言支持等。通过遵循最佳实践,可以构建出一个既美观又实用的现代化旅游预订平台。