فهرست منبع

Merge branch 'llp_yunshu' into dev

llp 1 هفته پیش
والد
کامیت
b05f46d732
23فایلهای تغییر یافته به همراه3253 افزوده شده و 5 حذف شده
  1. 1 1
      packages/Longyi/Pay/Afterpay/src/Http/routes.php
  2. 2 2
      packages/Longyi/Pay/Afterpay/src/Payment/Afterpay.php
  3. 5 1
      packages/Longyi/Pay/Airwallex/src/Payment/Airwallex.php
  4. 20 0
      packages/Webkul/Admin/src/Config/acl.php
  5. 6 0
      packages/Webkul/Admin/src/Config/menu.php
  6. 169 0
      packages/Webkul/Admin/src/DataGrids/Settings/ShippingZoneRatesDataGrid.php
  7. 207 0
      packages/Webkul/Admin/src/Http/Controllers/Settings/ShippingZoneRateController.php
  8. 73 0
      packages/Webkul/Admin/src/Resources/lang/en/app.php
  9. 73 0
      packages/Webkul/Admin/src/Resources/lang/zh_CN/app.php
  10. 2 0
      packages/Webkul/Admin/src/Resources/views/components/button/index.blade.php
  11. 940 0
      packages/Webkul/Admin/src/Resources/views/settings/shipping-zone-rates/index.blade.php
  12. 20 0
      packages/Webkul/Admin/src/Routes/settings-routes.php
  13. 98 0
      packages/Webkul/Shipping/src/Carriers/ZoneRate.php
  14. 8 0
      packages/Webkul/Shipping/src/Config/carriers.php
  15. 40 0
      packages/Webkul/Shipping/src/Config/system.php
  16. 5 0
      packages/Webkul/Shipping/src/Contracts/ShippingZoneRate.php
  17. 38 0
      packages/Webkul/Shipping/src/Database/Migrations/2026_06_24_000000_create_shipping_zone_rates_table.php
  18. 33 0
      packages/Webkul/Shipping/src/Database/Migrations/2026_06_24_100000_add_order_subtotal_to_shipping_zone_rates.php
  19. 1278 0
      packages/Webkul/Shipping/src/Database/Seeders/ShippingZoneRateSeeder.php
  20. 209 0
      packages/Webkul/Shipping/src/Models/ShippingZoneRate.php
  21. 7 0
      packages/Webkul/Shipping/src/Models/ShippingZoneRateProxy.php
  22. 4 1
      packages/Webkul/Shipping/src/Providers/ModuleServiceProvider.php
  23. 15 0
      packages/Webkul/Shipping/src/Providers/ShippingServiceProvider.php

+ 1 - 1
packages/Longyi/Pay/Afterpay/src/Http/routes.php

@@ -3,4 +3,4 @@
 use Illuminate\Support\Facades\Route;
 use Longyi\Pay\Afterpay\Http\Controllers\AfterpayController;
 
-Route::get('afterpay/webhook/return', [AfterpayController::class, 'return'])->name('afterpay.return');
+Route::get('afterpay/return', [AfterpayController::class, 'return'])->name('afterpay.return');

+ 2 - 2
packages/Longyi/Pay/Afterpay/src/Payment/Afterpay.php

@@ -106,8 +106,8 @@ class Afterpay extends Payment
                 'countryCode' => $shop->country,
             ],
             'merchant' => [
-                'redirectConfirmUrl' => $this->getBaseUlr() . '/afterpay/webhook/hook?order_id=' . $override['merchantOrderId'],
-                'redirectCancelUrl' => $this->getBaseUlr() . '/afterpay/webhook/hook?order_id=' . $override['merchantOrderId']
+                'redirectConfirmUrl' => $this->getBaseUlr() . '/afterpay/return?order_id=' . $override['merchantOrderId'],
+                'redirectCancelUrl' => $this->getBaseUlr() . '/afterpay/return?order_id=' . $override['merchantOrderId']
             ],
             'items' => $products
         ];

+ 5 - 1
packages/Longyi/Pay/Airwallex/src/Payment/Airwallex.php

@@ -61,7 +61,11 @@ class Airwallex extends Payment
     }
     public function createGatewayOrder()
     {
-        return $this->createIntents($this->getOrder(), ['merchantOrderId' => $this->getOrder()->increment_id]);
+        try {
+            return $this->createIntents($this->getOrder(), ['merchantOrderId' => $this->getOrder()->increment_id]);
+        } catch (\Exception $e) {
+            throw new \Exception($e->getMessage());
+        }
     }
 
     public function createIntents($order, $override)

+ 20 - 0
packages/Webkul/Admin/src/Config/acl.php

@@ -855,6 +855,26 @@ return [
         'name'  => 'admin::app.acl.import',
         'route' => 'admin.settings.data_transfer.imports.import',
         'sort'  => 4,
+    ], [
+        'key'   => 'settings.shipping_zone_rates',
+        'name'  => 'admin::app.components.layouts.sidebar.shipping-zone-rates',
+        'route' => 'admin.settings.shipping_zone_rates.index',
+        'sort'  => 11,
+    ], [
+        'key'   => 'settings.shipping_zone_rates.create',
+        'name'  => 'admin::app.acl.create',
+        'route' => 'admin.settings.shipping_zone_rates.store',
+        'sort'  => 1,
+    ], [
+        'key'   => 'settings.shipping_zone_rates.edit',
+        'name'  => 'admin::app.acl.edit',
+        'route' => 'admin.settings.shipping_zone_rates.edit',
+        'sort'  => 2,
+    ], [
+        'key'   => 'settings.shipping_zone_rates.delete',
+        'name'  => 'admin::app.acl.delete',
+        'route' => 'admin.settings.shipping_zone_rates.delete',
+        'sort'  => 3,
     ],
 
     /*

+ 6 - 0
packages/Webkul/Admin/src/Config/menu.php

@@ -348,6 +348,12 @@ return [
         'route'      => 'admin.settings.data_transfer.imports.index',
         'sort'       => 1,
         'icon'       => '',
+    ], [
+        'key'        => 'settings.shipping_zone_rates',
+        'name'       => 'admin::app.components.layouts.sidebar.shipping-zone-rates',
+        'route'      => 'admin.settings.shipping_zone_rates.index',
+        'sort'       => 11,
+        'icon'       => '',
     ],
 
     /**

+ 169 - 0
packages/Webkul/Admin/src/DataGrids/Settings/ShippingZoneRatesDataGrid.php

@@ -0,0 +1,169 @@
+<?php
+
+namespace Webkul\Admin\DataGrids\Settings;
+
+use Illuminate\Support\Facades\DB;
+use Webkul\DataGrid\DataGrid;
+
+class ShippingZoneRatesDataGrid extends DataGrid
+{
+    /**
+     * Primary column.
+     *
+     * @var string
+     */
+    protected $primaryColumn = 'shipping_zone_rate_id';
+
+    /**
+     * Prepare query builder.
+     *
+     * @return \Illuminate\Database\Query\Builder
+     */
+    public function prepareQueryBuilder()
+    {
+        $queryBuilder = DB::table('shipping_zone_rates')
+            ->select(
+                'shipping_zone_rates.id as shipping_zone_rate_id',
+                'shipping_zone_rates.carrier_code',
+                'shipping_zone_rates.carrier_title',
+                'shipping_zone_rates.country_code',
+                'shipping_zone_rates.state_code',
+                'shipping_zone_rates.order_subtotal_from',
+                'shipping_zone_rates.order_subtotal_to',
+                'shipping_zone_rates.price',
+                'shipping_zone_rates.delivery_type',
+                'shipping_zone_rates.delivery_description',
+                'shipping_zone_rates.is_active',
+                'shipping_zone_rates.sort_order',
+            );
+
+        $this->addFilter('shipping_zone_rate_id', 'shipping_zone_rates.id');
+        $this->addFilter('carrier_code', 'shipping_zone_rates.carrier_code');
+        $this->addFilter('carrier_title', 'shipping_zone_rates.carrier_title');
+        $this->addFilter('country_code', 'shipping_zone_rates.country_code');
+        $this->addFilter('state_code', 'shipping_zone_rates.state_code');
+        $this->addFilter('delivery_type', 'shipping_zone_rates.delivery_type');
+        $this->addFilter('price', 'shipping_zone_rates.price');
+
+        return $queryBuilder;
+    }
+
+    public function prepareColumns()
+    {
+        $this->addColumn([
+            'index'      => 'shipping_zone_rate_id',
+            'label'      => trans('admin::app.settings.shipping_zone_rates.index.datagrid.id'),
+            'type'       => 'integer',
+            'filterable' => true,
+            'sortable'   => true,
+        ]);
+
+        $this->addColumn([
+            'index'      => 'carrier_title',
+            'label'      => trans('admin::app.settings.shipping_zone_rates.index.datagrid.carrier-title'),
+            'type'       => 'string',
+            'searchable' => true,
+            'filterable' => true,
+            'sortable'   => true,
+        ]);
+
+        $this->addColumn([
+            'index'      => 'carrier_code',
+            'label'      => trans('admin::app.settings.shipping_zone_rates.index.datagrid.carrier-code'),
+            'type'       => 'string',
+            'searchable' => true,
+            'filterable' => true,
+            'sortable'   => true,
+        ]);
+
+        $this->addColumn([
+            'index'      => 'country_code',
+            'label'      => trans('admin::app.settings.shipping_zone_rates.index.datagrid.country-code'),
+            'type'       => 'string',
+            'searchable' => true,
+            'filterable' => true,
+            'sortable'   => true,
+        ]);
+
+        $this->addColumn([
+            'index'      => 'state_code',
+            'label'      => trans('admin::app.settings.shipping_zone_rates.index.datagrid.state-code'),
+            'type'       => 'string',
+            'searchable' => true,
+            'filterable' => true,
+            'sortable'   => true,
+        ]);
+
+        $this->addColumn([
+            'index'      => 'delivery_description',
+            'label'      => trans('admin::app.settings.shipping_zone_rates.index.datagrid.delivery-description'),
+            'type'       => 'string',
+            'searchable' => true,
+            'filterable' => true,
+            'sortable'   => true,
+        ]);
+
+        $this->addColumn([
+            'index'      => 'order_subtotal_from',
+            'label'      => trans('admin::app.settings.shipping_zone_rates.index.datagrid.subtotal-from'),
+            'type'       => 'decimal',
+            'searchable' => false,
+            'filterable' => false,
+            'sortable'   => true,
+        ]);
+
+        $this->addColumn([
+            'index'      => 'order_subtotal_to',
+            'label'      => trans('admin::app.settings.shipping_zone_rates.index.datagrid.subtotal-to'),
+            'type'       => 'decimal',
+            'searchable' => false,
+            'filterable' => false,
+            'sortable'   => true,
+        ]);
+
+        $this->addColumn([
+            'index'      => 'price',
+            'label'      => trans('admin::app.settings.shipping_zone_rates.index.datagrid.price'),
+            'type'       => 'decimal',
+            'searchable' => true,
+            'filterable' => true,
+            'sortable'   => true,
+        ]);
+
+        $this->addColumn([
+            'index'      => 'sort_order',
+            'label'      => trans('admin::app.settings.shipping_zone_rates.index.datagrid.sort-order'),
+            'type'       => 'integer',
+            'searchable' => false,
+            'filterable' => false,
+            'sortable'   => true,
+        ]);
+    }
+
+    public function prepareActions()
+    {
+        if (bouncer()->hasPermission('settings.shipping_zone_rates.edit')) {
+            $this->addAction([
+                'index'  => 'edit',
+                'icon'   => 'icon-edit',
+                'title'  => trans('admin::app.settings.shipping_zone_rates.index.datagrid.edit'),
+                'method' => 'GET',
+                'url'    => function ($row) {
+                    return route('admin.settings.shipping_zone_rates.edit', $row->shipping_zone_rate_id);
+                },
+            ]);
+        }
+
+        if (bouncer()->hasPermission('settings.shipping_zone_rates.delete')) {
+            $this->addAction([
+                'index'  => 'delete',
+                'icon'   => 'icon-delete',
+                'title'  => trans('admin::app.settings.shipping_zone_rates.index.datagrid.delete'),
+                'method' => 'DELETE',
+                'url'    => function ($row) {
+                    return route('admin.settings.shipping_zone_rates.delete', $row->shipping_zone_rate_id);
+                },
+            ]);
+        }
+    }
+}

+ 207 - 0
packages/Webkul/Admin/src/Http/Controllers/Settings/ShippingZoneRateController.php

@@ -0,0 +1,207 @@
+<?php
+
+namespace Webkul\Admin\Http\Controllers\Settings;
+
+use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Request;
+use Webkul\Admin\DataGrids\Settings\ShippingZoneRatesDataGrid;
+use Webkul\Admin\Http\Controllers\Controller;
+use Webkul\Shipping\Models\ShippingZoneRate;
+
+class ShippingZoneRateController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\View\View
+     */
+    public function index()
+    {
+        if (request()->ajax()) {
+            return datagrid(ShippingZoneRatesDataGrid::class)->process();
+        }
+
+        return view('admin::settings.shipping-zone-rates.index');
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     */
+    public function store(Request $request): JsonResponse
+    {
+        $this->validate($request, [
+            'carrier_code'         => 'required|string|max:255',
+            'carrier_title'        => 'required|string|max:255',
+            'country_code'         => 'nullable|string|max:10',
+            'state_code'           => 'nullable|string|max:10',
+            'order_subtotal_from'  => 'nullable|numeric',
+            'order_subtotal_to'    => 'nullable|numeric',
+            'price'                => 'required|numeric',
+            'delivery_type'        => 'required|string|max:255',
+            'delivery_description' => 'nullable|string|max:255',
+            'is_active'            => 'nullable|boolean',
+            'sort_order'           => 'nullable|integer',
+        ]);
+
+        ShippingZoneRate::create($request->only([
+            'carrier_code',
+            'carrier_title',
+            'country_code',
+            'state_code',
+            'order_subtotal_from',
+            'order_subtotal_to',
+            'price',
+            'delivery_type',
+            'delivery_description',
+            'is_active',
+            'sort_order',
+        ]));
+
+        return new JsonResponse([
+            'message' => trans('admin::app.settings.shipping_zone_rates.index.create-success'),
+        ]);
+    }
+
+    /**
+     * Show the form for editing the specified resource.
+     */
+    public function edit(int $id): JsonResponse
+    {
+        $rate = ShippingZoneRate::findOrFail($id);
+
+        return new JsonResponse([
+            'data' => $rate->toArray(),
+        ]);
+    }
+
+    /**
+     * Update the specified resource in storage.
+     */
+    public function update(Request $request): JsonResponse
+    {
+        $this->validate($request, [
+            'carrier_code'         => 'required|string|max:255',
+            'carrier_title'        => 'required|string|max:255',
+            'country_code'         => 'nullable|string|max:10',
+            'state_code'           => 'nullable|string|max:10',
+            'order_subtotal_from'  => 'nullable|numeric',
+            'order_subtotal_to'    => 'nullable|numeric',
+            'price'                => 'required|numeric',
+            'delivery_type'        => 'required|string|max:255',
+            'delivery_description' => 'nullable|string|max:255',
+            'is_active'            => 'nullable|boolean',
+            'sort_order'           => 'nullable|integer',
+        ]);
+
+        $rate = ShippingZoneRate::findOrFail($request->id);
+
+        $rate->update($request->only([
+            'carrier_code',
+            'carrier_title',
+            'country_code',
+            'state_code',
+            'order_subtotal_from',
+            'order_subtotal_to',
+            'price',
+            'delivery_type',
+            'delivery_description',
+            'is_active',
+            'sort_order',
+        ]));
+
+        return new JsonResponse([
+            'message' => trans('admin::app.settings.shipping_zone_rates.index.update-success'),
+        ]);
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     */
+    public function destroy(int $id): JsonResponse
+    {
+        try {
+            ShippingZoneRate::findOrFail($id)->delete();
+
+            return new JsonResponse([
+                'message' => trans('admin::app.settings.shipping_zone_rates.index.delete-success'),
+            ], 200);
+        } catch (\Exception $e) {
+            report($e);
+        }
+
+        return new JsonResponse([
+            'message' => trans('admin::app.settings.shipping_zone_rates.index.delete-error'),
+        ], 500);
+    }
+
+    /**
+     * Mass delete resources.
+     */
+    public function massDestroy(Request $request): JsonResponse
+    {
+        $request->validate([
+            'indices' => 'required|array',
+        ]);
+
+        try {
+            ShippingZoneRate::whereIn('id', $request->indices)->delete();
+
+            return new JsonResponse([
+                'message' => trans('admin::app.settings.shipping_zone_rates.index.delete-success'),
+            ]);
+        } catch (\Exception $e) {
+            report($e);
+        }
+
+        return new JsonResponse([
+            'message' => trans('admin::app.settings.shipping_zone_rates.index.delete-error'),
+        ], 500);
+    }
+
+    /**
+     * Import CSV file.
+     */
+    public function import(Request $request): JsonResponse
+    {
+        $request->validate([
+            'csv_file'   => 'required|file|mimes:csv,txt',
+            'truncate'   => 'nullable|boolean',
+        ]);
+
+        $file = $request->file('csv_file');
+        $filePath = $file->getRealPath();
+        $truncate = $request->boolean('truncate', false);
+
+        try {
+            $result = ShippingZoneRate::importFromCsv($filePath, $truncate);
+
+            if ($truncate) {
+                $message = trans('admin::app.settings.shipping_zone_rates.index.import-truncate-success', [
+                    'count' => $result['inserted'],
+                ]);
+            } else {
+                $message = trans('admin::app.settings.shipping_zone_rates.index.import-success', [
+                    'count' => $result['inserted'],
+                ]);
+            }
+
+            if (! empty($result['errors'])) {
+                $errorCount = count($result['errors']);
+                $message .= ' ' . trans('admin::app.settings.shipping_zone_rates.index.import-errors', [
+                    'count' => $errorCount,
+                ]);
+            }
+
+            return new JsonResponse([
+                'message'  => $message,
+                'total'    => $result['total'],
+                'inserted' => $result['inserted'],
+                'errors'   => $result['errors'],
+            ]);
+        } catch (\Exception $e) {
+            return new JsonResponse([
+                'message' => $e->getMessage(),
+            ], 422);
+        }
+    }
+}

+ 73 - 0
packages/Webkul/Admin/src/Resources/lang/en/app.php

@@ -3532,6 +3532,68 @@ return [
             'delete-success' => 'Theme deleted successfully',
             'update-success' => 'Theme updated successfully',
         ],
+
+        'shipping_zone_rates' => [
+            'index' => [
+                'title'           => 'Shipping Zone Rates',
+                'create-btn'      => 'Create Rate',
+                'import-btn'      => 'Import CSV',
+                'create-success'  => 'Rate created successfully.',
+                'update-success'  => 'Rate updated successfully.',
+                'delete-success'  => 'Rate deleted successfully.',
+                'delete-error'    => 'Error deleting rate.',
+                'import-success'  => 'Successfully imported :count rate records.',
+                'import-truncate-success' => 'Table truncated, successfully imported :count rate records.',
+                'import-errors'   => ', but :count records failed to import.',
+                'yes'             => 'Yes',
+                'no'              => 'No',
+                'cancel'          => 'Cancel',
+
+                'create' => [
+                    'title'                => 'Create Rate',
+                    'carrier-code'         => 'Carrier Code',
+                    'carrier-title'        => 'Carrier Title',
+                    'country-code'         => 'Country Code',
+                    'state-code'           => 'State Code',
+                    'subtotal-from'        => 'Order Subtotal From',
+                    'subtotal-to'          => 'Order Subtotal To',
+                    'price'                => 'Shipping Price',
+                    'delivery-type'        => 'Delivery Type',
+                    'delivery-description' => 'Delivery Description',
+                    'is-active'            => 'Is Active',
+                    'sort-order'           => 'Sort Order',
+                    'save-btn'             => 'Save',
+                ],
+
+                'edit' => [
+                    'title' => 'Edit Rate',
+                ],
+
+                'import' => [
+                    'title'          => 'Import from CSV',
+                    'description'    => 'CSV columns: Country, Region/State, City, Zip/Postal Code From, Zip/Postal Code To, Order Subtotal From, Order Subtotal To, Shipping Price, Delivery Type',
+                    'file'           => 'CSV File',
+                    'upload-btn'     => 'Upload & Import',
+                    'truncate'       => 'Truncate existing data before import',
+                    'truncate-label' => 'Truncate Option',
+                ],
+
+                'datagrid' => [
+                    'id'                   => 'ID',
+                    'carrier-code'         => 'Carrier Code',
+                    'carrier-title'        => 'Carrier Title',
+                    'country-code'         => 'Country Code',
+                    'state-code'           => 'State Code',
+                    'delivery-description' => 'Delivery Description',
+                    'subtotal-from'        => 'Subtotal From',
+                    'subtotal-to'          => 'Subtotal To',
+                    'price'                => 'Price',
+                    'sort-order'           => 'Sort Order',
+                    'edit'                 => 'Edit',
+                    'delete'               => 'Delete',
+                ],
+            ],
+        ],
     ],
 
     'reporting' => [
@@ -4386,6 +4448,15 @@ return [
                             'title'     => 'Type',
                         ],
                     ],
+
+                    'zone-rate' => [
+                        'allowed-countries' => 'Allowed Countries',
+                        'description'       => 'Description',
+                        'page-title'        => 'Zone Rate',
+                        'status'            => 'Status',
+                        'title'             => 'Title',
+                        'title-info'        => 'Zone-based shipping rates that vary by destination country/region and order subtotal. Configure specific rates via the Shipping Zone Rates table.',
+                    ],
                 ],
 
                 'payment-methods' => [
@@ -4668,6 +4739,7 @@ return [
                 'search-terms'             => 'Search Terms',
                 'settings'                 => 'Settings',
                 'shipments'                => 'Shipments',
+                'shipping-zone-rates'      => 'Shipping Zone Rates',
                 'sitemaps'                 => 'Sitemaps',
                 'tax-categories'           => 'Tax Categories',
                 'tax-rates'                => 'Tax Rates',
@@ -4931,6 +5003,7 @@ return [
         'search-terms'             => 'Search Terms',
         'settings'                 => 'Settings',
         'shipments'                => 'Shipments',
+        'shipping-zone-rates'      => 'Shipping Zone Rates',
         'sitemaps'                 => 'Sitemaps',
         'subscribers'              => 'Newsletter Subscribers',
         'tax-categories'           => 'Tax Categories',

+ 73 - 0
packages/Webkul/Admin/src/Resources/lang/zh_CN/app.php

@@ -3532,6 +3532,68 @@ return [
             'delete-success' => '成功删除主题',
             'update-success' => '成功更新主题',
         ],
+
+        'shipping_zone_rates' => [
+            'index' => [
+                'title'           => '运输区域费率',
+                'create-btn'      => '创建费率',
+                'import-btn'      => '导入CSV',
+                'create-success'  => '费率创建成功。',
+                'update-success'  => '费率更新成功。',
+                'delete-success'  => '费率删除成功。',
+                'delete-error'    => '删除费率时出错。',
+                'import-success'  => '成功导入 :count 条费率记录。',
+                'import-truncate-success' => '表已清空,成功导入 :count 条费率记录。',
+                'import-errors'   => ',但有 :count 条记录导入失败。',
+                'yes'             => '是',
+                'no'              => '否',
+                'cancel'          => '取消',
+
+                'create' => [
+                    'title'                => '创建费率',
+                    'carrier-code'         => '运输商代码',
+                    'carrier-title'        => '运输商名称',
+                    'country-code'         => '国家代码',
+                    'state-code'           => '州/省代码',
+                    'subtotal-from'        => '订单金额从',
+                    'subtotal-to'          => '订单金额到',
+                    'price'                => '运费价格',
+                    'delivery-type'        => '运输类型',
+                    'delivery-description' => '运输描述',
+                    'is-active'            => '是否启用',
+                    'sort-order'           => '排序',
+                    'save-btn'             => '保存',
+                ],
+
+                'edit' => [
+                    'title' => '编辑费率',
+                ],
+
+                'import' => [
+                    'title'          => '从CSV导入',
+                    'description'    => 'CSV文件列顺序:Country, Region/State, City, Zip/Postal Code From, Zip/Postal Code To, Order Subtotal From, Order Subtotal To, Shipping Price, Delivery Type',
+                    'file'           => 'CSV文件',
+                    'upload-btn'     => '上传并导入',
+                    'truncate'       => '导入前清空现有数据',
+                    'truncate-label' => '清空选项',
+                ],
+
+                'datagrid' => [
+                    'id'                   => 'ID',
+                    'carrier-code'         => '运输商代码',
+                    'carrier-title'        => '运输商名称',
+                    'country-code'         => '国家代码',
+                    'state-code'           => '州/省代码',
+                    'delivery-description' => '运输描述',
+                    'subtotal-from'        => '金额从',
+                    'subtotal-to'          => '金额到',
+                    'price'                => '运费',
+                    'sort-order'           => '排序',
+                    'edit'                 => '编辑',
+                    'delete'               => '删除',
+                ],
+            ],
+        ],
     ],
 
     'reporting' => [
@@ -4386,6 +4448,15 @@ return [
                             'title'     => '类型',
                         ],
                     ],
+
+                    'zone-rate' => [
+                        'allowed-countries' => '允许的国家',
+                        'description'       => '描述',
+                        'page-title'        => '区域费率',
+                        'status'            => '状态',
+                        'title'             => '标题',
+                        'title-info'        => '按目的地国家/地区和订单金额区间计算不同的运费。具体费率请在配送区域费率表中配置。',
+                    ],
                 ],
 
                 'payment-methods' => [
@@ -4668,6 +4739,7 @@ return [
                 'search-terms'             => '搜索词',
                 'settings'                 => '设置',
                 'shipments'                => '发货',
+                'shipping-zone-rates'      => '运输区域费率',
                 'sitemaps'                 => '站点地图',
                 'tax-categories'           => '税收分类',
                 'tax-rates'                => '税率',
@@ -4931,6 +5003,7 @@ return [
         'search-terms'             => '搜索词',
         'settings'                 => '设置',
         'shipments'                => '发货',
+        'shipping-zone-rates'      => '运输区域费率',
         'sitemaps'                 => '网站地图',
         'subscribers'              => '电子报订阅者',
         'tax-categories'           => '税务类别',

+ 2 - 0
packages/Webkul/Admin/src/Resources/views/components/button/index.blade.php

@@ -7,6 +7,7 @@
     >
         <button
             v-if="! loading"
+            :type="buttonType ?? 'button'"
             :class="[buttonClass, '']"
         >
             @{{ title }}
@@ -14,6 +15,7 @@
 
         <button
             v-else
+            :type="buttonType ?? 'button'"
             :class="[buttonClass, '']"
         >
             <!-- Spinner -->

+ 940 - 0
packages/Webkul/Admin/src/Resources/views/settings/shipping-zone-rates/index.blade.php

@@ -0,0 +1,940 @@
+<x-admin::layouts>
+    <x-slot:title>
+        @lang('admin::app.settings.shipping_zone_rates.index.title')
+    </x-slot>
+
+    {!! view_render_event('bagisto.admin.settings.shipping_zone_rates.create.before') !!}
+
+    <v-shipping-zone-rates>
+        <div class="flex items-center justify-between">
+            <p class="text-xl font-bold text-gray-800 dark:text-white">
+                @lang('admin::app.settings.shipping_zone_rates.index.title')
+            </p>
+
+            <div class="flex items-center gap-x-2.5">
+                <!-- Import CSV Button -->
+                @if (bouncer()->hasPermission('settings.shipping_zone_rates.create'))
+                    <button
+                        type="button"
+                        class="secondary-button"
+                        @click="$refs.importModal.toggle()"
+                    >
+                        @lang('admin::app.settings.shipping_zone_rates.index.import-btn')
+                    </button>
+                @endif
+
+                <!-- Create Button -->
+                @if (bouncer()->hasPermission('settings.shipping_zone_rates.create'))
+                    <button
+                        type="button"
+                        class="primary-button"
+                        @click="selectedRate=0; resetForm(); $refs.rateUpdateOrCreateModal.toggle()"
+                    >
+                        @lang('admin::app.settings.shipping_zone_rates.index.create-btn')
+                    </button>
+                @endif
+            </div>
+        </div>
+
+        <!-- DataGrid Shimmer -->
+        <x-admin::shimmer.datagrid />
+
+        <x-admin::datagrid
+            :src="route('admin.settings.shipping_zone_rates.index')"
+            ref="datagrid"
+        >
+            <template #body="{
+                isLoading,
+                available,
+                applied,
+                selectAll,
+                sort,
+                performAction
+            }">
+                <template v-if="isLoading">
+                    <x-admin::shimmer.datagrid.table.body />
+                </template>
+
+                <template v-else>
+                    <div
+                        v-for="record in available.records"
+                        class="row grid items-center gap-2.5 border-b px-4 py-4 text-gray-600 transition-all hover:bg-gray-50 dark:border-gray-800 dark:text-gray-300 dark:hover:bg-gray-950"
+                        :style="`grid-template-columns: repeat(${gridsCount}, minmax(0, 1fr))`"
+                    >
+                        <!-- ID -->
+                        <p>@{{ record.shipping_zone_rate_id }}</p>
+
+                        <!-- Carrier Title -->
+                        <p>@{{ record.carrier_title }}</p>
+
+                        <!-- Carrier Code -->
+                        <p>@{{ record.carrier_code }}</p>
+
+                        <!-- Country Code -->
+                        <p>@{{ record.country_code }}</p>
+
+                        <!-- State Code -->
+                        <p>@{{ record.state_code }}</p>
+
+                        <!-- Delivery Description -->
+                        <p>@{{ record.delivery_description }}</p>
+
+                        <!-- Subtotal From -->
+                        <p>@{{ record.order_subtotal_from }}</p>
+
+                        <!-- Subtotal To -->
+                        <p>@{{ record.order_subtotal_to }}</p>
+
+                        <!-- Price -->
+                        <p>@{{ record.price }}</p>
+
+                        <!-- Sort Order -->
+                        <p>@{{ record.sort_order }}</p>
+
+                        <!-- Actions -->
+                        <div class="flex justify-end">
+                            @if (bouncer()->hasPermission('settings.shipping_zone_rates.edit'))
+                                <a @click="selectedRate=1; editModal(record.actions.find(action => action.index === 'edit')?.url)">
+                                    <span
+                                        :class="record.actions.find(action => action.index === 'edit')?.icon"
+                                        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"
+                                    >
+                                    </span>
+                                </a>
+                            @endif
+
+                            @if (bouncer()->hasPermission('settings.shipping_zone_rates.delete'))
+                                <a @click="performAction(record.actions.find(action => action.index === 'delete'))">
+                                    <span
+                                        :class="record.actions.find(action => action.index === 'delete')?.icon"
+                                        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"
+                                    >
+                                    </span>
+                                </a>
+                            @endif
+                        </div>
+                    </div>
+                </template>
+            </template>
+        </x-admin::datagrid>
+
+        <!-- Create / Edit Modal -->
+        <x-admin::form
+            v-slot="{ meta, errors, handleSubmit }"
+            as="div"
+            ref="modalForm"
+        >
+            <form
+                @submit="handleSubmit($event, updateOrCreate)"
+                ref="rateCreateForm"
+            >
+                <x-admin::modal ref="rateUpdateOrCreateModal">
+                    <!-- Modal Header -->
+                    <x-slot:header>
+                        <p class="text-lg font-bold text-gray-800 dark:text-white">
+                            <span v-if="selectedRate">
+                                @lang('admin::app.settings.shipping_zone_rates.index.edit.title')
+                            </span>
+
+                            <span v-else>
+                                @lang('admin::app.settings.shipping_zone_rates.index.create.title')
+                            </span>
+                        </p>
+                    </x-slot>
+
+                    <!-- Modal Content -->
+                    <x-slot:content>
+                        <x-admin::form.control-group.control
+                            type="hidden"
+                            name="id"
+                            v-model="selectedRateData.id"
+                        />
+
+                        <!-- Carrier Code -->
+                        <x-admin::form.control-group>
+                            <x-admin::form.control-group.label class="required">
+                                @lang('admin::app.settings.shipping_zone_rates.index.create.carrier-code')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="text"
+                                name="carrier_code"
+                                rules="required"
+                                v-model="selectedRateData.carrier_code"
+                                :label="trans('admin::app.settings.shipping_zone_rates.index.create.carrier-code')"
+                            />
+
+                            <x-admin::form.control-group.error control-name="carrier_code" />
+                        </x-admin::form.control-group>
+
+                        <!-- Carrier Title -->
+                        <x-admin::form.control-group>
+                            <x-admin::form.control-group.label class="required">
+                                @lang('admin::app.settings.shipping_zone_rates.index.create.carrier-title')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="text"
+                                name="carrier_title"
+                                rules="required"
+                                v-model="selectedRateData.carrier_title"
+                                :label="trans('admin::app.settings.shipping_zone_rates.index.create.carrier-title')"
+                            />
+
+                            <x-admin::form.control-group.error control-name="carrier_title" />
+                        </x-admin::form.control-group>
+
+                        <!-- Country Code -->
+                        <x-admin::form.control-group>
+                            <x-admin::form.control-group.label>
+                                @lang('admin::app.settings.shipping_zone_rates.index.create.country-code')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="text"
+                                name="country_code"
+                                v-model="selectedRateData.country_code"
+                                :label="trans('admin::app.settings.shipping_zone_rates.index.create.country-code')"
+                            />
+
+                            <x-admin::form.control-group.error control-name="country_code" />
+                        </x-admin::form.control-group>
+
+                        <!-- State Code -->
+                        <x-admin::form.control-group>
+                            <x-admin::form.control-group.label>
+                                @lang('admin::app.settings.shipping_zone_rates.index.create.state-code')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="text"
+                                name="state_code"
+                                v-model="selectedRateData.state_code"
+                                :label="trans('admin::app.settings.shipping_zone_rates.index.create.state-code')"
+                            />
+
+                            <x-admin::form.control-group.error control-name="state_code" />
+                        </x-admin::form.control-group>
+
+                        <!-- Order Subtotal From -->
+                        <x-admin::form.control-group>
+                            <x-admin::form.control-group.label>
+                                @lang('admin::app.settings.shipping_zone_rates.index.create.subtotal-from')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="text"
+                                name="order_subtotal_from"
+                                v-model="selectedRateData.order_subtotal_from"
+                                :label="trans('admin::app.settings.shipping_zone_rates.index.create.subtotal-from')"
+                            />
+
+                            <x-admin::form.control-group.error control-name="order_subtotal_from" />
+                        </x-admin::form.control-group>
+
+                        <!-- Order Subtotal To -->
+                        <x-admin::form.control-group>
+                            <x-admin::form.control-group.label>
+                                @lang('admin::app.settings.shipping_zone_rates.index.create.subtotal-to')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="text"
+                                name="order_subtotal_to"
+                                v-model="selectedRateData.order_subtotal_to"
+                                :label="trans('admin::app.settings.shipping_zone_rates.index.create.subtotal-to')"
+                            />
+
+                            <x-admin::form.control-group.error control-name="order_subtotal_to" />
+                        </x-admin::form.control-group>
+
+                        <!-- Price -->
+                        <x-admin::form.control-group>
+                            <x-admin::form.control-group.label class="required">
+                                @lang('admin::app.settings.shipping_zone_rates.index.create.price')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="text"
+                                name="price"
+                                rules="required"
+                                v-model="selectedRateData.price"
+                                :label="trans('admin::app.settings.shipping_zone_rates.index.create.price')"
+                            />
+
+                            <x-admin::form.control-group.error control-name="price" />
+                        </x-admin::form.control-group>
+
+                        <!-- Delivery Type -->
+                        <x-admin::form.control-group>
+                            <x-admin::form.control-group.label class="required">
+                                @lang('admin::app.settings.shipping_zone_rates.index.create.delivery-type')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="text"
+                                name="delivery_type"
+                                rules="required"
+                                v-model="selectedRateData.delivery_type"
+                                :label="trans('admin::app.settings.shipping_zone_rates.index.create.delivery-type')"
+                            />
+
+                            <x-admin::form.control-group.error control-name="delivery_type" />
+                        </x-admin::form.control-group>
+
+                        <!-- Delivery Description -->
+                        <x-admin::form.control-group>
+                            <x-admin::form.control-group.label>
+                                @lang('admin::app.settings.shipping_zone_rates.index.create.delivery-description')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="text"
+                                name="delivery_description"
+                                v-model="selectedRateData.delivery_description"
+                                :label="trans('admin::app.settings.shipping_zone_rates.index.create.delivery-description')"
+                            />
+
+                            <x-admin::form.control-group.error control-name="delivery_description" />
+                        </x-admin::form.control-group>
+
+                        <!-- Is Active -->
+                        <x-admin::form.control-group>
+                            <x-admin::form.control-group.label>
+                                @lang('admin::app.settings.shipping_zone_rates.index.create.is-active')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="select"
+                                name="is_active"
+                                v-model="selectedRateData.is_active"
+                            >
+                                <option value="1">@lang('admin::app.settings.shipping_zone_rates.index.yes')</option>
+                                <option value="0">@lang('admin::app.settings.shipping_zone_rates.index.no')</option>
+                            </x-admin::form.control-group.control>
+
+                            <x-admin::form.control-group.error control-name="is_active" />
+                        </x-admin::form.control-group>
+
+                        <!-- Sort Order -->
+                        <x-admin::form.control-group>
+                            <x-admin::form.control-group.label>
+                                @lang('admin::app.settings.shipping_zone_rates.index.create.sort-order')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="text"
+                                name="sort_order"
+                                v-model="selectedRateData.sort_order"
+                                :label="trans('admin::app.settings.shipping_zone_rates.index.create.sort-order')"
+                            />
+
+                            <x-admin::form.control-group.error control-name="sort_order" />
+                        </x-admin::form.control-group>
+                    </x-slot>
+
+                    <!-- Modal Footer -->
+                    <x-slot:footer>
+                        <x-admin::button
+                            button-type="button"
+                            class="secondary-button mr-2"
+                            :title="trans('admin::app.settings.shipping_zone_rates.index.cancel')"
+                            @click="$refs.rateUpdateOrCreateModal.close()"
+                        />
+
+                        <x-admin::button
+                            button-type="submit"
+                            class="primary-button"
+                            :title="trans('admin::app.settings.shipping_zone_rates.index.create.save-btn')"
+                            ::loading="isLoading"
+                            ::disabled="isLoading"
+                        />
+                    </x-slot>
+                </x-admin::modal>
+            </form>
+        </x-admin::form>
+
+        <!-- Import CSV Modal -->
+        <x-admin::form
+            v-slot="{ meta, errors, handleSubmit }"
+            as="div"
+            ref="importModalForm"
+        >
+            <form
+                @submit="handleSubmit($event, importCsv)"
+                ref="importForm"
+            >
+                <x-admin::modal ref="importModal">
+                    <!-- Modal Header -->
+                    <x-slot:header>
+                        <p class="text-lg font-bold text-gray-800 dark:text-white">
+                            @lang('admin::app.settings.shipping_zone_rates.index.import.title')
+                        </p>
+                    </x-slot>
+
+                    <!-- Modal Content -->
+                    <x-slot:content>
+                        <div class="mb-4 text-sm text-gray-600 dark:text-gray-300">
+                            @lang('admin::app.settings.shipping_zone_rates.index.import.description')
+                        </div>
+
+                        <!-- CSV File -->
+                        <x-admin::form.control-group>
+                            <x-admin::form.control-group.label class="required">
+                                @lang('admin::app.settings.shipping_zone_rates.index.import.file')
+                            </x-admin::form.control-group.label>
+
+                            <x-admin::form.control-group.control
+                                type="file"
+                                name="csv_file"
+                                rules="required"
+                                accept=".csv"
+                            />
+
+                            <x-admin::form.control-group.error control-name="csv_file" />
+                        </x-admin::form.control-group>
+
+                        <!-- Truncate Checkbox -->
+                        <x-admin::form.control-group>
+                            <x-admin::form.control-group.label>
+                                @lang('admin::app.settings.shipping_zone_rates.index.import.truncate-label')
+                            </x-admin::form.control-group.label>
+
+                            <label class="inline-flex items-center gap-2 cursor-pointer">
+                                <input
+                                    type="checkbox"
+                                    name="truncate"
+                                    value="1"
+                                    class="peer hidden"
+                                >
+                                <span class="flex h-5 w-5 items-center justify-center rounded-md border-2 border-gray-300 transition-all peer-checked:border-blue-600 peer-checked:bg-blue-600 dark:border-gray-700">
+                                    <span class="icon-tick text-sm text-white opacity-0 peer-checked:opacity-100"></span>
+                                </span>
+                                <span class="text-sm text-gray-600 dark:text-gray-300">
+                                    @lang('admin::app.settings.shipping_zone_rates.index.import.truncate')
+                                </span>
+                            </label>
+                        </x-admin::form.control-group>
+                    </x-slot>
+
+                    <!-- Modal Footer -->
+                    <x-slot:footer>
+                        <x-admin::button
+                            button-type="button"
+                            class="secondary-button mr-2"
+                            :title="trans('admin::app.settings.shipping_zone_rates.index.cancel')"
+                            @click="$refs.importModal.close()"
+                        />
+
+                        <x-admin::button
+                            button-type="submit"
+                            class="primary-button"
+                            :title="trans('admin::app.settings.shipping_zone_rates.index.import.upload-btn')"
+                            ::loading="importLoading"
+                            ::disabled="importLoading"
+                        />
+                    </x-slot>
+                </x-admin::modal>
+            </form>
+        </x-admin::form>
+    </v-shipping-zone-rates>
+
+    {!! view_render_event('bagisto.admin.settings.shipping_zone_rates.create.after') !!}
+
+    @pushOnce('scripts')
+        <script
+            type="text/x-template"
+            id="v-shipping-zone-rates-template"
+        >
+            <div>
+                <div class="flex items-center justify-between">
+                    <p class="text-xl font-bold text-gray-800 dark:text-white">
+                        @lang('admin::app.settings.shipping_zone_rates.index.title')
+                    </p>
+
+                    <div class="flex items-center gap-x-2.5">
+                        <!-- Import CSV Button -->
+                        @if (bouncer()->hasPermission('settings.shipping_zone_rates.create'))
+                            <button
+                                type="button"
+                                class="secondary-button"
+                                @click="$refs.importModal.toggle()"
+                            >
+                                @lang('admin::app.settings.shipping_zone_rates.index.import-btn')
+                            </button>
+                        @endif
+
+                        <!-- Create Button -->
+                        @if (bouncer()->hasPermission('settings.shipping_zone_rates.create'))
+                            <button
+                                type="button"
+                                class="primary-button"
+                                @click="selectedRate=0; resetForm(); $refs.rateUpdateOrCreateModal.toggle()"
+                            >
+                                @lang('admin::app.settings.shipping_zone_rates.index.create-btn')
+                            </button>
+                        @endif
+                    </div>
+                </div>
+
+                <x-admin::datagrid
+                    :src="route('admin.settings.shipping_zone_rates.index')"
+                    ref="datagrid"
+                >
+                    <template #body="{
+                        isLoading,
+                        available,
+                        applied,
+                        selectAll,
+                        sort,
+                        performAction
+                    }">
+                        <template v-if="isLoading">
+                            <x-admin::shimmer.datagrid.table.body />
+                        </template>
+
+                        <template v-else>
+                            <div
+                                v-for="record in available.records"
+                                class="row grid items-center gap-2.5 border-b px-4 py-4 text-gray-600 transition-all hover:bg-gray-50 dark:border-gray-800 dark:text-gray-300 dark:hover:bg-gray-950"
+                                :style="`grid-template-columns: repeat(${gridsCount}, minmax(0, 1fr))`"
+                            >
+                                <!-- ID -->
+                                <p>@{{ record.shipping_zone_rate_id }}</p>
+
+                                <!-- Carrier Title -->
+                                <p>@{{ record.carrier_title }}</p>
+
+                                <!-- Carrier Code -->
+                                <p>@{{ record.carrier_code }}</p>
+
+                                <!-- Country Code -->
+                                <p>@{{ record.country_code }}</p>
+
+                                <!-- State Code -->
+                                <p>@{{ record.state_code }}</p>
+
+                                <!-- Delivery Description -->
+                                <p>@{{ record.delivery_description }}</p>
+
+                                <!-- Subtotal From -->
+                                <p>@{{ record.order_subtotal_from }}</p>
+
+                                <!-- Subtotal To -->
+                                <p>@{{ record.order_subtotal_to }}</p>
+
+                                <!-- Price -->
+                                <p>@{{ record.price }}</p>
+
+                                <!-- Sort Order -->
+                                <p>@{{ record.sort_order }}</p>
+
+                                <!-- Actions -->
+                                <div class="flex justify-end">
+                                    @if (bouncer()->hasPermission('settings.shipping_zone_rates.edit'))
+                                        <a @click="selectedRate=1; editModal(record.actions.find(action => action.index === 'edit')?.url)">
+                                            <span
+                                                :class="record.actions.find(action => action.index === 'edit')?.icon"
+                                                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"
+                                            >
+                                            </span>
+                                        </a>
+                                    @endif
+
+                                    @if (bouncer()->hasPermission('settings.shipping_zone_rates.delete'))
+                                        <a @click="performAction(record.actions.find(action => action.index === 'delete'))">
+                                            <span
+                                                :class="record.actions.find(action => action.index === 'delete')?.icon"
+                                                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"
+                                            >
+                                            </span>
+                                        </a>
+                                    @endif
+                                </div>
+                            </div>
+                        </template>
+                    </template>
+                </x-admin::datagrid>
+
+                <!-- Create / Edit Modal -->
+                <x-admin::form
+                    v-slot="{ meta, errors, handleSubmit }"
+                    as="div"
+                    ref="modalForm"
+                >
+                    <form
+                        @submit="handleSubmit($event, updateOrCreate)"
+                        ref="rateCreateForm"
+                    >
+                        <x-admin::modal ref="rateUpdateOrCreateModal">
+                            <x-slot:header>
+                                <p class="text-lg font-bold text-gray-800 dark:text-white">
+                                    <span v-if="selectedRate">
+                                        @lang('admin::app.settings.shipping_zone_rates.index.edit.title')
+                                    </span>
+                                    <span v-else>
+                                        @lang('admin::app.settings.shipping_zone_rates.index.create.title')
+                                    </span>
+                                </p>
+                            </x-slot>
+
+                            <x-slot:content>
+                                <x-admin::form.control-group.control
+                                    type="hidden"
+                                    name="id"
+                                    v-model="selectedRateData.id"
+                                />
+
+                                <div class="grid grid-cols-2 gap-4">
+                                    <!-- Carrier Code -->
+                                    <x-admin::form.control-group>
+                                        <x-admin::form.control-group.label class="required">
+                                            @lang('admin::app.settings.shipping_zone_rates.index.create.carrier-code')
+                                        </x-admin::form.control-group.label>
+                                        <x-admin::form.control-group.control
+                                            type="text"
+                                            name="carrier_code"
+                                            rules="required"
+                                            v-model="selectedRateData.carrier_code"
+                                        />
+                                        <x-admin::form.control-group.error control-name="carrier_code" />
+                                    </x-admin::form.control-group>
+
+                                    <!-- Carrier Title -->
+                                    <x-admin::form.control-group>
+                                        <x-admin::form.control-group.label class="required">
+                                            @lang('admin::app.settings.shipping_zone_rates.index.create.carrier-title')
+                                        </x-admin::form.control-group.label>
+                                        <x-admin::form.control-group.control
+                                            type="text"
+                                            name="carrier_title"
+                                            rules="required"
+                                            v-model="selectedRateData.carrier_title"
+                                        />
+                                        <x-admin::form.control-group.error control-name="carrier_title" />
+                                    </x-admin::form.control-group>
+
+                                    <!-- Country Code -->
+                                    <x-admin::form.control-group>
+                                        <x-admin::form.control-group.label>
+                                            @lang('admin::app.settings.shipping_zone_rates.index.create.country-code')
+                                        </x-admin::form.control-group.label>
+                                        <x-admin::form.control-group.control
+                                            type="text"
+                                            name="country_code"
+                                            v-model="selectedRateData.country_code"
+                                        />
+                                        <x-admin::form.control-group.error control-name="country_code" />
+                                    </x-admin::form.control-group>
+
+                                    <!-- State Code -->
+                                    <x-admin::form.control-group>
+                                        <x-admin::form.control-group.label>
+                                            @lang('admin::app.settings.shipping_zone_rates.index.create.state-code')
+                                        </x-admin::form.control-group.label>
+                                        <x-admin::form.control-group.control
+                                            type="text"
+                                            name="state_code"
+                                            v-model="selectedRateData.state_code"
+                                        />
+                                        <x-admin::form.control-group.error control-name="state_code" />
+                                    </x-admin::form.control-group>
+
+                                    <!-- Order Subtotal From -->
+                                    <x-admin::form.control-group>
+                                        <x-admin::form.control-group.label>
+                                            @lang('admin::app.settings.shipping_zone_rates.index.create.subtotal-from')
+                                        </x-admin::form.control-group.label>
+                                        <x-admin::form.control-group.control
+                                            type="text"
+                                            name="order_subtotal_from"
+                                            v-model="selectedRateData.order_subtotal_from"
+                                        />
+                                        <x-admin::form.control-group.error control-name="order_subtotal_from" />
+                                    </x-admin::form.control-group>
+
+                                    <!-- Order Subtotal To -->
+                                    <x-admin::form.control-group>
+                                        <x-admin::form.control-group.label>
+                                            @lang('admin::app.settings.shipping_zone_rates.index.create.subtotal-to')
+                                        </x-admin::form.control-group.label>
+                                        <x-admin::form.control-group.control
+                                            type="text"
+                                            name="order_subtotal_to"
+                                            v-model="selectedRateData.order_subtotal_to"
+                                        />
+                                        <x-admin::form.control-group.error control-name="order_subtotal_to" />
+                                    </x-admin::form.control-group>
+
+                                    <!-- Price -->
+                                    <x-admin::form.control-group>
+                                        <x-admin::form.control-group.label class="required">
+                                            @lang('admin::app.settings.shipping_zone_rates.index.create.price')
+                                        </x-admin::form.control-group.label>
+                                        <x-admin::form.control-group.control
+                                            type="text"
+                                            name="price"
+                                            rules="required"
+                                            v-model="selectedRateData.price"
+                                        />
+                                        <x-admin::form.control-group.error control-name="price" />
+                                    </x-admin::form.control-group>
+
+                                    <!-- Delivery Type -->
+                                    <x-admin::form.control-group>
+                                        <x-admin::form.control-group.label class="required">
+                                            @lang('admin::app.settings.shipping_zone_rates.index.create.delivery-type')
+                                        </x-admin::form.control-group.label>
+                                        <x-admin::form.control-group.control
+                                            type="text"
+                                            name="delivery_type"
+                                            rules="required"
+                                            v-model="selectedRateData.delivery_type"
+                                        />
+                                        <x-admin::form.control-group.error control-name="delivery_type" />
+                                    </x-admin::form.control-group>
+                                </div>
+
+                                <!-- Delivery Description -->
+                                <x-admin::form.control-group>
+                                    <x-admin::form.control-group.label>
+                                        @lang('admin::app.settings.shipping_zone_rates.index.create.delivery-description')
+                                    </x-admin::form.control-group.label>
+                                    <x-admin::form.control-group.control
+                                        type="text"
+                                        name="delivery_description"
+                                        v-model="selectedRateData.delivery_description"
+                                    />
+                                    <x-admin::form.control-group.error control-name="delivery_description" />
+                                </x-admin::form.control-group>
+
+                                <div class="grid grid-cols-2 gap-4">
+                                    <!-- Is Active -->
+                                    <x-admin::form.control-group>
+                                        <x-admin::form.control-group.label>
+                                            @lang('admin::app.settings.shipping_zone_rates.index.create.is-active')
+                                        </x-admin::form.control-group.label>
+                                        <x-admin::form.control-group.control
+                                            type="select"
+                                            name="is_active"
+                                            v-model="selectedRateData.is_active"
+                                        >
+                                            <option value="1">@lang('admin::app.settings.shipping_zone_rates.index.yes')</option>
+                                            <option value="0">@lang('admin::app.settings.shipping_zone_rates.index.no')</option>
+                                        </x-admin::form.control-group.control>
+                                        <x-admin::form.control-group.error control-name="is_active" />
+                                    </x-admin::form.control-group>
+
+                                    <!-- Sort Order -->
+                                    <x-admin::form.control-group>
+                                        <x-admin::form.control-group.label>
+                                            @lang('admin::app.settings.shipping_zone_rates.index.create.sort-order')
+                                        </x-admin::form.control-group.label>
+                                        <x-admin::form.control-group.control
+                                            type="text"
+                                            name="sort_order"
+                                            v-model="selectedRateData.sort_order"
+                                        />
+                                        <x-admin::form.control-group.error control-name="sort_order" />
+                                    </x-admin::form.control-group>
+                                </div>
+                            </x-slot>
+
+                            <x-slot:footer>
+                                <x-admin::button
+                                    button-type="button"
+                                    class="secondary-button mr-2"
+                                    :title="trans('admin::app.settings.shipping_zone_rates.index.cancel')"
+                                    @click="$refs.rateUpdateOrCreateModal.close()"
+                                />
+                                <x-admin::button
+                                    button-type="submit"
+                                    class="primary-button"
+                                    :title="trans('admin::app.settings.shipping_zone_rates.index.create.save-btn')"
+                                    ::loading="isLoading"
+                                    ::disabled="isLoading"
+                                />
+                            </x-slot>
+                        </x-admin::modal>
+                    </form>
+                </x-admin::form>
+
+                <!-- Import CSV Modal -->
+                <x-admin::form
+                    v-slot="{ meta, errors, handleSubmit }"
+                    as="div"
+                    ref="importModalForm"
+                >
+                    <form
+                        @submit="handleSubmit($event, importCsv)"
+                        ref="importForm"
+                    >
+                        <x-admin::modal ref="importModal">
+                            <x-slot:header>
+                                <p class="text-lg font-bold text-gray-800 dark:text-white">
+                                    @lang('admin::app.settings.shipping_zone_rates.index.import.title')
+                                </p>
+                            </x-slot>
+
+                            <x-slot:content>
+                                <div class="mb-4 text-sm text-gray-600 dark:text-gray-300">
+                                    @lang('admin::app.settings.shipping_zone_rates.index.import.description')
+                                </div>
+
+                                <x-admin::form.control-group>
+                                    <x-admin::form.control-group.label class="required">
+                                        @lang('admin::app.settings.shipping_zone_rates.index.import.file')
+                                    </x-admin::form.control-group.label>
+                                    <x-admin::form.control-group.control
+                                        type="file"
+                                        name="csv_file"
+                                        rules="required"
+                                        accept=".csv"
+                                    />
+                                    <x-admin::form.control-group.error control-name="csv_file" />
+                                </x-admin::form.control-group>
+
+                                <x-admin::form.control-group>
+                                    <label class="inline-flex items-center gap-2 cursor-pointer">
+                                        <input
+                                            type="checkbox"
+                                            name="truncate"
+                                            value="1"
+                                            class="peer hidden"
+                                        >
+                                        <span class="flex h-5 w-5 items-center justify-center rounded-md border-2 border-gray-300 transition-all peer-checked:border-blue-600 peer-checked:bg-blue-600 dark:border-gray-700">
+                                            <span class="icon-tick text-sm text-white opacity-0 peer-checked:opacity-100"></span>
+                                        </span>
+                                        <span class="text-sm text-gray-600 dark:text-gray-300">
+                                            @lang('admin::app.settings.shipping_zone_rates.index.import.truncate')
+                                        </span>
+                                    </label>
+                                </x-admin::form.control-group>
+                            </x-slot>
+
+                            <x-slot:footer>
+                                <x-admin::button
+                                    button-type="button"
+                                    class="secondary-button mr-2"
+                                    :title="trans('admin::app.settings.shipping_zone_rates.index.cancel')"
+                                    @click="$refs.importModal.close()"
+                                />
+                                <x-admin::button
+                                    button-type="submit"
+                                    class="primary-button"
+                                    :title="trans('admin::app.settings.shipping_zone_rates.index.import.upload-btn')"
+                                    ::loading="importLoading"
+                                    ::disabled="importLoading"
+                                />
+                            </x-slot>
+                        </x-admin::modal>
+                    </form>
+                </x-admin::form>
+            </div>
+        </script>
+
+        <script type="module">
+            app.component('v-shipping-zone-rates', {
+                template: '#v-shipping-zone-rates-template',
+
+                data() {
+                    return {
+                        selectedRate: 0,
+                        selectedRateData: {},
+                        isLoading: false,
+                        importLoading: false,
+                    }
+                },
+
+                computed: {
+                    gridsCount() {
+                        let count = this.$refs.datagrid.available.columns.length;
+                        if (this.$refs.datagrid.available.actions.length) {
+                            ++count;
+                        }
+                        if (this.$refs.datagrid.available.massActions.length) {
+                            ++count;
+                        }
+                        return count;
+                    },
+                },
+
+                methods: {
+                    updateOrCreate(params, { resetForm, setErrors }) {
+                        this.isLoading = true;
+
+                        let formData = new FormData(this.$refs.rateCreateForm);
+
+                        if (params.id) {
+                            formData.append('_method', 'put');
+                        }
+
+                        this.$axios.post(
+                            params.id
+                                ? "{{ route('admin.settings.shipping_zone_rates.update') }}"
+                                : "{{ route('admin.settings.shipping_zone_rates.store') }}",
+                            formData
+                        )
+                            .then((response) => {
+                                this.isLoading = false;
+                                this.$emitter.emit('add-flash', { type: 'success', message: response.data.message });
+                                this.$refs.rateUpdateOrCreateModal.close();
+                                this.$refs.datagrid.get();
+                                resetForm();
+                            })
+                            .catch(error => {
+                                this.isLoading = false;
+                                if (error.response.status == 422) {
+                                    setErrors(error.response.data.errors);
+                                }
+                            });
+                    },
+
+                    editModal(url) {
+                        this.$axios.get(url)
+                            .then((response) => {
+                                this.selectedRateData = response.data.data;
+                                this.$refs.rateUpdateOrCreateModal.toggle();
+                            })
+                            .catch(error => this.$emitter.emit('add-flash', {
+                                type: 'error', message: error.response.data.message
+                            }));
+                    },
+
+                    importCsv(params, { resetForm, setErrors }) {
+                        this.importLoading = true;
+
+                        let formData = new FormData(this.$refs.importForm);
+
+                        this.$axios.post(
+                            "{{ route('admin.settings.shipping_zone_rates.import') }}",
+                            formData
+                        )
+                            .then((response) => {
+                                this.importLoading = false;
+                                this.$emitter.emit('add-flash', { type: 'success', message: response.data.message });
+                                this.$refs.importModal.close();
+                                this.$refs.datagrid.get();
+                                resetForm();
+                            })
+                            .catch(error => {
+                                this.importLoading = false;
+                                if (error.response.status == 422) {
+                                    setErrors(error.response.data.errors);
+                                } else {
+                                    this.$emitter.emit('add-flash', {
+                                        type: 'error',
+                                        message: error.response.data.message || 'Import failed'
+                                    });
+                                }
+                            });
+                    },
+
+                    resetForm() {
+                        this.selectedRateData = {};
+                    }
+                }
+            })
+        </script>
+    @endPushOnce
+</x-admin::layouts>

+ 20 - 0
packages/Webkul/Admin/src/Routes/settings-routes.php

@@ -8,6 +8,7 @@ use Webkul\Admin\Http\Controllers\Settings\ExchangeRateController;
 use Webkul\Admin\Http\Controllers\Settings\InventorySourceController;
 use Webkul\Admin\Http\Controllers\Settings\LocaleController;
 use Webkul\Admin\Http\Controllers\Settings\RoleController;
+use Webkul\Admin\Http\Controllers\Settings\ShippingZoneRateController;
 use Webkul\Admin\Http\Controllers\Settings\Tax\TaxCategoryController;
 use Webkul\Admin\Http\Controllers\Settings\Tax\TaxRateController;
 use Webkul\Admin\Http\Controllers\Settings\ThemeController;
@@ -223,4 +224,23 @@ Route::prefix('settings')->group(function () {
             Route::get('download-error-report/{id}', 'downloadErrorReport')->name('admin.settings.data_transfer.imports.download_error_report');
         });
     });
+
+    /**
+     * Shipping Zone Rates routes.
+     */
+    Route::controller(ShippingZoneRateController::class)->prefix('shipping-zone-rates')->group(function () {
+        Route::get('', 'index')->name('admin.settings.shipping_zone_rates.index');
+
+        Route::post('create', 'store')->name('admin.settings.shipping_zone_rates.store');
+
+        Route::get('edit/{id}', 'edit')->name('admin.settings.shipping_zone_rates.edit');
+
+        Route::put('edit', 'update')->name('admin.settings.shipping_zone_rates.update');
+
+        Route::delete('edit/{id}', 'destroy')->name('admin.settings.shipping_zone_rates.delete');
+
+        Route::post('mass-delete', 'massDestroy')->name('admin.settings.shipping_zone_rates.mass_delete');
+
+        Route::post('import', 'import')->name('admin.settings.shipping_zone_rates.import');
+    });
 });

+ 98 - 0
packages/Webkul/Shipping/src/Carriers/ZoneRate.php

@@ -0,0 +1,98 @@
+<?php
+
+namespace Webkul\Shipping\Carriers;
+
+use Webkul\Checkout\Facades\Cart;
+use Webkul\Checkout\Models\CartShippingRate;
+use Webkul\Shipping\Models\ShippingZoneRate;
+
+class ZoneRate extends AbstractShipping
+{
+    /**
+     * Shipping method carrier code.
+     *
+     * @var string
+     */
+    protected $code = 'zonerate';
+
+    /**
+     * Shipping method code.
+     *
+     * @var string
+     */
+    protected $method = 'zonerate_zonerate';
+
+    /**
+     * Calculate rate for zone-based shipping.
+     *
+     * @return \Webkul\Checkout\Models\CartShippingRate|bool
+     */
+    public function calculate(): \Webkul\Checkout\Models\CartShippingRate|bool
+    {
+        if (! $this->isAvailable()) {
+            return false;
+        }
+
+        $cart = Cart::getCart();
+
+        if (! $cart) {
+            return false;
+        }
+
+        $shippingAddress = $cart->shipping_address;
+
+        if (! $shippingAddress) {
+            return false;
+        }
+
+        $countryCode = $shippingAddress->country;
+
+        // 检查该国家是否在允许列表中
+        $allowedCountries = $this->getConfigData('allowed_countries');
+        if ($allowedCountries) {
+            $allowedList = array_filter(explode(',', $allowedCountries));
+            if (! empty($allowedList) && ! in_array($countryCode, $allowedList)) {
+                return false;
+            }
+        }
+
+        $stateCode = $shippingAddress->state;
+
+        // 查询匹配的费率:先精确匹配 country+state,再匹配 country+*
+        $rate = ShippingZoneRate::active()
+            ->byCountry($countryCode)
+            ->byState($stateCode)
+            ->byPrice($cart->base_grand_total)
+            ->byRaw($countryCode, $stateCode)
+            ->first();
+
+        if (! $rate) {
+            return false;
+        }
+
+        $cartShippingRate = new CartShippingRate;
+
+        $cartShippingRate->carrier = $this->getCode();
+        $cartShippingRate->carrier_title = $rate->carrier_title;
+        $cartShippingRate->method = $this->getMethod();
+        $cartShippingRate->method_title = $rate->delivery_type;
+        $cartShippingRate->method_description = $rate->delivery_description;
+        $cartShippingRate->price = core()->convertPrice($rate->price);
+        $cartShippingRate->base_price = $rate->price;
+
+        return $cartShippingRate;
+    }
+
+    /**
+     * Get country options for the multiselect field in admin configuration.
+     */
+    public function getCountryOptions(): array
+    {
+        return core()->countries()->map(function ($country) {
+            return [
+                'title' => $country->name,
+                'value' => $country->code,
+            ];
+        })->toArray();
+    }
+}

+ 8 - 0
packages/Webkul/Shipping/src/Config/carriers.php

@@ -19,4 +19,12 @@ return [
         'default_rate' => '0',
         'class'        => 'Webkul\Shipping\Carriers\Free',
     ],
+
+    'zonerate' => [
+        'code'         => 'zonerate',
+        'title'        => 'Zone Rate',
+        'description'  => 'Zone-based shipping rates that vary by destination country and order subtotal.',
+        'active'       => true,
+        'class'        => 'Webkul\Shipping\Carriers\ZoneRate',
+    ],
 ];

+ 40 - 0
packages/Webkul/Shipping/src/Config/system.php

@@ -0,0 +1,40 @@
+<?php
+
+return [
+    [
+        'key'    => 'sales.carriers.zonerate',
+        'name'   => 'admin::app.configuration.index.sales.shipping-methods.zone-rate.page-title',
+        'info'   => 'admin::app.configuration.index.sales.shipping-methods.zone-rate.title-info',
+        'sort'   => 3,
+        'fields' => [
+            [
+                'name'          => 'title',
+                'title'         => 'admin::app.configuration.index.sales.shipping-methods.zone-rate.title',
+                'type'          => 'text',
+                'depends'       => 'active:1',
+                'validation'    => 'required_if:active,1',
+                'channel_based' => true,
+                'locale_based'  => true,
+            ], [
+                'name'          => 'description',
+                'title'         => 'admin::app.configuration.index.sales.shipping-methods.zone-rate.description',
+                'type'          => 'textarea',
+                'channel_based' => true,
+                'locale_based'  => true,
+            ],             [
+                'name'          => 'allowed_countries',
+                'title'         => 'admin::app.configuration.index.sales.shipping-methods.zone-rate.allowed-countries',
+                'type'          => 'multiselect',
+                'options'       => 'Webkul\Shipping\Carriers\ZoneRate@getCountryOptions',
+                'channel_based' => true,
+                'locale_based'  => false,
+            ], [
+                'name'          => 'active',
+                'title'         => 'admin::app.configuration.index.sales.shipping-methods.zone-rate.status',
+                'type'          => 'boolean',
+                'channel_based' => true,
+                'locale_based'  => false,
+            ],
+        ],
+    ],
+];

+ 5 - 0
packages/Webkul/Shipping/src/Contracts/ShippingZoneRate.php

@@ -0,0 +1,5 @@
+<?php
+
+namespace Webkul\Shipping\Contracts;
+
+interface ShippingZoneRate {}

+ 38 - 0
packages/Webkul/Shipping/src/Database/Migrations/2026_06_24_000000_create_shipping_zone_rates_table.php

@@ -0,0 +1,38 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::create('shipping_zone_rates', function (Blueprint $table) {
+            $table->increments('id');
+            $table->string('carrier_code');
+            $table->string('carrier_title');
+            $table->string('country_code')->nullable()->default('*');
+            $table->string('state_code')->nullable()->default('*');
+            $table->decimal('price', 12, 4)->default(0);
+            $table->string('delivery_type');
+            $table->string('delivery_description')->nullable();
+            $table->boolean('is_active')->default(true);
+            $table->integer('sort_order')->default(0);
+            $table->timestamps();
+
+            $table->index(['carrier_code', 'country_code', 'state_code']);
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('shipping_zone_rates');
+    }
+};

+ 33 - 0
packages/Webkul/Shipping/src/Database/Migrations/2026_06_24_100000_add_order_subtotal_to_shipping_zone_rates.php

@@ -0,0 +1,33 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('shipping_zone_rates', function (Blueprint $table) {
+            if (!Schema::hasColumn('shipping_zone_rates', 'order_subtotal_from')) {
+                $table->decimal('order_subtotal_from', 12, 4)->default(0)->after('state_code');
+            }
+            if (!Schema::hasColumn('shipping_zone_rates', 'order_subtotal_to')) {
+                $table->decimal('order_subtotal_to', 12, 4)->default(0)->after('order_subtotal_from');
+            }
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('shipping_zone_rates', function (Blueprint $table) {
+            $table->dropColumn(['order_subtotal_from', 'order_subtotal_to']);
+        });
+    }
+};

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1278 - 0
packages/Webkul/Shipping/src/Database/Seeders/ShippingZoneRateSeeder.php


+ 209 - 0
packages/Webkul/Shipping/src/Models/ShippingZoneRate.php

@@ -0,0 +1,209 @@
+<?php
+
+namespace Webkul\Shipping\Models;
+
+use Illuminate\Database\Eloquent\Model;
+use Webkul\Shipping\Contracts\ShippingZoneRate as ShippingZoneRateContract;
+
+class ShippingZoneRate extends Model implements ShippingZoneRateContract
+{
+    /**
+     * The attributes that are mass assignable.
+     *
+     * @var array
+     */
+    protected $fillable = [
+        'carrier_code',
+        'carrier_title',
+        'country_code',
+        'state_code',
+        'order_subtotal_from',
+        'order_subtotal_to',
+        'price',
+        'delivery_type',
+        'delivery_description',
+        'is_active',
+        'sort_order',
+    ];
+
+    /**
+     * The attributes that should be cast.
+     *
+     * @var array
+     */
+    protected $casts = [
+        'price'      => 'float',
+        'is_active'  => 'boolean',
+        'sort_order' => 'integer',
+    ];
+
+    /**
+     * Scope a query to only include active rates.
+     */
+    public function scopeActive($query)
+    {
+        return $query->where('is_active', true);
+    }
+
+    /**
+     * Scope a query to filter by country code.
+     */
+    public function scopeByCountry($query, string $countryCode)
+    {
+        if (empty($countryCode)) {
+            return $query->where('country_code', '*');
+        }
+
+        return $query->where(function ($q) use ($countryCode) {
+            $q->where('country_code', $countryCode)
+                ->orWhere('country_code', '*');
+        });
+    }
+
+    /**
+     * Scope a query to filter by state code.
+     */
+    public function scopeByState($query, ?string $stateCode)
+    {
+        if (empty($stateCode)) {
+            return $query->where('state_code', '*');
+        }
+
+        return $query->where(function ($q) use ($stateCode) {
+            $q->where('state_code', $stateCode)
+              ->orWhere('state_code', '*');
+        });
+    }
+
+    public function scopeByPrice($query, $price)
+    {
+        return $query->where('order_subtotal_from', '<=', $price)
+                     ->where('order_subtotal_to', '>=', $price);
+    }
+
+    public function scopeByRaw($query, string $countryCode, ?string $stateCode)
+    {
+        $stateCode = $stateCode ?? '';
+
+        return $query->orderByRaw(
+            'CASE WHEN `country_code` = ? THEN 0 ELSE 1 END, CASE WHEN `state_code` = ? THEN 0 ELSE 1 END',
+            [$countryCode, $stateCode]
+        );
+    }
+    /**
+     * Scope a query to filter by carrier code.
+     */
+    public function scopeByCarrier($query, string $carrierCode)
+    {
+        return $query->where('carrier_code', $carrierCode);
+    }
+
+    /**
+     * 从 CSV 文件导入运输费率数据。
+     *
+     * CSV 列顺序:Country, Region/State, City, Zip/Postal Code From,
+     * Zip/Postal Code To, Order Subtotal From, Order Subtotal To, Shipping Price, Delivery Type
+     *
+     * @param string $filePath  CSV 文件的绝对路径
+     * @param bool   $truncate  是否先清空表
+     * @return array 返回导入统计 ['total' => int, 'inserted' => int, 'errors' => array]
+     */
+    public static function importFromCsv(string $filePath, bool $truncate = true): array
+    {
+        if (!file_exists($filePath)) {
+            throw new \InvalidArgumentException("CSV file not found: {$filePath}");
+        }
+
+        if ($truncate) {
+            static::truncate();
+        }
+
+        $handle = fopen($filePath, 'r');
+        if (!$handle) {
+            throw new \RuntimeException("Cannot open CSV file: {$filePath}");
+        }
+
+        // 读取第一行作为表头
+        $headers = fgetcsv($handle);
+        if (!$headers) {
+            fclose($handle);
+            throw new \RuntimeException('CSV file is empty or has no header row.');
+        }
+
+        // 去除 BOM 和空白
+        $headers = array_map(function ($h) {
+            return trim(str_replace("\xEF\xBB\xBF", '', $h));
+        }, $headers);
+
+        $total    = 0;
+        $inserted = 0;
+        $errors   = [];
+
+        $sortOrderMap = []; // carrier_code => sort_order 计数器
+
+        while (($row = fgetcsv($handle)) !== false) {
+            $total++;
+
+            // 跳过空行
+            if (empty(array_filter($row))) {
+                continue;
+            }
+
+            try {
+                $data = array_combine($headers, $row);
+
+                $countryCode = trim($data['Country'] ?? '*') ?: '*';
+                $stateCode   = trim($data['Region/State'] ?? '*') ?: '*';
+                $price       = (float) ($data['Shipping Price'] ?? 0);
+                $from        = (float) ($data['Order Subtotal From'] ?? 0);
+                $to          = (float) ($data['Order Subtotal To'] ?? 0);
+
+                // 从 Delivery Type 提取 carrier code 和 description
+                $deliveryType = trim($data['Delivery Type'] ?? '');
+
+                // 生成 carrier_code:取 Delivery Type 括号前的部分,小写+下划线
+                preg_match('/^([^\(]+)/', $deliveryType, $matches);
+                $carrierTitle = trim($matches[1] ?? $deliveryType);
+                $carrierCode  = strtolower(preg_replace('/[^a-zA-Z0-9]+/', '_', trim($carrierTitle)));
+                $carrierCode  = trim($carrierCode, '_');
+
+                // sort_order:同一 carrier_code 按出现顺序递增
+                if (!isset($sortOrderMap[$carrierCode])) {
+                    $sortOrderMap[$carrierCode] = 0;
+                }
+                $sortOrderMap[$carrierCode]++;
+                $sortOrder = $sortOrderMap[$carrierCode];
+
+                static::create([
+                    'carrier_code'         => $carrierCode,
+                    'carrier_title'        => $carrierTitle,
+                    'country_code'         => $countryCode,
+                    'state_code'           => $stateCode,
+                    'order_subtotal_from'  => $from,
+                    'order_subtotal_to'    => $to,
+                    'price'                => $price,
+                    'delivery_type'        => $deliveryType,
+                    'delivery_description' => $deliveryType,
+                    'is_active'            => true,
+                    'sort_order'           => $sortOrder,
+                ]);
+
+                $inserted++;
+            } catch (\Throwable $e) {
+                $errors[] = [
+                    'line'  => $total + 1, // +1 因为第一行是表头
+                    'row'   => $row,
+                    'error' => $e->getMessage(),
+                ];
+            }
+        }
+
+        fclose($handle);
+
+        return [
+            'total'    => $total,
+            'inserted' => $inserted,
+            'errors'   => $errors,
+        ];
+    }
+}

+ 7 - 0
packages/Webkul/Shipping/src/Models/ShippingZoneRateProxy.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace Webkul\Shipping\Models;
+
+use Konekt\Concord\Proxies\ModelProxy;
+
+class ShippingZoneRateProxy extends ModelProxy {}

+ 4 - 1
packages/Webkul/Shipping/src/Providers/ModuleServiceProvider.php

@@ -3,6 +3,7 @@
 namespace Webkul\Shipping\Providers;
 
 use Webkul\Core\Providers\CoreModuleServiceProvider;
+use Webkul\Shipping\Models\ShippingZoneRate;
 
 class ModuleServiceProvider extends CoreModuleServiceProvider
 {
@@ -11,5 +12,7 @@ class ModuleServiceProvider extends CoreModuleServiceProvider
      *
      * @var array
      */
-    protected $models = [];
+    protected $models = [
+        ShippingZoneRate::class,
+    ];
 }

+ 15 - 0
packages/Webkul/Shipping/src/Providers/ShippingServiceProvider.php

@@ -18,6 +18,18 @@ class ShippingServiceProvider extends ServiceProvider
         $this->registerConfig();
     }
 
+    /**
+     * Bootstrap services.
+     *
+     * @return void
+     */
+    public function boot()
+    {
+        $this->loadMigrationsFrom(
+            dirname(__DIR__).'/Database/Migrations'
+        );
+    }
+
     /**
      * Register package config.
      *
@@ -28,5 +40,8 @@ class ShippingServiceProvider extends ServiceProvider
         $this->mergeConfigFrom(
             dirname(__DIR__).'/Config/carriers.php', 'carriers'
         );
+        $this->mergeConfigFrom(
+            dirname(__DIR__) . '/Config/system.php', 'core'
+        );
     }
 }