Tuyệt vời, bạn đã có hệ thống CRUD Page hoàn chỉnh. Bây giờ mình sẽ hướng dẫn phân quyền chi tiết ch

resources\views\admin\pages\create.blade.php

@extends('layouts.admin')
@section('content')
<div class="container mt-4">
  <form action="{{ route('admin.pages.store') }}" method="POST">
    @csrf
    <div class="mb-3">
      <label class="form-label">Tiêu đề</label>
      <input type="text" name="title" class="form-control" value="{{ old('title') }}" required>
    </div>
    <div class="mb-3">
      <label class="form-label">Slug</label>
      <input type="text" name="slug" class="form-control" value="{{ old('slug') }}" required>
    </div>
    <hr>
    <div class="mb-3">
      <label for="image" class="form-label">Ảnh</label>
      <div class="input-group">
        <input id="image_url" class="form-control" type="text" name="image" value="{{ old('image', $page->image ?? '') }}">
        <button type="button" class="btn btn-secondary" onclick="openLfm('image_url', 'image_preview')">Chọn ảnh</button>
      </div>
      <img id="image-preview" src="{{ old('image', $page->image ?? '') }}" style="max-height: 150px; margin-top: 10px">
    </div>
    <div class="mb-3">
      <label for="content_vi" class="form-label">Nội dung (Tiếng Việt)</label>
      <textarea name="content_vi" class="form-control editor" rows="10">{{ old('content_vi', $page->content_vi ?? '') }}</textarea>
    </div>
    <div class="mb-3">
      <label for="content_en" class="form-label">Nội dung (English)</label>
      <textarea name="content_en" class="form-control editor" rows="10">{{ old('content_en', $page->content_en ?? '') }}</textarea>
    </div>
    <div>
      <button type="submit" class="btn btn-primary">Tạo trang</button>
      <a href="{{ route('admin.pages.index') }}" class="btn btn-light">Quay lại</a>
    </div>
  </form>
</div>
@endsection
@section('scripts')
<script src="https://cdn.ckeditor.com/ckeditor5/39.0.1/classic/ckeditor.js"></script>
<script>
  const editors = {};
  function insertImageToEditor(editorName, imageUrl) {
    const editor = editors[editorName];
    if (editor) {
      editor.model.change(writer => {
        const imageElement = writer.createElement('imageBlock', {
          src: imageUrl
        });
        editor.model.insertContent(imageElement, editor.model.document.selection);
      });
    }
  }
  function openLfmForEditor(editorName) {
    window.open('/laravel-filemanager?type=Images', 'fm', 'width=800,height=600');
    window.SetUrl = function(items) {
      const url = Array.isArray(items) ? items[0].url : items.url;
      insertImageToEditor(editorName, url);
    };
  }
  document.querySelectorAll('.editor').forEach((textarea, index) => {
    const name = textarea.getAttribute('name') || 'editor_' + index;
    ClassicEditor.create(textarea, {
      toolbar: {
        items: [
          'heading', '|'
          , 'bold', 'italic', 'link', 'bulletedList', 'numberedList', '|'
          , 'undo', 'redo'
        ]
      }
    }).then(editor => {
      editors[name] = editor;
      // Tạo nút chèn ảnh
      const insertBtn = document.createElement('button');
      insertBtn.type = 'button';
      insertBtn.className = 'btn btn-sm btn-secondary mt-2';
      insertBtn.innerText = 'Chèn ảnh từ thư viện';
      insertBtn.addEventListener('click', () => {
        openLfmForEditor(name);
      });
      // Gắn nút sau editor
      textarea.parentNode.appendChild(insertBtn);
    }).catch(error => {
      console.error(error);
    });
    // Thêm nút chèn ảnh ngay sau textarea
    const button = document.createElement('button');
    button.type = 'button';
    button.innerText = 'Chèn ảnh từ thư viện';
    button.className = 'btn btn-sm btn-secondary mt-2';
    button.onclick = () => openLfmForEditor(name);
    textarea.parentNode.insertBefore(button, textarea.nextSibling);
  });
</script>
<script src="{{ asset('js/lfm.js') }}"></script>
@endsection

public\js\lfm.js

function openLfm(inputId, previewId) {
  window.open('/laravel-filemanager?type=image', 'fm', 'width=800,height=600');
  window.SetUrl = function(url) {
    // Nếu url là mảng, lấy phần tử đầu tiên và trường url
    if (Array.isArray(url)) {
      url = url[0]?.url || '';
    }
    if (!url) {
      alert('Không lấy được url ảnh');
      return;
    }
    // Gán url vào input hoặc ảnh hiển thị
     document.getElementById(inputId).value = url;
    document.getElementById(previewId).src = url;
  };
}
function generateSlug(sourceId, targetId) {
  let title = document.getElementById(sourceId).value;
  let slug = title.toLowerCase();
  slug = slug.replace(/á|à|ả|ạ|ã|ă|ắ|ằ|ẳ|ẵ|ặ|â|ấ|ầ|ẩ|ẫ|ậ/gi, 'a');
  slug = slug.replace(/é|è|ẻ|ẽ|ẹ|ê|ế|ề|ể|ễ|ệ/gi, 'e');
  slug = slug.replace(/i|í|ì|ỉ|ĩ|ị/gi, 'i');
  slug = slug.replace(/ó|ò|ỏ|õ|ọ|ô|ố|ồ|ổ|ỗ|ộ|ơ|ớ|ờ|ở|ỡ|ợ/gi, 'o');
  slug = slug.replace(/ú|ù|ủ|ũ|ụ|ư|ứ|ừ|ử|ữ|ự/gi, 'u');
  slug = slug.replace(/ý|ỳ|ỷ|ỹ|ỵ/gi, 'y');
  slug = slug.replace(/đ/gi, 'd');
  slug = slug.replace(/[`~!@#|$%^&*()+=,.?/<>:;"'[\]{}\\]/gi, '');
  slug = slug.replace(/\s+/g, '-');
  slug = slug.replace(/-+/g, '-');
  slug = slug.replace(/^-+|-+$/g, '');
  document.getElementById(targetId).value = slug;
}

resources\views\admin\pages\edit.blade.php

@extends('layouts.admin')
@section('content')
<div class="container">
  <h2 class="font-semibold text-xl text-gray-800 leading-tight">
    Sửa trang: {{ $page->title }}
  </h2>
  <div class="container mt-4">
    <form action="{{ route('admin.pages.update', $page) }}" method="POST">
      @csrf
      @method('PUT')
      <div class="mb-3">
        <label class="form-label">Tiêu đề</label>
        <input type="text" name="title" class="form-control" value="{{ old('title', $page->title) }}" required>
      </div>
      <div class="mb-3">
        <label class="form-label">Slug</label>
        <input type="text" name="slug" class="form-control" value="{{ old('slug', $page->slug) }}" required>
      </div>
      <hr>
      <div class="mb-3">
        <label for="image" class="form-label">Ảnh</label>
        <div class="input-group">
          <input id="image_url" class="form-control" type="text" name="image" value="{{ old('image', $page->image ?? '') }}">
          <button type="button" class="btn btn-secondary" onclick="openLfm('image_url', 'image_preview')">Chọn ảnh</button>
        </div>
        <img id="image-preview" src="{{ old('image', $page->image ?? '') }}" style="max-height: 150px; margin-top: 10px">
      </div>
      <div class="mb-3">
        <label for="content_vi" class="form-label">Nội dung (Tiếng Việt)</label>
        <textarea name="content_vi" class="form-control editor" rows="10">{{ old('content_vi', $page->getTranslation('content', 'vi')) }}</textarea>
      </div>
      <div class="mb-3">
        <label for="content_en" class="form-label">Nội dung (English)</label>
        <textarea name="content_en" class="form-control editor" rows="10">{{ old('content_en', $page->getTranslation('content', 'en')) }}</textarea>
      </div>
      <button type="button" class="btn btn-secondary mb-3" onclick="addField()">+ Thêm trường</button>
      <div>
        <button type="submit" class="btn btn-primary">Cập nhật</button>
        <a href="{{ route('admin.pages.index') }}" class="btn btn-light">Quay lại</a>
      </div>
    </form>
  </div>
</div>
@endsection
@section('scripts')
<script src="https://cdn.ckeditor.com/ckeditor5/39.0.1/classic/ckeditor.js"></script>
<script>
  const editors = {};
  function insertImageToEditor(editorName, imageUrl) {
    const editor = editors[editorName];
    if (editor) {
      editor.model.change(writer => {
        const imageElement = writer.createElement('imageBlock', {
          src: imageUrl
        });
        editor.model.insertContent(imageElement, editor.model.document.selection);
      });
    }
  }
  function openLfmForEditor(editorName) {
    window.open('/laravel-filemanager?type=Images', 'fm', 'width=800,height=600');
    window.SetUrl = function(items) {
      const url = Array.isArray(items) ? items[0].url : items.url;
      insertImageToEditor(editorName, url);
    };
  }
  document.querySelectorAll('.editor').forEach((textarea, index) => {
    const name = textarea.getAttribute('name') || 'editor_' + index;
    ClassicEditor.create(textarea, {
      toolbar: {
        items: [
          'heading', '|'
          , 'bold', 'italic', 'link', 'bulletedList', 'numberedList', '|'
          , 'undo', 'redo'
        ]
      }
    }).then(editor => {
      editors[name] = editor;
      // Tạo nút chèn ảnh
      const insertBtn = document.createElement('button');
      insertBtn.type = 'button';
      insertBtn.className = 'btn btn-sm btn-secondary mt-2';
      insertBtn.innerText = 'Chèn ảnh từ thư viện';
      insertBtn.addEventListener('click', () => {
        openLfmForEditor(name);
      });
      // Gắn nút sau editor
      textarea.parentNode.appendChild(insertBtn);
    }).catch(error => {
      console.error(error);
    });
    // Thêm nút chèn ảnh ngay sau textarea
    const button = document.createElement('button');
    button.type = 'button';
    button.innerText = 'Chèn ảnh từ thư viện';
    button.className = 'btn btn-sm btn-secondary mt-2';
    button.onclick = () => openLfmForEditor(name);
    textarea.parentNode.insertBefore(button, textarea.nextSibling);
  });
</script>
<script src="{{ asset('js/lfm.js') }}"></script>
@endsection

resources\views\admin\pages\index.blade.php

@extends('layouts.admin')
@section('content')
<div class="container">
  <h2 class="font-semibold text-xl text-gray-800 leading-tight">Danh sách trang</h2>
  <div class="container mt-4">
    <a href="{{ route('admin.pages.create') }}" class="btn btn-success mb-3">Tạo trang mới</a>
    <table class="table table-bordered">
      <thead>
        <tr>
          <th>ID</th>
          <th>Tiêu đề</th>
          <th>Slug</th>
          <th>Thao tác</th>
        </tr>
      </thead>
      <tbody>
        @foreach($pages as $page)
        <tr>
          <td>{{ $page->id }}</td>
          <td>{{ $page->title }}</td>
          <td>{{ $page->slug }}</td>
          <td>
            <a href="{{ route('admin.pages.edit', $page) }}" class="btn btn-sm btn-primary">Sửa</a>
            <form action="{{ route('admin.pages.destroy', $page) }}" method="POST" class="d-inline" onsubmit="return confirm('Xoá trang này?')">
              @csrf
              @method('DELETE')
              <button class="btn btn-sm btn-danger">Xoá</button>
            </form>
          </td>
        </tr>
        @endforeach
      </tbody>
    </table>
  </div>
</div>
@endsection
@section('scripts')
@endsection

app\Models\Page.php

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Spatie\Translatable\HasTranslations;
class Page extends Model
{
  use HasTranslations;
  public $translatable = ['title', 'content'];
  protected $casts = [
    'fields' => 'array',
  ];
  protected $fillable = [
    'title',
    'slug',
    'image',
    'content',
    'fields'
  ];
}

app\Http\Controllers\Admin\PageController.php

<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Page;
use Illuminate\Http\Request;
class PageController extends Controller
{
  /**
   * Display a listing of the resource.
   */
  public function index()
  {
    $pages = Page::latest()->get();
    return view('admin.pages.index', compact('pages'));
  }
  /**
   * Show the form for creating a new resource.
   */
  public function create()
  {
    return view('admin.pages.create');
  }
  /**
   * Store a newly created resource in storage.
   */
  public function store(Request $request)
  {
    $request->validate([
      'title' => 'required|string|max:255',
      'slug' => 'required|string|max:255|unique:pages,slug',
    ]);
    Page::create([
      'title' => $request->title,
      'slug' => $request->slug,
      'image' => $request->image,
      'content' => [
        'vi' => $request->content_vi,
        'en' => $request->content_en,
      ],
      'fields' => is_array($request->fields) ? json_encode($request->fields) : $request->fields,
    ]);
    return redirect()->route('admin.pages.index')->with('success', 'Trang đã được tạo.');
  }
  /**
   * Display the specified resource.
   */
  public function show(Page $page)
  {
    return view('admin.pages.show', compact('page'));
  }
  /**
   * Show the form for editing the specified resource.
   */
  public function edit(Page $page)
  {
    return view('admin.pages.edit', compact('page'));
  }
  /**
   * Update the specified resource in storage.
   */
  public function update(Request $request, Page $page)
  {
    $request->validate([
      'title' => 'required|string|max:255',
      'slug' => 'required|string|max:255|unique:pages,slug,' . $page->id,
    ]);
    $page->update([
      'title' => $request->title,
      'slug' => $request->slug,
      'image' =>  $request->image,
      'content' => [
        'vi' => $request->content_vi,
        'en' => $request->content_en,
      ],
      'fields' => $request->fields ?? [],
    ]);
    return redirect()->route('admin.pages.index')->with('success', 'Cập nhật trang thành công.');
  }
  /**
   * Remove the specified resource from storage.
   */
  public function destroy(Page $page)
  {
    $page->delete();
    return redirect()->route('admin.pages.index')->with('success', 'Đã xoá trang.');
  }
}

database\migrations\2025_06_02_025221_create_pages_table.php

<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
  /**
   * Run the migrations.
   */
  public function up(): void
  {
    Schema::create('pages', function (Blueprint $table) {
      $table->id();
      $table->string('title');
      $table->string('slug')->unique();
      $table->string('image')->nullable();
      $table->string('featured_image')->nullable();
      $table->longText('content')->nullable();
      $table->json('meta')->nullable(); // SEO: meta_title, meta_description, etc.
      $table->boolean('is_active')->default(true);
      $table->json('fields')->nullable();
      $table->timestamps();
    });
  }
  /**
   * Reverse the migrations.
   */
  public function down(): void
  {
    Schema::dropIfExists('pages');
  }
};

✅ 1. Cập nhật middleware và đăng ký permission check

Trong App\Http\Controllers\Admin\PageController, thêm middleware ở __construct:

public function __construct()
{
    $this->middleware('permission:view page')->only(['index', 'show']);
    $this->middleware('permission:create page')->only(['create', 'store']);
    $this->middleware('permission:edit page')->only(['edit', 'update']);
    $this->middleware('permission:delete page')->only(['destroy']);
}

✅ 2. Cập nhật Route

Bạn có thể nhóm route trong routes/web.php hoặc admin.php:

routes\web.php

<?php
use App\Http\Controllers\Admin\PageController;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
use UniSharp\LaravelFilemanager\Lfm;
Auth::routes();
Route::get('/', function () {
  return view('welcome');
});
// Route::prefix('admin')->name('admin.')->middleware(['auth'])->group(function () {
//   Route::resource('pages', PageController::class);
// });
Route::middleware(['auth', 'permission:view page'])->prefix('admin')->name('admin.')->group(function () {
  Route::resource('pages', PageController::class);
});
// LaravelFilemanager
Route::group(['prefix' => 'laravel-filemanager', 'middleware' => ['web', 'auth']], function () {
  Lfm::routes();
});

✅ 3. Cập nhật giao diện Blade (ẩn nút theo quyền)

Ví dụ trong resources/views/admin/pages/index.blade.php:

resources\views\admin\pages\index.blade.php

@can('create page')
    <a href="{{ route('admin.pages.create') }}" class="btn btn-success mb-3">Tạo trang mới</a>
@endcan

✅ 4. Gợi ý: Thêm Policy (nếu muốn granular hơn)

Nếu bạn dùng cả policy (để kiểm soát theo từng bản ghi), bạn có thể:

php artisan make:policy PagePolicy --model=Page

rong PagePolicy.php:

public function update(User $user, Page $page)
{
    return $user->can('edit page');
}

Và trong controller:

$this->authorize('update', $page);

app\Http\Controllers\Admin\PageController.php

public function update(Request $request, Page $page)
  {
    $this->authorize('update', $page);
    $request->validate([
      'title' => 'required|string|max:255',
      'slug' => 'required|string|max:255|unique:pages,slug,' . $page->id,
    ]);
    $page->update([
      'title' => $request->title,
      'slug' => $request->slug,
      'image' =>  $request->image,
      'content' => [
        'vi' => $request->content_vi,
        'en' => $request->content_en,
      ],
      'fields' => $request->fields ?? [],
    ]);
    return redirect()->route('admin.pages.index')->with('success', 'Cập nhật trang thành công.');
  }

✅ 5. Đảm bảo bạn có quyền trong DB

Chạy Seeder hoặc tạo RolePermission thủ công:

bashCopyEditphp artisan db:seed --class=RolePermissionSeeder

Seeder bạn đã có ở phần trước.

Last updated

Was this helpful?