From 01f96a9b8037df436d56e26f577eddd4a47a480c Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 30 Apr 2025 14:31:48 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B2=D0=B0=D1=88=D0=B5=20=D1=81=D0=BE=D0=BE?= =?UTF-8?q?=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=BE=D0=BC=D0=BC?= =?UTF-8?q?=D0=B8=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/NotificationDisplay.vue | 113 ++ frontend/src/components/Sidebar.vue | 2 +- frontend/src/composables/useAuthFlow.js | 223 +++ frontend/src/composables/useChat.js | 383 +++++ frontend/src/composables/useNotifications.js | 50 + frontend/src/composables/useTokenBalances.js | 99 ++ frontend/src/utils/helpers.js | 45 + frontend/src/utils/storage.js | 53 + frontend/src/views/HomeView.vue | 1428 ++--------------- 9 files changed, 1089 insertions(+), 1307 deletions(-) create mode 100644 frontend/src/components/NotificationDisplay.vue create mode 100644 frontend/src/composables/useAuthFlow.js create mode 100644 frontend/src/composables/useChat.js create mode 100644 frontend/src/composables/useNotifications.js create mode 100644 frontend/src/composables/useTokenBalances.js create mode 100644 frontend/src/utils/helpers.js create mode 100644 frontend/src/utils/storage.js diff --git a/frontend/src/components/NotificationDisplay.vue b/frontend/src/components/NotificationDisplay.vue new file mode 100644 index 0000000..b52ca68 --- /dev/null +++ b/frontend/src/components/NotificationDisplay.vue @@ -0,0 +1,113 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/Sidebar.vue b/frontend/src/components/Sidebar.vue index bb0273b..7840d25 100644 --- a/frontend/src/components/Sidebar.vue +++ b/frontend/src/components/Sidebar.vue @@ -45,7 +45,7 @@ -
+

Баланс токенов:

ETH: diff --git a/frontend/src/composables/useAuthFlow.js b/frontend/src/composables/useAuthFlow.js new file mode 100644 index 0000000..c6781cc --- /dev/null +++ b/frontend/src/composables/useAuthFlow.js @@ -0,0 +1,223 @@ +import { ref, onUnmounted } from 'vue'; +import api from '../api/axios'; +import { useAuth } from './useAuth'; +import { useNotifications } from './useNotifications'; + +export function useAuthFlow(options = {}) { + const { onAuthSuccess } = options; // Callback после успешной аутентификации/привязки + + const auth = useAuth(); + const { showSuccessMessage, showErrorMessage } = useNotifications(); + + // Состояния Telegram + const telegramAuth = ref({ + showVerification: false, + verificationCode: '', + botLink: '', + checkInterval: null, + error: '', + isLoading: false, // Добавим isLoading + }); + + // Состояния Email + const emailAuth = ref({ + showForm: false, + showVerification: false, + email: '', + verificationEmail: '', // Храним email, на который отправили код + verificationCode: '', + formatError: false, + isLoading: false, // Для отправки запроса на init + isVerifying: false, // Для проверки кода + error: '', + }); + + // --- Telegram --- + + const clearTelegramInterval = () => { + if (telegramAuth.value.checkInterval) { + clearInterval(telegramAuth.value.checkInterval); + telegramAuth.value.checkInterval = null; + console.log('[useAuthFlow] Интервал проверки Telegram авторизации очищен'); + } + }; + + const handleTelegramAuth = async () => { + if (telegramAuth.value.isLoading) return; + telegramAuth.value.isLoading = true; + telegramAuth.value.error = ''; + try { + const response = await api.post('/api/auth/telegram/init'); + if (response.data.success) { + telegramAuth.value.verificationCode = response.data.verificationCode; + telegramAuth.value.botLink = response.data.botLink; + telegramAuth.value.showVerification = true; + + // Начинаем проверку статуса + clearTelegramInterval(); // На всякий случай + telegramAuth.value.checkInterval = setInterval(async () => { + try { + console.log('[useAuthFlow] Проверка статуса Telegram...'); + // Используем checkAuth из useAuth для обновления состояния + const checkResponse = await auth.checkAuth(); + const telegramId = auth.telegramId.value; + + if (auth.isAuthenticated.value && telegramId) { + console.log('[useAuthFlow] Telegram успешно связан/подтвержден.'); + clearTelegramInterval(); + telegramAuth.value.showVerification = false; + telegramAuth.value.verificationCode = ''; + telegramAuth.value.error = ''; + + showSuccessMessage('Telegram успешно подключен!'); + if (onAuthSuccess) onAuthSuccess('telegram'); // Вызываем callback + // Нет необходимости продолжать интервал + return; + } + } catch (intervalError) { + console.error('[useAuthFlow] Ошибка при проверке статуса Telegram в интервале:', intervalError); + // Решаем, останавливать ли интервал при ошибке + // telegramAuth.value.error = 'Ошибка проверки статуса Telegram.'; + // clearTelegramInterval(); + } + }, 3000); // Проверяем каждые 3 секунды + + } else { + telegramAuth.value.error = response.data.error || 'Ошибка инициализации Telegram'; + showErrorMessage(telegramAuth.value.error); + } + } catch (error) { + console.error('[useAuthFlow] Ошибка инициализации Telegram аутентификации:', error); + const message = error?.response?.data?.error || 'Ошибка при инициализации аутентификации через Telegram'; + telegramAuth.value.error = message; + showErrorMessage(message); + } finally { + telegramAuth.value.isLoading = false; + } + }; + + const cancelTelegramAuth = () => { + clearTelegramInterval(); + telegramAuth.value.showVerification = false; + telegramAuth.value.verificationCode = ''; + telegramAuth.value.error = ''; + telegramAuth.value.isLoading = false; + console.log('[useAuthFlow] Аутентификация Telegram отменена'); + }; + + // --- Email --- + + const showEmailForm = () => { + emailAuth.value.showForm = true; + emailAuth.value.showVerification = false; + emailAuth.value.email = ''; + emailAuth.value.formatError = false; + emailAuth.value.error = ''; + emailAuth.value.isLoading = false; + emailAuth.value.isVerifying = false; + }; + + const sendEmailVerification = async () => { + emailAuth.value.formatError = false; + emailAuth.value.error = ''; + + if (!emailAuth.value.email || !emailAuth.value.email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) { + emailAuth.value.formatError = true; + return; + } + + if (emailAuth.value.isLoading) return; + emailAuth.value.isLoading = true; + + try { + const response = await api.post('/api/auth/email/init', { email: emailAuth.value.email }); + if (response.data.success) { + emailAuth.value.verificationEmail = emailAuth.value.email; // Сохраняем email + emailAuth.value.showForm = false; + emailAuth.value.showVerification = true; + emailAuth.value.verificationCode = ''; // Очищаем поле кода + console.log('[useAuthFlow] Код верификации Email отправлен на:', emailAuth.value.verificationEmail); + } else { + emailAuth.value.error = response.data.error || 'Ошибка инициализации аутентификации по email'; + showErrorMessage(emailAuth.value.error); + } + } catch (error) { + console.error('[useAuthFlow] Ошибка при запросе инициализации Email:', error); + const message = error?.response?.data?.error || 'Ошибка при запросе кода подтверждения'; + emailAuth.value.error = message; + showErrorMessage(message); + } finally { + emailAuth.value.isLoading = false; + } + }; + + const verifyEmailCode = async () => { + emailAuth.value.error = ''; + if (!emailAuth.value.verificationCode) { + emailAuth.value.error = 'Пожалуйста, введите код верификации'; + return; + } + if (emailAuth.value.isVerifying) return; + emailAuth.value.isVerifying = true; + + try { + const response = await api.post('/api/auth/email/verify-code', { + email: emailAuth.value.verificationEmail, + code: emailAuth.value.verificationCode, + }); + + if (response.data.success) { + console.log('[useAuthFlow] Email успешно подтвержден:', emailAuth.value.verificationEmail); + emailAuth.value.showForm = false; + emailAuth.value.showVerification = false; + emailAuth.value.error = ''; + + // Обновляем состояние аутентификации через useAuth + await auth.checkAuth(); + showSuccessMessage(`Email ${emailAuth.value.verificationEmail} успешно подтвержден!`); + + if (onAuthSuccess) onAuthSuccess('email'); // Вызываем callback + + } else { + emailAuth.value.error = response.data.message || 'Неверный код верификации'; + // Не используем showErrorMessage здесь, т.к. ошибка отображается локально в форме + } + } catch (error) { + console.error('[useAuthFlow] Ошибка проверки кода Email:', error); + const message = error?.response?.data?.error || 'Ошибка при проверке кода'; + emailAuth.value.error = message; + // Не используем showErrorMessage здесь + } finally { + emailAuth.value.isVerifying = false; + } + }; + + const cancelEmailAuth = () => { + emailAuth.value.showForm = false; + emailAuth.value.showVerification = false; + emailAuth.value.email = ''; + emailAuth.value.verificationCode = ''; + emailAuth.value.error = ''; + emailAuth.value.formatError = false; + emailAuth.value.isLoading = false; + emailAuth.value.isVerifying = false; + console.log('[useAuthFlow] Аутентификация Email отменена'); + }; + + // Очистка интервала при размонтировании + onUnmounted(() => { + clearTelegramInterval(); + }); + + return { + telegramAuth, + handleTelegramAuth, + cancelTelegramAuth, + + emailAuth, + showEmailForm, + sendEmailVerification, + verifyEmailCode, + cancelEmailAuth, + }; +} \ No newline at end of file diff --git a/frontend/src/composables/useChat.js b/frontend/src/composables/useChat.js new file mode 100644 index 0000000..56b0460 --- /dev/null +++ b/frontend/src/composables/useChat.js @@ -0,0 +1,383 @@ +import { ref, computed, watch, onMounted } from 'vue'; +import api from '../api/axios'; +import { getFromStorage, setToStorage, removeFromStorage } from '../utils/storage'; +import { generateUniqueId } from '../utils/helpers'; + +export function useChat(auth) { + const messages = ref([]); + const newMessage = ref(''); + const attachments = ref([]); // Теперь это массив File объектов + const userLanguage = ref('ru'); + const isLoading = ref(false); // Общая загрузка (например, при отправке) + const hasUserSentMessage = ref(getFromStorage('hasUserSentMessage') === true); + + const messageLoading = ref({ + isLoadingHistory: false, // Загрузка истории + hasMoreMessages: false, + offset: 0, + limit: 30, + isHistoryLoadingInProgress: false, // Флаг для предотвращения параллельных запросов истории + isLinkingGuest: false, // Флаг для процесса связывания гостевых сообщений (пока не используется активно) + }); + + const guestId = ref(getFromStorage('guestId', '')); + + const shouldLoadHistory = computed(() => { + return auth.isAuthenticated.value || !!guestId.value; + }); + + // --- Загрузка истории --- + const loadMessages = async (options = {}) => { + const { silent = false, initial = false, authType = null } = options; + + if (messageLoading.value.isHistoryLoadingInProgress) { + console.warn('[useChat] Загрузка истории уже идет, пропуск.'); + return; + } + messageLoading.value.isHistoryLoadingInProgress = true; + + // Если initial=true, сбрасываем offset и hasMoreMessages + if (initial) { + console.log('[useChat] Начальная загрузка истории...'); + messageLoading.value.offset = 0; + messageLoading.value.hasMoreMessages = false; + messages.value = []; // Очищаем текущие сообщения перед начальной загрузкой + } + + // Не загружаем больше, если уже грузим или больше нет + if (!initial && (!messageLoading.value.hasMoreMessages || messageLoading.value.isLoadingHistory)) { + messageLoading.value.isHistoryLoadingInProgress = false; + return; + } + + messageLoading.value.isLoadingHistory = true; + if (!silent && initial) isLoading.value = true; // Показываем общий лоадер только при начальной загрузке + + console.log( + `[useChat] Загрузка истории сообщений (initial: ${initial}, authType: ${authType}, offset: ${messageLoading.value.offset})...` + ); + + try { + // --- Логика ожидания привязки гостя (упрощенная) --- + // TODO: Рассмотреть более надежный механизм, если это необходимо + if (authType) { + console.log(`[useChat] Ожидание после ${authType} аутентификации...`); + await new Promise((resolve) => setTimeout(resolve, 1500)); // Увеличена задержка + console.log('[useChat] Ожидание завершено, продолжаем загрузку истории.'); + } + // --- Конец логики ожидания --- + + // Определяем, нужно ли запрашивать count + let totalMessages = -1; + if (initial || messageLoading.value.offset === 0) { + try { + const countResponse = await api.get('/api/chat/history', { params: { count_only: true } }); + if (!countResponse.data.success) throw new Error('Не удалось получить количество сообщений'); + totalMessages = countResponse.data.total || countResponse.data.count || 0; + console.log(`[useChat] Всего сообщений в истории: ${totalMessages}`); + } catch(countError) { + console.error('[useChat] Ошибка получения количества сообщений:', countError); + // Не прерываем выполнение, попробуем загрузить без total + } + } + + let effectiveOffset = messageLoading.value.offset; + // Если это первая загрузка и мы знаем total, рассчитаем смещение для последних сообщений + if (initial && totalMessages > 0 && totalMessages > messageLoading.value.limit) { + effectiveOffset = Math.max(0, totalMessages - messageLoading.value.limit); + console.log(`[useChat] Рассчитано начальное смещение: ${effectiveOffset}`); + } + + const response = await api.get('/api/chat/history', { + params: { + offset: effectiveOffset, + limit: messageLoading.value.limit, + }, + }); + + if (response.data.success) { + const loadedMessages = response.data.messages || []; + console.log(`[useChat] Загружено ${loadedMessages.length} сообщений.`); + + if (loadedMessages.length > 0) { + // Добавляем к существующим (в начало для истории, в конец для начальной загрузки) + if (initial) { + messages.value = loadedMessages; + } else { + messages.value = [...loadedMessages, ...messages.value]; + } + + // Обновляем смещение для следующей загрузки + // Если загружали последние, offset = total - limit + loaded + if (initial && totalMessages > 0 && effectiveOffset > 0) { + messageLoading.value.offset = effectiveOffset + loadedMessages.length; + } else { + messageLoading.value.offset += loadedMessages.length; + } + console.log(`[useChat] Новое смещение: ${messageLoading.value.offset}`); + + // Проверяем, есть ли еще сообщения для загрузки + // Используем totalMessages, если он был успешно получен + if (totalMessages >= 0) { + messageLoading.value.hasMoreMessages = messageLoading.value.offset < totalMessages; + } else { + // Если total не известен, считаем, что есть еще, если загрузили полный лимит + messageLoading.value.hasMoreMessages = loadedMessages.length === messageLoading.value.limit; + } + console.log(`[useChat] Есть еще сообщения: ${messageLoading.value.hasMoreMessages}`); + } else { + // Если сообщений не пришло, значит, больше нет + messageLoading.value.hasMoreMessages = false; + } + + // Очищаем гостевые данные после успешной аутентификации и загрузки + if (authType) { + removeFromStorage('guestMessages'); + removeFromStorage('guestId'); + guestId.value = ''; + } + + // Считаем, что пользователь отправлял сообщение, если история не пуста + if (messages.value.length > 0) { + hasUserSentMessage.value = true; + setToStorage('hasUserSentMessage', true); + } + + } else { + console.error('[useChat] API вернул ошибку при загрузке истории:', response.data.error); + messageLoading.value.hasMoreMessages = false; // Считаем, что больше нет при ошибке + } + } catch (error) { + console.error('[useChat] Ошибка загрузки истории сообщений:', error); + messageLoading.value.hasMoreMessages = false; // Считаем, что больше нет при ошибке + } finally { + messageLoading.value.isLoadingHistory = false; + messageLoading.value.isHistoryLoadingInProgress = false; + if (initial) isLoading.value = false; + } + }; + + // --- Отправка сообщения --- + const handleSendMessage = async (payload) => { + // --- НАЧАЛО ДОБАВЛЕННЫХ ЛОГОВ --- + console.log('[useChat] handleSendMessage called. Payload:', payload); + console.log('[useChat] Current auth state:', { + isAuthenticated: auth.isAuthenticated.value, + userId: auth.userId.value, + authType: auth.authType.value, + }); + // --- КОНЕЦ ДОБАВЛЕННЫХ ЛОГОВ --- + + const { message: text, attachments: files } = payload; // files - массив File объектов + const userMessageContent = text.trim(); + + // Проверка на пустое сообщение (если нет ни текста, ни файлов) + if (!userMessageContent && (!files || files.length === 0)) { + console.warn('[useChat] Попытка отправить пустое сообщение.'); + return; + } + + isLoading.value = true; + const tempId = generateUniqueId(); + const isGuestMessage = !auth.isAuthenticated.value; + + // Создаем локальное сообщение для отображения + const userMessage = { + id: tempId, + content: userMessageContent || `[${files.length} вложений]`, // Отображение для UI + sender_type: 'user', + role: 'user', + isLocal: true, + isGuest: isGuestMessage, + timestamp: new Date().toISOString(), + // Генерируем инфо для отображения в Message.vue (без File объектов) + attachments: files ? files.map(f => ({ + originalname: f.name, + size: f.size, + mimetype: f.type, + // url: URL.createObjectURL(f) // Можно создать временный URL для превью, если Message.vue его использует + })) : [], + hasError: false + }; + messages.value.push(userMessage); + + // Очистка ввода происходит в ChatInterface + // newMessage.value = ''; + // attachments.value = []; + + try { + const formData = new FormData(); + formData.append('message', userMessageContent); + formData.append('language', userLanguage.value); + + if (files && files.length > 0) { + files.forEach((file) => { + formData.append('attachments', file, file.name); + }); + } + + let apiUrl = '/api/chat/message'; + if (isGuestMessage) { + if (!guestId.value) { + guestId.value = generateUniqueId(); + setToStorage('guestId', guestId.value); + } + formData.append('guestId', guestId.value); + apiUrl = '/api/chat/guest-message'; + } + + const response = await api.post(apiUrl, formData, { + headers: { + // Content-Type устанавливается браузером для FormData + } + }); + + const userMsgIndex = messages.value.findIndex((m) => m.id === tempId); + + if (response.data.success) { + console.log('[useChat] Сообщение успешно отправлено:', response.data); + // Обновляем локальное сообщение данными с сервера + if (userMsgIndex !== -1) { + const serverUserMessage = response.data.userMessage || { id: response.data.messageId }; + messages.value[userMsgIndex].id = serverUserMessage.id || tempId; // Используем серверный ID + messages.value[userMsgIndex].isLocal = false; + messages.value[userMsgIndex].timestamp = serverUserMessage.created_at || new Date().toISOString(); + // Опционально: обновить content/attachments с сервера, если они отличаются + } + + // Добавляем ответ ИИ, если есть + if (response.data.aiMessage) { + messages.value.push({ + ...response.data.aiMessage, + sender_type: 'assistant', // Убедимся, что тип правильный + role: 'assistant', + }); + } + + // Сохраняем гостевое сообщение (если нужно) + // В текущей реализации HomeView гостевые сообщения из localstorage загружаются только при старте + // Если нужна синхронизация после отправки, логику нужно добавить/изменить + /* + if (isGuestMessage) { + try { + const storedMessages = getFromStorage('guestMessages', []); + // Добавляем сообщение пользователя (с серверным ID) + storedMessages.push({ + id: messages.value[userMsgIndex].id, + content: userMessageContent, + sender_type: 'user', + role: 'user', + isGuest: true, + timestamp: messages.value[userMsgIndex].timestamp, + attachmentsInfo: messages.value[userMsgIndex].attachments // Сохраняем инфо о файлах + }); + setToStorage('guestMessages', storedMessages); + } catch (storageError) { + console.error('[useChat] Ошибка сохранения гостевого сообщения в localStorage:', storageError); + } + } + */ + + hasUserSentMessage.value = true; + setToStorage('hasUserSentMessage', true); + + } else { + throw new Error(response.data.error || 'Ошибка отправки сообщения от API'); + } + + } catch (error) { + console.error('[useChat] Ошибка отправки сообщения:', error); + const userMsgIndex = messages.value.findIndex((m) => m.id === tempId); + if (userMsgIndex !== -1) { + messages.value[userMsgIndex].hasError = true; + messages.value[userMsgIndex].isLocal = false; // Убираем статус "отправка" + } + // Добавляем системное сообщение об ошибке + messages.value.push({ + id: `error-${Date.now()}`, + content: 'Произошла ошибка при отправке сообщения. Пожалуйста, попробуйте еще раз.', + sender_type: 'system', + role: 'system', + timestamp: new Date().toISOString(), + }); + } finally { + isLoading.value = false; + } + }; + + // --- Управление гостевыми сообщениями --- + const loadGuestMessagesFromStorage = () => { + if (!auth.isAuthenticated.value && guestId.value) { + try { + const storedMessages = getFromStorage('guestMessages'); + if (storedMessages && Array.isArray(storedMessages) && storedMessages.length > 0) { + console.log(`[useChat] Найдено ${storedMessages.length} сохраненных гостевых сообщений`); + // Добавляем только если текущий список пуст (чтобы не дублировать при HMR) + if(messages.value.length === 0) { + messages.value = storedMessages.map(m => ({ ...m, isGuest: true })); // Помечаем как гостевые + hasUserSentMessage.value = true; + } + } + } catch (e) { + console.error('[useChat] Ошибка загрузки гостевых сообщений из localStorage:', e); + removeFromStorage('guestMessages'); // Очистить при ошибке парсинга + } + } + }; + + // --- Watchers --- + // Сортировка сообщений при изменении + watch(messages, (newMessages) => { + // Сортируем только если массив изменился + if (newMessages.length > 1) { + messages.value.sort((a, b) => { + const dateA = new Date(a.timestamp || a.created_at || 0); + const dateB = new Date(b.timestamp || b.created_at || 0); + return dateA - dateB; + }); + } + }, { deep: false }); // deep: false т.к. нас интересует только добавление/удаление элементов + + // Сброс чата при выходе пользователя + watch(() => auth.isAuthenticated.value, (isAuth, wasAuth) => { + if (!isAuth && wasAuth) { // Если пользователь разлогинился + console.log('[useChat] Пользователь вышел, сброс состояния чата.'); + messages.value = []; + messageLoading.value.offset = 0; + messageLoading.value.hasMoreMessages = false; + hasUserSentMessage.value = false; + newMessage.value = ''; + attachments.value = []; + // Гостевые данные очищаются при успешной аутентификации в loadMessages + // или если пользователь сам очистит localStorage + } + }); + + // --- Инициализация --- + onMounted(() => { + if (!auth.isAuthenticated.value && guestId.value) { + loadGuestMessagesFromStorage(); + } else if (auth.isAuthenticated.value) { + loadMessages({ initial: true }); + } + // Добавляем слушатель для возможности принудительной перезагрузки + // window.addEventListener('load-chat-history', () => loadMessages({ initial: true })); + }); + + // onUnmounted(() => { + // window.removeEventListener('load-chat-history', () => loadMessages({ initial: true })); + // }); + + return { + messages, + newMessage, // v-model + attachments, // v-model + isLoading, + messageLoading, // Содержит isLoadingHistory и hasMoreMessages + userLanguage, + hasUserSentMessage, + loadMessages, + handleSendMessage, + loadGuestMessagesFromStorage, // Экспортируем на всякий случай + }; +} \ No newline at end of file diff --git a/frontend/src/composables/useNotifications.js b/frontend/src/composables/useNotifications.js new file mode 100644 index 0000000..80c14ae --- /dev/null +++ b/frontend/src/composables/useNotifications.js @@ -0,0 +1,50 @@ +import { ref } from 'vue'; + +export function useNotifications() { + const notifications = ref({ + successMessage: '', + showSuccess: false, + errorMessage: '', + showError: false, + // Можно добавить info/warning по аналогии + }); + + let successTimeout = null; + let errorTimeout = null; + + const showSuccessMessage = (message, duration = 3000) => { + clearTimeout(successTimeout); + notifications.value.successMessage = message; + notifications.value.showSuccess = true; + successTimeout = setTimeout(() => { + notifications.value.showSuccess = false; + }, duration); + }; + + const showErrorMessage = (message, duration = 3000) => { + clearTimeout(errorTimeout); + notifications.value.errorMessage = message; + notifications.value.showError = true; + errorTimeout = setTimeout(() => { + notifications.value.showError = false; + }, duration); + }; + + const hideSuccessMessage = () => { + clearTimeout(successTimeout); + notifications.value.showSuccess = false; + }; + + const hideErrorMessage = () => { + clearTimeout(errorTimeout); + notifications.value.showError = false; + }; + + return { + notifications, + showSuccessMessage, + showErrorMessage, + hideSuccessMessage, + hideErrorMessage + }; +} \ No newline at end of file diff --git a/frontend/src/composables/useTokenBalances.js b/frontend/src/composables/useTokenBalances.js new file mode 100644 index 0000000..aa04470 --- /dev/null +++ b/frontend/src/composables/useTokenBalances.js @@ -0,0 +1,99 @@ +import { ref, watch, onUnmounted } from 'vue'; +import { fetchTokenBalances } from '../services/tokens'; +import { useAuth } from './useAuth'; // Предполагаем, что useAuth предоставляет identities + +export function useTokenBalances() { + const auth = useAuth(); // Получаем доступ к состоянию аутентификации + const tokenBalances = ref({ + eth: '0', + bsc: '0', + arbitrum: '0', + polygon: '0', + }); + let balanceUpdateInterval = null; + + const getIdentityValue = (type) => { + if (!auth.identities?.value) return null; + const identity = auth.identities.value.find((identity) => identity.provider === type); + return identity ? identity.provider_id : null; + }; + + const updateBalances = async () => { + if (auth.isAuthenticated.value) { + const walletAddress = auth.address?.value || getIdentityValue('wallet'); + if (walletAddress) { + try { + console.log('[useTokenBalances] Запрос балансов для адреса:', walletAddress); + const balances = await fetchTokenBalances(walletAddress); + console.log('[useTokenBalances] Полученные балансы:', balances); + tokenBalances.value = { + eth: balances.eth || '0', + bsc: balances.bsc || '0', + arbitrum: balances.arbitrum || '0', + polygon: balances.polygon || '0', + }; + console.log('[useTokenBalances] Обновленные балансы:', tokenBalances.value); + } catch (error) { + console.error('[useTokenBalances] Ошибка при обновлении балансов:', error); + // Возможно, стоит сбросить балансы при ошибке + tokenBalances.value = { eth: '0', bsc: '0', arbitrum: '0', polygon: '0' }; + } + } else { + console.log('[useTokenBalances] Не найден адрес кошелька для запроса балансов.'); + tokenBalances.value = { eth: '0', bsc: '0', arbitrum: '0', polygon: '0' }; + } + } else { + console.log('[useTokenBalances] Пользователь не аутентифицирован, сброс балансов.'); + tokenBalances.value = { eth: '0', bsc: '0', arbitrum: '0', polygon: '0' }; + } + }; + + const startBalanceUpdates = (intervalMs = 300000) => { + stopBalanceUpdates(); // Остановить предыдущий интервал, если он был + console.log('[useTokenBalances] Запуск обновления балансов...'); + updateBalances(); // Обновить сразу + balanceUpdateInterval = setInterval(updateBalances, intervalMs); + }; + + const stopBalanceUpdates = () => { + if (balanceUpdateInterval) { + console.log('[useTokenBalances] Остановка обновления балансов.'); + clearInterval(balanceUpdateInterval); + balanceUpdateInterval = null; + } + }; + + // Следим за аутентификацией и наличием кошелька + watch( + () => [auth.isAuthenticated.value, auth.address?.value, getIdentityValue('wallet')], + ([isAuth, directAddress, identityAddress]) => { + const hasWallet = directAddress || identityAddress; + if (isAuth && hasWallet) { + // Если пользователь аутентифицирован, имеет кошелек, и интервал не запущен + if (!balanceUpdateInterval) { + startBalanceUpdates(); + } + // Если адрес изменился, принудительно обновить + // updateBalances(); // Убрал, т.к. startBalanceUpdates уже вызывает updateBalances() + } else if (!isAuth || !hasWallet) { + // Если пользователь вышел, отвязал кошелек, или не аутентифицирован + stopBalanceUpdates(); + // Сбрасываем балансы + tokenBalances.value = { eth: '0', bsc: '0', arbitrum: '0', polygon: '0' }; + } + }, + { immediate: true } // Запустить проверку сразу при инициализации + ); + + // Остановка интервала при размонтировании + onUnmounted(() => { + stopBalanceUpdates(); + }); + + return { + tokenBalances, + updateBalances, + startBalanceUpdates, // Можно не экспортировать, если управление полностью автоматическое + stopBalanceUpdates, // Можно не экспортировать + }; +} \ No newline at end of file diff --git a/frontend/src/utils/helpers.js b/frontend/src/utils/helpers.js new file mode 100644 index 0000000..146b38f --- /dev/null +++ b/frontend/src/utils/helpers.js @@ -0,0 +1,45 @@ +/** + * Генерирует уникальный ID + * @returns {string} - Уникальный ID + */ +export const generateUniqueId = () => { + return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; +}; + +/** + * Сокращает адрес кошелька + * @param {string} address - Адрес кошелька + * @returns {string} - Сокращенный адрес + */ +export const truncateAddress = (address) => { + if (!address) return ''; + // Добавим проверку на длину, чтобы не было ошибок + if (address.length <= 10) return address; + return `${address.substring(0, 6)}...${address.substring(address.length - 4)}`; +}; + +/** + * Форматирует время в человекочитаемый вид + * @param {string | number | Date} timestamp - Метка времени + * @returns {string} - Форматированное время + */ +export const formatTime = (timestamp) => { + if (!timestamp) return ''; + try { + const date = new Date(timestamp); + if (isNaN(date.getTime())) { + console.warn('Invalid timestamp for formatTime:', timestamp); + return ''; + } + return date.toLocaleString([], { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + } catch (error) { + console.error('Error formatting time:', error, timestamp); + return ''; + } +}; \ No newline at end of file diff --git a/frontend/src/utils/storage.js b/frontend/src/utils/storage.js new file mode 100644 index 0000000..c9c284b --- /dev/null +++ b/frontend/src/utils/storage.js @@ -0,0 +1,53 @@ +export const isLocalStorageAvailable = () => { + try { + const test = '__storage_test__'; + window.localStorage.setItem(test, test); + window.localStorage.removeItem(test); + return true; + } catch (e) { + console.error('localStorage is not available:', e); + return false; + } +}; + +export const getFromStorage = (key, defaultValue = null) => { + if (!isLocalStorageAvailable()) return defaultValue; + try { + const item = window.localStorage.getItem(key); + // Пытаемся распарсить JSON, если не получается - возвращаем как есть или defaultValue + try { + return item ? JSON.parse(item) : defaultValue; + } catch (e) { + return item || defaultValue; + } + } catch (e) { + console.error(`Error getting ${key} from localStorage:`, e); + return defaultValue; + } +}; + +export const setToStorage = (key, value) => { + if (!isLocalStorageAvailable()) return false; + try { + // Сериализуем объекты и массивы в JSON + const valueToStore = typeof value === 'object' || Array.isArray(value) + ? JSON.stringify(value) + : value; + window.localStorage.setItem(key, valueToStore); + return true; + } catch (e) { + console.error(`Error setting ${key} in localStorage:`, e); + return false; + } +}; + +export const removeFromStorage = (key) => { + if (!isLocalStorageAvailable()) return false; + try { + window.localStorage.removeItem(key); + return true; + } catch (e) { + console.error(`Error removing ${key} from localStorage:`, e); + return false; + } +}; \ No newline at end of file diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index af4e74a..daf7e79 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -23,7 +23,7 @@ + + +