diff --git a/app/Http/Controllers/Admin/ProductController.php b/app/Http/Controllers/Admin/ProductController.php
index 296354b..945ac3a 100644
--- a/app/Http/Controllers/Admin/ProductController.php
+++ b/app/Http/Controllers/Admin/ProductController.php
@@ -584,6 +584,136 @@ class ProductController extends Controller
}
}
+ /**
+ * 匯出商品資料(CSV / Excel),套用與 index 相同的篩選(搜尋/分類/公司)。
+ * 參考 SalesController::handleExport 的串流下載模式;欄位順序對齊匯入範本,方便編輯後回匯入。
+ */
+ public function export(Request $request)
+ {
+ $user = auth()->user();
+ $type = $request->input('export', 'csv');
+ $isExcel = ($type === 'excel');
+ $ext = $isExcel ? 'xls' : 'csv';
+ $contentType = $isExcel ? 'application/vnd.ms-excel; charset=utf-8' : 'text/csv; charset=utf-8';
+
+ // 與 index 相同的商品查詢與篩選(租戶隔離沿用 Product 全域 scope)
+ $query = Product::with(['category.translations', 'translations', 'company']);
+
+ if ($request->filled('search')) {
+ $search = $request->search;
+ $query->where(function ($q) use ($search) {
+ $q->where('name', 'like', "%{$search}%")
+ ->orWhere('barcode', 'like', "%{$search}%")
+ ->orWhere('spec', 'like', "%{$search}%");
+ });
+ }
+
+ if ($request->filled('category_id')) {
+ $query->where('category_id', $request->category_id);
+ }
+
+ if ($user->isSystemAdmin() && $request->filled('product_company_id')) {
+ $query->where('company_id', $request->product_company_id);
+ }
+
+ // 欄位順序對齊匯入範本(ProductImportService::downloadTemplate)
+ $headers = [
+ '分類',
+ '商品名稱(zh_TW)',
+ '商品名稱(en)',
+ '商品名稱(ja)',
+ '條碼',
+ '規格',
+ '生產公司',
+ '售價',
+ '會員價',
+ '成本',
+ '履帶貨道上限',
+ '彈簧貨道上限',
+ '物料代碼',
+ '全額點數',
+ '半額點數',
+ '半額折抵金額',
+ '是否啟用',
+ '圖片檔名',
+ ];
+
+ $filename = '商品資料_' . now()->format('YmdHis') . '.' . $ext;
+
+ $callback = function () use ($query, $headers, $isExcel) {
+ $file = fopen('php://output', 'w');
+ if ($isExcel) {
+ fwrite($file, '');
+ fwrite($file, '
');
+ fwrite($file, '');
+ foreach ($headers as $header) {
+ fwrite($file, '| ' . htmlspecialchars($header) . ' | ');
+ }
+ fwrite($file, '
');
+ } else {
+ fwrite($file, "\xEF\xBB\xBF"); // UTF-8 BOM,Excel 開啟不亂碼
+ fputcsv($file, $headers);
+ }
+
+ $locale = app()->getLocale();
+
+ $query->orderBy('id')->chunk(500, function ($products) use ($file, $isExcel, $locale) {
+ foreach ($products as $p) {
+ $meta = is_array($p->metadata) ? $p->metadata : [];
+ $nameEn = optional($p->translations->firstWhere('locale', 'en'))->value;
+ $nameJa = optional($p->translations->firstWhere('locale', 'ja'))->value;
+
+ $categoryName = '';
+ if ($p->category) {
+ $catTrans = $p->category->translations->firstWhere('locale', $locale)
+ ?? $p->category->translations->firstWhere('locale', 'zh_TW');
+ $categoryName = $catTrans->value ?? ($p->category->name ?? '');
+ }
+
+ $imageFilename = $p->image_url ? basename($p->image_url) : '';
+
+ $row = [
+ $categoryName,
+ $p->name,
+ $nameEn,
+ $nameJa,
+ $p->barcode,
+ $p->spec,
+ $p->manufacturer,
+ $p->price,
+ $p->member_price,
+ $p->cost,
+ $p->track_limit,
+ $p->spring_limit,
+ $meta['material_code'] ?? '',
+ $meta['points_full'] ?? 0,
+ $meta['points_half'] ?? 0,
+ $meta['points_half_amount'] ?? 0,
+ $p->is_active ? 1 : 0,
+ $imageFilename,
+ ];
+
+ if ($isExcel) {
+ fwrite($file, '');
+ foreach ($row as $cell) {
+ fwrite($file, '| ' . htmlspecialchars((string) $cell) . ' | ');
+ }
+ fwrite($file, '
');
+ } else {
+ fputcsv($file, $row);
+ }
+ }
+ });
+
+ if ($isExcel) {
+ fwrite($file, '
');
+ }
+ fclose($file);
+ };
+
+ return response()->streamDownload($callback, $filename, ['Content-Type' => $contentType]);
+ }
+
public function downloadTemplate(ProductImportService $importService)
{
return $importService->downloadTemplate();
diff --git a/lang/en.json b/lang/en.json
index 2d49c90..9c4bff1 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -777,6 +777,7 @@
"Expiry Time": "Expiry Time",
"Expiry date tracking and warnings": "Expiry date tracking and warnings",
"Export Report": "Export Report",
+ "Export": "Export",
"Export to CSV": "Export to CSV",
"Export to Excel": "Export to Excel",
"External URL": "External URL",
@@ -965,6 +966,8 @@
"Image Downloaded": "Image Downloaded",
"Immediate": "Immediate",
"Import Excel": "Import Excel",
+ "Export CSV": "Export CSV",
+ "Export Excel": "Export Excel",
"Import Products": "Import Products",
"Import Staff Cards": "Import Staff Cards",
"Import failed": "Import failed",
diff --git a/lang/zh_TW.json b/lang/zh_TW.json
index 806adcb..5512d47 100644
--- a/lang/zh_TW.json
+++ b/lang/zh_TW.json
@@ -778,6 +778,7 @@
"Expiry Time": "到期時間",
"Expiry date tracking and warnings": "有效期限追蹤與預警",
"Export Report": "匯出報表",
+ "Export": "匯出",
"Export to CSV": "匯出成 CSV",
"Export to Excel": "匯出成 Excel",
"External URL": "外連 URL",
@@ -966,6 +967,8 @@
"Image Downloaded": "圖片已下載",
"Immediate": "立即",
"Import Excel": "Excel 匯入",
+ "Export CSV": "匯出 CSV",
+ "Export Excel": "匯出 Excel",
"Import Products": "匯入商品",
"Import Staff Cards": "匯入員工識別卡",
"Import failed": "匯入失敗",
diff --git a/resources/views/admin/products/index.blade.php b/resources/views/admin/products/index.blade.php
index 6473b0e..7aa8f93 100644
--- a/resources/views/admin/products/index.blade.php
+++ b/resources/views/admin/products/index.blade.php
@@ -36,13 +36,6 @@ hover:bg-slate-100 dark:hover:bg-white/5 rounded-lg flex items-center justify-be
{{ __('Add Product') }}
-
+
+
+
+
+
diff --git a/routes/web.php b/routes/web.php
index 28e9aa7..ed4e10d 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -213,6 +213,7 @@ Route::middleware(['auth', 'auth.session', 'verified', 'tenant.access'])->prefix
Route::prefix('data-config')->name('data-config.')->group(function () {
Route::middleware('can:menu.data-config.products')->group(function () {
Route::get('/products/template', [App\Http\Controllers\Admin\ProductController::class, 'downloadTemplate'])->name('products.template');
+ Route::get('/products/export', [App\Http\Controllers\Admin\ProductController::class, 'export'])->name('products.export');
Route::post('/products/import', [App\Http\Controllers\Admin\ProductController::class, 'import'])->name('products.import');
Route::resource('products', App\Http\Controllers\Admin\ProductController::class)->except(['show']);
Route::patch('/products/{id}/toggle-status', [App\Http\Controllers\Admin\ProductController::class, 'toggleStatus'])->name('products.status.toggle');