From f9d3217099e02d9cb012753b1c56cf513e9bce51 Mon Sep 17 00:00:00 2001 From: sky121113 Date: Wed, 13 May 2026 17:37:18 +0800 Subject: [PATCH] =?UTF-8?q?[FEAT]=20=E5=84=AA=E5=8C=96=E5=BB=A3=E5=91=8A?= =?UTF-8?q?=E7=B4=A0=E6=9D=90=E4=B8=8A=E5=82=B3=E9=AB=94=E9=A9=97=E8=88=87?= =?UTF-8?q?=E9=80=B2=E5=BA=A6=E9=A1=AF=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 在廣告建立與編輯彈窗中實作基於 XMLHttpRequest 的 AJAX 上傳機制。 2. 新增即時上傳進度條顯示,並整合全域頂部載入條。 3. 實作上傳期間禁用提交按鈕與顯示「上傳中...」狀態,提升使用者反饋感。 4. 更新 AdvertisementController 確保 JSON 回應後能正確快閃成功訊息至 Session。 --- .../Admin/AdvertisementController.php | 2 + resources/views/admin/ads/index.blade.php | 105 ++++++++++++++---- .../admin/ads/partials/ad-modal.blade.php | 14 ++- 3 files changed, 95 insertions(+), 26 deletions(-) diff --git a/app/Http/Controllers/Admin/AdvertisementController.php b/app/Http/Controllers/Admin/AdvertisementController.php index 3b7ad67..b327127 100644 --- a/app/Http/Controllers/Admin/AdvertisementController.php +++ b/app/Http/Controllers/Admin/AdvertisementController.php @@ -102,6 +102,7 @@ class AdvertisementController extends AdminController ]); if ($request->wantsJson()) { + session()->flash('success', __('Advertisement created successfully.')); return response()->json([ 'success' => true, 'message' => __('Advertisement created successfully.'), @@ -163,6 +164,7 @@ class AdvertisementController extends AdminController $advertisement->update($data); if ($request->wantsJson()) { + session()->flash('success', __('Advertisement updated successfully.')); return response()->json([ 'success' => true, 'message' => __('Advertisement updated successfully.'), diff --git a/resources/views/admin/ads/index.blade.php b/resources/views/admin/ads/index.blade.php index ede43b7..75e6801 100644 --- a/resources/views/admin/ads/index.blade.php +++ b/resources/views/admin/ads/index.blade.php @@ -728,6 +728,8 @@ $baseRoute = 'admin.data-config.advertisements'; document.addEventListener('alpine:init', () => { Alpine.data('adManager', () => ({ isLoading: false, + isSubmitting: false, + uploadProgress: 0, activeTab: 'list', selectedMachineId: null, machines: [], @@ -1035,7 +1037,7 @@ $baseRoute = 'admin.data-config.advertisements'; const form = this.$refs.adFormEl; const formData = new FormData(form); - // Basic validation + // 基本驗證 if (!this.adForm.name) { window.showToast?.('{{ __("Please enter a name") }}', 'error'); return; @@ -1045,31 +1047,88 @@ $baseRoute = 'admin.data-config.advertisements'; return; } - try { - const url = this.adFormMode === 'add' ? this.urls.store : this.urls.update.replace(':id', this.adForm.id); - if (this.adFormMode === 'edit') formData.append('_method', 'PUT'); + // 防止重複提交 + if (this.isSubmitting) return; + this.isSubmitting = true; + this.uploadProgress = 0; - const response = await fetch(url, { - method: 'POST', - body: formData, - headers: { - 'X-CSRF-TOKEN': '{{ csrf_token() }}', - 'Accept': 'application/json' - } - }); + const url = this.adFormMode === 'add' ? this.urls.store : this.urls.update.replace(':id', this.adForm.id); + if (this.adFormMode === 'edit') formData.append('_method', 'PUT'); - const result = await response.json(); - if (result.success) { - window.showToast?.(result.message, 'success'); - this.isAdModalOpen = false; - location.reload(); // Reload to refresh the list - } else { - window.showToast?.(result.message || 'Error', 'error'); - } - } catch (e) { - console.error('Failed to submit ad', e); - window.showToast?.('System Error', 'error'); + // 取得頂部進度條並切換為真實進度模式 + const bar = document.getElementById('top-loading-bar'); + if (bar) { + bar.style.animation = 'none'; + bar.style.width = '0%'; + bar.style.opacity = '1'; + bar.classList.add('loading'); } + + const xhr = new XMLHttpRequest(); + + // 監聽上傳進度 + xhr.upload.addEventListener('progress', (e) => { + if (e.lengthComputable) { + const pct = Math.round((e.loaded / e.total) * 100); + this.uploadProgress = pct; + if (bar) { + // 最多到 95%,完成時才跳到 100% + bar.style.width = Math.min(pct, 95) + '%'; + } + } + }); + + xhr.addEventListener('load', () => { + try { + const result = JSON.parse(xhr.responseText); + if (xhr.status >= 200 && xhr.status < 300 && result.success) { + // 進度跳到 100% + if (bar) bar.style.width = '100%'; + this.uploadProgress = 100; + this.isAdModalOpen = false; + // 延遲 300ms 讓進度條動畫完成後再重整 + setTimeout(() => { + location.reload(); + }, 300); + } else { + window.showToast?.(result.message || 'Error', 'error'); + this.isSubmitting = false; + this.uploadProgress = 0; + if (bar) { + bar.style.width = '0%'; + bar.style.opacity = '0'; + bar.classList.remove('loading'); + } + } + } catch (e) { + console.error('Failed to parse response', e); + window.showToast?.('System Error', 'error'); + this.isSubmitting = false; + this.uploadProgress = 0; + if (bar) { + bar.style.width = '0%'; + bar.style.opacity = '0'; + bar.classList.remove('loading'); + } + } + }); + + xhr.addEventListener('error', () => { + console.error('Upload failed'); + window.showToast?.('{{ __("Upload failed, please try again") }}', 'error'); + this.isSubmitting = false; + this.uploadProgress = 0; + if (bar) { + bar.style.width = '0%'; + bar.style.opacity = '0'; + bar.classList.remove('loading'); + } + }); + + xhr.open('POST', url); + xhr.setRequestHeader('X-CSRF-TOKEN', '{{ csrf_token() }}'); + xhr.setRequestHeader('Accept', 'application/json'); + xhr.send(formData); }, formatDateForInput(dateStr) { diff --git a/resources/views/admin/ads/partials/ad-modal.blade.php b/resources/views/admin/ads/partials/ad-modal.blade.php index f49f685..488db21 100644 --- a/resources/views/admin/ads/partials/ad-modal.blade.php +++ b/resources/views/admin/ads/partials/ad-modal.blade.php @@ -220,11 +220,19 @@ class="px-6 py-3 text-sm font-black text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 transition-colors uppercase tracking-widest"> {{ __('Cancel') }} -