Просмотр исходного кода

Merge branch 'variant' of http://gogs.hnwmzp.cn/chengwenliang/nshop into dev

chengwl 1 день назад
Родитель
Сommit
78c5820d21
27 измененных файлов с 1797 добавлено и 274 удалено
  1. 1 0
      composer.json
  2. 239 1
      composer.lock
  3. 32 0
      database/migrations/2026_04_10_064108_create_media_table.php
  4. 7 2
      packages/Longyi/Core/src/Contracts/ProductVariant.php
  5. 5 5
      packages/Longyi/Core/src/Database/Migrations/2026_03_07_000001_create_product_variant_images_table.php
  6. 25 0
      packages/Longyi/Core/src/Database/Migrations/2026_03_07_000002_add_meta_to_product_images_table.php
  7. 105 0
      packages/Longyi/Core/src/Database/Migrations/2026_04_16_000001_alter_product_variant_images_use_media_id.php
  8. 162 0
      packages/Longyi/Core/src/Helpers/ProductImage.php
  9. 114 48
      packages/Longyi/Core/src/Http/Controllers/Admin/FlexibleVariantController.php
  10. 104 0
      packages/Longyi/Core/src/Models/ProductImage.php
  11. 69 44
      packages/Longyi/Core/src/Models/ProductVariant.php
  12. 20 0
      packages/Longyi/Core/src/Providers/LongyiCoreServiceProvider.php
  13. 128 0
      packages/Longyi/Core/src/Repositories/ProductImageRepository.php
  14. 1 1
      packages/Longyi/Core/src/Repositories/ProductVariantRepository.php
  15. 338 126
      packages/Longyi/Core/src/Resources/views/admin/catalog/products/edit/types/flexible_variant.blade.php
  16. 191 0
      packages/Longyi/Core/src/Resources/views/admin/catalog/products/edit/types/flexible_variant/mediaupload.blade copy.php
  17. 206 0
      packages/Longyi/Core/src/Resources/views/admin/catalog/products/edit/types/flexible_variant/mediaupload.blade.php
  18. 3 1
      packages/Longyi/Core/src/Routes/admin-routes.php
  19. 1 0
      packages/Webkul/Admin/readme.md
  20. 1 1
      packages/Webkul/Admin/src/Resources/assets/css/app.css
  21. 2 2
      packages/Webkul/Admin/src/Resources/assets/js/app.js
  22. 2 2
      packages/Webkul/BagistoApi/src/State/LoginProcessor.php
  23. 0 1
      public/themes/admin/default/build/assets/app-BoBmzsCW.css
  24. 36 36
      public/themes/admin/default/build/assets/app-D8rTgrvs.js
  25. 1 1
      public/themes/admin/default/build/assets/app-CbKA8MhO.css
  26. 1 0
      public/themes/admin/default/build/assets/app-DvtzemP6.css
  27. 3 3
      public/themes/admin/default/build/manifest.json

+ 1 - 0
composer.json

@@ -46,6 +46,7 @@
         "prettus/l5-repository": "^2.6",
         "pusher/pusher-php-server": "^7.0",
         "shetabit/visitor": "^4.1",
+        "spatie/laravel-medialibrary": "^11.21",
         "spatie/laravel-responsecache": "^7.4",
         "spatie/laravel-sitemap": "^7.3",
         "stevebauman/purify": "^6.3"

+ 239 - 1
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "0d6a2d88fb8413fb7204b29f3496d0f1",
+    "content-hash": "d3db3887e839723d5366412b0ebe8f01",
     "packages": [
         {
             "name": "api-platform/documentation",
@@ -8841,6 +8841,244 @@
             ],
             "time": "2025-02-24T09:20:47+00:00"
         },
+        {
+            "name": "spatie/image",
+            "version": "3.9.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/spatie/image.git",
+                "reference": "6a322b5e9268e3903d4fb6e1ff08b7dcc3aa9429"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/spatie/image/zipball/6a322b5e9268e3903d4fb6e1ff08b7dcc3aa9429",
+                "reference": "6a322b5e9268e3903d4fb6e1ff08b7dcc3aa9429",
+                "shasum": ""
+            },
+            "require": {
+                "ext-exif": "*",
+                "ext-json": "*",
+                "ext-mbstring": "*",
+                "php": "^8.2",
+                "spatie/image-optimizer": "^1.7.5",
+                "spatie/temporary-directory": "^2.2",
+                "symfony/process": "^6.4|^7.0|^8.0"
+            },
+            "require-dev": {
+                "ext-gd": "*",
+                "ext-imagick": "*",
+                "laravel/sail": "^1.34",
+                "pestphp/pest": "^3.0|^4.0",
+                "phpstan/phpstan": "^1.10.50",
+                "spatie/pest-plugin-snapshots": "^2.1",
+                "spatie/pixelmatch-php": "^1.0",
+                "spatie/ray": "^1.40.1",
+                "symfony/var-dumper": "^6.4|^7.0|^8.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Spatie\\Image\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Freek Van der Herten",
+                    "email": "freek@spatie.be",
+                    "homepage": "https://spatie.be",
+                    "role": "Developer"
+                }
+            ],
+            "description": "Manipulate images with an expressive API",
+            "homepage": "https://github.com/spatie/image",
+            "keywords": [
+                "image",
+                "spatie"
+            ],
+            "support": {
+                "source": "https://github.com/spatie/image/tree/3.9.4"
+            },
+            "funding": [
+                {
+                    "url": "https://spatie.be/open-source/support-us",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/spatie",
+                    "type": "github"
+                }
+            ],
+            "time": "2026-03-13T14:23:45+00:00"
+        },
+        {
+            "name": "spatie/image-optimizer",
+            "version": "1.8.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/spatie/image-optimizer.git",
+                "reference": "2ad9ac7c19501739183359ae64ea6c15869c23d9"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/spatie/image-optimizer/zipball/2ad9ac7c19501739183359ae64ea6c15869c23d9",
+                "reference": "2ad9ac7c19501739183359ae64ea6c15869c23d9",
+                "shasum": ""
+            },
+            "require": {
+                "ext-fileinfo": "*",
+                "php": "^7.3|^8.0",
+                "psr/log": "^1.0 | ^2.0 | ^3.0",
+                "symfony/process": "^4.2|^5.0|^6.0|^7.0|^8.0"
+            },
+            "require-dev": {
+                "pestphp/pest": "^1.21|^2.0|^3.0|^4.0",
+                "phpunit/phpunit": "^8.5.21|^9.4.4|^10.0|^11.0|^12.0",
+                "symfony/var-dumper": "^4.2|^5.0|^6.0|^7.0|^8.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Spatie\\ImageOptimizer\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Freek Van der Herten",
+                    "email": "freek@spatie.be",
+                    "homepage": "https://spatie.be",
+                    "role": "Developer"
+                }
+            ],
+            "description": "Easily optimize images using PHP",
+            "homepage": "https://github.com/spatie/image-optimizer",
+            "keywords": [
+                "image-optimizer",
+                "spatie"
+            ],
+            "support": {
+                "issues": "https://github.com/spatie/image-optimizer/issues",
+                "source": "https://github.com/spatie/image-optimizer/tree/1.8.1"
+            },
+            "time": "2025-11-26T10:57:19+00:00"
+        },
+        {
+            "name": "spatie/laravel-medialibrary",
+            "version": "11.21.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/spatie/laravel-medialibrary.git",
+                "reference": "d6e2595033ffd130d4dd5d124510ab3304794c44"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/spatie/laravel-medialibrary/zipball/d6e2595033ffd130d4dd5d124510ab3304794c44",
+                "reference": "d6e2595033ffd130d4dd5d124510ab3304794c44",
+                "shasum": ""
+            },
+            "require": {
+                "composer/semver": "^3.4",
+                "ext-exif": "*",
+                "ext-fileinfo": "*",
+                "ext-json": "*",
+                "illuminate/bus": "^10.2|^11.0|^12.0|^13.0",
+                "illuminate/conditionable": "^10.2|^11.0|^12.0|^13.0",
+                "illuminate/console": "^10.2|^11.0|^12.0|^13.0",
+                "illuminate/database": "^10.2|^11.0|^12.0|^13.0",
+                "illuminate/pipeline": "^10.2|^11.0|^12.0|^13.0",
+                "illuminate/support": "^10.2|^11.0|^12.0|^13.0",
+                "maennchen/zipstream-php": "^3.1",
+                "php": "^8.2",
+                "spatie/image": "^3.3.2",
+                "spatie/laravel-package-tools": "^1.16.1",
+                "spatie/temporary-directory": "^2.2",
+                "symfony/console": "^6.4.1|^7.0|^8.0"
+            },
+            "conflict": {
+                "php-ffmpeg/php-ffmpeg": "<0.6.1"
+            },
+            "require-dev": {
+                "aws/aws-sdk-php": "^3.293.10",
+                "ext-imagick": "*",
+                "ext-pdo_sqlite": "*",
+                "ext-zip": "*",
+                "guzzlehttp/guzzle": "^7.8.1",
+                "larastan/larastan": "^2.7|^3.0",
+                "league/flysystem-aws-s3-v3": "^3.22",
+                "mockery/mockery": "^1.6.7",
+                "orchestra/testbench": "^8.36|^9.15|^10.8|^11.0",
+                "pestphp/pest": "^2.36|^3.0|^4.0",
+                "phpstan/extension-installer": "^1.3.1",
+                "spatie/laravel-ray": "^1.33",
+                "spatie/pdf-to-image": "^2.2|^3.0",
+                "spatie/pest-expectations": "^1.13",
+                "spatie/pest-plugin-snapshots": "^2.1"
+            },
+            "suggest": {
+                "league/flysystem-aws-s3-v3": "Required to use AWS S3 file storage",
+                "php-ffmpeg/php-ffmpeg": "Required for generating video thumbnails",
+                "spatie/pdf-to-image": "Required for generating thumbnails of PDFs and SVGs"
+            },
+            "type": "library",
+            "extra": {
+                "laravel": {
+                    "providers": [
+                        "Spatie\\MediaLibrary\\MediaLibraryServiceProvider"
+                    ]
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Spatie\\MediaLibrary\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Freek Van der Herten",
+                    "email": "freek@spatie.be",
+                    "homepage": "https://spatie.be",
+                    "role": "Developer"
+                }
+            ],
+            "description": "Associate files with Eloquent models",
+            "homepage": "https://github.com/spatie/laravel-medialibrary",
+            "keywords": [
+                "cms",
+                "conversion",
+                "downloads",
+                "images",
+                "laravel",
+                "laravel-medialibrary",
+                "media",
+                "spatie"
+            ],
+            "support": {
+                "issues": "https://github.com/spatie/laravel-medialibrary/issues",
+                "source": "https://github.com/spatie/laravel-medialibrary/tree/11.21.0"
+            },
+            "funding": [
+                {
+                    "url": "https://spatie.be/open-source/support-us",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/spatie",
+                    "type": "github"
+                }
+            ],
+            "time": "2026-02-21T15:58:56+00:00"
+        },
         {
             "name": "spatie/laravel-package-tools",
             "version": "1.19.0",

+ 32 - 0
database/migrations/2026_04_10_064108_create_media_table.php

@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::create('media', function (Blueprint $table) {
+            $table->id();
+
+            $table->morphs('model');
+            $table->uuid()->nullable()->unique();
+            $table->string('collection_name');
+            $table->string('name');
+            $table->string('file_name');
+            $table->string('mime_type')->nullable();
+            $table->string('disk');
+            $table->string('conversions_disk')->nullable();
+            $table->unsignedBigInteger('size');
+            $table->json('manipulations');
+            $table->json('custom_properties');
+            $table->json('generated_conversions');
+            $table->json('responsive_images');
+            $table->unsignedInteger('order_column')->nullable()->index();
+
+            $table->nullableTimestamps();
+        });
+    }
+};

+ 7 - 2
packages/Longyi/Core/src/Contracts/ProductVariant.php

@@ -25,7 +25,12 @@ interface ProductVariant
     public function getEffectivePrice(?int $customerGroupId = null, ?int $channelId = null): float;
 
     /**
-     * Get images for this variant (many-to-many with product_images).
+     * Pivot relation to Media entries via product_variant_images.
      */
-    public function images();
+    public function variantImages();
+
+    /**
+     * Get all images (formatted) for this variant.
+     */
+    public function getAllImages(): \Illuminate\Support\Collection;
 }

+ 5 - 5
packages/Longyi/Core/src/Database/Migrations/2026_03_07_000001_create_product_variant_images_table.php

@@ -11,7 +11,7 @@ return new class extends Migration
         Schema::create('product_variant_images', function (Blueprint $table) {
             $table->increments('id');
             $table->unsignedInteger('product_variant_id');
-            $table->unsignedInteger('product_image_id');
+            $table->unsignedBigInteger('media_id');
             $table->integer('position')->default(0);
 
             $table->foreign('product_variant_id')
@@ -19,13 +19,13 @@ return new class extends Migration
                 ->on('product_variants')
                 ->onDelete('cascade');
 
-            $table->foreign('product_image_id')
+            $table->foreign('media_id')
                 ->references('id')
-                ->on('product_images')
+                ->on('media')
                 ->onDelete('cascade');
 
-            $table->unique(['product_variant_id', 'product_image_id'], 'variant_image_unique');
-            $table->index('product_image_id', 'idx_image_id');
+            $table->unique(['product_variant_id', 'media_id'], 'variant_media_unique');
+            $table->index('media_id', 'idx_media_id');
         });
     }
 

+ 25 - 0
packages/Longyi/Core/src/Database/Migrations/2026_03_07_000002_add_meta_to_product_images_table.php

@@ -0,0 +1,25 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::table('product_images', function (Blueprint $table) {
+            $table->string('label')->nullable()->after('position')->comment('图片 alt / label 文本');
+            $table->boolean('is_base_image')->default(false)->after('label')->comment('是否为主图');
+            $table->boolean('is_small_image')->default(false)->after('is_base_image')->comment('是否为小图');
+            $table->boolean('is_thumbnail')->default(false)->after('is_small_image')->comment('是否为缩略图');
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('product_images', function (Blueprint $table) {
+            $table->dropColumn(['label', 'is_base_image', 'is_small_image', 'is_thumbnail']);
+        });
+    }
+};

+ 105 - 0
packages/Longyi/Core/src/Database/Migrations/2026_04_16_000001_alter_product_variant_images_use_media_id.php

@@ -0,0 +1,105 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        // ── Step 1: release any FKs / indexes that may still exist ───────────
+        Schema::table('product_variant_images', function (Blueprint $table) {
+            $existing = $this->existingConstraints();
+
+            if (in_array('product_variant_images_product_image_id_foreign', $existing['fks'])) {
+                $table->dropForeign('product_variant_images_product_image_id_foreign');
+            }
+            if (in_array('product_variant_images_product_variant_id_foreign', $existing['fks'])) {
+                $table->dropForeign('product_variant_images_product_variant_id_foreign');
+            }
+            if (in_array('variant_image_unique', $existing['indexes'])) {
+                $table->dropUnique('variant_image_unique');
+            }
+            if (in_array('idx_image_id', $existing['indexes'])) {
+                $table->dropIndex('idx_image_id');
+            }
+        });
+
+        // ── Step 2: rename column if it hasn't been renamed yet ──────────────
+        if (Schema::hasColumn('product_variant_images', 'product_image_id')) {
+            Schema::table('product_variant_images', function (Blueprint $table) {
+                $table->renameColumn('product_image_id', 'media_id');
+            });
+        }
+
+        // ── Step 3: ensure media_id is bigint unsigned to match media.id ─────
+        DB::statement('ALTER TABLE product_variant_images MODIFY COLUMN media_id BIGINT UNSIGNED NOT NULL');
+
+        // ── Step 4: restore constraints ──────────────────────────────────────
+        Schema::table('product_variant_images', function (Blueprint $table) {
+            $table->foreign('product_variant_id')
+                ->references('id')
+                ->on('product_variants')
+                ->onDelete('cascade');
+
+            $table->foreign('media_id')
+                ->references('id')
+                ->on('media')
+                ->onDelete('cascade');
+
+            $table->unique(['product_variant_id', 'media_id'], 'variant_media_unique');
+            $table->index('media_id', 'idx_media_id');
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('product_variant_images', function (Blueprint $table) {
+            $table->dropForeign(['media_id']);
+            $table->dropForeign(['product_variant_id']);
+            $table->dropUnique('variant_media_unique');
+            $table->dropIndex('idx_media_id');
+        });
+
+        DB::statement('ALTER TABLE product_variant_images MODIFY COLUMN media_id INT UNSIGNED NOT NULL');
+
+        Schema::table('product_variant_images', function (Blueprint $table) {
+            $table->renameColumn('media_id', 'product_image_id');
+        });
+
+        Schema::table('product_variant_images', function (Blueprint $table) {
+            $table->foreign('product_variant_id')
+                ->references('id')
+                ->on('product_variants')
+                ->onDelete('cascade');
+
+            $table->unique(['product_variant_id', 'product_image_id'], 'variant_image_unique');
+            $table->index('product_image_id', 'idx_image_id');
+        });
+    }
+
+    private function existingConstraints(): array
+    {
+        $fks = DB::select("
+            SELECT CONSTRAINT_NAME
+            FROM information_schema.TABLE_CONSTRAINTS
+            WHERE TABLE_NAME = 'product_variant_images'
+              AND CONSTRAINT_SCHEMA = DATABASE()
+              AND CONSTRAINT_TYPE = 'FOREIGN KEY'
+        ");
+
+        $indexes = DB::select("
+            SELECT DISTINCT INDEX_NAME
+            FROM information_schema.STATISTICS
+            WHERE TABLE_NAME = 'product_variant_images'
+              AND TABLE_SCHEMA = DATABASE()
+        ");
+
+        return [
+            'fks'     => array_column($fks, 'CONSTRAINT_NAME'),
+            'indexes' => array_column($indexes, 'INDEX_NAME'),
+        ];
+    }
+};

+ 162 - 0
packages/Longyi/Core/src/Helpers/ProductImage.php

@@ -0,0 +1,162 @@
+<?php
+
+namespace Longyi\Core\Helpers;
+
+use Webkul\Product\ProductImage as BaseProductImage;
+
+/**
+ * Extended ProductImage helper.
+ *
+ * Adds support for role-based image retrieval:
+ *   - getBaseImage($product)      → image with is_base_image = true (falls back to first)
+ *   - getSmallImage($product)     → image with is_small_image = true (falls back to first)
+ *   - getThumbnailImage($product) → image with is_thumbnail = true  (falls back to first)
+ *
+ * Also overrides getProductBaseImage() to honour the is_base_image flag.
+ */
+class ProductImage extends BaseProductImage
+{
+    /**
+     * Get the image designated as the base image.
+     * Falls back to the first image if none is explicitly set.
+     *
+     * @param  \Webkul\Product\Contracts\Product  $product
+     * @return array|null
+     */
+    public function getBaseImage($product): ?array
+    {
+        if (! $product) {
+            return null;
+        }
+
+        $image = $product->images->first(fn ($img) => $img->is_base_image)
+            ?? $product->images->first();
+
+        return $image ? $this->getCachedImageUrlsPublic($image->path) : null;
+    }
+
+    /**
+     * Get the image designated as the small image.
+     * Falls back to the first image if none is explicitly set.
+     *
+     * @param  \Webkul\Product\Contracts\Product  $product
+     * @return array|null
+     */
+    public function getSmallImage($product): ?array
+    {
+        if (! $product) {
+            return null;
+        }
+
+        $image = $product->images->first(fn ($img) => $img->is_small_image)
+            ?? $product->images->first();
+
+        return $image ? $this->getCachedImageUrlsPublic($image->path) : null;
+    }
+
+    /**
+     * Get the image designated as the thumbnail.
+     * Falls back to the first image if none is explicitly set.
+     *
+     * @param  \Webkul\Product\Contracts\Product  $product
+     * @return array|null
+     */
+    public function getThumbnailImage($product): ?array
+    {
+        if (! $product) {
+            return null;
+        }
+
+        $image = $product->images->first(fn ($img) => $img->is_thumbnail)
+            ?? $product->images->first();
+
+        return $image ? $this->getCachedImageUrlsPublic($image->path) : null;
+    }
+
+    /**
+     * Override: honour is_base_image flag when loading the "base" product image.
+     *
+     * @param  \Webkul\Product\Contracts\Product  $product
+     * @param  array|null  $galleryImages
+     * @return array|null
+     */
+    public function getProductBaseImage($product, ?array $galleryImages = null)
+    {
+        if (! $product) {
+            return null;
+        }
+
+        // If gallery images are already resolved externally, use the first one (original behaviour)
+        if ($galleryImages) {
+            return $galleryImages[0];
+        }
+
+        return $this->loadBaseImageFromProduct($product);
+    }
+
+    /**
+     * Load the base image from the product, preferring is_base_image = true.
+     */
+    protected function loadBaseImageFromProduct($product): ?array
+    {
+        $images = $product?->images;
+
+        if (! $images || $images->isEmpty()) {
+            return $this->getFallbackImageUrlsPublic();
+        }
+
+        $baseImage = $images->first(fn ($img) => $img->is_base_image) ?? $images->first();
+
+        return $this->getCachedImageUrlsPublic($baseImage->path);
+    }
+
+    /**
+     * Expose the private getCachedImageUrls() from the parent via reflection
+     * by duplicating the logic here (parent method is private, not accessible).
+     */
+    protected function getCachedImageUrlsPublic(string $path): array
+    {
+        if (! $this->isDriverLocalPublic()) {
+            return [
+                'small_image_url'    => \Illuminate\Support\Facades\Storage::url($path),
+                'medium_image_url'   => \Illuminate\Support\Facades\Storage::url($path),
+                'large_image_url'    => \Illuminate\Support\Facades\Storage::url($path),
+                'original_image_url' => \Illuminate\Support\Facades\Storage::url($path),
+            ];
+        }
+
+        return [
+            'small_image_url'    => url('cache/small/'.$path),
+            'medium_image_url'   => url('cache/medium/'.$path),
+            'large_image_url'    => url('cache/large/'.$path),
+            'original_image_url' => url('cache/original/'.$path),
+        ];
+    }
+
+    protected function getFallbackImageUrlsPublic(): array
+    {
+        $smallImageUrl = core()->getConfigData('catalog.products.cache_small_image.url')
+            ? \Illuminate\Support\Facades\Storage::url(core()->getConfigData('catalog.products.cache_small_image.url'))
+            : bagisto_asset('images/small-product-placeholder.webp', 'shop');
+
+        $mediumImageUrl = core()->getConfigData('catalog.products.cache_medium_image.url')
+            ? \Illuminate\Support\Facades\Storage::url(core()->getConfigData('catalog.products.cache_medium_image.url'))
+            : bagisto_asset('images/medium-product-placeholder.webp', 'shop');
+
+        $largeImageUrl = core()->getConfigData('catalog.products.cache_large_image.url')
+            ? \Illuminate\Support\Facades\Storage::url(core()->getConfigData('catalog.products.cache_large_image.url'))
+            : bagisto_asset('images/large-product-placeholder.webp', 'shop');
+
+        return [
+            'small_image_url'    => $smallImageUrl,
+            'medium_image_url'   => $mediumImageUrl,
+            'large_image_url'    => $largeImageUrl,
+            'original_image_url' => bagisto_asset('images/large-product-placeholder.webp', 'shop'),
+        ];
+    }
+
+    protected function isDriverLocalPublic(): bool
+    {
+        return \Illuminate\Support\Facades\Storage::getAdapter() instanceof \League\Flysystem\Local\LocalFilesystemAdapter;
+    }
+}

+ 114 - 48
packages/Longyi/Core/src/Http/Controllers/Admin/FlexibleVariantController.php

@@ -4,13 +4,10 @@ namespace Longyi\Core\Http\Controllers\Admin;
 
 use Illuminate\Http\JsonResponse;
 use Illuminate\Http\Request;
-use Illuminate\Http\UploadedFile;
 use Illuminate\Routing\Controller;
+use Illuminate\Support\Collection;
 use Illuminate\Foundation\Validation\ValidatesRequests;
 use Illuminate\Support\Facades\DB;
-use Illuminate\Support\Facades\Storage;
-use Illuminate\Support\Str;
-use Intervention\Image\ImageManager;
 use Longyi\Core\Helpers\FlexibleVariantOption;
 use Longyi\Core\Models\ProductOption;
 use Longyi\Core\Models\ProductOptionValue;
@@ -18,6 +15,7 @@ use Longyi\Core\Models\ProductVariant;
 use Longyi\Core\Repositories\ProductOptionRepository;
 use Longyi\Core\Repositories\ProductOptionValueRepository;
 use Longyi\Core\Repositories\ProductVariantRepository;
+use Spatie\MediaLibrary\MediaCollections\Models\Media;
 use Webkul\Product\Helpers\Indexers\Price as PriceIndexer;
 use Webkul\Product\Repositories\ProductRepository;
 
@@ -351,7 +349,7 @@ class FlexibleVariantController extends Controller
             'variants.*.values' => 'array',
             'variants.*.values.*.code' => 'required|string',
             'variants.*.image_ids' => 'array',
-            'variants.*.image_ids.*' => 'integer|exists:product_images,id',
+            'variants.*.image_ids.*' => 'integer|exists:media,id',
         ]);
 
 
@@ -420,8 +418,8 @@ class FlexibleVariantController extends Controller
             if (isset($variantData['image_ids'])) {
                 $imageSync = collect($variantData['image_ids'])
                     ->values()
-                    ->mapWithKeys(fn ($imageId, $i) => [$imageId => ['position' => $i]]);
-                $variant->images()->sync($imageSync);
+                    ->mapWithKeys(fn ($mediaId, $i) => [$mediaId => ['position' => $i]]);
+                $variant->variantImages()->sync($imageSync);
             }
 
             $variants[$variantIndex]['variant_id'] = $variant->id;
@@ -442,13 +440,15 @@ class FlexibleVariantController extends Controller
         //删除无用的变体
         $this->_product->flexibleVariants()->whereNotIn('id', $variantIds)->delete();
         DB::commit();
+        
+        
 
         $this->reindexProduct($productId);
         return response()->json([
             'success' => true,
             'message' => 'Variants saved successfully',
             'data' => [
-                'variants' => $variants,
+                'variants' =>  $variants = $this->flexibleVariantHelper->getProductVariants($productId),
                 'selected_options' => $this->_selectedOptions,
             ],
         ]);
@@ -660,71 +660,137 @@ class FlexibleVariantController extends Controller
     /**
      * Sync images for a variant.
      *
-     * Accepts a mix of existing product_image IDs and new file uploads.
-     * New files are stored into the product gallery first, then all IDs
-     * are synced to the variant via the pivot table.
-     *
      * POST /variants/{id}/images
      * Body (multipart/form-data):
-     *   image_ids[]   - existing product_image IDs to keep/attach
-     *   uploads[]     - new image files to upload
+     *   media_ids[]  - existing Media IDs (spatie/medialibrary) to attach/keep
+     *   uploads[]    - new image files to upload (stored via medialibrary on the variant)
+     *
+     * Workflow:
+     *   1. New uploads are added to the variant's 'variant_images' media collection.
+     *   2. All resulting Media IDs (new + existing) are synced into
+     *      the product_variant_images pivot table with position order.
      */
-    public function syncVariantImages(int $id, Request $request): JsonResponse
+    public function syncVariantImages(Request $request): JsonResponse
     {
         $this->validate($request, [
-            'image_ids'   => 'array',
-            'image_ids.*' => 'integer|exists:product_images,id',
-            'uploads'     => 'array',
-            'uploads.*'   => 'image|max:5120',
+            'variant_ids'   => 'required|array',
+            'variant_ids.*' => 'required|exists:product_variants,id',
+            'file'          => 'sometimes|image|max:5120',
+            'media_ids'     => 'sometimes|array',
+            'media_ids.*'   => 'integer|exists:media,id',
         ]);
 
-        $variant = ProductVariant::findOrFail($id);
-        $product = $variant->product;
+        $variants = ProductVariant::findMany($request->input('variant_ids'));
+
+        $uploadedMedia = null;
 
-        $imageIds = collect($request->input('image_ids', []));
+        if ($request->hasFile('file')) {
+            $uploadedMedia = $variants->first()
+                ->addMedia($request->file('file'))
+                ->toMediaCollection(ProductVariant::IMAGES_COLLECTION);
 
-        if ($request->hasFile('uploads')) {
-            foreach ($request->file('uploads') as $file) {
-                $newImage = $this->storeProductImage($file, $product);
-                $imageIds->push($newImage->id);
+            foreach ($variants as $variant) {
+                $position = $variant->variantImages()->count();
+                $variant->variantImages()->attach($uploadedMedia->id, ['position' => $position]);
             }
         }
 
-        $syncData = $imageIds->values()->mapWithKeys(
-            fn ($imageId, $i) => [$imageId => ['position' => $i]]
-        );
-
-        $variant->images()->sync($syncData);
+        // Only sync explicit media_ids when the caller provides them (e.g. for reordering).
+        // Never sync an empty array implicitly — that would wipe all variant images.
+        if ($request->has('media_ids')) {
+            $mediaIds = collect($request->input('media_ids'));
+            $variants->each(fn ($variant) => $variant->variantImages()->sync($mediaIds->toArray()));
+        }
 
         return response()->json([
             'success' => true,
-            'data'    => $variant->load('images')->images,
+            'data'    => $uploadedMedia ? $this->formatMediaItem($uploadedMedia) : null,
+        ], 201);
+    }
+
+    /**
+     * Upload a new image for a variant without replacing existing ones.
+     *
+     * POST /variants/{id}/images/upload
+     */
+    public function uploadVariantImage(int $id, Request $request): JsonResponse
+    {
+        $this->validate($request, [
+            'file' => 'required|image|max:5120',
         ]);
+
+        $variant = ProductVariant::findOrFail($id);
+
+        $media = $variant
+            ->addMedia($request->file('file'))
+            ->toMediaCollection(ProductVariant::IMAGES_COLLECTION);
+
+        // Append to pivot table at the end
+        $nextPosition = $variant->variantImages()->count();
+        $variant->variantImages()->attach($media->id, ['position' => $nextPosition]);
+
+        return response()->json([
+            'success' => true,
+            'data'    => $this->formatMediaItem($media),
+        ], 201);
     }
 
     /**
-     * Store an uploaded file into the product gallery (product_images table).
+     * Remove a single media item from a variant (detach from pivot + delete media record).
+     *
+     * DELETE /variants/{id}/images/{mediaId}
      */
-    protected function storeProductImage(UploadedFile $file, $product): \Webkul\Product\Models\ProductImage
+    public function removeVariantImage(int $mediaId, Request $request): JsonResponse
     {
-        $directory = 'product/' . $product->id;
-
-        if (Str::contains($file->getMimeType(), 'image')) {
-            $manager = new ImageManager;
-            $image = $manager->make($file)->encode('webp');
-            $path = $directory . '/' . Str::random(40) . '.webp';
-            Storage::put($path, $image);
-        } else {
-            $path = $file->store($directory);
-        }
+        $this->validate($request, [
+            'variant_ids'   => 'required|array',
+            'variant_ids.*' => 'required|exists:product_variants,id'
+        ]);
+
 
-        return $product->images()->create([
-            'type'     => 'images',
-            'path'     => $path,
-            'position' => $product->images()->count(),
+        $variants = ProductVariant::findMany($request->input('variant_ids'));
+
+        $variants->each(fn ($variant) => $variant->variantImages()->detach($request->input('media_id')));
+
+
+        // Only delete the media record if no other variant still references it
+        $stillReferenced = \DB::table('product_variant_images')
+            ->where('media_id', $mediaId)
+            ->exists();
+
+        if (!$stillReferenced) {
+            Media::findOrFail($mediaId)->delete();
+        }
+        return response()->json([
+            'success' => true,
+            'message' => 'Image removed successfully',
         ]);
     }
 
+    /**
+     * Format a variant's images for API response.
+     */
+    protected function formatVariantImages($variants): Collection
+    {
+        return $variants->map(fn ($variant) => $variant->variantImages()->orderBy('position')->get()->map(fn ($media) => $this->formatMediaItem($media))->all());
+    }
+
+    /**
+     * Format a single Media item for API response.
+     */
+    protected function formatMediaItem(Media $media): array
+    {
+        return [
+            'id'           => $media->id,
+            'url'          => $media->getUrl(),
+            'thumb_url'    => $media->hasGeneratedConversion('thumb') ? $media->getUrl('thumb') : $media->getUrl(),
+            'file_name'    => $media->file_name,
+            'mime_type'    => $media->mime_type,
+            'size'         => $media->size,
+            'position'     => $media->pivot?->position,
+        ];
+    }
+
     /**
      * Trigger price reindex for a product and its flexible variants.
      */

+ 104 - 0
packages/Longyi/Core/src/Models/ProductImage.php

@@ -0,0 +1,104 @@
+<?php
+
+namespace Longyi\Core\Models;
+
+use Illuminate\Support\Facades\Storage;
+use Webkul\Product\Models\ProductImage as BaseProductImage;
+use Webkul\Product\Models\ProductProxy;
+
+/**
+ * Extended ProductImage model.
+ *
+ * Adds label, is_base_image, is_small_image, is_thumbnail columns.
+ *
+ * @property string|null $label
+ * @property bool        $is_base_image
+ * @property bool        $is_small_image
+ * @property bool        $is_thumbnail
+ */
+class ProductImage extends BaseProductImage
+{
+    /**
+     * @var array
+     */
+    protected $fillable = [
+        'type',
+        'path',
+        'product_id',
+        'position',
+        'label',
+        'is_base_image',
+        'is_small_image',
+        'is_thumbnail',
+    ];
+
+    /**
+     * @var array
+     */
+    protected $casts = [
+        'is_base_image'  => 'boolean',
+        'is_small_image' => 'boolean',
+        'is_thumbnail'   => 'boolean',
+    ];
+
+    /**
+     * @var array
+     */
+    protected $appends = ['url', 'roles'];
+
+    /**
+     * Get image roles as a plain array for easy front-end consumption.
+     *
+     * @return array<string>
+     */
+    public function getRolesAttribute(): array
+    {
+        $roles = [];
+
+        if ($this->is_base_image) {
+            $roles[] = 'base_image';
+        }
+
+        if ($this->is_small_image) {
+            $roles[] = 'small_image';
+        }
+
+        if ($this->is_thumbnail) {
+            $roles[] = 'thumbnail';
+        }
+
+        return $roles;
+    }
+
+    /**
+     * Convenience: check if this image has a specific role.
+     */
+    public function hasRole(string $role): bool
+    {
+        return in_array($role, $this->getRolesAttribute(), true);
+    }
+
+    /**
+     * Scope: filter images that are designated as base image.
+     */
+    public function scopeBaseImage($query)
+    {
+        return $query->where('is_base_image', true);
+    }
+
+    /**
+     * Scope: filter images that are designated as small image.
+     */
+    public function scopeSmallImage($query)
+    {
+        return $query->where('is_small_image', true);
+    }
+
+    /**
+     * Scope: filter images that are designated as thumbnail.
+     */
+    public function scopeThumbnail($query)
+    {
+        return $query->where('is_thumbnail', true);
+    }
+}

+ 69 - 44
packages/Longyi/Core/src/Models/ProductVariant.php

@@ -8,27 +8,32 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 use Illuminate\Database\Eloquent\Relations\MorphMany;
 use Illuminate\Database\Eloquent\SoftDeletes;
 use Longyi\Core\Contracts\ProductVariant as ProductVariantContract;
+use Spatie\MediaLibrary\HasMedia;
+use Spatie\MediaLibrary\InteractsWithMedia;
+use Spatie\MediaLibrary\MediaCollections\Models\Media;
 use Webkul\Product\Models\Product;
 use Webkul\Product\Models\ProductCustomerGroupPriceProxy;
-use Webkul\Product\Models\ProductImageProxy;
 use Webkul\Product\Models\ProductPriceIndexProxy;
 
 /**
- * @property int    $id
- * @property int    $product_id
- * @property string $sku
- * @property string $name
- * @property float  $price
- * @property float  $cost
- * @property float  $weight
- * @property int    $quantity
+ * @property int     $id
+ * @property int     $product_id
+ * @property string  $sku
+ * @property string  $name
+ * @property float   $price
+ * @property float   $cost
+ * @property float   $weight
+ * @property int     $quantity
  * @property boolean $status
- * @property array  $images
- * @property int    $sort_order
+ * @property int     $sort_order
  */
-class ProductVariant extends Model implements ProductVariantContract
+class ProductVariant extends Model implements ProductVariantContract, HasMedia
 {
     use SoftDeletes;
+    use InteractsWithMedia;
+
+    /** Collection name for variant images. */
+    const IMAGES_COLLECTION = 'variant_images';
 
     /**
      * The table associated with the model.
@@ -64,18 +69,18 @@ class ProductVariant extends Model implements ProductVariantContract
      * @var array
      */
     protected $casts = [
-        'product_id' => 'integer',
-        'price' => 'decimal:4',
-        'compare_price' => 'decimal:4',
-        'special_price' => 'decimal:4',
-        'cost' => 'decimal:4',
-        'weight' => 'decimal:4',
-        'quantity' => 'integer',
-        'status' => 'boolean',
-        'sort_order' => 'integer',
+        'product_id'        => 'integer',
+        'price'             => 'decimal:4',
+        'compare_price'     => 'decimal:4',
+        'special_price'     => 'decimal:4',
+        'cost'              => 'decimal:4',
+        'weight'            => 'decimal:4',
+        'quantity'          => 'integer',
+        'status'            => 'boolean',
+        'sort_order'        => 'integer',
         'special_price_from' => 'date',
-        'special_price_to' => 'date',
-        'deleted_at' => 'datetime',
+        'special_price_to'  => 'date',
+        'deleted_at'        => 'datetime',
     ];
 
     /**
@@ -85,15 +90,24 @@ class ProductVariant extends Model implements ProductVariantContract
     {
         parent::boot();
 
-        // When deleting a variant, detach all option values
         static::deleting(function ($variant) {
             $variant->values()->detach();
-            $variant->images()->detach();
+            $variant->variantImages()->detach();
         });
     }
 
     /**
-     * Get the parent product
+     * Register media collections.
+     * All uploaded variant images are stored in the 'variant_images' collection.
+     */
+    public function registerMediaCollections(): void
+    {
+        $this->addMediaCollection(self::IMAGES_COLLECTION)
+            ->acceptsMimeTypes(['image/jpeg', 'image/png', 'image/webp', 'image/gif']);
+    }
+
+    /**
+     * Get the parent product.
      */
     public function product(): BelongsTo
     {
@@ -107,6 +121,7 @@ class ProductVariant extends Model implements ProductVariantContract
     {
         return $this->morphMany(ProductPriceIndexProxy::modelClass(), 'priceable');
     }
+
     public function basePrices(): MorphMany
     {
         return $this->price_indices()->whereNull('customer_group_id');
@@ -121,7 +136,7 @@ class ProductVariant extends Model implements ProductVariantContract
     }
 
     /**
-     * Get all option values for this variant (many-to-many)
+     * Get all option values for this variant (many-to-many).
      */
     public function values(): BelongsToMany
     {
@@ -134,7 +149,31 @@ class ProductVariant extends Model implements ProductVariantContract
     }
 
     /**
-     * Check if variant is saleable
+     * Pivot relation: media entries linked to this variant via product_variant_images.
+     * Use this for attaching/detaching/syncing existing Media records.
+     */
+    public function variantImages(): BelongsToMany
+    {
+        return $this->belongsToMany(
+            Media::class,
+            'product_variant_images',
+            'product_variant_id',
+            'media_id'
+        )->withPivot('position')
+         ->orderByPivot('position');
+    }
+
+    /**
+     * Helper: get all images for this variant (pivot + own media collection merged).
+     * Returns pivot images first (in position order), then any directly-owned media.
+     */
+    public function getAllImages(): \Illuminate\Support\Collection
+    {
+        return $this->variantImages()->get();
+    }
+
+    /**
+     * Check if variant is saleable.
      */
     public function isSaleable(): bool
     {
@@ -192,21 +231,7 @@ class ProductVariant extends Model implements ProductVariantContract
     }
 
     /**
-     * Get the images for this variant (many-to-many with product_images).
-     */
-    public function images(): BelongsToMany
-    {
-        return $this->belongsToMany(
-            ProductImageProxy::modelClass(),
-            'product_variant_images',
-            'product_variant_id',
-            'product_image_id'
-        )->withPivot('position')
-         ->orderByPivot('position');
-    }
-
-    /**
-     * Scope to get only active variants
+     * Scope to get only active variants.
      */
     public function scopeActive($query)
     {
@@ -214,7 +239,7 @@ class ProductVariant extends Model implements ProductVariantContract
     }
 
     /**
-     * Scope to get only saleable variants
+     * Scope to get only saleable variants.
      */
     public function scopeSaleable($query)
     {

+ 20 - 0
packages/Longyi/Core/src/Providers/LongyiCoreServiceProvider.php

@@ -85,6 +85,19 @@ class LongyiCoreServiceProvider extends ServiceProvider
             \Longyi\Core\Contracts\ProductVariant::class,
             \Longyi\Core\Models\ProductVariant::class
         );
+
+        // Replace Webkul's ProductImage Model with our extended version that adds
+        // label, is_base_image, is_small_image, is_thumbnail support.
+        $this->app->bind(
+            \Webkul\Product\Contracts\ProductImage::class,
+            \Longyi\Core\Models\ProductImage::class
+        );
+
+
+        $this->app->singleton(
+            \Webkul\Product\ProductImage::class,
+            \Longyi\Core\Helpers\ProductImage::class
+        );
     }
 
     /**
@@ -103,5 +116,12 @@ class LongyiCoreServiceProvider extends ServiceProvider
         $this->app->singleton(
             \Longyi\Core\Repositories\ProductVariantRepository::class
         );
+
+        // Override Webkul's ProductImageRepository with our extended version
+        // that supports label, is_base_image, is_small_image, is_thumbnail.
+        $this->app->singleton(
+            \Webkul\Product\Repositories\ProductImageRepository::class,
+            \Longyi\Core\Repositories\ProductImageRepository::class
+        );
     }
 }

+ 128 - 0
packages/Longyi/Core/src/Repositories/ProductImageRepository.php

@@ -0,0 +1,128 @@
+<?php
+
+namespace Longyi\Core\Repositories;
+
+use Illuminate\Http\UploadedFile;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Str;
+use Intervention\Image\ImageManager;
+use Webkul\Product\Repositories\ProductImageRepository as BaseProductImageRepository;
+
+/**
+ * Extended ProductImageRepository.
+ *
+ * Overrides the Webkul upload pipeline to support the extra columns:
+ *   label, is_base_image, is_small_image, is_thumbnail.
+ *
+ * The extra data is expected inside $data['images']['meta'], keyed by
+ * the same index used in $data['images']['files']:
+ *
+ * $data['images'] = [
+ *     'files' => [
+ *         0 => <UploadedFile>,   // new upload
+ *         7 => 7,                // existing image id to keep
+ *     ],
+ *     'meta' => [
+ *         0 => ['label' => 'Front', 'is_base_image' => true, 'is_small_image' => true, 'is_thumbnail' => false],
+ *         7 => ['label' => 'Back',  'is_base_image' => false, ...],
+ *     ],
+ * ]
+ */
+class ProductImageRepository extends BaseProductImageRepository
+{
+    public function model(): string
+    {
+        return \Webkul\Product\Contracts\ProductImage::class;
+    }
+
+    /**
+     * Upload & sync product images including label / role meta.
+     */
+    public function upload($data, $product, string $uploadFileType = 'images'): void
+    {
+        $previousIds = $product->images()->pluck('id');
+
+        $position = 0;
+        $files    = $data[$uploadFileType]['files'] ?? [];
+        $meta     = $data[$uploadFileType]['meta']  ?? [];
+
+        foreach ($files as $indexOrModelId => $file) {
+            $imageMeta = $this->extractMeta($meta[$indexOrModelId] ?? []);
+
+            if ($file instanceof UploadedFile) {
+                $path = $this->storeFile($file, $product);
+
+                $this->create(array_merge([
+                    'type'       => $uploadFileType,
+                    'path'       => $path,
+                    'product_id' => $product->id,
+                    'position'   => ++$position,
+                ], $imageMeta));
+            } else {
+                // $file is an existing model ID — keep it, update position & meta
+                if (is_numeric($index = $previousIds->search($indexOrModelId))) {
+                    $previousIds->forget($index);
+                }
+
+                $this->update(array_merge(['position' => ++$position], $imageMeta), $indexOrModelId);
+            }
+        }
+
+        // Delete images that were removed
+        foreach ($previousIds as $modelId) {
+            if ($model = $this->find($modelId)) {
+                Storage::delete($model->path);
+                $this->delete($modelId);
+            }
+        }
+    }
+
+    /**
+     * Update only the meta fields (label / roles) for a single image.
+     * Does NOT change the file or position.
+     */
+    public function updateImageMeta(int $imageId, array $meta): bool
+    {
+        $image = $this->find($imageId);
+
+        if (! $image) {
+            return false;
+        }
+
+        $image->update($this->extractMeta($meta));
+
+        return true;
+    }
+
+    /**
+     * Store a file and return its path.
+     */
+    protected function storeFile(UploadedFile $file, $product): string
+    {
+        $directory = 'product/' . $product->id;
+
+        if (Str::contains($file->getMimeType(), 'image')) {
+            $manager = new ImageManager;
+            $image   = $manager->make($file)->encode('webp');
+            $path    = $directory . '/' . Str::random(40) . '.webp';
+            Storage::put($path, $image);
+
+            return $path;
+        }
+
+        return $file->store($directory);
+    }
+
+    /**
+     * Extract and normalise the meta fields from raw input.
+     */
+    protected function extractMeta(array $raw): array
+    {
+        return [
+            'label'          => $raw['label']          ?? null,
+            'is_base_image'  => ! empty($raw['is_base_image']),
+            'is_small_image' => ! empty($raw['is_small_image']),
+            'is_thumbnail'   => ! empty($raw['is_thumbnail']),
+        ];
+    }
+}

+ 1 - 1
packages/Longyi/Core/src/Repositories/ProductVariantRepository.php

@@ -30,7 +30,7 @@ class ProductVariantRepository extends Repository
     {
         $query = $this->model
             ->where('product_id', $productId)
-            ->with('values.option')
+            ->with(['values.option','variantImages'])
             ->orderBy('sort_order');
 
         if ($onlyActive) {

Разница между файлами не показана из-за своего большого размера
+ 338 - 126
packages/Longyi/Core/src/Resources/views/admin/catalog/products/edit/types/flexible_variant.blade.php


Разница между файлами не показана из-за своего большого размера
+ 191 - 0
packages/Longyi/Core/src/Resources/views/admin/catalog/products/edit/types/flexible_variant/mediaupload.blade copy.php


+ 206 - 0
packages/Longyi/Core/src/Resources/views/admin/catalog/products/edit/types/flexible_variant/mediaupload.blade.php

@@ -0,0 +1,206 @@
+<script type="text/x-template" id="flexiblevariant-uploaddialog-template">
+    <el-dialog title="Image Upload" width="660" class="flexiblevariant-uploaddialog"
+        :model-value="isShow" @update:model-value="$emit('update:isShow', $event)"
+        @closed="closeHandler"
+    >
+        <div>
+             <el-upload
+                v-model:file-list="fileList"
+                ref="uploadRef"
+                :auto-upload="false"
+                :http-request="submitUpload"
+                list-type="picture"
+                :limit="1"
+                :on-remove="removeImage"
+                :on-change="uploadChange"
+                :on-exceed="uploadExceed"
+                :before-upload="beforeUpload"
+                :before-remove="beforeRemove"
+            >
+                
+                <template #trigger>
+                    <el-button type="primary">select file</el-button>
+                </template>
+                <template #tip>
+                    <div class="el-upload__tip">
+                        <p class="text-xs">Image files with a size less than 500kb.</p>
+                        <p class="text-xs">Only one image is allowed to be uploaded.</p>
+                    </div>
+                </template>
+                
+            </el-upload>
+        </div>
+
+     
+        <template #footer>
+            <div class="dialog-footer">
+                <el-button @click="cancelHandler">Cancel</el-button>
+                <el-button type="primary" @click="confirmHandler">Upload</el-button>
+            </div>
+        </template>
+
+    </el-dialog>
+</script>
+<script type="module">
+
+        app.component('flexiblevariant-uploaddialog', {
+            template: '#flexiblevariant-uploaddialog-template',
+
+            props: {
+
+                paramData: {
+                    type: Object,
+                    default: function() {
+                        return {};
+                    }
+                },
+                isShow: {
+                    type: Boolean,
+                    default: false
+                }
+                
+            },
+            emits: ['update:isShow','confirm','cancel','delete'], // 显式声明事件(推荐)
+            data() {
+                return {
+                    fileList: [],
+                };
+            },
+            watch: {
+                'paramData.variantImages':{
+                    handler(newVal, oldVal) {
+                        if(newVal.length) {
+                            this.fileList = [{
+                                id: newVal[0].id,
+                                name: newVal[0].file_name,
+                                url: newVal[0].original_url
+                            }];
+                        } else {
+                            this.fileList = [];
+                        }
+                        
+                    },
+                    
+                }
+            },
+
+            mounted() {
+                
+            },
+
+            methods: {
+                async beforeRemove() {
+                    let res = await this.$confirm('This operation cannot be undone.',{
+                        title: 'Are you sure to delete?',
+                        showClose: false,
+                        confirmButtonText: 'Delete',
+                        cancelButtonText: 'Cancel',
+                        type: 'warning',
+                    }).then(() => true).catch(() => false);
+
+                    return res;
+                },
+                removeImage(file,fileList) {
+                    if(file.status === 'success' && file.id) {
+                        let loading = this.$loading({
+                            target: '.flexiblevariant-uploaddialog'
+                        });
+                        axios.delete(`/admin/flexible-variant/variants/images/${file.id}`, {
+                            data:{
+                                variant_ids: this.paramData.variantIds
+                            }
+                        }).then((result) => {
+                            this.$message({
+                                message: 'Delete Success',
+                                type: 'success'
+                            });
+                            this.$emit('delete', {
+                                variantIds: this.paramData.variantIds,
+                                variant_images:[]
+                            });
+                            loading.close();
+                        }).catch((error) => {
+                            this.$message({
+                                message: error.message,
+                                type: 'error'
+                            });
+                            loading.close();
+                        });
+                    }
+                },
+                uploadExceed(files) {
+                    this.$refs.uploadRef.clearFiles();
+                    const file = files[0];
+                    file.uid = this.$genFileId();
+                    this.$refs.uploadRef.handleStart(file);
+                },
+                uploadChange(addFile, fileList) {
+                    console.log(addFile.size,addFile,fileList);
+                },
+                beforeUpload(rawFile) {
+                    let res = true;
+                    if(rawFile.size > 5000000) {
+                        this.$message({
+                            message: 'Image files with a size less than 500kb.',
+                            type: 'error'
+                        });
+                        res = false;
+                    }
+                    return res;
+                },
+                submitUpload(option) {
+                    let loading = this.$loading({
+                        target: '.flexiblevariant-uploaddialog'
+                    });
+                    const formData = new FormData();
+                    formData.append('file', option.file);
+                    this.paramData.variantIds.forEach((id) => {
+                        formData.append('variant_ids[]', id);
+                    });
+                    axios.post(`/admin/flexible-variant/variants/images`, formData, {
+                        headers: { 'Content-Type': 'multipart/form-data' }
+                    }).then((result) => {
+                        /**
+                         * 1.更新fileList
+                         * 2.触发confirm事件,把新的variant数据发给父组件
+                         * 3.关闭dialog
+                        */
+                        this.fileList = [{
+                            name: result.data.data.file_name,
+                            url: result.data.data.thumb_url
+                        }];
+                        this.$emit('confirm', {
+                            variantIds: this.paramData.variantIds,
+                            variant_images:[{
+                                file_name: result.data.data.file_name,
+                                original_url: result.data.data.url,
+                                id: result.data.data.id,
+                            }]
+                        });
+                        this.$emit('update:isShow', false);
+                        loading.close();
+
+                    }).catch((error) => {
+                        this.$message({
+                            message: error.message,
+                            type: 'error'
+                        });
+                        loading.close();
+                    });
+
+                },
+
+                confirmHandler() {
+                    this.$refs.uploadRef.submit()
+                },
+                cancelHandler() {
+                    this.$emit('update:isShow', false);
+                    this.$emit('cancel', {});
+                },
+                closeHandler() {
+                    this.fileList = [];
+                }
+            },
+
+        });
+</script>

+ 3 - 1
packages/Longyi/Core/src/Routes/admin-routes.php

@@ -32,7 +32,9 @@ Route::group(['middleware' => ['admin']], function () {
             Route::post('{id}/save-variants', [FlexibleVariantController::class, 'saveVariants'])->name('admin.flexible_variant.variants.save-variants');
             Route::put('/{id}', [FlexibleVariantController::class, 'updateVariant'])->name('admin.flexible_variant.variants.update');
             Route::delete('/{id}', [FlexibleVariantController::class, 'deleteVariant'])->name('admin.flexible_variant.variants.destroy');
-            Route::post('/{id}/images', [FlexibleVariantController::class, 'syncVariantImages'])->name('admin.flexible_variant.variants.images.sync');
+            Route::post('/images', [FlexibleVariantController::class, 'syncVariantImages'])->name('admin.flexible_variant.variants.images.sync');
+            Route::post('/{id}/images/upload', [FlexibleVariantController::class, 'uploadVariantImage'])->name('admin.flexible_variant.variants.images.upload');
+            Route::delete('/images/{mediaId}', [FlexibleVariantController::class, 'removeVariantImage'])->name('admin.flexible_variant.variants.images.remove');
 
             // Bulk operations
             Route::post('/bulk/quantities', [FlexibleVariantController::class, 'bulkUpdateQuantities'])->name('admin.flexible_variant.variants.bulk.quantities');

+ 1 - 0
packages/Webkul/Admin/readme.md

@@ -46,6 +46,7 @@ v-form v-field 等参考 vee-validate 文档: https://vee-validate.logaretm.co
 php artisan config:clear
 php artisan cache:clear
 php artisan view:clear
+php artisan route:clear
 ```
 
 php artisan vendor:publish --provider="Longyi\Core\Providers\LongyiCoreServiceProvider" --force

+ 1 - 1
packages/Webkul/Admin/src/Resources/assets/css/app.css

@@ -577,7 +577,7 @@
 }
 
 .icon {
-  display: inline;
+  /* display: inline; */
   width: 1em;
   height: 1em;
   vertical-align: -0.15em;

+ 2 - 2
packages/Webkul/Admin/src/Resources/assets/js/app.js

@@ -128,7 +128,7 @@ import VeeValidate from "./plugins/vee-validate";
 import Draggable from "./plugins/draggable";
 import VueDraggablePlus from "./plugins/vuedraggableplus";
 // 引入element-plus组件库
-import ElementPlus from 'element-plus';
+import ElementPlus,{ genFileId } from 'element-plus';
 import 'element-plus/dist/index.css';
 import 'element-plus/theme-chalk/dark/css-vars.css';
 
@@ -138,7 +138,7 @@ import 'element-plus/theme-chalk/dark/css-vars.css';
 import VueCal from 'vue-cal';
 import 'vue-cal/dist/vuecal.css';
 
-
+app.config.globalProperties.$genFileId = genFileId; // 不得已只能挂在app上
 app.use(ElementPlus,{ zIndex: 12000 });
 app.component('vue-cal', VueCal);
 

+ 2 - 2
packages/Webkul/BagistoApi/src/State/LoginProcessor.php

@@ -20,7 +20,7 @@ class LoginProcessor implements ProcessorInterface
     public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
     {
         if ($data instanceof LoginInput) {
-            //if ($operation->getName() === 'create') {
+            // if ($operation->getName() === 'create') {
                 $this->validator->validateLoginInput($data);
 
                 $customer = Customer::where('email', $data->email)->first();
@@ -73,7 +73,7 @@ class LoginProcessor implements ProcessorInterface
                     'success'  => true,
                     'message'  => __('bagistoapi::app.graphql.login.successful'),
                 ];
-            //}
+            // }
         }
 
         return (object) [

Разница между файлами не показана из-за своего большого размера
+ 0 - 1
public/themes/admin/default/build/assets/app-BoBmzsCW.css


Разница между файлами не показана из-за своего большого размера
+ 36 - 36
public/themes/admin/default/build/assets/app-D8rTgrvs.js


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
public/themes/admin/default/build/assets/app-CbKA8MhO.css


Разница между файлами не показана из-за своего большого размера
+ 1 - 0
public/themes/admin/default/build/assets/app-DvtzemP6.css


+ 3 - 3
public/themes/admin/default/build/manifest.json

@@ -264,7 +264,7 @@
     "isDynamicEntry": true
   },
   "src/Resources/assets/css/app.css": {
-    "file": "assets/app-BoBmzsCW.css",
+    "file": "assets/app-DvtzemP6.css",
     "src": "src/Resources/assets/css/app.css",
     "isEntry": true
   },
@@ -450,7 +450,7 @@
     "src": "src/Resources/assets/images/unpaid-invoices.svg"
   },
   "src/Resources/assets/js/app.js": {
-    "file": "assets/app-D8rTgrvs.js",
+    "file": "assets/app-CWWlRoqL.js",
     "name": "app",
     "src": "src/Resources/assets/js/app.js",
     "isEntry": true,
@@ -501,7 +501,7 @@
       "node_modules/vue-cal/dist/drag-and-drop.es.js"
     ],
     "css": [
-      "assets/app-CbKA8MhO.css"
+      "assets/app-CexndA5t.css"
     ]
   },
   "src/Resources/assets/js/chart.js": {