ваше сообщение коммита
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
|||||||
{"body":{"guestId":"1745825541972-nwmi3pcpe","language":"ru","message":""},"level":"error","message":"Error in /guest-message: null value in column \"content\" of relation \"guest_messages\" violates not-null constraint","stack":"error: null value in column \"content\" of relation \"guest_messages\" violates not-null constraint\n at /app/node_modules/pg-pool/index.js:45:11\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async /app/routes/chat.js:211:20","timestamp":"2025-04-28T07:37:57.316Z"}
|
|
||||||
{"body":{"guestId":"1745826229509-a9cc49arg","language":"ru","message":""},"level":"error","message":"Error in /guest-message: null value in column \"content\" of relation \"guest_messages\" violates not-null constraint","stack":"error: null value in column \"content\" of relation \"guest_messages\" violates not-null constraint\n at /app/node_modules/pg-pool/index.js:45:11\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async /app/routes/chat.js:211:20","timestamp":"2025-04-28T07:43:49.319Z"}
|
|
||||||
{"body":{"guestId":"1745827727896-sqo5zp8c5","language":"ru","message":""},"level":"error","message":"Error in /guest-message: invalid input syntax for type json","stack":"error: invalid input syntax for type json\n at /app/node_modules/pg-pool/index.js:45:11\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async /app/routes/chat.js:227:20","timestamp":"2025-04-28T08:08:48.293Z"}
|
|
||||||
{"code":"42703","file":"parse_target.c","length":146,"level":"error","line":"1066","message":"Error saving guest message: column \"attachment_filename\" of relation \"guest_messages\" does not exist","name":"error","position":"82","routine":"checkInsertTargets","severity":"ERROR","stack":"error: column \"attachment_filename\" of relation \"guest_messages\" does not exist\n at /app/node_modules/pg-pool/index.js:45:11\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async /app/routes/chat.js:224:20","timestamp":"2025-04-28T14:28:29.099Z"}
|
|
||||||
{"code":"23502","column":"content","detail":"Failing row contains (2, 1745850508983-zb9xi0g4t, null, ru, f, 2025-04-28 14:53:07.021316+00, image (15).png, image/png, 18375, \\x89504e470d0a1a0a0000000d4948445200000213000000ee08060000008330...).","file":"execMain.c","length":383,"level":"error","line":"2006","message":"Error saving guest message: null value in column \"content\" of relation \"guest_messages\" violates not-null constraint","name":"error","routine":"ExecConstraints","schema":"public","severity":"ERROR","stack":"error: null value in column \"content\" of relation \"guest_messages\" violates not-null constraint\n at /app/node_modules/pg-pool/index.js:45:11\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async /app/routes/chat.js:224:20","table":"guest_messages","timestamp":"2025-04-28T14:53:07.027Z"}
|
|
||||||
{"level":"error","message":"Error fetching message history for user 1: syntax error at or near \"$\"","stack":"error: syntax error at or near \"$\"\n at /app/node_modules/pg-pool/index.js:45:11\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async /app/routes/chat.js:484:20","timestamp":"2025-04-28T14:57:24.546Z"}
|
|
||||||
{"level":"error","message":"Error fetching message history for user 1: syntax error at or near \"$\"","stack":"error: syntax error at or near \"$\"\n at /app/node_modules/pg-pool/index.js:45:11\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async /app/routes/chat.js:484:20","timestamp":"2025-04-28T14:58:35.485Z"}
|
|
||||||
|
|||||||
113
frontend/src/components/NotificationDisplay.vue
Normal file
113
frontend/src/components/NotificationDisplay.vue
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<template>
|
||||||
|
<div class="notification-container">
|
||||||
|
<transition name="fade">
|
||||||
|
<div v-if="notifications.showSuccess" class="notification success">
|
||||||
|
<span class="icon">✅</span>
|
||||||
|
<span class="message">{{ notifications.successMessage }}</span>
|
||||||
|
<!-- <button @click="hideSuccess" class="close-btn">×</button> -->
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
<transition name="fade">
|
||||||
|
<div v-if="notifications.showError" class="notification error">
|
||||||
|
<span class="icon">❌</span>
|
||||||
|
<span class="message">{{ notifications.errorMessage }}</span>
|
||||||
|
<!-- <button @click="hideError" class="close-btn">×</button> -->
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
<!-- Можно добавить info/warning по аналогии -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps } from 'vue';
|
||||||
|
// Если нужна возможность закрывать вручную, импортируем useNotifications и используем hide* функции
|
||||||
|
// import { useNotifications } from '../composables/useNotifications';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
notifications: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
default: () => ({
|
||||||
|
successMessage: '',
|
||||||
|
showSuccess: false,
|
||||||
|
errorMessage: '',
|
||||||
|
showError: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Пример, если нужно закрытие вручную
|
||||||
|
// const { hideSuccessMessage, hideErrorMessage } = useNotifications();
|
||||||
|
// const hideSuccess = () => { hideSuccessMessage(); };
|
||||||
|
// const hideError = () => { hideErrorMessage(); };
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.notification-container {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 18px;
|
||||||
|
border-radius: var(--radius-md, 8px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
color: var(--color-white, #fff);
|
||||||
|
min-width: 250px;
|
||||||
|
max-width: 400px; /* Ограничиваем ширину */
|
||||||
|
word-wrap: break-word; /* Перенос длинных слов */
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification.success {
|
||||||
|
background-color: var(--color-success, #4CAF50);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification.error {
|
||||||
|
background-color: var(--color-danger, #f44336);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-right: 10px;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Анимация появления/исчезновения */
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.5s ease, transform 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стили для кнопки закрытия (если нужна) */
|
||||||
|
/*
|
||||||
|
.close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: inherit;
|
||||||
|
font-size: 1.2em;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 0 0 10px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
.close-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
</style>
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Блок баланса токенов -->
|
<!-- Блок баланса токенов -->
|
||||||
<div v-if="isAuthenticated && hasIdentityType('wallet')" class="token-balances">
|
<div v-if="isAuthenticated && hasIdentityType('wallet') && tokenBalances && tokenBalances.eth !== undefined" class="token-balances">
|
||||||
<h3>Баланс токенов:</h3>
|
<h3>Баланс токенов:</h3>
|
||||||
<div class="token-balance">
|
<div class="token-balance">
|
||||||
<span class="token-name">ETH:</span>
|
<span class="token-name">ETH:</span>
|
||||||
|
|||||||
223
frontend/src/composables/useAuthFlow.js
Normal file
223
frontend/src/composables/useAuthFlow.js
Normal file
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
383
frontend/src/composables/useChat.js
Normal file
383
frontend/src/composables/useChat.js
Normal file
@@ -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, // Экспортируем на всякий случай
|
||||||
|
};
|
||||||
|
}
|
||||||
50
frontend/src/composables/useNotifications.js
Normal file
50
frontend/src/composables/useNotifications.js
Normal file
@@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
99
frontend/src/composables/useTokenBalances.js
Normal file
99
frontend/src/composables/useTokenBalances.js
Normal file
@@ -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, // Можно не экспортировать
|
||||||
|
};
|
||||||
|
}
|
||||||
45
frontend/src/utils/helpers.js
Normal file
45
frontend/src/utils/helpers.js
Normal file
@@ -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 '';
|
||||||
|
}
|
||||||
|
};
|
||||||
53
frontend/src/utils/storage.js
Normal file
53
frontend/src/utils/storage.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user