ваше сообщение коммита

This commit is contained in:
2025-04-21 16:52:32 +03:00
parent ab05988017
commit 4648aab7d5
20 changed files with 4206 additions and 4068 deletions

View File

@@ -3,7 +3,7 @@
<div class="conversation-list">
<div class="list-header">
<h3>Диалоги</h3>
<button @click="createNewConversation" class="new-conversation-btn">
<button class="new-conversation-btn" @click="createNewConversation">
<span>+</span> Новый диалог
</button>
</div>
@@ -40,205 +40,110 @@
</template>
<script setup>
import { ref, onMounted, computed, defineEmits, watch, inject } from 'vue';
import axios from 'axios';
import { ref, onMounted, computed, defineEmits, watch, inject } from 'vue';
import axios from 'axios';
const emit = defineEmits(['select-conversation']);
const auth = inject('auth');
const isAuthenticated = computed(() => auth.isAuthenticated.value);
const emit = defineEmits(['select-conversation']);
const auth = inject('auth');
const isAuthenticated = computed(() => auth.isAuthenticated.value);
const conversations = ref([]);
const loading = ref(true);
const selectedConversationId = ref(null);
const conversations = ref([]);
const loading = ref(true);
const selectedConversationId = ref(null);
// Следим за изменением статуса аутентификации
watch(() => isAuthenticated.value, (authenticated) => {
if (!authenticated) {
conversations.value = []; // Очищаем список бесед при отключении
selectedConversationId.value = null;
}
});
// Загрузка списка диалогов
const fetchConversations = async () => {
try {
loading.value = true;
const response = await axios.get('/api/messages/conversations');
conversations.value = response.data;
// Если есть диалоги и не выбран ни один, выбираем первый
if (conversations.value.length > 0 && !selectedConversationId.value) {
selectConversation(conversations.value[0].conversation_id);
// Следим за изменением статуса аутентификации
watch(
() => isAuthenticated.value,
(authenticated) => {
if (!authenticated) {
conversations.value = []; // Очищаем список бесед при отключении
selectedConversationId.value = null;
}
}
} catch (error) {
console.error('Error fetching conversations:', error);
} finally {
loading.value = false;
}
};
);
// Выбор диалога
const selectConversation = (conversationId) => {
selectedConversationId.value = conversationId;
emit('select-conversation', conversationId);
};
// Загрузка списка диалогов
const fetchConversations = async () => {
try {
loading.value = true;
const response = await axios.get('/api/messages/conversations');
conversations.value = response.data;
// Создание нового диалога
const createNewConversation = async () => {
try {
const response = await axios.post('/api/messages/conversations', {
title: 'Новый диалог',
});
// Если есть диалоги и не выбран ни один, выбираем первый
if (conversations.value.length > 0 && !selectedConversationId.value) {
selectConversation(conversations.value[0].conversation_id);
}
} catch (error) {
console.error('Error fetching conversations:', error);
} finally {
loading.value = false;
}
};
// Добавляем новый диалог в список
const newConversation = {
conversation_id: response.data.id,
title: response.data.title,
username: authStore.username,
address: authStore.address,
message_count: 0,
last_activity: response.data.created_at,
created_at: response.data.created_at,
};
// Выбор диалога
const selectConversation = (conversationId) => {
selectedConversationId.value = conversationId;
emit('select-conversation', conversationId);
};
conversations.value.unshift(newConversation);
// Создание нового диалога
const createNewConversation = async () => {
try {
const response = await axios.post('/api/messages/conversations', {
title: 'Новый диалог',
});
// Выбираем новый диалог
selectConversation(newConversation.conversation_id);
} catch (error) {
console.error('Error creating conversation:', error);
}
};
// Добавляем новый диалог в список
const newConversation = {
conversation_id: response.data.id,
title: response.data.title,
username: authStore.username,
address: authStore.address,
message_count: 0,
last_activity: response.data.created_at,
created_at: response.data.created_at,
};
// Форматирование времени
const formatTime = (timestamp) => {
if (!timestamp) return '';
conversations.value.unshift(newConversation);
const date = new Date(timestamp);
const now = new Date();
const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));
// Выбираем новый диалог
selectConversation(newConversation.conversation_id);
} catch (error) {
console.error('Error creating conversation:', error);
}
};
if (diffDays === 0) {
// Сегодня - показываем только время
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
} else if (diffDays === 1) {
// Вчера
return 'Вчера';
} else if (diffDays < 7) {
// В течение недели - показываем день недели
const days = ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'];
return days[date.getDay()];
} else {
// Более недели назад - показываем дату
return date.toLocaleDateString();
}
};
// Форматирование времени
const formatTime = (timestamp) => {
if (!timestamp) return '';
// Загрузка диалогов при монтировании компонента
onMounted(() => {
fetchConversations();
});
const date = new Date(timestamp);
const now = new Date();
const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));
// Экспорт методов для использования в родительском компоненте
defineExpose({
fetchConversations,
});
if (diffDays === 0) {
// Сегодня - показываем только время
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
} else if (diffDays === 1) {
// Вчера
return 'Вчера';
} else if (diffDays < 7) {
// В течение недели - показываем день недели
const days = ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'];
return days[date.getDay()];
} else {
// Более недели назад - показываем дату
return date.toLocaleDateString();
}
};
// Загрузка диалогов при монтировании компонента
onMounted(() => {
fetchConversations();
});
// Экспорт методов для использования в родительском компоненте
defineExpose({
fetchConversations,
});
</script>
<style scoped>
.conversation-list {
display: flex;
flex-direction: column;
width: 300px;
border-right: 1px solid #e0e0e0;
background-color: #f9f9f9;
height: 100%;
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #e0e0e0;
}
.list-header h3 {
margin: 0;
font-size: 1.2rem;
}
.new-conversation-btn {
display: flex;
align-items: center;
background-color: #4caf50;
color: white;
border: none;
border-radius: 4px;
padding: 0.5rem 0.75rem;
cursor: pointer;
font-size: 0.9rem;
}
.new-conversation-btn span {
font-size: 1.2rem;
margin-right: 0.25rem;
}
.loading,
.empty-list {
padding: 2rem;
text-align: center;
color: #666;
}
.empty-list p {
margin: 0.5rem 0;
}
.conversations {
flex: 1;
overflow-y: auto;
}
.conversation-item {
padding: 1rem;
border-bottom: 1px solid #e0e0e0;
cursor: pointer;
transition: background-color 0.2s;
}
.conversation-item:hover {
background-color: #f0f0f0;
}
.conversation-item.active {
background-color: #e8f5e9;
color: #4caf50;
}
.conversation-title {
font-weight: 500;
margin-bottom: 0.5rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.conversation-meta {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
color: #888;
}
.time {
font-size: 0.8rem;
}
.connect-wallet-prompt {
text-align: center;
padding: 2rem;
color: #666;
}
</style>

View File

@@ -1,14 +1,14 @@
<template>
<div class="message-input">
<textarea
ref="textareaRef"
v-model="message"
placeholder="Введите сообщение..."
@keydown.enter.prevent="handleEnter"
ref="textareaRef"
:disabled="sending"
></textarea>
@keydown.enter.prevent="handleEnter"
/>
<button @click="sendMessage" class="send-button" :disabled="!message.trim() || sending">
<button class="send-button" :disabled="!message.trim() || sending" @click="sendMessage">
<span v-if="sending">Отправка...</span>
<span v-else>Отправить</span>
</button>
@@ -16,199 +16,149 @@
</template>
<script setup>
import { ref, defineEmits, nextTick } from 'vue';
import axios from 'axios';
import { ref, defineEmits, nextTick } from 'vue';
import axios from 'axios';
const props = defineProps({
conversationId: {
type: [Number, String],
required: true,
},
});
const props = defineProps({
conversationId: {
type: [Number, String],
required: true,
},
});
const emit = defineEmits(['message-sent']);
const message = ref('');
const sending = ref(false);
const textareaRef = ref(null);
const emit = defineEmits(['message-sent']);
const message = ref('');
const sending = ref(false);
const textareaRef = ref(null);
// Обработка нажатия Enter
const handleEnter = (event) => {
// Если нажат Shift+Enter, добавляем перенос строки
if (event.shiftKey) {
return;
}
// Обработка нажатия Enter
const handleEnter = (event) => {
// Если нажат Shift+Enter, добавляем перенос строки
if (event.shiftKey) {
return;
}
// Иначе отправляем сообщение
sendMessage();
};
// Отправка сообщения
const sendMessage = async () => {
const messageText = message.value.trim();
if (!messageText) return;
const userMessage = {
id: Date.now(),
content: messageText,
role: auth.isAuthenticated ? 'user' : 'guest',
timestamp: new Date().toISOString()
// Иначе отправляем сообщение
sendMessage();
};
messages.value.push(userMessage);
// Отправка сообщения
const sendMessage = async () => {
const messageText = message.value.trim();
if (!messageText) return;
try {
// Логируем параметры запроса
console.log('Sending message to Ollama:', {
message: messageText,
language: userLanguage.value
});
const userMessage = {
id: Date.now(),
content: messageText,
role: auth.isAuthenticated ? 'user' : 'guest',
timestamp: new Date().toISOString(),
};
const response = await axios.post('/api/chat/message', {
message: messageText,
language: userLanguage.value
});
messages.value.push(userMessage);
// Логируем ответ от Ollama
console.log('Response from Ollama:', response.data);
try {
// Логируем параметры запроса
console.log('Sending message to Ollama:', {
message: messageText,
language: userLanguage.value,
});
// Обработка ответа
messages.value.push({
id: Date.now() + 1,
content: response.data.message,
role: 'assistant',
timestamp: new Date().toISOString()
});
const response = await axios.post('/api/chat/message', {
message: messageText,
language: userLanguage.value,
});
// Логируем ответ от Ollama
console.log('Response from Ollama:', response.data);
// Обработка ответа
messages.value.push({
id: Date.now() + 1,
content: response.data.message,
role: 'assistant',
timestamp: new Date().toISOString(),
});
// Очищаем поле ввода
message.value = '';
// Фокусируемся на поле ввода
nextTick(() => {
textareaRef.value.focus();
});
// Уведомляем родительский компонент о новых сообщениях
emit('message-sent', [response.data.userMessage, response.data.aiMessage]);
} catch (error) {
console.error('Ошибка при отправке сообщения:', error);
} finally {
sending.value = false;
}
};
// Сброс поля ввода
const resetInput = () => {
message.value = '';
};
// Экспорт методов для использования в родительском компоненте
defineExpose({
resetInput,
focus: () => textareaRef.value?.focus(),
});
const sendGuestMessage = async (messageText) => {
if (!messageText.trim()) return;
const userMessage = {
id: Date.now(),
content: messageText,
role: 'user',
timestamp: new Date().toISOString(),
isGuest: true,
};
// Добавляем сообщение пользователя в локальную историю
messages.value.push(userMessage);
// Сохраняем сообщение в массиве гостевых сообщений
guestMessages.value.push(userMessage);
// Сохраняем гостевые сообщения в localStorage
localStorage.setItem('guestMessages', JSON.stringify(guestMessages.value));
// Очищаем поле ввода
message.value = '';
newMessage.value = '';
// Фокусируемся на поле ввода
nextTick(() => {
textareaRef.value.focus();
});
// Прокрутка вниз
await nextTick();
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
}
// Уведомляем родительский компонент о новых сообщениях
emit('message-sent', [response.data.userMessage, response.data.aiMessage]);
} catch (error) {
console.error('Ошибка при отправке сообщения:', error);
} finally {
sending.value = false;
}
};
// Устанавливаем состояние загрузки
isLoading.value = true;
// Сброс поля ввода
const resetInput = () => {
message.value = '';
};
// Вместо отправки запроса к Ollama, отправляем сообщение с кнопками для аутентификации
const authMessage = {
id: Date.now() + 1,
content: 'Чтобы продолжить, пожалуйста, аутентифицируйтесь.',
role: 'assistant',
timestamp: new Date().toISOString(),
isGuest: true,
showAuthOptions: true, // Указываем, что нужно показать кнопки аутентификации
};
// Экспорт методов для использования в родительском компоненте
defineExpose({
resetInput,
focus: () => textareaRef.value?.focus(),
});
messages.value.push(authMessage);
guestMessages.value.push(authMessage);
localStorage.setItem('guestMessages', JSON.stringify(guestMessages.value));
const sendGuestMessage = async (messageText) => {
if (!messageText.trim()) return;
// Прокрутка вниз
await nextTick();
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
}
const userMessage = {
id: Date.now(),
content: messageText,
role: 'user',
timestamp: new Date().toISOString(),
isGuest: true
isLoading.value = false;
};
// Добавляем сообщение пользователя в локальную историю
messages.value.push(userMessage);
// Сохраняем сообщение в массиве гостевых сообщений
guestMessages.value.push(userMessage);
// Сохраняем гостевые сообщения в localStorage
localStorage.setItem('guestMessages', JSON.stringify(guestMessages.value));
// Очищаем поле ввода
newMessage.value = '';
// Прокрутка вниз
await nextTick();
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
}
// Устанавливаем состояние загрузки
isLoading.value = true;
// Вместо отправки запроса к Ollama, отправляем сообщение с кнопками для аутентификации
const authMessage = {
id: Date.now() + 1,
content: 'Чтобы продолжить, пожалуйста, аутентифицируйтесь.',
role: 'assistant',
timestamp: new Date().toISOString(),
isGuest: true,
showAuthOptions: true // Указываем, что нужно показать кнопки аутентификации
};
messages.value.push(authMessage);
guestMessages.value.push(authMessage);
localStorage.setItem('guestMessages', JSON.stringify(guestMessages.value));
// Прокрутка вниз
await nextTick();
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
}
isLoading.value = false;
};
</script>
<style scoped>
.message-input {
display: flex;
padding: 1rem;
border-top: 1px solid #e0e0e0;
background-color: #fff;
}
textarea {
flex: 1;
min-height: 40px;
max-height: 120px;
padding: 0.75rem;
border: 1px solid #e0e0e0;
border-radius: 4px;
resize: none;
font-family: inherit;
font-size: 0.9rem;
line-height: 1.4;
}
textarea:focus {
outline: none;
border-color: #4caf50;
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
}
.send-button {
margin-left: 0.5rem;
padding: 0 1rem;
height: 40px;
background-color: #4caf50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: background-color 0.2s;
}
.send-button:hover:not(:disabled) {
background-color: #43a047;
}
.send-button:disabled {
background-color: #9e9e9e;
cursor: not-allowed;
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div v-if="isAuthenticated">
<div class="message-thread" ref="threadContainer">
<div ref="threadContainer" class="message-thread">
<div v-if="loading" class="loading">Загрузка сообщений...</div>
<div v-else-if="messages.length === 0" class="empty-thread">
@@ -8,7 +8,11 @@
</div>
<div v-else class="messages">
<div v-for="message in messages" :key="message.id" :class="['message', message.sender_type]">
<div
v-for="message in messages"
:key="message.id"
:class="['message', message.sender_type]"
>
<div class="message-content">{{ message.content }}</div>
<div class="message-meta">
<span class="time">{{ formatTime(message.created_at) }}</span>
@@ -26,188 +30,110 @@
</template>
<script setup>
import { ref, onMounted, watch, nextTick, defineExpose } from 'vue';
import axios from 'axios';
import { ref, onMounted, watch, nextTick, defineExpose } from 'vue';
import axios from 'axios';
const props = defineProps({
conversationId: {
type: [Number, String],
required: true,
},
});
const messages = ref([]);
const loading = ref(true);
const threadContainer = ref(null);
const isAuthenticated = ref(false);
// Загрузка сообщений диалога
const fetchMessages = async () => {
try {
loading.value = true;
const response = await axios.get(
`/api/messages/conversations/${props.conversationId}/messages`
);
messages.value = response.data;
// Прокрутка к последнему сообщению
await nextTick();
scrollToBottom();
} catch (error) {
console.error('Error fetching messages:', error);
} finally {
loading.value = false;
}
};
// Добавление новых сообщений
const addMessages = (newMessages) => {
if (Array.isArray(newMessages)) {
messages.value = [...messages.value, ...newMessages];
} else {
messages.value.push(newMessages);
}
// Прокрутка к последнему сообщению
nextTick(() => {
scrollToBottom();
const props = defineProps({
conversationId: {
type: [Number, String],
required: true,
},
});
};
// Прокрутка к последнему сообщению
const scrollToBottom = () => {
if (threadContainer.value) {
threadContainer.value.scrollTop = threadContainer.value.scrollHeight;
}
};
const messages = ref([]);
const loading = ref(true);
const threadContainer = ref(null);
const isAuthenticated = ref(false);
// Форматирование времени
const formatTime = (timestamp) => {
if (!timestamp) return '';
// Загрузка сообщений диалога
const fetchMessages = async () => {
try {
loading.value = true;
const response = await axios.get(
`/api/messages/conversations/${props.conversationId}/messages`
);
messages.value = response.data;
const date = new Date(timestamp);
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
};
// Получение названия канала
const channelName = (channel) => {
const channels = {
web: 'Веб',
telegram: 'Telegram',
email: 'Email',
// Прокрутка к последнему сообщению
await nextTick();
scrollToBottom();
} catch (error) {
console.error('Error fetching messages:', error);
} finally {
loading.value = false;
}
};
return channels[channel] || channel;
};
// Добавление новых сообщений
const addMessages = (newMessages) => {
if (Array.isArray(newMessages)) {
messages.value = [...messages.value, ...newMessages];
} else {
messages.value.push(newMessages);
}
// Наблюдение за изменением ID диалога
watch(
() => props.conversationId,
(newId, oldId) => {
if (newId && newId !== oldId) {
// Прокрутка к последнему сообщению
nextTick(() => {
scrollToBottom();
});
};
// Прокрутка к последнему сообщению
const scrollToBottom = () => {
if (threadContainer.value) {
threadContainer.value.scrollTop = threadContainer.value.scrollHeight;
}
};
// Форматирование времени
const formatTime = (timestamp) => {
if (!timestamp) return '';
const date = new Date(timestamp);
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
};
// Получение названия канала
const channelName = (channel) => {
const channels = {
web: 'Веб',
telegram: 'Telegram',
email: 'Email',
};
return channels[channel] || channel;
};
// Наблюдение за изменением ID диалога
watch(
() => props.conversationId,
(newId, oldId) => {
if (newId && newId !== oldId) {
fetchMessages();
}
}
);
// Следим за изменением статуса аутентификации
watch(
() => isAuthenticated.value,
(authenticated) => {
if (!authenticated) {
messages.value = []; // Очищаем сообщения при отключении
}
}
);
// Загрузка сообщений при монтировании компонента
onMounted(() => {
if (props.conversationId) {
fetchMessages();
}
}
);
});
// Следим за изменением статуса аутентификации
watch(() => isAuthenticated.value, (authenticated) => {
if (!authenticated) {
messages.value = []; // Очищаем сообщения при отключении
}
});
// Загрузка сообщений при монтировании компонента
onMounted(() => {
if (props.conversationId) {
fetchMessages();
}
});
// Экспорт методов для использования в родительском компоненте
defineExpose({
fetchMessages,
addMessages,
});
// Экспорт методов для использования в родительском компоненте
defineExpose({
fetchMessages,
addMessages,
});
</script>
<style scoped>
.message-thread {
flex: 1;
overflow-y: auto;
padding: 1rem;
display: flex;
flex-direction: column;
}
.loading,
.empty-thread {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: #666;
}
.messages {
display: flex;
flex-direction: column;
gap: 1rem;
}
.message {
max-width: 80%;
padding: 0.75rem 1rem;
border-radius: 8px;
position: relative;
}
.message.user {
align-self: flex-end;
background-color: #e3f2fd;
border: 1px solid #bbdefb;
}
.message.ai {
align-self: flex-start;
background-color: #f5f5f5;
border: 1px solid #e0e0e0;
}
.message.admin {
align-self: flex-start;
background-color: #fff3e0;
border: 1px dashed #ffb74d;
}
.message-content {
white-space: pre-wrap;
word-break: break-word;
}
.message-meta {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 0.25rem;
font-size: 0.7rem;
color: #999;
}
.time {
font-size: 0.7rem;
}
.channel {
font-size: 0.7rem;
padding: 0.1rem 0.3rem;
border-radius: 3px;
background-color: #f0f0f0;
}
.connect-wallet-prompt {
text-align: center;
padding: 2rem;
color: #666;
}
</style>

View File

@@ -1,173 +1,90 @@
<template>
<div class="email-connection">
<div v-if="!showVerification" class="email-form">
<input
v-model="email"
type="email"
placeholder="Введите email"
class="email-input"
/>
<button
@click="requestCode"
:disabled="isLoading || !isValidEmail"
class="email-btn"
>
<input v-model="email" type="email" placeholder="Введите email" class="email-input" />
<button :disabled="isLoading || !isValidEmail" class="email-btn" @click="requestCode">
{{ isLoading ? 'Отправка...' : 'Получить код' }}
</button>
</div>
<div v-else class="verification-form">
<p class="verification-info">Код отправлен на {{ email }}</p>
<input
v-model="code"
type="text"
placeholder="Введите код"
class="code-input"
/>
<button
@click="verifyCode"
:disabled="isLoading || !code"
class="verify-btn"
>
<input v-model="code" type="text" placeholder="Введите код" class="code-input" />
<button :disabled="isLoading || !code" class="verify-btn" @click="verifyCode">
{{ isLoading ? 'Проверка...' : 'Подтвердить' }}
</button>
<button
@click="resetForm"
class="reset-btn"
>
Изменить email
</button>
<button class="reset-btn" @click="resetForm">Изменить email</button>
</div>
<div v-if="error" class="error">{{ error }}</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import axios from '@/api/axios';
import { useAuth } from '@/composables/useAuth';
import { ref, computed } from 'vue';
import axios from '@/api/axios';
import { useAuth } from '@/composables/useAuth';
const emit = defineEmits(['close']);
const { linkIdentity } = useAuth();
const emit = defineEmits(['close']);
const { linkIdentity } = useAuth();
const email = ref('');
const code = ref('');
const error = ref('');
const isLoading = ref(false);
const showVerification = ref(false);
const email = ref('');
const code = ref('');
const error = ref('');
const isLoading = ref(false);
const showVerification = ref(false);
const isValidEmail = computed(() => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value);
});
const isValidEmail = computed(() => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value);
});
const requestCode = async () => {
try {
isLoading.value = true;
error.value = '';
const response = await axios.post('/api/auth/email/request-verification', {
email: email.value
});
if (response.data.success) {
showVerification.value = true;
} else {
error.value = response.data.error || 'Ошибка отправки кода';
const requestCode = async () => {
try {
isLoading.value = true;
error.value = '';
const response = await axios.post('/api/auth/email/request-verification', {
email: email.value,
});
if (response.data.success) {
showVerification.value = true;
} else {
error.value = response.data.error || 'Ошибка отправки кода';
}
} catch (err) {
error.value = err.response?.data?.error || 'Ошибка отправки кода';
} finally {
isLoading.value = false;
}
} catch (err) {
error.value = err.response?.data?.error || 'Ошибка отправки кода';
} finally {
isLoading.value = false;
}
};
};
const verifyCode = async () => {
try {
isLoading.value = true;
error.value = '';
const response = await axios.post('/api/auth/email/verify', {
email: email.value,
code: code.value
});
if (response.data.success) {
// Связываем email с текущим пользователем
await linkIdentity('email', email.value);
emit('close');
} else {
error.value = response.data.error || 'Неверный код';
const verifyCode = async () => {
try {
isLoading.value = true;
error.value = '';
const response = await axios.post('/api/auth/email/verify', {
email: email.value,
code: code.value,
});
if (response.data.success) {
// Связываем email с текущим пользователем
await linkIdentity('email', email.value);
emit('close');
} else {
error.value = response.data.error || 'Неверный код';
}
} catch (err) {
error.value = err.response?.data?.error || 'Ошибка проверки кода';
} finally {
isLoading.value = false;
}
} catch (err) {
error.value = err.response?.data?.error || 'Ошибка проверки кода';
} finally {
isLoading.value = false;
}
};
};
const resetForm = () => {
email.value = '';
code.value = '';
error.value = '';
showVerification.value = false;
};
const resetForm = () => {
email.value = '';
code.value = '';
error.value = '';
showVerification.value = false;
};
</script>
<style scoped>
.email-connection {
padding: 20px;
max-width: 400px;
}
.email-form,
.verification-form {
display: flex;
flex-direction: column;
gap: 10px;
}
.email-input,
.code-input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.email-btn,
.verify-btn,
.reset-btn {
padding: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.email-btn,
.verify-btn {
background-color: #48bb78;
color: white;
}
.reset-btn {
background-color: #e2e8f0;
color: #4a5568;
}
.verification-info {
color: #4a5568;
font-size: 14px;
}
.error {
color: #e53e3e;
margin-top: 5px;
font-size: 14px;
}
button:disabled {
opacity: 0.7;
cursor: not-allowed;
}
</style>

View File

@@ -2,7 +2,7 @@
<div class="telegram-connect">
<div v-if="!showQR" class="intro">
<p>Подключите свой аккаунт Telegram для быстрой авторизации</p>
<button @click="startConnection" class="connect-button" :disabled="loading">
<button class="connect-button" :disabled="loading" @click="startConnection">
<span class="telegram-icon">📱</span>
{{ loading ? 'Загрузка...' : 'Подключить Telegram' }}
</button>
@@ -10,14 +10,11 @@
<div v-else class="qr-section">
<p>Отсканируйте QR-код в приложении Telegram</p>
<div class="qr-container" v-html="qrCode"></div>
<!-- eslint-disable-next-line vue/no-v-html -->
<div class="qr-container" v-html="qrCode" />
<p class="or-divider">или</p>
<a :href="botLink" target="_blank" class="bot-link">
Открыть бота в Telegram
</a>
<button @click="resetConnection" class="reset-button">
Отмена
</button>
<a :href="botLink" target="_blank" class="bot-link"> Открыть бота в Telegram </a>
<button class="reset-button" @click="resetConnection">Отмена</button>
</div>
<div v-if="error" class="error">{{ error }}</div>
@@ -25,183 +22,87 @@
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import axios from '@/api/axios';
import { useAuth } from '@/composables/useAuth';
import QRCode from 'qrcode';
import { ref, onMounted, onUnmounted } from 'vue';
import axios from '@/api/axios';
import { useAuth } from '@/composables/useAuth';
import QRCode from 'qrcode';
const emit = defineEmits(['close']);
const { linkIdentity } = useAuth();
const emit = defineEmits(['close']);
const { linkIdentity } = useAuth();
const loading = ref(false);
const error = ref('');
const showQR = ref(false);
const qrCode = ref('');
const botLink = ref('');
const pollInterval = ref(null);
const connectionToken = ref('');
const loading = ref(false);
const error = ref('');
const showQR = ref(false);
const qrCode = ref('');
const botLink = ref('');
const pollInterval = ref(null);
const connectionToken = ref('');
const startConnection = async () => {
try {
loading.value = true;
const startConnection = async () => {
try {
loading.value = true;
error.value = '';
const response = await axios.post('/api/auth/telegram/start-connection');
if (response.data.success) {
connectionToken.value = response.data.token;
botLink.value = `https://t.me/${response.data.botUsername}?start=${connectionToken.value}`;
// Генерируем QR-код
const qr = await QRCode.toDataURL(botLink.value);
qrCode.value = `<img src="${qr}" alt="Telegram QR Code" />`;
showQR.value = true;
startPolling();
} else {
error.value = response.data.error || 'Не удалось начать процесс подключения';
}
} catch (err) {
error.value = err.response?.data?.error || 'Ошибка при подключении Telegram';
} finally {
loading.value = false;
}
};
const checkConnection = async () => {
try {
const response = await axios.post('/api/auth/telegram/check-connection', {
token: connectionToken.value,
});
if (response.data.success && response.data.telegramId) {
// Связываем Telegram с текущим пользователем
await linkIdentity('telegram', response.data.telegramId);
stopPolling();
emit('close');
}
} catch (error) {
console.error('Error checking connection:', error);
}
};
const startPolling = () => {
pollInterval.value = setInterval(checkConnection, 2000);
};
const stopPolling = () => {
if (pollInterval.value) {
clearInterval(pollInterval.value);
pollInterval.value = null;
}
};
const resetConnection = () => {
stopPolling();
showQR.value = false;
error.value = '';
const response = await axios.post('/api/auth/telegram/start-connection');
if (response.data.success) {
connectionToken.value = response.data.token;
botLink.value = `https://t.me/${response.data.botUsername}?start=${connectionToken.value}`;
// Генерируем QR-код
const qr = await QRCode.toDataURL(botLink.value);
qrCode.value = `<img src="${qr}" alt="Telegram QR Code" />`;
showQR.value = true;
startPolling();
} else {
error.value = response.data.error || 'Не удалось начать процесс подключения';
}
} catch (err) {
error.value = err.response?.data?.error || 'Ошибка при подключении Telegram';
} finally {
loading.value = false;
}
};
qrCode.value = '';
botLink.value = '';
connectionToken.value = '';
};
const checkConnection = async () => {
try {
const response = await axios.post('/api/auth/telegram/check-connection', {
token: connectionToken.value
});
if (response.data.success && response.data.telegramId) {
// Связываем Telegram с текущим пользователем
await linkIdentity('telegram', response.data.telegramId);
stopPolling();
emit('close');
}
} catch (error) {
console.error('Error checking connection:', error);
}
};
const startPolling = () => {
pollInterval.value = setInterval(checkConnection, 2000);
};
const stopPolling = () => {
if (pollInterval.value) {
clearInterval(pollInterval.value);
pollInterval.value = null;
}
};
const resetConnection = () => {
stopPolling();
showQR.value = false;
error.value = '';
qrCode.value = '';
botLink.value = '';
connectionToken.value = '';
};
onUnmounted(() => {
stopPolling();
});
onUnmounted(() => {
stopPolling();
});
</script>
<style scoped>
.telegram-connect {
padding: 20px;
max-width: 400px;
}
.intro,
.qr-section {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
text-align: center;
}
.connect-button {
display: flex;
align-items: center;
justify-content: center;
padding: 10px 20px;
background-color: #0088cc;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.2s;
}
.connect-button:hover:not(:disabled) {
background-color: #0077b5;
}
.telegram-icon {
margin-right: 10px;
font-size: 18px;
}
.qr-container {
background: white;
padding: 10px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.qr-container img {
max-width: 200px;
height: auto;
}
.or-divider {
color: #666;
margin: 10px 0;
}
.bot-link {
color: #0088cc;
text-decoration: none;
padding: 8px 16px;
border: 1px solid #0088cc;
border-radius: 4px;
transition: all 0.2s;
}
.bot-link:hover {
background-color: #0088cc;
color: white;
}
.reset-button {
padding: 8px 16px;
background-color: #e2e8f0;
color: #4a5568;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.reset-button:hover {
background-color: #cbd5e0;
}
.error {
color: #e53e3e;
margin-top: 10px;
text-align: center;
}
button:disabled {
opacity: 0.7;
cursor: not-allowed;
}
</style>

View File

@@ -2,11 +2,7 @@
<div class="wallet-connection">
<div v-if="!isConnected" class="connect-section">
<p>Подключите свой кошелек для доступа к расширенным функциям</p>
<button
@click="connectWallet"
:disabled="isLoading"
class="wallet-btn"
>
<button :disabled="isLoading" class="wallet-btn" @click="connectWallet">
<span class="wallet-icon">💳</span>
{{ isLoading ? 'Подключение...' : 'Подключить кошелек' }}
</button>
@@ -20,106 +16,48 @@
</template>
<script setup>
import { ref, computed } from 'vue';
import { useAuth } from '@/composables/useAuth';
import { connectWithWallet } from '@/services/wallet';
import { ref, computed } from 'vue';
import { useAuth } from '@/composables/useAuth';
import { connectWithWallet } from '@/services/wallet';
const emit = defineEmits(['close']);
const { linkIdentity } = useAuth();
const emit = defineEmits(['close']);
const { linkIdentity } = useAuth();
const isLoading = ref(false);
const error = ref('');
const address = ref('');
const isLoading = ref(false);
const error = ref('');
const address = ref('');
const isConnected = computed(() => !!address.value);
const isConnected = computed(() => !!address.value);
const formatAddress = (addr) => {
if (!addr) return '';
return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
};
const formatAddress = (addr) => {
if (!addr) return '';
return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
};
const connectWallet = async () => {
if (isLoading.value) return;
try {
isLoading.value = true;
error.value = '';
// Подключаем кошелек
const result = await connectWithWallet();
if (result.success) {
address.value = result.address;
// Связываем кошелек с текущим пользователем
await linkIdentity('wallet', result.address);
emit('close');
} else {
error.value = result.error || 'Не удалось подключить кошелек';
const connectWallet = async () => {
if (isLoading.value) return;
try {
isLoading.value = true;
error.value = '';
// Подключаем кошелек
const result = await connectWithWallet();
if (result.success) {
address.value = result.address;
// Связываем кошелек с текущим пользователем
await linkIdentity('wallet', result.address);
emit('close');
} else {
error.value = result.error || 'Не удалось подключить кошелек';
}
} catch (err) {
console.error('Error connecting wallet:', err);
error.value = err.message || 'Произошла ошибка при подключении кошелька';
} finally {
isLoading.value = false;
}
} catch (err) {
console.error('Error connecting wallet:', err);
error.value = err.message || 'Произошла ошибка при подключении кошелька';
} finally {
isLoading.value = false;
}
};
};
</script>
<style scoped>
.wallet-connection {
padding: 20px;
max-width: 400px;
}
.connect-section,
.status-section {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
text-align: center;
}
.wallet-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 10px 20px;
background-color: #4a5568;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: all 0.2s;
}
.wallet-btn:hover:not(:disabled) {
background-color: #2d3748;
}
.wallet-icon {
margin-right: 10px;
font-size: 18px;
}
.address {
font-family: monospace;
background-color: #f7fafc;
padding: 8px 12px;
border-radius: 4px;
font-size: 14px;
}
.error {
color: #e53e3e;
margin-top: 10px;
text-align: center;
}
button:disabled {
opacity: 0.7;
cursor: not-allowed;
}
</style>

View File

@@ -2,8 +2,4 @@ import TelegramConnect from './TelegramConnect.vue';
import WalletConnection from './WalletConnection.vue';
import EmailConnect from './EmailConnect.vue';
export {
TelegramConnect,
WalletConnection,
EmailConnect
};
export { TelegramConnect, WalletConnection, EmailConnect };