/** * Photo Processing Progress Tracking * * Отслеживает прогресс асинхронной обработки фото через Celery. * Показывает прогресс-бар и обновляет UI по мере обработки. */ class PhotoProgressTracker { constructor(options = {}) { this.options = { pollInterval: options.pollInterval || 2000, // 2 секунды maxRetries: options.maxRetries || 5, onSuccess: options.onSuccess || (() => {}), onError: options.onError || (() => {}), onProgress: options.onProgress || (() => {}), }; this.activeTrackers = new Map(); // task_id -> status } /** * Запустить отслеживание одного фото * * @param {string} taskId - ID задачи Celery * @param {HTMLElement} photoElement - DOM элемент фото (опционально) * @param {Function} onComplete - Callback при завершении */ trackPhoto(taskId, photoElement = null, onComplete = null) { console.log(`[PhotoProgress] Tracking photo: ${taskId}`); this.activeTrackers.set(taskId, { status: 'pending', progress: 0, photoElement: photoElement, onComplete: onComplete, retries: 0, }); // Запускаем периодическое опрашивание статуса this._pollStatus(taskId); } /** * Отслеживание нескольких фото одновременно * * @param {string[]} taskIds - Массив task_id * @param {Function} onAllComplete - Callback когда все завершены */ trackMultiple(taskIds, onAllComplete = null) { console.log(`[PhotoProgress] Tracking ${taskIds.length} photos`); taskIds.forEach((taskId, index) => { this.trackPhoto(taskId, null, () => { // Проверяем все ли завершены const allCompleted = taskIds.every(id => { const tracker = this.activeTrackers.get(id); return tracker && tracker.status === 'success'; }); if (allCompleted && onAllComplete) { console.log('[PhotoProgress] All photos processed successfully'); onAllComplete(); } }); }); } /** * Приватный метод для опрашивания статуса */ _pollStatus(taskId) { const statusUrl = `/products/api/photos/status/${taskId}/`; fetch(statusUrl) .then(response => { if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return response.json(); }) .then(data => { this._handleStatusUpdate(taskId, data); }) .catch(error => { console.error(`[PhotoProgress] Error polling status for ${taskId}:`, error); this._handleError(taskId, error); }); } /** * Обработка обновления статуса */ _handleStatusUpdate(taskId, data) { const tracker = this.activeTrackers.get(taskId); if (!tracker) return; const celeryStatus = data.status; // PENDING, STARTED, SUCCESS, FAILURE, RETRY let progress = data.progress || 0; let status = 'processing'; console.log(`[PhotoProgress] ${taskId}: ${celeryStatus} (${progress}%)`); switch (celeryStatus) { case 'PENDING': progress = 0; status = 'pending'; this._updateUI(tracker, progress, '⏳ В очереди на обработку...'); // Продолжаем опрашивать setTimeout(() => this._pollStatus(taskId), this.options.pollInterval); break; case 'STARTED': progress = 50; status = 'processing'; this._updateUI(tracker, progress, '⚙️ Обрабатывается изображение...'); // Продолжаем опрашивать setTimeout(() => this._pollStatus(taskId), this.options.pollInterval); break; case 'SUCCESS': progress = 100; status = 'success'; this._updateUI(tracker, progress, '✅ Обработано успешно'); tracker.status = 'success'; this.activeTrackers.set(taskId, tracker); // Вызываем callback if (tracker.onComplete) { tracker.onComplete(data.result); } this.options.onSuccess(taskId, data.result); break; case 'FAILURE': progress = 0; status = 'failed'; this._updateUI(tracker, progress, '❌ Ошибка при обработке'); tracker.status = 'failed'; this.activeTrackers.set(taskId, tracker); // Вызываем обработчик ошибки this.options.onError(taskId, data.error || 'Unknown error'); break; case 'RETRY': progress = 25; status = 'retrying'; this._updateUI(tracker, progress, '🔄 Повторная попытка...'); // Продолжаем опрашивать setTimeout(() => this._pollStatus(taskId), this.options.pollInterval); break; default: // Неизвестный статус, продолжаем опрашивать setTimeout(() => this._pollStatus(taskId), this.options.pollInterval); } // Обновляем в кэше tracker.status = status; tracker.progress = progress; this.activeTrackers.set(taskId, tracker); // Вызываем callback прогресса this.options.onProgress(taskId, progress, status); } /** * Обновление UI элемента */ _updateUI(tracker, progress, message) { if (!tracker.photoElement) return; const element = tracker.photoElement; // Обновляем/создаем прогресс-бар let progressBar = element.querySelector('.photo-progress-bar'); if (!progressBar) { progressBar = document.createElement('div'); progressBar.className = 'photo-progress-bar'; progressBar.innerHTML = `
`; element.appendChild(progressBar); } // Обновляем прогресс const progressFill = progressBar.querySelector('.progress-fill'); progressFill.style.width = progress + '%'; // Обновляем сообщение const messageEl = progressBar.querySelector('.progress-message'); messageEl.textContent = message; // CSS стили if (!document.getElementById('photo-progress-styles')) { const style = document.createElement('style'); style.id = 'photo-progress-styles'; style.textContent = ` .photo-progress-bar { margin-top: 10px; padding: 10px; background: #f5f5f5; border-radius: 4px; } .progress-container { width: 100%; height: 24px; background: #e0e0e0; border-radius: 12px; overflow: hidden; margin-bottom: 8px; } .progress-fill { height: 100%; background: linear-gradient(90deg, #4CAF50 0%, #45a049 100%); transition: width 0.3s ease; display: flex; align-items: center; justify-content: center; color: white; font-size: 12px; font-weight: bold; } .progress-message { font-size: 14px; color: #666; text-align: center; } `; document.head.appendChild(style); } } /** * Обработка ошибки */ _handleError(taskId, error) { const tracker = this.activeTrackers.get(taskId); if (!tracker) return; tracker.retries = (tracker.retries || 0) + 1; if (tracker.retries < this.options.maxRetries) { console.warn(`[PhotoProgress] Retrying ${taskId} (${tracker.retries}/${this.options.maxRetries})`); setTimeout(() => this._pollStatus(taskId), this.options.pollInterval * 2); } else { console.error(`[PhotoProgress] Max retries exceeded for ${taskId}`); tracker.status = 'failed'; this._updateUI(tracker, 0, '❌ Ошибка: превышено максимальное количество попыток'); this.options.onError(taskId, 'Max retries exceeded'); } } /** * Остановить отслеживание фото */ stopTracking(taskId) { this.activeTrackers.delete(taskId); console.log(`[PhotoProgress] Stopped tracking ${taskId}`); } /** * Получить статус фото */ getStatus(taskId) { return this.activeTrackers.get(taskId); } } // Глобальный экземпляр window.photoProgressTracker = new PhotoProgressTracker({ pollInterval: 2000, onSuccess: (taskId, result) => { console.log(`[PhotoProgress] Photo ${taskId} processing completed:`, result); }, onError: (taskId, error) => { console.error(`[PhotoProgress] Photo ${taskId} error:`, error); alert(`Ошибка при обработке фото: ${error}`); }, onProgress: (taskId, progress, status) => { // Можно использовать для обновления глобального статуса } }); /** * Интеграция с Django формой загрузки фото * * Добавьте этот скрипт в шаблон формы создания товара: * *
* ... * * ... *
* * */