feat: новая функция
This commit is contained in:
@@ -42,6 +42,7 @@
|
||||
import { ref, watch, onMounted, computed, onUnmounted } from 'vue';
|
||||
import { RouterView } from 'vue-router';
|
||||
import { useAuth, provideAuth } from './composables/useAuth';
|
||||
import { provideFooterDle } from './composables/useFooterDle';
|
||||
import { useTokenBalancesWebSocket } from './composables/useTokenBalancesWebSocket';
|
||||
import eventBus from './utils/eventBus';
|
||||
import wsClient from './utils/websocket';
|
||||
@@ -64,6 +65,8 @@
|
||||
// --- Логика загрузки баланса токенов через WebSocket ---
|
||||
// Предоставляем auth контекст
|
||||
provideAuth();
|
||||
// Предоставляем контекст для выбранного DLE в футере
|
||||
provideFooterDle();
|
||||
|
||||
// Инициализируем WebSocket composable
|
||||
const {
|
||||
|
||||
@@ -12,7 +12,13 @@
|
||||
|
||||
<template>
|
||||
<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) }]">
|
||||
<template v-if="props.canSelectMessages">
|
||||
<input type="checkbox" class="admin-select-checkbox" :checked="selectedMessageIds.includes(message.id)" @change="() => toggleSelectMessage(message.id)" />
|
||||
@@ -26,7 +32,19 @@
|
||||
</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">
|
||||
<textarea
|
||||
ref="messageInputRef"
|
||||
@@ -34,69 +52,73 @@
|
||||
@input="handleInput"
|
||||
placeholder="Введите сообщение..."
|
||||
:disabled="isLoading || !props.canSend"
|
||||
rows="1"
|
||||
autofocus
|
||||
@keydown.enter.prevent="sendMessage"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
/>
|
||||
<div class="chat-icons">
|
||||
<button
|
||||
class="chat-icon-btn"
|
||||
title="Удерживайте для записи аудио"
|
||||
@mousedown="startAudioRecording"
|
||||
@mouseup="stopAudioRecording"
|
||||
@mouseleave="stopAudioRecording"
|
||||
: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"/>
|
||||
<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"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="chat-icon-btn"
|
||||
title="Удерживайте для записи видео"
|
||||
@mousedown="startVideoRecording"
|
||||
@mouseup="stopVideoRecording"
|
||||
@mouseleave="stopVideoRecording"
|
||||
: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>
|
||||
</button>
|
||||
<button class="chat-icon-btn" title="Прикрепить файл" @click="handleFileUpload" :disabled="!props.canSend">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
|
||||
<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>
|
||||
</button>
|
||||
<button class="chat-icon-btn" title="Очистить поле ввода" @click="clearInput">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
|
||||
<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>
|
||||
</button>
|
||||
<button
|
||||
class="chat-icon-btn send-button"
|
||||
title="Отправить сообщение"
|
||||
:disabled="isSendDisabled"
|
||||
@click="sendMessage"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
|
||||
<path d="M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button v-if="props.canGenerateAI" class="chat-icon-btn ai-reply-btn" title="Сгенерировать ответ ІІ" @click="handleAiReply" :disabled="isAiLoading">
|
||||
<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>
|
||||
</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="chat-icons">
|
||||
<button
|
||||
class="chat-icon-btn"
|
||||
title="Удерживайте для записи аудио"
|
||||
@mousedown="startAudioRecording"
|
||||
@mouseup="stopAudioRecording"
|
||||
@mouseleave="stopAudioRecording"
|
||||
: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"/>
|
||||
<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"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="chat-icon-btn"
|
||||
title="Удерживайте для записи видео"
|
||||
@mousedown="startVideoRecording"
|
||||
@mouseup="stopVideoRecording"
|
||||
@mouseleave="stopVideoRecording"
|
||||
: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>
|
||||
</button>
|
||||
<button class="chat-icon-btn" title="Прикрепить файл" @click="handleFileUpload" :disabled="!props.canSend">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
|
||||
<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>
|
||||
</button>
|
||||
<button class="chat-icon-btn" title="Клавиатура" @click="handleKeyboardToggle">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
|
||||
<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"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="chat-icon-btn" title="Очистить поле ввода" @click="clearInput">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
|
||||
<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>
|
||||
</button>
|
||||
<button
|
||||
class="chat-icon-btn send-button"
|
||||
title="Отправить сообщение"
|
||||
:disabled="isSendDisabled"
|
||||
@click="sendMessage"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
|
||||
<path d="M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button v-if="props.canGenerateAI" class="chat-icon-btn ai-reply-btn" title="Сгенерировать ответ ІІ" @click="handleAiReply" :disabled="isAiLoading">
|
||||
<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>
|
||||
</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 class="attachment-preview" v-if="localAttachments.length > 0">
|
||||
<div v-for="(file, index) in localAttachments" :key="index" class="preview-item">
|
||||
@@ -367,6 +389,13 @@ const clearInput = () => {
|
||||
nextTick(adjustTextareaHeight); // Сбросить высоту textarea
|
||||
};
|
||||
|
||||
const handleKeyboardToggle = () => {
|
||||
// Показываем виртуальную клавиатуру или переключаем режим
|
||||
if (messageInputRef.value) {
|
||||
messageInputRef.value.focus();
|
||||
}
|
||||
};
|
||||
|
||||
// --- Отправка сообщения ---
|
||||
const isSendDisabled = computed(() => {
|
||||
return props.isLoading || !props.canSend || (!props.newMessage.trim() && localAttachments.value.length === 0);
|
||||
@@ -384,6 +413,53 @@ const sendMessage = () => {
|
||||
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 ---
|
||||
const scrollToBottom = () => {
|
||||
if (messagesContainer.value) {
|
||||
@@ -529,33 +605,116 @@ async function handleAiReply() {
|
||||
<style scoped>
|
||||
.chat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
/* На мобильных устройствах возвращаем вертикальный layout */
|
||||
@media (max-width: 1024px) {
|
||||
.chat-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1 1 auto;
|
||||
flex: 0 0 auto;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
padding-bottom: 8px;
|
||||
padding: var(--spacing-md) var(--spacing-md);
|
||||
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 {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-bottom: 12px;
|
||||
margin-top: 8px;
|
||||
flex: 0 0 auto;
|
||||
margin: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-radius: 12px 12px 0 0;
|
||||
box-shadow: 0 -2px 8px rgba(0,0,0,0.04);
|
||||
flex-shrink: 0;
|
||||
border-radius: 0;
|
||||
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 {
|
||||
width: 100%;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
background: #f5f5f5;
|
||||
border-radius: 12px;
|
||||
resize: none;
|
||||
outline: none;
|
||||
font-size: var(--font-size-md);
|
||||
line-height: 1.5;
|
||||
padding: var(--spacing-sm);
|
||||
min-height: var(--chat-input-min-height, 40px);
|
||||
max-height: var(--chat-input-max-height, 120px);
|
||||
padding: 16px;
|
||||
margin: 0;
|
||||
transition: all var(--transition-fast);
|
||||
color: var(--color-dark);
|
||||
overflow-y: hidden;
|
||||
height: auto;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.chat-input textarea:focus {
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: var(--spacing-sm);
|
||||
width: 100%;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-icons {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: nowrap;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
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 {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
color: var(--color-grey);
|
||||
padding: 0;
|
||||
color: var(--color-dark, #333);
|
||||
transition: all 0.2s ease;
|
||||
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);
|
||||
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 {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
background: #f5f5f5;
|
||||
border-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.chat-icon-btn.send-button {
|
||||
background-color: var(--color-primary);
|
||||
background-color: var(--color-primary, #4CAF50);
|
||||
color: white;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-color: var(--color-primary, #4CAF50);
|
||||
font-weight: 600;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.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;
|
||||
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 {
|
||||
background-color: #ccc;
|
||||
border-color: #ccc;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@@ -666,11 +881,12 @@ async function handleAiReply() {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
margin-top: auto;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--color-grey-light);
|
||||
max-height: 100px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
@@ -725,11 +941,12 @@ async function handleAiReply() {
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
padding: var(--spacing-md);
|
||||
padding: var(--spacing-md) var(--spacing-md) 8px;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.chat-icon-btn {
|
||||
@@ -753,7 +970,7 @@ async function handleAiReply() {
|
||||
border-top: 1px solid #eee !important;
|
||||
}
|
||||
.chat-messages {
|
||||
padding: var(--spacing-md) !important;
|
||||
padding: var(--spacing-md) var(--spacing-md) 8px !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
}
|
||||
@@ -768,7 +985,7 @@ async function handleAiReply() {
|
||||
border-top: 1px solid #eee !important;
|
||||
}
|
||||
.chat-messages {
|
||||
padding: var(--spacing-md) !important;
|
||||
padding: var(--spacing-md) var(--spacing-md) 8px !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
.chat-container {
|
||||
|
||||
@@ -14,6 +14,17 @@
|
||||
<div class="header">
|
||||
<div class="header-content">
|
||||
<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>
|
||||
<button
|
||||
class="header-wallet-btn"
|
||||
@@ -27,8 +38,9 @@
|
||||
</template>
|
||||
|
||||
<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 { useFooterDle } from '../composables/useFooterDle';
|
||||
import eventBus from '../utils/eventBus';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -48,8 +60,44 @@ const toggleSidebar = () => {
|
||||
const auth = useAuthContext();
|
||||
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 refreshInterval = null;
|
||||
|
||||
onMounted(() => {
|
||||
// Следим за изменениями авторизации и сообщаем о них через eventBus
|
||||
unwatch = watch(isAuthenticated, (newValue, oldValue) => {
|
||||
@@ -63,17 +111,14 @@ onMounted(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// Подписываемся на централизованные события очистки и обновления данных
|
||||
window.addEventListener('clear-application-data', () => {
|
||||
console.log('[Header] Clearing header data');
|
||||
// Очищаем данные при выходе из системы
|
||||
// Header не нуждается в очистке данных
|
||||
});
|
||||
// Обновляем данные DLE из блокчейна периодически (каждые 5 минут)
|
||||
const { refreshFooterDle } = useFooterDle();
|
||||
refreshInterval = setInterval(() => {
|
||||
refreshFooterDle();
|
||||
}, 5 * 60 * 1000); // 5 минут
|
||||
|
||||
window.addEventListener('refresh-application-data', () => {
|
||||
console.log('[Header] Refreshing header data');
|
||||
// Header не нуждается в обновлении данных
|
||||
});
|
||||
// НЕ очищаем footerDle при отключении кошелька, так как это глобальная настройка,
|
||||
// не связанная с пользовательским кошельком
|
||||
});
|
||||
|
||||
// Очищаем наблюдатель при удалении компонента
|
||||
@@ -81,6 +126,10 @@ onBeforeUnmount(() => {
|
||||
if (unwatch) {
|
||||
unwatch();
|
||||
}
|
||||
// Очищаем интервал обновления
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -102,6 +151,45 @@ onBeforeUnmount(() => {
|
||||
|
||||
.header-text {
|
||||
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 {
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
<h4 class="consent-document-title">{{ doc.title }}</h4>
|
||||
<p v-if="doc.summary" class="consent-document-summary">{{ doc.summary }}</p>
|
||||
<a
|
||||
:href="`/content/published/${doc.id}`"
|
||||
:href="`/public/page/${doc.id}`"
|
||||
target="_blank"
|
||||
class="consent-document-link"
|
||||
@click.stop
|
||||
@@ -371,7 +371,6 @@ function copyEmail(email) {
|
||||
max-width: 75%;
|
||||
word-wrap: break-word;
|
||||
position: relative;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.user-message {
|
||||
@@ -603,7 +602,7 @@ function copyEmail(email) {
|
||||
|
||||
.consent-document-item:hover {
|
||||
border-color: var(--color-primary, #007bff);
|
||||
box-shadow: 0 2px 8px rgba(0, 123, 255, 0.1);
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.consent-document-item:last-child {
|
||||
@@ -718,7 +717,6 @@ function copyEmail(email) {
|
||||
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 {
|
||||
@@ -728,7 +726,6 @@ function copyEmail(email) {
|
||||
margin-right: auto;
|
||||
max-width: 70%;
|
||||
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');
|
||||
if (docsResponse.data && docsResponse.data.length > 0) {
|
||||
docsResponse.data.forEach(doc => {
|
||||
resources.push(`${window.location.origin}/content/published/${doc.id}`);
|
||||
resources.push(`${window.location.origin}/public/page/${doc.id}`);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -70,7 +70,7 @@ export const connectWallet = async () => {
|
||||
const docsResponse = await axios.get('/consent/documents');
|
||||
if (docsResponse.data && docsResponse.data.length > 0) {
|
||||
docsResponse.data.forEach(doc => {
|
||||
resources.push(`${window.location.origin}/content/published/${doc.id}`);
|
||||
resources.push(`${window.location.origin}/public/page/${doc.id}`);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -197,8 +197,6 @@
|
||||
.chat-wrapper {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #e9ecef;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
@@ -2970,6 +2970,9 @@ const handleDeploymentCompleted = (result) => {
|
||||
console.log('🎉 Поэтапный деплой завершен:', result);
|
||||
showDeploymentWizard.value = false;
|
||||
|
||||
// Эмитируем событие о завершении деплоя для обновления Header
|
||||
eventBus.emit('dle-deployed', result);
|
||||
|
||||
// Перенаправляем на главную страницу управления
|
||||
router.push('/management');
|
||||
};
|
||||
|
||||
@@ -32,6 +32,53 @@
|
||||
|
||||
<!-- Основной контент -->
|
||||
<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 -->
|
||||
<div class="danger-card">
|
||||
<div class="danger-header">
|
||||
@@ -68,9 +115,12 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps, defineEmits, onMounted } from 'vue';
|
||||
import { ref, defineProps, defineEmits, onMounted, computed } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
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 { deactivateDLE } from '../../utils/dle-contract.js';
|
||||
import api from '../../api/axios';
|
||||
@@ -110,6 +160,74 @@ const goBackToBlocks = () => {
|
||||
// Получаем адрес пользователя из контекста аутентификации
|
||||
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(() => {
|
||||
window.addEventListener('clear-application-data', () => {
|
||||
@@ -136,7 +254,7 @@ const loadDLEInfo = async () => {
|
||||
console.log('Загружаем информацию о DLE:', address);
|
||||
|
||||
// Загружаем данные DLE из блокчейна через API
|
||||
const response = await api.post('/dle-core/read-dle-info', {
|
||||
const response = await api.post('/blockchain/read-dle-info', {
|
||||
dleAddress: address
|
||||
});
|
||||
|
||||
@@ -147,7 +265,8 @@ const loadDLEInfo = async () => {
|
||||
dleInfo.value = {
|
||||
name: dleData.name, // Название DLE из блокчейна
|
||||
symbol: dleData.symbol, // Символ DLE из блокчейна
|
||||
address: dleData.dleAddress || address // Адрес из API или из URL
|
||||
address: dleData.dleAddress || address, // Адрес из API или из URL
|
||||
logoURI: dleData.logoURI || '' // URL логотипа
|
||||
};
|
||||
} else {
|
||||
console.error('Ошибка загрузки DLE:', response.data.error);
|
||||
@@ -156,12 +275,9 @@ const loadDLEInfo = async () => {
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при загрузке информации о DLE:', error);
|
||||
// В случае ошибки показываем базовую информацию
|
||||
dleInfo.value = {
|
||||
name: 'DLE ' + address.slice(0, 8) + '...',
|
||||
symbol: 'DLE',
|
||||
address: address
|
||||
};
|
||||
// В случае ошибки НЕ устанавливаем fallback данные, оставляем null
|
||||
// чтобы не показывать некорректную информацию
|
||||
dleInfo.value = null;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
@@ -302,6 +418,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
/* Карточки */
|
||||
.footer-card,
|
||||
.danger-card {
|
||||
background: white;
|
||||
border: 1px solid #e9ecef;
|
||||
@@ -309,6 +426,75 @@ onMounted(() => {
|
||||
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 {
|
||||
background: #f8f9fa;
|
||||
padding: 15px 20px;
|
||||
|
||||
Reference in New Issue
Block a user