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
: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 Role
và Permission
thủ công:
bashCopyEditphp artisan db:seed --class=RolePermissionSeeder
Seeder bạn đã có ở phần trước.
PreviousĐể phân quyền CRUD cho các trang (pages) trong Laravel sử dụng package spatie/laravel-permissionNextTuyệt, để xây dựng giao diện quản lý người dùng và phân quyền với spatie/laravel-permission
Last updated
Was this helpful?