295 lines
12 KiB
JavaScript
295 lines
12 KiB
JavaScript
|
|
/**
|
|||
|
|
* SSE (Server-Sent Events) клиент для синхронизации файлов в реальном времени
|
|||
|
|
*
|
|||
|
|
* Автоматически подключается к SSE endpoint и обновляет UI при изменениях файлов
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
class FileSyncSSE {
|
|||
|
|
constructor() {
|
|||
|
|
this.eventSource = null;
|
|||
|
|
this.reconnectInterval = 5000; // 5 секунд
|
|||
|
|
this.maxReconnectAttempts = 10;
|
|||
|
|
this.reconnectAttempts = 0;
|
|||
|
|
this.isConnected = false;
|
|||
|
|
|
|||
|
|
this.init();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
init() {
|
|||
|
|
console.log('🔄 Инициализация SSE для синхронизации файлов...');
|
|||
|
|
this.connect();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
connect() {
|
|||
|
|
try {
|
|||
|
|
// Закрываем предыдущее соединение
|
|||
|
|
if (this.eventSource) {
|
|||
|
|
this.eventSource.close();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Создаем новое SSE соединение
|
|||
|
|
this.eventSource = new EventSource('/crm_extensions/file_storage/api/sse_events.php');
|
|||
|
|
|
|||
|
|
// Обработчик успешного подключения
|
|||
|
|
this.eventSource.onopen = (event) => {
|
|||
|
|
console.log('✅ SSE подключение установлено');
|
|||
|
|
this.isConnected = true;
|
|||
|
|
this.reconnectAttempts = 0;
|
|||
|
|
this.showConnectionStatus('connected');
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Обработчик сообщений
|
|||
|
|
this.eventSource.onmessage = (event) => {
|
|||
|
|
try {
|
|||
|
|
const data = JSON.parse(event.data);
|
|||
|
|
this.handleEvent(data);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('❌ Ошибка парсинга SSE данных:', error);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Обработчик ошибок
|
|||
|
|
this.eventSource.onerror = (event) => {
|
|||
|
|
console.error('❌ SSE ошибка:', event);
|
|||
|
|
this.isConnected = false;
|
|||
|
|
this.showConnectionStatus('disconnected');
|
|||
|
|
|
|||
|
|
// Попытка переподключения
|
|||
|
|
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|||
|
|
this.reconnectAttempts++;
|
|||
|
|
console.log(`🔄 Попытка переподключения ${this.reconnectAttempts}/${this.maxReconnectAttempts}...`);
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
this.connect();
|
|||
|
|
}, this.reconnectInterval);
|
|||
|
|
} else {
|
|||
|
|
console.error('❌ Максимальное количество попыток переподключения достигнуто');
|
|||
|
|
this.showConnectionStatus('failed');
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('❌ Ошибка создания SSE соединения:', error);
|
|||
|
|
this.showConnectionStatus('error');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
handleEvent(data) {
|
|||
|
|
console.log('📨 SSE событие:', data);
|
|||
|
|
|
|||
|
|
switch (data.type) {
|
|||
|
|
case 'connected':
|
|||
|
|
console.log('✅ SSE подключен:', data.data.message);
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
case 'disconnected':
|
|||
|
|
console.log('❌ SSE отключен:', data.data.message);
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
case 'heartbeat':
|
|||
|
|
// Heartbeat - просто обновляем статус
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
case 'file_created':
|
|||
|
|
this.handleFileCreated(data.data);
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
case 'file_updated':
|
|||
|
|
this.handleFileUpdated(data.data);
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
case 'file_deleted':
|
|||
|
|
this.handleFileDeleted(data.data);
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
case 'folder_renamed':
|
|||
|
|
this.handleFolderRenamed(data.data);
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
case 'folder_deleted':
|
|||
|
|
this.handleFolderDeleted(data.data);
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
default:
|
|||
|
|
console.log('❓ Неизвестное SSE событие:', data.type);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
handleFileCreated(data) {
|
|||
|
|
console.log('📄 Файл создан:', data);
|
|||
|
|
|
|||
|
|
// Показываем уведомление
|
|||
|
|
this.showNotification('Файл добавлен', `Файл "${data.fileName}" добавлен в ${data.module}`, 'success');
|
|||
|
|
|
|||
|
|
// Обновляем список файлов если мы на странице детального просмотра
|
|||
|
|
this.refreshFileList(data.module, data.recordId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
handleFileUpdated(data) {
|
|||
|
|
console.log('📝 Файл обновлен:', data);
|
|||
|
|
|
|||
|
|
// Показываем уведомление
|
|||
|
|
this.showNotification('Файл обновлен', `Файл "${data.fileName}" обновлен в ${data.module}`, 'info');
|
|||
|
|
|
|||
|
|
// Обновляем список файлов
|
|||
|
|
this.refreshFileList(data.module, data.recordId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
handleFileDeleted(data) {
|
|||
|
|
console.log('🗑️ Файл удален:', data);
|
|||
|
|
|
|||
|
|
// Показываем уведомление
|
|||
|
|
this.showNotification('Файл удален', `Файл "${data.fileName}" удален из ${data.module}`, 'warning');
|
|||
|
|
|
|||
|
|
// Обновляем список файлов
|
|||
|
|
this.refreshFileList(data.module, data.recordId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
handleFolderRenamed(data) {
|
|||
|
|
console.log('📁 Папка переименована:', data);
|
|||
|
|
|
|||
|
|
// Показываем уведомление
|
|||
|
|
this.showNotification('Папка переименована', `Папка переименована в ${data.module}`, 'info');
|
|||
|
|
|
|||
|
|
// Обновляем список файлов
|
|||
|
|
this.refreshFileList(data.module, data.recordId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
handleFolderDeleted(data) {
|
|||
|
|
console.log('🗂️ Папка удалена:', data);
|
|||
|
|
|
|||
|
|
// Показываем уведомление
|
|||
|
|
this.showNotification('Папка удалена', `Папка удалена из ${data.module}`, 'error');
|
|||
|
|
|
|||
|
|
// Обновляем список файлов
|
|||
|
|
this.refreshFileList(data.module, data.recordId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
refreshFileList(module, recordId) {
|
|||
|
|
// Проверяем, находимся ли мы на странице детального просмотра нужного модуля
|
|||
|
|
const currentModule = window.location.search.match(/module=([^&]+)/);
|
|||
|
|
const currentRecord = window.location.search.match(/record=([^&]+)/);
|
|||
|
|
|
|||
|
|
if (currentModule && currentModule[1] === module &&
|
|||
|
|
currentRecord && currentRecord[1] === recordId) {
|
|||
|
|
|
|||
|
|
console.log('🔄 Обновляем список файлов...');
|
|||
|
|
|
|||
|
|
// Обновляем страницу или конкретный блок с файлами
|
|||
|
|
if (typeof refreshFileList === 'function') {
|
|||
|
|
refreshFileList();
|
|||
|
|
} else {
|
|||
|
|
// Fallback - обновляем всю страницу
|
|||
|
|
setTimeout(() => {
|
|||
|
|
window.location.reload();
|
|||
|
|
}, 1000);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
showNotification(title, message, type = 'info') {
|
|||
|
|
// Используем существующую систему уведомлений CRM
|
|||
|
|
if (typeof Vtiger_Helper_Js !== 'undefined' && Vtiger_Helper_Js.showPnotify) {
|
|||
|
|
Vtiger_Helper_Js.showPnotify({
|
|||
|
|
title: title,
|
|||
|
|
text: message,
|
|||
|
|
type: type,
|
|||
|
|
delay: 5000
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
// Fallback - обычный alert
|
|||
|
|
alert(`${title}: ${message}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
showConnectionStatus(status) {
|
|||
|
|
// Создаем или обновляем индикатор статуса подключения
|
|||
|
|
let statusElement = document.getElementById('sse-connection-status');
|
|||
|
|
|
|||
|
|
if (!statusElement) {
|
|||
|
|
statusElement = document.createElement('div');
|
|||
|
|
statusElement.id = 'sse-connection-status';
|
|||
|
|
statusElement.style.cssText = `
|
|||
|
|
position: fixed;
|
|||
|
|
top: 10px;
|
|||
|
|
right: 10px;
|
|||
|
|
padding: 8px 12px;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
font-size: 12px;
|
|||
|
|
z-index: 9999;
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
`;
|
|||
|
|
document.body.appendChild(statusElement);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
switch (status) {
|
|||
|
|
case 'connected':
|
|||
|
|
statusElement.textContent = '🟢 Файлы синхронизируются';
|
|||
|
|
statusElement.style.backgroundColor = '#d4edda';
|
|||
|
|
statusElement.style.color = '#155724';
|
|||
|
|
statusElement.style.border = '1px solid #c3e6cb';
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
case 'disconnected':
|
|||
|
|
statusElement.textContent = '🟡 Переподключение...';
|
|||
|
|
statusElement.style.backgroundColor = '#fff3cd';
|
|||
|
|
statusElement.style.color = '#856404';
|
|||
|
|
statusElement.style.border = '1px solid #ffeaa7';
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
case 'failed':
|
|||
|
|
statusElement.textContent = '🔴 Синхронизация недоступна';
|
|||
|
|
statusElement.style.backgroundColor = '#f8d7da';
|
|||
|
|
statusElement.style.color = '#721c24';
|
|||
|
|
statusElement.style.border = '1px solid #f5c6cb';
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
case 'error':
|
|||
|
|
statusElement.textContent = '❌ Ошибка подключения';
|
|||
|
|
statusElement.style.backgroundColor = '#f8d7da';
|
|||
|
|
statusElement.style.color = '#721c24';
|
|||
|
|
statusElement.style.border = '1px solid #f5c6cb';
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Автоматически скрываем через 5 секунд для успешного подключения
|
|||
|
|
if (status === 'connected') {
|
|||
|
|
setTimeout(() => {
|
|||
|
|
if (statusElement) {
|
|||
|
|
statusElement.style.opacity = '0.7';
|
|||
|
|
}
|
|||
|
|
}, 5000);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
disconnect() {
|
|||
|
|
if (this.eventSource) {
|
|||
|
|
this.eventSource.close();
|
|||
|
|
this.eventSource = null;
|
|||
|
|
}
|
|||
|
|
this.isConnected = false;
|
|||
|
|
console.log('🔌 SSE соединение закрыто');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Автоматически инициализируем SSE при загрузке страницы
|
|||
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|||
|
|
// Проверяем, что мы в CRM (не в админке или других разделах)
|
|||
|
|
if (window.location.pathname.includes('/index.php') &&
|
|||
|
|
!window.location.pathname.includes('/admin') &&
|
|||
|
|
!window.location.pathname.includes('/install')) {
|
|||
|
|
|
|||
|
|
console.log('🚀 Запуск SSE синхронизации файлов...');
|
|||
|
|
window.fileSyncSSE = new FileSyncSSE();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Экспортируем для использования в других модулях
|
|||
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|||
|
|
module.exports = FileSyncSSE;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
|