feat: новая функция

This commit is contained in:
2025-10-16 18:44:30 +03:00
parent e0300480e1
commit 927d174f66
33 changed files with 1494 additions and 700 deletions

View File

@@ -217,11 +217,15 @@ const handleWalletAuth = async () => {
const disconnectWallet = async () => {
// console.log('[BaseLayout] Выполняется выход из системы...');
try {
await api.post('/auth/logout');
showSuccessMessage('Вы успешно вышли из системы');
removeFromStorage('guestMessages');
removeFromStorage('hasUserSentMessage');
emit('auth-action-completed');
// Используем централизованную функцию disconnect из useAuth
const result = await auth.disconnect();
if (result.success) {
showSuccessMessage('Вы успешно вышли из системы');
emit('auth-action-completed');
} else {
showErrorMessage(result.error || 'Произошла ошибка при выходе из системы');
}
} catch (error) {
// console.error('[BaseLayout] Ошибка при выходе из системы:', error);
showErrorMessage('Произошла ошибка при выходе из системы');

View File

@@ -13,9 +13,10 @@
<template>
<div class="contact-table-modal">
<div class="contact-table-header">
<!-- Кнопка "Личные сообщения" для всех пользователей -->
<el-button v-if="canChatWithAdmins" type="info" @click="goToPersonalMessages" style="margin-right: 1em;">Личные сообщения</el-button>
<el-button v-if="canSendToUsers" type="success" :disabled="!selectedIds.length" @click="() => openChatForSelected()" style="margin-right: 1em;">Публичное сообщение</el-button>
<el-button v-if="canViewContacts" type="warning" :disabled="!selectedIds.length" @click="() => openPrivateChatForSelected()" style="margin-right: 1em;">Приватное сообщение</el-button>
<el-button v-if="canSendToUsers" type="success" :disabled="!hasSelectedEditor" @click="sendPublicMessage" style="margin-right: 1em;">Публичное сообщение</el-button>
<el-button v-if="canViewContacts" type="warning" :disabled="!hasSelectedEditor" @click="sendPrivateMessage" style="margin-right: 1em;">Приватное сообщение</el-button>
<el-button v-if="canManageSettings" type="info" :disabled="!selectedIds.length" @click="showBroadcastModal = true" style="margin-right: 1em;">Рассылка</el-button>
<el-button v-if="canDeleteMessages" type="warning" :disabled="!selectedIds.length" @click="deleteMessagesSelected" style="margin-right: 1em;">Удалить сообщения</el-button>
<el-button v-if="canDeleteData" type="danger" :disabled="!selectedIds.length" @click="deleteSelected" style="margin-right: 1em;">Удалить</el-button>
@@ -88,13 +89,16 @@
</tr>
</thead>
<tbody>
<tr v-for="contact in contactsArray" :key="contact.id" :class="{ 'new-contact-row': newIds.includes(contact.id) }">
<tr v-for="contact in filteredContacts" :key="contact.id" :class="{ 'new-contact-row': newIds.includes(contact.id) }">
<td v-if="canViewContacts"><input type="checkbox" v-model="selectedIds" :value="contact.id" /></td>
<td>
<span v-if="contact.contact_type === 'admin'" class="admin-badge">Админ</span>
<span v-else-if="contact.contact_type === 'editor'" class="editor-badge">Редактор</span>
<span v-else-if="contact.contact_type === 'readonly'" class="readonly-badge">Чтение</span>
<span v-else class="user-badge">Пользователь</span>
<span
v-if="getRoleDisplayName(contact.role)"
:class="getRoleClass(contact.role)"
>
{{ getRoleDisplayName(contact.role) }}
</span>
<span v-else class="user-badge">Неизвестно</span>
</td>
<td>{{ contact.name || '-' }}</td>
<td>{{ contact.email || '-' }}</td>
@@ -122,8 +126,12 @@ import BroadcastModal from './BroadcastModal.vue';
import tablesService from '../services/tablesService';
import messagesService from '../services/messagesService';
import { useTagsWebSocket } from '../composables/useTagsWebSocket';
import { useContactsAndMessagesWebSocket } from '../composables/useContactsWebSocket';
import { usePermissions } from '@/composables/usePermissions';
import { useAuthContext } from '@/composables/useAuth';
import api from '../api/axios';
import { sendMessage } from '../services/messagesService';
import { useRoles } from '@/composables/useRoles';
const props = defineProps({
contacts: { type: Array, default: () => [] },
newContacts: { type: Array, default: () => [] },
@@ -131,11 +139,14 @@ const props = defineProps({
markMessagesAsReadForUser: { type: Function, default: null },
markContactAsRead: { type: Function, default: null }
});
const contactsArray = ref([]); // теперь управляем вручную
// Используем переданные через props данные вместо создания собственного массива
const contactsArray = computed(() => props.contacts || []);
const newIds = computed(() => props.newContacts.map(c => c.id));
const newMsgUserIds = computed(() => props.newMessages.map(m => String(m.user_id)));
const router = useRouter();
const { canViewContacts, canSendToUsers, canDeleteData, canDeleteMessages, canManageSettings, canChatWithAdmins, canEditData } = usePermissions();
const { userAccessLevel, userId, isAuthenticated } = useAuthContext();
const { roles, getRoleDisplayName, getRoleClass, fetchRoles, clearRoles } = useRoles();
// Фильтры
const filterSearch = ref('');
@@ -155,14 +166,106 @@ const showBroadcastModal = ref(false);
const selectedIds = ref([]);
const selectAll = ref(false);
// Проверяем, есть ли среди выбранных контактов editor
const hasSelectedEditor = computed(() => {
return selectedIds.value.some(id => {
const contact = contactsArray.value.find(c => c.id === id);
return contact?.role === 'editor';
});
});
// Фильтрация контактов для USER - видит только editor админов и себя
const filteredContacts = computed(() => {
console.log('[ContactTable] 🔍 Фильтрация контактов:');
console.log('[ContactTable] userAccessLevel:', userAccessLevel.value);
console.log('[ContactTable] userId:', userId.value);
console.log('[ContactTable] Все контакты:', contactsArray.value);
if (userAccessLevel.value?.level === 'user') {
// USER видит только editor админов и себя
const filtered = contactsArray.value.filter(contact => {
const isEditor = contact.role === 'editor'; // Используем role вместо contact_type
const isSelf = contact.id === userId.value;
console.log(`[ContactTable] Контакт ${contact.id}: role=${contact.role}, contact_type=${contact.contact_type}, isEditor=${isEditor}, isSelf=${isSelf}`);
console.log(`[ContactTable] Полный объект контакта:`, contact);
return isEditor || isSelf;
});
console.log('[ContactTable] Отфильтрованные контакты:', filtered);
return filtered;
}
// READONLY и EDITOR видят всех
console.log('[ContactTable] Показываем всех (не user роль)');
return contactsArray.value;
});
// WebSocket для тегов - ОТКЛЮЧАЕМ из-за циклических запросов
// const { onTagsUpdate } = useTagsWebSocket();
// let unsubscribeFromTags = null;
let lastTagsHash = ref(''); // Хеш последних загруженных тегов
let tagsUpdateInterval = null; // Интервал для периодического обновления тегов
// Реактивная загрузка ролей и контактов при авторизации
watch(isAuthenticated, async (newValue) => {
if (newValue) {
console.log('[ContactTable] Пользователь авторизован, загружаем роли');
try {
await fetchRoles();
// Контакты загружаются автоматически через useContactsAndMessagesWebSocket
} catch (error) {
console.log('[ContactTable] Ошибка загрузки ролей (возможно, пользователь не авторизован):', error.message);
}
}
});
// Контакты обновляются автоматически через useContactsAndMessagesWebSocket при смене пользователя
// WebSocket для обновления контактов в реальном времени
let ws = null;
function setupContactsWebSocket() {
if (ws) {
ws.close();
}
const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
ws = new WebSocket(`${wsProtocol}://${window.location.host}/ws`);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'contacts-updated') {
console.log('[ContactTable] Получено WebSocket уведомление об обновлении контактов');
// Контакты обновляются автоматически через useContactsAndMessagesWebSocket
fetchRoles(); // Обновляем роли
}
};
ws.onopen = () => {
console.log('[ContactTable] WebSocket подключен для обновления контактов');
};
ws.onerror = (error) => {
console.error('[ContactTable] WebSocket ошибка:', error);
};
}
onMounted(async () => {
await fetchContacts();
// Контакты загружаются автоматически через useContactsAndMessagesWebSocket
// Загружаем роли только если пользователь авторизован
if (isAuthenticated.value) {
try {
await fetchRoles();
} catch (error) {
console.log('[ContactTable] Ошибка загрузки ролей в onMounted:', error.message);
}
}
// Настраиваем WebSocket для обновления контактов в реальном времени
setupContactsWebSocket();
// ContactTable - дочерний компонент, данные управляются через props
// Централизованные события обрабатываются в родительском компоненте (ContactsView)
// Здесь только очищаем локальные состояния таблицы при изменении props.contacts
// ВРЕМЕННО ОТКЛЮЧАЕМ - await loadAvailableTags();
// ВРЕМЕННО ОТКЛЮЧАЕМ - Вместо WebSocket используем периодическое обновление каждые 30 секунд
@@ -184,6 +287,16 @@ onUnmounted(() => {
// unsubscribeFromTags();
// }
// Закрываем WebSocket для контактов
if (ws) {
ws.close();
ws = null;
}
// Удаляем обработчики централизованных событий
window.removeEventListener('clear-application-data', () => {});
window.removeEventListener('refresh-application-data', () => {});
// Очищаем интервал
if (tagsUpdateInterval) {
clearInterval(tagsUpdateInterval);
@@ -243,27 +356,9 @@ function buildQuery() {
return params.toString();
}
async function fetchContacts() {
try {
// Загружаем обычные контакты
let url = '/users';
const query = buildQuery();
if (query) url += '?' + query;
console.log('[ContactTable] Загружаем контакты по URL:', url);
const res = await api.get(url);
console.log('[ContactTable] Получен ответ:', res.data);
contactsArray.value = res.data.contacts || [];
console.log('[ContactTable] Загружено контактов:', contactsArray.value.length);
console.log('[ContactTable] Первые 3 контакта:', contactsArray.value.slice(0, 3));
} catch (error) {
console.error('[ContactTable] Ошибка загрузки контактов:', error);
contactsArray.value = [];
}
}
// Функция fetchContacts больше не нужна - данные загружаются через useContactsAndMessagesWebSocket
function onAnyFilterChange() {
fetchContacts();
}
// Фильтрация происходит реактивно через computed свойство filteredContacts
function resetFilters() {
filterSearch.value = '';
@@ -273,7 +368,7 @@ function resetFilters() {
filterNewMessages.value = '';
filterBlocked.value = 'all';
selectedTagIds.value = []; // Сбрасываем выбранные теги
fetchContacts();
// Фильтрация происходит реактивно через computed свойство filteredContacts
}
function formatDateOnly(date) {
@@ -301,7 +396,7 @@ async function showDetails(contact) {
function onImported() {
showImportModal.value = false;
fetchContacts();
// Контакты обновляются автоматически через useContactsAndMessagesWebSocket
}
async function openChatForSelected() {
@@ -311,13 +406,61 @@ async function openChatForSelected() {
const contactId = selectedIds.value[0];
// Находим контакт в списке
const contact = contactsArray.value.find(c => c.id === contactId);
const contact = filteredContacts.value.find(c => c.id === contactId);
if (!contact) return;
// Открываем чат с этим контактом (user_chat)
await showDetails(contact);
}
// Новая функция для отправки публичного сообщения
async function sendPublicMessage() {
if (selectedIds.value.length === 0) return;
const contactId = selectedIds.value[0];
const contact = filteredContacts.value.find(c => c.id === contactId);
if (!contact) return;
try {
const content = prompt('Введите текст публичного сообщения:');
if (!content) return;
await sendMessage({
recipientId: contactId,
content,
messageType: 'public'
});
ElMessage.success('Публичное сообщение отправлено');
} catch (error) {
ElMessage.error('Ошибка отправки сообщения: ' + (error.message || error));
}
}
// Новая функция для отправки приватного сообщения
async function sendPrivateMessage() {
if (selectedIds.value.length === 0) return;
const contactId = selectedIds.value[0];
const contact = filteredContacts.value.find(c => c.id === contactId);
if (!contact) return;
try {
const content = prompt('Введите текст приватного сообщения:');
if (!content) return;
await sendMessage({
recipientId: contactId,
content,
messageType: 'private'
});
ElMessage.success('Приватное сообщение отправлено');
} catch (error) {
ElMessage.error('Ошибка отправки сообщения: ' + (error.message || error));
}
}
async function openPrivateChatForSelected(contact = null) {
let targetContact = contact;
@@ -334,7 +477,7 @@ async function openPrivateChatForSelected(contact = null) {
console.log('[ContactTable] Доступные контакты:', contactsArray.value.map(c => ({ id: c.id, name: c.name })));
// Находим контакт в списке
targetContact = contactsArray.value.find(c => c.id === contactId);
targetContact = filteredContacts.value.find(c => c.id === contactId);
if (!targetContact) {
console.error('[ContactTable] Контакт не найден с ID:', contactId);
return;
@@ -359,18 +502,41 @@ function goToPersonalMessages() {
function toggleSelectAll() {
if (selectAll.value) {
selectedIds.value = contactsArray.value.map(c => c.id);
selectedIds.value = filteredContacts.value.map(c => c.id);
} else {
selectedIds.value = [];
}
}
watch(contactsArray, () => {
watch(contactsArray, (newContacts, oldContacts) => {
console.log('[ContactTable] Contacts array changed:', {
oldLength: oldContacts?.length || 0,
newLength: newContacts?.length || 0
});
// Сбросить выбор при обновлении данных
selectedIds.value = [];
selectAll.value = false;
// Если контакты очищены (например, при отключении кошелька), очищаем и локальные фильтры
if (newContacts?.length === 0 && oldContacts?.length > 0) {
console.log('[ContactTable] Contacts cleared, resetting filters');
filterSearch.value = '';
filterContactType.value = 'all';
filterDateFrom.value = null;
filterDateTo.value = null;
filterNewMessages.value = '';
filterBlocked.value = 'all';
}
});
// Функция для обработки изменений фильтров
const onAnyFilterChange = () => {
// Просто сбрасываем выбор при изменении фильтров
selectedIds.value = [];
selectAll.value = false;
};
async function deleteSelected() {
if (!selectedIds.value.length) return;
try {
@@ -383,7 +549,7 @@ async function deleteSelected() {
await fetch(`/api/users/${id}`, { method: 'DELETE' });
}
ElMessage.success('Контакты удалены');
fetchContacts();
// Контакты обновляются автоматически через useContactsAndMessagesWebSocket
selectedIds.value = [];
selectAll.value = false;
} catch (e) {

View File

@@ -235,7 +235,9 @@ const updateAuth = async ({
if (!isAuthenticated.value && wasAuthenticated) {
console.log('[useAuth] User logged out, clearing application data');
// Очищаем глобальные данные приложения
window.dispatchEvent(new CustomEvent('clear-application-data'));
const event = new CustomEvent('clear-application-data');
console.log('[useAuth] Dispatching clear-application-data event:', event);
window.dispatchEvent(event);
}
// Централизованное обновление данных при подключении
@@ -436,8 +438,8 @@ const disconnect = async () => {
// Удаляем все идентификаторы перед выходом
await axios.post('/auth/logout');
// Обновляем состояние в памяти
updateAuth({
// Обновляем состояние в памяти через updateAuth (это запустит централизованные события)
await updateAuth({
authenticated: false,
authType: null,
userId: null,

View File

@@ -15,6 +15,7 @@ import api from '../api/axios';
import { getFromStorage, setToStorage, removeFromStorage } from '../utils/storage';
import { generateUniqueId } from '../utils/helpers';
import websocketModule from '../services/websocketService';
import { getPublicMessages } from '../services/messagesService';
const { websocketService } = websocketModule;
@@ -104,7 +105,7 @@ export function useChat(auth) {
let totalMessages = -1;
if (initial || messageLoading.value.offset === 0) {
try {
const countResponse = await api.get('/chat/history', { params: { count_only: true } });
const countResponse = await api.get('/messages/public', { params: { count_only: true } });
if (!countResponse.data.success) throw new Error('Не удалось получить количество сообщений');
totalMessages = countResponse.data.total || countResponse.data.count || 0;
// console.log(`[useChat] Всего сообщений в истории: ${totalMessages}`);
@@ -121,15 +122,17 @@ export function useChat(auth) {
// console.log(`[useChat] Рассчитано начальное смещение: ${effectiveOffset}`);
}
const response = await api.get('/chat/history', {
params: {
offset: effectiveOffset,
limit: messageLoading.value.limit,
},
// Используем новый API для публичных сообщений с пагинацией
const response = await api.get('/messages/public', {
params: {
offset: effectiveOffset,
limit: messageLoading.value.limit
}
});
if (response.data.success) {
const loadedMessages = response.data.messages || [];
if (response.data.success && response.data.messages) {
const loadedMessages = response.data.messages;
const totalFromResponse = response.data.total;
// console.log(`[useChat] Загружено ${loadedMessages.length} сообщений.`);
if (loadedMessages.length > 0) {
@@ -142,7 +145,7 @@ export function useChat(auth) {
// Обновляем смещение для следующей загрузки
// Если загружали последние, offset = total - limit + loaded
if (initial && totalMessages > 0 && effectiveOffset > 0) {
if (initial && totalFromResponse > 0 && effectiveOffset > 0) {
messageLoading.value.offset = effectiveOffset + loadedMessages.length;
} else {
messageLoading.value.offset += loadedMessages.length;
@@ -150,12 +153,12 @@ export function useChat(auth) {
// console.log(`[useChat] Новое смещение: ${messageLoading.value.offset}`);
// Проверяем, есть ли еще сообщения для загрузки
// Используем totalMessages, если он был успешно получен
if (totalMessages >= 0) {
messageLoading.value.hasMoreMessages = messageLoading.value.offset < totalMessages;
// Используем totalFromResponse из нового API
if (totalFromResponse >= 0) {
messageLoading.value.hasMoreMessages = messageLoading.value.offset < totalFromResponse;
} else {
// Если total не известен, считаем, что есть еще, если загрузили полный лимит
messageLoading.value.hasMoreMessages = loadedMessages.length === messageLoading.value.limit;
// Если total не известен, используем hasMore из ответа
messageLoading.value.hasMoreMessages = response.data.hasMore || false;
}
// console.log(`[useChat] Есть еще сообщения: ${messageLoading.value.hasMoreMessages}`);
} else {

View File

@@ -12,7 +12,7 @@
import { ref, onMounted, onUnmounted } from 'vue';
import { getContacts } from '../services/contactsService';
import { getAllMessages } from '../services/messagesService';
import { getPublicMessages } from '../services/messagesService';
import axios from 'axios';
export function useContactsAndMessagesWebSocket() {
@@ -111,9 +111,19 @@ export function useContactsAndMessagesWebSocket() {
}
async function fetchMessages() {
const all = await getAllMessages();
messages.value = all;
filterNewMessages();
try {
// Используем новый API для публичных сообщений с пагинацией
const response = await getPublicMessages(null, { limit: 50, offset: 0 });
if (response.success && response.messages) {
messages.value = response.messages;
filterNewMessages();
} else {
messages.value = [];
}
} catch (error) {
console.error('[useContactsWebSocket] Ошибка загрузки публичных сообщений:', error);
messages.value = [];
}
}
function markMessagesAsRead() {
@@ -170,6 +180,18 @@ export function useContactsAndMessagesWebSocket() {
lastReadMessageDate.value = {};
}
// Подписываемся на централизованные события очистки и обновления данных
window.addEventListener('clear-application-data', () => {
console.log('[useContactsWebSocket] Received clear-application-data event, clearing contacts data');
clearContactsData(); // Очищаем данные при выходе из системы
console.log('[useContactsWebSocket] Contacts data cleared successfully');
});
window.addEventListener('refresh-application-data', () => {
console.log('[useContactsWebSocket] Refreshing contacts data');
fetchContacts(); // Обновляем данные при входе в систему
});
// Централизованная подписка на изменения аутентификации
onMounted(async () => {
await fetchContactsReadStatus();
@@ -177,17 +199,6 @@ export function useContactsAndMessagesWebSocket() {
await fetchReadStatus();
await fetchMessages();
setupWebSocket();
// Подписываемся на централизованные события очистки и обновления данных
window.addEventListener('clear-application-data', () => {
console.log('[useContactsWebSocket] Clearing contacts data');
clearContactsData(); // Очищаем данные при выходе из системы
});
window.addEventListener('refresh-application-data', () => {
console.log('[useContactsWebSocket] Refreshing contacts data');
fetchContacts(); // Обновляем данные при входе в систему
});
});
onUnmounted(() => {
if (ws) ws.close();

View File

@@ -0,0 +1,104 @@
import { ref, computed, onMounted, onUnmounted } from 'vue'
import api from '../api/axios'
const roles = ref([])
const isLoading = ref(false)
const error = ref(null)
export function useRoles() {
// Загружаем роли с сервера (из базы данных через миграции)
const fetchRoles = async () => {
try {
isLoading.value = true
error.value = null
const response = await api.get('/users/roles')
if (response.data.success) {
roles.value = response.data.roles
console.log('[useRoles] Загружены роли из базы данных:', roles.value)
} else {
throw new Error(response.data.error || 'Ошибка загрузки ролей')
}
} catch (err) {
console.error('[useRoles] Ошибка при загрузке ролей:', err)
error.value = err.message
// Не показываем ошибку если пользователь не авторизован
if (err.response?.status === 401) {
console.log('[useRoles] Пользователь не авторизован, пропускаем загрузку ролей')
error.value = null
}
} finally {
isLoading.value = false
}
}
// Получаем название роли по ID
const getRoleName = (roleId) => {
const role = roles.value.find(r => r.id === roleId)
return role ? role.name : 'Неизвестно'
}
// Получаем CSS класс для роли
const getRoleClass = (roleName) => {
const classMap = {
'user': 'user-badge',
'readonly': 'readonly-badge',
'editor': 'editor-badge'
}
return classMap[roleName] || 'user-badge'
}
// Получаем отображаемое название роли
const getRoleDisplayName = (roleName) => {
const displayMap = {
'user': 'Пользователь',
'readonly': 'Чтение',
'editor': 'Редактор'
}
return displayMap[roleName] || 'Неизвестно'
}
// Функция для очистки ролей
const clearRoles = () => {
roles.value = []
error.value = null
console.log('[useRoles] Роли очищены')
}
// Computed для проверки загрузки
const isReady = computed(() => roles.value.length > 0 && !isLoading.value)
// Подписываемся на централизованные события
const handleClearData = () => {
console.log('[useRoles] Получено событие очистки данных')
clearRoles()
}
const handleRefreshData = () => {
console.log('[useRoles] Получено событие обновления данных, загружаем роли')
fetchRoles()
}
onMounted(() => {
window.addEventListener('clear-application-data', handleClearData)
window.addEventListener('refresh-application-data', handleRefreshData)
})
onUnmounted(() => {
window.removeEventListener('clear-application-data', handleClearData)
window.removeEventListener('refresh-application-data', handleRefreshData)
})
return {
roles: computed(() => roles.value),
isLoading: computed(() => isLoading.value),
error: computed(() => error.value),
isReady,
fetchRoles,
clearRoles,
getRoleName,
getRoleClass,
getRoleDisplayName
}
}

View File

@@ -290,6 +290,11 @@ const routes = [
name: 'connect-wallet',
component: () => import('../views/ConnectWalletView.vue')
},
{
path: '/accelerator/registration',
name: 'accelerator-registration',
component: () => import('../views/accelerator/AcceleratorRegistrationView.vue')
},
];
const router = createRouter({

View File

@@ -44,7 +44,8 @@ const adminChatService = {
async getAdminContacts() {
try {
console.log('[adminChatService] Запрашиваем админские контакты...');
const response = await api.get('/messages/admin/contacts');
// Используем существующий API для получения контактов
const response = await api.get('/users');
console.log('[adminChatService] Получен ответ:', response.data);
return response.data;
} catch (error) {
@@ -60,7 +61,8 @@ const adminChatService = {
*/
async getMessages(adminId) {
try {
const response = await api.get(`/messages/admin/${adminId}`);
// Используем новый API для приватных сообщений
const response = await api.get('/messages/private');
return response.data;
} catch (error) {
console.error('[adminChatService] Ошибка получения сообщений:', error);

View File

@@ -15,7 +15,8 @@ import api from '@/api/axios';
export default {
async getMessagesByUserId(userId) {
if (!userId) return [];
const { data } = await api.get(`/messages?userId=${userId}`);
// Используем новый API для публичных сообщений конкретного пользователя
const { data } = await api.get(`/messages/public?userId=${userId}`);
return data;
},
async sendMessage({ conversationId, message, attachments = [], toUserId }) {
@@ -34,7 +35,8 @@ export default {
},
async getMessagesByConversationId(conversationId) {
if (!conversationId) return [];
const { data } = await api.get(`/messages?conversationId=${conversationId}`);
// Используем новый API для публичных сообщений
const { data } = await api.get('/messages/public');
return data;
},
async getConversationByUserId(userId) {
@@ -64,6 +66,58 @@ export default {
};
export async function getAllMessages() {
const { data } = await api.get('/messages');
// Используем новый API для публичных сообщений
const { data } = await api.get('/messages/public');
return data;
}
// Новые методы для работы с типами сообщений
export async function sendMessage({ recipientId, content, messageType = 'public' }) {
const { data } = await api.post('/messages/send', {
recipientId,
content,
messageType
});
return data;
}
export async function getPublicMessages(userId = null, options = {}) {
const params = { ...options };
if (userId) params.userId = userId;
const { data } = await api.get('/messages/public', { params });
return data;
}
export async function getPrivateMessages(options = {}) {
const { data } = await api.get('/messages/private', { params: options });
return data;
}
// Новые функции для работы с диалогами
export async function getConversations(userId) {
const { data } = await api.get('/messages/conversations', { params: { userId } });
return data;
}
export async function createConversation(userId, title) {
const { data } = await api.post('/messages/conversations', { userId, title });
return data;
}
// Функция для отметки сообщений как прочитанных
export async function markMessagesAsRead(userId, lastReadAt) {
const { data } = await api.post('/messages/mark-read', { userId, lastReadAt });
return data;
}
// Функция для получения статуса прочтения
export async function getReadStatus() {
const { data } = await api.get('/messages/read-status');
return data;
}
// Функция для удаления истории сообщений
export async function deleteMessageHistory(userId) {
const { data } = await api.delete(`/messages/delete-history/${userId}`);
return data;
}

View File

@@ -16,72 +16,16 @@
<span>Контакты</span>
<span v-if="newContacts.length" class="badge">+{{ newContacts.length }}</span>
</div>
<ContactTable v-if="canViewContacts" :contacts="contacts" :new-contacts="newContacts" :new-messages="newMessages" @markNewAsRead="markMessagesAsRead"
:markMessagesAsReadForUser="markMessagesAsReadForUser" :markContactAsRead="markContactAsRead" @close="goBack" />
<!-- Таблица-заглушка для обычных пользователей -->
<div v-else class="contact-table-placeholder">
<div class="contact-table-header">
<h2>Контакты</h2>
<button class="close-btn" @click="goBack">×</button>
</div>
<!-- Форма фильтров (неактивная) -->
<div class="filters-form-placeholder">
<div class="form-row">
<div class="form-item">
<label>Поиск</label>
<input type="text" disabled placeholder="Поиск по имени, email, telegram, кошельку" />
</div>
<div class="form-item">
<label>Тип контакта</label>
<select disabled>
<option>Все</option>
</select>
</div>
<div class="form-item">
<label>Дата от</label>
<input type="date" disabled />
</div>
<div class="form-item">
<label>Дата до</label>
<input type="date" disabled />
</div>
<button class="btn-disabled" disabled>Сбросить фильтры</button>
</div>
</div>
<!-- Таблица с замаскированными данными -->
<table class="contact-table-masked">
<thead>
<tr>
<th>Имя</th>
<th>Email</th>
<th>Telegram</th>
<th>Кошелек</th>
<th>Дата создания</th>
<th>Действие</th>
</tr>
</thead>
<tbody>
<tr v-for="i in 3" :key="i">
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td>
<button class="details-btn-disabled" disabled>Подробнее</button>
</td>
</tr>
</tbody>
</table>
<div class="access-notice">
<i class="fas fa-info-circle"></i>
Полные данные контактов доступны только администраторам
</div>
</div>
<!-- Таблица контактов для всех пользователей -->
<ContactTable
:contacts="contacts"
:new-contacts="newContacts"
:new-messages="newMessages"
@markNewAsRead="markMessagesAsRead"
:markMessagesAsReadForUser="markMessagesAsReadForUser"
:markContactAsRead="markContactAsRead"
@close="goBack"
/>
</BaseLayout>
</template>
@@ -150,137 +94,4 @@ function goBack() {
margin-left: 7px;
}
/* Стили для таблицы-заглушки */
.contact-table-placeholder {
background: #fff;
border-radius: 16px;
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
padding: 32px 24px 24px 24px;
width: 100%;
margin-top: 40px;
position: relative;
overflow-x: auto;
}
.contact-table-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.contact-table-header h2 {
margin: 0;
color: #333;
}
.close-btn {
position: absolute;
top: 18px;
right: 18px;
background: none;
border: none;
font-size: 2rem;
cursor: pointer;
color: #bbb;
transition: color 0.2s;
}
.close-btn:hover {
color: #333;
}
.filters-form-placeholder {
margin-bottom: 24px;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
}
.form-row {
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: end;
}
.form-item {
display: flex;
flex-direction: column;
min-width: 150px;
}
.form-item label {
font-size: 0.9rem;
color: #666;
margin-bottom: 4px;
}
.form-item input,
.form-item select {
padding: 8px 12px;
border: 1px solid #dee2e6;
border-radius: 4px;
font-size: 0.9rem;
background: #f8f9fa;
color: #6c757d;
cursor: not-allowed;
}
.btn-disabled {
padding: 8px 16px;
border: 1px solid #dee2e6;
border-radius: 4px;
background: #f8f9fa;
color: #6c757d;
cursor: not-allowed;
height: fit-content;
}
.contact-table-masked {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
.contact-table-masked th,
.contact-table-masked td {
padding: 12px 8px;
text-align: left;
border-bottom: 1px solid #e9ecef;
}
.contact-table-masked th {
background: #f8f9fa;
font-weight: 600;
color: #495057;
}
.contact-table-masked td {
color: #adb5bd;
font-family: monospace;
}
.details-btn-disabled {
padding: 6px 12px;
border: 1px solid #dee2e6;
border-radius: 4px;
background: #f8f9fa;
color: #6c757d;
font-size: 0.8rem;
cursor: not-allowed;
}
.access-notice {
margin-top: 20px;
padding: 12px 16px;
background: #e3f2fd;
border: 1px solid #bbdefb;
border-radius: 8px;
color: #1976d2;
display: flex;
align-items: center;
gap: 8px;
font-size: 0.9rem;
}
</style>

View File

@@ -53,6 +53,13 @@
Подробнее
</button>
</div>
<!-- Блок Акселератор -->
<div class="crm-accelerator-block">
<h2>Акселератор</h2>
<button class="details-btn" @click="goToAcceleratorRegistration">
Подробнее
</button>
</div>
</div>
</BaseLayout>
</template>
@@ -242,6 +249,10 @@ function goToManagement() {
function goToWeb3App() {
router.push({ name: 'vds-mock' });
}
function goToAcceleratorRegistration() {
router.push({ name: 'accelerator-registration' });
}
</script>
<style scoped>
@@ -402,4 +413,23 @@ strong {
.crm-web3-block .details-btn {
margin-top: 0;
}
.crm-accelerator-block {
margin: 32px 0 24px 0;
padding: 24px;
background: #f8fafc;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
display: flex;
align-items: center;
justify-content: space-between;
}
.crm-accelerator-block h2 {
margin: 0;
font-size: 1.4rem;
font-weight: 600;
}
.crm-accelerator-block .details-btn {
margin-top: 0;
}
</style>

View File

@@ -51,6 +51,7 @@ import { useRouter, useRoute } from 'vue-router';
import BaseLayout from '../components/BaseLayout.vue';
import adminChatService from '../services/adminChatService.js';
import { usePermissions } from '@/composables/usePermissions';
import { getPrivateMessages } from '../services/messagesService';
const router = useRouter();
const route = useRoute();
@@ -65,15 +66,41 @@ let ws = null;
async function fetchPersonalMessages() {
try {
isLoading.value = true;
console.log('[PersonalMessagesView] Загружаем личные сообщения...');
const response = await adminChatService.getAdminContacts();
console.log('[PersonalMessagesView] API ответ:', response);
personalMessages.value = response?.contacts || [];
console.log('[PersonalMessagesView] Загружено бесед:', personalMessages.value.length);
console.log('[PersonalMessagesView] Беседы:', personalMessages.value);
newMessagesCount.value = personalMessages.value.length; // Simplified for now
console.log('[PersonalMessagesView] Загружаем приватные сообщения...');
// Загружаем приватные сообщения через новый API с пагинацией
const response = await getPrivateMessages({ limit: 100, offset: 0 });
console.log('[PersonalMessagesView] Загружено приватных сообщений:', response.messages?.length || 0);
const privateMessages = response.success && response.messages ? response.messages : [];
// Группируем сообщения по отправителям для отображения списка бесед
const messageGroups = {};
privateMessages.forEach(msg => {
const senderId = msg.sender_id || 'unknown';
if (!messageGroups[senderId]) {
messageGroups[senderId] = {
id: senderId,
name: `Админ ${senderId}`,
last_message: msg.content,
last_message_at: msg.created_at,
messages: []
};
}
messageGroups[senderId].messages.push(msg);
// Обновляем последнее сообщение
if (new Date(msg.created_at) > new Date(messageGroups[senderId].last_message_at)) {
messageGroups[senderId].last_message = msg.content;
messageGroups[senderId].last_message_at = msg.created_at;
}
});
personalMessages.value = Object.values(messageGroups);
newMessagesCount.value = personalMessages.value.length;
console.log('[PersonalMessagesView] Сформировано бесед:', personalMessages.value.length);
} catch (error) {
console.error('[PersonalMessagesView] Ошибка загрузки личных сообщений:', error);
console.error('[PersonalMessagesView] Ошибка загрузки приватных сообщений:', error);
personalMessages.value = [];
} finally {
isLoading.value = false;

View File

@@ -0,0 +1,368 @@
<!--
Copyright (c) 2024-2025 Тарабанов Александр Викторович
All rights reserved.
This software is proprietary and confidential.
Unauthorized copying, modification, or distribution is prohibited.
For licensing inquiries: info@hb3-accelerator.com
Website: https://hb3-accelerator.com
GitHub: https://github.com/HB3-ACCELERATOR
-->
<template>
<BaseLayout
:is-authenticated="isAuthenticated"
:identities="identities"
:token-balances="tokenBalances"
:is-loading-tokens="isLoadingTokens"
@auth-action-completed="$emit('auth-action-completed')"
>
<div class="accelerator-registration-container">
<div class="page-header">
<h1>Программы акселератора HB3</h1>
<p class="page-description">
Выберите подходящую программу для развития вашего бизнеса
</p>
</div>
<div class="program-selection">
<div class="form-section">
<h2>Выберите вид деятельности</h2>
<div class="form-group">
<label for="activityType">Вид деятельности *</label>
<select
id="activityType"
v-model="selectedActivity"
@change="onActivityChange"
class="form-select"
>
<option value="">Выберите вид деятельности</option>
<option value="fintech">Финансовые технологии (FinTech)</option>
<option value="blockchain">Блокчейн и Web3</option>
<option value="ai">Искусственный интеллект</option>
<option value="ecommerce">Электронная коммерция</option>
<option value="healthtech">Медицинские технологии</option>
<option value="edtech">Образовательные технологии</option>
<option value="realestate">Недвижимость и PropTech</option>
<option value="logistics">Логистика и транспорт</option>
<option value="greentech">Зеленые технологии</option>
<option value="agritech">Агротехнологии</option>
<option value="other">Другое</option>
</select>
</div>
<!-- Кнопка подключиться появляется при выборе деятельности -->
<div v-if="selectedActivity" class="connect-section">
<div class="selected-activity-info">
<h3>{{ getActivityInfo(selectedActivity).title }}</h3>
<p>{{ getActivityInfo(selectedActivity).description }}</p>
</div>
<button
@click="goToProgramDescription"
class="btn btn-primary btn-connect"
>
Подключиться
</button>
</div>
</div>
<!-- Кнопка назад -->
<div class="form-actions">
<button
@click="goBack"
class="btn btn-secondary"
>
Назад к CRM
</button>
</div>
</div>
</div>
</BaseLayout>
</template>
<script setup>
import { ref, defineProps, defineEmits } from 'vue';
import { useRouter } from 'vue-router';
import BaseLayout from '../../components/BaseLayout.vue';
// Определяем props
const props = defineProps({
isAuthenticated: Boolean,
identities: Array,
tokenBalances: Object,
isLoadingTokens: Boolean
});
// Определяем emits
const emit = defineEmits(['auth-action-completed']);
const router = useRouter();
const selectedActivity = ref('');
// Информация о видах деятельности
const activityInfo = {
fintech: {
title: 'Финансовые технологии (FinTech)',
description: 'Развитие инновационных решений в сфере финансовых услуг, платежных систем и банкинга.'
},
blockchain: {
title: 'Блокчейн и Web3',
description: 'Создание децентрализованных приложений, смарт-контрактов и Web3 решений.'
},
ai: {
title: 'Искусственный интеллект',
description: 'Разработка AI-решений, машинного обучения и автоматизации бизнес-процессов.'
},
ecommerce: {
title: 'Электронная коммерция',
description: 'Создание и развитие онлайн-магазинов, маркетплейсов и платформ для продаж.'
},
healthtech: {
title: 'Медицинские технологии',
description: 'Инновационные решения в сфере здравоохранения, телемедицины и биотехнологий.'
},
edtech: {
title: 'Образовательные технологии',
description: 'Разработка платформ для онлайн-обучения, образовательных приложений и EdTech решений.'
},
realestate: {
title: 'Недвижимость и PropTech',
description: 'Инновации в сфере недвижимости, платформы для аренды и продажи недвижимости.'
},
logistics: {
title: 'Логистика и транспорт',
description: 'Оптимизация цепей поставок, транспортных маршрутов и логистических процессов.'
},
greentech: {
title: 'Зеленые технологии',
description: 'Решения для экологии, возобновляемой энергетики и устойчивого развития.'
},
agritech: {
title: 'Агротехнологии',
description: 'Инновации в сельском хозяйстве, точное земледелие и агротехнические решения.'
},
other: {
title: 'Другое',
description: 'Инновационные проекты в других сферах деятельности.'
}
};
// Обработка изменения выбранной деятельности
const onActivityChange = () => {
console.log('Выбрана деятельность:', selectedActivity.value);
};
// Получение информации о выбранной деятельности
const getActivityInfo = (activity) => {
return activityInfo[activity] || { title: 'Неизвестная деятельность', description: '' };
};
// Переход к описанию программы
const goToProgramDescription = () => {
router.push({
name: 'accelerator-program-description',
params: { activity: selectedActivity.value }
});
};
// Навигация назад
const goBack = () => {
router.push({ name: 'crm' });
};
</script>
<style scoped>
.accelerator-registration-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: var(--color-white);
border-radius: var(--radius-lg);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.page-header {
text-align: center;
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 2px solid var(--color-light);
}
.page-header h1 {
color: var(--color-dark);
margin-bottom: 10px;
font-size: 2rem;
font-weight: 600;
}
.page-description {
color: var(--color-text-secondary);
font-size: 1.1rem;
margin: 0;
}
.program-selection {
display: flex;
flex-direction: column;
gap: 30px;
}
.form-section {
background: #f8fafc;
padding: 24px;
border-radius: 10px;
border: 1px solid #e2e8f0;
}
.form-section h2 {
color: var(--color-dark);
margin: 0 0 20px 0;
font-size: 1.3rem;
font-weight: 600;
}
.form-group {
margin-bottom: 20px;
}
.form-group:last-child {
margin-bottom: 0;
}
.form-group label {
display: block;
margin-bottom: 6px;
color: var(--color-dark);
font-weight: 500;
font-size: 0.9rem;
}
.form-select {
width: 100%;
padding: 12px 16px;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.2s, box-shadow 0.2s;
background-color: white;
cursor: pointer;
}
.form-select:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.connect-section {
margin-top: 30px;
padding: 20px;
background: white;
border-radius: 8px;
border: 2px solid var(--color-primary);
text-align: center;
}
.selected-activity-info h3 {
color: var(--color-dark);
margin: 0 0 10px 0;
font-size: 1.2rem;
font-weight: 600;
}
.selected-activity-info p {
color: var(--color-text-secondary);
margin: 0 0 20px 0;
font-size: 1rem;
line-height: 1.5;
}
.btn-connect {
padding: 15px 30px;
font-size: 1.1rem;
font-weight: 600;
min-width: 200px;
}
.form-actions {
display: flex;
gap: 15px;
justify-content: flex-start;
padding-top: 20px;
border-top: 1px solid #e2e8f0;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
display: inline-block;
text-align: center;
}
.btn-primary {
background: var(--color-primary);
color: white;
}
.btn-primary:hover:not(:disabled) {
background: var(--color-primary-dark);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.btn-primary:disabled {
background: #94a3b8;
cursor: not-allowed;
transform: none;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
transform: translateY(-1px);
}
/* Адаптивность */
@media (max-width: 768px) {
.accelerator-registration-container {
margin: 10px;
padding: 15px;
}
.form-actions {
flex-direction: column;
}
.btn {
width: 100%;
}
.btn-connect {
width: 100%;
min-width: auto;
}
.page-header h1 {
font-size: 1.5rem;
}
.page-description {
font-size: 1rem;
}
.connect-section {
padding: 15px;
}
}
</style>

View File

@@ -160,6 +160,7 @@ import Message from '../../components/Message.vue';
import ChatInterface from '../../components/ChatInterface.vue';
import contactsService from '../../services/contactsService.js';
import messagesService from '../../services/messagesService.js';
import { getPublicMessages, getConversationByUserId } from '../../services/messagesService.js';
import { useAuthContext } from '@/composables/useAuth';
import { usePermissions } from '@/composables/usePermissions';
import { useContactsAndMessagesWebSocket } from '@/composables/useContactsWebSocket';
@@ -401,11 +402,15 @@ async function loadMessages() {
console.log('[ContactDetailsView] 📥 loadMessages START for:', contact.value.id);
isLoadingMessages.value = true;
try {
// Загружаем ВСЕ публичные сообщения этого пользователя (как на главной странице)
const loadedMessages = await messagesService.getMessagesByUserId(contact.value.id);
console.log('[ContactDetailsView] 📩 Loaded messages:', loadedMessages.length, 'for', contact.value.id);
// Загружаем только публичные сообщения этого пользователя с пагинацией
const response = await getPublicMessages(contact.value.id, { limit: 50, offset: 0 });
console.log('[ContactDetailsView] 📩 Loaded messages:', response.messages?.length || 0, 'for', contact.value.id);
messages.value = loadedMessages;
if (response.success && response.messages) {
messages.value = response.messages;
} else {
messages.value = [];
}
if (messages.value.length > 0) {
lastMessageDate.value = messages.value[messages.value.length - 1].created_at;
@@ -417,7 +422,7 @@ async function loadMessages() {
// Гости не имеют conversations
if (!String(contact.value.id).startsWith('guest_')) {
try {
const conv = await messagesService.getConversationByUserId(contact.value.id);
const conv = await getConversationByUserId(contact.value.id);
conversationId.value = conv?.id || null;
} catch (convError) {
console.warn('[ContactDetailsView] Не удалось загрузить conversationId:', convError.message);