[FEAT] 商品圖片尺寸標準化與自動裁切功能實作
1. 增強 ImageHandler trait,支援居中裁切(Center Crop)與指定尺寸縮放功能。 2. 更新 ProductController 的 store 與 update 方法,強制將商品圖片處理為 320x320 像素之 WebP 格式。 3. 優化 create 與 edit 商品頁面,新增 320x320 建議尺寸與自動裁切之提示文字。 4. 新增繁體中文、英文與日文的多語系翻譯對應。
This commit is contained in:
parent
426253b1c3
commit
2fd6419129
@ -195,7 +195,7 @@ class ProductController extends Controller
|
|||||||
|
|
||||||
$imageUrl = null;
|
$imageUrl = null;
|
||||||
if ($request->hasFile('image')) {
|
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);
|
$imageUrl = Storage::url($path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -317,7 +317,7 @@ class ProductController extends Controller
|
|||||||
$oldPath = str_replace('/storage/', '', $product->image_url);
|
$oldPath = str_replace('/storage/', '', $product->image_url);
|
||||||
Storage::disk('public')->delete($oldPath);
|
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);
|
$data['image_url'] = Storage::url($path);
|
||||||
} elseif ($request->boolean('remove_image')) {
|
} elseif ($request->boolean('remove_image')) {
|
||||||
if ($product->image_url) {
|
if ($product->image_url) {
|
||||||
|
|||||||
@ -9,14 +9,16 @@ use Illuminate\Support\Str;
|
|||||||
trait ImageHandler
|
trait ImageHandler
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* 將圖片轉換為 WebP 並儲存
|
* 將圖片轉換為 WebP 並儲存 (支援自動裁切與縮放)
|
||||||
*
|
*
|
||||||
* @param UploadedFile $file 原始檔案
|
* @param UploadedFile $file 原始檔案
|
||||||
* @param string $directory 儲存目錄 (不含 disk 名稱)
|
* @param string $directory 儲存目錄 (不含 disk 名稱)
|
||||||
* @param int $quality 壓縮品質 (0-100)
|
* @param int $quality 壓縮品質 (0-100)
|
||||||
|
* @param int|null $targetWidth 目標寬度
|
||||||
|
* @param int|null $targetHeight 目標高度
|
||||||
* @return string 儲存的路徑
|
* @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';
|
$filename = Str::random(40) . '.webp';
|
||||||
$path = "{$directory}/{$filename}";
|
$path = "{$directory}/{$filename}";
|
||||||
@ -27,6 +29,8 @@ trait ImageHandler
|
|||||||
return $file->store($directory, 'public');
|
return $file->store($directory, 'public');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$origWidth = $imageInfo[0];
|
||||||
|
$origHeight = $imageInfo[1];
|
||||||
$mime = $imageInfo['mime'];
|
$mime = $imageInfo['mime'];
|
||||||
$source = null;
|
$source = null;
|
||||||
|
|
||||||
@ -52,9 +56,49 @@ trait ImageHandler
|
|||||||
return $file->store($directory, 'public');
|
return $file->store($directory, 'public');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 確保支援真彩色 (解決 palette image 問題)
|
// 處理裁切與縮放
|
||||||
if (!imageistruecolor($source)) {
|
if ($targetWidth && $targetHeight) {
|
||||||
imagepalettetotruecolor($source);
|
$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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 確保目錄存在
|
// 確保目錄存在
|
||||||
|
|||||||
@ -1081,6 +1081,7 @@
|
|||||||
"PI_MERCHANT_ID": "PI_MERCHANT_ID",
|
"PI_MERCHANT_ID": "PI_MERCHANT_ID",
|
||||||
"PNG, JPG up to 2MB": "PNG, JPG up to 2MB",
|
"PNG, JPG up to 2MB": "PNG, JPG up to 2MB",
|
||||||
"PNG, JPG, WEBP up to 10MB": "PNG, JPG, WEBP up to 10MB",
|
"PNG, JPG, WEBP up to 10MB": "PNG, JPG, WEBP up to 10MB",
|
||||||
|
"Recommended: 320x320 (Auto-cropped)": "Recommended: 320x320 (Auto-cropped)",
|
||||||
"POS Reboot": "POS Reboot",
|
"POS Reboot": "POS Reboot",
|
||||||
"PS_LEVEL": "PS_LEVEL",
|
"PS_LEVEL": "PS_LEVEL",
|
||||||
"PS_MERCHANT_ID": "PS_MERCHANT_ID",
|
"PS_MERCHANT_ID": "PS_MERCHANT_ID",
|
||||||
|
|||||||
@ -1081,6 +1081,7 @@
|
|||||||
"PI_MERCHANT_ID": "PI_MERCHANT_ID",
|
"PI_MERCHANT_ID": "PI_MERCHANT_ID",
|
||||||
"PNG, JPG up to 2MB": "PNG, JPG (最大 2MB)",
|
"PNG, JPG up to 2MB": "PNG, JPG (最大 2MB)",
|
||||||
"PNG, JPG, WEBP up to 10MB": "PNG, JPG, WEBP (最大 10MB)",
|
"PNG, JPG, WEBP up to 10MB": "PNG, JPG, WEBP (最大 10MB)",
|
||||||
|
"Recommended: 320x320 (Auto-cropped)": "推奨サイズ: 320x320 (自動クロップ)",
|
||||||
"POS Reboot": "POS再起動",
|
"POS Reboot": "POS再起動",
|
||||||
"PS_LEVEL": "PS_LEVEL",
|
"PS_LEVEL": "PS_LEVEL",
|
||||||
"PS_MERCHANT_ID": "PS_MERCHANT_ID",
|
"PS_MERCHANT_ID": "PS_MERCHANT_ID",
|
||||||
|
|||||||
@ -1081,6 +1081,7 @@
|
|||||||
"PI_MERCHANT_ID": "Pi 拍錢包 商店代號",
|
"PI_MERCHANT_ID": "Pi 拍錢包 商店代號",
|
||||||
"PNG, JPG up to 2MB": "支援 PNG, JPG (最大 2MB)",
|
"PNG, JPG up to 2MB": "支援 PNG, JPG (最大 2MB)",
|
||||||
"PNG, JPG, WEBP up to 10MB": "支援 PNG, JPG, WEBP 格式,且不超過 10MB",
|
"PNG, JPG, WEBP up to 10MB": "支援 PNG, JPG, WEBP 格式,且不超過 10MB",
|
||||||
|
"Recommended: 320x320 (Auto-cropped)": "建議尺寸:320x320 (系統將自動裁切)",
|
||||||
"POS Reboot": "刷卡重啟",
|
"POS Reboot": "刷卡重啟",
|
||||||
"PS_LEVEL": "PS_LEVEL",
|
"PS_LEVEL": "PS_LEVEL",
|
||||||
"PS_MERCHANT_ID": "全盈+Pay 商店代號",
|
"PS_MERCHANT_ID": "全盈+Pay 商店代號",
|
||||||
|
|||||||
@ -60,6 +60,7 @@
|
|||||||
<div class="text-center">
|
<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-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-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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -76,6 +76,7 @@
|
|||||||
<div class="text-center">
|
<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-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-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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user