系列文章:
以下是一個簡單的 Laravel 範例,實現類似 Google Docs 的基礎功能,包括用戶認證、文檔創建、編輯、和即時更新(使用 Pusher 和 Quill 編輯器)。
一、安裝 Laravel 和所需的套件
a.建立laravel 12 專案
指令:composer create-project --prefer-dist laravel/laravel google-docs-clone
cd google-docs-clone
b.安裝認證包( Laravel Breeze)
指令:composer require laravel/breeze --dev
php artisan breeze:install
npm install
npm run dev
php artisan migrate
c.安裝 Pusher 和 Laravel Echo
指令:composer require pusher/pusher-php-server
npm install --save laravel-echo pusher-js
二、編輯
.env 中設定 Pusher BROADCAST_DRIVER=pusher
PUSHER_APP_ID=your-app-id
PUSHER_APP_KEY=your-app-key
PUSHER_APP_SECRET=your-app-secret
PUSHER_APP_CLUSTER=your-app-cluster
註冊一個 Pusher 帳戶來獲取這些值:https://pusher.com/,如下圖
a.指令:php artisan make:model Document -m
b.編輯 database/migrations/xxxx_xx_xx_create_documents_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('documents', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('title');
$table->text('content'); // 存儲文檔內容
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('documents');
}
};
c.指令:php artisan migrate
四、建立 Controller 和路由
a.指令:php artisan make:controller DocumentController
b.編輯 DocumentController.php,直接套用即可。
<?php
namespace App\Http\Controllers;
use App\Models\Document;
use Illuminate\Http\Request;
class DocumentController extends Controller
{
// 顯示所有文檔
public function index()
{
$documents = Document::where('user_id', auth()->id())->get(); // 根據用戶顯示文檔
return view('documents.index', compact('documents'));
}
// 顯示創建文檔頁面
public function create()
{
return view('documents.create');
}
// 儲存新文檔
public function store(Request $request)
{
$request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
]);
$document = new Document();
$document->title = $request->title;
$document->content = $request->content;
$document->user_id = auth()->id();
$document->save();
return redirect()->route('documents.index')->with('success', 'Document created successfully!');
}
// 顯示編輯文檔頁面
public function edit($id)
{
$document = Document::findOrFail($id);
return view('documents.edit', compact('document'));
}
// 更新文檔
public function update(Request $request, $id)
{
$request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
]);
$document = Document::findOrFail($id);
$document->title = $request->title;
$document->content = $request->content;
$document->save();
return redirect()->route('documents.index')->with('success', 'Document updated successfully!');
}
}
c.編輯 routes/web.php,直接套用即可。
<?php
use App\Http\Controllers\DocumentController;
use App\Http\Controllers\ProfileController;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
//Route::get('/dashboard', function () {
// return view('dashboard');
//})->middleware(['auth', 'verified'])->name('dashboard');
Route::get('/dashboard', function () {
return view('dashboard');
})->middleware(['auth'])->name('dashboard');
Route::middleware('auth')->group(function () {
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
// 顯示所有文檔
Route::get('documents', [DocumentController::class, 'index'])->name('documents.index');
// 創建新文檔
Route::get('documents/create', [DocumentController::class, 'create'])->name('documents.create');
Route::post('documents', [DocumentController::class, 'store'])->name('documents.store');
// 編輯文檔
Route::get('documents/{id}/edit', [DocumentController::class, 'edit'])->name('documents.edit');
Route::post('documents/{id}/update', [DocumentController::class, 'update'])->name('documents.update');
});
require __DIR__.'/auth.php';
五、建立建
DocumentUpdated 事件 a.指令:php artisan make:event DocumentUpdated
b.編輯 app/Events/DocumentUpdated.php ,直接套用即可。
<?php
namespace App\Events;
use App\Models\Document;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class DocumentUpdated implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $document;
public function __construct(Document $document)
{
$this->document = $document;
}
public function broadcastOn()
{
return new Channel('document.' . $this->document->id);
}
public function broadcastWith()
{
return ['content' => $this->document->content];
}
}
六、建立 Quill 編輯器前端頁面
a.編輯 resources/views/documents/create.blade.php,直接套用即可。
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Create Document') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
<!-- Form to create a new document -->
<form action="{{ url('documents') }}" method="POST">
@csrf
<div class="form-group">
<label for="title">Title</label>
<input type="text" name="title" class="form-control" required>
</div>
<div id="editor"></div>
<textarea name="content" id="content" style="display:none;"></textarea>
<button type="submit" class="btn btn-primary mt-2">Create Document</button>
</form>
</div>
</div>
</div>
</div>
<!-- Quill.js -->
<script src="https://cdn.quilljs.com/1.3.6/quill.js"></script>
<!-- Quill.css -->
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
<script>
// Initialize Quill editor
var quill = new Quill('#editor', {
theme: 'snow', // Use the 'snow' theme
placeholder: 'Start writing your document...',
modules: {
toolbar: [
[{ 'header': '1' }, { 'header': '2' }, { 'font': [] }],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['bold', 'italic', 'underline'],
[{ 'align': [] }],
['link'],
['image']
]
}
});
// Update the hidden textarea with the editor content when it's changed
quill.on('text-change', function () {
document.getElementById('content').value = quill.root.innerHTML;
});
</script>
</x-app-layout>
b.編輯 resources/views/documents/edit.blade.php,直接套用即可。
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Edit Document') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
<form action="{{ url('documents/' . $document->id . '/update') }}" method="POST">
@csrf
@method('POST')
<div class="form-group">
<label for="title">Title</label>
<input type="text" name="title" class="form-control" value="{{ $document->title }}" required>
</div>
<div id="editor">{!! $document->content !!}</div>
<textarea name="content" id="content" style="display:none;">{{ $document->content }}</textarea>
<button type="submit" class="btn btn-primary mt-2">Update Document</button>
</form>
</div>
</div>
</div>
</div>
<!-- Quill.js -->
<script src="https://cdn.quilljs.com/1.3.6/quill.js"></script>
<!-- Quill.css -->
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
<script>
var quill = new Quill('#editor', {
theme: 'snow',
placeholder: 'Start editing your document...',
modules: {
toolbar: [
[{ 'header': '1' }, { 'header': '2' }, { 'font': [] }],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['bold', 'italic', 'underline'],
[{ 'align': [] }],
['link'],
['image']
]
}
});
// Update the hidden textarea with the editor content when it's changed
quill.on('text-change', function () {
document.getElementById('content').value = quill.root.innerHTML;
});
</script>
</x-app-layout>
c.編輯 resources/views/documents/index.blade.php,直接套用即可。
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Your Documents') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
<h3 class="text-lg font-medium">Here are your documents</h3>
<!-- Document List -->
<ul class="space-y-4 mt-4">
@foreach($documents as $document)
<li class="bg-gray-100 p-4 rounded-md shadow-md">
<h4 class="font-medium text-gray-800">{{ $document->title }}</h4>
<p class="text-sm text-gray-600">{{ $document->created_at->format('M d, Y') }}</p>
<a href="{{ route('documents.edit', $document->id) }}" class="text-blue-500 hover:text-blue-700">Edit</a>
</li>
@endforeach
</ul>
<!-- If no documents -->
@if($documents->isEmpty())
<p class="mt-4 text-gray-600">You have no documents yet.</p>
@endif
</div>
</div>
</div>
</div>
</x-app-layout>
d.編輯 resources/views/dashboard.blade.php,直接套用即可。
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Dashboard') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
<h3 class="text-lg font-medium">Welcome, {{ Auth::user()->name }}!</h3>
<p class="mt-2 text-gray-600">You're logged in successfully. Here’s your dashboard.</p>
<!-- Link to documents -->
<div class="mt-6">
<a href="{{ route('documents.index') }}" class="inline-block bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600">
View Documents
</a>
</div>
<!-- Create new document -->
<div class="mt-6">
<a href="{{ route('documents.create') }}" class="inline-block bg-green-500 text-white px-4 py-2 rounded-md hover:bg-green-600">
Create New Document
</a>
</div>
</div>
</div>
</div>
</div>
</x-app-layout>
e.編輯 resources/views/layouts/navigation.blade.php,直接套用即可。
<nav x-data="{ open: false }" class="bg-white border-b border-gray-100">
<!-- Primary Navigation Menu -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex">
<!-- Logo -->
<div class="shrink-0 flex items-center">
<a href="{{ route('dashboard') }}">
<x-application-logo class="block h-9 w-auto fill-current text-gray-800" />
</a>
</div>
<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
{{ __('Dashboard') }}
</x-nav-link>
</div>
</div>
<!-- Settings Dropdown -->
<div class="hidden sm:flex sm:items-center sm:ms-6">
<x-dropdown align="right" width="48">
<x-slot name="trigger">
<button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150">
<div>{{ Auth::user()->name }}</div>
<div class="ms-1">
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</div>
</button>
</x-slot>
<x-slot name="content">
<x-dropdown-link :href="route('profile.edit')">
{{ __('Profile') }}
</x-dropdown-link>
<!-- Authentication -->
<form method="POST" action="{{ route('logout') }}">
@csrf
<x-dropdown-link :href="route('logout')"
onclick="event.preventDefault();
this.closest('form').submit();">
{{ __('Log Out') }}
</x-dropdown-link>
</form>
</x-slot>
</x-dropdown>
</div>
<!-- Hamburger -->
<div class="-me-2 flex items-center sm:hidden">
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out">
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
<path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>
<!-- Responsive Navigation Menu -->
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
<div class="pt-2 pb-3 space-y-1">
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
{{ __('Dashboard') }}
</x-responsive-nav-link>
</div>
<!-- Responsive Settings Options -->
<div class="pt-4 pb-1 border-t border-gray-200">
<div class="px-4">
<div class="font-medium text-base text-gray-800">{{ Auth::user()->name }}</div>
<div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div>
</div>
<div class="mt-3 space-y-1">
<x-responsive-nav-link :href="route('profile.edit')">
{{ __('Profile') }}
</x-responsive-nav-link>
<!-- Authentication -->
<form method="POST" action="{{ route('logout') }}">
@csrf
<x-responsive-nav-link :href="route('logout')"
onclick="event.preventDefault();
this.closest('form').submit();">
{{ __('Log Out') }}
</x-responsive-nav-link>
</form>
</div>
</div>
</div>
</nav>
七、監聽文檔更新
a.編輯 resources/js/app.js,直接套用即可。
import './bootstrap';
import Alpine from 'alpinejs';
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Alpine = Alpine;
Alpine.start();
window.Pusher = Pusher;
const echo = new Echo({
broadcaster: 'pusher',
key: 'your-pusher-app-key',
cluster: 'your-pusher-app-cluster',
forceTLS: true
});
const documentId = window.documentId; // 文檔ID
echo.channel('document.' + documentId)
.listen('DocumentUpdated', (event) => {
quill.root.innerHTML = event.content;
});
八、結果畫面:
a. 修改
edit.blade.php,直接套用即可。<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Edit Document') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
<form action="{{ url('documents/' . $document->id . '/update') }}" method="POST">
@csrf
@method('POST')
<div class="form-group">
<label for="title">Title</label>
<input type="text" name="title" class="form-control" value="{{ $document->title }}" required>
</div>
<div id="editor">{!! $document->content !!}</div>
<textarea name="content" id="content" style="display:none;">{{ $document->content }}</textarea>
<button type="submit" class="btn btn-primary mt-2">Update Document</button>
</form>
<!-- Delete Document Button -->
<form action="{{ route('documents.destroy', $document->id) }}" method="POST" class="mt-4">
@csrf
@method('DELETE') <!-- This is required for delete requests -->
<button type="submit" class="bg-red-500 text-white px-4 py-2 rounded-md hover:bg-red-600">
Delete Document
</button>
</form>
</div>
</div>
</div>
</div>
<!-- Quill.js -->
<script src="https://cdn.quilljs.com/1.3.6/quill.js"></script>
<!-- Quill.css -->
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
<script>
var quill = new Quill('#editor', {
theme: 'snow',
placeholder: 'Start editing your document...',
modules: {
toolbar: [
[{ 'header': '1' }, { 'header': '2' }, { 'font': [] }],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['bold', 'italic', 'underline'],
[{ 'align': [] }],
['link'],
['image']
]
}
});
// Update the hidden textarea with the editor content when it's changed
quill.on('text-change', function () {
document.getElementById('content').value = quill.root.innerHTML;
});
</script>
</x-app-layout>
b. 更新
web.php 路由,直接套用即可。<?php
use App\Http\Controllers\DocumentController;
use App\Http\Controllers\ProfileController;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
//Route::get('/dashboard', function () {
// return view('dashboard');
//})->middleware(['auth', 'verified'])->name('dashboard');
Route::get('/dashboard', function () {
return view('dashboard');
})->middleware(['auth'])->name('dashboard');
Route::middleware('auth')->group(function () {
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
// 顯示所有文檔
Route::get('documents', [DocumentController::class, 'index'])->name('documents.index');
// 創建新文檔
Route::get('documents/create', [DocumentController::class, 'create'])->name('documents.create');
Route::post('documents', [DocumentController::class, 'store'])->name('documents.store');
// 編輯文檔
Route::get('documents/{id}/edit', [DocumentController::class, 'edit'])->name('documents.edit');
Route::post('documents/{id}/update', [DocumentController::class, 'update'])->name('documents.update');
// Delete Route
Route::delete('documents/{id}', [DocumentController::class, 'destroy'])->name('documents.destroy');
});
require __DIR__.'/auth.php';
c.更新
DocumentController,直接套用即可。<?php
namespace App\Http\Controllers;
use App\Models\Document;
use Illuminate\Http\Request;
class DocumentController extends Controller
{
// 顯示所有文檔
public function index()
{
$documents = Document::where('user_id', auth()->id())->get(); // 根據用戶顯示文檔
return view('documents.index', compact('documents'));
}
// 顯示創建文檔頁面
public function create()
{
return view('documents.create');
}
// 儲存新文檔
public function store(Request $request)
{
$request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
]);
$document = new Document();
$document->title = $request->title;
$document->content = $request->content;
$document->user_id = auth()->id();
$document->save();
return redirect()->route('documents.index')->with('success', 'Document created successfully!');
}
// 顯示編輯文檔頁面
public function edit($id)
{
$document = Document::findOrFail($id);
return view('documents.edit', compact('document'));
}
// 更新文檔
public function update(Request $request, $id)
{
$request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
]);
$document = Document::findOrFail($id);
$document->title = $request->title;
$document->content = $request->content;
$document->save();
return redirect()->route('documents.index')->with('success', 'Document updated successfully!');
}
// Delete document
public function destroy($id)
{
$document = Document::findOrFail($id);
// Delete the document
$document->delete();
// Redirect with success message
return redirect()->route('documents.index')->with('success', 'Document deleted successfully.');
}
}
d.顯示刪除後的結果,
index.blade.php 中顯示刪除成功的訊息。直接套用即可。<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Your Documents') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
<!-- Display success message -->
@if(session('success'))
<div class="bg-green-100 text-green-700 p-4 rounded-md mb-4">
{{ session('success') }}
</div>
@endif
<h3 class="text-lg font-medium">Here are your documents</h3>
<!-- Document List -->
<ul class="space-y-4 mt-4">
@foreach($documents as $document)
<li class="bg-gray-100 p-4 rounded-md shadow-md">
<h4 class="font-medium text-gray-800">{{ $document->title }}</h4>
<p class="text-sm text-gray-600">{{ $document->created_at->format('M d, Y') }}</p>
<a href="{{ route('documents.edit', $document->id) }}" class="text-blue-500 hover:text-blue-700">Edit</a>
</li>
@endforeach
</ul>
<!-- If no documents -->
@if($documents->isEmpty())
<p class="mt-4 text-gray-600">You have no documents yet.</p>
@endif
</div>
</div>
</div>
</div>
</x-app-layout>
a.在
index.blade.php 中顯示刪除連結<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Your Documents') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
<!-- Display success message -->
@if(session('success'))
<div class="bg-green-100 text-green-700 p-4 rounded-md mb-4">
{{ session('success') }}
</div>
@endif
<h3 class="text-lg font-medium">Here are your documents</h3>
<!-- Document List -->
<ul class="space-y-4 mt-4">
@foreach($documents as $document)
<li class="bg-gray-100 p-4 rounded-md shadow-md flex justify-between items-center">
<div>
<h4 class="font-medium text-gray-800">{{ $document->title }}</h4>
<p class="text-sm text-gray-600">{{ $document->created_at->format('M d, Y') }}</p>
</div>
<div>
<!-- Edit Link -->
<a href="{{ route('documents.edit', $document->id) }}" class="text-blue-500 hover:text-blue-700 mr-4">Edit</a>
<!-- Delete Link -->
<form action="{{ route('documents.destroy', $document->id) }}" method="POST" style="display:inline;">
@csrf
@method('DELETE') <!-- This method is needed to send DELETE request -->
<button type="submit" class="text-red-500 hover:text-red-700">
Delete
</button>
</form>
</div>
</li>
@endforeach
</ul>
<!-- If no documents -->
@if($documents->isEmpty())
<p class="mt-4 text-gray-600">You have no documents yet.</p>
@endif
</div>
</div>
</div>
</div>
</x-app-layout>
十一、結果畫面:






沒有留言:
張貼留言