فصل چهارم: صفحات و مسیریابی
در این فصل، به پیادهسازی صفحات اصلی و سیستم مسیریابی (Routing) اپلیکیشن مرکز اجاره کشتیهای کروز با استفاده از Nuxt 3 میپردازیم. Nuxt از سیستم مسیریابی مبتنی بر فایل استفاده میکند که صفحات بهصورت خودکار از ساختار پوشه pages/ تولید میشوند. همچنین، با استفاده از layouts، یک قالب کلی برای اپلیکیشن تعریف میکنیم و از قابلیتهای پیشرفته مانند SEO و اعتبارسنجی فرمها بهره میبریم. در ادامه، هر بخش بهطور کامل توضیح داده شده و کدهای مربوطه شرح داده میشوند.
قالب کلی (Layout) - layouts/default.vue
قالبها در Nuxt برای ایجاد ساختار مشترک بین صفحات استفاده میشوند. فایل default.vue بهعنوان قالب پیشفرض برای تمام صفحات اپلیکیشن عمل میکند.
کد:
<!-- layouts/default.vue -->
<template>
<div class="min-h-screen bg-gray-50">
<header class="bg-blue-800 text-white p-4">
<nav class="container mx-auto flex justify-between items-center">
<NuxtLink to="/" class="text-2xl font-bold">Cruise Rental</NuxtLink>
<div class="space-x-4">
<NuxtLink to="/ships" class="hover:underline">Ships</NuxtLink>
<NuxtLink to="/bookings" class="hover:underline">Bookings</NuxtLink>
</div>
</nav>
</header>
<main>
<slot />
</main>
</div>
</template>
توضیحات:
-
ساختار قالب:
-
<div class="min-h-screen bg-gray-50">: یک ظرف اصلی با حداقل ارتفاع برابر با کل صفحه و پسزمینه خاکستری روشن. <header>: شامل نوار ناوبری (Navigation Bar) با رنگ پسزمینه آبی تیره و متن سفید.<nav>: شامل لینکهای ناوبری به صفحات اصلی (صفحه اصلی، لیست کشتیها، و رزروها) با استفاده از<NuxtLink>برای مسیریابی سمت کلاینت.<main>: تگ<slot />جایی است که محتوای صفحات خاص (مانند صفحه اصلی یا لیست کشتیها) رندر میشود.-
استایلدهی:
-
از Tailwind CSS برای استایلدهی استفاده شده است (مانند
flex justify-between items-centerبرای تراز کردن آیتمهای ناوبری). - کلاس
space-x-4فاصله افقی بین لینکهای ناوبری ایجاد میکند. - افکت
hover:underlineهنگام حرکت ماوس روی لینکها، خط زیرین اضافه میکند. - کاربرد: این قالب به تمام صفحات اپلیکیشن یک ساختار یکپارچه میدهد و ناوبری را در دسترس کاربران قرار میدهد.
صفحه اصلی (Homepage) - pages/index.vue
صفحه اصلی، صفحه فرود اپلیکیشن است که کاربران را به سمت مرور کشتیها هدایت میکند و برای سئو بهینهسازی شده است.
کد:
<!-- pages/index.vue -->
<template>
<div class="min-h-screen flex flex-col items-center justify-center bg-gradient-to-b from-blue-100 to-gray-100">
<h1 class="text-5xl font-bold mb-4 text-center">Cruise Ship Rental Center</h1>
<p class="text-lg mb-8 text-center max-w-2xl">
Discover luxury and adventure with our premium cruise ship rentals.
</p>
<NuxtLink to="/ships" class="btn text-lg">Browse Ships</NuxtLink>
</div>
</template>
<script setup>
useSeoMeta({
title: 'Cruise Ship Rental Center | Luxury Cruises 2025',
description: 'Rent premium cruise ships for your next adventure. Explore our fleet and book today!',
ogImage: '/og-image.jpg', // Add an image in public/
});
</script>
توضیحات:
-
بخش Template:
-
یک ظرف تمامصفحه با گرادیان پسزمینه از آبی روشن به خاکستری روشن (
bg-gradient-to-b). - عنوان بزرگ (
text-5xl) و پاراگراف توضیحی با عرض محدود (max-w-2xl) برای خوانایی. - دکمه "Browse Ships" با استفاده از کلاس سفارشی
btn(تعریفشده در Tailwind CSS) که کاربران را به صفحه لیست کشتیها هدایت میکند. -
بخش Script:
-
از
useSeoMetaبرای تنظیم متادیتای سئو استفاده شده است:- title: عنوان صفحه برای نمایش در مرورگر و موتورهای جستجو.
- description: توضیح مختصر برای بهبود سئو.
- ogImage: تصویر Open Graph برای نمایش در شبکههای اجتماعی (تصویر باید در پوشه
public/قرار گیرد).
- کاربرد: این صفحه کاربران را با اپلیکیشن آشنا میکند و با متادیتای سئو، رتبهبندی در موتورهای جستجو را بهبود میدهد.
لیست کشتیها (Ships List) - pages/ships/index.vue
این صفحه لیستی از کشتیهای موجود را با قابلیت فیلتر و صفحهبندی نمایش میدهد.
کد:
<!-- pages/ships/index.vue -->
<template>
<div class="container mx-auto p-6">
<h1 class="text-3xl font-bold mb-6">Available Cruise Ships</h1>
<!-- Filters -->
<div class="mb-6 flex gap-4">
<input v-model="search" placeholder="Search ships..." class="border p-2 rounded" />
<input v-model.number="maxPrice" type="number" placeholder="Max price/day" class="border p-2 rounded" />
</div>
<!-- Ship Grid -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<ShipCard v-for="ship in filteredShips" :key="ship.id" :ship="ship" />
</div>
<!-- Pagination -->
<div class="mt-6 flex justify-center gap-2">
<button
v-for="page in totalPages"
:key="page"
@click="shipsStore.setPage(page)"
:class="['px-4 py-2 rounded', shipsStore.pagination.page === page ? 'bg-blue-500 text-white' : 'bg-gray-200']"
>
{{ page }}
</button>
</div>
</div>
</template>
<script setup>
import { useShipsStore } from '~/stores/ships';
const shipsStore = useShipsStore();
const search = ref('');
const maxPrice = ref(Infinity);
watch(search, (value) => shipsStore.setSearch(value));
watch(maxPrice, (value) => shipsStore.setMaxPrice(value));
const filteredShips = computed(() => shipsStore.filteredShips);
const totalPages = computed(() => shipsStore.totalPages);
onMounted(() => {
shipsStore.fetchShips();
});
useSeoMeta({
title: 'Browse Cruise Ships | Rental Center 2025',
description: 'Explore our fleet of luxury cruise ships available for rent.',
});
</script>
توضیحات:
-
بخش Template:
-
فیلترها: دو ورودی برای جستجوی نام کشتی (
search) و فیلتر قیمت حداکثر (maxPrice) با استایل Tailwind. - گرید کشتیها: از کامپوننت
ShipCardبرای نمایش کشتیها در یک گرید پاسخگو استفاده میشود (یک ستون در موبایل، سه ستون در دسکتاپ). - صفحهبندی: دکمههایی برای هر صفحه که با کلیک روی آنها، شماره صفحه در store بهروزرسانی میشود.
-
بخش Script:
-
Store: از
useShipsStoreبرای دسترسی به دادهها و متدهای store کشتیها استفاده میشود. - Refs: متغیرهای
searchوmaxPriceبرای اتصال به ورودیهای فیلتر. - Watchers: با تغییر
searchیاmaxPrice، متدهایsetSearchوsetMaxPriceدر store فراخوانی میشوند. -
Computed Properties:
filteredShips: لیست کشتیهای فیلترشده را از store میگیرد.totalPages: تعداد کل صفحات را محاسبه میکند.- onMounted: هنگام بارگذاری صفحه، متد
fetchShipsبرای دریافت دادههای کشتیها فراخوانی میشود. - SEO: متادیتا برای بهبود سئو تنظیم شده است.
- کاربرد: این صفحه به کاربران امکان میدهد کشتیها را جستجو، فیلتر و در صفحات مختلف مشاهده کنند.
جزئیات کشتی (Ship Details) - pages/ships/[id].vue
این صفحه جزئیات یک کشتی خاص را نمایش داده و فرمی برای رزرو آن ارائه میدهد.
کد:
<!-- pages/ships/[id].vue -->
<template>
<div v-if="ship" class="container mx-auto p-6">
<h1 class="text-3xl font-bold mb-4">{{ ship.name }}</h1>
<NuxtImg :src="ship.image" alt="Ship" class="w-full h-64 object-cover mb-4 rounded" />
<p class="mb-2">Capacity: {{ ship.capacity }} passengers</p>
<p class="mb-2">Price: ${{ ship.pricePerDay }}/day</p>
<p class="mb-4">Amenities: {{ ship.amenities.join(', ') }}</p>
<h2 class="text-2xl font-bold mb-4">Book This Ship</h2>
<form @submit.prevent="submit" class="space-y-4">
<div>
<label class="block">Start Date</label>
<input v-model="form.startDate" type="date" class="border p-2 rounded w-full" />
<span v-if="errors.startDate" class="text-red-500">{{ errors.startDate }}</span>
</div>
<div>
<label class="block">End Date</label>
<input v-model="form.endDate" type="date" class="border p-2 rounded w-full" />
<span v-if="errors.endDate" class="text-red-500">{{ errors.endDate }}</span>
</div>
<button type="submit" class="btn">Book Now</button>
</form>
</div>
<div v-else>Ship not found.</div>
</template>
<script setup>
import { useShipsStore } from '~/stores/ships';
import { useBookingsStore } from '~/stores/bookings';
import { useForm } from 'vee-validate';
import * as yup from 'yup';
const route = useRoute();
const shipsStore = useShipsStore();
const bookingsStore = useBookingsStore();
const ship = computed(() => shipsStore.getShipById(Number(route.params.id)));
const schema = yup.object({
startDate: yup.date().required('Start date is required').min(new Date(), 'Start date must be today or later'),
endDate: yup.date().required('End date is required').min(yup.ref('startDate'), 'End date must be after start date'),
});
const { handleSubmit, errors, defineField } = useForm({ validationSchema: schema });
const [startDate] = defineField('startDate');
const [endDate] = defineField('endDate');
const form = reactive({ startDate: '', endDate: '' });
const submit = handleSubmit(async (values) => {
if (!ship.value) return;
const days = (new Date(values.endDate) - new Date(values.startDate)) / (1000 * 60 * 60 * 24);
const totalPrice = days * ship.value.pricePerDay;
await bookingsStore.addBooking({
shipId: ship.value.id,
startDate: new Date(values.startDate),
endDate: new Date(values.endDate),
totalPrice,
});
alert('Booking successful!');
form.startDate = '';
form.endDate = '';
});
useSeoMeta({
title: () => ship.value ? `${ship.value.name} | Cruise Rental 2025` : 'Ship Details',
description: () => ship.value ? `Book ${ship.value.name} for your next adventure!` : 'View cruise ship details.',
});
</script>
توضیحات:
-
بخش Template:
-
اگر کشتی یافت شود (
v-if="ship"):- نمایش نام، تصویر (با
<NuxtImg>برای بهینهسازی)، ظرفیت، قیمت روزانه و امکانات. - فرم رزرو با دو ورودی برای تاریخ شروع و پایان و نمایش خطاها (
errors) با رنگ قرمز. - دکمه "Book Now" با کلاس
btn.
- نمایش نام، تصویر (با
- در غیر این صورت، پیام "Ship not found" نمایش داده میشود.
-
بخش Script:
-
وابستگیها: استفاده از storeهای
shipsوbookings، و کتابخانههایvee-validateوyupبرای اعتبارسنجی. - مسیریابی:
useRouteبرای دریافتidاز URL. - داده کشتی:
shipبا استفاده ازgetShipByIdاز store دریافت میشود. -
اعتبارسنجی فرم:
- از
yupبرای تعریف اسکیما استفاده شده که تاریخ شروع را اجباری و بعد از امروز، و تاریخ پایان را بعد از تاریخ شروع الزام میکند. useFormوdefineFieldبرای مدیریت فرم و خطاها.
- از
-
ارسال فرم: متد
submitتعداد روزهای رزرو را محاسبه کرده، قیمت کل را تعیین میکند و رزرو را به store اضافه میکند. - SEO: متادیتای پویا بر اساس نام کشتی تنظیم شده است.
- کاربرد: این صفحه امکان مشاهده جزئیات و رزرو یک کشتی خاص را فراهم میکند.
داشبورد رزروها (Bookings Dashboard) - pages/bookings.vue
این صفحه تاریخچه رزروهای کاربر را نمایش میدهد.
کد:
<!-- pages/bookings.vue -->
<template>
<div class="container mx-auto p-6">
<h1 class="text-3xl font-bold mb-6">Your Bookings</h1>
<ul v-if="bookings.length" class="space-y-4">
<li v-for="booking in bookings" :key="booking.id" class="card p-4">
<p>Ship: {{ getShipName(booking.shipId) }}</p>
<p>Dates: {{ formatDate(booking.startDate) }} to {{ formatDate(booking.endDate) }}</p>
<p>Total: ${{ booking.totalPrice }}</p>
</li>
</ul>
<p v-else>No bookings yet.</p>
</div>
</template>
<script setup>
import { useBookingsStore } from '~/stores/bookings';
import { useShipsStore } from '~/stores/ships';
const bookingsStore = useBookingsStore();
const shipsStore = useShipsStore();
const bookings = computed(() => bookingsStore.bookings);
const getShipName = (shipId: number) => {
const ship = shipsStore.getShipById(shipId);
return ship ? ship.name : 'Unknown';
};
const formatDate = (date: Date) => new Date(date).toLocaleDateString();
onMounted(() => {
bookingsStore.fetchBookings();
});
useSeoMeta({
title: 'Your Bookings | Cruise Rental 2025',
description: 'View and manage your cruise ship bookings.',
});
</script>
توضیحات:
-
بخش Template:
-
اگر رزرو وجود داشته باشد، لیستی از رزروها با استفاده از کلاس
cardنمایش داده میشود. - هر رزرو شامل نام کشتی، تاریخها و قیمت کل است.
- در غیر این صورت، پیام "No bookings yet" نمایش داده میشود.
-
بخش Script:
-
Storeها: استفاده از
useBookingsStoreوuseShipsStoreبرای دسترسی به رزروها و اطلاعات کشتیها. - Computed:
bookingsلیست رزروها را از store دریافت میکند. - getShipName: نام کشتی را بر اساس
shipIdپیدا میکند. - formatDate: تاریخها را به فرمت خوانا تبدیل میکند.
- onMounted: هنگام بارگذاری صفحه، رزروها از سرور دریافت میشوند.
- SEO: متادیتا برای بهبود سئو تنظیم شده است.
- کاربرد: این صفحه به کاربران امکان میدهد رزروهای خود را مشاهده و مدیریت کنند.
جمعبندی فصل چهارم
در این فصل، صفحات اصلی اپلیکیشن (صفحه اصلی، لیست کشتیها، جزئیات کشتی، و داشبورد رزروها) را با استفاده از سیستم مسیریابی Nuxt پیادهسازی کردیم. از قالب پیشفرض برای یکپارچگی، Tailwind CSS برای استایلدهی، و قابلیتهای سئو و اعتبارسنجی برای بهبود تجربه کاربری استفاده شد. در فصلهای بعدی، به کامپوننتها و مسیرهای سرور برای تکمیل اپلیکیشن خواهیم پرداخت.