[FEAT] 商品圖片尺寸標準化與自動裁切功能實作

1. 增強 ImageHandler trait,支援居中裁切(Center Crop)與指定尺寸縮放功能。
2. 更新 ProductController 的 store 與 update 方法,強制將商品圖片處理為 320x320 像素之 WebP 格式。
3. 優化 create 與 edit 商品頁面,新增 320x320 建議尺寸與自動裁切之提示文字。
4. 新增繁體中文、英文與日文的多語系翻譯對應。
This commit is contained in:
sky121113 2026-05-13 09:46:24 +08:00
parent 426253b1c3
commit 2fd6419129
7 changed files with 56 additions and 7 deletions

View File

@ -195,7 +195,7 @@ class ProductController extends Controller
$imageUrl = null;
if ($request->hasFile('image')) {
$path = $this->storeAsWebp($request->file('image'), 'products');
$path = $this->storeAsWebp($request->file('image'), 'products', 80, 320, 320);
$imageUrl = Storage::url($path);
}
@ -317,7 +317,7 @@ class ProductController extends Controller
$oldPath = str_replace('/storage/', '', $product->image_url);
Storage::disk('public')->delete($oldPath);
}
$path = $this->storeAsWebp($request->file('image'), 'products');
$path = $this->storeAsWebp($request->file('image'), 'products', 80, 320, 320);
$data['image_url'] = Storage::url($path);
} elseif ($request->boolean('remove_image')) {
if ($product->image_url) {

View File

@ -9,14 +9,16 @@ use Illuminate\Support\Str;
trait ImageHandler
{
/**
* 將圖片轉換為 WebP 並儲存
* 將圖片轉換為 WebP 並儲存 (支援自動裁切與縮放)
*
* @param UploadedFile $file 原始檔案
* @param string $directory 儲存目錄 (不含 disk 名稱)
* @param int $quality 壓縮品質 (0-100)
* @param int|null $targetWidth 目標寬度
* @param int|null $targetHeight 目標高度
* @return string 儲存的路徑
*/
protected function storeAsWebp(UploadedFile $file, string $directory, int $quality = 80): string
protected function storeAsWebp(UploadedFile $file, string $directory, int $quality = 80, ?int $targetWidth = null, ?int $targetHeight = null): string
{
$filename = Str::random(40) . '.webp';
$path = "{$directory}/{$filename}";
@ -27,6 +29,8 @@ trait ImageHandler
return $file->store($directory, 'public');
}
$origWidth = $imageInfo[0];
$origHeight = $imageInfo[1];
$mime = $imageInfo['mime'];
$source = null;
@ -52,9 +56,49 @@ trait ImageHandler
return $file->store($directory, 'public');
}
// 確保支援真彩色 (解決 palette image 問題)
if (!imageistruecolor($source)) {
imagepalettetotruecolor($source);
// 處理裁切與縮放
if ($targetWidth && $targetHeight) {
$targetImage = imagecreatetruecolor($targetWidth, $targetHeight);
// 處理透明背景 (如果是 PNG/WebP)
if ($mime == 'image/png' || $mime == 'image/webp') {
imagealphablending($targetImage, false);
imagesavealpha($targetImage, true);
$transparent = imagecolorallocatealpha($targetImage, 255, 255, 255, 127);
imagefilledrectangle($targetImage, 0, 0, $targetWidth, $targetHeight, $transparent);
}
// 居中裁切計算法
$sourceAspect = $origWidth / $origHeight;
$targetAspect = $targetWidth / $targetHeight;
if ($sourceAspect > $targetAspect) {
// 原圖太寬,裁切兩邊
$tempHeight = $origHeight;
$tempWidth = (int)($origHeight * $targetAspect);
$srcX = (int)(($origWidth - $tempWidth) / 2);
$srcY = 0;
} else {
// 原圖太高,裁切上下
$tempWidth = $origWidth;
$tempHeight = (int)($origWidth / $targetAspect);
$srcX = 0;
$srcY = (int)(($origHeight - $tempHeight) / 2);
}
imagecopyresampled(
$targetImage, $source,
0, 0, $srcX, $srcY,
$targetWidth, $targetHeight, $tempWidth, $tempHeight
);
imagedestroy($source);
$source = $targetImage;
} else {
// 確保支援真彩色 (解決 palette image 問題)
if (!imageistruecolor($source)) {
imagepalettetotruecolor($source);
}
}
// 確保目錄存在

View File

@ -1081,6 +1081,7 @@
"PI_MERCHANT_ID": "PI_MERCHANT_ID",
"PNG, JPG up to 2MB": "PNG, JPG up to 2MB",
"PNG, JPG, WEBP up to 10MB": "PNG, JPG, WEBP up to 10MB",
"Recommended: 320x320 (Auto-cropped)": "Recommended: 320x320 (Auto-cropped)",
"POS Reboot": "POS Reboot",
"PS_LEVEL": "PS_LEVEL",
"PS_MERCHANT_ID": "PS_MERCHANT_ID",

View File

@ -1081,6 +1081,7 @@
"PI_MERCHANT_ID": "PI_MERCHANT_ID",
"PNG, JPG up to 2MB": "PNG, JPG (最大 2MB)",
"PNG, JPG, WEBP up to 10MB": "PNG, JPG, WEBP (最大 10MB)",
"Recommended: 320x320 (Auto-cropped)": "推奨サイズ: 320x320 (自動クロップ)",
"POS Reboot": "POS再起動",
"PS_LEVEL": "PS_LEVEL",
"PS_MERCHANT_ID": "PS_MERCHANT_ID",

View File

@ -1081,6 +1081,7 @@
"PI_MERCHANT_ID": "Pi 拍錢包 商店代號",
"PNG, JPG up to 2MB": "支援 PNG, JPG (最大 2MB)",
"PNG, JPG, WEBP up to 10MB": "支援 PNG, JPG, WEBP 格式,且不超過 10MB",
"Recommended: 320x320 (Auto-cropped)": "建議尺寸320x320 (系統將自動裁切)",
"POS Reboot": "刷卡重啟",
"PS_LEVEL": "PS_LEVEL",
"PS_MERCHANT_ID": "全盈+Pay 商店代號",

View File

@ -60,6 +60,7 @@
<div class="text-center">
<p class="text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-tighter">{{ __('Click to upload') }}</p>
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 mt-1">{{ __('PNG, JPG, WEBP up to 10MB') }}</p>
<p class="text-[10px] font-black text-cyan-500/80 dark:text-cyan-400/80 mt-1 uppercase tracking-tighter">{{ __('Recommended: 320x320 (Auto-cropped)') }}</p>
</div>
</div>
</template>

View File

@ -76,6 +76,7 @@
<div class="text-center">
<p class="text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-tighter">{{ __('Click to upload') }}</p>
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 mt-1">{{ __('PNG, JPG, WEBP up to 10MB') }}</p>
<p class="text-[10px] font-black text-cyan-500/80 dark:text-cyan-400/80 mt-1 uppercase tracking-tighter">{{ __('Recommended: 320x320 (Auto-cropped)') }}</p>
</div>
</div>
</template>