2025年12月28日 星期日

利用 laravel 12 做一個類似Google Drive 的作品-檔案上傳下載


零、目標:
1.專案目標:
   A.環境準備
   B.建立 Laravel 專案 + 登入系統
   C.建立資料庫(資料夾 / 檔案)
   D.資料夾 CRUD
   E.檔案上傳 / 下載
   F.檔案列表(像 Google Drive)
   G.分享連結
   H.權限與安全
    I.回收桶(Soft Delete)
2.目前目標:
   E.檔案上傳 / 下載+F.檔案列表(像 Google Drive)
       a.上傳檔案到指定資料夾
       b.在資料夾中看到[資料夾+檔案]混合列表
       c.下載檔案
       d.檔案真實存到 storage

一、建立 File Model + Migration
    1.指令:php artisan make:model File -m
    2.編輯 database/migrations/xxxx_create_files_table.php
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('folder_id')->nullable()
  ->constrained('folders')->cascadeOnDelete();
$table->string('name');          // 原始檔名
$table->string('path');          // 儲存路徑
$table->unsignedBigInteger('size');
    如下圖:
    3.指令:php artisan migrate

二、File Model
    1.編輯 app/models/File.php
use App\Models\Folder;
    與
    protected $fillable = [
        'user_id',
        'folder_id',
        'name',
        'path',
        'size',
    ];
    public function folder(){
        return $this->belongsTo(Folder::class);
    }
    如下圖:

三、建立 FileController
    1.指令:php artisan make:controller FileController
    2.編輯 app/Http/Controllers/FileController.php
use App\Models\File;
use App\Models\Folder;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
    public function store(Request $request){
        $request->validate([
            'file' => 'required|file|max:10240', // 10MB
        ]);
        $uploaded = $request->file('file');
        $path = $uploaded->store('files', 'public');
        File::create([
            'user_id' => Auth::id(),
            'folder_id' => $request->folder_id,
            'name' => $uploaded->getClientOriginalName(),
            'path' => $path,
            'size' => $uploaded->getSize(),
        ]);
        return back();
    }

    public function download(File $file){
        abort_if($file->user_id !== Auth::id(), 403);
        return Storage::disk('public')->download($file->path, $file->name);
    }
    如下圖:

四、設定路由
    1. 編輯 routes/web.php
use App\Http\Controllers\FileController;
    // File
    Route::post('/files', [FileController::class, 'store'])->name('files.store');
    Route::get('/files/{file}/download', [FileController::class, 'download'])
        ->name('files.download');

    如下圖

五、顯示檔案(修改 folders.show)
    1.編輯 FolderController@show
use App\Models\File;
    與
// File
$files = File::where('folder_id', $folder->id)->get();
return view('folders.show', compact('folder', 'subFolders','files'));
    如下圖

六、修改畫面:上傳+檔案列表
    1.編輯 resources/views/folders/show.blade.php
      {{-- 上傳表單 --}}
        <form method="POST" action="{{ route('files.store') }}" enctype="multipart/form-data" class="mb-4">
            @csrf
            <input type="hidden" name="folder_id" value="{{ $folder->id }}">
            <input type="file" name="file">
            <button class="bg-green-600 text-white px-4 py-2 rounded">
                上傳檔案
            </button>
        </form>

        {{-- 檔案列表 --}}
        <h2 class="font-semibold mb-2">檔案</h2>

        <ul class="space-y-2">
            @forelse($files as $file)
                <li class="border p-3 rounded flex justify-between">
                    📄 {{ $file->name }}
                    <a href="{{ route('files.download', $file) }}"
                    class="text-blue-600 hover:underline">
                    下載
                    </a>
                </li>
                @empty
                <li class="text-gray-400">尚無檔案</li>
            @endforelse
        </ul>
    如下圖

七、建立 storage 連結(必要)
    1.指令:php artisan storage:link

八、測試流程
    1.點擊某資料夾
    2.上傳檔案
    3.看到檔案列表
    4.點[下載]成功
九、問題01:希望能增加兩種功能:1.檔案重新命名 2.檔案刪除
    調整方式如下:
    1.調整 web.php
    // 檔案重新命名
    Route::patch('/files/{file}', [FileController::class, 'update'])->name('files.update');
    // 檔案刪除
    Route::delete('/files/{file}', [FileController::class, 'destroy'])->name('files.destroy');
    如下圖:

    2.FileController 方法
    public function update(Request $request, File $file){
    // 安全檢查
    abort_if($file->user_id !== Auth::id(), 403);
    // 驗證新名稱
    $request->validate([
        'name' => 'required|string|max:255',
    ]);
    $file->update([
        'name' => $request->name,
    ]);
    return back()->with('success', '檔案名稱已更新');
    }
    與
    public function destroy(File $file){
    // 安全檢查
    abort_if($file->user_id !== Auth::id(), 403);
    // 刪除實體檔案
    if (Storage::disk('public')->exists($file->path)) {
        Storage::disk('public')->delete($file->path);
    }
    // 刪除資料庫紀錄
    $file->delete();
    return back()->with('success', '檔案已刪除');
    }
    如下圖:

    3.編輯 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>
            @foreach ($breadcrumbs as $crumb)
                / <a href="{{ route('folders.show', $crumb) }}" class="hover:underline">{{ $crumb->name }}</a>
            @endforeach
        </div>

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

        {{-- 成功訊息 --}}
        @if(session('success'))
            <div class="mb-4 text-green-600">
                {{ session('success') }}
            </div>
        @endif

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

        {{-- 子資料夾列表 --}}
        <ul class="space-y-2 mb-6">
            @forelse($subFolders as $sub)
                <li class="border p-3 rounded hover:bg-gray-100 flex justify-between items-center"
                    x-data="{ editing: false, name: '{{ e($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 required>
                                <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>
            @endforelse
        </ul>

        {{-- 上傳檔案 --}}
        <form method="POST" action="{{ route('files.store') }}" enctype="multipart/form-data" class="mb-4 flex gap-2">
            @csrf
            <input type="hidden" name="folder_id" value="{{ $folder->id }}">
            <input type="file" name="file" required>
            <button class="bg-green-600 text-white px-4 py-2 rounded">上傳檔案</button>
        </form>

        {{-- 檔案列表 --}}
        <h2 class="font-semibold mb-2">檔案</h2>
        <ul class="space-y-2">
            @forelse($files as $file)
                <li class="border p-3 rounded flex justify-between items-center"
                    x-data="{ editing: false, name: '{{ e($file->name) }}' }">

                    {{-- 檔案名稱 --}}
                    <div class="flex items-center gap-2">
                        📄
                        <template x-if="!editing">
                            <span @dblclick="editing = true" class="cursor-pointer text-blue-600">
                                {{ $file->name }}
                            </span>
                        </template>
                        <template x-if="editing">
                            <form method="POST" action="{{ route('files.update', $file) }}" 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 required>
                                <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('files.download', $file) }}" class="text-blue-600 hover:underline">下載</a>
                        <button type="button" @click="editing = true" class="text-sm text-gray-600">重新命名</button>
                        <form method="POST" action="{{ route('files.destroy', $file) }}"
                              onsubmit="return confirm('確定要刪除這個檔案嗎?')">
                            @csrf
                            @method('DELETE')
                            <button class="text-red-600 text-sm">刪除</button>
                        </form>
                    </div>

                </li>
            @empty
                <li class="text-gray-400">尚無檔案</li>
            @endforelse
        </ul>

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

    3.呈現如下圖

十、問題02:希望能顯示 檔案大小(用 KB / MB 自動換算)以及 上傳日期(格式化顯示)
    編輯 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>
            @foreach ($breadcrumbs as $crumb)
                / <a href="{{ route('folders.show', $crumb) }}" class="hover:underline">{{ $crumb->name }}</a>
            @endforeach
        </div>

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

        {{-- 成功訊息 --}}
        @if(session('success'))
            <div class="mb-4 text-green-600">
                {{ session('success') }}
            </div>
        @endif

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

        {{-- 子資料夾列表 --}}
        <ul class="space-y-2 mb-6">
            @forelse($subFolders as $sub)
                <li class="border p-3 rounded hover:bg-gray-100 flex justify-between items-center"
                    x-data="{ editing: false, name: '{{ e($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 required>
                                <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>
            @endforelse
        </ul>

        {{-- 上傳檔案 --}}
        <form method="POST" action="{{ route('files.store') }}" enctype="multipart/form-data" class="mb-4 flex gap-2">
            @csrf
            <input type="hidden" name="folder_id" value="{{ $folder->id }}">
            <input type="file" name="file" required>
            <button class="bg-green-600 text-white px-4 py-2 rounded">上傳檔案</button>
        </form>

        {{-- 檔案列表 --}}
        <h2 class="font-semibold mb-2">檔案</h2>
        <ul class="space-y-2">
            @forelse($files as $file)
                <li class="border p-3 rounded flex justify-between items-center"
                    x-data="{ editing: false, name: '{{ e($file->name) }}' }">

                    {{-- 檔案名稱 + 大小 + 上傳日期 --}}
                    <div class="flex items-center gap-4">
                        📄
                        <template x-if="!editing">
                            <span @dblclick="editing = true" class="cursor-pointer text-blue-600">
                                {{ $file->name }}
                            </span>
                        </template>
                        <template x-if="editing">
                            <form method="POST" action="{{ route('files.update', $file) }}" 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 required>
                                <button class="text-green-600 text-sm">儲存</button>
                                <button type="button" @click="editing = false" class="text-gray-500 text-sm">取消</button>
                            </form>
                        </template>

                        {{-- 檔案大小 --}}
                        <span class="text-gray-500 text-sm">
                            @php
                                if($file->size >= 1048576){
                                    $size = number_format($file->size / 1048576, 2) . ' MB';
                                } elseif($file->size >= 1024){
                                    $size = number_format($file->size / 1024, 2) . ' KB';
                                } else {
                                    $size = $file->size . ' B';
                                }
                            @endphp
                            {{ $size }}
                        </span>

                        {{-- 上傳日期 --}}
                        <span class="text-gray-400 text-sm">{{ $file->created_at->format('Y-m-d H:i') }}</span>
                    </div>

                    {{-- 操作按鈕 --}}
                    <div class="flex gap-2 items-center">
                        <a href="{{ route('files.download', $file) }}" class="text-blue-600 hover:underline">下載</a>
                        <button type="button" @click="editing = true" class="text-sm text-gray-600">重新命名</button>
                        <form method="POST" action="{{ route('files.destroy', $file) }}"
                              onsubmit="return confirm('確定要刪除這個檔案嗎?')">
                            @csrf
                            @method('DELETE')
                            <button class="text-red-600 text-sm">刪除</button>
                        </form>
                    </div>

                </li>
            @empty
                <li class="text-gray-400">尚無檔案</li>
            @endforelse
        </ul>

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

    結果如下圖:

十一、問題03:希望能加上排序功能
    1.修改 FolderController@show,直接將整個直接替換成下面的程式碼
    public function show(Folder $folder, Request $request){
        // 安全檢查:只能看自己的資料夾
        abort_if($folder->user_id !== Auth::id(), 403);
        $subFolders = Folder::where('parent_id', $folder->id)->get();
        // 建立 breadcrumb    
        $breadcrumbs = collect();
        $current = $folder;
        while($current) {
            $breadcrumbs->prepend($current);
            $current = $current->parent;
        }

        // 檔案排序
        $sort = $request->get('sort', 'name'); // 預設按名稱
        $direction = $request->get('direction', 'asc'); // 預設升序

        $files = File::where('folder_id', $folder->id)
            ->orderBy($sort, $direction)
            ->get();

        return view('folders.show', compact('folder', 'subFolders','files','breadcrumbs','sort','direction'));
    }
    2.編輯 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>
            @foreach ($breadcrumbs as $crumb)
                / <a href="{{ route('folders.show', $crumb) }}" class="hover:underline">{{ $crumb->name }}</a>
            @endforeach
        </div>

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

        {{-- 成功訊息 --}}
        @if(session('success'))
            <div class="mb-4 text-green-600">
                {{ session('success') }}
            </div>
        @endif

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

        {{-- 子資料夾列表 --}}
        <ul class="space-y-2 mb-6">
            @forelse($subFolders as $sub)
                <li class="border p-3 rounded hover:bg-gray-100 flex justify-between items-center"
                    x-data="{ editing: false, name: '{{ e($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 required>
                                <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>
            @endforelse
        </ul>

        {{-- 上傳檔案 --}}
        <form method="POST" action="{{ route('files.store') }}" enctype="multipart/form-data" class="mb-4 flex gap-2">
            @csrf
            <input type="hidden" name="folder_id" value="{{ $folder->id }}">
            <input type="file" name="file" required>
            <button class="bg-green-600 text-white px-4 py-2 rounded">上傳檔案</button>
        </form>

        {{-- 檔案排序 --}}
        <div class="mb-2 flex items-center gap-2 text-sm text-gray-500">
            排序:
            @php
                $options = ['name' => '名稱', 'size' => '大小', 'created_at' => '上傳日期'];
            @endphp
            @foreach($options as $key => $label)
                <a href="{{ route('folders.show', $folder) }}?sort={{ $key }}&direction={{ $sort === $key && $direction === 'asc' ? 'desc' : 'asc' }}"
                   class="hover:underline {{ $sort === $key ? 'font-semibold' : '' }}">
                   {{ $label }}
                   @if($sort === $key)
                       {{ $direction === 'asc' ? '↑' : '↓' }}
                   @endif
                </a>
                @if(!$loop->last) | @endif
            @endforeach
        </div>

        {{-- 檔案列表 --}}
        <h2 class="font-semibold mb-2">檔案</h2>
        <ul class="space-y-2">
            @forelse($files as $file)
                <li class="border p-3 rounded flex justify-between items-center"
                    x-data="{ editing: false, name: '{{ e($file->name) }}' }">

                    {{-- 檔案名稱 + 大小 + 上傳日期 --}}
                    <div class="flex items-center gap-4">
                        📄
                        <template x-if="!editing">
                            <span @dblclick="editing = true" class="cursor-pointer text-blue-600">
                                {{ $file->name }}
                            </span>
                        </template>
                        <template x-if="editing">
                            <form method="POST" action="{{ route('files.update', $file) }}" 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 required>
                                <button class="text-green-600 text-sm">儲存</button>
                                <button type="button" @click="editing = false" class="text-gray-500 text-sm">取消</button>
                            </form>
                        </template>

                        {{-- 檔案大小 --}}
                        <span class="text-gray-500 text-sm">
                            @php
                                if($file->size >= 1048576){
                                    $size = number_format($file->size / 1048576, 2) . ' MB';
                                } elseif($file->size >= 1024){
                                    $size = number_format($file->size / 1024, 2) . ' KB';
                                } else {
                                    $size = $file->size . ' B';
                                }
                            @endphp
                            {{ $size }}
                        </span>

                        {{-- 上傳日期 --}}
                        <span class="text-gray-400 text-sm">{{ $file->created_at->format('Y-m-d H:i') }}</span>
                    </div>

                    {{-- 操作按鈕 --}}
                    <div class="flex gap-2 items-center">
                        <a href="{{ route('files.download', $file) }}" class="text-blue-600 hover:underline">下載</a>
                        <button type="button" @click="editing = true" class="text-sm text-gray-600">重新命名</button>
                        <form method="POST" action="{{ route('files.destroy', $file) }}"
                              onsubmit="return confirm('確定要刪除這個檔案嗎?')">
                            @csrf
                            @method('DELETE')
                            <button class="text-red-600 text-sm">刪除</button>
                        </form>
                    </div>

                </li>
            @empty
                <li class="text-gray-400">尚無檔案</li>
            @endforelse
        </ul>

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

    3.最後成果

十二、問題04:index.blade.php 希望能跟 show.blade.php 具有同樣功能
    1.編輯 FolderController@index,直接將整個直接替換成下面的程式碼
    public function index(Request $request){
        // 根目錄資料夾
        $folders = Folder::where('user_id', Auth::id())
            ->whereNull('parent_id')
            ->get();

        // 檔案排序設定
        $sort = $request->query('sort', 'name'); // 預設按名稱排序
        $direction = $request->query('direction', 'asc');

        // 安全檢查:只允許這三個欄位排序
        if (!in_array($sort, ['name', 'size', 'created_at'])) {
            $sort = 'name';
        }
        if (!in_array($direction, ['asc', 'desc'])) {
            $direction = 'asc';
        }

        // 根目錄檔案
        $files = File::where('user_id', Auth::id())
            ->whereNull('folder_id')
            ->orderBy($sort, $direction)
            ->get();

        return view('folders.index', compact('folders', 'files', 'sort', 'direction'));
    }

    2.編輯 index.blade.php,直接將整個直接替換成下面的程式碼
<x-app-layout>
    <div class="p-6 max-w-4xl mx-auto">

        {{-- Breadcrumb --}}
        <div class="mb-4 text-sm text-gray-500">
            我的雲端硬碟
        </div>

        <h1 class="text-xl font-bold mb-4">我的資料夾</h1>

        {{-- 成功訊息 --}}
        @if(session('success'))
            <div class="mb-4 text-green-600">
                {{ session('success') }}
            </div>
        @endif

        {{-- 新增根資料夾 --}}
        <form method="POST" action="{{ route('folders.store') }}" class="mb-4 flex gap-2">
            @csrf
            <input type="text" name="name" placeholder="新資料夾名稱" class="border rounded p-2 flex-1" required>
            <button class="bg-blue-500 text-white px-4 py-2 rounded">新增</button>
        </form>

        {{-- 資料夾列表 --}}
        <ul class="space-y-2 mb-6">
            @forelse($folders as $folder)
                <li class="border p-3 rounded hover:bg-gray-100 flex justify-between items-center"
                    x-data="{ editing: false, name: '{{ e($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 required>
                                <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 type="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>
            @empty
                <li class="text-gray-400">尚無資料夾</li>
            @endforelse
        </ul>

        {{-- 上傳檔案 --}}
        <form method="POST" action="{{ route('files.store') }}" enctype="multipart/form-data" class="mb-4 flex gap-2">
            @csrf
            <input type="hidden" name="folder_id" value="">
            <input type="file" name="file" required>
            <button class="bg-green-600 text-white px-4 py-2 rounded">上傳檔案</button>
        </form>

        {{-- 檔案排序 --}}
        <div class="mb-2 flex items-center gap-2 text-sm text-gray-500">
            排序:
            @php
                $options = ['name' => '名稱', 'size' => '大小', 'created_at' => '上傳日期'];
            @endphp
            @foreach($options as $key => $label)
                <a href="{{ route('folders.index') }}?sort={{ $key }}&direction={{ $sort === $key && $direction === 'asc' ? 'desc' : 'asc' }}"
                   class="hover:underline {{ $sort === $key ? 'font-semibold' : '' }}">
                   {{ $label }}
                   @if($sort === $key)
                       {{ $direction === 'asc' ? '↑' : '↓' }}
                   @endif
                </a>
                @if(!$loop->last) | @endif
            @endforeach
        </div>

        {{-- 檔案列表 --}}
        <h2 class="font-semibold mb-2">檔案</h2>
        <ul class="space-y-2">
            @forelse($files as $file)
                <li class="border p-3 rounded flex justify-between items-center"
                    x-data="{ editing: false, name: '{{ e($file->name) }}' }">

                    {{-- 檔案名稱 + 大小 + 上傳日期 --}}
                    <div class="flex items-center gap-4">
                        📄
                        <template x-if="!editing">
                            <span @dblclick="editing = true" class="cursor-pointer text-blue-600">
                                {{ $file->name }}
                            </span>
                        </template>
                        <template x-if="editing">
                            <form method="POST" action="{{ route('files.update', $file) }}" 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 required>
                                <button class="text-green-600 text-sm">儲存</button>
                                <button type="button" @click="editing = false" class="text-gray-500 text-sm">取消</button>
                            </form>
                        </template>

                        {{-- 檔案大小 --}}
                        <span class="text-gray-500 text-sm">
                            @php
                                if($file->size >= 1048576){
                                    $size = number_format($file->size / 1048576, 2) . ' MB';
                                } elseif($file->size >= 1024){
                                    $size = number_format($file->size / 1024, 2) . ' KB';
                                } else {
                                    $size = $file->size . ' B';
                                }
                            @endphp
                            {{ $size }}
                        </span>

                        {{-- 上傳日期 --}}
                        <span class="text-gray-400 text-sm">{{ $file->created_at->format('Y-m-d H:i') }}</span>
                    </div>

                    {{-- 操作按鈕 --}}
                    <div class="flex gap-2 items-center">
                        <a href="{{ route('files.download', $file) }}" class="text-blue-600 hover:underline">下載</a>
                        <button type="button" @click="editing = true" class="text-sm text-gray-600">重新命名</button>
                        <form method="POST" action="{{ route('files.destroy', $file) }}"
                              onsubmit="return confirm('確定要刪除這個檔案嗎?')">
                            @csrf
                            @method('DELETE')
                            <button class="text-red-600 text-sm">刪除</button>
                        </form>
                    </div>

                </li>
            @empty
                <li class="text-gray-400">尚無檔案</li>
            @endforelse
        </ul>

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

    3.結果如下圖:
十三、問題05:現在只能一次上傳一個檔案,可不可以一次上傳很多檔案?
    1.編輯 FileController@store,直接將整個直接替換成下面的程式碼
    public function store(Request $request){
        $request->validate([
            'files.*' => 'required|file|max:10240', // 每個檔案最大 10MB
        ]);

        $uploadedFiles = $request->file('files');

        foreach ($uploadedFiles as $uploaded) {
            $path = $uploaded->store('files', 'public');

            File::create([
            'user_id' => Auth::id(),
            'folder_id' => $request->folder_id,
            'name' => $uploaded->getClientOriginalName(),
            'path' => $path,
            'size' => $uploaded->getSize(),
            ]);
        }

        return back()->with('success', count($uploadedFiles) . ' 個檔案上傳成功');
    }
    2.編輯 index.blade.php,直接將整個直接替換成下面的程式碼
<x-app-layout>
    <div class="p-6 max-w-4xl mx-auto">

        {{-- Breadcrumb --}}
        <div class="mb-4 text-sm text-gray-500">
            我的雲端硬碟
        </div>

        <h1 class="text-xl font-bold mb-4">我的資料夾</h1>

        {{-- 成功訊息 --}}
        @if(session('success'))
            <div class="mb-4 text-green-600">
                {{ session('success') }}
            </div>
        @endif

        {{-- 新增根資料夾 --}}
        <form method="POST" action="{{ route('folders.store') }}" class="mb-4 flex gap-2">
            @csrf
            <input type="text" name="name" placeholder="新資料夾名稱" class="border rounded p-2 flex-1" required>
            <button class="bg-blue-500 text-white px-4 py-2 rounded">新增</button>
        </form>

        {{-- 根目錄資料夾列表 --}}
        <ul class="space-y-2 mb-6">
            @forelse($folders as $folder)
                <li class="border p-3 rounded hover:bg-gray-100 flex justify-between items-center"
                    x-data="{ editing: false, name: '{{ e($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 required>
                                <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 type="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>
            @empty
                <li class="text-gray-400">尚無資料夾</li>
            @endforelse
        </ul>

        {{-- 多檔案上傳 --}}
        <form method="POST" action="{{ route('files.store') }}" enctype="multipart/form-data" class="mb-4 flex gap-2">
            @csrf
            <input type="hidden" name="folder_id" value="">
            <input type="file" name="files[]" multiple required>
            <button class="bg-green-600 text-white px-4 py-2 rounded">上傳檔案</button>
        </form>

        {{-- 檔案排序 --}}
        <div class="mb-2 flex items-center gap-2 text-sm text-gray-500">
            排序:
            @php
                $options = ['name' => '名稱', 'size' => '大小', 'created_at' => '上傳日期'];
            @endphp
            @foreach($options as $key => $label)
                <a href="{{ route('folders.index') }}?sort={{ $key }}&direction={{ $sort === $key && $direction === 'asc' ? 'desc' : 'asc' }}"
                   class="hover:underline {{ $sort === $key ? 'font-semibold' : '' }}">
                   {{ $label }}
                   @if($sort === $key)
                       {{ $direction === 'asc' ? '↑' : '↓' }}
                   @endif
                </a>
                @if(!$loop->last) | @endif
            @endforeach
        </div>

        {{-- 根目錄檔案列表 --}}
        <h2 class="font-semibold mb-2">檔案</h2>
        <ul class="space-y-2">
            @forelse($files as $file)
                <li class="border p-3 rounded flex justify-between items-center"
                    x-data="{ editing: false, name: '{{ e($file->name) }}' }">

                    {{-- 檔案名稱 + 大小 + 上傳日期 --}}
                    <div class="flex items-center gap-4">
                        📄
                        <template x-if="!editing">
                            <span @dblclick="editing = true" class="cursor-pointer text-blue-600">
                                {{ $file->name }}
                            </span>
                        </template>
                        <template x-if="editing">
                            <form method="POST" action="{{ route('files.update', $file) }}" 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 required>
                                <button class="text-green-600 text-sm">儲存</button>
                                <button type="button" @click="editing = false" class="text-gray-500 text-sm">取消</button>
                            </form>
                        </template>

                        {{-- 檔案大小 --}}
                        <span class="text-gray-500 text-sm">
                            @php
                                if($file->size >= 1048576){
                                    $size = number_format($file->size / 1048576, 2) . ' MB';
                                } elseif($file->size >= 1024){
                                    $size = number_format($file->size / 1024, 2) . ' KB';
                                } else {
                                    $size = $file->size . ' B';
                                }
                            @endphp
                            {{ $size }}
                        </span>

                        {{-- 上傳日期 --}}
                        <span class="text-gray-400 text-sm">{{ $file->created_at->format('Y-m-d H:i') }}</span>
                    </div>

                    {{-- 操作按鈕 --}}
                    <div class="flex gap-2 items-center">
                        <a href="{{ route('files.download', $file) }}" class="text-blue-600 hover:underline">下載</a>
                        <button type="button" @click="editing = true" class="text-sm text-gray-600">重新命名</button>
                        <form method="POST" action="{{ route('files.destroy', $file) }}"
                              onsubmit="return confirm('確定要刪除這個檔案嗎?')">
                            @csrf
                            @method('DELETE')
                            <button class="text-red-600 text-sm">刪除</button>
                        </form>
                    </div>

                </li>
            @empty
                <li class="text-gray-400">尚無檔案</li>
            @endforelse
        </ul>

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


    3.編輯 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>
            @foreach ($breadcrumbs as $crumb)
                / <a href="{{ route('folders.show', $crumb) }}" class="hover:underline">{{ $crumb->name }}</a>
            @endforeach
        </div>

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

        {{-- 成功訊息 --}}
        @if(session('success'))
            <div class="mb-4 text-green-600">
                {{ session('success') }}
            </div>
        @endif

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

        {{-- 子資料夾列表 --}}
        <ul class="space-y-2 mb-6">
            @forelse($subFolders as $sub)
                <li class="border p-3 rounded hover:bg-gray-100 flex justify-between items-center"
                    x-data="{ editing: false, name: '{{ e($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 required>
                                <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 type="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-red-600 text-sm">刪除</button>
                        </form>
                    </div>
                </li>
            @empty
                <li class="text-gray-400">此資料夾尚無子資料夾</li>
            @endforelse
        </ul>

        {{-- 多檔案上傳 --}}
        <form method="POST" action="{{ route('files.store') }}" enctype="multipart/form-data" class="mb-4 flex gap-2">
            @csrf
            <input type="hidden" name="folder_id" value="{{ $folder->id }}">
            <input type="file" name="files[]" multiple required>
            <button class="bg-green-600 text-white px-4 py-2 rounded">上傳檔案</button>
        </form>

        {{-- 檔案排序 --}}
        <div class="mb-2 flex items-center gap-2 text-sm text-gray-500">
            排序:
            @php
                $options = ['name' => '名稱', 'size' => '大小', 'created_at' => '上傳日期'];
            @endphp
            @foreach($options as $key => $label)
                <a href="{{ route('folders.show', $folder) }}?sort={{ $key }}&direction={{ $sort === $key && $direction === 'asc' ? 'desc' : 'asc' }}"
                   class="hover:underline {{ $sort === $key ? 'font-semibold' : '' }}">
                   {{ $label }}
                   @if($sort === $key)
                       {{ $direction === 'asc' ? '↑' : '↓' }}
                   @endif
                </a>
                @if(!$loop->last) | @endif
            @endforeach
        </div>

        {{-- 子資料夾檔案列表 --}}
        <h2 class="font-semibold mb-2">檔案</h2>
        <ul class="space-y-2">
            @forelse($files as $file)
                <li class="border p-3 rounded flex justify-between items-center"
                    x-data="{ editing: false, name: '{{ e($file->name) }}' }">

                    <div class="flex items-center gap-4">
                        📄
                        <template x-if="!editing">
                            <span @dblclick="editing = true" class="cursor-pointer text-blue-600">{{ $file->name }}</span>
                        </template>
                        <template x-if="editing">
                            <form method="POST" action="{{ route('files.update', $file) }}" 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 required>
                                <button class="text-green-600 text-sm">儲存</button>
                                <button type="button" @click="editing = false" class="text-gray-500 text-sm">取消</button>
                            </form>
                        </template>

                        {{-- 檔案大小 --}}
                        <span class="text-gray-500 text-sm">
                            @php
                                if($file->size >= 1048576){
                                    $size = number_format($file->size / 1048576, 2) . ' MB';
                                } elseif($file->size >= 1024){
                                    $size = number_format($file->size / 1024, 2) . ' KB';
                                } else {
                                    $size = $file->size . ' B';
                                }
                            @endphp
                            {{ $size }}
                        </span>

                        {{-- 上傳日期 --}}
                        <span class="text-gray-400 text-sm">{{ $file->created_at->format('Y-m-d H:i') }}</span>
                    </div>

                    {{-- 操作按鈕 --}}
                    <div class="flex gap-2 items-center">
                        <a href="{{ route('files.download', $file) }}" class="text-blue-600 hover:underline">下載</a>
                        <button type="button" @click="editing = true" class="text-sm text-gray-600">重新命名</button>
                        <form method="POST" action="{{ route('files.destroy', $file) }}"
                              onsubmit="return confirm('確定要刪除這個檔案嗎?')">
                            @csrf
                            @method('DELETE')
                            <button class="text-red-600 text-sm">刪除</button>
                        </form>
                    </div>

                </li>
            @empty
                <li class="text-gray-400">尚無檔案</li>
            @endforelse
        </ul>

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



沒有留言:

張貼留言

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

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