[FIX] 修復 WebP 轉換 Palette 錯誤並優化照片獨立槽位上傳 UI
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m1s
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m1s
This commit is contained in:
@@ -148,20 +148,30 @@ class MachineSettingController extends AdminController
|
||||
'updater_id' => auth()->id(),
|
||||
]));
|
||||
|
||||
// 處理圖片更新 (若有上傳新圖片,則替換或附加,這裡採簡單邏輯:若有傳 images 則全換)
|
||||
// 處理圖片更新 (支援 3 個獨立槽位)
|
||||
if ($request->hasFile('images')) {
|
||||
// 刪除舊圖
|
||||
if (!empty($machine->images)) {
|
||||
foreach ($machine->images as $oldPath) {
|
||||
Storage::disk('public')->delete($oldPath);
|
||||
}
|
||||
$currentImages = $machine->images ?? [];
|
||||
$newImages = $request->file('images');
|
||||
$updated = false;
|
||||
|
||||
foreach ($newImages as $index => $file) {
|
||||
// 限制 3 個槽位 (0, 1, 2)
|
||||
if ($index < 0 || $index > 2) continue;
|
||||
|
||||
// 刪除該槽位的舊圖
|
||||
if (isset($currentImages[$index]) && !empty($currentImages[$index])) {
|
||||
\Illuminate\Support\Facades\Storage::disk('public')->delete($currentImages[$index]);
|
||||
}
|
||||
|
||||
$imagePaths = [];
|
||||
foreach (array_slice($request->file('images'), 0, 3) as $image) {
|
||||
$imagePaths[] = $this->processAndStoreImage($image);
|
||||
// 處理並儲存新圖
|
||||
$currentImages[$index] = $this->processAndStoreImage($file);
|
||||
$updated = true;
|
||||
}
|
||||
|
||||
if ($updated) {
|
||||
ksort($currentImages);
|
||||
$machine->update(['images' => array_values($currentImages)]);
|
||||
}
|
||||
$machine->update(['images' => $imagePaths]);
|
||||
}
|
||||
|
||||
return redirect()->route('admin.basic-settings.machines.index')
|
||||
@@ -169,46 +179,47 @@ class MachineSettingController extends AdminController
|
||||
}
|
||||
|
||||
/**
|
||||
* 處理圖片並轉換為 WebP
|
||||
* 處理並儲存圖片 (轉換為 WebP 並調整大小)
|
||||
*/
|
||||
private function processAndStoreImage($file): string
|
||||
protected function processAndStoreImage($file)
|
||||
{
|
||||
$filename = Str::random(40) . '.webp';
|
||||
$path = 'machines/' . $filename;
|
||||
$path = 'machines/' . \Illuminate\Support\Str::random(40) . '.webp';
|
||||
|
||||
// 建立圖資源
|
||||
$image = null;
|
||||
$extension = strtolower($file->getClientOriginalExtension());
|
||||
// 載入原圖
|
||||
$imageInfo = getimagesize($file->getRealPath());
|
||||
$mime = $imageInfo['mime'];
|
||||
|
||||
switch ($extension) {
|
||||
case 'jpeg':
|
||||
case 'jpg':
|
||||
switch ($mime) {
|
||||
case 'image/jpeg':
|
||||
$image = imagecreatefromjpeg($file->getRealPath());
|
||||
break;
|
||||
case 'png':
|
||||
case 'image/png':
|
||||
$image = imagecreatefrompng($file->getRealPath());
|
||||
break;
|
||||
case 'gif':
|
||||
case 'image/gif':
|
||||
$image = imagecreatefromgif($file->getRealPath());
|
||||
break;
|
||||
case 'webp':
|
||||
$image = imagecreatefromwebp($file->getRealPath());
|
||||
break;
|
||||
default:
|
||||
return $file->store('machines', 'public');
|
||||
}
|
||||
|
||||
if ($image) {
|
||||
// 確保目錄存在
|
||||
Storage::disk('public')->makeDirectory('machines');
|
||||
$fullPath = Storage::disk('public')->path($path);
|
||||
// [修正] imagewebp(): Palette image not supported by webp
|
||||
// 若為 Palette 圖片 (例如 GIF),轉換為 Truecolor
|
||||
if (!imageistruecolor($image)) {
|
||||
imagepalettetotruecolor($image);
|
||||
}
|
||||
|
||||
// 轉換並儲存
|
||||
imagewebp($image, $fullPath, 80); // 品質 80
|
||||
\Illuminate\Support\Facades\Storage::disk('public')->makeDirectory('machines');
|
||||
$fullPath = \Illuminate\Support\Facades\Storage::disk('public')->path($path);
|
||||
|
||||
// 轉換並儲存 (品質 80)
|
||||
imagewebp($image, $fullPath, 80);
|
||||
imagedestroy($image);
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
// Fallback to standard store if GD fails
|
||||
return $file->store('machines', 'public');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
<div class="space-y-10 pb-20" x-data="{ selectedFileCount: 0, handleFileChange(e) { this.selectedFileCount = e.target.files.length; } }">
|
||||
<div class="space-y-10 pb-20" x-data="{
|
||||
selectedFiles: [null, null, null],
|
||||
handleFileChange(e, index) {
|
||||
if (e.target.files.length > 0) {
|
||||
this.selectedFiles[index] = URL.createObjectURL(e.target.files[0]);
|
||||
}
|
||||
}
|
||||
}">
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
|
||||
<div class="flex items-center gap-4">
|
||||
@@ -227,46 +234,44 @@
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
@if(!empty($machine->image_urls))
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
@foreach($machine->image_urls as $url)
|
||||
<div class="relative aspect-square rounded-2xl overflow-hidden border border-slate-100 dark:border-slate-800 shadow-sm group">
|
||||
<img src="{{ $url }}" class="absolute inset-0 w-full h-full object-cover transition-transform duration-500 group-hover:scale-110">
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="p-6 rounded-2xl border border-dashed border-slate-200 dark:border-slate-800 text-center">
|
||||
<p class="text-xs font-bold text-slate-400 capitalize">{{ __('No images uploaded') }}</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
@for($i = 0; $i < 3; $i++)
|
||||
<div class="relative group">
|
||||
<label class="relative flex flex-col items-center justify-center aspect-square border-2 border-dashed border-slate-200 dark:border-slate-800 rounded-3xl cursor-pointer bg-slate-50/50 dark:bg-slate-900/50 hover:bg-slate-100 dark:hover:bg-slate-800/80 transition-all overflow-hidden">
|
||||
<!-- Current Image or Preview -->
|
||||
<div class="absolute inset-0 w-full h-full">
|
||||
@if(isset($machine->image_urls[$i]))
|
||||
<img src="{{ $machine->image_urls[$i] }}" class="w-full h-full object-cover" x-show="!selectedFiles[{{ $i }}]">
|
||||
@endif
|
||||
<template x-if="selectedFiles[{{ $i }}]">
|
||||
<img :src="selectedFiles[{{ $i }}]" class="w-full h-full object-cover">
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Upload New Images') }} ({{ __('Max 3') }})</label>
|
||||
<label class="relative flex flex-col items-center justify-center w-full h-32 border-2 border-dashed border-slate-200 dark:border-slate-800 rounded-3xl cursor-pointer bg-slate-50/50 dark:bg-slate-900/50 hover:bg-slate-100 dark:hover:bg-slate-800/80 transition-all group">
|
||||
<template x-if="selectedFileCount === 0">
|
||||
<div class="flex flex-col items-center justify-center pt-5 pb-6">
|
||||
<svg class="w-8 h-8 mb-3 text-slate-400 group-hover:text-cyan-500 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||||
<!-- Overlay for Empty/Hover -->
|
||||
<div class="relative z-10 flex flex-col items-center justify-center p-4 bg-white/60 dark:bg-black/40 backdrop-blur-sm opacity-0 group-hover:opacity-100 transition-opacity w-full h-full"
|
||||
:class="{'opacity-100': !selectedFiles[{{ $i }}] && !@json(isset($machine->image_urls[$i]))}">
|
||||
<svg class="w-6 h-6 mb-1 text-slate-500 dark:text-slate-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
<p class="text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ __('Click to upload') }}</p>
|
||||
<span class="text-[10px] font-black uppercase tracking-widest text-slate-600 dark:text-slate-200">
|
||||
{{ isset($machine->image_urls[$i]) || false ? __('Change') : __('Upload') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="selectedFileCount > 0">
|
||||
<div class="flex flex-col items-center justify-center pt-5 pb-6">
|
||||
<div class="w-10 h-10 rounded-full bg-emerald-500/10 flex items-center justify-center text-emerald-500 mb-2">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-xs font-black text-emerald-500 uppercase tracking-widest" x-text="`${selectedFileCount} {{ __('files selected') }}`"></p>
|
||||
</div>
|
||||
</template>
|
||||
<input type="file" name="images[]" multiple accept="image/*" class="hidden" @change="handleFileChange">
|
||||
|
||||
<input type="file" name="images[{{ $i }}]" accept="image/*" class="hidden" @change="handleFileChange($event, {{ $i }})">
|
||||
</label>
|
||||
<p class="text-[10px] text-slate-400 mt-2 font-bold uppercase tracking-widest">* {{ __('Uploading new images will replace all existing images.') }}</p>
|
||||
<div class="mt-2 text-center">
|
||||
<span class="text-[9px] font-black text-slate-400 uppercase tracking-[0.2em]">{{ __('Slot') }} {{ $i + 1 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@endfor
|
||||
</div>
|
||||
<p class="text-[10px] text-slate-400 mt-4 font-bold uppercase tracking-widest leading-relaxed">
|
||||
* {{ __('You can upload images one by one. Supporting up to 3 slots.') }}<br>
|
||||
* {{ __('Click any slot to select or replace a photo.') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user