bianjunhui пре 1 недеља
родитељ
комит
ab204c5a19

+ 30 - 0
packages/Longyi/ImageUpload/src/Database/Migrations/2026_07_14_000001_create_image_folders_table.php

@@ -0,0 +1,30 @@
+<?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('image_folders', function (Blueprint $table) {
+            $table->id();
+            $table->string('name', 255)->comment('文件夹名称');
+            $table->unsignedBigInteger('parent_id')->default(0)->index()->comment('父文件夹ID,0为根目录');
+            $table->string('path', 500)->comment('完整路径,用/分隔');
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('image_folders');
+    }
+};

+ 35 - 0
packages/Longyi/ImageUpload/src/Database/Migrations/2026_07_14_000002_create_image_uploads_table.php

@@ -0,0 +1,35 @@
+<?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('image_uploads', function (Blueprint $table) {
+            $table->id();
+            $table->unsignedBigInteger('folder_id')->default(0)->index()->comment('所属文件夹ID,0为根目录');
+            $table->string('original_name', 255)->comment('原始文件名');
+            $table->string('file_name', 255)->comment('存储文件名');
+            $table->string('file_path', 500)->comment('S3存储路径');
+            $table->string('file_url', 1000)->comment('完整访问URL');
+            $table->string('mime_type', 100)->comment('文件MIME类型');
+            $table->unsignedBigInteger('file_size')->default(0)->comment('文件大小(字节)');
+            $table->string('disk', 50)->default('s3')->comment('存储磁盘');
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('image_uploads');
+    }
+};

+ 104 - 86
packages/Longyi/ImageUpload/src/Http/Controllers/Admin/ImageUploadController.php

@@ -4,7 +4,8 @@ namespace Longyi\ImageUpload\Http\Controllers\Admin;
 
 use App\Http\Controllers\Controller;
 use Illuminate\Http\Request;
-use Illuminate\Support\Facades\Storage;
+use Longyi\ImageUpload\Models\ImageFolder;
+use Longyi\ImageUpload\Models\ImageUpload as ImageUploadModel;
 use Longyi\ImageUpload\Services\ImageUploadService;
 use Exception;
 
@@ -77,6 +78,29 @@ class ImageUploadController extends Controller
 
             $results = $this->imageUploadService->uploadMultiple($files, $directory);
 
+            // 解析 folder_id:如果传了 directory,按目录名查找文件夹
+            $folderId = 0;
+            if (! empty($directory)) {
+                $folder = ImageFolder::where('name', $directory)->first();
+                $folderId = $folder ? $folder->id : 0;
+            }
+
+            // 将上传成功的图片记录写入 image_uploads 表
+            foreach ($results as $result) {
+                if (! empty($result['success'])) {
+                    ImageUploadModel::create([
+                        'folder_id'      => $folderId,
+                        'original_name'  => $result['original_name'] ?? '',
+                        'file_name'      => basename($result['path'] ?? ''),
+                        'file_path'      => $result['path'] ?? '',
+                        'file_url'       => $result['url'] ?? '',
+                        'mime_type'      => $result['mime_type'] ?? '',
+                        'file_size'      => $result['size'] ?? 0,
+                        'disk'           => $result['disk'] ?? 's3',
+                    ]);
+                }
+            }
+
             $successCount = count(array_filter($results, fn($r) => $r['success'] ?? false));
             $failedCount = count($results) - $successCount;
 
@@ -97,7 +121,7 @@ class ImageUploadController extends Controller
     }
 
     /**
-     * Delete an image.
+     * Delete an image (S3 + database record).
      *
      * @param Request $request
      * @return \Illuminate\Http\JsonResponse
@@ -113,19 +137,16 @@ class ImageUploadController extends Controller
             $path = $request->input('path');
             $disk = $request->input('disk');
 
+            // 删除 S3 文件
             $deleted = $this->imageUploadService->delete($path, $disk);
 
-            if ($deleted) {
-                return response()->json([
-                    'success' => true,
-                    'message' => __('Image deleted successfully'),
-                ]);
-            } else {
-                return response()->json([
-                    'success' => false,
-                    'message' => __('Failed to delete image'),
-                ], 422);
-            }
+            // 同时删除数据库记录
+            ImageUploadModel::where('file_path', $path)->delete();
+
+            return response()->json([
+                'success' => true,
+                'message' => __('Image deleted successfully'),
+            ]);
         } catch (Exception $e) {
             return response()->json([
                 'success' => false,
@@ -166,7 +187,7 @@ class ImageUploadController extends Controller
     }
 
     /**
-     * Get folders list.
+     * Get folders list (from database).
      *
      * @param Request $request
      * @return \Illuminate\Http\JsonResponse
@@ -174,29 +195,17 @@ class ImageUploadController extends Controller
     public function getFolders(Request $request)
     {
         try {
-            $disk = config('image_upload.default_disk', 's3');
-            $basePath = config('image_upload.upload_directory', 'images/uploads');
-
-            $folders = [];
-            $storage = Storage::disk($disk);
-
-            // Get all directories under base path
-            $allDirs = $storage->allDirectories($basePath);
-
-            foreach ($allDirs as $dir) {
-                $relativePath = str_replace($basePath . '/', '', $dir);
-                if (! empty($relativePath)) {
-                    $folders[] = [
-                        'path' => $dir,
-                        'name' => $relativePath,
-                    ];
-                }
-            }
+            $folders = ImageFolder::orderBy('created_at', 'desc')->get()->map(function ($folder) {
+                return [
+                    'id'   => $folder->id,
+                    'name' => $folder->name,
+                    'path' => $folder->path,
+                ];
+            });
 
             return response()->json([
                 'success' => true,
-                'data' => [
-                    'base_path' => $basePath,
+                'data'    => [
                     'folders' => $folders,
                 ],
             ]);
@@ -209,7 +218,7 @@ class ImageUploadController extends Controller
     }
 
     /**
-     * Create a folder.
+     * Create a folder (database only, no S3).
      *
      * @param Request $request
      * @return \Illuminate\Http\JsonResponse
@@ -218,34 +227,39 @@ class ImageUploadController extends Controller
     {
         try {
             $request->validate([
-                'folder_name' => 'required|string|max:255',
+                'folder_name'   => 'required|string|max:255',
                 'parent_folder' => 'nullable|string',
             ]);
 
-            $folderName = $request->input('folder_name');
-            $parentFolder = $request->input('parent_folder', '');
+            $folderName   = trim($request->input('folder_name'));
+            $parentFolder = trim($request->input('parent_folder', ''));
 
-            $disk = config('image_upload.default_disk', 's3');
-            $basePath = config('image_upload.upload_directory', 'uploads');
-            $folderPath = $basePath;
+            // 检查同名文件夹
+            $exists = ImageFolder::where('name', $folderName)
+                ->where('parent_id', 0)
+                ->exists();
 
-            if (! empty($parentFolder)) {
-                $folderPath .= '/' . trim($parentFolder, '/');
+            if ($exists) {
+                return response()->json([
+                    'success' => false,
+                    'message' => __('Folder already exists'),
+                ], 422);
             }
-            $folderPath .= '/' . trim($folderName, '/');
-            // For S3, create a placeholder file to "create" the folder
-            $storage = Storage::disk($disk);
 
-            if (! $storage->exists($folderPath)) {
-                $storage->put($folderPath . '/.gitkeep', '');
-            }
+            $basePath = config('image_upload.upload_directory', 'uploads');
+            $folder = ImageFolder::create([
+                'name'      => $folderName,
+                'parent_id' => 0,
+                'path'      => $basePath . '/' . $folderName,
+            ]);
 
             return response()->json([
                 'success' => true,
                 'message' => __('Folder created successfully'),
-                'data' => [
-                    'path' => $folderPath,
-                    'name' => trim($folderName, '/'),
+                'data'    => [
+                    'id'   => $folder->id,
+                    'name' => $folder->name,
+                    'path' => $folder->path,
                 ],
             ]);
         } catch (Exception $e) {
@@ -257,7 +271,7 @@ class ImageUploadController extends Controller
     }
 
     /**
-     * Delete a folder.
+     * Delete a folder (database + related images on S3).
      *
      * @param Request $request
      * @return \Illuminate\Http\JsonResponse
@@ -266,17 +280,27 @@ class ImageUploadController extends Controller
     {
         try {
             $request->validate([
-                'folder_path' => 'required|string',
+                'folder_id' => 'required|integer',
             ]);
 
-            $folderPath = $request->input('folder_path');
-            $disk = config('image_upload.default_disk', 's3');
-            $storage = Storage::disk($disk);
-
-            if ($storage->exists($folderPath)) {
-                $storage->deleteDirectory($folderPath);
+            $folderId = $request->input('folder_id');
+            $folder = ImageFolder::findOrFail($folderId);
+
+            // 删除该文件夹下所有图片的 S3 文件和数据库记录
+            $images = ImageUploadModel::where('folder_id', $folderId)->get();
+            foreach ($images as $image) {
+                // 从 S3 删除图片文件
+                try {
+                    $this->imageUploadService->delete($image->file_path, $image->disk);
+                } catch (\Exception $e) {
+                    // S3 删除失败不影响数据库删除
+                }
+                $image->delete();
             }
 
+            // 删除文件夹记录
+            $folder->delete();
+
             return response()->json([
                 'success' => true,
                 'message' => __('Folder deleted successfully'),
@@ -290,7 +314,7 @@ class ImageUploadController extends Controller
     }
 
     /**
-     * Get images in a folder.
+     * Get images in a folder (from database).
      *
      * @param Request $request
      * @return \Illuminate\Http\JsonResponse
@@ -298,37 +322,31 @@ class ImageUploadController extends Controller
     public function getImages(Request $request)
     {
         try {
-            $folderPath = $request->input('folder_path', '');
-            $disk = config('image_upload.default_disk', 's3');
-            $basePath = config('image_upload.upload_directory', 'images/uploads');
+            $folderId = $request->input('folder_id', 0);
 
-            $searchPath = $basePath;
-            if (! empty($folderPath)) {
-                $searchPath .= '/' . trim($folderPath, '/');
-            }
+            $query = ImageUploadModel::orderBy('created_at', 'desc');
 
-            $storage = Storage::disk($disk);
-            $files = $storage->files($searchPath);
-
-            $images = [];
-            $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'];
-
-            foreach ($files as $file) {
-                $extension = strtolower(pathinfo($file, PATHINFO_EXTENSION));
-                if (in_array($extension, $allowedExtensions)) {
-                    $images[] = [
-                        'path' => $file,
-                        'name' => basename($file),
-                        'url' => $storage->url($file),
-                        'size' => $storage->size($file),
-                        'last_modified' => $storage->lastModified($file),
-                    ];
-                }
+            if ($folderId > 0) {
+                $query->where('folder_id', $folderId);
+            } else {
+                $query->where('folder_id', 0);
             }
 
+            $images = $query->get()->map(function ($image) {
+                return [
+                    'id'            => $image->id,
+                    'path'          => $image->file_path,
+                    'name'          => $image->original_name,
+                    'url'           => $image->file_url,
+                    'size'          => $image->file_size,
+                    'mime_type'     => $image->mime_type,
+                    'last_modified' => $image->updated_at->timestamp,
+                ];
+            });
+
             return response()->json([
                 'success' => true,
-                'data' => $images,
+                'data'    => $images,
             ]);
         } catch (Exception $e) {
             return response()->json([

+ 50 - 0
packages/Longyi/ImageUpload/src/Models/ImageFolder.php

@@ -0,0 +1,50 @@
+<?php
+
+namespace Longyi\ImageUpload\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class ImageFolder extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var string
+     */
+    protected $table = 'image_folders';
+
+    /**
+     * The attributes that are mass assignable.
+     *
+     * @var array
+     */
+    protected $fillable = [
+        'name',
+        'parent_id',
+        'path',
+    ];
+
+    /**
+     * Get the images in this folder.
+     */
+    public function images()
+    {
+        return $this->hasMany(ImageUpload::class, 'folder_id');
+    }
+
+    /**
+     * Get the parent folder.
+     */
+    public function parent()
+    {
+        return $this->belongsTo(ImageFolder::class, 'parent_id');
+    }
+
+    /**
+     * Get the child folders.
+     */
+    public function children()
+    {
+        return $this->hasMany(ImageFolder::class, 'parent_id');
+    }
+}

+ 39 - 0
packages/Longyi/ImageUpload/src/Models/ImageUpload.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace Longyi\ImageUpload\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class ImageUpload extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var string
+     */
+    protected $table = 'image_uploads';
+
+    /**
+     * The attributes that are mass assignable.
+     *
+     * @var array
+     */
+    protected $fillable = [
+        'folder_id',
+        'original_name',
+        'file_name',
+        'file_path',
+        'file_url',
+        'mime_type',
+        'file_size',
+        'disk',
+    ];
+
+    /**
+     * Get the folder that owns this image.
+     */
+    public function folder()
+    {
+        return $this->belongsTo(ImageFolder::class, 'folder_id');
+    }
+}

+ 8 - 0
packages/Longyi/ImageUpload/src/Providers/ImageUploadServiceProvider.php

@@ -19,6 +19,9 @@ class ImageUploadServiceProvider extends ServiceProvider
         $this->loadRoutesFrom(__DIR__ . '/../Routes/admin-routes.php');
         $this->loadRoutesFrom(__DIR__ . '/../Routes/shop-routes.php');
 
+        // Load database migrations
+        $this->loadMigrationsFrom(__DIR__ . '/../Database/Migrations');
+
         // Load views
         $this->loadViewsFrom(__DIR__ . '/../Resources/views', 'imageupload');
 
@@ -39,6 +42,11 @@ class ImageUploadServiceProvider extends ServiceProvider
         $this->publishes([
             __DIR__ . '/../Resources/lang' => resource_path('lang/vendor/imageupload'),
         ], 'translations');
+
+        // Publish migrations
+        $this->publishes([
+            __DIR__ . '/../Database/Migrations' => database_path('migrations'),
+        ], 'migrations');
     }
 
     /**

+ 28 - 31
packages/Longyi/ImageUpload/src/Resources/views/components/media-manager.blade.php

@@ -62,25 +62,13 @@
                                 @lang('imageupload::app.admin.media-manager.all_folders')
                             </p>
 
-                            <!-- Root Folder -->
-                            <div
-                                class="flex items-center justify-between px-2 py-1.5 rounded cursor-pointer text-sm mb-0.5"
-                                :class="currentFolder === '' ? 'bg-blue-100 dark:bg-blue-800 text-blue-700 dark:text-blue-200' : 'hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300'"
-                                @click="selectFolder('')"
-                            >
-                                <span class="flex items-center gap-1.5 truncate">
-                                    <span class="icon-folder text-sm"></span>
-                                    @lang('imageupload::app.admin.media-manager.root_directory')
-                                </span>
-                            </div>
-
                             <!-- Sub Folders -->
                             <div
                                 v-for="folder in folders"
-                                :key="folder.path"
+                                :key="folder.id"
                                 class="flex items-center justify-between px-2 py-1.5 rounded cursor-pointer text-sm mb-0.5 group"
-                                :class="currentFolder === folder.name ? 'bg-blue-100 dark:bg-blue-800 text-blue-700 dark:text-blue-200' : 'hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300'"
-                                @click="selectFolder(folder.name)"
+                                :class="currentFolderId === folder.id ? 'bg-blue-100 dark:bg-blue-800 text-blue-700 dark:text-blue-200' : 'hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300'"
+                                @click="selectFolder(folder.id)"
                             >
                                 <span class="flex items-center gap-1.5 truncate flex-1">
                                     <span class="icon-folder text-sm"></span>
@@ -229,7 +217,8 @@
             return {
                 folders: [],
                 images: [],
-                currentFolder: '',
+                currentFolderId: 0,
+                currentFolderName: '',
                 selectedImage: null,
                 showCreateFolder: false,
                 newFolderName: '',
@@ -261,7 +250,7 @@
                 }
             },
 
-            // Load folders from API
+            // Load folders from API (database)
             async loadFolders() {
                 try {
                     const response = await fetch('{{ route("admin.image_upload.folders") }}', {
@@ -279,12 +268,12 @@
                 }
             },
 
-            // Load images from current folder
+            // Load images from current folder (database)
             async loadImages() {
                 this.loading = true;
                 try {
                     const url = new URL('{{ route("admin.image_upload.images") }}', window.location.origin);
-                    url.searchParams.set('folder_path', this.currentFolder);
+                    url.searchParams.set('folder_id', this.currentFolderId);
 
                     const response = await fetch(url.toString(), {
                         headers: {
@@ -303,14 +292,20 @@
                 }
             },
 
-            // Select a folder
-            selectFolder(folderName) {
-                this.currentFolder = folderName;
+            // Select a folder by id
+            selectFolder(folderId) {
+                this.currentFolderId = folderId;
                 this.selectedImage = null;
+                if (folderId === 0) {
+                    this.currentFolderName = '';
+                } else {
+                    const folder = this.folders.find(f => f.id === folderId);
+                    this.currentFolderName = folder ? folder.name : '';
+                }
                 this.loadImages();
             },
 
-            // Create a new folder
+            // Create a new folder (database only)
             async createFolder() {
                 if (!this.newFolderName.trim()) return;
 
@@ -356,7 +351,7 @@
                 this.deleteFolder(folder);
             },
 
-            // Delete folder
+            // Delete folder (database + S3 images)
             async deleteFolder(folder) {
                 try {
                     const response = await fetch('{{ route("admin.image_upload.folder.delete") }}', {
@@ -367,7 +362,7 @@
                             'X-CSRF-TOKEN': '{{ csrf_token() }}',
                         },
                         body: JSON.stringify({
-                            folder_path: folder.path,
+                            folder_id: folder.id,
                         }),
                     });
 
@@ -378,8 +373,9 @@
                             message: '@lang('imageupload::app.admin.media-manager.folder_deleted')',
                         });
 
-                        if (this.currentFolder === folder.name) {
-                            this.currentFolder = '';
+                        if (this.currentFolderId === folder.id) {
+                            this.currentFolderId = 0;
+                            this.currentFolderName = '';
                         }
                         await this.loadFolders();
                         await this.loadImages();
@@ -406,7 +402,7 @@
                 this.deleteImage(image);
             },
 
-            // Delete image
+            // Delete image (S3 + database)
             async deleteImage(image) {
                 try {
                     const response = await fetch('{{ route("admin.image_upload.delete") }}', {
@@ -452,7 +448,7 @@
                 event.target.value = '';
             },
 
-            // Upload files
+            // Upload files to S3, record in database
             async uploadFiles(files) {
                 if (files.length === 0) return;
 
@@ -464,8 +460,9 @@
                         formData.append('images[]', file);
                     });
 
-                    if (this.currentFolder) {
-                        formData.append('directory', this.currentFolder);
+                    // 传当前文件夹名作为 directory,后端会用它在 image_uploads 表关联 folder_id
+                    if (this.currentFolderId > 0 && this.currentFolderName) {
+                        formData.append('directory', this.currentFolderName);
                     }
 
                     const response = await fetch('{{ route("admin.image_upload.upload_multiple") }}', {