ваше сообщение коммита
This commit is contained in:
@@ -13,6 +13,9 @@
|
||||
<template>
|
||||
<div class="contact-table-modal">
|
||||
<div class="contact-table-header">
|
||||
<el-button v-if="canRead" type="info" @click="goToPersonalMessages" style="margin-right: 1em;">Личные сообщения</el-button>
|
||||
<el-button v-if="canEdit" type="success" :disabled="!selectedIds.length" @click="() => openChatForSelected()" style="margin-right: 1em;">Публичное сообщение</el-button>
|
||||
<el-button v-if="canRead" type="warning" :disabled="!selectedIds.length" @click="() => openPrivateChatForSelected()" 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="canDelete" type="warning" :disabled="!selectedIds.length" @click="deleteMessagesSelected" style="margin-right: 1em;">Удалить сообщения</el-button>
|
||||
<el-button v-if="canDelete" type="danger" :disabled="!selectedIds.length" @click="deleteSelected" style="margin-right: 1em;">Удалить</el-button>
|
||||
@@ -72,20 +75,25 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<table class="contact-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-if="canEdit || canDelete || canManageSettings"><input type="checkbox" v-model="selectAll" @change="toggleSelectAll" /></th>
|
||||
<th>Имя</th>
|
||||
<th>Email</th>
|
||||
<th>Telegram</th>
|
||||
<th>Кошелек</th>
|
||||
<th>Дата создания</th>
|
||||
<th>Действие</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-if="canRead"><input type="checkbox" v-model="selectAll" @change="toggleSelectAll" /></th>
|
||||
<th>Тип</th>
|
||||
<th>Имя</th>
|
||||
<th>Email</th>
|
||||
<th>Telegram</th>
|
||||
<th>Кошелек</th>
|
||||
<th>Дата создания</th>
|
||||
<th>Действие</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="contact in contactsArray" :key="contact.id" :class="{ 'new-contact-row': newIds.includes(contact.id) }">
|
||||
<td v-if="canEdit || canDelete || canManageSettings"><input type="checkbox" v-model="selectedIds" :value="contact.id" /></td>
|
||||
<td v-if="canRead"><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 class="user-badge">Пользователь</span>
|
||||
</td>
|
||||
<td>{{ contact.name || '-' }}</td>
|
||||
<td>{{ contact.email || '-' }}</td>
|
||||
<td>{{ contact.telegram || '-' }}</td>
|
||||
@@ -125,7 +133,7 @@ const contactsArray = ref([]); // теперь управляем вручную
|
||||
const newIds = computed(() => props.newContacts.map(c => c.id));
|
||||
const newMsgUserIds = computed(() => props.newMessages.map(m => String(m.user_id)));
|
||||
const router = useRouter();
|
||||
const { canEdit, canDelete, canManageSettings } = usePermissions();
|
||||
const { canRead, canEdit, canDelete, canManageSettings } = usePermissions();
|
||||
|
||||
// Фильтры
|
||||
const filterSearch = ref('');
|
||||
@@ -234,11 +242,21 @@ function buildQuery() {
|
||||
}
|
||||
|
||||
async function fetchContacts() {
|
||||
let url = '/users';
|
||||
const query = buildQuery();
|
||||
if (query) url += '?' + query;
|
||||
const res = await api.get(url);
|
||||
contactsArray.value = res.data.contacts || [];
|
||||
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 = [];
|
||||
}
|
||||
}
|
||||
|
||||
function onAnyFilterChange() {
|
||||
@@ -284,6 +302,59 @@ function onImported() {
|
||||
fetchContacts();
|
||||
}
|
||||
|
||||
async function openChatForSelected() {
|
||||
if (selectedIds.value.length === 0) return;
|
||||
|
||||
// Берем первый выбранный контакт
|
||||
const contactId = selectedIds.value[0];
|
||||
|
||||
// Находим контакт в списке
|
||||
const contact = contactsArray.value.find(c => c.id === contactId);
|
||||
if (!contact) return;
|
||||
|
||||
// Открываем чат с этим контактом (user_chat)
|
||||
await showDetails(contact);
|
||||
}
|
||||
|
||||
async function openPrivateChatForSelected(contact = null) {
|
||||
let targetContact = contact;
|
||||
|
||||
// Если контакт не передан, берем из выбранных
|
||||
if (!targetContact) {
|
||||
if (selectedIds.value.length === 0) {
|
||||
console.error('[ContactTable] Нет выбранных контактов');
|
||||
return;
|
||||
}
|
||||
|
||||
// Берем первый выбранный контакт
|
||||
const contactId = selectedIds.value[0];
|
||||
console.log('[ContactTable] Ищем контакт с ID:', contactId);
|
||||
console.log('[ContactTable] Доступные контакты:', contactsArray.value.map(c => ({ id: c.id, name: c.name })));
|
||||
|
||||
// Находим контакт в списке
|
||||
targetContact = contactsArray.value.find(c => c.id === contactId);
|
||||
if (!targetContact) {
|
||||
console.error('[ContactTable] Контакт не найден с ID:', contactId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем, что у контакта есть ID
|
||||
if (!targetContact.id) {
|
||||
console.error('[ContactTable] У контакта нет ID:', targetContact);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[ContactTable] Открываем приватный чат с контактом:', targetContact);
|
||||
|
||||
// Открываем приватный чат с этим контактом (admin_chat)
|
||||
router.push({ name: 'admin-chat', params: { adminId: targetContact.id } });
|
||||
}
|
||||
|
||||
function goToPersonalMessages() {
|
||||
router.push({ name: 'personal-messages' });
|
||||
}
|
||||
|
||||
function toggleSelectAll() {
|
||||
if (selectAll.value) {
|
||||
selectedIds.value = contactsArray.value.map(c => c.id);
|
||||
@@ -473,4 +544,22 @@ async function deleteMessagesSelected() {
|
||||
font-size: 1.2em;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.admin-badge {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.user-badge {
|
||||
background: #f3e5f5;
|
||||
color: #7b1fa2;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
@@ -23,6 +23,18 @@
|
||||
message.hasError ? 'has-error' : '',
|
||||
]"
|
||||
>
|
||||
<!-- Информация об отправителе для приватного чата -->
|
||||
<div v-if="message.message_type === 'admin_chat'" class="message-sender-info">
|
||||
<div class="sender-label">
|
||||
<span class="sender-direction">
|
||||
{{ isCurrentUserMessage ? 'Вы →' : '← Получено от' }}
|
||||
</span>
|
||||
<span class="sender-wallet">
|
||||
{{ formatWalletAddress(isCurrentUserMessage ? message.recipient_wallet : message.sender_wallet) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Текстовый контент, если есть -->
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-if="message.content" class="message-content" v-html="formattedContent" />
|
||||
@@ -60,6 +72,14 @@
|
||||
<div class="message-time">
|
||||
{{ formattedTime }}
|
||||
</div>
|
||||
<div v-if="message.message_type === 'admin_chat'" class="message-read-status">
|
||||
<span v-if="isCurrentUserMessage" class="read-status">
|
||||
{{ message.isRead ? '✓ Прочитано' : '○ Отправлено' }}
|
||||
</span>
|
||||
<span v-else class="read-status received">
|
||||
Получено
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="message.isLocal" class="message-status">
|
||||
<span class="sending-indicator">Отправка...</span>
|
||||
</div>
|
||||
@@ -82,6 +102,34 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
// Простая функция для определения, является ли сообщение отправленным текущим пользователем
|
||||
// Используем данные из самого сообщения для определения направления
|
||||
const isCurrentUserMessage = computed(() => {
|
||||
// Если это admin_chat, используем sender_id для определения
|
||||
if (props.message.message_type === 'admin_chat') {
|
||||
// Для простоты, считаем что если sender_id равен user_id, то это ответное сообщение
|
||||
// Это может потребовать корректировки в зависимости от логики
|
||||
return props.message.sender_id === props.message.user_id;
|
||||
}
|
||||
|
||||
// Для обычных сообщений используем стандартную логику
|
||||
return props.message.sender_type === 'user' || props.message.role === 'user';
|
||||
});
|
||||
|
||||
// Функция для форматирования wallet адреса
|
||||
const formatWalletAddress = (address) => {
|
||||
if (!address || address === 'Админ') {
|
||||
return 'Админ';
|
||||
}
|
||||
|
||||
// Если это wallet адрес (начинается с 0x), показываем сокращенную версию
|
||||
if (address.startsWith('0x') && address.length === 42) {
|
||||
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||
}
|
||||
|
||||
return address;
|
||||
};
|
||||
|
||||
// --- Работа с вложениями ---
|
||||
const attachment = computed(() => {
|
||||
// Ожидаем массив attachments, даже если там только один элемент
|
||||
@@ -405,4 +453,51 @@ function copyEmail(email) {
|
||||
.system-btn:hover {
|
||||
background: var(--color-primary-dark, #2563eb);
|
||||
}
|
||||
|
||||
/* Стили для информации об отправителе в приватном чате */
|
||||
.message-sender-info {
|
||||
margin-bottom: 8px;
|
||||
padding: 4px 8px;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 4px;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.sender-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sender-direction {
|
||||
font-weight: 600;
|
||||
color: var(--color-primary, #3b82f6);
|
||||
}
|
||||
|
||||
.sender-wallet {
|
||||
font-family: monospace;
|
||||
color: var(--color-text-secondary, #666);
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* Стили для статуса прочтения */
|
||||
.message-read-status {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.read-status {
|
||||
font-size: 0.8em;
|
||||
color: var(--color-text-secondary, #666);
|
||||
}
|
||||
|
||||
.read-status.received {
|
||||
color: var(--color-success, #10b981);
|
||||
}
|
||||
|
||||
.read-status:contains('✓') {
|
||||
color: var(--color-success, #10b981);
|
||||
}
|
||||
</style>
|
||||
@@ -24,7 +24,7 @@ export function usePermissions() {
|
||||
* Проверяет, может ли пользователь только читать данные
|
||||
*/
|
||||
const canRead = computed(() => {
|
||||
return userAccessLevel.value && userAccessLevel.value.hasAccess;
|
||||
return (userAccessLevel.value && userAccessLevel.value.hasAccess) || isAdmin.value;
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -161,6 +161,18 @@ const routes = [
|
||||
name: 'contacts-list',
|
||||
component: () => import('../views/ContactsView.vue')
|
||||
},
|
||||
{
|
||||
path: '/admin-chat/:adminId',
|
||||
name: 'admin-chat',
|
||||
component: () => import('../views/AdminChatView.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
{
|
||||
path: '/personal-messages',
|
||||
name: 'personal-messages',
|
||||
component: () => import('../views/PersonalMessagesView.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
|
||||
{
|
||||
path: '/settings/ai/telegram',
|
||||
@@ -197,16 +209,6 @@ const routes = [
|
||||
name: 'page-edit',
|
||||
component: () => import('../views/content/PageEditView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/pages/public',
|
||||
name: 'public-pages',
|
||||
component: () => import('../views/content/PublicPagesView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/pages/public/:id',
|
||||
name: 'public-page-view',
|
||||
component: () => import('../views/content/PublicPageView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/management',
|
||||
name: 'management',
|
||||
|
||||
74
frontend/src/services/adminChatService.js
Normal file
74
frontend/src/services/adminChatService.js
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import api from '../api/axios';
|
||||
|
||||
/**
|
||||
* Сервис для работы с админскими чатами
|
||||
*/
|
||||
const adminChatService = {
|
||||
/**
|
||||
* Отправляет сообщение другому администратору
|
||||
* @param {number} recipientAdminId - ID получателя
|
||||
* @param {string} content - Содержимое сообщения
|
||||
* @param {Array} attachments - Вложения
|
||||
* @returns {Promise} - Результат отправки
|
||||
*/
|
||||
async sendMessage(recipientAdminId, content, attachments = []) {
|
||||
try {
|
||||
const response = await api.post('/messages/admin/send', {
|
||||
recipientAdminId,
|
||||
content,
|
||||
attachments
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('[adminChatService] Ошибка отправки сообщения:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Получает список админских контактов
|
||||
* @returns {Promise} - Список контактов
|
||||
*/
|
||||
async getAdminContacts() {
|
||||
try {
|
||||
console.log('[adminChatService] Запрашиваем админские контакты...');
|
||||
const response = await api.get('/messages/admin/contacts');
|
||||
console.log('[adminChatService] Получен ответ:', response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('[adminChatService] Ошибка получения админов для приватного чата:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Получает сообщения из приватной беседы с другим администратором
|
||||
* @param {number} adminId - ID администратора
|
||||
* @returns {Promise} - Сообщения
|
||||
*/
|
||||
async getMessages(adminId) {
|
||||
try {
|
||||
const response = await api.get(`/messages/admin/${adminId}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('[adminChatService] Ошибка получения сообщений:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default adminChatService;
|
||||
|
||||
|
||||
169
frontend/src/views/AdminChatView.vue
Normal file
169
frontend/src/views/AdminChatView.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<!--
|
||||
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>
|
||||
<div class="admin-chat-header">
|
||||
<span>Приватный чат</span>
|
||||
<button class="close-btn" @click="goBack">×</button>
|
||||
</div>
|
||||
|
||||
<div v-if="isLoadingMessages" class="loading-container">
|
||||
<div class="loading">Загрузка сообщений...</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="chat-container">
|
||||
<ChatInterface
|
||||
:messages="messages"
|
||||
:attachments="chatAttachments"
|
||||
:newMessage="chatNewMessage"
|
||||
:isLoading="isLoadingMessages"
|
||||
:isAdmin="true"
|
||||
@send-message="handleSendMessage"
|
||||
@update:newMessage="val => chatNewMessage = val"
|
||||
@update:attachments="val => chatAttachments = val"
|
||||
@load-more="loadMessages"
|
||||
/>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import BaseLayout from '../components/BaseLayout.vue';
|
||||
import ChatInterface from '../components/ChatInterface.vue';
|
||||
import adminChatService from '../services/adminChatService.js';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const adminId = computed(() => route.params.adminId);
|
||||
const messages = ref([]);
|
||||
const chatAttachments = ref([]);
|
||||
const chatNewMessage = ref('');
|
||||
const isLoadingMessages = ref(false);
|
||||
|
||||
async function loadMessages() {
|
||||
if (!adminId.value) return;
|
||||
|
||||
try {
|
||||
isLoadingMessages.value = true;
|
||||
console.log('[AdminChatView] Загружаем сообщения для админа:', adminId.value);
|
||||
|
||||
const response = await adminChatService.getMessages(adminId.value);
|
||||
console.log('[AdminChatView] Получен ответ:', response);
|
||||
|
||||
messages.value = response?.messages || [];
|
||||
console.log('[AdminChatView] Загружено сообщений:', messages.value.length);
|
||||
} catch (error) {
|
||||
console.error('[AdminChatView] Ошибка загрузки сообщений:', error);
|
||||
messages.value = [];
|
||||
} finally {
|
||||
isLoadingMessages.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSendMessage({ message, attachments }) {
|
||||
if (!message.trim() || !adminId.value) return;
|
||||
|
||||
try {
|
||||
console.log('[AdminChatView] Отправляем сообщение:', message, 'админу:', adminId.value);
|
||||
|
||||
await adminChatService.sendMessage(adminId.value, message, attachments);
|
||||
|
||||
// Очищаем поле ввода
|
||||
chatNewMessage.value = '';
|
||||
chatAttachments.value = [];
|
||||
|
||||
// Перезагружаем сообщения
|
||||
await loadMessages();
|
||||
|
||||
console.log('[AdminChatView] Сообщение отправлено успешно');
|
||||
} catch (error) {
|
||||
console.error('[AdminChatView] Ошибка отправки сообщения:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
if (window.history.length > 1) {
|
||||
router.back();
|
||||
} else {
|
||||
router.push({ name: 'crm' });
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadMessages();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-chat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: #f5f5f5;
|
||||
border-bottom: 1px solid #ddd;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
height: 200px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
height: calc(100vh - 120px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading {
|
||||
color: #888;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Стили для ChatInterface */
|
||||
:deep(.chat-messages) {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
:deep(.chat-input) {
|
||||
border-top: 1px solid #ddd;
|
||||
padding: 1rem;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
</style>
|
||||
@@ -16,7 +16,7 @@
|
||||
<span>Контакты</span>
|
||||
<span v-if="newContacts.length" class="badge">+{{ newContacts.length }}</span>
|
||||
</div>
|
||||
<ContactTable v-if="canRead" :contacts="contacts" :new-contacts="newContacts" :new-messages="newMessages" @markNewAsRead="markContactsAsRead"
|
||||
<ContactTable v-if="canRead" :contacts="contacts" :new-contacts="newContacts" :new-messages="newMessages" @markNewAsRead="markMessagesAsRead"
|
||||
:markMessagesAsReadForUser="markMessagesAsReadForUser" :markContactAsRead="markContactAsRead" @close="goBack" />
|
||||
|
||||
<!-- Таблица-заглушка для обычных пользователей -->
|
||||
@@ -86,7 +86,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import BaseLayout from '../components/BaseLayout.vue';
|
||||
import ContactTable from '../components/ContactTable.vue';
|
||||
@@ -96,12 +96,23 @@ import { usePermissions } from '@/composables/usePermissions';
|
||||
|
||||
const {
|
||||
contacts, newContacts, newMessages,
|
||||
markContactsAsRead, markMessagesAsReadForUser, markContactAsRead
|
||||
markMessagesAsRead, markMessagesAsReadForUser, markContactAsRead
|
||||
} = useContactsAndMessagesWebSocket();
|
||||
const router = useRouter();
|
||||
const { isAdmin } = useAuthContext();
|
||||
const auth = useAuthContext();
|
||||
const { canRead } = usePermissions();
|
||||
|
||||
// Отладочная информация о правах доступа
|
||||
onMounted(() => {
|
||||
console.log('[ContactsView] Permissions debug:', {
|
||||
canRead: canRead.value,
|
||||
isAdmin: auth.isAdmin?.value,
|
||||
userAccessLevel: auth.userAccessLevel?.value,
|
||||
userId: auth.userId?.value,
|
||||
address: auth.address?.value
|
||||
});
|
||||
});
|
||||
|
||||
function goBack() {
|
||||
if (window.history.length > 1) {
|
||||
router.back();
|
||||
|
||||
272
frontend/src/views/PersonalMessagesView.vue
Normal file
272
frontend/src/views/PersonalMessagesView.vue
Normal file
@@ -0,0 +1,272 @@
|
||||
<!--
|
||||
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>
|
||||
<div class="personal-messages-header">
|
||||
<span>Личные сообщения</span>
|
||||
<span v-if="newMessagesCount > 0" class="badge">+{{ newMessagesCount }}</span>
|
||||
<button class="close-btn" @click="goBack">×</button>
|
||||
</div>
|
||||
|
||||
<div v-if="isLoading" class="loading-container">
|
||||
<div class="loading">Загрузка бесед...</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="personalMessages.length === 0" class="empty-state">
|
||||
<p>У вас пока нет личных бесед с другими администраторами</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="personal-messages-list">
|
||||
<div
|
||||
v-for="message in personalMessages"
|
||||
:key="message.id"
|
||||
class="message-item"
|
||||
>
|
||||
<div class="message-info">
|
||||
<div class="admin-name">{{ message.name }}</div>
|
||||
<div class="message-preview">{{ message.last_message || 'Нет сообщений' }}</div>
|
||||
<div class="message-date">{{ formatDate(message.last_message_at) }}</div>
|
||||
</div>
|
||||
<el-button type="primary" size="small" @click="openPersonalChat(message.id)">
|
||||
Открыть
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, computed, watch } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import BaseLayout from '../components/BaseLayout.vue';
|
||||
import adminChatService from '../services/adminChatService.js';
|
||||
import { usePermissions } from '@/composables/usePermissions';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const { canRead } = usePermissions();
|
||||
|
||||
const isLoading = ref(true);
|
||||
const personalMessages = ref([]);
|
||||
const newMessagesCount = ref(0);
|
||||
|
||||
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
|
||||
} catch (error) {
|
||||
console.error('[PersonalMessagesView] Ошибка загрузки личных сообщений:', error);
|
||||
personalMessages.value = [];
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function connectWebSocket() {
|
||||
try {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
const wsUrl = `${protocol}://${window.location.host}/ws`;
|
||||
ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('[PersonalMessagesView] WebSocket подключен');
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.type === 'contacts-updated' ||
|
||||
data.type === 'messages-updated' ||
|
||||
data.type === 'contact-updated' ||
|
||||
data.type === 'admin-status-changed') {
|
||||
console.log('[PersonalMessagesView] Получено обновление через WebSocket:', data.type);
|
||||
fetchPersonalMessages();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[PersonalMessagesView] Ошибка парсинга WebSocket сообщения:', error);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('[PersonalMessagesView] Ошибка WebSocket:', error);
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('[PersonalMessagesView] WebSocket отключен, переподключаемся через 3 секунды');
|
||||
setTimeout(() => {
|
||||
if (ws?.readyState === WebSocket.CLOSED) {
|
||||
connectWebSocket();
|
||||
}
|
||||
}, 3000);
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[PersonalMessagesView] Ошибка подключения WebSocket:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function disconnectWebSocket() {
|
||||
if (ws) {
|
||||
ws.close();
|
||||
ws = null;
|
||||
}
|
||||
}
|
||||
|
||||
function openPersonalChat(adminId) {
|
||||
console.log('[PersonalMessagesView] Открываем приватный чат с админом:', adminId);
|
||||
router.push({ name: 'admin-chat', params: { adminId } });
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
if (window.history.length > 1) {
|
||||
router.back();
|
||||
} else {
|
||||
router.push({ name: 'crm' });
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
// Следим за изменениями роута для обновления при возврате на страницу
|
||||
watch(() => route.path, async (newPath) => {
|
||||
if (newPath === '/personal-messages' && canRead.value) {
|
||||
console.log('[PersonalMessagesView] Возврат на страницу, обновляем список');
|
||||
await fetchPersonalMessages();
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
if (canRead.value) {
|
||||
await fetchPersonalMessages();
|
||||
connectWebSocket();
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
disconnectWebSocket();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.personal-messages-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: #f5f5f5;
|
||||
border-bottom: 1px solid #ddd;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.badge {
|
||||
background: #ff4757;
|
||||
color: white;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
height: 200px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.personal-messages-list {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 0.5rem;
|
||||
background: white;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.message-item:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.message-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.admin-name {
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.message-preview {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.message-date {
|
||||
color: #999;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* Стили для загрузки */
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 2rem;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
color: #666;
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -381,23 +381,22 @@ async function loadMessages() {
|
||||
if (!contact.value || !contact.value.id) return;
|
||||
isLoadingMessages.value = true;
|
||||
try {
|
||||
// Получаем conversationId для контакта
|
||||
const conv = await messagesService.getConversationByUserId(contact.value.id);
|
||||
conversationId.value = conv?.id || null;
|
||||
if (conversationId.value) {
|
||||
messages.value = await messagesService.getMessagesByConversationId(conversationId.value);
|
||||
// Загружаем ВСЕ публичные сообщения этого пользователя (как на главной странице)
|
||||
messages.value = await messagesService.getMessagesByUserId(contact.value.id);
|
||||
if (messages.value.length > 0) {
|
||||
lastMessageDate.value = messages.value[messages.value.length - 1].created_at;
|
||||
} else {
|
||||
lastMessageDate.value = null;
|
||||
}
|
||||
} else {
|
||||
messages.value = [];
|
||||
lastMessageDate.value = null;
|
||||
}
|
||||
|
||||
// Также получаем conversationId для отправки новых сообщений
|
||||
const conv = await messagesService.getConversationByUserId(contact.value.id);
|
||||
conversationId.value = conv?.id || null;
|
||||
} catch (e) {
|
||||
console.error('[ContactDetailsView] Ошибка загрузки сообщений:', e);
|
||||
messages.value = [];
|
||||
lastMessageDate.value = null;
|
||||
conversationId.value = null;
|
||||
} finally {
|
||||
isLoadingMessages.value = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user