يوفر هذا الدليل تعليمات خطوة بخطوة لإنشاء مشروع Laravel، وإعداد Vue وInertia.js، وتثبيت وتهيئة حزمة squipix/tailwind-datatables.
أولاً، أنشئ مشروع Laravel جديداً. يُرجى البدء بتطوير مشروعك من هذه الخطوة؛ فهي ضرورية لضمان بداية صحيحة:
laravel new squipix-datatable-vue && cd squipix-datatable-vue && composer require laravel/breeze --dev && touch resources/js/bootstrap.js && php artisan breeze:install vue && rm resources/js/app.ts && rm vite.config.ts && rm -rf resources/js/pages
إصلاح فوضى ملف package.json
ملف package.json الذي يأتي مع تثبيت Laravel-Vue الجديد يكون نظيفاً ومنظماً. ومع ذلك، عند تشغيل أمر breeze:install vue، يُفسد هذا السقالة البنية الموجودة. يُعالج سكريبت التثبيت لدينا معظم الأضرار الناجمة عن هذه الإضافة، لكنك تحتاج الآن إلى تنظيف ملف package.json الخاص بك لأنه أصبح فوضوياً بسبب أمر breeze:install vue وسيتسبب في تعارضات.
في هذه المرحلة، يجب أن يبدو ملف package.json كما يلي:
{
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"build:ssr": "vite build && vite build --ssr",
"dev": "vite",
"format": "prettier --write resources/",
"format:check": "prettier --check resources/",
"lint": "eslint . --fix"
},
"devDependencies": {
"[@eslint](https://github.com/eslint)/js": "^9.19.0",
"[@tailwindcss](https://github.com/tailwindcss)/forms": "^0.5.3",
"[@tailwindcss](https://github.com/tailwindcss)/postcss": "^4.1.11",
"[@types](https://github.com/types)/node": "^22.13.5",
"[@vue](https://github.com/vue)/eslint-config-typescript": "^14.3.0",
"autoprefixer": "^10.4.12",
"eslint": "^9.17.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-vue": "^9.32.0",
"postcss": "^8.4.31",
"prettier": "^3.4.2",
"prettier-plugin-organize-imports": "^4.1.0",
"prettier-plugin-tailwindcss": "^0.6.11",
"typescript-eslint": "^8.23.0",
"vue-tsc": "^2.2.4"
},
"dependencies": {
"[@inertiajs](https://github.com/inertiajs)/vue3": "^2.0.0",
"[@vitejs](https://github.com/vitejs)/plugin-vue": "^5.2.1",
"[@vueuse](https://github.com/vueuse)/core": "^12.8.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"concurrently": "^9.0.1",
"laravel-vite-plugin": "^1.0",
"lucide-vue-next": "^0.468.0",
"reka-ui": "^2.2.0",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.1",
"tw-animate-css": "^1.2.5",
"typescript": "^5.2.2",
"vite": "^6.2.0",
"vue": "^3.5.13",
"ziggy-js": "^2.4.2"
}
}
composer require squipix/tailwind-datatables
نشر أصول الحزمة:
composer update
php artisan vendor:publish --tag=tailwind-datatables-views
php artisan vendor:publish --tag=squipix-tailwind-datatables-config
php artisan vendor:publish --tag=tailwind-datatables-css
بعد ذلك، قم بتحديث محتوى ملف resources/js/app.js كما هو موضح أدناه. يجب أن يكون هذا الملف موجوداً بالفعل.
// resources/js/app.js
import '../css/app.css';
// ********************************************************************
// * DataTables والمكتبات ذات الصلة (الاحتياجات العامة فقط) *
// ********************************************************************
import jQuery from 'jquery';
window.jQuery = jQuery;
window.$ = jQuery;
import 'datatables.net';
import 'datatables.net-buttons';
import 'datatables.net-buttons/js/buttons.html5.js';
import 'datatables.net-buttons/js/buttons.print.js';
import 'datatables.net-buttons/js/buttons.colVis.js';
import 'datatables.net-responsive';
import JSZip from 'jszip';
import pdfMake from 'pdfmake/build/pdfmake';
import pdfFonts from 'pdfmake/build/vfs_fonts';
window.JSZip = JSZip;
window.pdfMake = pdfMake;
window.pdfMake.vfs = pdfFonts.vfs;
import Swal from 'sweetalert2';
window.Swal = Swal;
import dayjs from 'dayjs';
window.dayjs = dayjs;
import Alpine from 'alpinejs';
import { registerGorlabsDatatablesAlpineComponents } from '../../vendor/squipix/tailwind-datatables/resources/js/crud-datatable';
// ********************************************************************
// * كود تهيئة Inertia.js و Vue الحالي (لا تغيّره) *
// ********************************************************************
import { createInertiaApp } from '[@inertiajs](https://github.com/inertiajs)/vue3';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { createApp, h } from 'vue';
import { ZiggyVue } from 'ziggy-js';
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
createInertiaApp({
title: (title) => `${title} - ${appName}`,
resolve: (name) =>
resolvePageComponent(
`./Pages/${name}.vue`,
import.meta.glob('./Pages/**/*.vue'),
),
setup({ el, App, props, plugin }) {
return createApp({ render: () => h(App, props) })
.use(plugin)
.use(ZiggyVue)
.mount(el);
},
progress: {
color: '#4B5563',
},
});
// القسم الجديد المضاف: انتظر تحميل Alpine.js وتسجيل المكونات
// يُطلق مستمع الحدث هذا عند تحميل Alpine.js من CDN وجاهزيته.
registerGorlabsDatatablesAlpineComponents(Alpine); // تسجيل المكونات
Alpine.start();
تحديث محتوى postcss.config.js
export default {
plugins: {
'[@tailwindcss](https://github.com/tailwindcss)/postcss': {},
autoprefixer: {},
},
};
قم بتحديث محتوى ملف vite.config.js كما هو موضح أدناه. يجب أن يكون هذا الملف موجوداً بالفعل.
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '[@vitejs](https://github.com/vitejs)/plugin-vue';
export default defineConfig({
plugins: [
laravel({
input: [
'resources/css/app.css',
'resources/js/app.js'
],
refresh: true,
}),
vue({
template: {
transformAssetUrls: {
base: null,
includeAbsolute: false,
},
},
}),
],
build: {
minify: true, // تفعيل الضغط
sourcemap: true, // إنشاء sourcemap لتسهيل تصحيح الأخطاء
},
server: {
cors: true,
},
});
resources/js/types/global.d.ts//resources/js/types/globals.d.ts
import { AppPageProps } from '@/types/index';
import type Swal from 'sweetalert2'; // استخدام type import
import type dayjs from 'dayjs'; // استخدام type import
import type Alpine from 'alpinejs'; // استخدام type import
import type * as pdfMake from 'pdfmake/build/pdfmake'; // استخدام type import
import type * as JSZip from 'jszip'; // استخدام type import
// توسيع واجهة ImportMeta لـ Vite...
declare module 'vite/client' {
interface ImportMetaEnv {
readonly VITE_APP_NAME: string;
[key: string]: string | boolean | undefined;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
readonly glob: <T>(pattern: string) => Record<string, () => Promise<T>>;
}
}
declare module '[@inertiajs](https://github.com/inertiajs)/core' {
interface PageProps extends InertiaPageProps, AppPageProps {}
}
declare module 'vue' {
interface ComponentCustomProperties {
$inertia: typeof Router;
$page: Page;
$headManager: ReturnType<typeof createHeadManager>;
}
}
declare global {
interface Window {
GorlabsDatatables: {
date: (format: string) => (data: any, type: string, row: any) => string;
statusBadge: (publishedText: string, draftText: string) => (data: any, type: string, row: any) => string;
actions: (updateUrlPrefix: string, deleteUrlPrefix: string) => (data: any, type: string, row: any) => string;
// أضف هنا بقية توابع GorlabsDatatables
};
Swal: any; // يمكنك إضافة تعريف مماثل لـ SweetAlert2
openFormModal: (title: string, url: string, item: any | null) => void; // للدالة العامة أيضاً
}
}
// ********************************************************************
// * القسم الجديد المضاف: تعريفات كائن Window العامة *
// ********************************************************************
declare global {
interface Window {
Swal: typeof Swal;
dayjs: typeof dayjs;
Alpine: typeof Alpine;
pdfMake: typeof pdfMake;
JSZip: typeof JSZip;
jQuery: JQuery;
$: JQuery;
crudDataTable: (config: any) => any;
}
}
أنشئ ملف migration لإعداد جدول posts واستبدل محتواه بالتالي:
php artisan make:migration create_posts_table
سيكون مسار الملف المُنشأ: database/migrations/YYYY_MM_DD_HHMMSS_create_posts_table.php (حيث YYYY_MM_DD_HHMMSS تتغير بحسب التاريخ والوقت). قم بتحديث محتواه كما يلي:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* تشغيل عمليات الترحيل.
*/
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->timestamp('published_at')->nullable(); // تاريخ النشر
$table->boolean('is_published')->default(false); // الحالة (هل تم النشر؟)
$table->string('status')->default('draft'); // أضيف كمثال
$table->timestamps();
});
}
/**
* عكس عمليات الترحيل.
*/
public function down(): void
{
Schema::dropIfExists('posts');
}
};
أنشئ نموذج Post وملف PostFactory.php واستبدل محتواهما بالتالي:
php artisan make:model Post -f
سيكون مسار الملف المُنشأ: app/Models/Post.php. قم بتحديث محتواه كما يلي:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
use HasFactory;
protected $fillable = [
'title',
'content',
'published_at',
'is_published',
'status',
];
protected $casts = [
'is_published' => 'boolean',
'published_at' => 'datetime',
];
}
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* [@extends](https://github.com/extends) \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Post>
*/
class PostFactory extends Factory
{
/**
* تعريف الحالة الافتراضية للنموذج.
*
* [@return](https://github.com/return) array<string, mixed>
*/
public function definition(): array
{
return [
'title' => fake()->sentence(rand(3, 8)),
'content' => fake()->paragraphs(rand(1, 3), true),
'published_at' => fake()->optional(0.8)->dateTimeThisYear(), // يعطي تاريخاً باحتمال 80%
'is_published' => fake()->boolean(90), // true باحتمال 90%
'status' => fake()->randomElement(['draft', 'published', 'archived']),
];
}
}
php artisan make:seeder PostSeeder
<?php
namespace Database\Seeders;
use App\Models\Post;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class PostSeeder extends Seeder
{
/**
* تشغيل بذور قاعدة البيانات.
*/
public function run(): void
{
// إنشاء 50 منشوراً وهمياً
Post::factory()->count(50)->create();
}
}
<?php
namespace Database\Seeders;
use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Database\Factories\PostFactory;use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* بذر قاعدة بيانات التطبيق.
*/
public function run(): void
{
// User::factory(10)->create();
User::factory()->create([
'name' => 'Test User',
'email' => 'test@example.com',
]);
$this->call([PostSeeder::class,]);
}
}
أنشئ فئة PostsDataTable واستبدل محتواها بالتالي:
php artisan datatable:make PostsDataTable --model=Post
سيكون مسار الملف المُنشأ: app/DataTables/PostsDataTable.php. قم بتحديث محتواه كما يلي:
<?php
namespace App\DataTables;
use App\Models\Post;
use Illuminate\Database\Eloquent\Builder as QueryBuilder;
use Yajra\DataTables\EloquentDataTable;
use Yajra\DataTables\Html\
Builder as HtmlBuilder;
use Yajra\DataTables\Html\Button;
use Yajra\DataTables\Html\Column;
use Yajra\DataTables\Services\DataTable;
class PostsDataTable extends DataTable
{
public function query(Post $model): QueryBuilder
{
return $model->newQuery()->select('id', 'title', 'content', 'is_published', 'published_at', 'status', 'created_at', 'updated_at');
}
public function dataTable(QueryBuilder $query): EloquentDataTable
{
return (new EloquentDataTable($query))
->setRowId('id')
->addColumn('select_checkbox', function(Post $post) {
return '';
})
->addColumn('actions', function(Post $post) {
return '';
})
->editColumn('is_published', function(Post $post) {
return $post->is_published ? 1 : 0;
})
->editColumn('published_at', function(Post $post) {
return $post->published_at ? $post->published_at->format('Y-m-d H:i:s') : null;
})
->editColumn('title', function(Post $post) {
$title = $post->title;
if (strlen($title) > 24) {
return substr($title, 0, 24) . ' ...';
}
return $title;
})
->editColumn('content', function(Post $post) {
$content = $post->content;
if (strlen($content) > 34) {
return substr($content, 0, 34) . ' ...';
}
return $content;
});
}
public function html(): HtmlBuilder
{
return $this->builder()
->setTableId('posts-table')
->columns($this->getColumns())
->minifiedAjax()
->orderBy(1)
->select(false)
->buttons([
Button::make('create'),
Button::make('export'),
Button::make('print'),
Button::make('reset'),
Button::make('reload')
]);
}
public function getColumns(): array
{
$columns = [
Column::computed('select_checkbox')
->title('<input type="checkbox" id="select-all-checkbox" class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500">')
->exportable(false)
->printable(false)
->width(10)
->addClass('text-center')
->orderable(false)
->searchable(false)
->footer('')
->responsivePriority(1),
Column::make('id')->responsivePriority(2),
Column::make('title')->responsivePriority(3),
Column::make('content')->responsivePriority(5),
Column::make('published_at')
->title('Published At')
->width(150)
->responsivePriority(4),
Column::make('is_published')
->title('Published')
->width(100)
->responsivePriority(4),
Column::make('status')->responsivePriority(4),
];
$columns[] = Column::computed('actions')
->title('ACTIONS')
->exportable(false)
->printable(false)
->width(120)
->addClass('text-center')
->orderable(false)
->searchable(false)
->footer('')
->responsivePriority(1);
return $columns;
}
protected function filename(): string
{
return 'Posts_' . date('YmdHis');
}
}
أنشئ فئة PostController واستبدل محتواها بالتالي:
php artisan make:controller PostController
سيكون مسار الملف المُنشأ: app/Http/Controllers/PostController.php. قم بتحديث محتواه كما يلي:
<?php
namespace App\Http\Controllers;
use App\DataTables\PostsDataTable;
use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Inertia\Inertia;
class PostController extends Controller
{
public function index(PostsDataTable $postsDataTable)
{
$columns = $postsDataTable->getColumns();
$formattedColumns = collect($columns)->map(function ($column) {
return [
'data' => $column->data,
'name' => $column->name,
'title' => $column->title,
'orderable' => $column->orderable,
'searchable' => $column->searchable,
'className' => $column->className ?? '',
];
})->toArray();
return Inertia::render('Posts/Index', [
'columns' => $formattedColumns,
]);
}
public function ajaxData(PostsDataTable $dataTable)
{
Log::info('معاملات طلب DataTables AJAX:', request()->all());
return $dataTable->dataTable($dataTable->query(new Post()))->make(true);
}
public function create()
{
return view('tailwind-datatables::datatables.form', ['post' => new Post()]);
}
public function store(Request $request)
{
$request->validate([
'title' => 'required|string|max:255',
'content' => 'nullable|string',
'is_published' => 'nullable|boolean',
'published_at' => 'nullable|date',
]);
$data = $request->except(['published_at']);
$data['published_at'] = $request->input('published_at') ? now()->parse($request->input('published_at')) : null;
$data['is_published'] = (bool) $request->input('is_published', 0);
$post = Post::create($data);
return response()->json(['success' => 'تم إنشاء المنشور بنجاح.', 'post' => $post]);
}
public function edit(Post $post)
{
$post->published_at_formatted = $post->published_at ? $post->published_at->format('Y-m-d\TH:i') : '';
return view('tailwind-datatables::datatables.form', compact('post'));
}
public function update(Request $request, Post $post)
{
$request->validate([
'title' => 'required|string|max:255',
'content' => 'nullable|string',
'is_published' => 'boolean|nullable',
'published_at' => 'nullable|date',
]);
$data = $request->except(['published_at']);
$data['is_published'] = (bool) $request->input('is_published', 0);
$data['published_at'] = $request->input('published_at') ? now()->parse($request->input('published_at')) : null;
$post->update($data);
return response()->json(['success' => 'تم تحديث المنشور بنجاح.', 'post' => $post]);
}
public function destroy(Post $post)
{
try {
$post->delete();
return response()->json(['success' => 'تم حذف المنشور بنجاح.']);
} catch (\Exception $e) {
Log::error('حدث خطأ أثناء حذف المنشور: ' . $e->getMessage(), ['post_id' => $post->id]);
return response()->json(['error' => 'حدث خطأ أثناء حذف المنشور.'], 500);
}
}
}
أنشئ ملف resources/js/Pages/Posts/Index.vue واستبدل محتواه بالتالي:
mkdir -p resources/js/Pages/Posts
touch resources/js/Pages/Posts/Index.vue
سيكون مسار الملف المُنشأ: resources/js/Pages/Posts/Index.vue. قم بتحديث محتواه كما يلي:
<script setup lang="ts">
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import { Head } from '[@inertiajs](https://github.com/inertiajs)/vue3';
import { onMounted, ref } from 'vue';
// تعريف خاصية columns القادمة من الخلفية
const props = defineProps<{
columns: Array<{
data: string;
name: string;
title: string;
orderable: boolean;
searchable: boolean;
className?: string; // خيار className اختياري
}>;
}>();
console.log('Props Columns:', props.columns);
// إنشاء ref لبنية HTML الخاصة بـ DataTables
const dataTableContainer = ref<HTMLElement | null>(null);
// تعريف إعداد crudDataTable باستخدام الأعمدة من props
const datatableConfig = {
datatableId: 'posts-table',
ajaxUrl: route('posts.data'),
columns: props.columns, // الحصول على الأعمدة من props!
perPage: 10,
perPageSelect: [10, 25, 50, 100, -1],
addNewButtonText: 'إضافة منشور جديد',
addEditUrl: route('posts.create'),
updateUrlPrefix: route('posts.update', ''),
deleteUrlPrefix: route('posts.destroy', ''),
initialFormState: {},
responsive: true, // تفعيل ميزة الاستجابة
};
// ref لتخزين نسخة DataTables
const dataTableInstance = ref<any>(null);
const $ = window.$ as any;
onMounted(() => {
// التأكد من توفر jQuery وDataTables عالمياً
if (typeof window.$ === 'undefined' || typeof (window.$ as any).fn.DataTable === 'undefined') {
console.error('jQuery أو DataTables غير متوفر عالمياً.');
return;
}
// التحقق من وجود GorlabsDatatables
if (typeof window.GorlabsDatatables === 'undefined') {
console.error('GorlabsDatatables غير متوفر عالمياً.');
return;
}
// تهيئة DataTables
// نستخدم 'as any' لمنع TypeScript من تحميل نوع jQuery بشكل زائد
dataTableInstance.value = $('#' + datatableConfig.datatableId).DataTable({...
How can I help you explore Laravel packages today?