Баланс токенов:
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 @@
+
+
+