[FEAT] 優化廣告與機台管理介面及效能

1. [廣告管理] 修復編輯素材時刪除按鈕顯示邏輯並優化預覽功能。
2. [廣告管理] 修正請求回傳格式為 JSON,解決 AJAX 解析錯誤。
3. [機台管理] 實作 Alpine.js 無感頁籤切換(機台列表與效期管理)。
4. [機台管理] 移除冗餘返回按鈕,改為動態標題與頁籤重設邏輯。
5. [機台管理] 統一後端查詢,減少切換分頁時的延遲感。
6. [商品管理] 支援商品圖片 WebP 自動轉換,並調整上傳大小限制 (10MB)。
7. [UI] 修正多個管理模組的 JS 時序競爭與 Preline HSSelect 重置問題。
This commit is contained in:
2026-04-01 13:01:45 +08:00
parent 2e49129d77
commit 953d6a41f3
14 changed files with 387 additions and 166 deletions

View File

@@ -34,7 +34,29 @@
</div>
</div>
<form id="edit-form" action="{{ route('admin.basic-settings.machines.update', $machine) }}" method="POST" enctype="multipart/form-data" class="space-y-6">
<form id="edit-form" x-data="{
imagePreviews: [
{{ isset($machine->images[0]) ? "'" . Storage::disk('public')->url($machine->images[0]) . "'" : 'null' }},
{{ isset($machine->images[1]) ? "'" . Storage::disk('public')->url($machine->images[1]) . "'" : 'null' }},
{{ isset($machine->images[2]) ? "'" . Storage::disk('public')->url($machine->images[2]) . "'" : 'null' }}
],
removeImages: [false, false, false],
handleImageUpload(event, index) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
this.imagePreviews[index] = e.target.result;
this.removeImages[index] = false;
};
reader.readAsDataURL(file);
},
removeImage(index) {
this.imagePreviews[index] = null;
this.removeImages[index] = true;
if (this.$refs['imageInput_' + index]) this.$refs['imageInput_' + index].value = '';
}
}" action="{{ route('admin.basic-settings.machines.update', $machine) }}" method="POST" enctype="multipart/form-data" class="space-y-6">
@csrf
@method('PUT')
@@ -58,7 +80,9 @@
</div>
@endif
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="flex flex-col lg:flex-row gap-8 items-start">
<!-- Main Content Area -->
<div class="flex-1 space-y-8 order-2 lg:order-1 capitalize">
<!-- Left: Basic info & Hardware -->
<div class="lg:col-span-2 space-y-6">
<!-- Basic Information -->
@@ -196,8 +220,71 @@
</div>
</div>
<!-- Right: System & Payment -->
<div class="space-y-8">
<!-- Right Column: Images & Primary System Settings -->
<div class="w-full lg:w-96 space-y-8 order-1 lg:order-2 lg:sticky top-24">
<!-- Machine Images -->
<div class="luxury-card rounded-[2.5rem] p-8 animate-luxury-in">
<div class="flex items-center gap-3 mb-8">
<div class="w-10 h-10 rounded-xl bg-cyan-500/10 flex items-center justify-center text-cyan-500">
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
</svg>
</div>
<h3 class="text-lg font-black text-slate-800 dark:text-white tracking-tight">{{ __('Machine Photos') }}</h3>
</div>
<div class="space-y-8">
@for($i = 0; $i < 3; $i++)
<div class="space-y-3">
<div class="flex items-center justify-between px-1 text-xs font-black text-slate-400 uppercase tracking-widest">
<span>{{ __('Photo Slot') }} {{ $i + 1 }}</span>
<template x-if="imagePreviews[{{ $i }}]">
<span class="text-emerald-500 flex items-center gap-1 lowercase tracking-normal font-bold">
<svg class="size-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="m4.5 12.75 6 6 9-13.5"/></svg>
{{ __('set') }}
</span>
</template>
</div>
<div class="relative group">
<input type="hidden" name="remove_image_{{ $i }}" :value="removeImages[{{ $i }}] ? '1' : '0'">
<template x-if="!imagePreviews[{{ $i }}]">
<div @click="$refs.imageInput_{{ $i }}.click()" class="aspect-video rounded-2xl border-2 border-dashed border-slate-200 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 flex flex-col items-center justify-center gap-3 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800/80 hover:border-cyan-500/50 transition-all duration-300 group/upload">
<div class="p-2.5 rounded-xl bg-white dark:bg-slate-800 shadow-sm border border-slate-100 dark:border-slate-700 group-hover/upload:scale-110 group-hover/upload:text-cyan-500 transition-all duration-300 text-slate-400">
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/></svg>
</div>
<div class="text-center">
<p class="text-[10px] font-black text-slate-500 dark:text-slate-400 uppercase tracking-tighter">{{ __('Click to upload') }}</p>
</div>
</div>
</template>
<template x-if="imagePreviews[{{ $i }}]">
<div class="relative aspect-video rounded-2xl overflow-hidden border border-slate-200 dark:border-slate-800 shadow-lg group/image">
<img :src="imagePreviews[{{ $i }}]" class="w-full h-full object-cover">
<div class="absolute inset-0 bg-slate-950/40 opacity-0 group-hover/image:opacity-100 transition-opacity duration-300 flex items-center justify-center gap-3">
<button type="button" @click="$refs.imageInput_{{ $i }}.click()" class="p-2.5 rounded-xl bg-white text-slate-800 hover:bg-cyan-500 hover:text-white transition-all duration-300 shadow-lg">
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" /></svg>
</button>
<button type="button" @click="removeImage({{ $i }})" class="p-2.5 rounded-xl bg-white text-slate-800 hover:bg-rose-500 hover:text-white transition-all duration-300 shadow-lg">
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" /></svg>
</button>
</div>
</div>
</template>
<input type="file" name="image_{{ $i }}" x-ref="imageInput_{{ $i }}" class="hidden" accept="image/*" @change="handleImageUpload($event, {{ $i }})">
</div>
</div>
@endfor
<div class="p-4 rounded-2xl bg-slate-50 dark:bg-slate-900/50 border border-slate-100 dark:border-white/5">
<p class="text-[10px] font-bold text-slate-400 leading-relaxed uppercase tracking-widest text-center">
{{ __('PNG, JPG, WEBP up to 10MB') }}
</p>
</div>
</div>
</div>
<div class="luxury-card rounded-3xl p-8 animate-luxury-in relative z-20" style="animation-delay: 200ms">
<div class="flex items-center gap-3 mb-8">
<div class="w-10 h-10 rounded-xl bg-emerald-500/10 flex items-center justify-center text-emerald-500">