اتصال به API

لایه داده قالب از رابط کاربری جدا است: کامپوننت‌ها هیچ‌وقت به داده نمونه دست نمی‌زنند و همه از src/services/* می‌خوانند. نتیجه این‌که وصل‌کردن بک‌اند واقعی یک تغییر کوچک است.

قرارداد

متدمسیرکار
GET/resource?page=1&perPage=10&search=&sort=&order=&filters…فهرست با صفحه‌بندی
GET/resource/:idیک رکورد
POST/resourceساخت
PUT/resource/:idجایگزینی
PATCH/resource/:idبه‌روزرسانی جزئی
DELETE/resource/:idحذف

پاسخ فهرست یک پاکت ثابت دارد:

{
  "items": [],
  "total": 128,
  "page": 1,
  "perPage": 10,
  "pages": 13,
  "hasNext": true,
  "hasPrev": false
}

روشن کردن حالت واقعی

// src/config/config.js
api: {
  baseUrl: '/api',
  timeout: 8000,
  useMocks: false,   // ← همین‌جا کافی است
},

یا بدون تغییر کد، قبل از بارگذاری main.js:

<script>
  window.NOVA_ADMIN_API = { baseUrl: 'https://api.example.com/v1', useMocks: false, timeout: 10000 };
</script>

از این لحظه همان توابع services.* درخواست واقعی می‌فرستند؛ هیچ کنترل‌کننده‌ای تغییر نمی‌کند.

نمونه درخواست

import * as services from './services/index.js';

const page = await services.orderService.list({ page: 1, perPage: 20, search: 'INV', sort: 'createdAt', order: 'desc', filters: { status: 'paid' } });
const order = await services.orderService.get('in-1024');
const created = await services.orderService.create({ customer: 'شرکت آلفا', total: 24000000 });
await services.orderService.patch(created.id, { status: 'paid' });
await services.orderService.remove(created.id);

خطاها

هر پاسخ ناموفق به ApiError تبدیل می‌شود:

import { ApiError } from './services/client.js';

try {
  await services.productService.get('missing-id');
} catch (error) {
  if (error instanceof ApiError) {
    console.error(error.status, error.code, error.message);
    toast.danger('خطا', error.message);
  }
}
کدوضعیتمعنی
api_error۵۰۰خطای عمومی
not_found۴۰۴رکورد پیدا نشد
validation_error۴۲۲داده ورودی نامعتبر
unauthorized۴۰۱نیاز به ورود

افزودن سرویس جدید

// src/services/marketing.service.js
import { createResourceService } from './resource.js';
import { campaigns } from '../data/marketing.js';

export const campaignService = createResourceService({
  name: 'campaigns',
  collection: () => campaigns,
  idPrefix: 'cp',
  searchFields: ['name', 'channel', 'owner'],
  sortFields: ['name', 'budget', 'startedAt'],
});

سپس در src/services/index.js آن را export و در رجیستری پیش‌فرض ثبت کنید:

export { campaignService } from './marketing.service.js';
// …
campaigns: campaignService,

از این لحظه data-resource="campaigns" در هر جدولی کار می‌کند.

تطبیق با بک‌اندهای مختلف

اگر API شما پاسخ را در پاکت دیگری می‌فرستد ({ data: [...], meta: {...} })، فقط client.js را تطبیق دهید: خروجی fetch را به شکل استاندارد بالا تبدیل کنید و بقیه لایه دست‌نخورده می‌ماند.

نکته: تأخیر مصنوعی (mockLatency) برای دیده‌شدن حالت‌های بارگذاری است

در حالت واقعی این تأخیر وجود ندارد.

هشدار: هرگز داده نمونه را مستقیم در کنترل‌کننده import نکنید؛ همین کار باعث می‌شود بعداً نتوانید به API وصل شوید

API integration

The template's data layer is separate from the UI: components never touch sample data and all of them read from src/services/*. As a result, connecting a real backend is a small change.

Contract

MethodPathPurpose
GET/resource?page=1&amp;perPage=10&amp;search=&amp;sort=&amp;order=&amp;filters…Paginated list
GET/resource/:idSingle record
POST/resourceCreate
PUT/resource/:idReplace
PATCH/resource/:idPartial update
DELETE/resource/:idDelete

The list response has a fixed envelope:

{
  "items": [],
  "total": 128,
  "page": 1,
  "perPage": 10,
  "pages": 13,
  "hasNext": true,
  "hasPrev": false
}

Enabling real mode

// src/config/config.js
api: {
  baseUrl: '/api',
  timeout: 8000,
  useMocks: false,   // ← this is all it takes
},

Or, without changing code, before main.js loads:

<script>
  window.NOVA_ADMIN_API = { baseUrl: 'https://api.example.com/v1', useMocks: false, timeout: 10000 };
</script>

From then on the same services.* functions send real requests; no controller changes.

Request examples

import * as services from './services/index.js';

const page = await services.orderService.list({ page: 1, perPage: 20, search: 'INV', sort: 'createdAt', order: 'desc', filters: { status: 'paid' } });
const order = await services.orderService.get('in-1024');
const created = await services.orderService.create({ customer: 'Alpha Co.', total: 24000000 });
await services.orderService.patch(created.id, { status: 'paid' });
await services.orderService.remove(created.id);

Errors

Every failed response is turned into an ApiError:

import { ApiError } from './services/client.js';

try {
  await services.productService.get('missing-id');
} catch (error) {
  if (error instanceof ApiError) {
    console.error(error.status, error.code, error.message);
    toast.danger('Error', error.message);
  }
}
CodeStatusMeaning
api_error500Generic error
not_found404Record not found
validation_error422Invalid input data
unauthorized401Sign-in required

Adding a new service

// src/services/marketing.service.js
import { createResourceService } from './resource.js';
import { campaigns } from '../data/marketing.js';

export const campaignService = createResourceService({
  name: 'campaigns',
  collection: () => campaigns,
  idPrefix: 'cp',
  searchFields: ['name', 'channel', 'owner'],
  sortFields: ['name', 'budget', 'startedAt'],
});

Then export it from src/services/index.js and register it in the default registry:

export { campaignService } from './marketing.service.js';
// …
campaigns: campaignService,

From then on data-resource="campaigns" works in any table.

Adapting to different backends

If your API returns responses in a different envelope ({ data: [...], meta: {...} }), only adapt client.js: convert the fetch output to the standard shape above and the rest of the layer stays untouched.

Tip: the artificial delay (mockLatency) exists so loading states are visible

There is no such delay in real mode.

Warning: never import sample data directly in a controller; doing so is exactly what later prevents you from connecting to an API

الربط بـ API

طبقة البيانات في القالب منفصلة عن الواجهة: لا تلمس المكونات البيانات النموذجية أبداً وجميعها تقرأ من src/services/*. والنتيجة أن ربط واجهة خلفية حقيقية تغيير صغير.

العقد

الطريقةالمسارالوظيفة
GET/resource?page=1&amp;perPage=10&amp;search=&amp;sort=&amp;order=&amp;filters…قائمة مع ترقيم الصفحات
GET/resource/:idسجل واحد
POST/resourceإنشاء
PUT/resource/:idاستبدال
PATCH/resource/:idتحديث جزئي
DELETE/resource/:idحذف

لاستجابة القائمة غلاف ثابت:

{
  "items": [],
  "total": 128,
  "page": 1,
  "perPage": 10,
  "pages": 13,
  "hasNext": true,
  "hasPrev": false
}

تفعيل الوضع الحقيقي

// src/config/config.js
api: {
  baseUrl: '/api',
  timeout: 8000,
  useMocks: false,   // ← هذا وحده يكفي
},

أو دون تغيير الشيفرة، قبل تحميل main.js:

<script>
  window.NOVA_ADMIN_API = { baseUrl: 'https://api.example.com/v1', useMocks: false, timeout: 10000 };
</script>

من هذه اللحظة ترسل دوال services.* نفسها طلبات حقيقية؛ ولا يتغير أي متحكم.

أمثلة الطلبات

import * as services from './services/index.js';

const page = await services.orderService.list({ page: 1, perPage: 20, search: 'INV', sort: 'createdAt', order: 'desc', filters: { status: 'paid' } });
const order = await services.orderService.get('in-1024');
const created = await services.orderService.create({ customer: 'شركة ألفا', total: 24000000 });
await services.orderService.patch(created.id, { status: 'paid' });
await services.orderService.remove(created.id);

الأخطاء

تُحوَّل كل استجابة فاشلة إلى ApiError:

import { ApiError } from './services/client.js';

try {
  await services.productService.get('missing-id');
} catch (error) {
  if (error instanceof ApiError) {
    console.error(error.status, error.code, error.message);
    toast.danger('خطأ', error.message);
  }
}
الرمزالحالةالمعنى
api_error٥٠٠خطأ عام
not_found٤٠٤لم يُعثر على السجل
validation_error٤٢٢بيانات إدخال غير صالحة
unauthorized٤٠١يلزم تسجيل الدخول

إضافة خدمة جديدة

// src/services/marketing.service.js
import { createResourceService } from './resource.js';
import { campaigns } from '../data/marketing.js';

export const campaignService = createResourceService({
  name: 'campaigns',
  collection: () => campaigns,
  idPrefix: 'cp',
  searchFields: ['name', 'channel', 'owner'],
  sortFields: ['name', 'budget', 'startedAt'],
});

ثم صدّرها من src/services/index.js وسجّلها في السجل الافتراضي:

export { campaignService } from './marketing.service.js';
// …
campaigns: campaignService,

من هذه اللحظة يعمل data-resource="campaigns" في أي جدول.

التوافق مع واجهات خلفية مختلفة

إذا كانت واجهتك البرمجية ترسل الاستجابة في غلاف آخر ({ data: [...], meta: {...} })، فعدّل client.js فقط: حوّل مخرجات fetch إلى الشكل القياسي أعلاه وتبقى بقية الطبقة كما هي.

ملاحظة: التأخير المصطنع (mockLatency) موجود لإظهار حالات التحميل

ولا يوجد هذا التأخير في الوضع الحقيقي.

تحذير: لا تستورد أبداً البيانات النموذجية مباشرة في متحكم؛ فهذا بالضبط ما يمنعك لاحقاً من الربط بـ API