ضمیمه ب: توضیحات کامل مدل‌های داده (فصل ۳)

این ضمیمه جزئیات عمیق‌تر درباره مدل‌های داده (Ship و Booking) و نحوه استفاده آن‌ها در APIها، storeها، و صفحات اپلیکیشن مرکز اجاره کشتی‌های کروز را ارائه می‌دهد. این اطلاعات که در فصل سوم به‌صورت خلاصه یا ناقص پوشش داده شده بودند، در اینجا به‌طور جامع شرح داده می‌شوند تا درک کامل‌تری از ساختار داده‌ها و کاربرد آن‌ها فراهم شود.


مدل‌های داده

مدل‌های داده در فایل types/index.ts تعریف شده‌اند و برای اطمینان از ایمنی نوع (Type Safety) در سراسر اپلیکیشن با TypeScript استفاده می‌شوند. این مدل‌ها پایه‌ای برای تعاملات API، مدیریت وضعیت با Pinia، و نمایش داده‌ها در رابط کاربری هستند.

مدل Ship

  • تعریف:
  // types/index.ts
  export interface Ship {
    id: number;
    name: string;
    capacity: number;
    pricePerDay: number;
    amenities: string[];
    image: string;
  }
  • فیلدها:

    • id: شناسه یکتا برای هر کشتی (عدد).
    • name: نام کشتی (رشته، مانند "Ocean Explorer").
    • capacity: ظرفیت مسافران (عدد، مثلاً ۵۰۰).
    • pricePerDay: قیمت اجاره روزانه (عدد، مثلاً ۱۰۰۰۰ دلار).
    • amenities: آرایه‌ای از امکانات کشتی (مانند ["Pool", "Gym"]).
    • image: مسیر تصویر کشتی (رشته، مانند /images/ocean.jpg).
  • کاربرد در API:

  • مسیر سرور (server/api/ships.ts):

    import type { Ship } from '~/types';

    export default defineEventHandler((): Ship[] => {
      return [
        { 
          id: 1, 
          name: 'Ocean Explorer', 
          capacity: 500, 
          pricePerDay: 10000, 
          amenities: ['Pool', 'Gym'], 
          image: '/images/ocean.jpg' 
        },
        { 
          id: 2, 
          name: 'Luxury Liner', 
          capacity: 1000, 
          pricePerDay: 20000, 
          amenities: ['Spa', 'Theater'], 
          image: '/images/luxury.jpg' 
        },
      ];
    });
  • توضیحات: این API یک آرایه از اشیاء Ship را برمی‌گرداند که با مدل تعریف‌شده در types/index.ts مطابقت دارند. داده‌ها به‌صورت موقت در حافظه ذخیره شده‌اند، اما در فصل ۱۱ با Supabase یا Prisma جایگزین شدند.

  • کاربرد در رابط کاربری:

    • در components/ShipCard.vue:
      <template>
        <div class="card">
          <NuxtImg :src="ship.image" alt="Ship" class="w-full h-48 object-cover" provider="static" />
          <div class="p-4">
            <h2 class="text-xl font-semibold">{{ ship.name }}</h2>
            <p class="text-gray-600">Capacity: {{ ship.capacity }}</p>
            <p class="text-gray-600">Price: ${{ ship.pricePerDay }}/day</p>
            <NuxtLink :to="`/ships/${ship.id}`" class="text-blue-500 hover:underline">View Details</NuxtLink>
          </div>
        </div>
      </template>
      <script setup>
      defineProps<{ ship: Ship }>();
      </script>
  • توضیحات: کامپوننت ShipCard از مدل Ship برای نمایش اطلاعات کشتی استفاده می‌کند. پراپ ship با تایپ Ship تعریف شده تا ایمنی نوع تضمین شود.

    • در pages/ships/[id].vue:
      <script setup>
      import type { Ship } from '~/types';
      const route = useRoute();
      const { data: ship } = await useAsyncData('ship', () => $fetch(`/api/ships/${route.params.id}`));
      </script>
      <template>
        <div v-if="ship" class="container mx-auto p-6">
          <h1 class="text-3xl font-bold">{{ ship.name }}</h1>
          <NuxtImg :src="ship.image" alt="Ship" class="w-full h-64 object-cover mb-4 rounded" />
          <p>Capacity: {{ ship.capacity }}</p>
          <p>Price: ${{ ship.pricePerDay }}/day</p>
          <p>Amenities: {{ ship.amenities.join(', ') }}</p>
        </div>
      </template>
  • توضیحات: این صفحه از مدل Ship برای نمایش جزئیات یک کشتی خاص استفاده می‌کند. داده‌ها از API دریافت شده و با <NuxtImg> بهینه‌سازی می‌شوند.

  • کاربرد در Store:

  • در stores/ships.ts:

    import { defineStore } from 'pinia';
    import type { Ship } from '~/types';

    export const useShipsStore = defineStore('ships', {
      state: () => ({
        ships: [] as Ship[],
        search: '',
      }),
      getters: {
        filteredShips: (state) => state.ships.filter(ship => 
          ship.name.toLowerCase().includes(state.search.toLowerCase())
        ),
      },
      actions: {
        async fetchShips() {
          this.ships = await $fetch('/api/ships');
        },
        setSearch(query: string) {
          this.search = query;
        },
      },
    });
  • توضیحات: store از مدل Ship برای ذخیره و فیلتر کردن لیست کشتی‌ها استفاده می‌کند. متد fetchShips داده‌ها را از /api/ships دریافت می‌کند، و filteredShips برای جستجوی پویا استفاده می‌شود.

مدل Booking

  • تعریف:
  // types/index.ts
  export interface Booking {
    id: number;
    shipId: number;
    startDate: Date;
    endDate: Date;
    totalPrice: number;
  }
  • فیلدها:

    • id: شناسه یکتا برای رزرو (عدد).
    • shipId: شناسه کشتی مرتبط (مرجع به Ship.id).
    • startDate: تاریخ شروع رزرو (شیء Date).
    • endDate: تاریخ پایان رزرو (شیء Date).
    • totalPrice: هزینه کل رزرو (عدد، مثلاً ۳۰۰۰۰ دلار).
  • کاربرد در API:

  • مسیر سرور (server/api/bookings.ts):

    import type { Booking } from '~/types';

    let bookings: Booking[] = [];

    export default defineEventHandler({
      async get() {
        return bookings;
      },
      async post(event) {
        const body = await readBody(event);
        const newBooking: Booking = {
          id: bookings.length + 1,
          shipId: body.shipId,
          startDate: new Date(body.startDate),
          endDate: new Date(body.endDate),
          totalPrice: body.totalPrice,
        };
        bookings.push(newBooking);
        return newBooking;
      },
    });
  • توضیحات: این API امکان دریافت (GET) و افزودن (POST) رزروها را فراهم می‌کند. داده‌های ورودی با مدل Booking مطابقت دارند و به‌صورت موقت در آرایه bookings ذخیره می‌شوند (در فصل ۱۱ با Supabase جایگزین شد).

  • کاربرد در رابط کاربری:

    • در pages/bookings.vue:
      <script setup>
      import { useBookingsStore } from '~/stores/bookings';
      import { useFormatDate } from '~/composables/useFormatDate';

      const bookingsStore = useBookingsStore();
      onMounted(() => bookingsStore.fetchBookings());
      </script>
      <template>
        <div class="container mx-auto p-6">
          <h1 class="text-3xl font-bold mb-6">Your Bookings</h1>
          <div v-for="booking in bookingsStore.bookings" :key="booking.id" class="card mb-4">
            <p>Ship ID: {{ booking.shipId }}</p>
            <p>Dates: {{ useFormatDate(booking.startDate) }} to {{ useFormatDate(booking.endDate) }}</p>
            <p>Total Price: ${{ booking.totalPrice }}</p>
          </div>
        </div>
      </template>
  • توضیحات: این صفحه لیست رزروها را از store دریافت کرده و با استفاده از useFormatDate تاریخ‌ها را به‌صورت خوانا نمایش می‌دهد.

  • کاربرد در Store:

  • در stores/bookings.ts:

    import { defineStore } from 'pinia';
    import type { Booking } from '~/types';

    export const useBookingsStore = defineStore('bookings', {
      state: () => ({
        bookings: [] as Booking[],
      }),
      actions: {
        async fetchBookings() {
          this.bookings = await $fetch('/api/bookings');
        },
        async addBooking(booking: Omit<Booking, 'id'>) {
          const newBooking = await $fetch('/api/bookings', {
            method: 'POST',
            body: booking,
          });
          this.bookings.push(newBooking);
        },
      },
    });
  • توضیحات: store از مدل Booking برای مدیریت رزروها استفاده می‌کند. متد fetchBookings لیست رزروها را از API دریافت می‌کند، و addBooking رزرو جدید را به API ارسال کرده و به لیست اضافه می‌کند.

ادغام با پایگاه داده (فصل ۱۱)

در فصل سوم، داده‌ها به‌صورت موقت در حافظه ذخیره شدند، اما در فصل ۱۱ با Supabase یا Prisma جایگزین شدند. در زیر، جزئیات این ادغام برای مدل‌های Ship و Booking ارائه شده است:

  • Supabase:

  • جدول ships:

    CREATE TABLE ships (
      id SERIAL PRIMARY KEY,
      name TEXT NOT NULL,
      capacity INTEGER NOT NULL,
      price_per_day INTEGER NOT NULL,
      amenities TEXT[] NOT NULL,
      image TEXT NOT NULL
    );
  • جدول bookings:
    CREATE TABLE bookings (
      id SERIAL PRIMARY KEY,
      ship_id INTEGER REFERENCES ships(id),
      start_date TIMESTAMP NOT NULL,
      end_date TIMESTAMP NOT NULL,
      total_price INTEGER NOT NULL
    );
  • به‌روزرسانی API:
    // server/api/ships.ts
    import { serverSupabaseClient } from '#supabase/server';
    import type { Ship } from '~/types';

    export default defineEventHandler(async (event): Promise<Ship[]> => {
      const client = serverSupabaseClient(event);
      const { data } = await client.from('ships').select('*');
      return data || [];
    });
    // server/api/bookings.ts
    import { serverSupabaseClient } from '#supabase/server';
    import type { Booking } from '~/types';

    export default defineEventHandler({
      async get(event) {
        const client = serverSupabaseClient(event);
        const { data } = await client.from('bookings').select('*');
        return data || [];
      },
      async post(event) {
        const client = serverSupabaseClient(event);
        const body = await readBody(event);
        const newBooking: Booking = {
          id: Date.now(), // در تولید از UUID یا SERIAL استفاده کنید
          shipId: body.shipId,
          startDate: new Date(body.startDate),
          endDate: new Date(body.endDate),
          totalPrice: body.totalPrice,
        };
        const { data } = await client.from('bookings').insert(newBooking).select();
        return data[0];
      },
    });
  • Prisma:

  • Schema:

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

    model Ship {
      id          Int      @id @default(autoincrement())
      name        String
      capacity    Int
      pricePerDay Int
      amenities   String[]
      image       String
      bookings    Booking[]
    }

    model Booking {
      id         Int      @id @default(autoincrement())
      shipId     Int
      startDate  DateTime
      endDate    DateTime
      totalPrice Int
      ship       Ship     @relation(fields: [shipId], references: [id])
    }
  • به‌روزرسانی API:
    // server/api/ships.ts
    import { PrismaClient } from '@prisma/client';
    import type { Ship } from '~/types';

    const prisma = new PrismaClient();

    export default defineEventHandler(async (): Promise<Ship[]> => {
      return prisma.ship.findMany();
    });

چرا این بخش در ضمیمه‌ها خلاصه بود؟

ضمیمه‌های اولیه فقط schemaهای پایگاه داده (Supabase و Prisma) را ارائه کردند و توضیحات عمیق درباره:

  • تعریف دقیق مدل‌های Ship و Booking و فیلدهای آن‌ها.
  • کاربرد این مدل‌ها در APIها، storeها، و رابط کاربری.
  • ادغام کامل با پایگاه داده در فصل ۱۱.

را پوشش ندادند. این ضمیمه تمام این جنبه‌ها را با کدهای عملی و توضیحات جامع ارائه می‌دهد.


نکات عملی برای پیاده‌سازی

  1. ایجاد فایل types/index.ts:

  2. فایل را در ریشه پروژه ایجاد کرده و مدل‌های Ship و Booking را کپی کنید.

  3. اطمینان حاصل کنید که تمام APIها و storeها از این تایپ‌ها استفاده می‌کنند.

  4. تست مدل‌ها:

  5. با اجرای npm run dev، بررسی کنید که APIها (/api/ships و /api/bookings) داده‌های سازگار با مدل‌ها را برمی‌گردانند.

  6. از TypeScript برای شناسایی خطاهای تایپ در زمان توسعه استفاده کنید.

  7. گسترش مدل‌ها:

  8. می‌توانید فیلدهای اضافی به Ship اضافه کنید (مانند description یا rating).

  9. برای Booking، فیلدهایی مانند userId (برای ارتباط با کاربر) یا status (برای وضعیت رزرو) اضافه کنید.

  10. ادغام با پایگاه داده:

  11. برای Supabase، جداول را در داشبورد ایجاد کرده و RLS (Row Level Security) را فعال کنید.

  12. برای Prisma، از npx prisma migrate dev برای اعمال schema استفاده کنید.

جمع‌بندی ضمیمه ب

این ضمیمه جزئیات کامل مدل‌های داده Ship و Booking را ارائه داد، از جمله تعریف، کاربرد در APIها، storeها، و رابط کاربری، و همچنین ادغام با پایگاه داده‌های Supabase و Prisma. این اطلاعات مکمل فصل سوم بوده و پایه‌ای محکم برای مدیریت داده‌ها در اپلیکیشن فراهم می‌کند. برای اطلاعات بیشتر، به فصل‌های ۶ و ۱۱ مراجعه کنید یا مخزن فرضی GitHub (https://github.com/khosronz/cruise-rental-app) را بررسی کنید.