[FEAT] 優化廣告與機台管理介面及效能
1. [廣告管理] 修復編輯素材時刪除按鈕顯示邏輯並優化預覽功能。 2. [廣告管理] 修正請求回傳格式為 JSON,解決 AJAX 解析錯誤。 3. [機台管理] 實作 Alpine.js 無感頁籤切換(機台列表與效期管理)。 4. [機台管理] 移除冗餘返回按鈕,改為動態標題與頁籤重設邏輯。 5. [機台管理] 統一後端查詢,減少切換分頁時的延遲感。 6. [商品管理] 支援商品圖片 WebP 自動轉換,並調整上傳大小限制 (10MB)。 7. [UI] 修正多個管理模組的 JS 時序競爭與 Preline HSSelect 重置問題。
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user