feat: новая функция
This commit is contained in:
@@ -194,7 +194,7 @@ router.post('/verify', async (req, res) => {
|
|||||||
|
|
||||||
// Добавляем ссылки на документы в resources
|
// Добавляем ссылки на документы в resources
|
||||||
documents.forEach(doc => {
|
documents.forEach(doc => {
|
||||||
resources.push(`${origin}/content/published/${doc.id}`);
|
resources.push(`${origin}/public/page/${doc.id}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,14 +24,8 @@ const aiAssistantSettingsService = require('../services/aiAssistantSettingsServi
|
|||||||
const aiAssistantRulesService = require('../services/aiAssistantRulesService');
|
const aiAssistantRulesService = require('../services/aiAssistantRulesService');
|
||||||
const botManager = require('../services/botManager');
|
const botManager = require('../services/botManager');
|
||||||
const universalMediaProcessor = require('../services/UniversalMediaProcessor');
|
const universalMediaProcessor = require('../services/UniversalMediaProcessor');
|
||||||
|
const consentService = require('../services/consentService');
|
||||||
// Маппинг названий документов на типы согласий
|
const { DOCUMENT_CONSENT_MAP } = consentService;
|
||||||
const DOCUMENT_CONSENT_MAP = {
|
|
||||||
'Политика конфиденциальности': 'privacy_policy',
|
|
||||||
'Права субъектов персональных данных и отзыв согласия': 'personal_data',
|
|
||||||
'Согласие на использование файлов cookie': 'cookies',
|
|
||||||
'Согласие на обработку персональных данных': 'personal_data_processing',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Настройка multer для обработки файлов в памяти
|
// Настройка multer для обработки файлов в памяти
|
||||||
const storage = multer.memoryStorage();
|
const storage = multer.memoryStorage();
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ const logger = require('../utils/logger');
|
|||||||
// Маппинг названий документов на типы согласий
|
// Маппинг названий документов на типы согласий
|
||||||
const DOCUMENT_CONSENT_MAP = {
|
const DOCUMENT_CONSENT_MAP = {
|
||||||
'Политика конфиденциальности': 'privacy_policy',
|
'Политика конфиденциальности': 'privacy_policy',
|
||||||
'Права субъектов персональных данных и отзыв согласия': 'personal_data',
|
|
||||||
'Согласие на использование файлов cookie': 'cookies',
|
'Согласие на использование файлов cookie': 'cookies',
|
||||||
'Согласие на обработку персональных данных': 'personal_data_processing'
|
'Согласие на обработку персональных данных': 'personal_data_processing'
|
||||||
};
|
};
|
||||||
@@ -119,7 +118,7 @@ async function getConsentDocuments(missingConsents = []) {
|
|||||||
title: doc.title,
|
title: doc.title,
|
||||||
summary: doc.summary,
|
summary: doc.summary,
|
||||||
consentType: DOCUMENT_CONSENT_MAP[doc.title],
|
consentType: DOCUMENT_CONSENT_MAP[doc.title],
|
||||||
url: `/content/published/${doc.id}`
|
url: `/public/page/${doc.id}`
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[ConsentService] Ошибка получения документов:', error);
|
logger.error('[ConsentService] Ошибка получения документов:', error);
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
import { ref, watch, onMounted, computed, onUnmounted } from 'vue';
|
import { ref, watch, onMounted, computed, onUnmounted } from 'vue';
|
||||||
import { RouterView } from 'vue-router';
|
import { RouterView } from 'vue-router';
|
||||||
import { useAuth, provideAuth } from './composables/useAuth';
|
import { useAuth, provideAuth } from './composables/useAuth';
|
||||||
|
import { provideFooterDle } from './composables/useFooterDle';
|
||||||
import { useTokenBalancesWebSocket } from './composables/useTokenBalancesWebSocket';
|
import { useTokenBalancesWebSocket } from './composables/useTokenBalancesWebSocket';
|
||||||
import eventBus from './utils/eventBus';
|
import eventBus from './utils/eventBus';
|
||||||
import wsClient from './utils/websocket';
|
import wsClient from './utils/websocket';
|
||||||
@@ -64,6 +65,8 @@
|
|||||||
// --- Логика загрузки баланса токенов через WebSocket ---
|
// --- Логика загрузки баланса токенов через WebSocket ---
|
||||||
// Предоставляем auth контекст
|
// Предоставляем auth контекст
|
||||||
provideAuth();
|
provideAuth();
|
||||||
|
// Предоставляем контекст для выбранного DLE в футере
|
||||||
|
provideFooterDle();
|
||||||
|
|
||||||
// Инициализируем WebSocket composable
|
// Инициализируем WebSocket composable
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -12,7 +12,13 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="chat-container" :style="{ '--chat-input-height': chatInputHeight + 'px' }">
|
<div class="chat-container" :style="{ '--chat-input-height': chatInputHeight + 'px' }">
|
||||||
<div ref="messagesContainer" class="chat-messages" @scroll="handleScroll">
|
<!-- Блок истории сообщений -->
|
||||||
|
<div
|
||||||
|
ref="messagesContainer"
|
||||||
|
class="chat-messages"
|
||||||
|
:style="{ width: messagesWidth + '%' }"
|
||||||
|
@scroll="handleScroll"
|
||||||
|
>
|
||||||
<div v-for="message in messages" :key="message.id" :class="['message-wrapper', { 'selected-message': selectedMessageIds.includes(message.id) }]">
|
<div v-for="message in messages" :key="message.id" :class="['message-wrapper', { 'selected-message': selectedMessageIds.includes(message.id) }]">
|
||||||
<template v-if="props.canSelectMessages">
|
<template v-if="props.canSelectMessages">
|
||||||
<input type="checkbox" class="admin-select-checkbox" :checked="selectedMessageIds.includes(message.id)" @change="() => toggleSelectMessage(message.id)" />
|
<input type="checkbox" class="admin-select-checkbox" :checked="selectedMessageIds.includes(message.id)" @change="() => toggleSelectMessage(message.id)" />
|
||||||
@@ -26,7 +32,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ref="chatInputRef" class="chat-input">
|
<!-- Разделитель для изменения размера -->
|
||||||
|
<div
|
||||||
|
class="resizer"
|
||||||
|
@mousedown="startResize"
|
||||||
|
@touchstart="startResize"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- Блок ввода сообщений -->
|
||||||
|
<div
|
||||||
|
ref="chatInputRef"
|
||||||
|
class="chat-input"
|
||||||
|
:style="{ width: inputWidth + '%' }"
|
||||||
|
>
|
||||||
<div class="input-area">
|
<div class="input-area">
|
||||||
<textarea
|
<textarea
|
||||||
ref="messageInputRef"
|
ref="messageInputRef"
|
||||||
@@ -34,69 +52,73 @@
|
|||||||
@input="handleInput"
|
@input="handleInput"
|
||||||
placeholder="Введите сообщение..."
|
placeholder="Введите сообщение..."
|
||||||
:disabled="isLoading || !props.canSend"
|
:disabled="isLoading || !props.canSend"
|
||||||
rows="1"
|
|
||||||
autofocus
|
autofocus
|
||||||
@keydown.enter.prevent="sendMessage"
|
@keydown.enter.prevent="sendMessage"
|
||||||
@focus="handleFocus"
|
@focus="handleFocus"
|
||||||
@blur="handleBlur"
|
@blur="handleBlur"
|
||||||
/>
|
/>
|
||||||
<div class="chat-icons">
|
</div>
|
||||||
<button
|
<div class="chat-icons">
|
||||||
class="chat-icon-btn"
|
<button
|
||||||
title="Удерживайте для записи аудио"
|
class="chat-icon-btn"
|
||||||
@mousedown="startAudioRecording"
|
title="Удерживайте для записи аудио"
|
||||||
@mouseup="stopAudioRecording"
|
@mousedown="startAudioRecording"
|
||||||
@mouseleave="stopAudioRecording"
|
@mouseup="stopAudioRecording"
|
||||||
:class="{ 'recording': isAudioRecording }"
|
@mouseleave="stopAudioRecording"
|
||||||
:disabled="!props.canSend"
|
:class="{ 'recording': isAudioRecording }"
|
||||||
>
|
:disabled="!props.canSend"
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
|
>
|
||||||
<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z" fill="currentColor"/>
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
|
||||||
<path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z" fill="currentColor"/>
|
<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z" fill="currentColor"/>
|
||||||
</svg>
|
<path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z" fill="currentColor"/>
|
||||||
</button>
|
</svg>
|
||||||
<button
|
</button>
|
||||||
class="chat-icon-btn"
|
<button
|
||||||
title="Удерживайте для записи видео"
|
class="chat-icon-btn"
|
||||||
@mousedown="startVideoRecording"
|
title="Удерживайте для записи видео"
|
||||||
@mouseup="stopVideoRecording"
|
@mousedown="startVideoRecording"
|
||||||
@mouseleave="stopVideoRecording"
|
@mouseup="stopVideoRecording"
|
||||||
:class="{ 'recording': isVideoRecording }"
|
@mouseleave="stopVideoRecording"
|
||||||
:disabled="!props.canSend"
|
:class="{ 'recording': isVideoRecording }"
|
||||||
>
|
:disabled="!props.canSend"
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
|
>
|
||||||
<path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z" fill="currentColor"/>
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
|
||||||
</svg>
|
<path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z" fill="currentColor"/>
|
||||||
</button>
|
</svg>
|
||||||
<button class="chat-icon-btn" title="Прикрепить файл" @click="handleFileUpload" :disabled="!props.canSend">
|
</button>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
|
<button class="chat-icon-btn" title="Прикрепить файл" @click="handleFileUpload" :disabled="!props.canSend">
|
||||||
<path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z" fill="currentColor"/>
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
|
||||||
</svg>
|
<path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z" fill="currentColor"/>
|
||||||
</button>
|
</svg>
|
||||||
<button class="chat-icon-btn" title="Очистить поле ввода" @click="clearInput">
|
</button>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
|
<button class="chat-icon-btn" title="Клавиатура" @click="handleKeyboardToggle">
|
||||||
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" fill="currentColor"/>
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
|
||||||
</svg>
|
<path d="M20 5H4c-1.1 0-1.99.9-1.99 2L2 17c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm-9 3h2v2h-2V8zm0 3h2v2h-2v-2zM8 8h2v2H8V8zm0 3h2v2H8v-2zm-1 2H5v-2h2v2zm0-3H5V8h2v2zm9 7H8v-2h8v2zm0-4h-2v-2h2v2zm0-3h-2V8h2v2zm3 3h-2v-2h2v2zm0-3h-2V8h2v2z" fill="currentColor"/>
|
||||||
</button>
|
</svg>
|
||||||
<button
|
</button>
|
||||||
class="chat-icon-btn send-button"
|
<button class="chat-icon-btn" title="Очистить поле ввода" @click="clearInput">
|
||||||
title="Отправить сообщение"
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
|
||||||
:disabled="isSendDisabled"
|
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" fill="currentColor"/>
|
||||||
@click="sendMessage"
|
</svg>
|
||||||
>
|
</button>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
|
<button
|
||||||
<path d="M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z" fill="currentColor"/>
|
class="chat-icon-btn send-button"
|
||||||
</svg>
|
title="Отправить сообщение"
|
||||||
</button>
|
:disabled="isSendDisabled"
|
||||||
<button v-if="props.canGenerateAI" class="chat-icon-btn ai-reply-btn" title="Сгенерировать ответ ІІ" @click="handleAiReply" :disabled="isAiLoading">
|
@click="sendMessage"
|
||||||
<template v-if="isAiLoading">
|
>
|
||||||
<svg class="ai-spinner" width="22" height="22" viewBox="0 0 50 50"><circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
|
||||||
</template>
|
<path d="M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z" fill="currentColor"/>
|
||||||
<template v-else>
|
</svg>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="10" rx="2"/><circle cx="12" cy="8" r="4"/><path d="M8 16v2M16 16v2"/></svg>
|
</button>
|
||||||
</template>
|
<button v-if="props.canGenerateAI" class="chat-icon-btn ai-reply-btn" title="Сгенерировать ответ ІІ" @click="handleAiReply" :disabled="isAiLoading">
|
||||||
</button>
|
<template v-if="isAiLoading">
|
||||||
</div>
|
<svg class="ai-spinner" width="22" height="22" viewBox="0 0 50 50"><circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle></svg>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="10" rx="2"/><circle cx="12" cy="8" r="4"/><path d="M8 16v2M16 16v2"/></svg>
|
||||||
|
</template>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="attachment-preview" v-if="localAttachments.length > 0">
|
<div class="attachment-preview" v-if="localAttachments.length > 0">
|
||||||
<div v-for="(file, index) in localAttachments" :key="index" class="preview-item">
|
<div v-for="(file, index) in localAttachments" :key="index" class="preview-item">
|
||||||
@@ -367,6 +389,13 @@ const clearInput = () => {
|
|||||||
nextTick(adjustTextareaHeight); // Сбросить высоту textarea
|
nextTick(adjustTextareaHeight); // Сбросить высоту textarea
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleKeyboardToggle = () => {
|
||||||
|
// Показываем виртуальную клавиатуру или переключаем режим
|
||||||
|
if (messageInputRef.value) {
|
||||||
|
messageInputRef.value.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// --- Отправка сообщения ---
|
// --- Отправка сообщения ---
|
||||||
const isSendDisabled = computed(() => {
|
const isSendDisabled = computed(() => {
|
||||||
return props.isLoading || !props.canSend || (!props.newMessage.trim() && localAttachments.value.length === 0);
|
return props.isLoading || !props.canSend || (!props.newMessage.trim() && localAttachments.value.length === 0);
|
||||||
@@ -384,6 +413,53 @@ const sendMessage = () => {
|
|||||||
nextTick(adjustTextareaHeight); // Сбросить высоту textarea после отправки
|
nextTick(adjustTextareaHeight); // Сбросить высоту textarea после отправки
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Изменение размера блоков ---
|
||||||
|
const messagesWidth = ref(70); // Начальная ширина блока истории (в процентах)
|
||||||
|
const inputWidth = ref(30); // Начальная ширина блока ввода (в процентах)
|
||||||
|
const isResizing = ref(false);
|
||||||
|
const resizeStartX = ref(0);
|
||||||
|
const resizeStartWidth = ref(0);
|
||||||
|
|
||||||
|
const startResize = (e) => {
|
||||||
|
isResizing.value = true;
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', handleResize);
|
||||||
|
document.addEventListener('mouseup', stopResize);
|
||||||
|
document.addEventListener('touchmove', handleResize);
|
||||||
|
document.addEventListener('touchend', stopResize);
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResize = (e) => {
|
||||||
|
if (!isResizing.value) return;
|
||||||
|
|
||||||
|
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
||||||
|
const chatContainer = document.querySelector('.chat-container');
|
||||||
|
if (!chatContainer) return;
|
||||||
|
|
||||||
|
const containerRect = chatContainer.getBoundingClientRect();
|
||||||
|
const containerWidth = containerRect.width;
|
||||||
|
const mouseX = clientX - containerRect.left; // Позиция курсора относительно левого края контейнера
|
||||||
|
|
||||||
|
// Вычисляем процент ширины блока истории от позиции курсора
|
||||||
|
const newMessagesWidth = (mouseX / containerWidth) * 100;
|
||||||
|
|
||||||
|
// Ограничиваем минимальную и максимальную ширину (от 20% до 80%)
|
||||||
|
const clampedWidth = Math.max(20, Math.min(80, newMessagesWidth));
|
||||||
|
|
||||||
|
messagesWidth.value = clampedWidth;
|
||||||
|
inputWidth.value = 100 - clampedWidth;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopResize = () => {
|
||||||
|
isResizing.value = false;
|
||||||
|
document.removeEventListener('mousemove', handleResize);
|
||||||
|
document.removeEventListener('mouseup', stopResize);
|
||||||
|
document.removeEventListener('touchmove', handleResize);
|
||||||
|
document.removeEventListener('touchend', stopResize);
|
||||||
|
};
|
||||||
|
|
||||||
// --- Прокрутка и UI ---
|
// --- Прокрутка и UI ---
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
if (messagesContainer.value) {
|
if (messagesContainer.value) {
|
||||||
@@ -529,33 +605,116 @@ async function handleAiReply() {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.chat-container {
|
.chat-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* На мобильных устройствах возвращаем вертикальный layout */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.chat-container {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-messages {
|
.chat-messages {
|
||||||
flex: 1 1 auto;
|
flex: 0 0 auto;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-bottom: 8px;
|
padding: var(--spacing-md) var(--spacing-md);
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-right: none;
|
||||||
|
/* Скрываем скроллбар, но сохраняем функциональность скролла */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
-ms-overflow-style: none; /* IE и Edge */
|
||||||
|
transition: width 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages::-webkit-scrollbar {
|
||||||
|
display: none; /* Chrome, Safari, Opera */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* На мобильных устройствах история занимает всё пространство */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.chat-messages {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 100% !important;
|
||||||
|
border-right: none;
|
||||||
|
padding: var(--spacing-md) var(--spacing-md) 8px;
|
||||||
|
/* Скрываем скроллбар и на мобильных */
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resizer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.resizer {
|
||||||
|
width: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
cursor: col-resize;
|
||||||
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resizer:hover {
|
||||||
|
background-color: var(--color-primary, #4CAF50);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resizer::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: -2px;
|
||||||
|
right: -2px;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-input {
|
.chat-input {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
flex: 0 0 auto;
|
||||||
margin-bottom: 12px;
|
margin: 0;
|
||||||
margin-top: 8px;
|
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
border-radius: 12px 12px 0 0;
|
border-radius: 0;
|
||||||
box-shadow: 0 -2px 8px rgba(0,0,0,0.04);
|
|
||||||
flex-shrink: 0;
|
|
||||||
min-height: 80px;
|
min-height: 80px;
|
||||||
|
height: 100%;
|
||||||
|
padding: var(--spacing-md) var(--spacing-md) 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: width 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* На мобильных устройствах блок ввода занимает всё пространство внизу */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.chat-input {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-top: none;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -563,80 +722,136 @@ async function handleAiReply() {
|
|||||||
|
|
||||||
.chat-input textarea {
|
.chat-input textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: #f5f5f5;
|
||||||
|
border-radius: 12px;
|
||||||
resize: none;
|
resize: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
font-size: var(--font-size-md);
|
font-size: var(--font-size-md);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
padding: var(--spacing-sm);
|
padding: 16px;
|
||||||
min-height: var(--chat-input-min-height, 40px);
|
margin: 0;
|
||||||
max-height: var(--chat-input-max-height, 120px);
|
|
||||||
transition: all var(--transition-fast);
|
transition: all var(--transition-fast);
|
||||||
color: var(--color-dark);
|
color: var(--color-dark);
|
||||||
overflow-y: hidden;
|
overflow-y: auto;
|
||||||
height: auto;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-input textarea:focus {
|
.chat-input textarea:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* На мобильных устройствах поле ввода меньше */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.chat-input textarea {
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
min-height: var(--chat-input-min-height, 40px);
|
||||||
|
max-height: var(--chat-input-max-height, 120px);
|
||||||
|
overflow-y: hidden;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-area {
|
.input-area {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-icons {
|
.chat-icons {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 6px;
|
gap: 8px;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--spacing-sm) 0;
|
||||||
|
border-top: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-icon-btn {
|
.chat-icon-btn {
|
||||||
width: 36px;
|
width: 40px;
|
||||||
height: 36px;
|
height: 40px;
|
||||||
border-radius: 50%;
|
padding: 0;
|
||||||
background: transparent;
|
border-radius: 8px;
|
||||||
border: none;
|
background: #f5f5f5;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all var(--transition-fast);
|
color: var(--color-dark, #333);
|
||||||
color: var(--color-grey);
|
transition: all 0.2s ease;
|
||||||
padding: 0;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
font-weight: 500;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-icon-btn:hover {
|
.chat-icon-btn svg {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-icon-btn:hover:not(:disabled) {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
background-color: #ffffff;
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-icon-btn:active:not(:disabled) {
|
||||||
|
transform: translateY(0);
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-icon-btn:disabled {
|
.chat-icon-btn:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-color: #e0e0e0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-icon-btn.send-button {
|
.chat-icon-btn.send-button {
|
||||||
background-color: var(--color-primary);
|
background-color: var(--color-primary, #4CAF50);
|
||||||
color: white;
|
color: white;
|
||||||
width: 36px;
|
border-color: var(--color-primary, #4CAF50);
|
||||||
height: 36px;
|
font-weight: 600;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-icon-btn.send-button:hover:not(:disabled) {
|
.chat-icon-btn.send-button:hover:not(:disabled) {
|
||||||
background-color: var(--color-primary-dark);
|
background-color: var(--color-primary-dark, #45a049);
|
||||||
|
border-color: var(--color-primary-dark, #45a049);
|
||||||
color: white;
|
color: white;
|
||||||
transform: scale(1.05);
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 6px rgba(76, 175, 80, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-icon-btn.send-button:active:not(:disabled) {
|
||||||
|
transform: translateY(0);
|
||||||
|
box-shadow: 0 1px 3px rgba(76, 175, 80, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-icon-btn.send-button:disabled {
|
.chat-icon-btn.send-button:disabled {
|
||||||
background-color: #ccc;
|
background-color: #ccc;
|
||||||
|
border-color: #ccc;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -666,11 +881,12 @@ async function handleAiReply() {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-top: 8px;
|
margin-top: auto;
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
border-top: 1px solid var(--color-grey-light);
|
border-top: 1px solid var(--color-grey-light);
|
||||||
max-height: 100px;
|
max-height: 200px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-item {
|
.preview-item {
|
||||||
@@ -725,11 +941,12 @@ async function handleAiReply() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-messages {
|
.chat-messages {
|
||||||
padding: var(--spacing-md);
|
padding: var(--spacing-md) var(--spacing-md) 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-input {
|
.chat-input {
|
||||||
padding: var(--spacing-xs) var(--spacing-sm);
|
padding: var(--spacing-xs) var(--spacing-sm);
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-icon-btn {
|
.chat-icon-btn {
|
||||||
@@ -753,7 +970,7 @@ async function handleAiReply() {
|
|||||||
border-top: 1px solid #eee !important;
|
border-top: 1px solid #eee !important;
|
||||||
}
|
}
|
||||||
.chat-messages {
|
.chat-messages {
|
||||||
padding: var(--spacing-md) !important;
|
padding: var(--spacing-md) var(--spacing-md) 8px !important;
|
||||||
overflow-y: auto !important;
|
overflow-y: auto !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -768,7 +985,7 @@ async function handleAiReply() {
|
|||||||
border-top: 1px solid #eee !important;
|
border-top: 1px solid #eee !important;
|
||||||
}
|
}
|
||||||
.chat-messages {
|
.chat-messages {
|
||||||
padding: var(--spacing-md) !important;
|
padding: var(--spacing-md) var(--spacing-md) 8px !important;
|
||||||
overflow-y: auto !important;
|
overflow-y: auto !important;
|
||||||
}
|
}
|
||||||
.chat-container {
|
.chat-container {
|
||||||
|
|||||||
@@ -14,6 +14,17 @@
|
|||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
<div class="header-text">
|
<div class="header-text">
|
||||||
|
<div v-if="dleDisplayName" class="footer-dle-info">
|
||||||
|
<img
|
||||||
|
v-if="footerDle?.logoURI"
|
||||||
|
:src="footerDle.logoURI"
|
||||||
|
:alt="dleDisplayName.name"
|
||||||
|
class="footer-dle-logo"
|
||||||
|
@error="handleLogoError"
|
||||||
|
/>
|
||||||
|
<div v-else class="footer-dle-logo-placeholder">DLE</div>
|
||||||
|
<span class="dle-name">{{ dleDisplayName.name }} ({{ dleDisplayName.symbol }})</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="header-wallet-btn"
|
class="header-wallet-btn"
|
||||||
@@ -27,8 +38,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, defineEmits, onMounted, onBeforeUnmount, watch } from 'vue';
|
import { defineProps, defineEmits, onMounted, onBeforeUnmount, watch, computed } from 'vue';
|
||||||
import { useAuthContext } from '../composables/useAuth';
|
import { useAuthContext } from '../composables/useAuth';
|
||||||
|
import { useFooterDle } from '../composables/useFooterDle';
|
||||||
import eventBus from '../utils/eventBus';
|
import eventBus from '../utils/eventBus';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -48,8 +60,44 @@ const toggleSidebar = () => {
|
|||||||
const auth = useAuthContext();
|
const auth = useAuthContext();
|
||||||
const { isAuthenticated } = auth;
|
const { isAuthenticated } = auth;
|
||||||
|
|
||||||
|
// Используем composable для выбранного DLE
|
||||||
|
const { footerDle } = useFooterDle();
|
||||||
|
|
||||||
|
// Вычисляемое свойство для отображения названия
|
||||||
|
const dleDisplayName = computed(() => {
|
||||||
|
if (!footerDle.value || !footerDle.value.name || !footerDle.value.symbol) return null;
|
||||||
|
// Проверяем, что это не fallback данные (не начинается с "DLE " и адресом)
|
||||||
|
if (footerDle.value.name.startsWith('DLE ') && footerDle.value.name.includes('...')) {
|
||||||
|
return null; // Не показываем fallback данные
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: footerDle.value.name,
|
||||||
|
symbol: footerDle.value.symbol
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработка ошибки загрузки логотипа
|
||||||
|
const handleLogoError = (event) => {
|
||||||
|
console.log('[Header] Ошибка загрузки логотипа:', event.target.src);
|
||||||
|
event.target.style.display = 'none';
|
||||||
|
// Показываем placeholder, если его нет
|
||||||
|
const infoContainer = event.target.closest('.footer-dle-info');
|
||||||
|
if (infoContainer) {
|
||||||
|
let placeholder = infoContainer.querySelector('.footer-dle-logo-placeholder');
|
||||||
|
if (!placeholder) {
|
||||||
|
placeholder = document.createElement('div');
|
||||||
|
placeholder.className = 'footer-dle-logo-placeholder';
|
||||||
|
placeholder.textContent = 'DLE';
|
||||||
|
infoContainer.insertBefore(placeholder, event.target);
|
||||||
|
}
|
||||||
|
placeholder.style.display = 'flex';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Мониторинг изменений статуса аутентификации
|
// Мониторинг изменений статуса аутентификации
|
||||||
let unwatch = null;
|
let unwatch = null;
|
||||||
|
let refreshInterval = null;
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// Следим за изменениями авторизации и сообщаем о них через eventBus
|
// Следим за изменениями авторизации и сообщаем о них через eventBus
|
||||||
unwatch = watch(isAuthenticated, (newValue, oldValue) => {
|
unwatch = watch(isAuthenticated, (newValue, oldValue) => {
|
||||||
@@ -63,17 +111,14 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Подписываемся на централизованные события очистки и обновления данных
|
// Обновляем данные DLE из блокчейна периодически (каждые 5 минут)
|
||||||
window.addEventListener('clear-application-data', () => {
|
const { refreshFooterDle } = useFooterDle();
|
||||||
console.log('[Header] Clearing header data');
|
refreshInterval = setInterval(() => {
|
||||||
// Очищаем данные при выходе из системы
|
refreshFooterDle();
|
||||||
// Header не нуждается в очистке данных
|
}, 5 * 60 * 1000); // 5 минут
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('refresh-application-data', () => {
|
// НЕ очищаем footerDle при отключении кошелька, так как это глобальная настройка,
|
||||||
console.log('[Header] Refreshing header data');
|
// не связанная с пользовательским кошельком
|
||||||
// Header не нуждается в обновлении данных
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Очищаем наблюдатель при удалении компонента
|
// Очищаем наблюдатель при удалении компонента
|
||||||
@@ -81,6 +126,10 @@ onBeforeUnmount(() => {
|
|||||||
if (unwatch) {
|
if (unwatch) {
|
||||||
unwatch();
|
unwatch();
|
||||||
}
|
}
|
||||||
|
// Очищаем интервал обновления
|
||||||
|
if (refreshInterval) {
|
||||||
|
clearInterval(refreshInterval);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -102,6 +151,45 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
.header-text {
|
.header-text {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-dle-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-dle-logo {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 6px;
|
||||||
|
object-fit: contain;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-dle-logo-placeholder {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: linear-gradient(135deg, var(--color-primary), #0056b3);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 12px;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dle-name {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
<h4 class="consent-document-title">{{ doc.title }}</h4>
|
<h4 class="consent-document-title">{{ doc.title }}</h4>
|
||||||
<p v-if="doc.summary" class="consent-document-summary">{{ doc.summary }}</p>
|
<p v-if="doc.summary" class="consent-document-summary">{{ doc.summary }}</p>
|
||||||
<a
|
<a
|
||||||
:href="`/content/published/${doc.id}`"
|
:href="`/public/page/${doc.id}`"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="consent-document-link"
|
class="consent-document-link"
|
||||||
@click.stop
|
@click.stop
|
||||||
@@ -371,7 +371,6 @@ function copyEmail(email) {
|
|||||||
max-width: 75%;
|
max-width: 75%;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
position: relative;
|
position: relative;
|
||||||
box-shadow: var(--shadow-sm);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-message {
|
.user-message {
|
||||||
@@ -603,7 +602,7 @@ function copyEmail(email) {
|
|||||||
|
|
||||||
.consent-document-item:hover {
|
.consent-document-item:hover {
|
||||||
border-color: var(--color-primary, #007bff);
|
border-color: var(--color-primary, #007bff);
|
||||||
box-shadow: 0 2px 8px rgba(0, 123, 255, 0.1);
|
background: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.consent-document-item:last-child {
|
.consent-document-item:last-child {
|
||||||
@@ -718,7 +717,6 @@ function copyEmail(email) {
|
|||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
max-width: 70%;
|
max-width: 70%;
|
||||||
border-radius: 18px 18px 4px 18px;
|
border-radius: 18px 18px 4px 18px;
|
||||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.private-other-user {
|
.private-other-user {
|
||||||
@@ -728,7 +726,6 @@ function copyEmail(email) {
|
|||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
max-width: 70%;
|
max-width: 70%;
|
||||||
border-radius: 18px 18px 18px 4px;
|
border-radius: 18px 18px 18px 4px;
|
||||||
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Анимация появления сообщений */
|
/* Анимация появления сообщений */
|
||||||
|
|||||||
160
frontend/src/composables/useFooterDle.js
Normal file
160
frontend/src/composables/useFooterDle.js
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
/**
|
||||||
|
* 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/VC-HB3-Accelerator
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ref, provide, inject } from 'vue';
|
||||||
|
import api from '../api/axios';
|
||||||
|
import { getFromStorage, setToStorage } from '../utils/storage';
|
||||||
|
|
||||||
|
// === SINGLETON STATE ===
|
||||||
|
const footerDle = ref(null);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const selectedDleAddress = ref(null);
|
||||||
|
|
||||||
|
// Загрузка адреса выбранного DLE из localStorage при инициализации
|
||||||
|
function loadSavedDleAddress() {
|
||||||
|
try {
|
||||||
|
const savedAddress = getFromStorage('footerDleAddress', null);
|
||||||
|
if (savedAddress) {
|
||||||
|
selectedDleAddress.value = savedAddress;
|
||||||
|
// Загружаем актуальные данные из блокчейна
|
||||||
|
loadDleFromBlockchain(savedAddress).then((loadedDle) => {
|
||||||
|
if (loadedDle) {
|
||||||
|
footerDle.value = loadedDle;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[useFooterDle] Ошибка при загрузке адреса из localStorage:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализация при загрузке модуля
|
||||||
|
loadSavedDleAddress();
|
||||||
|
|
||||||
|
// === API ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Загружает данные DLE из блокчейна по адресу
|
||||||
|
*/
|
||||||
|
async function loadDleFromBlockchain(dleAddress) {
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
const response = await api.post('/blockchain/read-dle-info', {
|
||||||
|
dleAddress: dleAddress
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
const blockchainData = response.data.data;
|
||||||
|
return {
|
||||||
|
address: dleAddress,
|
||||||
|
name: blockchainData.name || '',
|
||||||
|
symbol: blockchainData.symbol || '',
|
||||||
|
logoURI: blockchainData.logoURI || ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[useFooterDle] Ошибка при загрузке DLE из блокчейна:', error);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Устанавливает выбранный DLE для отображения в футере
|
||||||
|
* Сохраняет только адрес, данные всегда загружаются из блокчейна
|
||||||
|
* @param {string} dleAddress - Адрес DLE
|
||||||
|
*/
|
||||||
|
async function setFooterDle(dleAddress) {
|
||||||
|
if (!dleAddress) {
|
||||||
|
footerDle.value = null;
|
||||||
|
selectedDleAddress.value = null;
|
||||||
|
// Удаляем из localStorage
|
||||||
|
try {
|
||||||
|
setToStorage('footerDleAddress', null);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[useFooterDle] Ошибка при удалении из localStorage:', error);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем только адрес
|
||||||
|
selectedDleAddress.value = dleAddress;
|
||||||
|
setToStorage('footerDleAddress', dleAddress);
|
||||||
|
|
||||||
|
// Всегда загружаем актуальные данные из блокчейна
|
||||||
|
const loadedDle = await loadDleFromBlockchain(dleAddress);
|
||||||
|
if (loadedDle) {
|
||||||
|
footerDle.value = loadedDle;
|
||||||
|
} else {
|
||||||
|
// Если не удалось загрузить, очищаем состояние
|
||||||
|
footerDle.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обновляет данные выбранного DLE из блокчейна
|
||||||
|
* Используется для периодического обновления или при необходимости
|
||||||
|
*/
|
||||||
|
async function refreshFooterDle() {
|
||||||
|
if (!selectedDleAddress.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadedDle = await loadDleFromBlockchain(selectedDleAddress.value);
|
||||||
|
if (loadedDle) {
|
||||||
|
footerDle.value = loadedDle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Очищает выбранный DLE
|
||||||
|
*/
|
||||||
|
function clearFooterDle() {
|
||||||
|
footerDle.value = null;
|
||||||
|
selectedDleAddress.value = null;
|
||||||
|
// Удаляем адрес из localStorage
|
||||||
|
try {
|
||||||
|
setToStorage('footerDleAddress', null);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[useFooterDle] Ошибка при очистке localStorage:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === SINGLETON API ===
|
||||||
|
const footerDleApi = {
|
||||||
|
footerDle,
|
||||||
|
isLoading,
|
||||||
|
selectedDleAddress,
|
||||||
|
setFooterDle,
|
||||||
|
clearFooterDle,
|
||||||
|
refreshFooterDle,
|
||||||
|
loadDleFromBlockchain
|
||||||
|
};
|
||||||
|
|
||||||
|
// === PROVIDE/INJECT HELPERS ===
|
||||||
|
const FOOTER_DLE_KEY = Symbol('footerDle');
|
||||||
|
|
||||||
|
export function provideFooterDle() {
|
||||||
|
provide(FOOTER_DLE_KEY, footerDleApi);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFooterDle() {
|
||||||
|
const ctx = inject(FOOTER_DLE_KEY);
|
||||||
|
if (!ctx) {
|
||||||
|
// Если контекст не предоставлен, возвращаем singleton напрямую
|
||||||
|
return footerDleApi;
|
||||||
|
}
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ export async function connectWithWallet() {
|
|||||||
const docsResponse = await axios.get('/consent/documents');
|
const docsResponse = await axios.get('/consent/documents');
|
||||||
if (docsResponse.data && docsResponse.data.length > 0) {
|
if (docsResponse.data && docsResponse.data.length > 0) {
|
||||||
docsResponse.data.forEach(doc => {
|
docsResponse.data.forEach(doc => {
|
||||||
resources.push(`${window.location.origin}/content/published/${doc.id}`);
|
resources.push(`${window.location.origin}/public/page/${doc.id}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export const connectWallet = async () => {
|
|||||||
const docsResponse = await axios.get('/consent/documents');
|
const docsResponse = await axios.get('/consent/documents');
|
||||||
if (docsResponse.data && docsResponse.data.length > 0) {
|
if (docsResponse.data && docsResponse.data.length > 0) {
|
||||||
docsResponse.data.forEach(doc => {
|
docsResponse.data.forEach(doc => {
|
||||||
resources.push(`${window.location.origin}/content/published/${doc.id}`);
|
resources.push(`${window.location.origin}/public/page/${doc.id}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -197,8 +197,6 @@
|
|||||||
.chat-wrapper {
|
.chat-wrapper {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|||||||
@@ -2970,6 +2970,9 @@ const handleDeploymentCompleted = (result) => {
|
|||||||
console.log('🎉 Поэтапный деплой завершен:', result);
|
console.log('🎉 Поэтапный деплой завершен:', result);
|
||||||
showDeploymentWizard.value = false;
|
showDeploymentWizard.value = false;
|
||||||
|
|
||||||
|
// Эмитируем событие о завершении деплоя для обновления Header
|
||||||
|
eventBus.emit('dle-deployed', result);
|
||||||
|
|
||||||
// Перенаправляем на главную страницу управления
|
// Перенаправляем на главную страницу управления
|
||||||
router.push('/management');
|
router.push('/management');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,6 +32,53 @@
|
|||||||
|
|
||||||
<!-- Основной контент -->
|
<!-- Основной контент -->
|
||||||
<div v-if="dleInfo" class="main-content">
|
<div v-if="dleInfo" class="main-content">
|
||||||
|
<!-- Отображение в футере -->
|
||||||
|
<div v-if="canSetFooterDle" class="footer-card">
|
||||||
|
<div class="footer-header">
|
||||||
|
<h3>Отображение в футере</h3>
|
||||||
|
</div>
|
||||||
|
<div class="footer-content">
|
||||||
|
<p>Выберите этот DLE для отображения в футере приложения. Название будет показано в строке с кнопкой бургера.</p>
|
||||||
|
<div v-if="isSelectedForFooter" class="selected-info">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
<span>Этот DLE отображается в футере</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="hasFooterDle" class="other-selected-info">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
<span>В футере отображается другой DLE: {{ footerDle.value?.name }} ({{ footerDle.value?.symbol }})</span>
|
||||||
|
</div>
|
||||||
|
<div class="footer-actions">
|
||||||
|
<button
|
||||||
|
v-if="!isSelectedForFooter"
|
||||||
|
@click="setAsFooterDle"
|
||||||
|
class="btn-primary"
|
||||||
|
:disabled="isLoading"
|
||||||
|
>
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
Отображать в футере
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="isSelectedForFooter"
|
||||||
|
@click="removeFromFooter"
|
||||||
|
class="btn-danger"
|
||||||
|
:disabled="isLoading"
|
||||||
|
>
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
Удалить из футера
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="hasFooterDle && !isSelectedForFooter"
|
||||||
|
@click="removeFromFooter"
|
||||||
|
class="btn-danger btn-sm"
|
||||||
|
:disabled="isLoading"
|
||||||
|
>
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
Удалить из футера
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Удаление DLE -->
|
<!-- Удаление DLE -->
|
||||||
<div class="danger-card">
|
<div class="danger-card">
|
||||||
<div class="danger-header">
|
<div class="danger-header">
|
||||||
@@ -68,9 +115,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, defineProps, defineEmits, onMounted } from 'vue';
|
import { ref, defineProps, defineEmits, onMounted, computed } from 'vue';
|
||||||
import { useRouter, useRoute } from 'vue-router';
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
import { useAuthContext } from '../../composables/useAuth';
|
import { useAuthContext } from '../../composables/useAuth';
|
||||||
|
import { useFooterDle } from '../../composables/useFooterDle';
|
||||||
|
import { usePermissions } from '../../composables/usePermissions';
|
||||||
|
import { ROLES } from '../../composables/permissions';
|
||||||
import BaseLayout from '../../components/BaseLayout.vue';
|
import BaseLayout from '../../components/BaseLayout.vue';
|
||||||
import { deactivateDLE } from '../../utils/dle-contract.js';
|
import { deactivateDLE } from '../../utils/dle-contract.js';
|
||||||
import api from '../../api/axios';
|
import api from '../../api/axios';
|
||||||
@@ -110,6 +160,74 @@ const goBackToBlocks = () => {
|
|||||||
// Получаем адрес пользователя из контекста аутентификации
|
// Получаем адрес пользователя из контекста аутентификации
|
||||||
const { address: userAddress } = useAuthContext();
|
const { address: userAddress } = useAuthContext();
|
||||||
|
|
||||||
|
// Используем composable для проверки прав доступа
|
||||||
|
const { currentRole } = usePermissions();
|
||||||
|
|
||||||
|
// Используем composable для выбранного DLE
|
||||||
|
const { footerDle, setFooterDle, clearFooterDle } = useFooterDle();
|
||||||
|
|
||||||
|
// Проверяем, может ли пользователь устанавливать DLE для футера (только редактор)
|
||||||
|
const canSetFooterDle = computed(() => {
|
||||||
|
return currentRole.value === ROLES.EDITOR;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Проверяем, выбран ли этот DLE для отображения в футере
|
||||||
|
const isSelectedForFooter = computed(() => {
|
||||||
|
if (!address || !footerDle.value) return false;
|
||||||
|
// Сравниваем адреса в нижнем регистре для надежности
|
||||||
|
return footerDle.value.address && footerDle.value.address.toLowerCase() === address.toLowerCase();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Проверяем, есть ли какой-либо DLE в футере
|
||||||
|
const hasFooterDle = computed(() => {
|
||||||
|
return footerDle.value !== null && footerDle.value.address !== null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Устанавливает выбранный DLE для отображения в футере
|
||||||
|
const setAsFooterDle = async () => {
|
||||||
|
// Проверяем права доступа (только редактор может устанавливать DLE для футера)
|
||||||
|
if (!canSetFooterDle.value) {
|
||||||
|
alert('❌ Только пользователи с ролью редактор могут устанавливать DLE для отображения в футере');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dleInfo.value || !address) {
|
||||||
|
alert('Информация о DLE не загружена');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Устанавливаем адрес, данные будут загружены из блокчейна
|
||||||
|
await setFooterDle(address);
|
||||||
|
|
||||||
|
alert(`✅ DLE "${dleInfo.value.name} (${dleInfo.value.symbol})" теперь отображается в футере приложения`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при установке выбранного DLE:', error);
|
||||||
|
alert('❌ Не удалось установить выбранный DLE');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Удаляет DLE из футера
|
||||||
|
const removeFromFooter = async () => {
|
||||||
|
// Проверяем права доступа (только редактор может удалять DLE из футера)
|
||||||
|
if (!canSetFooterDle.value) {
|
||||||
|
alert('❌ Только пользователи с ролью редактор могут удалять DLE из футера');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm('Вы уверены, что хотите удалить этот DLE из футера?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
clearFooterDle();
|
||||||
|
alert('✅ DLE удален из футера приложения');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при удалении DLE из футера:', error);
|
||||||
|
alert('❌ Не удалось удалить DLE из футера');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Подписываемся на централизованные события очистки и обновления данных
|
// Подписываемся на централизованные события очистки и обновления данных
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('clear-application-data', () => {
|
window.addEventListener('clear-application-data', () => {
|
||||||
@@ -136,7 +254,7 @@ const loadDLEInfo = async () => {
|
|||||||
console.log('Загружаем информацию о DLE:', address);
|
console.log('Загружаем информацию о DLE:', address);
|
||||||
|
|
||||||
// Загружаем данные DLE из блокчейна через API
|
// Загружаем данные DLE из блокчейна через API
|
||||||
const response = await api.post('/dle-core/read-dle-info', {
|
const response = await api.post('/blockchain/read-dle-info', {
|
||||||
dleAddress: address
|
dleAddress: address
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -147,7 +265,8 @@ const loadDLEInfo = async () => {
|
|||||||
dleInfo.value = {
|
dleInfo.value = {
|
||||||
name: dleData.name, // Название DLE из блокчейна
|
name: dleData.name, // Название DLE из блокчейна
|
||||||
symbol: dleData.symbol, // Символ DLE из блокчейна
|
symbol: dleData.symbol, // Символ DLE из блокчейна
|
||||||
address: dleData.dleAddress || address // Адрес из API или из URL
|
address: dleData.dleAddress || address, // Адрес из API или из URL
|
||||||
|
logoURI: dleData.logoURI || '' // URL логотипа
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
console.error('Ошибка загрузки DLE:', response.data.error);
|
console.error('Ошибка загрузки DLE:', response.data.error);
|
||||||
@@ -156,12 +275,9 @@ const loadDLEInfo = async () => {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при загрузке информации о DLE:', error);
|
console.error('Ошибка при загрузке информации о DLE:', error);
|
||||||
// В случае ошибки показываем базовую информацию
|
// В случае ошибки НЕ устанавливаем fallback данные, оставляем null
|
||||||
dleInfo.value = {
|
// чтобы не показывать некорректную информацию
|
||||||
name: 'DLE ' + address.slice(0, 8) + '...',
|
dleInfo.value = null;
|
||||||
symbol: 'DLE',
|
|
||||||
address: address
|
|
||||||
};
|
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
@@ -302,6 +418,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Карточки */
|
/* Карточки */
|
||||||
|
.footer-card,
|
||||||
.danger-card {
|
.danger-card {
|
||||||
background: white;
|
background: white;
|
||||||
border: 1px solid #e9ecef;
|
border: 1px solid #e9ecef;
|
||||||
@@ -309,6 +426,75 @@ onMounted(() => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footer-header {
|
||||||
|
background: #f0f7ff;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-header h3 {
|
||||||
|
color: var(--color-primary);
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content p {
|
||||||
|
color: var(--color-grey-dark);
|
||||||
|
margin-bottom: 15px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: #e6f7e6;
|
||||||
|
border: 1px solid #b3e5b3;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #2d5a2d;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-info i {
|
||||||
|
color: #28a745;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.other-selected-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: #fff3cd;
|
||||||
|
border: 1px solid #ffeaa7;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #856404;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.other-selected-info i {
|
||||||
|
color: #ffc107;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
.danger-header {
|
.danger-header {
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
padding: 15px 20px;
|
padding: 15px 20px;
|
||||||
|
|||||||
Reference in New Issue
Block a user