[FEAT] 優化廣告素材上傳體驗與進度顯示
1. 在廣告建立與編輯彈窗中實作基於 XMLHttpRequest 的 AJAX 上傳機制。 2. 新增即時上傳進度條顯示,並整合全域頂部載入條。 3. 實作上傳期間禁用提交按鈕與顯示「上傳中...」狀態,提升使用者反饋感。 4. 更新 AdvertisementController 確保 JSON 回應後能正確快閃成功訊息至 Session。
This commit is contained in:
parent
c482fb3c79
commit
f9d3217099
@ -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.'),
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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') }}
|
||||
</button>
|
||||
<button type="submit" class="btn-luxury-primary px-10 py-3">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<button type="submit" class="btn-luxury-primary px-10 py-3 disabled:opacity-60 disabled:cursor-not-allowed disabled:transform-none disabled:shadow-none transition-all"
|
||||
:disabled="isSubmitting">
|
||||
{{-- 正常狀態:勾勾圖示 --}}
|
||||
<svg x-show="!isSubmitting" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
|
||||
</svg>
|
||||
<span>{{ __('Save Material') }}</span>
|
||||
{{-- 上傳中:旋轉圖示 --}}
|
||||
<svg x-show="isSubmitting" x-cloak class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span x-show="!isSubmitting">{{ __('Save Material') }}</span>
|
||||
<span x-show="isSubmitting" x-cloak>{{ __('Uploading...') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user