From 4a86be6cbfc4dadc3573966213a6be3ed47be069 Mon Sep 17 00:00:00 2001
From: sky121113
Date: Tue, 16 Jun 2026 13:41:22 +0800
Subject: [PATCH] =?UTF-8?q?[FEAT]=20=E5=95=86=E5=93=81=E5=8C=AF=E5=85=A5?=
=?UTF-8?q?=E6=94=AF=E6=8F=B4=E5=9C=96=E7=89=87=EF=BC=9AExcel=20=E5=9C=96?=
=?UTF-8?q?=E7=89=87=E6=AA=94=E5=90=8D=E6=AC=84=20+=20=E5=9C=96=E7=89=87?=
=?UTF-8?q?=E5=A3=93=E7=B8=AE=E6=AA=94=E8=83=8C=E6=99=AF=E8=99=95=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
1. ProductImportService 新增 deferImages 模式與第 18 欄「圖片檔名」,匯入時收集 pending_images 供背景處理,並保留同步轉檔路徑。
2. 新增 AttachProductImagesJob:背景逐筆將圖片轉為 WebP 入庫、更新時刪除舊圖、重建目錄快取、清除暫存資料夾並寫入操作日誌。
3. ProductController::import 新增圖片 zip 上傳(上限 60MB)與安全解壓 extractImageZip:副檔名白名單、getimagesize 內容驗證、basename 防路徑穿越、項目數與單檔及總量上限防 zip bomb,解壓後派發背景 Job。
4. 匯入 Modal 新增選填的圖片 .zip 上傳欄位與背景處理提示文字。
5. config/queue.php 的 redis retry_after 提高至 1830(可由 REDIS_QUEUE_RETRY_AFTER 覆寫),避免長時間圖片任務未完成即被重複派發造成重複處理。
6. 三語系新增圖片匯入相關文案並對齊排序。
Co-Authored-By: Claude Opus 4.8 (1M context)
---
.../Controllers/Admin/ProductController.php | 118 +++++++++++++++++-
app/Jobs/Product/AttachProductImagesJob.php | 99 +++++++++++++++
app/Services/ProductImportService.php | 69 +++++++++-
config/queue.php | 3 +-
lang/en.json | 8 ++
lang/ja.json | 8 ++
lang/zh_TW.json | 8 ++
.../views/admin/products/index.blade.php | 33 ++++-
8 files changed, 336 insertions(+), 10 deletions(-)
create mode 100644 app/Jobs/Product/AttachProductImagesJob.php
diff --git a/app/Http/Controllers/Admin/ProductController.php b/app/Http/Controllers/Admin/ProductController.php
index e72eae8..0717ea8 100644
--- a/app/Http/Controllers/Admin/ProductController.php
+++ b/app/Http/Controllers/Admin/ProductController.php
@@ -594,6 +594,7 @@ class ProductController extends Controller
$request->validate([
'file' => 'required|file|mimes:xlsx|max:5120',
'company_id' => 'nullable|exists:companies,id',
+ 'images' => 'nullable|file|mimes:zip|max:61440', // 60MB;落在 php 預設 100M 與 Cloudflare 100M 之內,免改機器設定
]);
$companyId = $request->get('company_id') ?: auth()->user()->company_id;
@@ -605,23 +606,128 @@ class ProductController extends Controller
], 422);
}
- $results = $importService->import(
- $request->file('file')->getRealPath(),
- $companyId,
- auth()->id()
- );
+ // 有圖片壓縮檔:解壓到暫存目錄,圖片轉檔交由背景 Job 處理 (避免大量同步轉檔逾時)
+ $imageDir = $request->hasFile('images') ? $this->extractImageZip($request->file('images')) : null;
+
+ try {
+ $results = $importService->import(
+ $request->file('file')->getRealPath(),
+ $companyId,
+ auth()->id(),
+ $imageDir,
+ deferImages: $imageDir !== null
+ );
+ } catch (\Throwable $e) {
+ if ($imageDir && is_dir($imageDir)) {
+ \Illuminate\Support\Facades\File::deleteDirectory($imageDir);
+ }
+ throw $e;
+ }
foreach ($results['company_ids'] as $importedCompanyId) {
$this->catalogService->rebuildCache($importedCompanyId);
}
+ // 派發背景圖片處理 Job (Job 負責轉檔 + 重建快取 + 刪暫存目錄)
+ $queuedImages = 0;
+ if ($imageDir) {
+ if (!empty($results['pending_images'])) {
+ $queuedImages = count($results['pending_images']);
+ \App\Jobs\Product\AttachProductImagesJob::dispatch(
+ $results['pending_images'],
+ $imageDir,
+ $companyId,
+ auth()->id()
+ );
+ } else {
+ // 沒有任何圖片需處理,立即清掉暫存目錄
+ \Illuminate\Support\Facades\File::deleteDirectory($imageDir);
+ }
+ }
+
+ $message = __(':count products imported successfully', ['count' => $results['success_count']]);
+ if ($queuedImages > 0) {
+ $message .= ' · ' . __(':count images are being processed in the background', ['count' => $queuedImages]);
+ }
+
return response()->json([
'success' => true,
- 'message' => __(':count products imported successfully', ['count' => $results['success_count']]),
+ 'message' => $message,
'results' => $results,
]);
}
+ /**
+ * 將上傳的圖片壓縮檔安全解壓到暫存目錄,回傳目錄路徑。
+ * 防護:副檔名白名單、basename 防穿越、項目數/單檔/總量上限 (防 zip bomb)、內容驗證為圖片。
+ */
+ private function extractImageZip(\Illuminate\Http\UploadedFile $zipFile): string
+ {
+ $allowedExt = ['jpg', 'jpeg', 'png', 'webp'];
+ $maxEntries = 2000;
+ $maxFileBytes = 5 * 1024 * 1024; // 單檔解壓上限 5MB
+ $maxTotalBytes = 200 * 1024 * 1024; // 總解壓上限 200MB
+
+ $dir = storage_path('app/tmp/product_import_' . Str::uuid()->toString());
+ \Illuminate\Support\Facades\File::ensureDirectoryExists($dir);
+
+ $zip = new \ZipArchive();
+ if ($zip->open($zipFile->getRealPath()) !== true) {
+ throw new \RuntimeException(__('Unable to read the image archive'));
+ }
+
+ if ($zip->numFiles > $maxEntries) {
+ $zip->close();
+ throw new \RuntimeException(__('The image archive contains too many files'));
+ }
+
+ $totalBytes = 0;
+
+ for ($i = 0; $i < $zip->numFiles; $i++) {
+ $stat = $zip->statIndex($i);
+ $entryName = $stat['name'];
+
+ // 跳過目錄與含路徑穿越的項目,只取檔名
+ if (str_ends_with($entryName, '/') || str_contains($entryName, '..')) {
+ continue;
+ }
+
+ $base = basename($entryName);
+ $ext = strtolower(pathinfo($base, PATHINFO_EXTENSION));
+ if (!in_array($ext, $allowedExt, true)) {
+ continue;
+ }
+
+ if ($stat['size'] > $maxFileBytes) {
+ continue;
+ }
+
+ $totalBytes += $stat['size'];
+ if ($totalBytes > $maxTotalBytes) {
+ $zip->close();
+ throw new \RuntimeException(__('The image archive is too large when extracted'));
+ }
+
+ $stream = $zip->getStream($entryName);
+ if ($stream === false) {
+ continue;
+ }
+ $contents = stream_get_contents($stream);
+ fclose($stream);
+
+ // 內容驗證:確認真為圖片,擋掉偽裝檔
+ if ($contents === false || getimagesizefromstring($contents) === false) {
+ continue;
+ }
+
+ file_put_contents($dir . '/' . $base, $contents);
+ }
+
+ $zip->close();
+
+ return $dir;
+ }
+
/**
* 同步商品目錄至全公司機台 (Phase 2)
*/
diff --git a/app/Jobs/Product/AttachProductImagesJob.php b/app/Jobs/Product/AttachProductImagesJob.php
new file mode 100644
index 0000000..b4ac695
--- /dev/null
+++ b/app/Jobs/Product/AttachProductImagesJob.php
@@ -0,0 +1,99 @@
+ $pendingImages
+ */
+ public function __construct(
+ protected array $pendingImages,
+ protected string $imageDir,
+ protected ?int $companyId = null,
+ protected ?int $userId = null
+ ) {}
+
+ public function handle(ProductCatalogService $catalogService): void
+ {
+ $success = 0;
+ $missing = [];
+
+ try {
+ foreach ($this->pendingImages as $item) {
+ $product = Product::withoutGlobalScopes()->find($item['product_id']);
+ if (!$product) {
+ $missing[] = $item;
+ continue;
+ }
+
+ $path = rtrim($this->imageDir, '/') . '/' . basename($item['filename']);
+ if (!is_file($path) || getimagesize($path) === false) {
+ $missing[] = $item;
+ continue;
+ }
+
+ try {
+ $oldImage = $product->image_url;
+ $file = new UploadedFile($path, basename($path), null, null, true);
+ $storedPath = $this->storeAsWebp($file, 'products', 80, 320, 320);
+ $product->update(['image_url' => Storage::url($storedPath)]);
+
+ if ($oldImage) {
+ Storage::disk('public')->delete(str_replace('/storage/', '', $oldImage));
+ }
+ $success++;
+ } catch (\Throwable $e) {
+ $missing[] = $item;
+ }
+ }
+
+ // 圖片已更新,重建目錄快取讓機台拿到新圖
+ if ($this->companyId) {
+ $catalogService->rebuildCache($this->companyId);
+ }
+
+ SystemOperationLog::create([
+ 'company_id' => $this->companyId,
+ 'user_id' => $this->userId,
+ 'module' => 'product',
+ 'action' => 'update',
+ 'target_type' => Product::class,
+ 'note' => 'product_import_images_processed',
+ 'new_values' => [
+ 'image_success' => $success,
+ 'image_missing' => count($missing),
+ 'missing_detail' => $missing,
+ ],
+ ]);
+ } finally {
+ // 無論成敗,清掉暫存圖片資料夾
+ if (is_dir($this->imageDir)) {
+ File::deleteDirectory($this->imageDir);
+ }
+ }
+ }
+}
diff --git a/app/Services/ProductImportService.php b/app/Services/ProductImportService.php
index ad80d60..5958d0b 100644
--- a/app/Services/ProductImportService.php
+++ b/app/Services/ProductImportService.php
@@ -6,7 +6,10 @@ use App\Models\Product\Product;
use App\Models\Product\ProductCategory;
use App\Models\System\SystemOperationLog;
use App\Models\System\Translation;
+use App\Traits\ImageHandler;
+use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use OpenSpout\Reader\XLSX\Reader;
@@ -15,7 +18,14 @@ use OpenSpout\Common\Entity\Row;
class ProductImportService
{
- public function import(string $filePath, ?int $companyId = null, ?int $userId = null): array
+ use ImageHandler;
+
+ /**
+ * @param string|null $imageDir 已解壓的圖片資料夾路徑 (Excel「圖片檔名」欄對應此資料夾內檔案)
+ * @param bool $deferImages true 時不在此同步轉檔,改將 (product_id, filename) 收集到 results['pending_images']
+ * 供呼叫端丟背景 Job 處理,避免大量圖片同步轉檔逾時。
+ */
+ public function import(string $filePath, ?int $companyId = null, ?int $userId = null, ?string $imageDir = null, bool $deferImages = false): array
{
$reader = new Reader();
$reader->open($filePath);
@@ -27,6 +37,9 @@ class ProductImportService
'error_count' => 0,
'errors' => [],
'company_ids' => [],
+ 'image_success' => 0,
+ 'image_missing' => [],
+ 'pending_images' => [],
];
$rowCount = 0;
@@ -65,6 +78,7 @@ class ProductImportService
'points_half' => 'nullable|integer|min:0',
'points_half_amount' => 'nullable|integer|min:0',
'is_active' => 'nullable|boolean',
+ 'image_filename' => 'nullable|string|max:255',
]);
if ($validator->fails()) {
@@ -73,7 +87,7 @@ class ProductImportService
}
try {
- DB::transaction(function () use ($data, $rowCount, $userId, &$results) {
+ DB::transaction(function () use ($data, $rowCount, $userId, $imageDir, $deferImages, &$results) {
$categoryId = $this->resolveCategoryId($data['category'], $data['company_id']);
$lookup = $this->buildProductLookup($data);
$product = Product::withoutGlobalScopes()->where($lookup)->first();
@@ -116,6 +130,18 @@ class ProductImportService
$note = 'product_import_created';
}
+ if ($deferImages) {
+ if ($imageDir && $data['image_filename']) {
+ $results['pending_images'][] = [
+ 'product_id' => $product->id,
+ 'filename' => $data['image_filename'],
+ 'row' => $rowCount,
+ ];
+ }
+ } else {
+ $this->attachImage($product, $data['image_filename'], $imageDir, $rowCount, $results);
+ }
+
$results['success_count']++;
if ($product->company_id) {
$results['company_ids'][$product->company_id] = $product->company_id;
@@ -168,6 +194,7 @@ class ProductImportService
'半額點數(選填)',
'半額點數折抵金額(選填)',
'是否啟用(1/0,選填)',
+ '圖片檔名(選填,需一併上傳圖片壓縮檔)',
]));
$writer->addRow(Row::fromValues([
@@ -188,6 +215,7 @@ class ProductImportService
400,
40,
1,
+ 'latte.png',
]));
$writer->close();
@@ -214,9 +242,46 @@ class ProductImportService
'points_half' => (int) $this->numericValue($cells[14] ?? null, 0),
'points_half_amount' => (int) $this->numericValue($cells[15] ?? null, 0),
'is_active' => $this->booleanValue($cells[16] ?? null),
+ 'image_filename' => $this->stringValue($cells[17] ?? null),
];
}
+ /**
+ * 依「圖片檔名」欄將已解壓資料夾中的圖片轉 WebP 入庫並寫回 image_url。
+ * 失敗 (找不到檔/非圖) 只記錄至 image_missing,不影響商品本身寫入。
+ */
+ private function attachImage(Product $product, ?string $filename, ?string $imageDir, int $rowCount, array &$results): void
+ {
+ if (!$filename || !$imageDir) {
+ return;
+ }
+
+ try {
+ $path = rtrim($imageDir, '/') . '/' . basename($filename);
+
+ if (!is_file($path) || getimagesize($path) === false) {
+ $results['image_missing'][] = ['row' => $rowCount, 'filename' => $filename];
+ return;
+ }
+
+ $oldImage = $product->image_url;
+
+ $file = new UploadedFile($path, basename($path), null, null, true);
+ $storedPath = $this->storeAsWebp($file, 'products', 80, 320, 320);
+
+ $product->update(['image_url' => Storage::url($storedPath)]);
+
+ if ($oldImage) {
+ $oldPath = str_replace('/storage/', '', $oldImage);
+ Storage::disk('public')->delete($oldPath);
+ }
+
+ $results['image_success']++;
+ } catch (\Throwable $e) {
+ $results['image_missing'][] = ['row' => $rowCount, 'filename' => $filename];
+ }
+ }
+
private function isEmptyRow(array $data): bool
{
return empty($data['category'])
diff --git a/config/queue.php b/config/queue.php
index 01c6b05..7b9c2b0 100644
--- a/config/queue.php
+++ b/config/queue.php
@@ -66,7 +66,8 @@ return [
'driver' => 'redis',
'connection' => 'default',
'queue' => env('REDIS_QUEUE', 'default'),
- 'retry_after' => 90,
+ // 須大於最長 Job 的 timeout(AttachProductImagesJob 1800s),否則長任務未跑完即被重派造成重複處理
+ 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 1830),
'block_for' => null,
'after_commit' => false,
],
diff --git a/lang/en.json b/lang/en.json
index 375c531..17c6bc4 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -6,6 +6,9 @@
"30s": "30s",
"60 Seconds": "60 Seconds",
"60s": "60s",
+ ":count image files not found": ":count image files not found",
+ ":count images are being processed in the background": ":count images are being processed in the background",
+ ":count images matched": ":count images matched",
":count products imported successfully": ":count products imported successfully",
":count staff cards imported successfully": ":count staff cards imported successfully",
"A new verification link has been sent to your email address.": "A new verification link has been sent to your email address.",
@@ -1407,6 +1410,7 @@
"Optional remarks": "Optional remarks",
"Optional remarks...": "Optional remarks...",
"Optional. If you fill in any field, all fields below become required.": "Optional. If you fill in any field, all fields below become required.",
+ "Optional. Reference each filename in the image column, then upload them here as a .zip (Max 60MB)": "Optional. Reference each filename in the image column, then upload them here as a .zip (Max 60MB)",
"Or Choose Custom Date": "Or Choose Custom Date",
"Order Details": "Order Details",
"Order Info": "Order Info",
@@ -1594,6 +1598,7 @@
"Product Details": "Product Details",
"Product ID": "Product ID",
"Product Image": "Product Image",
+ "Product Images (ZIP)": "Product Images (ZIP)",
"Product Info": "Product Info",
"Product Items": "Product Items",
"Product List": "Product List",
@@ -2092,6 +2097,8 @@
"The Super Admin role cannot be deleted.": "The Super Admin role cannot be deleted.",
"The Super Admin role is immutable.": "The Super Admin role is immutable.",
"The Super Admin role name cannot be modified.": "The Super Admin role name cannot be modified.",
+ "The image archive contains too many files": "The image archive contains too many files",
+ "The image archive is too large when extracted": "The image archive is too large when extracted",
"The image is too large. Please upload an image smaller than 1MB.": "The image is too large. Please upload an image smaller than 1MB.",
"The image is too large. Please upload an image smaller than 5MB.": "The image is too large. Please upload an image smaller than 5MB.",
"The machine will be notified to fetch the latest system settings. It may take a moment to apply.": "The machine will be notified to fetch the latest system settings. It may take a moment to apply.",
@@ -2182,6 +2189,7 @@
"Type": "Type",
"Type to search or leave blank for system defaults.": "Type to search or leave blank for system defaults.",
"UI Elements": "UI Elements",
+ "Unable to read the image archive": "Unable to read the image archive",
"Unassigned": "Unassigned",
"Unassigned / No Change": "Unassigned / No Change",
"Unauthorized": "Unauthorized",
diff --git a/lang/ja.json b/lang/ja.json
index bdb48e8..dcc6d43 100644
--- a/lang/ja.json
+++ b/lang/ja.json
@@ -6,6 +6,9 @@
"30s": "30秒",
"60 Seconds": "60秒",
"60s": "60秒",
+ ":count image files not found": ":count 個の画像ファイルが見つかりません",
+ ":count images are being processed in the background": ":count 枚の画像をバックグラウンドで処理しています",
+ ":count images matched": ":count 枚の画像を紐付けました",
":count products imported successfully": ":count products imported successfully",
":count staff cards imported successfully": ":count 枚のスタッフカードが正常にインポートされました",
"A new verification link has been sent to your email address.": "新しい確認リンクがメールアドレスに送信されました。",
@@ -1407,6 +1410,7 @@
"Optional remarks": "任意の備考",
"Optional remarks...": "任意の備考...",
"Optional. If you fill in any field, all fields below become required.": "任意。いずれかの項目を入力すると、以下のすべての項目が必須になります。",
+ "Optional. Reference each filename in the image column, then upload them here as a .zip (Max 60MB)": "任意。画像のファイル名を「画像ファイル名」列に記入し、画像を .zip にまとめてアップロードしてください (最大 60MB)",
"Or Choose Custom Date": "または日付を選択",
"Order Details": "注文詳細",
"Order Info": "注文情報",
@@ -1594,6 +1598,7 @@
"Product Details": "商品詳細",
"Product ID": "商品ID",
"Product Image": "商品画像",
+ "Product Images (ZIP)": "商品画像 (ZIP)",
"Product Info": "商品情報",
"Product Items": "Product Items",
"Product List": "商品リスト",
@@ -2092,6 +2097,8 @@
"The Super Admin role cannot be deleted.": "特権管理者ロールは削除できません。",
"The Super Admin role is immutable.": "特権管理者ロールは変更できません。",
"The Super Admin role name cannot be modified.": "特権管理者ロールの名前は変更できません。",
+ "The image archive contains too many files": "画像アーカイブのファイル数が多すぎます",
+ "The image archive is too large when extracted": "画像アーカイブの展開後のサイズが大きすぎます",
"The image is too large. Please upload an image smaller than 1MB.": "画像サイズが大きすぎます。1MB未満の画像をアップロードしてください。",
"The image is too large. Please upload an image smaller than 5MB.": "画像サイズが大きすぎます。5MB未満の画像をアップロードしてください。",
"The machine will be notified to fetch the latest system settings. It may take a moment to apply.": "最新のシステム設定を取得するよう機台に通知しました。反映まで少々時間がかかる場合があります。",
@@ -2182,6 +2189,7 @@
"Type": "タイプ",
"Type to search or leave blank for system defaults.": "入力して検索、または空白のままでシステムデフォルトを使用します。",
"UI Elements": "UI要素",
+ "Unable to read the image archive": "画像アーカイブを読み込めません",
"Unassigned": "未割り当て",
"Unassigned / No Change": "未割り当て / 変更なし",
"Unauthorized": "未授権",
diff --git a/lang/zh_TW.json b/lang/zh_TW.json
index 42a4430..0d96db4 100644
--- a/lang/zh_TW.json
+++ b/lang/zh_TW.json
@@ -6,6 +6,9 @@
"30s": "30秒",
"60 Seconds": "60 秒",
"60s": "60秒",
+ ":count image files not found": ":count 個圖片檔找不到",
+ ":count images are being processed in the background": ":count 張圖片背景處理中",
+ ":count images matched": "已配對 :count 張圖片",
":count products imported successfully": "成功匯入 :count 筆商品",
":count staff cards imported successfully": "成功匯入 :count 張員工識別卡",
"A new verification link has been sent to your email address.": "已將新的驗證連結發送至您的電子郵件地址。",
@@ -1407,6 +1410,7 @@
"Optional remarks": "選填備註",
"Optional remarks...": "選填備註...",
"Optional. If you fill in any field, all fields below become required.": "選填。只要填寫任一欄位,以下欄位即全部必填。",
+ "Optional. Reference each filename in the image column, then upload them here as a .zip (Max 60MB)": "選填。在「圖片檔名」欄填寫檔名,再將圖片打包成 .zip 一併上傳 (上限 60MB)",
"Or Choose Custom Date": "或選擇自訂日期",
"Order Details": "訂單詳情",
"Order Info": "單據資訊",
@@ -1594,6 +1598,7 @@
"Product Details": "商品明細",
"Product ID": "商品 ID",
"Product Image": "商品圖片",
+ "Product Images (ZIP)": "商品圖片壓縮檔 (ZIP)",
"Product Info": "商品資訊",
"Product Items": "商品品項",
"Product List": "商品清單",
@@ -2092,6 +2097,8 @@
"The Super Admin role cannot be deleted.": "超級管理員角色不可刪除。",
"The Super Admin role is immutable.": "超級管理員角色不可修改。",
"The Super Admin role name cannot be modified.": "超級管理員角色的名稱無法修改。",
+ "The image archive contains too many files": "圖片壓縮檔內的檔案數量過多",
+ "The image archive is too large when extracted": "圖片壓縮檔解壓後的體積過大",
"The image is too large. Please upload an image smaller than 1MB.": "圖片檔案太大,請上傳小於 1MB 的圖片。",
"The image is too large. Please upload an image smaller than 5MB.": "圖片檔案太大,請上傳小於 5MB 的圖片。",
"The machine will be notified to fetch the latest system settings. It may take a moment to apply.": "已通知機台回抓最新系統設定,套用可能需要一點時間。",
@@ -2182,6 +2189,7 @@
"Type": "類型",
"Type to search or leave blank for system defaults.": "輸入關鍵字搜尋,或留空以使用系統預設。",
"UI Elements": "UI元素",
+ "Unable to read the image archive": "無法讀取圖片壓縮檔",
"Unassigned": "未指派",
"Unassigned / No Change": "未指派 / 不變動",
"Unauthorized": "未授權",
diff --git a/resources/views/admin/products/index.blade.php b/resources/views/admin/products/index.blade.php
index 3b94873..3533c9f 100644
--- a/resources/views/admin/products/index.blade.php
+++ b/resources/views/admin/products/index.blade.php
@@ -188,6 +188,27 @@ hover:bg-slate-100 dark:hover:bg-white/5 rounded-lg flex items-center justify-be
+
+
+
+
+
+ {{ __('Optional. Reference each filename in the image column, then upload them here as a .zip (Max 60MB)') }}
+
+
+
{{ __('Download Template') }}
@@ -827,7 +848,7 @@ hover:bg-slate-100 dark:hover:bg-white/5 rounded-lg flex items-center justify-be
names: { zh_TW: '', en: '', ja: '' },
company_id: ''
},
- importFormFields: { company_id: '{{ auth()->user()->company_id ?? "" }}', file: null },
+ importFormFields: { company_id: '{{ auth()->user()->company_id ?? "" }}', file: null, images: null },
init() {
this.categories = JSON.parse(this.$el.dataset.categories || '[]');
@@ -1265,6 +1286,9 @@ hover:bg-slate-100 dark:hover:bg-white/5 rounded-lg flex items-center justify-be
if (this.importFormFields.company_id) {
formData.append('company_id', this.importFormFields.company_id);
}
+ if (this.importFormFields.images) {
+ formData.append('images', this.importFormFields.images);
+ }
try {
const response = await fetch(`{{ route($baseRoute . '.import') }}`, {
@@ -1280,8 +1304,15 @@ hover:bg-slate-100 dark:hover:bg-white/5 rounded-lg flex items-center justify-be
if (data.success) {
this.isImportModalOpen = false;
this.importFormFields.file = null;
+ this.importFormFields.images = null;
const input = document.getElementById('product-import-file');
if (input) input.value = '';
+ const imagesInput = document.getElementById('product-import-images');
+ if (imagesInput) imagesInput.value = '';
+
+ if (data.results.image_missing && data.results.image_missing.length > 0) {
+ console.warn('Image files not found for some rows:', data.results.image_missing);
+ }
sessionStorage.setItem('productImportToast', JSON.stringify({
message: data.message,
type: 'success'