[FEAT] 優化廣告與機台管理介面及效能
1. [廣告管理] 修復編輯素材時刪除按鈕顯示邏輯並優化預覽功能。 2. [廣告管理] 修正請求回傳格式為 JSON,解決 AJAX 解析錯誤。 3. [機台管理] 實作 Alpine.js 無感頁籤切換(機台列表與效期管理)。 4. [機台管理] 移除冗餘返回按鈕,改為動態標題與頁籤重設邏輯。 5. [機台管理] 統一後端查詢,減少切換分頁時的延遲感。 6. [商品管理] 支援商品圖片 WebP 自動轉換,並調整上傳大小限制 (10MB)。 7. [UI] 修正多個管理模組的 JS 時序競爭與 Preline HSSelect 重置問題。
This commit is contained in:
@@ -378,12 +378,14 @@ $baseRoute = 'admin.data-config.advertisements';
|
||||
|
||||
adFormMode: 'add',
|
||||
fileName: '',
|
||||
mediaPreview: null,
|
||||
adForm: {
|
||||
id: null,
|
||||
name: '',
|
||||
type: 'image',
|
||||
duration: 15,
|
||||
is_active: true
|
||||
is_active: true,
|
||||
url: ''
|
||||
},
|
||||
|
||||
// Assign Modal
|
||||
@@ -522,8 +524,83 @@ $baseRoute = 'admin.data-config.advertisements';
|
||||
|
||||
handleFileChange(e) {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
this.fileName = file.name;
|
||||
if (!file) return;
|
||||
|
||||
// Validation
|
||||
const isVideo = file.type.startsWith('video/');
|
||||
const isImage = file.type.startsWith('image/');
|
||||
const maxSize = isVideo ? 50 * 1024 * 1024 : 10 * 1024 * 1024; // 50MB for video, 10MB for image
|
||||
|
||||
if (file.size > maxSize) {
|
||||
window.showToast?.(`{{ __("File is too large") }} (${isVideo ? '50MB' : '10MB'} MAX)`, 'error');
|
||||
e.target.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
this.fileName = file.name;
|
||||
|
||||
// Set form type based on file
|
||||
if (isVideo) this.adForm.type = 'video';
|
||||
else if (isImage) this.adForm.type = 'image';
|
||||
|
||||
// Local Preview
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
this.mediaPreview = event.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
// Update Select UI
|
||||
this.$nextTick(() => {
|
||||
window.HSSelect.getInstance('#ad_type_select')?.setValue(this.adForm.type);
|
||||
});
|
||||
},
|
||||
|
||||
removeMedia() {
|
||||
this.fileName = '';
|
||||
this.mediaPreview = null;
|
||||
if (this.$refs.fileInput) this.$refs.fileInput.value = '';
|
||||
// If editing, we still keep the original url in adForm.url but the UI shows "empty"
|
||||
},
|
||||
|
||||
async submitAdForm() {
|
||||
const form = this.$refs.adFormEl;
|
||||
const formData = new FormData(form);
|
||||
|
||||
// Basic validation
|
||||
if (!this.adForm.name) {
|
||||
window.showToast?.('{{ __("Please enter a name") }}', 'error');
|
||||
return;
|
||||
}
|
||||
if (this.adFormMode === 'add' && !this.fileName) {
|
||||
window.showToast?.('{{ __("Please select a file") }}', 'error');
|
||||
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');
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
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');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -533,10 +610,18 @@ $baseRoute = 'admin.data-config.advertisements';
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify(this.assignForm)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('Server error response:', errorText);
|
||||
throw new Error(`Server responded with ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.isAssignModalOpen = false;
|
||||
@@ -600,13 +685,17 @@ $baseRoute = 'admin.data-config.advertisements';
|
||||
|
||||
openAddModal() {
|
||||
this.adFormMode = 'add';
|
||||
this.adForm = { id: null, company_id: '', name: '', type: 'image', duration: 15, is_active: true };
|
||||
this.adForm = { id: null, company_id: '', name: '', type: 'image', duration: 15, is_active: true, url: '' };
|
||||
this.fileName = '';
|
||||
this.mediaPreview = null;
|
||||
this.isAdModalOpen = true;
|
||||
},
|
||||
|
||||
openEditModal(ad) {
|
||||
this.adFormMode = 'edit';
|
||||
this.adForm = { ...ad };
|
||||
this.fileName = '';
|
||||
this.mediaPreview = ad.url; // Use existing URL as preview
|
||||
this.isAdModalOpen = true;
|
||||
},
|
||||
|
||||
@@ -678,6 +767,11 @@ $baseRoute = 'admin.data-config.advertisements';
|
||||
|
||||
wrapper.appendChild(selectEl);
|
||||
|
||||
// Set the initial value after appending but before autoInit
|
||||
if (this.assignForm.advertisement_id) {
|
||||
selectEl.value = this.assignForm.advertisement_id;
|
||||
}
|
||||
|
||||
selectEl.addEventListener('change', (e) => {
|
||||
this.assignForm.advertisement_id = e.target.value;
|
||||
});
|
||||
@@ -685,6 +779,13 @@ $baseRoute = 'admin.data-config.advertisements';
|
||||
this.$nextTick(() => {
|
||||
if (window.HSStaticMethods && window.HSStaticMethods.autoInit) {
|
||||
window.HSStaticMethods.autoInit(['select']);
|
||||
|
||||
// If we have a value, ensure the Preline instance reflects it
|
||||
if (this.assignForm.advertisement_id) {
|
||||
setTimeout(() => {
|
||||
window.HSSelect.getInstance(selectEl)?.setValue(this.assignForm.advertisement_id);
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user