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

管理后台产品编辑页变体模块前端逻辑--基本功能

fogwind 1 неделя назад
Родитель
Сommit
adef4b01f4
33 измененных файлов с 2346 добавлено и 357 удалено
  1. 84 0
      config/flexible_variant.php
  2. 5 0
      packages/Longyi/Core/README.md
  3. 1 1
      packages/Longyi/Core/src/Http/Controllers/Admin/FlexibleVariantController.php
  4. 3 0
      packages/Longyi/Core/src/Providers/LongyiCoreServiceProvider.php
  5. 2 0
      packages/Longyi/Core/src/Resources/lang/en/app.php
  6. 0 268
      packages/Longyi/Core/src/Resources/views/admin/catalog/products/edit/types/flexible-variant.blade.php
  7. 989 0
      packages/Longyi/Core/src/Resources/views/admin/catalog/products/edit/types/flexible_variant.blade.php
  8. 144 0
      packages/Longyi/Core/src/Resources/views/admin/catalog/products/edit/types/flexible_variant/edit.blade.php
  9. 31 0
      packages/Longyi/Core/src/Resources/views/components/admin/nesteddraggable.blade.php
  10. 1 0
      packages/Longyi/Core/src/Resources/views/components/test.blade.php
  11. 2 0
      packages/Webkul/Admin/package.json
  12. 51 0
      packages/Webkul/Admin/readme.md
  13. 30 2
      packages/Webkul/Admin/src/Resources/assets/css/app.css
  14. 3 0
      packages/Webkul/Admin/src/Resources/assets/css/flexible_variant.css
  15. 125 0
      packages/Webkul/Admin/src/Resources/assets/js/VueComponents/LongyiOverlay.vue
  16. 18 0
      packages/Webkul/Admin/src/Resources/assets/js/VueComponents/readme.md
  17. 13 0
      packages/Webkul/Admin/src/Resources/assets/js/app.js
  18. 2 2
      packages/Webkul/Admin/src/Resources/assets/js/plugins/vee-validate.js
  19. 10 0
      packages/Webkul/Admin/src/Resources/assets/js/plugins/vuedraggableplus.js
  20. 27 0
      packages/Webkul/Admin/src/Resources/assets/js/stores/overlayManager.js
  21. 17 5
      packages/Webkul/Admin/src/Resources/views/catalog/products/edit.blade.php
  22. 1 1
      packages/Webkul/Admin/src/Resources/views/components/media/images.blade.php
  23. 25 1
      packages/Webkul/Admin/tailwind.config.js
  24. 1 0
      packages/Webkul/Admin/vite.config.js
  25. 1 0
      public/themes/admin/default/build/assets/app-BoBmzsCW.css
  26. 0 72
      public/themes/admin/default/build/assets/app-BxN-fTAz.js
  27. 0 1
      public/themes/admin/default/build/assets/app-C2Wq9G4i.css
  28. 1 0
      public/themes/admin/default/build/assets/app-CbKA8MhO.css
  29. 113 0
      public/themes/admin/default/build/assets/app-D8rTgrvs.js
  30. 0 1
      public/themes/admin/default/build/assets/app-DxbqLJkj.css
  31. 1 0
      public/themes/admin/default/build/assets/flexible_variant-rY1JvHK4.css
  32. 8 3
      public/themes/admin/default/build/manifest.json
  33. 637 0
      resources/admin-themes/default/views/catalog/products/index.blade.php

+ 84 - 0
config/flexible_variant.php

@@ -0,0 +1,84 @@
+<?php
+
+return [
+    /*
+    |--------------------------------------------------------------------------
+    | Flexible Variant Configuration
+    |--------------------------------------------------------------------------
+    |
+    | Configuration for the Flexible Variant product type
+    |
+    */
+
+    /*
+     * Enable or disable flexible variant functionality
+     */
+    'enabled' => env('FLEXIBLE_VARIANT_ENABLED', true),
+
+    /*
+     * Default option types
+     */
+    'option_types' => [
+        'select' => 'Select Dropdown',
+        'radio' => 'Radio Buttons',
+        'checkbox' => 'Checkboxes',
+        'color' => 'Color Swatches',
+        'button' => 'Button Select',
+    ],
+
+    /*
+     * Auto-generate variant SKU based on parent SKU + option codes
+     */
+    'auto_generate_sku' => env('FLEXIBLE_VARIANT_AUTO_SKU', false),
+
+    /*
+     * SKU separator for auto-generated SKUs
+     * Example: PARENT-SKU-RED-LARGE
+     */
+    'sku_separator' => '-',
+
+    /*
+     * Default sort order for new variants
+     */
+    'default_sort_order' => 0,
+
+    /*
+     * Enable soft deletes for variants
+     */
+    'soft_delete' => true,
+
+    /*
+     * Maximum number of options per product
+     */
+    'max_options_per_product' => 10,
+
+    /*
+     * Maximum number of values per option
+     */
+    'max_values_per_option' => 50,
+
+    /*
+     * Maximum number of variants per product
+     */
+    'max_variants_per_product' => 500,
+
+    /*
+     * Default image placeholder for variants without images
+     */
+    'default_variant_image' => null,
+
+    /*
+     * Enable inventory tracking for variants
+     */
+    'track_inventory' => true,
+
+    /*
+     * Allow backorders for out-of-stock variants
+     */
+    'allow_backorders' => false,
+
+    /*
+     * Automatically disable variants when quantity reaches zero
+     */
+    'auto_disable_on_zero_stock' => false,
+];

+ 5 - 0
packages/Longyi/Core/README.md

@@ -169,3 +169,8 @@ MIT License
 ## Support
 
 For issues and questions, please open an issue on GitHub.
+
+
+## components
+
+`Resources/views/components`中是Blade组件

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

@@ -322,7 +322,7 @@ class FlexibleVariantController extends Controller
      */
     public function getVariant(int $id): JsonResponse
     {
-        $variant = $this->productVariantRepository->with('optionValues.option')->find($id);
+        $variant = $this->productVariantRepository->with('values.option')->find($id);
 
         if (!$variant) {
             return response()->json([

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

@@ -2,6 +2,7 @@
 
 namespace Longyi\Core\Providers;
 
+use Illuminate\Support\Facades\Blade;
 use Illuminate\Support\ServiceProvider;
 use Illuminate\Support\Facades\Route;
 
@@ -18,6 +19,8 @@ class LongyiCoreServiceProvider extends ServiceProvider
 
         $this->loadViewsFrom(__DIR__ . '/../Resources/views', 'longyi');
 
+        Blade::anonymousComponentPath(__DIR__.'/../Resources/views/components', 'longyi');
+
         $this->registerRoutes();
 
         $this->publishes([

+ 2 - 0
packages/Longyi/Core/src/Resources/lang/en/app.php

@@ -17,6 +17,7 @@ return [
             'is-required' => 'Required',
             'meta' => 'Meta Data',
             'values' => 'Option Values',
+            'empty' => 'No options created yet',
             
             'types' => [
                 'select' => 'Select Dropdown',
@@ -59,6 +60,7 @@ return [
             'option-values' => 'Option Values',
             'saleable' => 'Saleable',
             'not-saleable' => 'Not Saleable',
+            'empty' => 'No variants created yet',
             
             'bulk' => [
                 'update-quantities' => 'Update Quantities',

+ 0 - 268
packages/Longyi/Core/src/Resources/views/admin/catalog/products/edit/types/flexible-variant.blade.php

@@ -1,268 +0,0 @@
-@push('scripts')
-    <script type="text/x-template" id="flexible-variant-template">
-        <div class="flexible-variant-container">
-            <!-- Options Section -->
-            <div class="panel">
-                <div class="panel-header">
-                    <h3>@{{ "@lang('longyi::app.flexible-variant.options.title')" }}</h3>
-                    <button 
-                        type="button" 
-                        class="btn btn-primary btn-sm"
-                        @click="showOptionModal = true"
-                    >
-                        @{{ "@lang('longyi::app.flexible-variant.options.create')" }}
-                    </button>
-                </div>
-
-                <div class="panel-body">
-                    <div v-if="productOptions.length === 0" class="empty-state">
-                        <p>@{{ "@lang('longyi::app.flexible-variant.options.title')" }}</p>
-                    </div>
-
-                    <table v-else class="table">
-                        <thead>
-                            <tr>
-                                <th>@{{ "@lang('longyi::app.flexible-variant.options.label')" }}</th>
-                                <th>@{{ "@lang('longyi::app.flexible-variant.options.type')" }}</th>
-                                <th>@{{ "@lang('longyi::app.flexible-variant.options.values')" }}</th>
-                                <th>@{{ "@lang('longyi::app.flexible-variant.options.is-required')" }}</th>
-                                <th>Actions</th>
-                            </tr>
-                        </thead>
-                        <tbody>
-                            <tr v-for="option in productOptions" :key="option.id">
-                                <td>@{{ option.label }}</td>
-                                <td>@{{ option.type }}</td>
-                                <td>@{{ option.values.length }} values</td>
-                                <td>
-                                    <span v-if="option.pivot.is_required" class="badge badge-success">Yes</span>
-                                    <span v-else class="badge badge-secondary">No</span>
-                                </td>
-                                <td>
-                                    <button @click="editOption(option)" class="btn btn-sm btn-info">Edit</button>
-                                    <button @click="removeOption(option.id)" class="btn btn-sm btn-danger">Remove</button>
-                                </td>
-                            </tr>
-                        </tbody>
-                    </table>
-                </div>
-            </div>
-
-            <!-- Variants Section -->
-            <div class="panel mt-4">
-                <div class="panel-header">
-                    <h3>@{{ "@lang('longyi::app.flexible-variant.variants.title')" }}</h3>
-                    <button 
-                        type="button" 
-                        class="btn btn-primary btn-sm"
-                        @click="showVariantModal = true"
-                        :disabled="productOptions.length === 0"
-                    >
-                        @{{ "@lang('longyi::app.flexible-variant.variants.create')" }}
-                    </button>
-                </div>
-
-                <div class="panel-body">
-                    <div v-if="variants.length === 0" class="empty-state">
-                        <p>No variants created yet</p>
-                    </div>
-
-                    <table v-else class="table">
-                        <thead>
-                            <tr>
-                                <th>
-                                    <input type="checkbox" @change="toggleAllVariants" v-model="allVariantsSelected">
-                                </th>
-                                <th>@{{ "@lang('longyi::app.flexible-variant.variants.sku')" }}</th>
-                                <th>@{{ "@lang('longyi::app.flexible-variant.variants.name')" }}</th>
-                                <th>@{{ "@lang('longyi::app.flexible-variant.variants.price')" }}</th>
-                                <th>@{{ "@lang('longyi::app.flexible-variant.variants.quantity')" }}</th>
-                                <th>@{{ "@lang('longyi::app.flexible-variant.variants.status')" }}</th>
-                                <th>Actions</th>
-                            </tr>
-                        </thead>
-                        <tbody>
-                            <tr v-for="variant in variants" :key="variant.id">
-                                <td>
-                                    <input type="checkbox" v-model="selectedVariants" :value="variant.id">
-                                </td>
-                                <td>@{{ variant.sku }}</td>
-                                <td>@{{ variant.name || '-' }}</td>
-                                <td>@{{ formatPrice(variant.price) }}</td>
-                                <td>
-                                    <input 
-                                        type="number" 
-                                        v-model.number="variant.quantity" 
-                                        @change="updateVariantQuantity(variant)"
-                                        class="form-control form-control-sm"
-                                        style="width: 80px;"
-                                    >
-                                </td>
-                                <td>
-                                    <span v-if="variant.status" class="badge badge-success">Active</span>
-                                    <span v-else class="badge badge-secondary">Inactive</span>
-                                </td>
-                                <td>
-                                    <button @click="editVariant(variant)" class="btn btn-sm btn-info">Edit</button>
-                                    <button @click="deleteVariant(variant.id)" class="btn btn-sm btn-danger">Delete</button>
-                                </td>
-                            </tr>
-                        </tbody>
-                    </table>
-
-                    <!-- Bulk Actions -->
-                    <div v-if="selectedVariants.length > 0" class="bulk-actions mt-3">
-                        <button @click="bulkEnableVariants" class="btn btn-sm btn-success">
-                            @{{ "@lang('longyi::app.flexible-variant.variants.bulk.enable')" }}
-                        </button>
-                        <button @click="bulkDisableVariants" class="btn btn-sm btn-warning">
-                            @{{ "@lang('longyi::app.flexible-variant.variants.bulk.disable')" }}
-                        </button>
-                    </div>
-                </div>
-            </div>
-
-            <!-- Option Modal (Simplified - would need full implementation) -->
-            <div v-if="showOptionModal" class="modal">
-                <!-- Modal content for adding/editing options -->
-            </div>
-
-            <!-- Variant Modal (Simplified - would need full implementation) -->
-            <div v-if="showVariantModal" class="modal">
-                <!-- Modal content for adding/editing variants -->
-            </div>
-        </div>
-    </script>
-
-    <script type="text/javascript">
-        Vue.component('flexible-variant', {
-            template: '#flexible-variant-template',
-
-            props: ['productId'],
-
-            data() {
-                return {
-                    productOptions: [],
-                    variants: [],
-                    selectedVariants: [],
-                    allVariantsSelected: false,
-                    showOptionModal: false,
-                    showVariantModal: false,
-                    loading: false,
-                };
-            },
-
-            mounted() {
-                if (this.productId) {
-                    this.loadProductOptions();
-                    this.loadVariants();
-                }
-            },
-
-            methods: {
-                async loadProductOptions() {
-                    try {
-                        const response = await axios.get(`/admin/flexible-variant/products/${this.productId}/options`);
-                        this.productOptions = response.data.data;
-                    } catch (error) {
-                        console.error('Failed to load options:', error);
-                    }
-                },
-
-                async loadVariants() {
-                    try {
-                        const response = await axios.get(`/admin/flexible-variant/products/${this.productId}/variants`);
-                        this.variants = response.data.data;
-                    } catch (error) {
-                        console.error('Failed to load variants:', error);
-                    }
-                },
-
-                async updateVariantQuantity(variant) {
-                    try {
-                        await axios.put(`/admin/flexible-variant/variants/${variant.id}`, {
-                            quantity: variant.quantity
-                        });
-                        this.$toastr.success('Quantity updated successfully');
-                    } catch (error) {
-                        this.$toastr.error('Failed to update quantity');
-                    }
-                },
-
-                async deleteVariant(variantId) {
-                    if (!confirm('Are you sure you want to delete this variant?')) {
-                        return;
-                    }
-
-                    try {
-                        await axios.delete(`/admin/flexible-variant/variants/${variantId}`);
-                        this.loadVariants();
-                        this.$toastr.success('Variant deleted successfully');
-                    } catch (error) {
-                        this.$toastr.error('Failed to delete variant');
-                    }
-                },
-
-                async bulkEnableVariants() {
-                    try {
-                        await axios.post('/admin/flexible-variant/variants/bulk/status', {
-                            variant_ids: this.selectedVariants,
-                            status: true
-                        });
-                        this.loadVariants();
-                        this.selectedVariants = [];
-                        this.$toastr.success('Variants enabled successfully');
-                    } catch (error) {
-                        this.$toastr.error('Failed to enable variants');
-                    }
-                },
-
-                async bulkDisableVariants() {
-                    try {
-                        await axios.post('/admin/flexible-variant/variants/bulk/status', {
-                            variant_ids: this.selectedVariants,
-                            status: false
-                        });
-                        this.loadVariants();
-                        this.selectedVariants = [];
-                        this.$toastr.success('Variants disabled successfully');
-                    } catch (error) {
-                        this.$toastr.error('Failed to disable variants');
-                    }
-                },
-
-                toggleAllVariants() {
-                    if (this.allVariantsSelected) {
-                        this.selectedVariants = this.variants.map(v => v.id);
-                    } else {
-                        this.selectedVariants = [];
-                    }
-                },
-
-                formatPrice(price) {
-                    return new Intl.NumberFormat('en-US', {
-                        style: 'currency',
-                        currency: 'USD'
-                    }).format(price);
-                },
-
-                editOption(option) {
-                    // TODO: Implement option editing
-                    console.log('Edit option:', option);
-                },
-
-                removeOption(optionId) {
-                    // TODO: Implement option removal
-                    console.log('Remove option:', optionId);
-                },
-
-                editVariant(variant) {
-                    // TODO: Implement variant editing
-                    console.log('Edit variant:', variant);
-                },
-            }
-        });
-    </script>
-@endpush
-
-<flexible-variant product-id="{{ $product->id ?? '' }}"></flexible-variant>

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


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

@@ -0,0 +1,144 @@
+<script type="text/x-template" id="flexiblevariant-eidtdialog-template">
+    <el-dialog title="Variant Edit" width="500" class="flexiblevariant-eidtdialog"
+        :model-value="isShow" @update:model-value="$emit('update:isShow', $event)"
+    >
+        <el-form :model="form" ref="variantFormRef">
+            <el-form-item label="name">
+                <el-input v-model="form.name" autocomplete="off" />
+            </el-form-item>
+            <el-form-item label="SKU"
+                prop="sku"
+                :rules="[{ required: true,  message: 'sku is required', trigger: 'blur',}]"
+            >
+                <el-input v-model="form.sku" autocomplete="off" />
+            </el-form-item>
+            <el-form-item label="price"
+                prop="price"
+                :rules="[
+                    { required: true, message: 'price is required', trigger: 'blur' },
+                    { pattern: /^(0|[1-9]\d*)(\.\d{1,4})?$/, message: 'Price must be a valid decimal number.', trigger: 'blur' }
+                ]"
+            >
+                <el-input v-model="form.price" autocomplete="off" />
+            </el-form-item>
+            <el-form-item label="quantity"
+                prop="quantity"
+                :rules="[{ required: true,  message: 'quantity is required', trigger: 'blur',}]"
+            >
+                <el-input v-model="form.quantity" autocomplete="off" />
+            </el-form-item>
+            <el-form-item label="weight">
+                <el-input v-model="form.weight" autocomplete="off" />
+            </el-form-item>
+        </el-form>
+        <template #footer>
+            <div class="dialog-footer">
+                <el-button @click="cancelHandler">Cancel</el-button>
+                <el-button type="primary" @click="saveVariant">
+                Confirm
+                </el-button>
+            </div>
+        </template>
+    </el-dialog>
+</script>
+<script type="module">
+
+        app.component('flexiblevariant-eidtdialog', {
+            template: '#flexiblevariant-eidtdialog-template',
+
+            props: {
+                variantId: Number,
+                isShow: {
+                    type: Boolean,
+                    default: false
+                }
+                
+            },
+            emits: ['update:isShow'], // 显式声明事件(推荐)
+            data() {
+                return {
+                    form: {
+                        sku: '',
+                        name: '',
+                        price: 0,
+                        weight: 0,
+                        quantity: 0,
+                        // status: false,
+                    },
+    
+                };
+            },
+            watch: {
+                variantId:{
+                    handler(newVal, oldVal, onCleanup) {
+                        const controller = new AbortController();
+                        
+                        this.getVariant(newVal);
+
+                        onCleanup(() => {
+                            // 终止过期请求
+                            controller.abort();
+                        });
+                    
+                    },
+                    flush: 'post', // 不然$loading无法添加到flexiblevariant-eidtdialog上
+                }
+            },
+
+            mounted() {
+                
+            },
+
+            methods: {
+               
+                getVariant(id) {
+                    let loading = this.$loading({
+                        target: '.flexiblevariant-eidtdialog'
+                    });
+                    axios.get(`/admin/flexible-variant/variants/${id}`).then((result) => {
+                        console.log(result);
+
+                        this.form = result.data.data;
+                        loading.close();
+                    }).catch((error) => {
+                        this.$message({
+                            message: error.message,
+                            type: 'error'
+                        });
+                        loading.close();
+                    });
+                },
+                async saveVariant() {
+                    // /admin/flexible-variant/variants/{id}
+                    await this.$refs.variantFormRef.validate();
+                    let requestData = {
+                        sku: this.form.sku,
+                        name: this.form.name,
+                        price: this.form.price,
+                        weight: this.form.weight,
+                        quantity: this.form.quantity,
+                    }
+                    try {
+                        let res = await axios.put(`/admin/flexible-variant/variants/${this.variantId}`,requestData);
+                        this.$message({
+                            message: 'Save successfully.',
+                            type: 'success',
+                        });
+                        this.$emit('update:isShow', false);
+                        this.$emit('confirm', res.data.data);
+                    } catch(err) {
+                        this.$message({
+                            message: err.message,
+                            type: 'error',
+                        });
+                    }
+                    
+                },
+                cancelHandler() {
+                    this.$emit('update:isShow', false);
+                    this.$emit('cancel', {});
+                }
+            },
+
+        });
+    </script>

+ 31 - 0
packages/Longyi/Core/src/Resources/views/components/admin/nesteddraggable.blade.php

@@ -0,0 +1,31 @@
+<!--这里的代码只是备份例子,不可使用,如要使用需要参考下面代码再开发-->
+    <script type="text/x-template" id="nested-draggable-template">
+        <draggable
+            class="min-h-[20px] border rounded p-4"
+            tag="ul"
+            :list="tasks"
+            :group="{ name: 'g1' }"
+            item-key="name"
+            animation="200"
+        >
+            <template #item="{ element }">
+            <li>
+                <p>@{{ element.name }}</p>
+                <nested-draggable :tasks="element.tasks" />
+            </li>
+            </template>
+        </draggable>
+    </script>
+
+    <script type="module">
+        app.component('nested-draggable', {
+            template: '#nested-draggable-template',
+             props: {
+                tasks: {
+                    required: true,
+                    type: Array
+                }
+            },
+
+        });
+    </script>

+ 1 - 0
packages/Longyi/Core/src/Resources/views/components/test.blade.php

@@ -0,0 +1 @@
+<h1>白香亦</h1>

+ 2 - 0
packages/Webkul/Admin/package.json

@@ -22,12 +22,14 @@
     "@vee-validate/rules": "^4.9.1",
     "@vitejs/plugin-vue": "^4.2.3",
     "dotenv": "^16.4.7",
+    "element-plus": "^2.13.3",
     "flatpickr": "^4.6.13",
     "fs": "^0.0.1-security",
     "mitt": "^3.0.1",
     "playwright": "^1.48.1",
     "readline-sync": "^1.4.10",
     "vee-validate": "^4.9.1",
+    "vue-draggable-plus": "^0.6.1",
     "vue-flatpickr": "^2.3.0",
     "vuedraggable": "^4.1.0"
   }

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

@@ -0,0 +1,51 @@
+# package.json包的作用
+
+- @playwright/test 编写测试用例用于自动化测试
+- @types/node 是 Node.js 核心模块的 TypeScript 类型定义包,它的作用是为 TypeScript 项目提供 Node.js 内置 API(如 fs、path、process、http 等)的类型声明。
+- autoprefixer 给css 代码添加各个浏览器的特有前缀
+- laravel-vite-plugin 在laravel中集成vite
+- postcss css语法转换工具(你可以自己规定一些css的写法,然后通过编写postcss插件将你规定的css写法转换为浏览器可读的css代码)
+- vue-cal vue日历组件
+- @vitejs/plugin-vue  Vite 官方提供的 Vue 3 单文件组件(SFC)插件,它的主要作用是在 Vite 构建的项目中支持 .vue 文件的解析和编译。
+- dotenv Dotenv is a zero-dependency module that loads environment variables from a .env file into process.env
+- flatpickr data-time picker (bagisto使用的是这个)
+- vue-flatpickr 这个包是flatpicker的vue1版本,严重过时,不要使用。
+- fs 这不是一个包(fs这个包名被收回了),没有用
+- mitt 发布订阅模式的库
+- playwright  Playwright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API. Playwright is built to enable cross-browser web automation that is ever-green, capable, reliable, and fast.
+- readline-sync 实现通过控制台跟用户对话
+- vee-validate 是一个专为 Vue.js 应用程序设计的强大表单验证库,它让开发者能够以声明式、组件化的方式轻松实现复杂表单的实时校验、错误提示与用户交互体验优化
+- vuedraggable vue 拖拽组件 在npmjs中vue2版本和vue3版本公用一个包名,如果要安装vue3版本需要执行npm install vuedraggable@next。
+- vue-draggable-plus 是另外一个vue拖拽组件,作者是vue团队的成员,比较新,也是基于SortableJS。文档:https://vue-draggable-plus.pages.dev/guide/
+> vuedraggable是基于SortableJS的,有些props文档中没有列出,需要参考SortableJS的文档,比如group,ghost-class属性
+
+# Blade模板文件中的语法
+例子:
+```html
+<script type="text/x-template" id="v-configuration-search-template">
+    <h3 class="text-base font-semibold text-gray-800 dark:text-white">
+        @{{ "@lang('longyi::app.flexible-variant.options.title')" }}
+    </h3>
+</script>
+```
+`@`符号告诉Blade模板引擎这个`{{}}`是JavaScript框架的模板语法
+
+
+v-form v-field 等参考 vee-validate 文档: https://vee-validate.logaretm.com/
+
+
+## 组件库和图标
+组件库使用element-plus
+
+图标在这个网站找: https://www.xicons.org/#/
+
+
+### Clear Cache
+
+```bash
+php artisan config:clear
+php artisan cache:clear
+php artisan view:clear
+```
+
+php artisan vendor:publish --provider="Longyi\Core\Providers\LongyiCoreServiceProvider" --force

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

@@ -9,7 +9,7 @@
     font-style: normal;
     font-display: block;
 }
-
+/**@layer 声明了一个层叠层,同一层内的规则将级联在一起,这给予了开发者对层叠机制的更多控制。*/
 @layer components {
     ::selection {
         background-color: rgba(0, 68, 242, .2);
@@ -404,7 +404,18 @@
     .secondary-button {
         @apply flex cursor-pointer place-content-center items-center gap-x-1 whitespace-nowrap rounded-md border-2 border-blue-600 bg-white px-3 py-1.5 font-semibold text-blue-600 transition-all hover:bg-[#eff6ff61] focus:bg-[#eff6ff61] dark:border-gray-400 dark:bg-gray-800 dark:text-white dark:hover:opacity-80;
     }
-
+    .middle-button{
+        @apply flex cursor-pointer place-content-center items-center gap-x-1 whitespace-nowrap rounded-md border-2 border-blue-600 bg-white px-2 py-1 font-normal text-blue-600 text-sm transition-all hover:bg-[#eff6ff61] focus:bg-[#eff6ff61] dark:border-gray-400 dark:bg-gray-800 dark:text-white dark:hover:opacity-80;
+    }
+    .small-white-button{
+        @apply flex cursor-pointer place-content-center items-center gap-x-1 whitespace-nowrap rounded-md border border-gray-600 bg-white px-2 py-1 font-normal  text-12 transition-all hover:bg-[#eff6ff61] focus:bg-[#eff6ff61];
+    }
+    .small-blue-button{
+        @apply flex cursor-pointer place-content-center items-center gap-x-1 whitespace-nowrap rounded-md border border-blue-700 bg-blue-600 px-2 py-1 font-normal text-white text-12 transition-all focus:opacity-[0.9] hover:opacity-[0.9];
+    }
+    .small-red-button{
+        @apply flex cursor-pointer place-content-center items-center gap-x-1 whitespace-nowrap rounded-md border border-red-500 bg-red-600 px-2 py-1 font-normal text-white text-12 transition-all focus:opacity-[0.9] hover:opacity-[0.9];
+    }
     .transparent-button {
         @apply flex cursor-pointer appearance-none place-content-center items-center gap-x-1 whitespace-nowrap rounded-md border-2 border-transparent px-3 py-1.5 font-semibold text-gray-600 transition-all marker:shadow hover:bg-gray-100 focus:bg-gray-100 dark:hover:bg-gray-950;
     }
@@ -541,6 +552,13 @@
     .CodeMirror {
         @apply !h-[calc(100vh-367px)]
     }
+
+    .admin-common-input{
+        @apply w-full rounded-md border px-3 py-2.5 text-sm text-gray-600 transition-all hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400
+    }
+    .admin-common-input.error{
+        @apply border !border-red-600 hover:border-red-600
+    }
 }
 
 .tox .tox-toolbar__group:last-child button {
@@ -556,4 +574,14 @@
 
 .tox .tox-toolbar__group:last-child button[aria-disabled="true"] {
     @apply cursor-not-allowed opacity-50;
+}
+
+.icon {
+  display: inline;
+  width: 1em;
+  height: 1em;
+  vertical-align: -0.15em;
+  fill: currentColor;
+  overflow: hidden;
+  stroke: currentColor;
 }

+ 3 - 0
packages/Webkul/Admin/src/Resources/assets/css/flexible_variant.css

@@ -0,0 +1,3 @@
+.test-flexible-h1{
+    color: #1248a6;
+}

+ 125 - 0
packages/Webkul/Admin/src/Resources/assets/js/VueComponents/LongyiOverlay.vue

@@ -0,0 +1,125 @@
+<template>
+    <transition
+        name="drawer-overlay"
+        enter-from-class="opacity-0"
+        enter-to-class="opacity-100"
+        leave-from-class="opacity-100"
+        leave-to-class="opacity-0"
+    >
+        <div @click.stop="overlayClick"
+            class="fixed inset-0 bg-gray-500 bg-opacity-50 transition-opacity w-full h-full"
+            :style="{ 'z-index': zIndex }"
+            v-show="isActive"
+        >
+            <!-- Content -->
+            <transition
+                name="drawer"
+                :enter-from-class="enterFromLeaveToClasses"
+                enter-active-class="transform transition duration-200 ease-in-out"
+                
+                leave-active-class="transform transition duration-200 ease-in-out"
+                :leave-to-class="enterFromLeaveToClasses"
+            >
+                <div @click.stop="() => {}"
+                    class="fixed"
+                    :class="[{
+                        'inset-x-0 top-0': position == 'top',
+                        'inset-x-0 bottom-0': position == 'bottom',
+                        'inset-y-0 ltr:right-0 rtl:left-0': position == 'right',
+                        'inset-y-0 ltr:left-0 rtl:right-0': position == 'left',
+                        'top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2': position == 'center',
+                    }, customClass]"
+                    
+                    v-if="isActive"
+                >
+                   
+                    <slot></slot>
+                    
+                    
+                </div>
+            </transition>
+
+        
+        </div>
+    </transition>
+
+    
+
+</template>
+
+<script>
+import { overlayManager } from "../stores/overlayManager.js";
+export default {
+    name: 'LongyiOverlay',
+    props: {
+        id: String,
+        position: {
+            type: String,
+            default: 'center',
+        },
+        isActive: {
+            type: Boolean,
+            default: false,
+        },
+        isOverlayClose: {
+            type: Boolean,
+            default: false,
+        },
+        customClass: {
+            type: String,
+            default: '',
+        }
+        
+    },
+
+    data() {
+        return {
+            zIndex: 0,
+            
+        };
+    },
+    mounted() {
+        this.zIndex = overlayManager.register(this.id);
+    },
+    watch: {
+        isActive: function(newVal, oldVal) {
+            if(newVal) {
+                document.body.style.overflow = 'hidden';
+            } else {
+                document.body.style.overflow ='auto';
+
+            }
+        },
+
+    },
+
+    computed: {
+        enterFromLeaveToClasses() {
+            if (this.position == 'top') {
+                return '-translate-y-full';
+            } else if (this.position == 'bottom') {
+                return 'translate-y-full';
+            } else if (this.position == 'left') {
+                return 'ltr:-translate-x-full rtl:translate-x-full';
+            } else if (this.position == 'right') {
+                return 'ltr:translate-x-full rtl:-translate-x-full';
+            } else if (this.position == 'center') {
+                return 'scale-95 opacity-0';
+            }
+        }
+    },
+
+    methods: {
+        overlayClick() {
+            if(this.isOverlayClose) {
+                this.close();
+            }
+        },
+        close() {
+        
+            this.$emit('close', { isActive: this.isActive });
+        }
+    },
+}
+</script>
+

+ 18 - 0
packages/Webkul/Admin/src/Resources/assets/js/VueComponents/readme.md

@@ -0,0 +1,18 @@
+VueComponents文件夹中的组件是注册到全局的通用组件,属于前端的vue组件,任何页面都可以使用。
+
+vue组件的大驼峰写法(比如:`<LongyiOverlay></LongyiOverlay>`)仅在SFC中有效。在blade模板中要使用短横线写法:`<longyi-overlay></longyi-overlay>`
+
+Admin/src/Resources/views/components 中的组件是Blade模板引擎的组件,是通过laravel注册的,属于服务端页面模板组件。
+
+LongyiOverlay 组件使用例子:
+```html
+<longyi-overlay id="optionmodal" :isActive="showOptionModal" @close="toggleOptionModal(false)">
+    <div class="w-1/3 h-1/3 bg-orange-300 overflow-y-auto">
+        <p class="text-xl">Modal content for adding/editing variants</p>
+        <p class="text-xl">Modal content for adding/editing variants</p>
+    </div>
+</longyi-overlay>
+```
+- 需要在app.js中将LongyiOverlay注册为全局组件。引入element-plus后,用不上LongyiOverlay了
+
+v-model的原理: https://cn.vuejs.org/guide/components/v-model.html

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

@@ -126,11 +126,23 @@ import Emitter from "./plugins/emitter";
 import Flatpickr from "./plugins/flatpickr";
 import VeeValidate from "./plugins/vee-validate";
 import Draggable from "./plugins/draggable";
+import VueDraggablePlus from "./plugins/vuedraggableplus";
+// 引入element-plus组件库
+import ElementPlus from 'element-plus';
+import 'element-plus/dist/index.css';
+import 'element-plus/theme-chalk/dark/css-vars.css';
+
+/**
+ * Global components registration.
+ */
 import VueCal from 'vue-cal';
 import 'vue-cal/dist/vuecal.css';
 
+
+app.use(ElementPlus,{ zIndex: 12000 });
 app.component('vue-cal', VueCal);
 
+
 [
     Admin,
     Axios,
@@ -139,6 +151,7 @@ app.component('vue-cal', VueCal);
     Flatpickr,
     VeeValidate,
     Draggable,
+    VueDraggablePlus,
 ].forEach((plugin) => app.use(plugin));
 
 /**

+ 2 - 2
packages/Webkul/Admin/src/Resources/assets/js/plugins/vee-validate.js

@@ -25,7 +25,7 @@ import sin from "../../locales/sin.json";
 import tr from "@vee-validate/i18n/dist/locale/tr.json";
 import uk from "@vee-validate/i18n/dist/locale/uk.json";
 import zh_CN from "@vee-validate/i18n/dist/locale/zh_CN.json";
-import { all } from '@vee-validate/rules';
+import { all } from '@vee-validate/rules'; // 全局验证器规则包 https://vee-validate.logaretm.com/v4/guide/global-validators#vee-validaterules
 
 window.defineRule = defineRule;
 
@@ -43,7 +43,7 @@ export default {
         /**
          * Registration of all global validators.
          */
-        Object.entries(all).forEach(([name, rule]) => defineRule(name, rule));
+        Object.entries(all).forEach(([name, rule]) => defineRule(name, rule)); // 注册全局验证器
 
         /**
          * This regular expression allows phone numbers with the following conditions:

+ 10 - 0
packages/Webkul/Admin/src/Resources/assets/js/plugins/vuedraggableplus.js

@@ -0,0 +1,10 @@
+import {VueDraggable as VueDraggablePlus} from 'vue-draggable-plus';
+
+export default {
+    install: (app) => {
+        /**
+         * Global component registration;
+         */
+        app.component("VueDraggablePlus", VueDraggablePlus);
+    },
+};

+ 27 - 0
packages/Webkul/Admin/src/Resources/assets/js/stores/overlayManager.js

@@ -0,0 +1,27 @@
+import { reactive, readonly } from 'vue'
+
+const state = reactive({
+  maxZIndex: 20000,
+  stack: {},
+  STEP: 10
+})
+
+export const overlayManager = {
+  // 只读状态,防止外部直接修改
+  state: readonly(state),
+
+  register(id) {
+    // 当栈空时可重置计数器(可选)
+    if (Object.keys(state.stack).length === 0) {
+      state.maxZIndex = 20000;
+    }
+    const zIndex = state.maxZIndex;
+    state.maxZIndex += state.STEP;
+    state.stack[id.toString()] = zIndex;
+    return zIndex;
+  },
+
+  unregister(id) {
+    delete state.stack[id.toString()];
+  }
+}

+ 17 - 5
packages/Webkul/Admin/src/Resources/views/catalog/products/edit.blade.php

@@ -148,12 +148,15 @@
 
                 $isSingleColumn = $groupedColumns->count() !== 2;
             @endphp
-
+            <!--
+                添加overflow-auto为了让flexible_cariant.blade.php中的el-table 组件能随窗口宽度的改变而自动改变宽度
+                https://juejin.cn/post/7274839871277482038
+            -->
             @foreach ($groupedColumns as $column => $groups)
 
                 {!! view_render_event("bagisto.admin.catalog.product.edit.form.column_{$column}.before", ['product' => $product]) !!}
 
-                <div class="flex flex-col gap-2 {{ $column == 1 ? 'flex-1 max-xl:flex-auto' : 'w-[360px] max-w-full max-sm:w-full' }}">
+                <div class="flex flex-col gap-2 {{ $column == 1 ? 'flex-1 max-xl:flex-auto overflow-auto' : 'w-[360px] max-w-full max-sm:w-full' }}">
                     @foreach ($groups as $group)
                         @php $customAttributes = $product->getEditableAttributes($group); @endphp
 
@@ -238,9 +241,14 @@
                         <!-- Videos View Blade File -->
                         @include('admin::catalog.products.edit.videos')
 
+                        <h1>1111111111111111111--start @php echo $product->type; @endphp</h1>
                         <!-- Product Type View Blade File -->
-                        @includeIf('admin::catalog.products.edit.types.' . $product->type)
-
+                        @if ($product->type === 'flexible_variant')
+                            @includeIf('longyi::admin.catalog.products.edit.types.' . $product->type)    
+                        @else
+                            @includeIf('admin::catalog.products.edit.types.' . $product->type)
+                        @endif
+                        <h1>1111111111111111111--end</h1>
                         <!-- Related, Cross Sells, Up Sells View Blade File -->
                         @include('admin::catalog.products.edit.links')
 
@@ -267,7 +275,11 @@
                             @include('admin::catalog.products.edit.videos')
 
                             <!-- Product Type View Blade File -->
-                            @includeIf('admin::catalog.products.edit.types.' . $product->type)
+                            @if ($product->type === 'flexible_variant')
+                                @includeIf('longyi::admin.catalog.products.edit.types.' . $product->type)    
+                            @else
+                                @includeIf('admin::catalog.products.edit.types.' . $product->type)
+                            @endif
 
                             <!-- Related, Cross Sells, Up Sells View Blade File -->
                             @include('admin::catalog.products.edit.links')

+ 1 - 1
packages/Webkul/Admin/src/Resources/views/components/media/images.blade.php

@@ -63,7 +63,7 @@
                                 @lang('admin::app.components.media.images.add-image-btn')
                                 
                                 <span class="text-xs">
-                                    @lang('admin::app.components.media.images.allowed-types')
+                                    @lang('admin::app.components.media.images.allowed-types')45678
                                 </span>
                             </p>
 

+ 25 - 1
packages/Webkul/Admin/tailwind.config.js

@@ -1,6 +1,12 @@
 /** @type {import('tailwindcss').Config} */
 module.exports = {
-    content: ["./src/Resources/**/*.blade.php", "./src/Resources/**/*.js"],
+    // 哪些文件中包含tailwind的类名,tailwind会扫描这些文件来生成对应的CSS
+    content: [
+        "./src/Resources/**/*.blade.php", 
+        "./src/Resources/**/*.js", 
+        "./src/Resources/**/*.vue", 
+        "../../Longyi/Core/src/Resources/**/*.blade.php"
+    ],
 
     theme: {
         container: {
@@ -33,6 +39,24 @@ module.exports = {
             fontFamily: {
                 inter: ['Inter'],
                 icon: ['icomoon']
+            },
+
+            fontSize: {
+                '12': ['0.75rem'], 
+                '14': ['0.875rem'], 
+                '16': ['1rem'],
+                '18': ['1.125rem'],
+                '20': ['1.25rem'],
+                '22': ['1.375rem'],
+                '24': ['1.5rem'],
+                '26': ['1.625rem'],
+                '28': ['1.75rem'],
+                '30': ['1.875rem'],
+                '32': ['2rem'],
+                '34': ['2.125rem'],
+                '36': ['2.25rem'],
+                '38': ['2.375rem'],
+                '40': ['2.5rem'],
             }
         },
     },

+ 1 - 0
packages/Webkul/Admin/vite.config.js

@@ -30,6 +30,7 @@ export default defineConfig(({ mode }) => {
                 buildDirectory: "themes/admin/default/build",
                 input: [
                     "src/Resources/assets/css/app.css",
+                    "src/Resources/assets/css/flexible_variant.css",
                     "src/Resources/assets/js/app.js",
                     "src/Resources/assets/js/chart.js",
                 ],

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


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


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


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


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


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


+ 1 - 0
public/themes/admin/default/build/assets/flexible_variant-rY1JvHK4.css

@@ -0,0 +1 @@
+.test-flexible-h1{color:#1248a6}

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

@@ -264,10 +264,15 @@
     "isDynamicEntry": true
   },
   "src/Resources/assets/css/app.css": {
-    "file": "assets/app-DxbqLJkj.css",
+    "file": "assets/app-BoBmzsCW.css",
     "src": "src/Resources/assets/css/app.css",
     "isEntry": true
   },
+  "src/Resources/assets/css/flexible_variant.css": {
+    "file": "assets/flexible_variant-rY1JvHK4.css",
+    "src": "src/Resources/assets/css/flexible_variant.css",
+    "isEntry": true
+  },
   "src/Resources/assets/fonts/bagisto-admin.woff": {
     "file": "assets/bagisto-admin-BzOkv6lg.woff",
     "src": "src/Resources/assets/fonts/bagisto-admin.woff"
@@ -445,7 +450,7 @@
     "src": "src/Resources/assets/images/unpaid-invoices.svg"
   },
   "src/Resources/assets/js/app.js": {
-    "file": "assets/app-BxN-fTAz.js",
+    "file": "assets/app-D8rTgrvs.js",
     "name": "app",
     "src": "src/Resources/assets/js/app.js",
     "isEntry": true,
@@ -496,7 +501,7 @@
       "node_modules/vue-cal/dist/drag-and-drop.es.js"
     ],
     "css": [
-      "assets/app-C2Wq9G4i.css"
+      "assets/app-CbKA8MhO.css"
     ]
   },
   "src/Resources/assets/js/chart.js": {

+ 637 - 0
resources/admin-themes/default/views/catalog/products/index.blade.php

@@ -0,0 +1,637 @@
+<x-admin::layouts>
+    <x-slot:title>
+        @lang('admin::app.catalog.products.index.title')
+    </x-slot>
+
+    <div class="flex items-center justify-between gap-4 max-sm:flex-wrap">
+        <p class="text-xl font-bold text-gray-800 dark:text-white">
+            @lang('admin::app.catalog.products.index.title') 八服赤眉
+        </p>
+        <x-longyi::test />
+        <div class="flex items-center gap-x-2.5">
+            <!-- Export Modal -->
+            <x-admin::datagrid.export :src="route('admin.catalog.products.index')" />
+
+            {!! view_render_event('bagisto.admin.catalog.products.create.before') !!}
+
+            @if (bouncer()->hasPermission('catalog.products.create'))
+                <v-create-product-form>
+                    <button
+                        type="button"
+                        class="primary-button"
+                    >
+                        @lang('admin::app.catalog.products.index.create-btn')
+                    </button>
+                </v-create-product-form>
+            @endif
+
+            {!! view_render_event('bagisto.admin.catalog.products.create.after') !!}
+        </div>
+    </div>
+
+    {!! view_render_event('bagisto.admin.catalog.products.list.before') !!}
+
+    <!-- Datagrid -->
+    <x-admin::datagrid
+        :src="route('admin.catalog.products.index')"
+        :isMultiRow="true"
+    >
+        <!-- Datagrid Header -->
+        @php
+            $hasPermission = bouncer()->hasPermission('catalog.products.edit') || bouncer()->hasPermission('catalog.products.delete');
+        @endphp
+
+        <template #header="{
+            isLoading,
+            available,
+            applied,
+            selectAll,
+            sort,
+            performAction
+        }">
+            <template v-if="isLoading">
+                <x-admin::shimmer.datagrid.table.head :isMultiRow="true" />
+            </template>
+
+            <template v-else>
+                <div class="row grid gap-2 md:grid-cols-[2fr_1fr_1fr] grid-rows-1 items-center border-b px-4 py-2.5 dark:border-gray-800">
+                    <div
+                        class="flex select-none items-center gap-2.5"
+                        v-for="(columnGroup, index) in [['name', 'sku', 'attribute_family'], ['base_image', 'price', 'quantity', 'product_id'], ['status', 'category_name', 'type']]"
+                    >
+                        @if ($hasPermission)
+                            <label
+                                class="flex w-max cursor-pointer select-none items-center gap-1"
+                                for="mass_action_select_all_records"
+                                v-if="! index"
+                            >
+                                <input
+                                    type="checkbox"
+                                    name="mass_action_select_all_records"
+                                    id="mass_action_select_all_records"
+                                    class="peer hidden"
+                                    :checked="['all', 'partial'].includes(applied.massActions.meta.mode)"
+                                    @change="selectAll"
+                                >
+
+                                <span
+                                    class="icon-uncheckbox cursor-pointer rounded-md text-2xl"
+                                    :class="[
+                                        applied.massActions.meta.mode === 'all' ? 'peer-checked:icon-checked peer-checked:text-blue-600' : (
+                                            applied.massActions.meta.mode === 'partial' ? 'peer-checked:icon-checkbox-partial peer-checked:text-blue-600' : ''
+                                        ),
+                                    ]"
+                                >
+                                </span>
+                            </label>
+                        @endif
+
+                        <p class="text-gray-600 dark:text-gray-300">
+                            <span class="[&>*]:after:content-['_/_']">
+                                <template v-for="column in columnGroup">
+                                    <span
+                                        class="after:content-['/'] last:after:content-['']"
+                                        :class="{
+                                            'font-medium text-gray-800 dark:text-white': applied.sort.column == column,
+                                            'cursor-pointer hover:text-gray-800 dark:hover:text-white': available.columns.find(columnTemp => columnTemp.index === column)?.sortable,
+                                        }"
+                                        @click="
+                                            available.columns.find(columnTemp => columnTemp.index === column)?.sortable ? sort(available.columns.find(columnTemp => columnTemp.index === column)): {}
+                                        "
+                                    >
+                                        @{{ available.columns.find(columnTemp => columnTemp.index === column)?.label }}
+                                    </span>
+                                </template>
+                            </span>
+
+                            <i
+                                class="align-text-bottom text-base text-gray-800 dark:text-white ltr:ml-1.5 rtl:mr-1.5"
+                                :class="[applied.sort.order === 'asc' ? 'icon-down-stat': 'icon-up-stat']"
+                                v-if="columnGroup.includes(applied.sort.column)"
+                            ></i>
+                        </p>
+                    </div>
+                </div>
+            </template>
+        </template>
+
+        <template #body="{
+            isLoading,
+            available,
+            applied,
+            selectAll,
+            sort,
+            performAction
+        }">
+            <template v-if="isLoading">
+                <x-admin::shimmer.datagrid.table.body :isMultiRow="true" />
+            </template>
+
+            <template v-else>
+                <div
+                    class="row border-b px-2 py-2.5 transition-all hover:bg-gray-50 dark:border-gray-800 dark:hover:bg-gray-950 sm:px-4 md:grid md:grid-cols-[2fr_1fr_1fr] md:grid-rows-1 md:gap-1.5"
+                    v-for="record in available.records"
+                >
+                    <!-- Mobile Layout -->
+                    <div class="block space-y-3 md:hidden">
+                        <!-- Header Row with Checkbox, Name and Actions -->
+                        <div class="flex items-start justify-between gap-3">
+                            <div class="flex items-start gap-2.5">
+                                @if ($hasPermission)
+                                    <input
+                                        type="checkbox"
+                                        :name="`mass_action_select_record_${record.product_id}`"
+                                        :id="`mass_action_select_record_${record.product_id}`"
+                                        :value="record.product_id"
+                                        class="peer hidden"
+                                        v-model="applied.massActions.indices"
+                                    >
+
+                                    <label
+                                        class="icon-uncheckbox peer-checked:icon-checked cursor-pointer rounded-md text-2xl peer-checked:text-blue-600"
+                                        :for="`mass_action_select_record_${record.product_id}`"
+                                    ></label>
+                                @endif
+
+                                <div class="relative flex-shrink-0">
+                                    <template v-if="record.base_image">
+                                        <img
+                                            class="h-12 w-12 rounded object-cover sm:h-16 sm:w-16"
+                                            :src='record.base_image'
+                                        />
+
+                                        <span class="absolute -bottom-1 -right-1 rounded-full bg-darkPink px-1 text-xs font-bold leading-normal text-white">
+                                            @{{ record.images_count }}
+                                        </span>
+                                    </template>
+
+                                    <template v-else>
+                                        <div class="relative h-12 w-12 rounded border border-dashed border-gray-300 dark:border-gray-800 dark:mix-blend-exclusion dark:invert sm:h-16 sm:w-16">
+                                            <img src="{{ bagisto_asset('images/product-placeholders/front.svg')}}" class="h-full w-full object-cover">
+
+                                            <p class="absolute bottom-0 w-full text-center text-[6px] font-semibold text-gray-400">
+                                                @lang('admin::app.catalog.products.index.datagrid.product-image')
+                                            </p>
+                                        </div>
+                                    </template>
+                                </div>
+
+                                <div class="flex flex-col gap-1 flex-1">
+                                    <p class="break-all text-sm font-semibold text-gray-800 dark:text-white sm:text-base">
+                                        @{{ record.name }}
+                                    </p>
+
+                                    <p class="text-xs text-gray-600 dark:text-gray-300 sm:text-sm">
+                                        @{{ "@lang('admin::app.catalog.products.index.datagrid.id-value')".replace(':id', record.product_id) }}
+                                    </p>
+
+                                    <p class="text-xs text-gray-600 dark:text-gray-300 sm:text-sm">
+                                        @{{ "@lang('admin::app.catalog.products.index.datagrid.sku-value')".replace(':sku', record.sku) }}
+                                    </p>
+
+                                    <p class="text-xs text-gray-600 dark:text-gray-300 sm:text-sm">
+                                        @{{ "@lang('admin::app.catalog.products.index.datagrid.attribute-family-value')".replace(':attribute_family', record.attribute_family) }}
+                                    </p>
+
+                                    <p class="text-xs text-gray-600 dark:text-gray-300 sm:text-sm">
+                                        @{{ record.category_name ?? 'N/A' }}
+                                    </p>
+
+                                    <p class="text-xs text-gray-600 dark:text-gray-300 sm:text-sm">
+                                        @{{ record.type }}
+                                    </p>
+
+                                    <p class="text-sm font-semibold text-gray-800 dark:text-white sm:text-base">
+                                        @{{ $admin.formatPrice(record.price) }}
+                                    </p>
+
+                                    <div>
+                                        <div v-if="['configurable', 'bundle', 'grouped' , 'booking'].includes(record.type)">
+                                            <p class="text-xs text-gray-600 dark:text-gray-300 sm:text-sm">
+                                                <span class="text-red-600">N/A</span>
+                                            </p>
+                                        </div>
+
+                                        <div v-else>
+                                            <p
+                                                class="text-xs text-gray-600 dark:text-gray-300 sm:text-sm"
+                                                v-if="record.quantity > 0"
+                                            >
+                                                <span class="text-green-600">
+                                                    @{{ "@lang('admin::app.catalog.products.index.datagrid.qty-value')".replace(':qty', record.quantity) }}
+                                                </span>
+                                            </p>
+
+                                            <p
+                                                class="text-xs text-gray-600 dark:text-gray-300 sm:text-sm"
+                                                v-else
+                                            >
+                                                <span class="text-red-600">
+                                                    @lang('admin::app.catalog.products.index.datagrid.out-of-stock')
+                                                </span>
+                                            </p>
+                                        </div>
+                                    </div>
+
+                                    <p :class="[record.status ? 'label-active': 'label-info']">
+                                        @{{ record.status ? "@lang('admin::app.catalog.products.index.datagrid.active')" : "@lang('admin::app.catalog.products.index.datagrid.disable')" }}
+                                    </p>
+                                </div>
+                            </div>
+
+                            <div class="flex items-center gap-1">
+                                <span
+                                    class="cursor-pointer rounded-md p-1.5 text-xl transition-all hover:bg-gray-200 dark:hover:bg-gray-800"
+                                    :class="action.icon"
+                                    v-text="! action.icon ? action.title : ''"
+                                    v-for="action in record.actions"
+                                    @click="performAction(action)"
+                                >
+                                </span>
+                            </div>
+                        </div>
+                    </div>
+
+                    <!-- Desktop Layout (Hidden on Mobile) -->
+                    <div class="hidden md:contents">
+                        <!-- Name, SKU, Attribute Family Columns -->
+                        <div class="flex gap-2.5">
+                            @if ($hasPermission)
+                                <input
+                                    type="checkbox"
+                                    :name="`mass_action_select_record_${record.product_id}`"
+                                    :id="`mass_action_select_record_${record.product_id}`"
+                                    :value="record.product_id"
+                                    class="peer hidden"
+                                    v-model="applied.massActions.indices"
+                                >
+
+                                <label
+                                    class="icon-uncheckbox peer-checked:icon-checked cursor-pointer rounded-md text-2xl peer-checked:text-blue-600"
+                                    :for="`mass_action_select_record_${record.product_id}`"
+                                ></label>
+                            @endif
+
+                            <div class="flex flex-col gap-1.5">
+                                <p class="break-all text-base font-semibold text-gray-800 dark:text-white">
+                                    @{{ record.name }}
+                                </p>
+
+                                <p class="text-gray-600 dark:text-gray-300">
+                                    @{{ "@lang('admin::app.catalog.products.index.datagrid.sku-value')".replace(':sku', record.sku) }}
+                                </p>
+
+                                <p class="text-gray-600 dark:text-gray-300">
+                                    @{{ "@lang('admin::app.catalog.products.index.datagrid.attribute-family-value')".replace(':attribute_family', record.attribute_family) }}
+                                </p>
+                            </div>
+                        </div>
+
+                        <!-- Image, Price, Id, Stock Columns -->
+                        <div class="flex gap-1.5">
+                            <div class="relative">
+                                <template v-if="record.base_image">
+                                    <img
+                                        class="max-h-[65px] min-h-[65px] min-w-[65px] max-w-[65px] rounded"
+                                        :src='record.base_image'
+                                    />
+
+                                    <span class="absolute bottom-px rounded-full bg-darkPink px-1.5 text-xs font-bold leading-normal text-white ltr:left-px rtl:right-px">
+                                        @{{ record.images_count }}
+                                    </span>
+                                </template>
+
+                                <template v-else>
+                                    <div class="relative h-[60px] max-h-[60px] w-full max-w-[60px] rounded border border-dashed border-gray-300 dark:border-gray-800 dark:mix-blend-exclusion dark:invert">
+                                        <img src="{{ bagisto_asset('images/product-placeholders/front.svg')}}">
+
+                                        <p class="absolute bottom-1.5 w-full text-center text-[6px] font-semibold text-gray-400">
+                                            @lang('admin::app.catalog.products.index.datagrid.product-image')
+                                        </p>
+                                    </div>
+                                </template>
+                            </div>
+
+                            <div class="flex flex-col gap-1.5">
+                                <p class="text-base font-semibold text-gray-800 dark:text-white">
+                                    @{{ $admin.formatPrice(record.price) }}
+                                </p>
+
+                                <!-- Parent Product Quantity -->
+                                <div v-if="['configurable', 'bundle', 'grouped' , 'booking'].includes(record.type)">
+                                    <p class="text-gray-600 dark:text-gray-300">
+                                        <span class="text-red-600">N/A</span>
+                                    </p>
+                                </div>
+
+                                <div v-else>
+                                    <p
+                                        class="text-gray-600 dark:text-gray-300"
+                                        v-if="record.quantity > 0"
+                                    >
+                                        <span class="text-green-600">
+                                            @{{ "@lang('admin::app.catalog.products.index.datagrid.qty-value')".replace(':qty', record.quantity) }}
+                                        </span>
+                                    </p>
+
+                                    <p
+                                        class="text-gray-600 dark:text-gray-300"
+                                        v-else
+                                    >
+                                        <span class="text-red-600">
+                                            @lang('admin::app.catalog.products.index.datagrid.out-of-stock')
+                                        </span>
+                                    </p>
+                                </div>
+
+                                <p class="text-gray-600 dark:text-gray-300">
+                                    @{{ "@lang('admin::app.catalog.products.index.datagrid.id-value')".replace(':id', record.product_id) }}
+                                </p>
+                            </div>
+                        </div>
+
+                        <!-- Status, Category, Type Columns -->
+                        <div class="flex items-center justify-between gap-x-4">
+                            <div class="flex flex-col gap-1.5">
+                                <p :class="[record.status ? 'label-active': 'label-info']">
+                                    @{{ record.status ? "@lang('admin::app.catalog.products.index.datagrid.active')" : "@lang('admin::app.catalog.products.index.datagrid.disable')" }}
+                                </p>
+
+                                <p class="text-gray-600 dark:text-gray-300">
+                                    @{{ record.category_name ?? 'N/A' }}
+                                </p>
+
+                                <p class="text-gray-600 dark:text-gray-300">
+                                    @{{ record.type }}
+                                </p>
+                            </div>
+
+                            <p
+                                class="flex items-center gap-1.5"
+                                v-if="available.actions.length"
+                            >
+                                <span
+                                    class="cursor-pointer rounded-md p-1.5 text-2xl transition-all hover:bg-gray-200 dark:hover:bg-gray-800 max-sm:place-self-center"
+                                    :class="action.icon"
+                                    v-text="! action.icon ? action.title : ''"
+                                    v-for="action in record.actions"
+                                    @click="performAction(action)"
+                                >
+                                </span>
+                            </p>
+                        </div>
+                    </div>
+                </div>
+            </template>
+        </template>
+    </x-admin::datagrid>
+
+    {!! view_render_event('bagisto.admin.catalog.products.list.after') !!}
+
+    @pushOnce('scripts')
+        <script
+            type="text/x-template"
+            id="v-create-product-form-template"
+        >
+            <div>
+                <!-- Product Create Button -->
+                @if (bouncer()->hasPermission('catalog.products.create'))
+                    <button
+                        type="button"
+                        class="primary-button"
+                        @click="$refs.productCreateModal.toggle()"
+                    >
+                        @lang('admin::app.catalog.products.index.create-btn')
+                    </button>
+                @endif
+
+                <x-admin::form
+                    v-slot="{ meta, errors, handleSubmit }"
+                    as="div"
+                >
+                    <form @submit="handleSubmit($event, create)">
+                        <!-- Customer Create Modal -->
+                        <x-admin::modal ref="productCreateModal">
+                            <!-- Modal Header -->
+                            <x-slot:header>
+                                <p
+                                    class="text-lg font-bold text-gray-800 dark:text-white"
+                                    v-if="! attributes.length"
+                                >
+                                    @lang('admin::app.catalog.products.index.create.title')
+                                </p>
+
+                                <p
+                                    class="text-lg font-bold text-gray-800 dark:text-white"
+                                    v-else
+                                >
+                                    @lang('admin::app.catalog.products.index.create.configurable-attributes')
+                                </p>
+                            </x-slot>
+
+                            <!-- Modal Content -->
+                            <x-slot:content>
+                                <div v-show="! attributes.length">
+                                    {!! view_render_event('bagisto.admin.catalog.products.create_form.general.controls.before') !!}
+
+                                    <!-- Product Type -->
+                                    <x-admin::form.control-group>
+                                        <x-admin::form.control-group.label class="required">
+                                            @lang('admin::app.catalog.products.index.create.type')
+                                        </x-admin::form.control-group.label>
+
+                                        <x-admin::form.control-group.control
+                                            type="select"
+                                            name="type"
+                                            rules="required"
+                                            :label="trans('admin::app.catalog.products.index.create.type')"
+                                        >
+                                            @foreach(config('product_types') as $key => $type)
+                                                <option value="{{ $key }}">
+                                                    @lang($type['name'])
+                                                </option>
+                                            @endforeach
+                                        </x-admin::form.control-group.control>
+
+                                        <x-admin::form.control-group.error control-name="type" />
+                                    </x-admin::form.control-group>
+
+                                    <!-- Attribute Family Id -->
+                                    <x-admin::form.control-group>
+                                        <x-admin::form.control-group.label class="required">
+                                            @lang('admin::app.catalog.products.index.create.family')
+                                        </x-admin::form.control-group.label>
+
+                                        <x-admin::form.control-group.control
+                                            type="select"
+                                            name="attribute_family_id"
+                                            rules="required"
+                                            :label="trans('admin::app.catalog.products.index.create.family')"
+                                        >
+                                            @foreach($families as $family)
+                                                <option 
+                                                    value="{{ $family->id }}"
+                                                    v-pre
+                                                >
+                                                    {{ $family->name }}
+                                                </option>
+                                            @endforeach
+                                        </x-admin::form.control-group.control>
+
+                                        <x-admin::form.control-group.error control-name="attribute_family_id" />
+                                    </x-admin::form.control-group>
+
+                                    <!-- SKU -->
+                                    <x-admin::form.control-group>
+                                        <x-admin::form.control-group.label class="required">
+                                            @lang('admin::app.catalog.products.index.create.sku')
+                                        </x-admin::form.control-group.label>
+
+                                        <x-admin::form.control-group.control
+                                            type="text"
+                                            name="sku"
+                                            ::rules="{ required: true, regex: /^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/ }"
+                                            :label="trans('admin::app.catalog.products.index.create.sku')"
+                                        />
+
+                                        <x-admin::form.control-group.error control-name="sku" />
+                                    </x-admin::form.control-group>
+
+                                    {!! view_render_event('bagisto.admin.catalog.products.create_form.general.controls.after') !!}
+                                </div>
+
+                                <div v-show="attributes.length">
+                                    {!! view_render_event('bagisto.admin.catalog.products.create_form.attributes.controls.before') !!}
+
+                                    <div
+                                        class="mb-2.5"
+                                        v-for="attribute in attributes"
+                                    >
+                                        <label
+                                            class="block text-xs font-medium leading-6 text-gray-800 dark:text-white"
+                                            v-text="attribute.name"
+                                        >
+                                        </label>
+
+                                        <div class="flex min-h-[38px] flex-wrap gap-1 rounded-md border p-1.5 dark:border-gray-800">
+                                            <p
+                                                class="flex items-center rounded bg-gray-600 px-2 py-1 font-semibold text-white"
+                                                v-for="option in attribute.options"
+                                            >
+                                                @{{ option.name }}
+
+                                                <span
+                                                    class="icon-cross cursor-pointer text-lg text-white ltr:ml-1.5 rtl:mr-1.5"
+                                                    @click="removeOption(option)"
+                                                >
+                                                </span>
+                                            </p>
+                                        </div>
+                                    </div>
+
+                                    {!! view_render_event('bagisto.admin.catalog.products.create_form.attributes.controls.after') !!}
+                                </div>
+                            </x-slot>
+
+                            <!-- Modal Footer -->
+                            <x-slot:footer>
+                                <div class="flex items-center gap-x-2.5">
+                                    <!-- Back Button -->
+                                    <x-admin::button
+                                        button-type="button"
+                                        class="transparent-button hover:bg-gray-200 dark:text-white dark:hover:bg-gray-800"
+                                        :title="trans('admin::app.catalog.products.index.create.back-btn')"
+                                        v-if="attributes.length"
+                                        @click="attributes = []"
+                                    />
+
+                                    <!-- Save Button -->
+                                    <x-admin::button
+                                        button-type="button"
+                                        class="primary-button"
+                                        :title="trans('admin::app.catalog.products.index.create.save-btn')"
+                                        ::loading="isLoading"
+                                        ::disabled="isLoading"
+                                    />
+                                </div>
+                            </x-slot>
+                        </x-admin::modal>
+                    </form>
+                </x-admin::form>
+            </div>
+        </script>
+
+        <script type="module">
+            app.component('v-create-product-form', {
+                template: '#v-create-product-form-template',
+
+                data() {
+                    return {
+                        attributes: [],
+
+                        superAttributes: {},
+
+                        isLoading: false,
+                    };
+                },
+
+                methods: {
+                   create(params, { resetForm, resetField, setErrors }) {
+                        // For flexible_variant type, skip super_attributes handling
+                        if (params.type !== 'flexible_variant') {
+                            this.attributes.forEach(attribute => {
+                                params.super_attributes ||= {};
+
+                                params.super_attributes[attribute.code] = this.superAttributes[attribute.code];
+                            });
+                        }
+
+                        this.$axios.post("{{ route('admin.catalog.products.store') }}", params)
+                            .then((response) => {
+                                if (response.data.data.redirect_url) {
+                                    window.location.href = response.data.data.redirect_url;
+                                } else {
+                                    // Skip attributes selection for flexible_variant
+                                    if (params.type === 'flexible_variant') {
+                                        console.warn('Flexible variant should redirect immediately');
+                                        return;
+                                    }
+                                    
+                                    this.attributes = response.data.data.attributes;
+
+                                    this.setSuperAttributes();
+                                }
+                            })
+                            .catch(error => {
+                                if (error.response.status == 422) {
+                                    setErrors(error.response.data.errors);
+                                }
+                            });
+                    },
+
+                    removeOption(option) {
+                        this.attributes.forEach(attribute => {
+                            attribute.options = attribute.options.filter(item => item.id != option.id);
+                        });
+
+                        this.attributes = this.attributes.filter(attribute => attribute.options.length > 0);
+
+                        this.setSuperAttributes();
+                    },
+
+                    setSuperAttributes() {
+                        this.superAttributes = {};
+
+                        this.attributes.forEach(attribute => {
+                            this.superAttributes[attribute.code] = [];
+
+                            attribute.options.forEach(option => {
+                                this.superAttributes[attribute.code].push(option.id);
+                            });
+                        });
+                    }
+                }
+            })
+        </script>
+    @endPushOnce
+</x-admin::layouts>