2025年12月28日 星期日

利用 laravel 12 做一個類似Google Drive 的作品-巢狀資料夾

零、目標:
1.專案目標:
   A.環境準備
   B.建立 Laravel 專案 + 登入系統
   C.建立資料庫(資料夾 / 檔案)
   D.資料夾 CRUD
   E.檔案上傳 / 下載
   F.檔案列表(像 Google Drive)
   G.分享連結
   H.權限與安全
    I.回收桶(Soft Delete)
2.目前目標:
   D.資料夾CRUD
       a.點擊資料夾進入
       b.在資料夾內新增子資料夾
       c.無限層資料夾
       d.基本路徑導覽

一、新增 Show 路由
    1.編輯 routes/web.php
Route::get('/folders/{folder}',[FolderController::class,'show'])->name('folders.show');
     如下圖:
    
二、FolderController 加入 show 方法
    1.編輯 app/Http/Controllers/FolderController.php
    public function show(Folder $folder){
        // 安全檢查:只能看自己的資料夾
        abort_if($folder->user_id !== Auth::id(), 403);
        $subFolders = Folder::where('parent_id', $folder->id)->get();
        return view('folders.show', compact('folder', 'subFolders'));
    }
    如下圖:

三、修改資料夾列表(可點擊)
    1.編輯 resources/view/folders/index.blade.php
@foreach($folders as $folder)
    <li class="border p-3 rounded hover:bg-gray-100">
        📁
        <a href="{{ route('folders.show', $folder) }}"
           class="text-blue-600 font-semibold">
           {{ $folder->name }}
        </a>
     </li>
@endforeach
    如下圖:

四、建立資料夾內容頁
    1.指令:cd resources/views/folders
                  type nul > show.blade.php
    2.編輯 resources/view/folders/show.blade.php
<x-app-layout>
    <div class="p-6 max-w-4xl mx-auto">

        {{-- breadcrumb --}}
        <div class="mb-4 text-sm text-gray-500">
            <a href="{{ route('folders.index') }}" class="hover:underline">
                我的雲端硬碟
            </a>
            /
            {{ $folder->name }}
        </div>

        <h1 class="text-xl font-bold mb-4">📁 {{ $folder->name }}</h1>

        {{-- 新增子資料夾 --}}
        <form method="POST" action="{{ route('folders.store') }}" class="mb-4">
            @csrf
            <input type="hidden" name="parent_id" value="{{ $folder->id }}">
            <input type="text" name="name" placeholder="子資料夾名稱"
                   class="border rounded p-2 mr-2">
            <button class="bg-blue-500 text-white px-4 py-2 rounded">
                新增
            </button>
        </form>

        {{-- 子資料夾列表 --}}
        <ul class="space-y-2">
            @forelse($subFolders as $sub)
                <li class="border p-3 rounded hover:bg-gray-100">
                    📁
                    <a href="{{ route('folders.show', $sub) }}"
                       class="text-blue-600 font-semibold">
                        {{ $sub->name }}
                    </a>
                </li>
            @empty
                <li class="text-gray-400">此資料夾尚無子資料夾</li>
            @endforelse
        </ul>

    </div>
</x-app-layout>

    如下圖:
五、測試流程
    1.開 /folders
    2.建立資料夾01
    3.點進01
    4.建立資料夾02
    5.點進02
    如果可以一直點進去,巢狀成功

六、問題說明01:
    想在 顯示資料夾頁面時,呈現完整的 breadcrumb(麵包屑),像這樣:
    我的雲端硬碟 / 父資料夾名稱 / 子資料夾名稱
    目前程式碼只有顯示:
    <a href="{{ route('folders.index') }}">我的雲端硬碟</a> / {{ $folder->name }}
    也就是只顯示 雲端硬碟 → 當前資料夾,沒有祖先資料夾的層級。
    調整方式如下:
    1.在控制器準備 breadcrumb 資料
    編輯 FolderController@show
        // 建立 breadcrumb
        $breadcrumbs = collect();
        $current = $folder;
        while($current) {
            $breadcrumbs->prepend($current);
            $current = $current->parent; // parent 關聯
        }
    如下圖

    2.在 Blade 顯示完整 breadcrumb
    編輯 show.blade.php
@foreach ($breadcrumbs as $crumb)
/ <a href="{{ route('folders.show', $crumb) }}" class="hover:underline">{{ $crumb->name }}</a>
@endforeach
    如下圖
    3.最後成果
    另外,為了一致性,在根目錄頁加上 breadcrumb(一致性)
    4.根目錄頁加上 breadcrumb(一致性)
    編輯resources/folders/index.blade.php
        {{-- breadcrumb --}}
        <div class="mb-4 text-sm text-gray-500">
            我的雲端硬碟
        </div>
    如下圖:

    網頁就會改變如下兩圖:
    
七、問題說明02:
    想要在目前架構下,新增兩種功能,分別是1.資料夾重新命名、2.資料夾刪除。
    1.資料夾重新命名
    2.資料夾刪除(含所有子資料夾,利用 cascade)
    調整方式如下:
    (一).新增兩條路由:
    編輯routes/web.php
Route::patch('/folders/{folder}', [FolderController::class, 'update'])->name('folders.update');
Route::delete('/folders/{folder}', [FolderController::class, 'destroy'])->name('folders.destroy');
    如下圖:
    (二).Controller:新增update+destory:
    編輯App/Http/Controllers/FolderController.php
    public function update(Request $request, Folder $folder){
        // 安全檢查:只能看自己的資料夾
        abort_if($folder->user_id !== Auth::id(), 403);
        $request->validate([
            'name' => 'required|string|max:255',
        ]);
        $folder->update([
            'name' => $request->name,
        ]);
        return back();
    }
    與
    public function destroy(Folder $folder){
        // 安全檢查:只能看自己的資料夾
        abort_if($folder->user_id !== Auth::id(), 403);
        // 你的 migration 已經設定 cascadeOnDelete
        // 子資料夾會自動刪除
        $folder->delete();
        return redirect()->route('folders.index');
    } 
    如下圖:
    (三).Blade:加在[子資料夾列表]中
    編輯 resiucrces/folders/show.blade.php
<li class="border p-3 rounded hover:bg-gray-100 flex justify-between items-center">
   <div>
     📁
       <a href="{{ route('folders.show', $sub) }}"
          class="text-blue-600 font-semibold">
          {{ $sub->name }}
       </a>
   </div>
   <div class="flex gap-2">
       {{-- 重新命名 --}}
       <form method="POST" action="{{ route('folders.update', $sub) }}">
         @csrf
         @method('PATCH')
         <input type="text" name="name"value="{{ $sub->name }}"
           class="border rounded p-1 text-sm w-32">
         <button class="text-green-600 text-sm">儲存</button>
       </form>
       {{-- 刪除 --}}
       <form method="POST" action="{{ route('folders.destroy', $sub) }}"
         onsubmit="return confirm('確定要刪除這個資料夾及其所有子資料夾嗎?')">
          @csrf
          @method('DELETE')
          <button class="text-red-600 text-sm">刪除</button>
       </form>
    </div>
</li>
    如下圖:
    修改完後的成果如下:
    與
    
八、問題說明03:
    在解決七、問題02的同時,發現index.blade.php 並沒有支援。所以編輯 resources/folders/index.blade.php,使其擁有重新命名與刪除的功能。
    (一),編輯 resources/folders/index.blade.php 如下:
        {{-- 新增根資料夾 --}}
        <form method="POST" action="{{ route('folders.store') }}" class="mb-4">
            @csrf
            <input type="text" name="name" placeholder="新資料夾名稱"
                   class="border rounded p-2 mr-2">
            <button class="bg-blue-500 text-white px-4 py-2 rounded">
                新增
            </button>
        </form>

        {{-- 資料夾列表 --}}
        <ul class="space-y-2">
        @foreach($folders as $folder)
            <li class="border p-3 rounded hover:bg-gray-100
                flex justify-between items-center">
            {{-- 資料夾名稱 --}}
            <div>
            📁
                <a href="{{ route('folders.show', $folder) }}"
                    class="text-blue-600 font-semibold">
                    {{ $folder->name }}
                </a>
            </div>
            {{-- 操作 --}}
            <div class="flex gap-3 items-center">
                {{-- 重新命名 --}}
                <form method="POST"
                    action="{{ route('folders.update', $folder) }}"
                    class="flex items-center gap-1">
                 @csrf
                 @method('PATCH')
                 <input type="text" name="name"
                   value="{{ $folder->name }}"
                   class="border rounded p-1 text-sm w-32">
                   <button class="text-green-600 text-sm">
                    儲存
                   </button>
                </form>
                {{-- 刪除 --}}
                <form method="POST"
                    action="{{ route('folders.destroy', $folder) }}"
                    onsubmit="return confirm('確定要刪除這個資料夾及其所有子資料夾嗎?')">
                  @csrf
                  @method('DELETE')
                  <button class="text-red-600 text-sm">
                    刪除
                  </button>
                </form>
            </div>
            </li>
        @endforeach
        </ul>
    如下圖:
    修改完後的成果如下:
    與
九、問題說明04:
    在解決八、問題說明03時,希望做到[點擊]才編輯資料夾名稱。所以再次編輯 resources/folders/index.blade.php 與  resources/folders/show.blade.php。
    (一)編輯resources/folders/index.blade.php
        <li class="border p-3 rounded hover:bg-gray-100
           flex justify-between items-center"
            x-data="{ editing: false, name: '{{ $folder->name }}' }">

        {{-- 左側:資料夾名稱 --}}
            <div class="flex items-center gap-2">
            📁
            {{-- 顯示模式 --}}
            <template x-if="!editing">
                <span
                    @dblclick="editing = true"
                    class="font-semibold text-blue-600 cursor-pointer">
                    {{ $folder->name }}
                </span>
            </template>

            {{-- 編輯模式 --}}
            <template x-if="editing">
                <form method="POST"
                    action="{{ route('folders.update', $folder) }}"
                    class="flex items-center gap-1">
                     @csrf
                     @method('PATCH')
                    <input type="text"
                       name="name"
                       x-model="name"
                       @keydown.enter.prevent="$el.form.submit()"
                       @keydown.escape="editing = false"
                       class="border rounded p-1 text-sm w-32"
                       autofocus>

                    <button class="text-green-600 text-sm">
                        儲存
                    </button>

                    <button type="button"
                        @click="editing = false"
                        class="text-gray-500 text-sm">
                        取消
                    </button>
                </form>
            </template>
            </div>

            {{-- 右側操作 --}}
            <div class="flex gap-2 items-center">
            {{-- 開啟 --}}
            <a href="{{ route('folders.show', $folder) }}"
                class="text-sm text-blue-500">
                 開啟
            </a>
            {{-- 重新命名 --}}
            <button @click="editing = true"
            class="text-sm text-gray-600">
                重新命名
            </button>
            {{-- 刪除 --}}
            <form method="POST"
              action="{{ route('folders.destroy', $folder) }}"
              onsubmit="return confirm('確定要刪除這個資料夾及其所有子資料夾嗎?')">
                @csrf
                @method('DELETE')
                <button class="text-sm text-red-600">
                刪除
                </button>
            </form>
        </div>
        </li>
    如下圖:
    (二)編輯resources/folders/show.blade.php
            <li class="border p-3 rounded hover:bg-gray-100
                flex justify-between items-center"
                x-data="{ editing: false, name: '{{ $sub->name }}' }">

                {{-- 左側:資料夾名稱 --}}
                <div class="flex items-center gap-2">
                 📁
                    {{-- 顯示模式 --}}
                    <template x-if="!editing">
                        <span @dblclick="editing = true"
                            class="font-semibold text-blue-600 cursor-pointer">
                            {{ $sub->name }}
                        </span>
                    </template>
                    {{-- 編輯模式 --}}
                    <template x-if="editing">
                        <form method="POST"
                            action="{{ route('folders.update', $sub) }}"
                            class="flex items-center gap-1">
                            @csrf
                            @method('PATCH')
                            <input type="text"
                                name="name"
                                x-model="name"
                                @keydown.enter.prevent="$el.form.submit()"
                                @keydown.escape="editing = false"
                                class="border rounded p-1 text-sm w-32"
                                autofocus>
                            <button class="text-green-600 text-sm">
                            儲存
                            </button>

                            <button type="button"
                                @click="editing = false"
                                class="text-gray-500 text-sm">
                            取消
                            </button>
                        </form>
                    </template>
                </div>

                    {{-- 右側操作 --}}
                    <div class="flex gap-2 items-center">

                        {{-- 開啟 --}}
                        <a href="{{ route('folders.show', $sub) }}"
                            class="text-sm text-blue-500">
                            開啟
                        </a>
                        {{-- 重新命名 --}}
                        <button @click="editing = true"
                            class="text-sm text-gray-600">
                        重新命名
                        </button>

                        {{-- 刪除 --}}
                        <form method="POST"
                            action="{{ route('folders.destroy', $sub) }}"
                            onsubmit="return confirm('確定要刪除這個資料夾及其所有子資料夾嗎?')">
                            @csrf
                            @method('DELETE')
                            <button class="text-sm text-red-600">
                            刪除
                            </button>
                        </form>
                    </div>
            </li>
            @empty
            <li class="text-gray-400">此資料夾尚無子資料夾</li>
    如下圖:
    修改完後的成果如下:
    與
十、問題說明05:
    在解決九、問題說明04時,發現刪除資料夾後,會回到根資料夾。因此,刪除資料夾後,希望能回到[父資料夾]。
    (一)編輯App/Http/Controllers/FolderController.php
    public function destroy(Folder $folder){
        // 安全檢查:只能看自己的資料夾
        abort_if($folder->user_id !== Auth::id(), 403);
        // 🔑 刪除前先記住 parent
        $parentId = $folder->parent_id;
        // 你的 migration 已經設定 cascadeOnDelete
        // 子資料夾會自動刪除
        $folder->delete();
        // 有 parent → 回到父資料夾
        if ($parentId) {
            return redirect()->route('folders.show', $parentId);
        }
        // 沒 parent → 回到根目錄
        return redirect()->route('folders.index');
    }    
    如下圖:
    (二)刪除成功提示:
    1.編輯App/Http/Controllers/FolderController.php
->with('success', '資料夾已刪除');
    如下圖:
    2.編輯resources/folders/index.blade.php
    @if (session('success'))
    <div class="mb-4 text-green-600">
        {{ session('success') }}
    </div>
    @endif
    如下圖:
    3.編輯resources/folders/show.blade.php
    @if (session('success'))
    <div class="mb-4 text-green-600">
        {{ session('success') }}
    </div>
    @endif
    如下圖:







沒有留言:

張貼留言

在本機Laravel 12 中實作 FaceBook OAuth2 登入

系列文章: 1. 利用 laravel 12 做一個類似Google文件的簡單範例 2. 在本機Laravel 12 中實作 Google OAuth2 登入 3. 如何在 Laravel 12 使用 Gmail SMTP 寄信 4. 在本機Laravel 12 中實作 Face...