ваше сообщение коммита
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
import globals from 'globals';
|
import globals from 'globals';
|
||||||
|
import * as vueParser from 'vue-eslint-parser';
|
||||||
import vuePlugin from 'eslint-plugin-vue';
|
import vuePlugin from 'eslint-plugin-vue';
|
||||||
import prettierPlugin from 'eslint-plugin-prettier';
|
import prettierPlugin from 'eslint-plugin-prettier';
|
||||||
import prettierConfig from '@vue/eslint-config-prettier';
|
import eslintConfigPrettier from 'eslint-config-prettier';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
@@ -35,29 +36,37 @@ export default [
|
|||||||
...globals.browser,
|
...globals.browser,
|
||||||
...globals.es2021,
|
...globals.es2021,
|
||||||
},
|
},
|
||||||
parser: vuePlugin.parser,
|
parser: vueParser,
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaFeatures: {
|
sourceType: 'module',
|
||||||
jsx: true,
|
ecmaVersion: 2022,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
vue: vuePlugin,
|
vue: vuePlugin,
|
||||||
prettier: prettierPlugin,
|
prettier: prettierPlugin,
|
||||||
},
|
},
|
||||||
processor: vuePlugin.processors['.vue'],
|
|
||||||
rules: {
|
rules: {
|
||||||
...prettierConfig.rules,
|
...vuePlugin.configs.base.rules,
|
||||||
|
...vuePlugin.configs['vue3-essential'].rules,
|
||||||
|
...vuePlugin.configs['vue3-strongly-recommended'].rules,
|
||||||
|
...vuePlugin.configs['vue3-recommended'].rules,
|
||||||
|
...eslintConfigPrettier.rules,
|
||||||
|
'prettier/prettier': 'warn',
|
||||||
|
'vue/comment-directive': 'off',
|
||||||
'vue/multi-word-component-names': 'off',
|
'vue/multi-word-component-names': 'off',
|
||||||
'vue/no-unused-vars': 'warn',
|
'vue/no-unused-vars': 'warn',
|
||||||
'vue/html-self-closing': ['warn', {
|
'vue/no-v-html': 'off',
|
||||||
html: {
|
'vue/html-self-closing': [
|
||||||
void: 'always',
|
'warn',
|
||||||
normal: 'always',
|
{
|
||||||
component: 'always'
|
html: {
|
||||||
}
|
void: 'always',
|
||||||
}],
|
normal: 'always',
|
||||||
|
component: 'always',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
'vue/component-name-in-template-casing': ['warn', 'PascalCase'],
|
'vue/component-name-in-template-casing': ['warn', 'PascalCase'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,137 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<router-view />
|
<div v-if="isLoading" class="loading-overlay">
|
||||||
|
<div class="loading-spinner" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RouterView />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, ref, provide, computed } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { RouterView } from 'vue-router';
|
||||||
import axios from 'axios';
|
import { useAuth } from './composables/useAuth';
|
||||||
|
import './assets/styles/home.css';
|
||||||
|
|
||||||
console.log('App.vue: Version with auth check loaded');
|
// Состояние загрузки
|
||||||
|
const isLoading = ref(false);
|
||||||
|
|
||||||
const router = useRouter();
|
// Использование composable для аутентификации
|
||||||
|
const { isAuthenticated } = useAuth();
|
||||||
|
|
||||||
// Создаем реактивное состояние с помощью ref
|
watch(isAuthenticated, (newValue, oldValue) => {
|
||||||
const authState = ref({
|
if (newValue !== oldValue) {
|
||||||
isAuthenticated: false,
|
console.log('Состояние аутентификации изменилось:', newValue);
|
||||||
userRole: null,
|
|
||||||
address: null
|
|
||||||
});
|
|
||||||
|
|
||||||
// Предоставляем состояние аутентификации всем компонентам
|
|
||||||
const auth = {
|
|
||||||
// Используем computed для реактивности
|
|
||||||
isAuthenticated: computed(() => authState.value.isAuthenticated),
|
|
||||||
userRole: computed(() => authState.value.userRole),
|
|
||||||
address: computed(() => authState.value.address),
|
|
||||||
async checkAuth() {
|
|
||||||
try {
|
|
||||||
const response = await axios.get('/api/auth/check');
|
|
||||||
console.log('Auth check response:', response.data);
|
|
||||||
authState.value = {
|
|
||||||
isAuthenticated: response.data.authenticated,
|
|
||||||
userRole: response.data.role,
|
|
||||||
address: response.data.address
|
|
||||||
};
|
|
||||||
console.log('Auth state updated:', authState.value);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Auth check failed:', error);
|
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
async disconnect() {
|
|
||||||
try {
|
|
||||||
await axios.post('/api/auth/logout');
|
|
||||||
authState.value = {
|
|
||||||
isAuthenticated: false,
|
|
||||||
userRole: null,
|
|
||||||
address: null
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Logout failed:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
provide('auth', auth);
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await auth.checkAuth();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: 'Roboto', 'Helvetica Neue', Arial, sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
color: #333;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
flex: 1;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-weight: 500;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background-color: #3498db;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background-color: #95a5a6;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger {
|
|
||||||
background-color: #e74c3c;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: rgba(255, 255, 255, 0.8);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
z-index: 9999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner {
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
border: 5px solid #f3f3f3;
|
|
||||||
border-top: 5px solid #3498db;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% { transform: rotate(0deg); }
|
|
||||||
100% { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ const api = axios.create({
|
|||||||
baseURL: getBaseUrl(),
|
baseURL: getBaseUrl(),
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Перехватчик запросов
|
// Перехватчик запросов
|
||||||
@@ -50,7 +50,7 @@ const sendGuestMessageToServer = async (messageText) => {
|
|||||||
try {
|
try {
|
||||||
await axios.post('/api/chat/guest-message', {
|
await axios.post('/api/chat/guest-message', {
|
||||||
message: messageText,
|
message: messageText,
|
||||||
language: userLanguage.value
|
// language: userLanguage.value, // TODO: Реализовать получение языка пользователя
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при отправке гостевого сообщения на сервер:', error);
|
console.error('Ошибка при отправке гостевого сообщения на сервер:', error);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@
|
|||||||
<div class="conversation-list">
|
<div class="conversation-list">
|
||||||
<div class="list-header">
|
<div class="list-header">
|
||||||
<h3>Диалоги</h3>
|
<h3>Диалоги</h3>
|
||||||
<button @click="createNewConversation" class="new-conversation-btn">
|
<button class="new-conversation-btn" @click="createNewConversation">
|
||||||
<span>+</span> Новый диалог
|
<span>+</span> Новый диалог
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,205 +40,110 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed, defineEmits, watch, inject } from 'vue';
|
import { ref, onMounted, computed, defineEmits, watch, inject } from 'vue';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
const emit = defineEmits(['select-conversation']);
|
const emit = defineEmits(['select-conversation']);
|
||||||
const auth = inject('auth');
|
const auth = inject('auth');
|
||||||
const isAuthenticated = computed(() => auth.isAuthenticated.value);
|
const isAuthenticated = computed(() => auth.isAuthenticated.value);
|
||||||
|
|
||||||
const conversations = ref([]);
|
const conversations = ref([]);
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const selectedConversationId = ref(null);
|
const selectedConversationId = ref(null);
|
||||||
|
|
||||||
// Следим за изменением статуса аутентификации
|
// Следим за изменением статуса аутентификации
|
||||||
watch(() => isAuthenticated.value, (authenticated) => {
|
watch(
|
||||||
if (!authenticated) {
|
() => isAuthenticated.value,
|
||||||
conversations.value = []; // Очищаем список бесед при отключении
|
(authenticated) => {
|
||||||
selectedConversationId.value = null;
|
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);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
);
|
||||||
console.error('Error fetching conversations:', error);
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Выбор диалога
|
// Загрузка списка диалогов
|
||||||
const selectConversation = (conversationId) => {
|
const fetchConversations = async () => {
|
||||||
selectedConversationId.value = conversationId;
|
try {
|
||||||
emit('select-conversation', conversationId);
|
loading.value = true;
|
||||||
};
|
const response = await axios.get('/api/messages/conversations');
|
||||||
|
conversations.value = response.data;
|
||||||
|
|
||||||
// Создание нового диалога
|
// Если есть диалоги и не выбран ни один, выбираем первый
|
||||||
const createNewConversation = async () => {
|
if (conversations.value.length > 0 && !selectedConversationId.value) {
|
||||||
try {
|
selectConversation(conversations.value[0].conversation_id);
|
||||||
const response = await axios.post('/api/messages/conversations', {
|
}
|
||||||
title: 'Новый диалог',
|
} catch (error) {
|
||||||
});
|
console.error('Error fetching conversations:', error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Добавляем новый диалог в список
|
// Выбор диалога
|
||||||
const newConversation = {
|
const selectConversation = (conversationId) => {
|
||||||
conversation_id: response.data.id,
|
selectedConversationId.value = conversationId;
|
||||||
title: response.data.title,
|
emit('select-conversation', conversationId);
|
||||||
username: authStore.username,
|
};
|
||||||
address: authStore.address,
|
|
||||||
message_count: 0,
|
|
||||||
last_activity: response.data.created_at,
|
|
||||||
created_at: response.data.created_at,
|
|
||||||
};
|
|
||||||
|
|
||||||
conversations.value.unshift(newConversation);
|
// Создание нового диалога
|
||||||
|
const createNewConversation = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/api/messages/conversations', {
|
||||||
|
title: 'Новый диалог',
|
||||||
|
});
|
||||||
|
|
||||||
// Выбираем новый диалог
|
// Добавляем новый диалог в список
|
||||||
selectConversation(newConversation.conversation_id);
|
const newConversation = {
|
||||||
} catch (error) {
|
conversation_id: response.data.id,
|
||||||
console.error('Error creating conversation:', error);
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
// Форматирование времени
|
conversations.value.unshift(newConversation);
|
||||||
const formatTime = (timestamp) => {
|
|
||||||
if (!timestamp) return '';
|
|
||||||
|
|
||||||
const date = new Date(timestamp);
|
// Выбираем новый диалог
|
||||||
const now = new Date();
|
selectConversation(newConversation.conversation_id);
|
||||||
const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));
|
} catch (error) {
|
||||||
|
console.error('Error creating conversation:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (diffDays === 0) {
|
// Форматирование времени
|
||||||
// Сегодня - показываем только время
|
const formatTime = (timestamp) => {
|
||||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
if (!timestamp) return '';
|
||||||
} else if (diffDays === 1) {
|
|
||||||
// Вчера
|
|
||||||
return 'Вчера';
|
|
||||||
} else if (diffDays < 7) {
|
|
||||||
// В течение недели - показываем день недели
|
|
||||||
const days = ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'];
|
|
||||||
return days[date.getDay()];
|
|
||||||
} else {
|
|
||||||
// Более недели назад - показываем дату
|
|
||||||
return date.toLocaleDateString();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Загрузка диалогов при монтировании компонента
|
const date = new Date(timestamp);
|
||||||
onMounted(() => {
|
const now = new Date();
|
||||||
fetchConversations();
|
const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));
|
||||||
});
|
|
||||||
|
|
||||||
// Экспорт методов для использования в родительском компоненте
|
if (diffDays === 0) {
|
||||||
defineExpose({
|
// Сегодня - показываем только время
|
||||||
fetchConversations,
|
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>
|
</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>
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="message-input">
|
<div class="message-input">
|
||||||
<textarea
|
<textarea
|
||||||
|
ref="textareaRef"
|
||||||
v-model="message"
|
v-model="message"
|
||||||
placeholder="Введите сообщение..."
|
placeholder="Введите сообщение..."
|
||||||
@keydown.enter.prevent="handleEnter"
|
|
||||||
ref="textareaRef"
|
|
||||||
:disabled="sending"
|
: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-if="sending">Отправка...</span>
|
||||||
<span v-else>Отправить</span>
|
<span v-else>Отправить</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -16,199 +16,149 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, defineEmits, nextTick } from 'vue';
|
import { ref, defineEmits, nextTick } from 'vue';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
conversationId: {
|
conversationId: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['message-sent']);
|
const emit = defineEmits(['message-sent']);
|
||||||
const message = ref('');
|
const message = ref('');
|
||||||
const sending = ref(false);
|
const sending = ref(false);
|
||||||
const textareaRef = ref(null);
|
const textareaRef = ref(null);
|
||||||
|
|
||||||
// Обработка нажатия Enter
|
// Обработка нажатия Enter
|
||||||
const handleEnter = (event) => {
|
const handleEnter = (event) => {
|
||||||
// Если нажат Shift+Enter, добавляем перенос строки
|
// Если нажат Shift+Enter, добавляем перенос строки
|
||||||
if (event.shiftKey) {
|
if (event.shiftKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Иначе отправляем сообщение
|
// Иначе отправляем сообщение
|
||||||
sendMessage();
|
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()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
messages.value.push(userMessage);
|
// Отправка сообщения
|
||||||
|
const sendMessage = async () => {
|
||||||
|
const messageText = message.value.trim();
|
||||||
|
if (!messageText) return;
|
||||||
|
|
||||||
try {
|
const userMessage = {
|
||||||
// Логируем параметры запроса
|
id: Date.now(),
|
||||||
console.log('Sending message to Ollama:', {
|
content: messageText,
|
||||||
message: messageText,
|
role: auth.isAuthenticated ? 'user' : 'guest',
|
||||||
language: userLanguage.value
|
timestamp: new Date().toISOString(),
|
||||||
});
|
};
|
||||||
|
|
||||||
const response = await axios.post('/api/chat/message', {
|
messages.value.push(userMessage);
|
||||||
message: messageText,
|
|
||||||
language: userLanguage.value
|
|
||||||
});
|
|
||||||
|
|
||||||
// Логируем ответ от Ollama
|
try {
|
||||||
console.log('Response from Ollama:', response.data);
|
// Логируем параметры запроса
|
||||||
|
console.log('Sending message to Ollama:', {
|
||||||
|
message: messageText,
|
||||||
|
language: userLanguage.value,
|
||||||
|
});
|
||||||
|
|
||||||
// Обработка ответа
|
const response = await axios.post('/api/chat/message', {
|
||||||
messages.value.push({
|
message: messageText,
|
||||||
id: Date.now() + 1,
|
language: userLanguage.value,
|
||||||
content: response.data.message,
|
});
|
||||||
role: 'assistant',
|
|
||||||
timestamp: new Date().toISOString()
|
// Логируем ответ от 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(() => {
|
await nextTick();
|
||||||
textareaRef.value.focus();
|
if (messagesContainer.value) {
|
||||||
});
|
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
// Уведомляем родительский компонент о новых сообщениях
|
// Устанавливаем состояние загрузки
|
||||||
emit('message-sent', [response.data.userMessage, response.data.aiMessage]);
|
isLoading.value = true;
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при отправке сообщения:', error);
|
|
||||||
} finally {
|
|
||||||
sending.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Сброс поля ввода
|
// Вместо отправки запроса к Ollama, отправляем сообщение с кнопками для аутентификации
|
||||||
const resetInput = () => {
|
const authMessage = {
|
||||||
message.value = '';
|
id: Date.now() + 1,
|
||||||
};
|
content: 'Чтобы продолжить, пожалуйста, аутентифицируйтесь.',
|
||||||
|
role: 'assistant',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
isGuest: true,
|
||||||
|
showAuthOptions: true, // Указываем, что нужно показать кнопки аутентификации
|
||||||
|
};
|
||||||
|
|
||||||
// Экспорт методов для использования в родительском компоненте
|
messages.value.push(authMessage);
|
||||||
defineExpose({
|
guestMessages.value.push(authMessage);
|
||||||
resetInput,
|
localStorage.setItem('guestMessages', JSON.stringify(guestMessages.value));
|
||||||
focus: () => textareaRef.value?.focus(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const sendGuestMessage = async (messageText) => {
|
// Прокрутка вниз
|
||||||
if (!messageText.trim()) return;
|
await nextTick();
|
||||||
|
if (messagesContainer.value) {
|
||||||
|
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
const userMessage = {
|
isLoading.value = false;
|
||||||
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));
|
|
||||||
|
|
||||||
// Очищаем поле ввода
|
|
||||||
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>
|
</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>
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="isAuthenticated">
|
<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-if="loading" class="loading">Загрузка сообщений...</div>
|
||||||
|
|
||||||
<div v-else-if="messages.length === 0" class="empty-thread">
|
<div v-else-if="messages.length === 0" class="empty-thread">
|
||||||
@@ -8,7 +8,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="messages">
|
<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-content">{{ message.content }}</div>
|
||||||
<div class="message-meta">
|
<div class="message-meta">
|
||||||
<span class="time">{{ formatTime(message.created_at) }}</span>
|
<span class="time">{{ formatTime(message.created_at) }}</span>
|
||||||
@@ -26,188 +30,110 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch, nextTick, defineExpose } from 'vue';
|
import { ref, onMounted, watch, nextTick, defineExpose } from 'vue';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
conversationId: {
|
conversationId: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
required: true,
|
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 messages = ref([]);
|
||||||
const scrollToBottom = () => {
|
const loading = ref(true);
|
||||||
if (threadContainer.value) {
|
const threadContainer = ref(null);
|
||||||
threadContainer.value.scrollTop = threadContainer.value.scrollHeight;
|
const isAuthenticated = ref(false);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Форматирование времени
|
// Загрузка сообщений диалога
|
||||||
const formatTime = (timestamp) => {
|
const fetchMessages = async () => {
|
||||||
if (!timestamp) return '';
|
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' });
|
await nextTick();
|
||||||
};
|
scrollToBottom();
|
||||||
|
} catch (error) {
|
||||||
// Получение названия канала
|
console.error('Error fetching messages:', error);
|
||||||
const channelName = (channel) => {
|
} finally {
|
||||||
const channels = {
|
loading.value = false;
|
||||||
web: 'Веб',
|
}
|
||||||
telegram: 'Telegram',
|
|
||||||
email: 'Email',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return channels[channel] || channel;
|
// Добавление новых сообщений
|
||||||
};
|
const addMessages = (newMessages) => {
|
||||||
|
if (Array.isArray(newMessages)) {
|
||||||
|
messages.value = [...messages.value, ...newMessages];
|
||||||
|
} else {
|
||||||
|
messages.value.push(newMessages);
|
||||||
|
}
|
||||||
|
|
||||||
// Наблюдение за изменением ID диалога
|
// Прокрутка к последнему сообщению
|
||||||
watch(
|
nextTick(() => {
|
||||||
() => props.conversationId,
|
scrollToBottom();
|
||||||
(newId, oldId) => {
|
});
|
||||||
if (newId && newId !== oldId) {
|
};
|
||||||
|
|
||||||
|
// Прокрутка к последнему сообщению
|
||||||
|
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();
|
fetchMessages();
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
// Следим за изменением статуса аутентификации
|
// Экспорт методов для использования в родительском компоненте
|
||||||
watch(() => isAuthenticated.value, (authenticated) => {
|
defineExpose({
|
||||||
if (!authenticated) {
|
fetchMessages,
|
||||||
messages.value = []; // Очищаем сообщения при отключении
|
addMessages,
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Загрузка сообщений при монтировании компонента
|
|
||||||
onMounted(() => {
|
|
||||||
if (props.conversationId) {
|
|
||||||
fetchMessages();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Экспорт методов для использования в родительском компоненте
|
|
||||||
defineExpose({
|
|
||||||
fetchMessages,
|
|
||||||
addMessages,
|
|
||||||
});
|
|
||||||
</script>
|
</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>
|
|
||||||
|
|||||||
@@ -1,173 +1,90 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="email-connection">
|
<div class="email-connection">
|
||||||
<div v-if="!showVerification" class="email-form">
|
<div v-if="!showVerification" class="email-form">
|
||||||
<input
|
<input v-model="email" type="email" placeholder="Введите email" class="email-input" />
|
||||||
v-model="email"
|
<button :disabled="isLoading || !isValidEmail" class="email-btn" @click="requestCode">
|
||||||
type="email"
|
|
||||||
placeholder="Введите email"
|
|
||||||
class="email-input"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
@click="requestCode"
|
|
||||||
:disabled="isLoading || !isValidEmail"
|
|
||||||
class="email-btn"
|
|
||||||
>
|
|
||||||
{{ isLoading ? 'Отправка...' : 'Получить код' }}
|
{{ isLoading ? 'Отправка...' : 'Получить код' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="verification-form">
|
<div v-else class="verification-form">
|
||||||
<p class="verification-info">Код отправлен на {{ email }}</p>
|
<p class="verification-info">Код отправлен на {{ email }}</p>
|
||||||
<input
|
<input v-model="code" type="text" placeholder="Введите код" class="code-input" />
|
||||||
v-model="code"
|
<button :disabled="isLoading || !code" class="verify-btn" @click="verifyCode">
|
||||||
type="text"
|
|
||||||
placeholder="Введите код"
|
|
||||||
class="code-input"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
@click="verifyCode"
|
|
||||||
:disabled="isLoading || !code"
|
|
||||||
class="verify-btn"
|
|
||||||
>
|
|
||||||
{{ isLoading ? 'Проверка...' : 'Подтвердить' }}
|
{{ isLoading ? 'Проверка...' : 'Подтвердить' }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button class="reset-btn" @click="resetForm">Изменить email</button>
|
||||||
@click="resetForm"
|
|
||||||
class="reset-btn"
|
|
||||||
>
|
|
||||||
Изменить email
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="error" class="error">{{ error }}</div>
|
<div v-if="error" class="error">{{ error }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import axios from '@/api/axios';
|
import axios from '@/api/axios';
|
||||||
import { useAuth } from '@/composables/useAuth';
|
import { useAuth } from '@/composables/useAuth';
|
||||||
|
|
||||||
const emit = defineEmits(['close']);
|
const emit = defineEmits(['close']);
|
||||||
const { linkIdentity } = useAuth();
|
const { linkIdentity } = useAuth();
|
||||||
|
|
||||||
const email = ref('');
|
const email = ref('');
|
||||||
const code = ref('');
|
const code = ref('');
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
const showVerification = ref(false);
|
const showVerification = ref(false);
|
||||||
|
|
||||||
const isValidEmail = computed(() => {
|
const isValidEmail = computed(() => {
|
||||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value);
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const requestCode = async () => {
|
const requestCode = async () => {
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
|
|
||||||
const response = await axios.post('/api/auth/email/request-verification', {
|
const response = await axios.post('/api/auth/email/request-verification', {
|
||||||
email: email.value
|
email: email.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
showVerification.value = true;
|
showVerification.value = true;
|
||||||
} else {
|
} else {
|
||||||
error.value = response.data.error || 'Ошибка отправки кода';
|
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 () => {
|
const verifyCode = async () => {
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
|
|
||||||
const response = await axios.post('/api/auth/email/verify', {
|
const response = await axios.post('/api/auth/email/verify', {
|
||||||
email: email.value,
|
email: email.value,
|
||||||
code: code.value
|
code: code.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
// Связываем email с текущим пользователем
|
// Связываем email с текущим пользователем
|
||||||
await linkIdentity('email', email.value);
|
await linkIdentity('email', email.value);
|
||||||
emit('close');
|
emit('close');
|
||||||
} else {
|
} else {
|
||||||
error.value = response.data.error || 'Неверный код';
|
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 = () => {
|
const resetForm = () => {
|
||||||
email.value = '';
|
email.value = '';
|
||||||
code.value = '';
|
code.value = '';
|
||||||
error.value = '';
|
error.value = '';
|
||||||
showVerification.value = false;
|
showVerification.value = false;
|
||||||
};
|
};
|
||||||
</script>
|
</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>
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="telegram-connect">
|
<div class="telegram-connect">
|
||||||
<div v-if="!showQR" class="intro">
|
<div v-if="!showQR" class="intro">
|
||||||
<p>Подключите свой аккаунт Telegram для быстрой авторизации</p>
|
<p>Подключите свой аккаунт Telegram для быстрой авторизации</p>
|
||||||
<button @click="startConnection" class="connect-button" :disabled="loading">
|
<button class="connect-button" :disabled="loading" @click="startConnection">
|
||||||
<span class="telegram-icon">📱</span>
|
<span class="telegram-icon">📱</span>
|
||||||
{{ loading ? 'Загрузка...' : 'Подключить Telegram' }}
|
{{ loading ? 'Загрузка...' : 'Подключить Telegram' }}
|
||||||
</button>
|
</button>
|
||||||
@@ -10,14 +10,11 @@
|
|||||||
|
|
||||||
<div v-else class="qr-section">
|
<div v-else class="qr-section">
|
||||||
<p>Отсканируйте QR-код в приложении Telegram</p>
|
<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>
|
<p class="or-divider">или</p>
|
||||||
<a :href="botLink" target="_blank" class="bot-link">
|
<a :href="botLink" target="_blank" class="bot-link"> Открыть бота в Telegram </a>
|
||||||
Открыть бота в Telegram
|
<button class="reset-button" @click="resetConnection">Отмена</button>
|
||||||
</a>
|
|
||||||
<button @click="resetConnection" class="reset-button">
|
|
||||||
Отмена
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="error" class="error">{{ error }}</div>
|
<div v-if="error" class="error">{{ error }}</div>
|
||||||
@@ -25,183 +22,87 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from 'vue';
|
import { ref, onMounted, onUnmounted } from 'vue';
|
||||||
import axios from '@/api/axios';
|
import axios from '@/api/axios';
|
||||||
import { useAuth } from '@/composables/useAuth';
|
import { useAuth } from '@/composables/useAuth';
|
||||||
import QRCode from 'qrcode';
|
import QRCode from 'qrcode';
|
||||||
|
|
||||||
const emit = defineEmits(['close']);
|
const emit = defineEmits(['close']);
|
||||||
const { linkIdentity } = useAuth();
|
const { linkIdentity } = useAuth();
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
const showQR = ref(false);
|
const showQR = ref(false);
|
||||||
const qrCode = ref('');
|
const qrCode = ref('');
|
||||||
const botLink = ref('');
|
const botLink = ref('');
|
||||||
const pollInterval = ref(null);
|
const pollInterval = ref(null);
|
||||||
const connectionToken = ref('');
|
const connectionToken = ref('');
|
||||||
|
|
||||||
const startConnection = async () => {
|
const startConnection = async () => {
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
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 = '';
|
error.value = '';
|
||||||
|
qrCode.value = '';
|
||||||
|
botLink.value = '';
|
||||||
|
connectionToken.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
const response = await axios.post('/api/auth/telegram/start-connection');
|
onUnmounted(() => {
|
||||||
|
stopPolling();
|
||||||
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 = '';
|
|
||||||
qrCode.value = '';
|
|
||||||
botLink.value = '';
|
|
||||||
connectionToken.value = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
stopPolling();
|
|
||||||
});
|
|
||||||
</script>
|
</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>
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,7 @@
|
|||||||
<div class="wallet-connection">
|
<div class="wallet-connection">
|
||||||
<div v-if="!isConnected" class="connect-section">
|
<div v-if="!isConnected" class="connect-section">
|
||||||
<p>Подключите свой кошелек для доступа к расширенным функциям</p>
|
<p>Подключите свой кошелек для доступа к расширенным функциям</p>
|
||||||
<button
|
<button :disabled="isLoading" class="wallet-btn" @click="connectWallet">
|
||||||
@click="connectWallet"
|
|
||||||
:disabled="isLoading"
|
|
||||||
class="wallet-btn"
|
|
||||||
>
|
|
||||||
<span class="wallet-icon">💳</span>
|
<span class="wallet-icon">💳</span>
|
||||||
{{ isLoading ? 'Подключение...' : 'Подключить кошелек' }}
|
{{ isLoading ? 'Подключение...' : 'Подключить кошелек' }}
|
||||||
</button>
|
</button>
|
||||||
@@ -20,106 +16,48 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { useAuth } from '@/composables/useAuth';
|
import { useAuth } from '@/composables/useAuth';
|
||||||
import { connectWithWallet } from '@/services/wallet';
|
import { connectWithWallet } from '@/services/wallet';
|
||||||
|
|
||||||
const emit = defineEmits(['close']);
|
const emit = defineEmits(['close']);
|
||||||
const { linkIdentity } = useAuth();
|
const { linkIdentity } = useAuth();
|
||||||
|
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
const address = ref('');
|
const address = ref('');
|
||||||
|
|
||||||
const isConnected = computed(() => !!address.value);
|
const isConnected = computed(() => !!address.value);
|
||||||
|
|
||||||
const formatAddress = (addr) => {
|
const formatAddress = (addr) => {
|
||||||
if (!addr) return '';
|
if (!addr) return '';
|
||||||
return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
|
return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const connectWallet = async () => {
|
const connectWallet = async () => {
|
||||||
if (isLoading.value) return;
|
if (isLoading.value) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
|
|
||||||
// Подключаем кошелек
|
// Подключаем кошелек
|
||||||
const result = await connectWithWallet();
|
const result = await connectWithWallet();
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
address.value = result.address;
|
address.value = result.address;
|
||||||
|
|
||||||
// Связываем кошелек с текущим пользователем
|
// Связываем кошелек с текущим пользователем
|
||||||
await linkIdentity('wallet', result.address);
|
await linkIdentity('wallet', result.address);
|
||||||
emit('close');
|
emit('close');
|
||||||
} else {
|
} else {
|
||||||
error.value = result.error || 'Не удалось подключить кошелек';
|
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>
|
</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>
|
|
||||||
@@ -2,8 +2,4 @@ import TelegramConnect from './TelegramConnect.vue';
|
|||||||
import WalletConnection from './WalletConnection.vue';
|
import WalletConnection from './WalletConnection.vue';
|
||||||
import EmailConnect from './EmailConnect.vue';
|
import EmailConnect from './EmailConnect.vue';
|
||||||
|
|
||||||
export {
|
export { TelegramConnect, WalletConnection, EmailConnect };
|
||||||
TelegramConnect,
|
|
||||||
WalletConnection,
|
|
||||||
EmailConnect
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ export function useAuth() {
|
|||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
// Фильтруем идентификаторы: убираем гостевые и оставляем только уникальные
|
// Фильтруем идентификаторы: убираем гостевые и оставляем только уникальные
|
||||||
const filteredIdentities = response.data.identities
|
const filteredIdentities = response.data.identities
|
||||||
.filter(identity => identity.provider !== 'guest')
|
.filter((identity) => identity.provider !== 'guest')
|
||||||
.reduce((acc, identity) => {
|
.reduce((acc, identity) => {
|
||||||
// Для каждого типа провайдера оставляем только один идентификатор
|
// Для каждого типа провайдера оставляем только один идентификатор
|
||||||
const existingIdentity = acc.find(i => i.provider === identity.provider);
|
const existingIdentity = acc.find((i) => i.provider === identity.provider);
|
||||||
if (!existingIdentity) {
|
if (!existingIdentity) {
|
||||||
acc.push(identity);
|
acc.push(identity);
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,15 @@ export function useAuth() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateAuth = async ({ authenticated, authType: newAuthType, userId: newUserId, address: newAddress, telegramId: newTelegramId, isAdmin: newIsAdmin, email: newEmail }) => {
|
const updateAuth = async ({
|
||||||
|
authenticated,
|
||||||
|
authType: newAuthType,
|
||||||
|
userId: newUserId,
|
||||||
|
address: newAddress,
|
||||||
|
telegramId: newTelegramId,
|
||||||
|
isAdmin: newIsAdmin,
|
||||||
|
email: newEmail,
|
||||||
|
}) => {
|
||||||
const wasAuthenticated = isAuthenticated.value;
|
const wasAuthenticated = isAuthenticated.value;
|
||||||
const previousUserId = userId.value;
|
const previousUserId = userId.value;
|
||||||
|
|
||||||
@@ -80,7 +88,7 @@ export function useAuth() {
|
|||||||
newAddress,
|
newAddress,
|
||||||
newTelegramId,
|
newTelegramId,
|
||||||
newIsAdmin,
|
newIsAdmin,
|
||||||
newEmail
|
newEmail,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Убедимся, что переменные являются реактивными
|
// Убедимся, что переменные являются реактивными
|
||||||
@@ -93,15 +101,18 @@ export function useAuth() {
|
|||||||
email.value = newEmail || null;
|
email.value = newEmail || null;
|
||||||
|
|
||||||
// Кэшируем данные аутентификации
|
// Кэшируем данные аутентификации
|
||||||
localStorage.setItem('authData', JSON.stringify({
|
localStorage.setItem(
|
||||||
authenticated,
|
'authData',
|
||||||
authType: newAuthType,
|
JSON.stringify({
|
||||||
userId: newUserId,
|
authenticated,
|
||||||
address: newAddress,
|
authType: newAuthType,
|
||||||
telegramId: newTelegramId,
|
userId: newUserId,
|
||||||
isAdmin: newIsAdmin,
|
address: newAddress,
|
||||||
email: newEmail
|
telegramId: newTelegramId,
|
||||||
}));
|
isAdmin: newIsAdmin,
|
||||||
|
email: newEmail,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Если аутентификация через кошелек, проверяем баланс токенов только при изменении адреса
|
// Если аутентификация через кошелек, проверяем баланс токенов только при изменении адреса
|
||||||
if (authenticated && newAuthType === 'wallet' && newAddress && newAddress !== address.value) {
|
if (authenticated && newAuthType === 'wallet' && newAddress && newAddress !== address.value) {
|
||||||
@@ -123,7 +134,7 @@ export function useAuth() {
|
|||||||
address: address.value,
|
address: address.value,
|
||||||
telegramId: telegramId.value,
|
telegramId: telegramId.value,
|
||||||
email: email.value,
|
email: email.value,
|
||||||
isAdmin: isAdmin.value
|
isAdmin: isAdmin.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Если пользователь только что аутентифицировался или сменил аккаунт,
|
// Если пользователь только что аутентифицировался или сменил аккаунт,
|
||||||
@@ -149,14 +160,14 @@ export function useAuth() {
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: 'No new guest IDs to process',
|
message: 'No new guest IDs to process',
|
||||||
processedIds: processedGuestIds.value
|
processedIds: processedGuestIds.value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создаем объект с идентификаторами для передачи на сервер
|
// Создаем объект с идентификаторами для передачи на сервер
|
||||||
const identifiersData = {
|
const identifiersData = {
|
||||||
userId: userId.value,
|
userId: userId.value,
|
||||||
guestId: localGuestId
|
guestId: localGuestId,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Добавляем все доступные идентификаторы
|
// Добавляем все доступные идентификаторы
|
||||||
@@ -181,11 +192,13 @@ export function useAuth() {
|
|||||||
// В качестве запасного варианта также обрабатываем старый формат ответа
|
// В качестве запасного варианта также обрабатываем старый формат ответа
|
||||||
else if (response.data.results && Array.isArray(response.data.results)) {
|
else if (response.data.results && Array.isArray(response.data.results)) {
|
||||||
const newProcessedIds = response.data.results
|
const newProcessedIds = response.data.results
|
||||||
.filter(result => result.guestId)
|
.filter((result) => result.guestId)
|
||||||
.map(result => result.guestId);
|
.map((result) => result.guestId);
|
||||||
|
|
||||||
if (newProcessedIds.length > 0) {
|
if (newProcessedIds.length > 0) {
|
||||||
processedGuestIds.value = [...new Set([...processedGuestIds.value, ...newProcessedIds])];
|
processedGuestIds.value = [
|
||||||
|
...new Set([...processedGuestIds.value, ...newProcessedIds]),
|
||||||
|
];
|
||||||
console.log('Updated processed guest IDs from results:', processedGuestIds.value);
|
console.log('Updated processed guest IDs from results:', processedGuestIds.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,14 +209,14 @@ export function useAuth() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
processedIds: processedGuestIds.value
|
processedIds: processedGuestIds.value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error linking messages:', error);
|
console.error('Error linking messages:', error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: error.message
|
error: error.message,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -232,7 +245,7 @@ export function useAuth() {
|
|||||||
address: response.data.address,
|
address: response.data.address,
|
||||||
telegramId: response.data.telegramId,
|
telegramId: response.data.telegramId,
|
||||||
email: response.data.email,
|
email: response.data.email,
|
||||||
isAdmin: response.data.isAdmin
|
isAdmin: response.data.isAdmin,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Если пользователь аутентифицирован, обновляем список идентификаторов и связываем сообщения
|
// Если пользователь аутентифицирован, обновляем список идентификаторов и связываем сообщения
|
||||||
@@ -285,7 +298,7 @@ export function useAuth() {
|
|||||||
address: null,
|
address: null,
|
||||||
telegramId: null,
|
telegramId: null,
|
||||||
email: null,
|
email: null,
|
||||||
isAdmin: false
|
isAdmin: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Обновляем отображение отключенного состояния
|
// Обновляем отображение отключенного состояния
|
||||||
@@ -400,7 +413,7 @@ export function useAuth() {
|
|||||||
|
|
||||||
const response = await axios.post('/api/auth/identities/link', {
|
const response = await axios.post('/api/auth/identities/link', {
|
||||||
type: provider,
|
type: provider,
|
||||||
value: providerId
|
value: providerId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
@@ -426,7 +439,7 @@ export function useAuth() {
|
|||||||
console.error('Ошибка при связывании идентификатора:', error);
|
console.error('Ошибка при связывании идентификатора:', error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: error.response?.data?.error || error.message
|
error: error.response?.data?.error || error.message,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -449,6 +462,6 @@ export function useAuth() {
|
|||||||
updateIdentities,
|
updateIdentities,
|
||||||
updateProcessedGuestIds,
|
updateProcessedGuestIds,
|
||||||
updateConnectionDisplay,
|
updateConnectionDisplay,
|
||||||
linkIdentity
|
linkIdentity,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,8 @@ import axios from 'axios';
|
|||||||
|
|
||||||
// Настройка axios
|
// Настройка axios
|
||||||
// В Docker контейнере localhost:8000 не работает, поэтому используем явное значение
|
// В Docker контейнере localhost:8000 не работает, поэтому используем явное значение
|
||||||
const apiUrl = window.location.hostname === 'localhost' ? 'http://localhost:8000' : import.meta.env.VITE_API_URL;
|
const apiUrl =
|
||||||
|
window.location.hostname === 'localhost' ? 'http://localhost:8000' : import.meta.env.VITE_API_URL;
|
||||||
axios.defaults.baseURL = apiUrl;
|
axios.defaults.baseURL = apiUrl;
|
||||||
axios.defaults.withCredentials = true;
|
axios.defaults.withCredentials = true;
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ const routes = [
|
|||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'home',
|
name: 'home',
|
||||||
component: HomeView
|
component: HomeView,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes
|
routes,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('router/index.js: Router created');
|
console.log('router/index.js: Router created');
|
||||||
@@ -27,7 +27,7 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем аутентификацию, если маршрут требует авторизации
|
// Проверяем аутентификацию, если маршрут требует авторизации
|
||||||
if (to.matched.some(record => record.meta.requiresAuth)) {
|
if (to.matched.some((record) => record.meta.requiresAuth)) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/api/auth/check');
|
const response = await axios.get('/api/auth/check');
|
||||||
if (response.data.authenticated) {
|
if (response.data.authenticated) {
|
||||||
|
|||||||
@@ -3,25 +3,25 @@ import api from '../api/axios';
|
|||||||
// Адреса смарт-контрактов токенов HB3A
|
// Адреса смарт-контрактов токенов HB3A
|
||||||
export const TOKEN_CONTRACTS = {
|
export const TOKEN_CONTRACTS = {
|
||||||
eth: {
|
eth: {
|
||||||
address: "0xd95a45fc46a7300e6022885afec3d618d7d3f27c",
|
address: '0xd95a45fc46a7300e6022885afec3d618d7d3f27c',
|
||||||
symbol: "HB3A",
|
symbol: 'HB3A',
|
||||||
network: "Ethereum"
|
network: 'Ethereum',
|
||||||
},
|
},
|
||||||
bsc: {
|
bsc: {
|
||||||
address: "0x1d47f12ffA279BFE59Ab16d56fBb10d89AECdD5D",
|
address: '0x1d47f12ffA279BFE59Ab16d56fBb10d89AECdD5D',
|
||||||
symbol: "HB3A",
|
symbol: 'HB3A',
|
||||||
network: "BSC"
|
network: 'BSC',
|
||||||
},
|
},
|
||||||
arbitrum: {
|
arbitrum: {
|
||||||
address: "0xdce769b847a0a697239777d0b1c7dd33b6012ba0",
|
address: '0xdce769b847a0a697239777d0b1c7dd33b6012ba0',
|
||||||
symbol: "HB3A",
|
symbol: 'HB3A',
|
||||||
network: "Arbitrum"
|
network: 'Arbitrum',
|
||||||
},
|
},
|
||||||
polygon: {
|
polygon: {
|
||||||
address: "0x351f59de4fedbdf7601f5592b93db3b9330c1c1d",
|
address: '0x351f59de4fedbdf7601f5592b93db3b9330c1c1d',
|
||||||
symbol: "HB3A",
|
symbol: 'HB3A',
|
||||||
network: "Polygon"
|
network: 'Polygon',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Получение балансов токенов
|
// Получение балансов токенов
|
||||||
@@ -35,7 +35,7 @@ export const fetchTokenBalances = async () => {
|
|||||||
eth: '0',
|
eth: '0',
|
||||||
bsc: '0',
|
bsc: '0',
|
||||||
arbitrum: '0',
|
arbitrum: '0',
|
||||||
polygon: '0'
|
polygon: '0',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -45,7 +45,7 @@ export async function connectWithWallet() {
|
|||||||
version: '1',
|
version: '1',
|
||||||
chainId: 1,
|
chainId: 1,
|
||||||
nonce,
|
nonce,
|
||||||
resources: [`${origin}/api/auth/verify`]
|
resources: [`${origin}/api/auth/verify`],
|
||||||
});
|
});
|
||||||
|
|
||||||
const message = siweMessage.prepareMessage();
|
const message = siweMessage.prepareMessage();
|
||||||
@@ -55,7 +55,7 @@ export async function connectWithWallet() {
|
|||||||
console.log('Requesting signature...');
|
console.log('Requesting signature...');
|
||||||
const signature = await window.ethereum.request({
|
const signature = await window.ethereum.request({
|
||||||
method: 'personal_sign',
|
method: 'personal_sign',
|
||||||
params: [message, address]
|
params: [message, address],
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Got signature:', signature);
|
console.log('Got signature:', signature);
|
||||||
@@ -65,7 +65,7 @@ export async function connectWithWallet() {
|
|||||||
const verificationResponse = await axios.post('/api/auth/verify', {
|
const verificationResponse = await axios.post('/api/auth/verify', {
|
||||||
message,
|
message,
|
||||||
signature,
|
signature,
|
||||||
address
|
address,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Verification response:', verificationResponse.data);
|
console.log('Verification response:', verificationResponse.data);
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ export const connectWallet = async () => {
|
|||||||
console.error('No Ethereum provider (like MetaMask) detected!');
|
console.error('No Ethereum provider (like MetaMask) detected!');
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Не найден кошелек MetaMask или другой Ethereum провайдер. Пожалуйста, установите расширение MetaMask.'
|
error:
|
||||||
|
'Не найден кошелек MetaMask или другой Ethereum провайдер. Пожалуйста, установите расширение MetaMask.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ export const connectWallet = async () => {
|
|||||||
if (!accounts || accounts.length === 0) {
|
if (!accounts || accounts.length === 0) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Не удалось получить доступ к аккаунтам. Пожалуйста, разрешите доступ в MetaMask.'
|
error: 'Не удалось получить доступ к аккаунтам. Пожалуйста, разрешите доступ в MetaMask.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ export const connectWallet = async () => {
|
|||||||
if (!nonce) {
|
if (!nonce) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Не удалось получить nonce от сервера.'
|
error: 'Не удалось получить nonce от сервера.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ export const connectWallet = async () => {
|
|||||||
chainId: 1, // Ethereum mainnet
|
chainId: 1, // Ethereum mainnet
|
||||||
nonce: nonce,
|
nonce: nonce,
|
||||||
issuedAt: new Date().toISOString(),
|
issuedAt: new Date().toISOString(),
|
||||||
resources: [`${origin}/api/auth/verify`]
|
resources: [`${origin}/api/auth/verify`],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Получаем строку сообщения для подписи
|
// Получаем строку сообщения для подписи
|
||||||
@@ -79,7 +80,7 @@ export const connectWallet = async () => {
|
|||||||
if (!signature) {
|
if (!signature) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Подпись не была получена. Пожалуйста, подпишите сообщение в MetaMask.'
|
error: 'Подпись не была получена. Пожалуйста, подпишите сообщение в MetaMask.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +91,7 @@ export const connectWallet = async () => {
|
|||||||
const verifyResponse = await axios.post('/api/auth/verify', {
|
const verifyResponse = await axios.post('/api/auth/verify', {
|
||||||
address: normalizedAddress,
|
address: normalizedAddress,
|
||||||
signature,
|
signature,
|
||||||
nonce
|
nonce,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Обновляем интерфейс для отображения подключенного состояния
|
// Обновляем интерфейс для отображения подключенного состояния
|
||||||
@@ -118,12 +119,12 @@ export const connectWallet = async () => {
|
|||||||
success: true,
|
success: true,
|
||||||
address: normalizedAddress,
|
address: normalizedAddress,
|
||||||
userId: verifyResponse.data.userId,
|
userId: verifyResponse.data.userId,
|
||||||
isAdmin: verifyResponse.data.isAdmin
|
isAdmin: verifyResponse.data.isAdmin,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: verifyResponse.data.error || 'Ошибка верификации на сервере.'
|
error: verifyResponse.data.error || 'Ошибка верификации на сервере.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -142,7 +143,7 @@ export const connectWallet = async () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: errorMessage
|
error: errorMessage,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -45,8 +45,8 @@ export default defineConfig({
|
|||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
credentials: true,
|
credentials: true,
|
||||||
rewrite: (path) => path
|
rewrite: (path) => path,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user