feat: новая функция
This commit is contained in:
@@ -17,7 +17,11 @@
|
||||
<template v-if="props.canSelectMessages">
|
||||
<input type="checkbox" class="admin-select-checkbox" :checked="selectedMessageIds.includes(message.id)" @change="() => toggleSelectMessage(message.id)" />
|
||||
</template>
|
||||
<Message :message="message" />
|
||||
<Message
|
||||
:message="message"
|
||||
:isPrivateChat="isPrivateChat"
|
||||
:currentUserId="currentUserId"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -131,7 +135,11 @@ const props = defineProps({
|
||||
// Новые props для точного контроля прав
|
||||
canSend: { type: Boolean, default: true }, // Может отправлять сообщения
|
||||
canGenerateAI: { type: Boolean, default: false }, // Может генерировать AI-ответы
|
||||
canSelectMessages: { type: Boolean, default: false } // Может выбирать сообщения
|
||||
canSelectMessages: { type: Boolean, default: false }, // Может выбирать сообщения
|
||||
|
||||
// Props для приватного чата
|
||||
isPrivateChat: { type: Boolean, default: false }, // Это приватный чат
|
||||
currentUserId: { type: [String, Number], default: null } // ID текущего пользователя
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
@@ -833,4 +841,20 @@ async function handleAiReply() {
|
||||
.admin-select-checkbox {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* Стили для приватного чата */
|
||||
.message-wrapper {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* Для приватного чата выравниваем сообщения по сторонам */
|
||||
.chat-messages:has(.private-current-user) .message-wrapper {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.chat-messages:has(.private-other-user) .message-wrapper {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
</style>
|
||||
@@ -14,7 +14,10 @@
|
||||
<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="canChatWithAdmins" type="info" @click="goToPersonalMessages" style="margin-right: 1em;">
|
||||
Личные сообщения
|
||||
<el-badge v-if="privateUnreadCount > 0" :value="privateUnreadCount" class="notification-badge" />
|
||||
</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>
|
||||
@@ -130,7 +133,7 @@ import { useContactsAndMessagesWebSocket } from '../composables/useContactsWebSo
|
||||
import { usePermissions } from '@/composables/usePermissions';
|
||||
import { useAuthContext } from '@/composables/useAuth';
|
||||
import api from '../api/axios';
|
||||
import { sendMessage } from '../services/messagesService';
|
||||
import { sendMessage, getPrivateUnreadCount } from '../services/messagesService';
|
||||
import { useRoles } from '@/composables/useRoles';
|
||||
const props = defineProps({
|
||||
contacts: { type: Array, default: () => [] },
|
||||
@@ -156,6 +159,22 @@ const filterDateTo = ref('');
|
||||
const filterNewMessages = ref('');
|
||||
const filterBlocked = ref('all');
|
||||
|
||||
// Уведомления для приватных сообщений
|
||||
const privateUnreadCount = ref(0);
|
||||
|
||||
// Функция для загрузки количества непрочитанных приватных сообщений
|
||||
async function loadPrivateUnreadCount() {
|
||||
try {
|
||||
const response = await getPrivateUnreadCount();
|
||||
if (response.success) {
|
||||
privateUnreadCount.value = response.unreadCount || 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ContactTable] Ошибка загрузки непрочитанных сообщений:', error);
|
||||
privateUnreadCount.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Новый фильтр тегов через мультисвязи
|
||||
const availableTags = ref([]);
|
||||
const selectedTagIds = ref([]);
|
||||
@@ -255,6 +274,7 @@ onMounted(async () => {
|
||||
if (isAuthenticated.value) {
|
||||
try {
|
||||
await fetchRoles();
|
||||
await loadPrivateUnreadCount();
|
||||
} catch (error) {
|
||||
console.log('[ContactTable] Ошибка загрузки ролей в onMounted:', error.message);
|
||||
}
|
||||
@@ -437,28 +457,15 @@ async function sendPublicMessage() {
|
||||
}
|
||||
}
|
||||
|
||||
// Новая функция для отправки приватного сообщения
|
||||
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));
|
||||
// Функция для открытия приватного чата
|
||||
function sendPrivateMessage() {
|
||||
if (selectedIds.value.length === 0) {
|
||||
ElMessage.warning('Выберите контакт для отправки приватного сообщения');
|
||||
return;
|
||||
}
|
||||
|
||||
// Открываем приватный чат вместо отправки через prompt
|
||||
openPrivateChatForSelected();
|
||||
}
|
||||
|
||||
async function openPrivateChatForSelected(contact = null) {
|
||||
@@ -746,4 +753,8 @@ async function deleteMessagesSelected() {
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.notification-badge {
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -14,11 +14,13 @@
|
||||
<div
|
||||
:class="[
|
||||
'message',
|
||||
message.sender_type === 'assistant' || message.role === 'assistant'
|
||||
? 'ai-message'
|
||||
: message.sender_type === 'system' || message.role === 'system'
|
||||
? 'system-message'
|
||||
: 'user-message',
|
||||
isPrivateChat
|
||||
? (isCurrentUserMessage ? 'private-current-user' : 'private-other-user')
|
||||
: message.sender_type === 'assistant' || message.role === 'assistant'
|
||||
? 'ai-message'
|
||||
: message.sender_type === 'system' || message.role === 'system'
|
||||
? 'system-message'
|
||||
: 'user-message',
|
||||
message.isLocal ? 'is-local' : '',
|
||||
message.hasError ? 'has-error' : '',
|
||||
]"
|
||||
@@ -100,11 +102,24 @@ const props = defineProps({
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isPrivateChat: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
currentUserId: {
|
||||
type: [String, Number],
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
// Простая функция для определения, является ли сообщение отправленным текущим пользователем
|
||||
// Используем данные из самого сообщения для определения направления
|
||||
const isCurrentUserMessage = computed(() => {
|
||||
// Для приватного чата используем sender_id и currentUserId
|
||||
if (props.isPrivateChat && props.currentUserId) {
|
||||
return props.message.sender_id == props.currentUserId;
|
||||
}
|
||||
|
||||
// Если это admin_chat, используем sender_id для определения
|
||||
if (props.message.message_type === 'admin_chat') {
|
||||
// Для простоты, считаем что если sender_id равен user_id, то это ответное сообщение
|
||||
@@ -500,4 +515,50 @@ function copyEmail(email) {
|
||||
.read-status:contains('✓') {
|
||||
color: var(--color-success, #10b981);
|
||||
}
|
||||
|
||||
/* Стили для приватного чата */
|
||||
.private-current-user {
|
||||
background: linear-gradient(135deg, #3b82f6, #1d4ed8); /* Синий градиент */
|
||||
color: white;
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
max-width: 70%;
|
||||
border-radius: 18px 18px 4px 18px;
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.private-other-user {
|
||||
background: linear-gradient(135deg, #10b981, #059669); /* Зеленый градиент */
|
||||
color: white;
|
||||
margin-left: 0;
|
||||
margin-right: auto;
|
||||
max-width: 70%;
|
||||
border-radius: 18px 18px 18px 4px;
|
||||
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
/* Анимация появления сообщений */
|
||||
.private-current-user,
|
||||
.private-other-user {
|
||||
animation: slideInMessage 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideInMessage {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Адаптивность для мобильных устройств */
|
||||
@media (max-width: 768px) {
|
||||
.private-current-user,
|
||||
.private-other-user {
|
||||
max-width: 85%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -12,6 +12,13 @@
|
||||
|
||||
import api from '@/api/axios';
|
||||
|
||||
// Вспомогательные функции для экспорта
|
||||
async function getConversationByUserId(userId) {
|
||||
if (!userId) return null;
|
||||
const { data } = await api.get(`/messages/conversations?userId=${userId}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export default {
|
||||
async getMessagesByUserId(userId) {
|
||||
if (!userId) return [];
|
||||
@@ -40,9 +47,7 @@ export default {
|
||||
return data;
|
||||
},
|
||||
async getConversationByUserId(userId) {
|
||||
if (!userId) return null;
|
||||
const { data } = await api.get(`/messages/conversations?userId=${userId}`);
|
||||
return data;
|
||||
return getConversationByUserId(userId);
|
||||
},
|
||||
async generateAiDraft(conversationId, messages, language = 'auto') {
|
||||
const { data } = await api.post('/chat/ai-draft', { conversationId, messages, language });
|
||||
@@ -65,6 +70,9 @@ export default {
|
||||
}
|
||||
};
|
||||
|
||||
// Экспортируем функцию для использования в других компонентах
|
||||
export { getConversationByUserId };
|
||||
|
||||
export async function getAllMessages() {
|
||||
// Используем новый API для публичных сообщений
|
||||
const { data } = await api.get('/messages/public');
|
||||
@@ -73,12 +81,22 @@ export async function getAllMessages() {
|
||||
|
||||
// Новые методы для работы с типами сообщений
|
||||
export async function sendMessage({ recipientId, content, messageType = 'public' }) {
|
||||
const { data } = await api.post('/messages/send', {
|
||||
recipientId,
|
||||
content,
|
||||
messageType
|
||||
});
|
||||
return data;
|
||||
if (messageType === 'private') {
|
||||
// Используем новый API для приватных сообщений
|
||||
const { data } = await api.post('/messages/private/send', {
|
||||
recipientId,
|
||||
content
|
||||
});
|
||||
return data;
|
||||
} else {
|
||||
// Используем старый API для публичных сообщений
|
||||
const { data } = await api.post('/messages/send', {
|
||||
recipientId,
|
||||
content,
|
||||
messageType
|
||||
});
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPublicMessages(userId = null, options = {}) {
|
||||
@@ -88,10 +106,6 @@ export async function getPublicMessages(userId = null, options = {}) {
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getPrivateMessages(options = {}) {
|
||||
const { data } = await api.get('/messages/private', { params: options });
|
||||
return data;
|
||||
}
|
||||
|
||||
// Новые функции для работы с диалогами
|
||||
export async function getConversations(userId) {
|
||||
@@ -120,4 +134,36 @@ export async function getReadStatus() {
|
||||
export async function deleteMessageHistory(userId) {
|
||||
const { data } = await api.delete(`/messages/delete-history/${userId}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
// Новые функции для приватных сообщений
|
||||
export async function getPrivateConversations() {
|
||||
const { data } = await api.get('/messages/private/conversations');
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getPrivateMessages(conversationId) {
|
||||
const { data } = await api.get(`/messages/private/${conversationId}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function sendPrivateMessage({ recipientId, content }) {
|
||||
const { data } = await api.post('/messages/private/send', {
|
||||
recipientId,
|
||||
content
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
// Функции для работы с уведомлениями
|
||||
export async function getPrivateUnreadCount() {
|
||||
const { data } = await api.get('/messages/private/unread-count');
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function markPrivateMessagesAsRead(conversationId) {
|
||||
const { data } = await api.post('/messages/private/mark-read', {
|
||||
conversationId
|
||||
});
|
||||
return data;
|
||||
}
|
||||
@@ -30,6 +30,8 @@
|
||||
:canSend="true"
|
||||
:canGenerateAI="false"
|
||||
:canSelectMessages="false"
|
||||
:isPrivateChat="true"
|
||||
:currentUserId="currentUserId"
|
||||
@send-message="handleSendMessage"
|
||||
@update:newMessage="val => chatNewMessage = val"
|
||||
@update:attachments="val => chatAttachments = val"
|
||||
@@ -44,12 +46,15 @@ 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';
|
||||
import { getPrivateMessages, sendPrivateMessage, getPrivateConversations, markPrivateMessagesAsRead } from '../services/messagesService.js';
|
||||
import { useAuthContext } from '@/composables/useAuth';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { userId } = useAuthContext();
|
||||
|
||||
const adminId = computed(() => route.params.adminId);
|
||||
const currentUserId = computed(() => userId.value);
|
||||
const messages = ref([]);
|
||||
const chatAttachments = ref([]);
|
||||
const chatNewMessage = ref('');
|
||||
@@ -60,12 +65,35 @@ async function loadMessages() {
|
||||
|
||||
try {
|
||||
isLoadingMessages.value = true;
|
||||
console.log('[AdminChatView] Загружаем сообщения для админа:', adminId.value);
|
||||
console.log('[AdminChatView] Загружаем приватные сообщения для админа:', adminId.value);
|
||||
|
||||
const response = await adminChatService.getMessages(adminId.value);
|
||||
console.log('[AdminChatView] Получен ответ:', response);
|
||||
// Получаем приватные чаты пользователя
|
||||
const conversationsResponse = await getPrivateConversations();
|
||||
console.log('[AdminChatView] Приватные чаты:', conversationsResponse);
|
||||
|
||||
// Находим чат с нужным админом
|
||||
const conversation = conversationsResponse.conversations?.find(conv =>
|
||||
conv.user_id == adminId.value
|
||||
);
|
||||
|
||||
if (conversation) {
|
||||
// Загружаем историю чата
|
||||
const messagesResponse = await getPrivateMessages(conversation.conversation_id);
|
||||
console.log('[AdminChatView] История чата:', messagesResponse);
|
||||
|
||||
messages.value = messagesResponse?.messages || [];
|
||||
|
||||
// Отмечаем сообщения как прочитанные
|
||||
try {
|
||||
await markPrivateMessagesAsRead(conversation.conversation_id);
|
||||
console.log('[AdminChatView] Сообщения отмечены как прочитанные');
|
||||
} catch (error) {
|
||||
console.error('[AdminChatView] Ошибка отметки сообщений как прочитанных:', error);
|
||||
}
|
||||
} else {
|
||||
messages.value = [];
|
||||
}
|
||||
|
||||
messages.value = response?.messages || [];
|
||||
console.log('[AdminChatView] Загружено сообщений:', messages.value.length);
|
||||
} catch (error) {
|
||||
console.error('[AdminChatView] Ошибка загрузки сообщений:', error);
|
||||
@@ -79,9 +107,12 @@ async function handleSendMessage({ message, attachments }) {
|
||||
if (!message.trim() || !adminId.value) return;
|
||||
|
||||
try {
|
||||
console.log('[AdminChatView] Отправляем сообщение:', message, 'админу:', adminId.value);
|
||||
console.log('[AdminChatView] Отправляем приватное сообщение:', message, 'админу:', adminId.value);
|
||||
|
||||
await adminChatService.sendMessage(adminId.value, message, attachments);
|
||||
await sendPrivateMessage({
|
||||
recipientId: parseInt(adminId.value),
|
||||
content: message
|
||||
});
|
||||
|
||||
// Очищаем поле ввода
|
||||
chatNewMessage.value = '';
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<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 type="primary" size="small" @click="openPersonalChat(message)">
|
||||
Открыть
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -51,7 +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';
|
||||
import { getPrivateConversations } from '../services/messagesService';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
@@ -66,41 +66,43 @@ let ws = null;
|
||||
async function fetchPersonalMessages() {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
console.log('[PersonalMessagesView] Загружаем приватные сообщения...');
|
||||
console.log('[PersonalMessagesView] Загружаем приватные чаты...');
|
||||
|
||||
// Загружаем приватные сообщения через новый API с пагинацией
|
||||
const response = await getPrivateMessages({ limit: 100, offset: 0 });
|
||||
console.log('[PersonalMessagesView] Загружено приватных сообщений:', response.messages?.length || 0);
|
||||
// Загружаем приватные чаты через новый API
|
||||
const response = await getPrivateConversations();
|
||||
console.log('[PersonalMessagesView] Загружено приватных чатов:', response.conversations?.length || 0);
|
||||
|
||||
const privateMessages = response.success && response.messages ? response.messages : [];
|
||||
const conversations = response.success && response.conversations ? response.conversations : [];
|
||||
|
||||
// Группируем сообщения по отправителям для отображения списка бесед
|
||||
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;
|
||||
}
|
||||
console.log('[PersonalMessagesView] Полученные conversations:', conversations);
|
||||
|
||||
// Проверяем, что у нас есть данные
|
||||
if (!conversations || conversations.length === 0) {
|
||||
console.log('[PersonalMessagesView] Нет приватных чатов');
|
||||
personalMessages.value = [];
|
||||
newMessagesCount.value = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Формируем список бесед
|
||||
personalMessages.value = conversations.map(conv => {
|
||||
console.log('[PersonalMessagesView] Обрабатываем conversation:', conv);
|
||||
return {
|
||||
id: conv.conversation_id,
|
||||
conversation_id: conv.conversation_id,
|
||||
user_id: conv.user_id,
|
||||
name: conv.title || `Чат с пользователем ${conv.user_id}`,
|
||||
last_message: 'Приватный чат',
|
||||
last_message_at: conv.updated_at,
|
||||
message_count: conv.message_count || 0
|
||||
};
|
||||
});
|
||||
|
||||
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;
|
||||
@@ -156,9 +158,19 @@ function disconnectWebSocket() {
|
||||
}
|
||||
}
|
||||
|
||||
function openPersonalChat(adminId) {
|
||||
console.log('[PersonalMessagesView] Открываем приватный чат с админом:', adminId);
|
||||
router.push({ name: 'admin-chat', params: { adminId } });
|
||||
function openPersonalChat(conversation) {
|
||||
console.log('[PersonalMessagesView] Открываем приватный чат:', conversation);
|
||||
|
||||
// Проверяем, что у нас есть user_id
|
||||
if (!conversation.user_id) {
|
||||
console.error('[PersonalMessagesView] Ошибка: user_id не найден в conversation:', conversation);
|
||||
return;
|
||||
}
|
||||
|
||||
// Переходим к чату с ID админа (user_id в conversation)
|
||||
const adminId = parseInt(conversation.user_id);
|
||||
console.log('[PersonalMessagesView] Переходим к чату с adminId:', adminId);
|
||||
router.push({ name: 'admin-chat', params: { adminId: adminId } });
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
|
||||
Reference in New Issue
Block a user