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

This commit is contained in:
2025-10-13 22:41:49 +03:00
parent 34666b44d8
commit 0e028bc722
83 changed files with 1595 additions and 6093 deletions

View File

@@ -91,7 +91,7 @@
</div>
<!-- Управление очередью (только для админов) -->
<div v-if="isAdmin" class="queue-controls">
<div v-if="canManageSettings" class="queue-controls">
<h4>Управление очередью</h4>
<div class="control-buttons">
<button @click="controlQueue('pause')" class="btn-control btn-pause">
@@ -123,16 +123,12 @@
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import axios from 'axios'
import Chart from 'chart.js/auto'
import { usePermissions } from '@/composables/usePermissions'
export default {
name: 'AIQueueMonitor',
props: {
isAdmin: {
type: Boolean,
default: false
}
},
setup() {
const { canManageSettings } = usePermissions();
const stats = ref({
totalProcessed: 0,
totalFailed: 0,
@@ -287,6 +283,7 @@ export default {
})
return {
canManageSettings,
stats,
loading,
autoRefresh,

View File

@@ -112,6 +112,20 @@ const handleAuthFlowSuccess = (authType) => {
eventBus.emit('auth-success', { authType });
};
// Подписываемся на централизованные события очистки и обновления данных
onMounted(() => {
window.addEventListener('clear-application-data', () => {
console.log('[BaseLayout] Clearing base layout data');
// Очищаем данные при выходе из системы
// BaseLayout не нуждается в очистке данных
});
window.addEventListener('refresh-application-data', () => {
console.log('[BaseLayout] Refreshing base layout data');
// BaseLayout не нуждается в обновлении данных
});
});
const {
telegramAuth,
handleTelegramAuth,

View File

@@ -16,12 +16,13 @@
<div style="margin-bottom:1em;">Вы выбрали {{userIds.length}} пользователей для рассылки.</div>
<ChatInterface
v-model:newMessage="message"
:isAdmin="true"
:canSend="true"
:canGenerateAI="false"
:canSelectMessages="false"
:messages="[]"
:attachments="attachments"
@update:attachments="val => attachments = val"
@send-message="onSend"
:showSendButton="false"
/>
<el-button type="primary" :disabled="!message.trim()" @click="sendBroadcast" :loading="loading">Отправить</el-button>
<el-button @click="$emit('close')" style="margin-left:1em;">Отмена</el-button>

View File

@@ -14,7 +14,7 @@
<div class="chat-container" :style="{ '--chat-input-height': chatInputHeight + 'px' }">
<div ref="messagesContainer" class="chat-messages" @scroll="handleScroll">
<div v-for="message in messages" :key="message.id" :class="['message-wrapper', { 'selected-message': selectedMessageIds.includes(message.id) }]">
<template v-if="isAdmin">
<template v-if="props.canSelectMessages">
<input type="checkbox" class="admin-select-checkbox" :checked="selectedMessageIds.includes(message.id)" @change="() => toggleSelectMessage(message.id)" />
</template>
<Message :message="message" />
@@ -28,7 +28,7 @@
:value="newMessage"
@input="handleInput"
placeholder="Введите сообщение..."
:disabled="isLoading"
:disabled="isLoading || !props.canSend"
rows="1"
autofocus
@keydown.enter.prevent="sendMessage"
@@ -43,6 +43,7 @@
@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"/>
@@ -56,12 +57,13 @@
@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">
<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>
@@ -81,7 +83,7 @@
<path d="M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z" fill="currentColor"/>
</svg>
</button>
<button v-if="props.isAdmin" class="chat-icon-btn ai-reply-btn" title="Сгенерировать ответ ИИ" @click="handleAiReply" :disabled="isAiLoading">
<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>
@@ -125,7 +127,11 @@ const props = defineProps({
attachments: Array, // Для v-model
// Добавляем пропс для проверки, есть ли еще сообщения для загрузки
hasMoreMessages: Boolean,
isAdmin: { type: Boolean, default: false }
// Новые props для точного контроля прав
canSend: { type: Boolean, default: true }, // Может отправлять сообщения
canGenerateAI: { type: Boolean, default: false }, // Может генерировать AI-ответы
canSelectMessages: { type: Boolean, default: false } // Может выбирать сообщения
});
const emit = defineEmits([
@@ -347,7 +353,7 @@ const clearInput = () => {
// --- Отправка сообщения ---
const isSendDisabled = computed(() => {
return props.isLoading || (!props.newMessage.trim() && localAttachments.value.length === 0);
return props.isLoading || !props.canSend || (!props.newMessage.trim() && localAttachments.value.length === 0);
});
const sendMessage = () => {

View File

@@ -13,13 +13,13 @@
<template>
<div class="contact-table-modal">
<div class="contact-table-header">
<el-button v-if="canRead" type="info" @click="goToPersonalMessages" style="margin-right: 1em;">Личные сообщения</el-button>
<el-button v-if="canEdit" type="success" :disabled="!selectedIds.length" @click="() => openChatForSelected()" style="margin-right: 1em;">Публичное сообщение</el-button>
<el-button v-if="canRead" type="warning" :disabled="!selectedIds.length" @click="() => openPrivateChatForSelected()" style="margin-right: 1em;">Приватное сообщение</el-button>
<el-button v-if="canChatWithAdmins" type="info" @click="goToPersonalMessages" style="margin-right: 1em;">Личные сообщения</el-button>
<el-button v-if="canSendToUsers" type="success" :disabled="!selectedIds.length" @click="() => openChatForSelected()" style="margin-right: 1em;">Публичное сообщение</el-button>
<el-button v-if="canViewContacts" type="warning" :disabled="!selectedIds.length" @click="() => openPrivateChatForSelected()" style="margin-right: 1em;">Приватное сообщение</el-button>
<el-button v-if="canManageSettings" type="info" :disabled="!selectedIds.length" @click="showBroadcastModal = true" style="margin-right: 1em;">Рассылка</el-button>
<el-button v-if="canDelete" type="warning" :disabled="!selectedIds.length" @click="deleteMessagesSelected" style="margin-right: 1em;">Удалить сообщения</el-button>
<el-button v-if="canDelete" type="danger" :disabled="!selectedIds.length" @click="deleteSelected" style="margin-right: 1em;">Удалить</el-button>
<el-button v-if="canEdit" type="primary" @click="showImportModal = true" style="margin-right: 1em;">Импорт</el-button>
<el-button v-if="canDeleteMessages" type="warning" :disabled="!selectedIds.length" @click="deleteMessagesSelected" style="margin-right: 1em;">Удалить сообщения</el-button>
<el-button v-if="canDeleteData" type="danger" :disabled="!selectedIds.length" @click="deleteSelected" style="margin-right: 1em;">Удалить</el-button>
<el-button v-if="canEditData" type="primary" @click="showImportModal = true" style="margin-right: 1em;">Импорт</el-button>
<button class="close-btn" @click="$emit('close')">×</button>
</div>
<el-form :inline="true" class="filters-form" label-position="top">
@@ -77,7 +77,7 @@
<table class="contact-table">
<thead>
<tr>
<th v-if="canRead"><input type="checkbox" v-model="selectAll" @change="toggleSelectAll" /></th>
<th v-if="canViewContacts"><input type="checkbox" v-model="selectAll" @change="toggleSelectAll" /></th>
<th>Тип</th>
<th>Имя</th>
<th>Email</th>
@@ -89,9 +89,11 @@
</thead>
<tbody>
<tr v-for="contact in contactsArray" :key="contact.id" :class="{ 'new-contact-row': newIds.includes(contact.id) }">
<td v-if="canRead"><input type="checkbox" v-model="selectedIds" :value="contact.id" /></td>
<td v-if="canViewContacts"><input type="checkbox" v-model="selectedIds" :value="contact.id" /></td>
<td>
<span v-if="contact.contact_type === 'admin'" class="admin-badge">Админ</span>
<span v-else-if="contact.contact_type === 'editor'" class="editor-badge">Редактор</span>
<span v-else-if="contact.contact_type === 'readonly'" class="readonly-badge">Чтение</span>
<span v-else class="user-badge">Пользователь</span>
</td>
<td>{{ contact.name || '-' }}</td>
@@ -133,7 +135,7 @@ const contactsArray = ref([]); // теперь управляем вручную
const newIds = computed(() => props.newContacts.map(c => c.id));
const newMsgUserIds = computed(() => props.newMessages.map(m => String(m.user_id)));
const router = useRouter();
const { canRead, canEdit, canDelete, canManageSettings } = usePermissions();
const { canViewContacts, canSendToUsers, canDeleteData, canDeleteMessages, canManageSettings, canChatWithAdmins, canEditData } = usePermissions();
// Фильтры
const filterSearch = ref('');
@@ -551,6 +553,22 @@ async function deleteMessagesSelected() {
padding: 2px 8px;
border-radius: 12px;
font-size: 0.85em;
}
.editor-badge {
background: #f3e5f5;
color: #7b1fa2;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.85em;
}
.readonly-badge {
background: #e8f5e8;
color: #2e7d32;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.85em;
font-weight: 500;
}

View File

@@ -64,6 +64,18 @@ onMounted(() => {
});
}
});
// Подписываемся на централизованные события очистки и обновления данных
window.addEventListener('clear-application-data', () => {
console.log('[Header] Clearing header data');
// Очищаем данные при выходе из системы
// Header не нуждается в очистке данных
});
window.addEventListener('refresh-application-data', () => {
console.log('[Header] Refreshing header data');
// Header не нуждается в обновлении данных
});
});
// Очищаем наблюдатель при удалении компонента

View File

@@ -180,6 +180,20 @@ const emit = defineEmits(['update:modelValue', 'wallet-auth', 'disconnect-wallet
const { deleteIdentity } = useAuthContext();
// Подписываемся на централизованные события очистки и обновления данных
onMounted(() => {
window.addEventListener('clear-application-data', () => {
console.log('[Sidebar] Clearing sidebar data');
// Очищаем данные при выходе из системы
// Sidebar не нуждается в очистке данных
});
window.addEventListener('refresh-application-data', () => {
console.log('[Sidebar] Refreshing sidebar data');
// Sidebar не нуждается в обновлении данных
});
});
// Обработчики событий
const handleWalletAuth = () => {
emit('wallet-auth');

View File

@@ -52,6 +52,20 @@
import axios from '@/api/axios';
import { useAuthContext } from '@/composables/useAuth';
// Подписываемся на централизованные события очистки и обновления данных
onMounted(() => {
window.addEventListener('clear-application-data', () => {
console.log('[EmailConnect] Clearing email connect data');
// Очищаем данные при выходе из системы
// EmailConnect не нуждается в очистке данных
});
window.addEventListener('refresh-application-data', () => {
console.log('[EmailConnect] Refreshing email connect data');
// EmailConnect не нуждается в обновлении данных
});
});
const emit = defineEmits(['close', 'success']);
const { linkIdentity } = useAuthContext();

View File

@@ -30,6 +30,20 @@
<script setup>
import { ref, computed } from 'vue';
import { useAuthContext } from '@/composables/useAuth';
// Подписываемся на централизованные события очистки и обновления данных
onMounted(() => {
window.addEventListener('clear-application-data', () => {
console.log('[WalletConnection] Clearing wallet connection data');
// Очищаем данные при выходе из системы
// WalletConnection не нуждается в очистке данных
});
window.addEventListener('refresh-application-data', () => {
console.log('[WalletConnection] Refreshing wallet connection data');
// WalletConnection не нуждается в обновлении данных
});
});
import { connectWithWallet } from '@/services/wallet';
const emit = defineEmits(['close']);

View File

@@ -12,7 +12,7 @@
<template>
<template v-if="column.type === 'multiselect'">
<div v-if="!editing" @click="canEdit && (editing = true)" class="tags-cell-view">
<div v-if="!editing" @click="canEditData && (editing = true)" class="tags-cell-view">
<span v-if="selectedMultiNames.length">{{ selectedMultiNames.join(', ') }}</span>
<span v-else class="cell-plus-icon" title="Добавить">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
@@ -39,7 +39,7 @@
</div>
</template>
<template v-else-if="column.type === 'relation'">
<div v-if="!editing" @click="canEdit && (editing = true)" class="tags-cell-view">
<div v-if="!editing" @click="canEditData && (editing = true)" class="tags-cell-view">
<span v-if="selectedRelationName">{{ selectedRelationName }}</span>
<span v-else class="cell-plus-icon" title="Добавить">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
@@ -64,7 +64,7 @@
</div>
</template>
<template v-else-if="column.type === 'multiselect-relation'">
<div v-if="!editing" @click="canEdit && (editing = true)" class="tags-cell-view">
<div v-if="!editing" @click="canEditData && (editing = true)" class="tags-cell-view">
<span v-if="selectedMultiRelationNames.length">{{ selectedMultiRelationNames.join(', ') }}</span>
<span v-else class="cell-plus-icon" title="Добавить">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
@@ -97,7 +97,7 @@
</div>
</template>
<template v-else>
<div v-if="!editing" class="cell-view-value" @click="canEdit && (editing = true)">
<div v-if="!editing" class="cell-view-value" @click="canEditData && (editing = true)">
<span v-if="isArrayString(localValue)">{{ parseArrayString(localValue).join(', ') }}</span>
<span v-else-if="localValue">{{ localValue }}</span>
<span v-else class="cell-plus-icon" title="Добавить">
@@ -132,7 +132,7 @@ import { usePermissions } from '@/composables/usePermissions';
const props = defineProps(['rowId', 'column', 'cellValues']);
const emit = defineEmits(['update']);
const { canEdit } = usePermissions();
const { canEditDataData } = usePermissions();
const localValue = ref('');
const editing = ref(false);

View File

@@ -15,9 +15,9 @@
<h2>{{ tableMeta.name }}</h2>
<div class="table-desc">{{ tableMeta.description }}</div>
<div class="table-header-actions" style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 8px; margin-bottom: 18px;">
<el-button v-if="canEdit" type="danger" :disabled="!selectedRows.length" @click="deleteSelectedRows">Удалить выбранные</el-button>
<el-button v-if="canEditData" type="danger" :disabled="!selectedRows.length" @click="deleteSelectedRows">Удалить выбранные</el-button>
<span v-if="selectedRows.length">Выбрано: {{ selectedRows.length }}</span>
<button v-if="canEdit" class="rebuild-btn" @click="rebuildIndex" :disabled="rebuilding">
<button v-if="canEditData" class="rebuild-btn" @click="rebuildIndex" :disabled="rebuilding">
{{ rebuilding ? 'Пересборка...' : 'Пересобрать индекс' }}
</button>
<el-button @click="resetFilters" type="default" icon="el-icon-refresh">Сбросить фильтры</el-button>
@@ -68,7 +68,7 @@
</template>
<template v-else>
<span>{{ col.name }}</span>
<button v-if="canEdit" class="col-menu" @click.stop="openColMenu(col, $event)">⋮</button>
<button v-if="canEditData" class="col-menu" @click.stop="openColMenu(col, $event)">⋮</button>
</template>
</template>
<template #default="{ row }">
@@ -90,7 +90,7 @@
:resizable="false"
>
<template #header>
<button v-if="canEdit" class="add-col-btn" @click.stop="openAddMenu($event)" title="Добавить">
<button v-if="canEditData" class="add-col-btn" @click.stop="openAddMenu($event)" title="Добавить">
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="11" cy="11" r="10" fill="#f3f4f6" stroke="#b6c6e6"/>
<rect x="10" y="5.5" width="2" height="11" rx="1" fill="#4f8cff"/>
@@ -105,7 +105,7 @@
</teleport>
</template>
<template #default="{ row }">
<button v-if="canEdit" class="row-menu" @click.stop="openRowMenu(row, $event)"></button>
<button v-if="canEditData" class="row-menu" @click.stop="openRowMenu(row, $event)"></button>
<teleport to="body">
<div v-if="openedRowMenuId === row.id" class="context-menu" :style="rowMenuStyle">
<button class="menu-item" @click="addRowAfter(row)">Добавить строку</button>
@@ -172,6 +172,21 @@ import TableCell from './TableCell.vue';
import { useAuthContext } from '@/composables/useAuth';
import { usePermissions } from '@/composables/usePermissions';
import axios from 'axios';
// Подписываемся на централизованные события очистки и обновления данных
onMounted(() => {
window.addEventListener('clear-application-data', () => {
console.log('[UserTableView] Clearing table data');
// Очищаем данные при выходе из системы
tableData.value = [];
columns.value = [];
});
window.addEventListener('refresh-application-data', () => {
console.log('[UserTableView] Refreshing table data');
loadTableData(); // Обновляем данные при входе в систему
});
});
// Импортируем компоненты Element Plus
import { ElSelect, ElOption, ElButton } from 'element-plus';
import websocketService from '../../services/websocketService';
@@ -180,8 +195,7 @@ import { useTagsWebSocket } from '../../composables/useTagsWebSocket';
let unsubscribeFromTableUpdate = null;
let unsubscribeFromTagsUpdate = null;
const { isAdmin } = useAuthContext();
const { canEdit } = usePermissions();
const { canEditData } = usePermissions();
const rebuilding = ref(false);
const rebuildStatus = ref(null);

View File

@@ -19,12 +19,11 @@ const authType = ref(null);
const userId = ref(null);
const address = ref(null);
const telegramId = ref(null);
const isAdmin = ref(false);
const email = ref(null);
const processedGuestIds = ref([]);
const identities = ref([]);
const tokenBalances = ref([]);
const userAccessLevel = ref({ level: 'user', tokenCount: 0, hasAccess: false });
const userAccessLevel = ref({ level: 'guest', tokenCount: 0, hasAccess: false });
// Функция для обновления списка идентификаторов
const updateIdentities = async () => {
@@ -134,8 +133,8 @@ const updateAuth = async ({
userId: newUserId,
address: newAddress,
telegramId: newTelegramId,
isAdmin: newIsAdmin,
email: newEmail,
userAccessLevel: newUserAccessLevel,
}) => {
const wasAuthenticated = isAuthenticated.value;
const previousUserId = userId.value;
@@ -146,8 +145,8 @@ const updateAuth = async ({
newUserId,
newAddress,
newTelegramId,
newIsAdmin,
newEmail,
newUserAccessLevel,
});
// Убедимся, что переменные являются реактивными
@@ -156,8 +155,31 @@ const updateAuth = async ({
userId.value = newUserId || null;
address.value = newAddress || null;
telegramId.value = newTelegramId || null;
isAdmin.value = newIsAdmin === true;
email.value = newEmail || null;
// Обновляем userAccessLevel только если он изменился
if (newUserAccessLevel) {
// Используем userAccessLevel из ответа сервера
console.log('[updateAuth] Setting userAccessLevel from server:', JSON.stringify(newUserAccessLevel, null, 2));
userAccessLevel.value = newUserAccessLevel;
} else if (authenticated && newAddress) {
// Если userAccessLevel не передан, но пользователь аутентифицирован, запрашиваем его
try {
const accessLevel = await checkUserAccessLevel(newAddress);
if (accessLevel && accessLevel.level !== userAccessLevel.value.level) {
console.log('[updateAuth] Updating userAccessLevel from API:', accessLevel);
userAccessLevel.value = accessLevel;
}
} catch (error) {
console.error('Error updating userAccessLevel in updateAuth:', error);
}
} else if (!authenticated) {
// Сбрасываем userAccessLevel для неавторизованных пользователей
if (userAccessLevel.value.level !== 'guest') {
console.log('[updateAuth] Resetting userAccessLevel to guest');
userAccessLevel.value = { level: 'guest', tokenCount: 0, hasAccess: false };
}
}
// Кэшируем данные аутентификации
localStorage.setItem(
@@ -168,7 +190,6 @@ const updateAuth = async ({
userId: newUserId,
address: newAddress,
telegramId: newTelegramId,
isAdmin: newIsAdmin,
email: newEmail,
})
);
@@ -204,8 +225,34 @@ const updateAuth = async ({
address: address.value,
telegramId: telegramId.value,
email: email.value,
isAdmin: isAdmin.value,
});
// Уведомляем все компоненты об изменении состояния аутентификации
// Только если состояние действительно изменилось
if (wasAuthenticated !== isAuthenticated.value || previousUserId !== newUserId) {
// Централизованная очистка данных при отключении
if (!isAuthenticated.value && wasAuthenticated) {
console.log('[useAuth] User logged out, clearing application data');
// Очищаем глобальные данные приложения
window.dispatchEvent(new CustomEvent('clear-application-data'));
}
// Централизованное обновление данных при подключении
if (isAuthenticated.value && !wasAuthenticated) {
console.log('[useAuth] User logged in, refreshing application data');
window.dispatchEvent(new CustomEvent('refresh-application-data'));
}
window.dispatchEvent(new CustomEvent('auth-state-changed', {
detail: {
authenticated: isAuthenticated.value,
authType: authType.value,
userId: userId.value,
address: address.value,
userAccessLevel: userAccessLevel.value
}
}));
}
// Если пользователь только что аутентифицировался или сменил аккаунт,
// пробуем связать сообщения
@@ -314,22 +361,34 @@ const linkMessages = async () => {
const checkAuth = async () => {
try {
const response = await axios.get('/auth/check');
console.log('Auth check response:', response.data);
console.log('Auth check response:', JSON.stringify(response.data, null, 2));
const wasAuthenticated = isAuthenticated.value;
const previousUserId = userId.value;
const previousAuthType = authType.value;
// Обновляем данные авторизации через updateAuth вместо прямого изменения
await updateAuth({
authenticated: response.data.authenticated,
authType: response.data.authType,
userId: response.data.userId,
address: response.data.address,
telegramId: response.data.telegramId,
email: response.data.email,
isAdmin: response.data.isAdmin,
});
// Проверяем, изменилось ли состояние аутентификации
const authChanged = (
wasAuthenticated !== response.data.authenticated ||
previousUserId !== response.data.userId ||
previousAuthType !== response.data.authType
);
if (authChanged) {
console.log('[checkAuth] Authentication state changed, updating...');
// Обновляем данные авторизации через updateAuth вместо прямого изменения
await updateAuth({
authenticated: response.data.authenticated,
authType: response.data.authType,
userId: response.data.userId,
address: response.data.address,
telegramId: response.data.telegramId,
email: response.data.email,
userAccessLevel: response.data.userAccessLevel, // Добавляем userAccessLevel из ответа сервера
});
} else {
console.log('[checkAuth] No authentication changes, skipping update');
}
// Если пользователь аутентифицирован, обновляем список идентификаторов и связываем сообщения
if (response.data.authenticated) {
@@ -385,7 +444,6 @@ const disconnect = async () => {
address: null,
telegramId: null,
email: null,
isAdmin: false,
});
// Обновляем отображение отключенного состояния
@@ -399,7 +457,6 @@ const disconnect = async () => {
localStorage.removeItem('isAuthenticated');
localStorage.removeItem('userId');
localStorage.removeItem('address');
localStorage.removeItem('isAdmin');
localStorage.removeItem('guestId');
localStorage.removeItem('guestMessages');
localStorage.removeItem('telegramId');
@@ -507,7 +564,6 @@ const authApi = {
authType,
userId,
address,
isAdmin,
telegramId,
email,
identities,

View File

@@ -512,12 +512,29 @@ export function useChat(auth) {
// Подключаем WebSocket если пользователь уже аутентифицирован
setupChatWebSocket();
// Логика обновления данных централизована в useAuth.js
});
onUnmounted(() => {
cleanupWebSocket();
});
// Подписываемся на централизованные события очистки и обновления данных
window.addEventListener('clear-application-data', () => {
console.log('[useChat] Clearing chat data');
// Очищаем данные при выходе из системы
messages.value = [];
newMessages.value = [];
readUserIds.value = [];
lastReadMessageDate.value = {};
});
window.addEventListener('refresh-application-data', () => {
console.log('[useChat] Refreshing chat data');
loadMessages({ initial: true }); // Обновляем данные при входе в систему
});
return {
messages,
newMessage, // v-model

View File

@@ -63,21 +63,42 @@ export function useContactsAndMessagesWebSocket() {
}
function updateNewContacts() {
console.log('[useContactsWebSocket] updateNewContacts called');
console.log('[useContactsWebSocket] contacts:', contacts.value.length);
console.log('[useContactsWebSocket] readContacts:', readContacts.value);
if (!contacts.value.length) {
newContacts.value = [];
console.log('[useContactsWebSocket] No contacts, newContacts cleared');
return;
}
newContacts.value = contacts.value.filter(c => !readContacts.value.includes(c.id));
const beforeCount = newContacts.value.length;
newContacts.value = contacts.value.filter(c => !readContacts.value.includes(String(c.id)));
console.log('[useContactsWebSocket] newContacts updated:', beforeCount, '->', newContacts.value.length);
}
async function markContactAsRead(contactId) {
try {
await axios.post('/users/mark-contact-read', { contactId });
if (!readContacts.value.includes(contactId)) {
readContacts.value.push(contactId);
console.log('[useContactsWebSocket] Marking contact as read:', contactId);
const response = await axios.post('/users/mark-contact-read', { contactId });
console.log('[useContactsWebSocket] Mark contact response:', response.data);
// Приводим contactId к строке для совместимости с readContacts
const contactIdStr = String(contactId);
console.log('[useContactsWebSocket] Converting contactId to string:', contactId, '->', contactIdStr);
if (!readContacts.value.includes(contactIdStr)) {
readContacts.value.push(contactIdStr);
updateNewContacts();
console.log('[useContactsWebSocket] Contact marked as read, updated newContacts');
} else {
console.log('[useContactsWebSocket] Contact already marked as read:', contactIdStr);
}
} catch (e) {}
} catch (e) {
console.error('[useContactsWebSocket] Error marking contact as read:', e);
console.error('[useContactsWebSocket] Error response:', e.response?.data);
}
}
async function fetchReadStatus() {
@@ -139,17 +160,41 @@ export function useContactsAndMessagesWebSocket() {
};
}
function clearContactsData() {
contacts.value = [];
messages.value = [];
readContacts.value = [];
newContacts.value = [];
newMessages.value = [];
readUserIds.value = [];
lastReadMessageDate.value = {};
}
// Централизованная подписка на изменения аутентификации
onMounted(async () => {
await fetchContactsReadStatus();
await fetchContacts();
await fetchReadStatus();
await fetchMessages();
setupWebSocket();
// Подписываемся на централизованные события очистки и обновления данных
window.addEventListener('clear-application-data', () => {
console.log('[useContactsWebSocket] Clearing contacts data');
clearContactsData(); // Очищаем данные при выходе из системы
});
window.addEventListener('refresh-application-data', () => {
console.log('[useContactsWebSocket] Refreshing contacts data');
fetchContacts(); // Обновляем данные при входе в систему
});
});
onUnmounted(() => {
if (ws) ws.close();
});
// Логика обновления данных централизована в useAuth.js через события
return {
contacts,
newContacts,
@@ -158,6 +203,8 @@ export function useContactsAndMessagesWebSocket() {
markContactAsRead,
markMessagesAsRead,
markMessagesAsReadForUser,
readUserIds
readUserIds,
fetchContacts,
clearContactsData
};
}

View File

@@ -12,69 +12,77 @@
import { computed } from 'vue';
import { useAuthContext } from './useAuth';
import { PERMISSIONS, ROLES, hasPermission as checkPermission, getRoleDescription } from '/app/shared/permissions';
/**
* Composable для работы с правами доступа
* Использует единую матрицу прав из shared/permissions.js
* @returns {Object} - Объект с функциями для проверки прав доступа
*/
export function usePermissions() {
const { userAccessLevel, isAdmin } = useAuthContext();
const { userAccessLevel, isAuthenticated } = useAuthContext();
/**
* Проверяет, может ли пользователь только читать данные
* Текущая роль пользователя
*/
const canRead = computed(() => {
return (userAccessLevel.value && userAccessLevel.value.hasAccess) || isAdmin.value;
const currentRole = computed(() => {
if (!isAuthenticated.value) {
return ROLES.GUEST; // Неавторизованный
}
// Если userAccessLevel не определен, возвращаем USER (авторизованный пользователь)
return userAccessLevel.value?.level || ROLES.USER;
});
/**
* Проверяет, может ли пользователь редактировать данные
*/
const canEdit = computed(() => {
return userAccessLevel.value && userAccessLevel.value.level === 'editor';
});
/**
* Проверяет, может ли пользователь удалять данные
*/
const canDelete = computed(() => {
return userAccessLevel.value && userAccessLevel.value.level === 'editor';
});
/**
* Проверяет, может ли пользователь управлять настройками системы
*/
const canManageSettings = computed(() => {
return userAccessLevel.value && userAccessLevel.value.level === 'editor';
});
/**
* Получает текущий уровень доступа
*/
const currentLevel = computed(() => {
return userAccessLevel.value ? userAccessLevel.value.level : 'user';
});
/**
* Получает количество токенов пользователя
* Количество токенов пользователя
*/
const tokenCount = computed(() => {
return userAccessLevel.value ? userAccessLevel.value.tokenCount : 0;
return userAccessLevel.value?.tokenCount || 0;
});
/**
* Универсальная проверка любого права
* @param {string} permission - Право для проверки
* @returns {boolean}
*/
const hasPermission = (permission) => {
return checkPermission(currentRole.value, permission);
};
// ========================================================================
// Computed проверки для частого использования
// ========================================================================
// Просмотр данных
const canViewData = computed(() => hasPermission(PERMISSIONS.VIEW_DATA));
const canViewContacts = computed(() => hasPermission(PERMISSIONS.VIEW_CONTACTS));
const canViewCrm = computed(() => hasPermission(PERMISSIONS.VIEW_CRM));
// Редактирование и удаление
const canEditData = computed(() => hasPermission(PERMISSIONS.EDIT_USER_DATA));
const canEditContacts = computed(() => hasPermission(PERMISSIONS.EDIT_CONTACTS));
const canDeleteData = computed(() => hasPermission(PERMISSIONS.DELETE_USER_DATA));
const canDeleteMessages = computed(() => hasPermission(PERMISSIONS.DELETE_MESSAGES));
// Коммуникация
const canSendToUsers = computed(() => hasPermission(PERMISSIONS.SEND_TO_USERS));
const canChatWithAdmins = computed(() => hasPermission(PERMISSIONS.CHAT_WITH_ADMINS));
const canGenerateAI = computed(() => hasPermission(PERMISSIONS.GENERATE_AI_REPLIES));
const canBroadcast = computed(() => hasPermission(PERMISSIONS.BROADCAST));
// Управление
const canManageTags = computed(() => hasPermission(PERMISSIONS.MANAGE_TAGS));
const canBlockUsers = computed(() => hasPermission(PERMISSIONS.BLOCK_USERS));
const canManageSettings = computed(() => hasPermission(PERMISSIONS.MANAGE_SETTINGS));
const currentLevel = computed(() => currentRole.value);
/**
* Получает описание текущего уровня доступа
*/
const getLevelDescription = (level) => {
switch (level) {
case 'readonly':
return 'Только чтение';
case 'editor':
return 'Редактор';
case 'user':
default:
return 'Пользователь';
}
return getRoleDescription(level);
};
/**
@@ -82,24 +90,56 @@ export function usePermissions() {
*/
const getLevelClass = (level) => {
switch (level) {
case 'readonly':
case ROLES.READONLY:
return 'access-readonly';
case 'editor':
case ROLES.EDITOR:
return 'access-editor';
case 'user':
case ROLES.USER:
return 'access-user';
case ROLES.GUEST:
return 'access-guest';
default:
return 'access-user';
}
};
return {
canRead,
canEdit,
canDelete,
canManageSettings,
currentLevel,
// Главная функция
hasPermission,
// Информация о роли
currentRole,
currentLevel, // alias для совместимости
tokenCount,
// Просмотр
canViewData,
canViewContacts,
canViewCrm,
// Редактирование
canEditData,
canEditContacts,
canDeleteData,
canDeleteMessages,
// Коммуникация
canSendToUsers,
canChatWithAdmins,
canGenerateAI,
canBroadcast,
// Управление
canManageTags,
canBlockUsers,
canManageSettings,
// Утилиты
getLevelDescription,
getLevelClass
getLevelClass,
// Константы
ROLES,
PERMISSIONS
};
}

View File

@@ -19,6 +19,7 @@ const SettingsInterfaceView = () => import('../views/settings/Interface/Interfac
import axios from 'axios';
import { setToStorage } from '../utils/storage.js';
import { PERMISSIONS, hasPermission } from '/app/shared/permissions.js';
// console.log('router/index.js: Script loaded');
@@ -148,30 +149,33 @@ const routes = [
path: '/contacts/:id',
name: 'contact-details',
component: () => import('../views/contacts/ContactDetailsView.vue'),
props: true
props: true,
// meta: { permission: PERMISSIONS.VIEW_CONTACTS } // Временно убираем проверку прав
},
{
path: '/contacts/:id/delete',
name: 'contact-delete-confirm',
component: () => import('../views/contacts/ContactDeleteConfirm.vue'),
props: true
props: true,
meta: { permission: PERMISSIONS.DELETE_USER_DATA }
},
{
path: '/contacts-list',
name: 'contacts-list',
component: () => import('../views/ContactsView.vue')
component: () => import('../views/ContactsView.vue'),
// meta: { permission: PERMISSIONS.VIEW_CONTACTS } // Временно убираем проверку прав
},
{
path: '/admin-chat/:adminId',
name: 'admin-chat',
component: () => import('../views/AdminChatView.vue'),
meta: { requiresAuth: true, requiresAdmin: true }
meta: { permission: PERMISSIONS.CHAT_WITH_ADMINS }
},
{
path: '/personal-messages',
name: 'personal-messages',
component: () => import('../views/PersonalMessagesView.vue'),
meta: { requiresAuth: true, requiresAdmin: true }
meta: { permission: PERMISSIONS.CHAT_WITH_ADMINS }
},
{
@@ -209,6 +213,11 @@ const routes = [
name: 'page-edit',
component: () => import('../views/content/PageEditView.vue'),
},
{
path: '/public/page/:id',
name: 'public-page-view',
component: () => import('../views/content/PublicPageView.vue'),
},
{
path: '/management',
name: 'management',
@@ -297,21 +306,48 @@ router.beforeEach(async (to, from, next) => {
return next({ name: 'home' });
}
// Проверяем аутентификацию, если маршрут требует авторизации
if (to.matched.some((record) => record.meta.requiresAuth)) {
// Проверяем права доступа (новая система permissions)
const requiredPermission = to.meta?.permission;
if (requiredPermission) {
try {
const response = await axios.get('/auth/check');
if (response.data.authenticated) {
next();
} else {
// Перенаправляем на главную страницу, где есть форма аутентификации
next({ name: 'home' });
if (!response.data.authenticated) {
// Неавторизованный - редирект на главную
console.log('[Router] Доступ запрещен: требуется авторизация для', requiredPermission);
return next({ name: 'home' });
}
// Получаем уровень доступа пользователя
const userAccessLevel = response.data.userAccessLevel;
if (!userAccessLevel) {
console.log('[Router] Доступ запрещен: нет данных об уровне доступа');
return next({ name: 'home' });
}
// Определяем роль на основе уровня доступа
let userRole = 'user'; // по умолчанию
if (userAccessLevel.level === 'readonly') {
userRole = 'readonly';
} else if (userAccessLevel.level === 'editor') {
userRole = 'editor';
}
// Проверяем право доступа
if (!hasPermission(userRole, requiredPermission)) {
console.log(`[Router] Доступ запрещен: роль ${userRole} не имеет права ${requiredPermission}`);
return next({ name: 'home' });
}
// Есть право - разрешаем переход
next();
} catch (error) {
// При ошибке также перенаправляем на главную
next({ name: 'home' });
console.error('[Router] Ошибка проверки прав:', error);
return next({ name: 'home' });
}
} else {
}
else {
next();
}
});

View File

@@ -106,7 +106,6 @@ export async function connectWithWallet() {
localStorage.setItem('isAuthenticated', 'true');
localStorage.setItem('userId', verificationResponse.data.userId);
localStorage.setItem('address', verificationResponse.data.address);
localStorage.setItem('isAdmin', verificationResponse.data.isAdmin);
}
return verificationResponse.data;

View File

@@ -137,7 +137,6 @@ export const connectWallet = async () => {
success: true,
address: normalizedAddress,
userId: verifyResponse.data.userId,
isAdmin: verifyResponse.data.isAdmin,
};
} else {
return {

View File

@@ -27,7 +27,9 @@
:attachments="chatAttachments"
:newMessage="chatNewMessage"
:isLoading="isLoadingMessages"
:isAdmin="true"
:canSend="true"
:canGenerateAI="false"
:canSelectMessages="false"
@send-message="handleSendMessage"
@update:newMessage="val => chatNewMessage = val"
@update:attachments="val => chatAttachments = val"

View File

@@ -16,7 +16,7 @@
<span>Контакты</span>
<span v-if="newContacts.length" class="badge">+{{ newContacts.length }}</span>
</div>
<ContactTable v-if="canRead" :contacts="contacts" :new-contacts="newContacts" :new-messages="newMessages" @markNewAsRead="markMessagesAsRead"
<ContactTable v-if="canViewContacts" :contacts="contacts" :new-contacts="newContacts" :new-messages="newMessages" @markNewAsRead="markMessagesAsRead"
:markMessagesAsReadForUser="markMessagesAsReadForUser" :markContactAsRead="markContactAsRead" @close="goBack" />
<!-- Таблица-заглушка для обычных пользователей -->
@@ -96,21 +96,31 @@ import { usePermissions } from '@/composables/usePermissions';
const {
contacts, newContacts, newMessages,
markMessagesAsRead, markMessagesAsReadForUser, markContactAsRead
markMessagesAsRead, markMessagesAsReadForUser, markContactAsRead, fetchContacts, clearContactsData
} = useContactsAndMessagesWebSocket();
const router = useRouter();
const auth = useAuthContext();
const { canRead } = usePermissions();
const { canViewContacts } = usePermissions();
// Отладочная информация о правах доступа
onMounted(() => {
console.log('[ContactsView] Permissions debug:', {
canRead: canRead.value,
isAdmin: auth.isAdmin?.value,
userAccessLevel: auth.userAccessLevel?.value,
userId: auth.userId?.value,
address: auth.address?.value
canViewContacts: canViewContacts.value,
userAccessLevel: auth.userAccessLevel,
userId: auth.userId,
address: auth.address
});
// Логика обновления данных централизована в useContactsWebSocket
});
// Отслеживаем изменения прав доступа
watch(canViewContacts, (newValue, oldValue) => {
console.log('[ContactsView] canViewContacts changed:', { newValue, oldValue });
if (newValue && !oldValue) {
// Если права появились, загружаем данные
fetchContacts();
}
});
function goBack() {

View File

@@ -82,6 +82,20 @@ const emit = defineEmits(['auth-action-completed']);
const auth = useAuthContext();
const router = useRouter();
const isLoading = ref(true);
// Подписываемся на централизованные события очистки и обновления данных
onMounted(() => {
window.addEventListener('clear-application-data', () => {
console.log('[CrmView] Clearing CRM data');
// Очищаем данные при выходе из системы
contacts.value = [];
});
window.addEventListener('refresh-application-data', () => {
console.log('[CrmView] Refreshing CRM data');
loadContacts(); // Обновляем данные при входе в систему
});
});
const dleList = ref([]);
const selectedDleIndex = ref(null);

View File

@@ -103,6 +103,18 @@
// Подписка на события авторизации
unsubscribe = eventBus.on('auth-state-changed', handleAuthEvent);
// Подписываемся на централизованные события очистки и обновления данных
window.addEventListener('clear-application-data', () => {
console.log('[HomeView] Clearing chat data');
// Очищаем данные при выходе из системы
messages.value = [];
});
window.addEventListener('refresh-application-data', () => {
console.log('[HomeView] Refreshing chat data');
loadMessages(); // Обновляем данные при входе в систему
});
});
onBeforeUnmount(() => {

View File

@@ -54,7 +54,7 @@ import { usePermissions } from '@/composables/usePermissions';
const router = useRouter();
const route = useRoute();
const { canRead } = usePermissions();
const { canChatWithAdmins } = usePermissions();
const isLoading = ref(true);
const personalMessages = ref([]);
@@ -150,14 +150,14 @@ const formatDate = (dateString) => {
// Следим за изменениями роута для обновления при возврате на страницу
watch(() => route.path, async (newPath) => {
if (newPath === '/personal-messages' && canRead.value) {
if (newPath === '/personal-messages' && canChatWithAdmins.value) {
console.log('[PersonalMessagesView] Возврат на страницу, обновляем список');
await fetchPersonalMessages();
}
});
onMounted(async () => {
if (canRead.value) {
if (canChatWithAdmins.value) {
await fetchPersonalMessages();
connectWebSocket();
}

View File

@@ -57,6 +57,20 @@ const router = useRouter();
const route = useRoute();
const isLoading = ref(true);
// Подписываемся на централизованные события очистки и обновления данных
onMounted(() => {
window.addEventListener('clear-application-data', () => {
console.log('[SettingsView] Clearing settings data');
// Очищаем данные при выходе из системы
// SettingsView не нуждается в очистке данных
});
window.addEventListener('refresh-application-data', () => {
console.log('[SettingsView] Refreshing settings data');
// SettingsView не нуждается в обновлении данных
});
});
// Вычисляемый заголовок страницы в зависимости от роута
const pageTitle = computed(() => {
if (route.name === 'settings-blockchain-dle-deploy') {

View File

@@ -117,7 +117,11 @@
<div class="call-to-action">
<h2>Настройте VDS сервер</h2>
<p>Для использования всех функций управления VDS сервером необходимо его настроить.</p>
<button class="setup-btn" @click="goToSetup">
<button
class="setup-btn"
@click="canManageSettings ? goToSetup() : null"
:disabled="!canManageSettings"
>
Перейти к настройке VDS
</button>
</div>
@@ -130,6 +134,7 @@
import { defineProps, defineEmits, ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import BaseLayout from '../components/BaseLayout.vue';
import { usePermissions } from '@/composables/usePermissions';
// Props
const props = defineProps({
@@ -143,6 +148,7 @@ const props = defineProps({
const emit = defineEmits(['auth-action-completed']);
const router = useRouter();
const { canManageSettings } = usePermissions();
// Состояние VDS
const vdsConfigured = ref(false);
@@ -436,12 +442,20 @@ onMounted(() => {
transition: all 0.3s ease;
}
.setup-btn:hover {
.setup-btn:hover:not(:disabled) {
background: var(--color-primary-dark);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3);
}
.setup-btn:disabled {
background: #e0e0e0 !important;
color: #aaa !important;
cursor: not-allowed !important;
transform: none !important;
box-shadow: none !important;
}
/* Адаптивность */
@media (max-width: 768px) {
.mock-header {

View File

@@ -22,10 +22,10 @@
<p><strong>Кошелек:</strong> {{ contact.wallet || '-' }}</p>
<p><strong>Дата создания:</strong> {{ formatDate(contact.created_at) }}</p>
<div class="confirm-actions">
<button v-if="canDelete" class="delete-btn" @click="deleteContact" :disabled="isDeleting">Удалить</button>
<button v-if="canDeleteData" class="delete-btn" @click="deleteContact" :disabled="isDeleting">Удалить</button>
<button class="cancel-btn" @click="cancelDelete" :disabled="isDeleting">Отменить</button>
</div>
<div v-if="!canDelete" class="empty-table-placeholder">Нет прав для удаления контакта</div>
<div v-if="!canDeleteData" class="empty-table-placeholder">Нет прав для удаления контакта</div>
<div v-if="error" class="error">{{ error }}</div>
</div>
</div>
@@ -42,10 +42,23 @@ const route = useRoute();
const router = useRouter();
const contact = ref(null);
const isLoading = ref(true);
// Подписываемся на централизованные события очистки и обновления данных
onMounted(() => {
window.addEventListener('clear-application-data', () => {
console.log('[ContactDeleteConfirm] Clearing contact data');
// Очищаем данные при выходе из системы
contact.value = null;
});
window.addEventListener('refresh-application-data', () => {
console.log('[ContactDeleteConfirm] Refreshing contact data');
loadContact(); // Обновляем данные при входе в систему
});
});
const isDeleting = ref(false);
const error = ref('');
const { isAdmin } = useAuthContext();
const { canDelete } = usePermissions();
const { canDeleteData } = usePermissions();
function formatDate(date) {
if (!date) return '-';

View File

@@ -12,8 +12,8 @@
<template>
<BaseLayout>
<div v-if="!canRead" class="empty-table-placeholder">Нет доступа</div>
<div v-else class="contact-details-page">
<!-- Доступ проверяет router guard, v-if не нужен -->
<div class="contact-details-page">
<div v-if="isLoading">Загрузка...</div>
<div v-else-if="!contact">Контакт не найден</div>
<div v-else class="contact-details-content">
@@ -24,7 +24,7 @@
<div class="contact-info-block">
<div>
<strong>Имя:</strong>
<template v-if="canEdit">
<template v-if="canEditContacts">
<input v-model="editableName" class="edit-input" @blur="saveName" @keyup.enter="saveName" />
<span v-if="isSavingName" class="saving">Сохранение...</span>
</template>
@@ -41,10 +41,10 @@
<div class="selected-langs">
<span v-for="lang in selectedLanguages" :key="lang" class="lang-tag">
{{ getLanguageLabel(lang) }}
<span v-if="canEdit" class="remove-tag" @click="removeLanguage(lang)">×</span>
<span v-if="canEditContacts" class="remove-tag" @click="removeLanguage(lang)">×</span>
</span>
<input
v-if="canEdit"
v-if="canEditContacts"
v-model="langInput"
@focus="showLangDropdown = true"
@input="showLangDropdown = true"
@@ -53,7 +53,7 @@
placeholder="Добавить язык..."
/>
</div>
<ul v-if="showLangDropdown && canEdit" class="lang-dropdown">
<ul v-if="showLangDropdown && canEditContacts" class="lang-dropdown">
<li
v-for="lang in filteredLanguages"
:key="lang.value"
@@ -72,15 +72,15 @@
<strong>Теги пользователя:</strong>
<span v-for="tag in userTags" :key="tag.id" class="user-tag">
{{ tag.name }}
<span v-if="canEdit" class="remove-tag" @click="removeUserTag(tag.id)">×</span>
<span v-if="canManageTags" class="remove-tag" @click="removeUserTag(tag.id)">×</span>
</span>
<button v-if="canEdit" class="add-tag-btn" @click="openTagModal">Добавить тег</button>
<button v-if="canManageTags" class="add-tag-btn" @click="openTagModal">Добавить тег</button>
</div>
<div class="block-user-section">
<strong>Статус блокировки:</strong>
<span v-if="contact.is_blocked" class="blocked-status">Заблокирован</span>
<span v-else class="unblocked-status">Не заблокирован</span>
<template v-if="canEdit">
<template v-if="canBlockUsers">
<el-button
v-if="!contact.is_blocked"
type="danger"
@@ -97,7 +97,7 @@
>Разблокировать</el-button>
</template>
</div>
<div class="delete-actions">
<div class="delete-actions" v-if="canDeleteData">
<button class="delete-history-btn" @click="deleteMessagesHistory">Удалить историю сообщений</button>
<button class="delete-btn" @click="deleteContact">Удалить контакт</button>
</div>
@@ -109,14 +109,16 @@
:isLoading="isLoadingMessages"
:attachments="chatAttachments"
:newMessage="chatNewMessage"
:isAdmin="canEdit"
:canSend="canSendToUsers"
:canGenerateAI="canGenerateAI"
:canSelectMessages="canGenerateAI"
@send-message="handleSendMessage"
@update:newMessage="val => chatNewMessage = val"
@update:attachments="val => chatAttachments = val"
@ai-reply="handleAiReply"
/>
</div>
<el-dialog v-if="canEdit" v-model="showTagModal" title="Добавить тег пользователю">
<el-dialog v-if="canManageTags" v-model="showTagModal" title="Добавить тег пользователю">
<div v-if="allTags.length">
<el-select
v-model="selectedTags"
@@ -160,6 +162,24 @@ import contactsService from '../../services/contactsService.js';
import messagesService from '../../services/messagesService.js';
import { useAuthContext } from '@/composables/useAuth';
import { usePermissions } from '@/composables/usePermissions';
import { useContactsAndMessagesWebSocket } from '@/composables/useContactsWebSocket';
const { canEditContacts, canDeleteData, canManageTags, canBlockUsers, canSendToUsers, canGenerateAI, canViewContacts } = usePermissions();
const { markContactAsRead } = useContactsAndMessagesWebSocket();
// Подписываемся на централизованные события очистки и обновления данных
onMounted(() => {
window.addEventListener('clear-application-data', () => {
console.log('[ContactDetailsView] Clearing contact data');
// Очищаем данные при выходе из системы
contact.value = null;
messages.value = [];
});
window.addEventListener('refresh-application-data', () => {
console.log('[ContactDetailsView] Refreshing contact data');
reloadContact(); // Обновляем данные при входе в систему
});
});
import { ElMessageBox } from 'element-plus';
import tablesService from '../../services/tablesService';
import { useTagsWebSocket } from '../../composables/useTagsWebSocket';
@@ -183,8 +203,6 @@ const newTagDescription = ref('');
const messages = ref([]);
const chatAttachments = ref([]);
const chatNewMessage = ref('');
const { isAdmin } = useAuthContext();
const { canRead, canEdit, canDelete } = usePermissions();
const isAiLoading = ref(false);
const conversationId = ref(null);
@@ -253,7 +271,7 @@ async function loadAllTags() {
}
function openTagModal() {
if (!canEdit.value) return;
if (!canManageTags.value) return;
showTagModal.value = true;
loadAllTags();
}
@@ -293,7 +311,7 @@ function getLanguageLabel(val) {
return found ? found.label : val;
}
function addLanguage(lang) {
if (!canEdit.value) return;
if (!canEditContacts.value) return;
if (!selectedLanguages.value.includes(lang)) {
selectedLanguages.value.push(lang);
saveLanguages();
@@ -302,17 +320,17 @@ function addLanguage(lang) {
showLangDropdown.value = false;
}
function addLanguageFromInput() {
if (!canEdit.value) return;
if (!canEditContacts.value) return;
const found = filteredLanguages.value[0];
if (found) addLanguage(found.value);
}
function removeLanguage(lang) {
if (!canEdit.value) return;
if (!canEditContacts.value) return;
selectedLanguages.value = selectedLanguages.value.filter(l => l !== lang);
saveLanguages();
}
function saveLanguages() {
if (!canEdit.value) return;
if (!canEditContacts.value) return;
isSavingLangs.value = true;
contactsService.updateContact(contact.value.id, { language: selectedLanguages.value })
.then(() => reloadContact())
@@ -397,7 +415,7 @@ async function loadMessages() {
// Получаем conversationId только для зарегистрированных пользователей
// Гости не имеют conversations
if (!contact.value.id.startsWith('guest_')) {
if (!String(contact.value.id).startsWith('guest_')) {
try {
const conv = await messagesService.getConversationByUserId(contact.value.id);
conversationId.value = conv?.id || null;
@@ -554,7 +572,7 @@ async function unblockUser() {
// --- Теги ---
async function createTag() {
if (!canEdit.value) return;
if (!canManageTags.value) return;
if (!newTagName.value) return;
const tableId = await ensureTagsTable();
const table = await tablesService.getTable(tableId);
@@ -614,7 +632,7 @@ async function loadUserTags() {
// После добавления/удаления тегов всегда обновляем userTags
async function addTagsToUser() {
if (!canEdit.value) return;
if (!canManageTags.value) return;
if (!contact.value || !contact.value.id) return;
if (!selectedTags.value || selectedTags.value.length === 0) return;
try {
@@ -628,7 +646,7 @@ async function addTagsToUser() {
}
async function removeUserTag(tagId) {
if (!canEdit.value) return;
if (!canManageTags.value) return;
if (!contact.value || !contact.value.id) return;
try {
await contactsService.removeTagFromContact(contact.value.id, tagId);
@@ -644,6 +662,17 @@ onMounted(async () => {
await loadUserTags();
await loadMessages();
// Помечаем контакт как прочитанный при загрузке страницы
// Для всех админов (EDITOR и READONLY) - каждый видит свой статус просмотра
console.log('[ContactDetailsView] DEBUG - canViewContacts:', canViewContacts.value);
console.log('[ContactDetailsView] DEBUG - userId:', userId.value);
if (userId.value && canViewContacts.value) {
console.log('[ContactDetailsView] Marking contact as read (admin):', userId.value);
await markContactAsRead(userId.value);
} else if (userId.value) {
console.log('[ContactDetailsView] Skipping markContactAsRead - user is not admin');
}
// Подписываемся на обновления тегов
unsubscribeFromTags = onTagsUpdate(async () => {
// console.log('[ContactDetailsView] Получено обновление тегов, перезагружаем списки тегов');

View File

@@ -22,17 +22,13 @@
<!-- Заголовок страницы -->
<div class="page-header">
<div class="header-content">
<h1>📄 Управление контентом</h1>
<p v-if="isAdmin && address">Создавайте и управляйте страницами вашего DLE</p>
<h1>Управление контентом</h1>
<p v-if="canEditData && address">Создавайте и управляйте страницами вашего DLE</p>
<p v-else>Просмотр опубликованных страниц DLE</p>
<button v-if="isAdmin && address" class="btn btn-primary" @click="goToCreate">
<button v-if="canEditData && address" class="btn btn-primary" @click="goToCreate">
<i class="fas fa-plus"></i>
Создать страницу
</button>
<button v-else class="btn btn-primary" @click="goToPublicPages">
<i class="fas fa-eye"></i>
Публичные страницы
</button>
</div>
<div class="header-actions">
<button class="close-btn" @click="goBack">×</button>
@@ -68,7 +64,7 @@
<!-- Вкладка Страницы -->
<div v-if="activeTab === 'pages'" class="pages-section">
<div class="section-header">
<h2 v-if="isAdmin && address">Созданные страницы</h2>
<h2 v-if="canEditData && address">Созданные страницы</h2>
<h2 v-else>Опубликованные страницы</h2>
<div class="search-box">
<input
@@ -91,7 +87,7 @@
>
<div class="page-card-header">
<h3>{{ page.title }}</h3>
<div class="page-actions" v-if="isAdmin && address">
<div class="page-actions" v-if="canEditData && address">
<button
class="action-btn edit-btn"
@click.stop="goToEdit(page.id)"
@@ -133,18 +129,14 @@
<div class="empty-icon">
<i class="fas fa-file-alt"></i>
</div>
<h3 v-if="isAdmin && address">Нет созданных страниц</h3>
<h3 v-if="canEditData && address">Нет созданных страниц</h3>
<h3 v-else>Нет опубликованных страниц</h3>
<p v-if="isAdmin && address">Создайте первую страницу для вашего DLE</p>
<p v-if="canEditData && address">Создайте первую страницу для вашего DLE</p>
<p v-else>Публичные страницы появятся здесь после их создания администраторами</p>
<button v-if="isAdmin && address" class="btn btn-primary" @click="goToCreate">
<button v-if="canEditData && address" class="btn btn-primary" @click="goToCreate">
<i class="fas fa-plus"></i>
Создать страницу
</button>
<button v-else class="btn btn-primary" @click="goToPublicPages">
<i class="fas fa-eye"></i>
Публичные страницы
</button>
</div>
<!-- Загрузка -->
@@ -193,6 +185,7 @@ import { useRouter } from 'vue-router';
import BaseLayout from '../../components/BaseLayout.vue';
import pagesService from '../../services/pagesService';
import { useAuthContext } from '../../composables/useAuth';
import { usePermissions } from '../../composables/usePermissions';
// Props
const props = defineProps({
@@ -218,7 +211,22 @@ const props = defineProps({
const emit = defineEmits(['auth-action-completed']);
const router = useRouter();
const { isAdmin, address } = useAuthContext();
const { address } = useAuthContext();
const { canEditData } = usePermissions();
// Подписываемся на централизованные события очистки и обновления данных
onMounted(() => {
window.addEventListener('clear-application-data', () => {
console.log('[ContentListView] Clearing pages data');
// Очищаем данные при выходе из системы
pages.value = [];
});
window.addEventListener('refresh-application-data', () => {
console.log('[ContentListView] Refreshing pages data');
loadPages(); // Обновляем данные при входе в систему
});
});
// Состояние
const activeTab = ref('pages');
@@ -250,16 +258,13 @@ function goToCreate() {
router.push({ name: 'content-create' });
}
function goToPublicPages() {
router.push({ name: 'public-pages' });
}
function goBack() {
router.go(-1);
}
function goToPage(id) {
if (isAdmin.value && address.value) {
if (canEditData.value && address.value) {
router.push({ name: 'page-view', params: { id } });
} else {
router.push({ name: 'public-page-view', params: { id } });
@@ -307,7 +312,7 @@ async function loadPages() {
isLoading.value = true;
// Проверяем роль админа через кошелек
if (isAdmin.value && address.value) {
if (canEditData.value && address.value) {
try {
// Пытаемся загрузить админские страницы
const response = await pagesService.getPages();

View File

@@ -19,13 +19,7 @@
@auth-action-completed="$emit('auth-action-completed')"
>
<div class="public-page-view">
<!-- Кнопка назад -->
<div class="back-button">
<button class="btn btn-outline" @click="goBack">
<i class="fas fa-arrow-left"></i>
Назад к списку страниц
</button>
</div>
<button class="close-btn" @click="goBack">×</button>
<!-- Заголовок страницы -->
<div class="page-header" v-if="page">
@@ -127,7 +121,7 @@ const isLoading = ref(false);
// Методы
function goBack() {
router.push({ name: 'public-pages' });
router.push({ name: 'content-list' });
}
function formatDate(date) {
@@ -176,10 +170,24 @@ onMounted(() => {
width: 100%;
max-width: 1200px;
margin: 0 auto;
position: relative;
}
.back-button {
margin-bottom: 20px;
.close-btn {
position: absolute;
top: 18px;
right: 18px;
background: none;
border: none;
font-size: 2rem;
cursor: pointer;
color: #bbb;
transition: color 0.2s;
z-index: 10;
}
.close-btn:hover {
color: #333;
}
.page-header {

View File

@@ -99,6 +99,20 @@ const editMode = ref(false);
const auth = useAuthContext();
// Подписываемся на централизованные события очистки и обновления данных
onMounted(() => {
window.addEventListener('clear-application-data', () => {
console.log('[EmailSettingsView] Clearing Email settings data');
// Очищаем данные при выходе из системы
settings.value = { smtpHost: '', smtpPort: '', smtpUser: '', smtpPass: '', enabled: false };
});
window.addEventListener('refresh-application-data', () => {
console.log('[EmailSettingsView] Refreshing Email settings data');
loadEmailSettings(); // Обновляем данные при входе в систему
});
});
const loadEmailSettings = async () => {
// Не загружаем если не авторизован
if (!auth.isAuthenticated.value) {

View File

@@ -58,6 +58,20 @@ const editMode = ref(false);
const auth = useAuthContext();
// Подписываемся на централизованные события очистки и обновления данных
onMounted(() => {
window.addEventListener('clear-application-data', () => {
console.log('[TelegramSettingsView] Clearing Telegram settings data');
// Очищаем данные при выходе из системы
settings.value = { botToken: '', webhookUrl: '', enabled: false };
});
window.addEventListener('refresh-application-data', () => {
console.log('[TelegramSettingsView] Refreshing Telegram settings data');
loadTelegramSettings(); // Обновляем данные при входе в систему
});
});
const loadTelegramSettings = async () => {
// Не загружаем если не авторизован
if (!auth.isAuthenticated.value) {

View File

@@ -67,12 +67,26 @@
</template>
<script setup>
import { ref } from 'vue';
import { ref, onMounted } from 'vue';
import AIProviderSettings from './AIProviderSettings.vue';
import { useAuthContext } from '@/composables/useAuth';
import { usePermissions } from '@/composables/usePermissions';
import NoAccessModal from '@/components/NoAccessModal.vue';
// Подписываемся на централизованные события очистки и обновления данных
onMounted(() => {
window.addEventListener('clear-application-data', () => {
console.log('[AiSettingsView] Clearing AI settings data');
// Очищаем данные при выходе из системы
// AiSettingsView не нуждается в очистке данных
});
window.addEventListener('refresh-application-data', () => {
console.log('[AiSettingsView] Refreshing AI settings data');
// AiSettingsView не нуждается в обновлении данных
});
});
const showProvider = ref(null);
const showTelegramSettings = ref(false);
const showEmailSettings = ref(false);
@@ -80,7 +94,6 @@ const showDbSettings = ref(false);
const showAiAssistantSettings = ref(false);
const showNoAccessModal = ref(false);
const { isAdmin } = useAuthContext();
const { canManageSettings } = usePermissions();
const providerLabels = {

View File

@@ -35,9 +35,9 @@
<span><strong>Editor:</strong> {{ token.editorThreshold || 2 }} токен{{ token.editorThreshold === 1 ? '' : token.editorThreshold < 5 ? 'а' : 'ов' }}</span>
<button
class="btn btn-sm"
:class="canEdit ? 'btn-danger' : 'btn-secondary'"
@click="canEdit ? removeToken(index) : null"
:disabled="!canEdit"
:class="canManageSettings ? 'btn-danger' : 'btn-secondary'"
@click="canManageSettings ? removeToken(index) : null"
:disabled="!canManageSettings"
>
Удалить
</button>
@@ -53,7 +53,7 @@
v-model="newToken.name"
class="form-control"
placeholder="test2"
:disabled="!canEdit"
:disabled="!canManageSettings"
>
</div>
<div class="form-group">
@@ -63,12 +63,12 @@
v-model="newToken.address"
class="form-control"
placeholder="0x..."
:disabled="!canEdit"
:disabled="!canManageSettings"
>
</div>
<div class="form-group">
<label>Сеть:</label>
<select v-model="newToken.network" class="form-control" :disabled="!canEdit">
<select v-model="newToken.network" class="form-control" :disabled="!canManageSettings">
<option value="">-- Выберите сеть --</option>
<optgroup v-for="(group, groupIndex) in networkGroups" :key="groupIndex" :label="group.label">
<option v-for="option in group.options" :key="option.value" :value="option.value">
@@ -86,7 +86,7 @@
placeholder="0"
min="0"
step="0.01"
:disabled="!canEdit"
:disabled="!canManageSettings"
>
<small class="form-text">Минимальный баланс токена для получения доступа</small>
</div>
@@ -102,7 +102,7 @@
class="form-control"
placeholder="1"
min="1"
:disabled="!canEdit"
:disabled="!canManageSettings"
>
<small class="form-text">Количество токенов для получения прав только на чтение</small>
</div>
@@ -114,16 +114,16 @@
class="form-control"
placeholder="2"
min="2"
:disabled="!canEdit"
:disabled="!canManageSettings"
>
<small class="form-text">Количество токенов для получения прав на редактирование и удаление</small>
</div>
</div>
<button
class="btn"
:class="canEdit ? 'btn-primary' : 'btn-secondary'"
@click="canEdit ? addToken() : null"
:disabled="!canEdit"
:class="canManageSettings ? 'btn-primary' : 'btn-secondary'"
@click="canManageSettings ? addToken() : null"
:disabled="!canManageSettings"
>
Добавить токен
</button>
@@ -132,7 +132,7 @@
</template>
<script setup>
import { reactive, computed } from 'vue';
import { reactive, computed, onMounted } from 'vue';
import useBlockchainNetworks from '@/composables/useBlockchainNetworks';
import api from '@/api/axios';
import { useAuthContext } from '@/composables/useAuth';
@@ -152,8 +152,22 @@ const newToken = reactive({
});
const { networkGroups, networks } = useBlockchainNetworks();
const { isAdmin, checkTokenBalances, address, checkAuth, userAccessLevel, checkUserAccessLevel } = useAuthContext();
const { canEdit, getLevelClass, getLevelDescription } = usePermissions();
const { checkTokenBalances, address, checkAuth, userAccessLevel, checkUserAccessLevel } = useAuthContext();
const { canManageSettings, getLevelClass, getLevelDescription } = usePermissions();
// Подписываемся на централизованные события очистки и обновления данных
onMounted(() => {
window.addEventListener('clear-application-data', () => {
console.log('[AuthTokensSettings] Clearing tokens data');
// Очищаем данные при выходе из системы
tokens.value = [];
});
window.addEventListener('refresh-application-data', () => {
console.log('[AuthTokensSettings] Refreshing tokens data');
loadTokens(); // Обновляем данные при входе в систему
});
});
async function addToken() {
if (!newToken.name || !newToken.address || !newToken.network) {

View File

@@ -854,8 +854,8 @@
@click="deploySmartContracts"
type="button"
class="btn btn-primary btn-lg deploy-btn"
:disabled="!isFormValid || !canEdit || adminTokenCheck.isLoading"
:title="`isFormValid: ${isFormValid}, isAdmin: ${adminTokenCheck.isAdmin}, isLoading: ${adminTokenCheck.isLoading}`"
:disabled="!isFormValid || !canManageSettings || adminTokenCheck.isLoading"
:title="`isFormValid: ${isFormValid}, canManageSettings: ${canManageSettings}, isLoading: ${adminTokenCheck.isLoading}`"
>
<i class="fas fa-cogs"></i>
Поэтапный деплой DLE
@@ -921,13 +921,27 @@ function normalizePrivateKey(raw) {
// Получаем контекст авторизации для адреса кошелька
const { address, isAdmin } = useAuthContext();
const { canEdit } = usePermissions();
const { address } = useAuthContext();
const { canManageSettings } = usePermissions();
// Подписываемся на централизованные события очистки и обновления данных
onMounted(() => {
window.addEventListener('clear-application-data', () => {
console.log('[DleDeployFormView] Clearing DLE deploy data');
// Очищаем данные при выходе из системы
// DleDeployFormView не нуждается в очистке данных
});
window.addEventListener('refresh-application-data', () => {
console.log('[DleDeployFormView] Refreshing DLE deploy data');
checkAdminTokens(); // Обновляем данные при входе в систему
});
});
// Состояние для проверки админских токенов
const adminTokenCheck = ref({
isLoading: false,
isAdmin: false,
canManageSettings: false,
error: null
});
@@ -2381,7 +2395,7 @@ watch(address, (newAddress) => {
// Функция проверки админских токенов
const checkAdminTokens = async () => {
if (!address.value) {
adminTokenCheck.value = { isLoading: false, isAdmin: false, error: 'Кошелек не подключен' };
adminTokenCheck.value = { isLoading: false, canManageSettings: false, error: 'Кошелек не подключен' };
return;
}
@@ -2391,7 +2405,7 @@ const checkAdminTokens = async () => {
const response = await api.get(`/dle-v2/check-admin-tokens?address=${address.value}`);
if (response.data.success) {
adminTokenCheck.value = { ...adminTokenCheck.value, isAdmin: response.data.data.isAdmin };
adminTokenCheck.value = { ...adminTokenCheck.value, canManageSettings: response.data.data.userAccessLevel.hasAccess };
console.log('Проверка админских токенов:', response.data.data);
} else {
adminTokenCheck.value = { ...adminTokenCheck.value, error: response.data.message || 'Ошибка проверки токенов' };
@@ -2589,7 +2603,7 @@ const handleDeploymentCompleted = (result) => {
console.log('🔍 unifiedPrivateKey.value:', unifiedPrivateKey.value);
console.log('🔍 keyValidation.unified:', keyValidation.unified);
console.log('🔍 dleSettings.coordinates:', dleSettings.coordinates);
console.log('🔍 Кнопка должна быть активна:', !(!validation.jurisdiction || !validation.name || !validation.tokenSymbol || !validation.partners || !validation.partnersValid || !validation.quorum || !validation.networks || !validation.privateKey || !validation.keyValid || !validation.coordinates) && adminTokenCheck.value.isAdmin && !adminTokenCheck.value.isLoading);
console.log('🔍 Кнопка должна быть активна:', !(!validation.jurisdiction || !validation.name || !validation.tokenSymbol || !validation.partners || !validation.partnersValid || !validation.quorum || !validation.networks || !validation.privateKey || !validation.keyValid || !validation.coordinates) && adminTokenCheck.value.canManageSettings && !adminTokenCheck.value.isLoading);
return Boolean(
validation.jurisdiction &&

View File

@@ -73,7 +73,13 @@
<span class="feature"> Безопасно</span>
<span class="feature"> Для локальных и VPS</span>
</div>
<button class="btn-primary" @click="goToWebSsh">Подробнее</button>
<button
class="btn-primary"
@click="canManageSettings ? goToWebSsh() : null"
:disabled="!canManageSettings"
>
Подробнее
</button>
</div>
<!-- Модальное окно с формой WEB SSH -->
@@ -93,9 +99,23 @@ import { useRouter } from 'vue-router';
import { useAuthContext } from '@/composables/useAuth';
import { usePermissions } from '@/composables/usePermissions';
import NoAccessModal from '@/components/NoAccessModal.vue';
import { onMounted } from 'vue';
// Подписываемся на централизованные события очистки и обновления данных
onMounted(() => {
window.addEventListener('clear-application-data', () => {
console.log('[InterfaceSettingsView] Clearing interface data');
// Очищаем данные при выходе из системы
// InterfaceSettingsView не нуждается в очистке данных
});
window.addEventListener('refresh-application-data', () => {
console.log('[InterfaceSettingsView] Refreshing interface data');
// InterfaceSettingsView не нуждается в обновлении данных
});
});
import { ref } from 'vue';
const router = useRouter();
const { isAdmin } = useAuthContext();
const { canManageSettings } = usePermissions();
const goBack = () => router.push('/settings');

View File

@@ -28,7 +28,7 @@
</template>
<script setup>
import { ref } from 'vue';
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import WebSshForm from '@/components/WebSshForm.vue';
import Header from '@/components/Header.vue';
@@ -57,6 +57,20 @@ const toggleSidebar = () => {
const auth = useAuthContext();
const isAuthenticated = auth.isAuthenticated.value;
const identities = auth.identities?.value || [];
// Подписываемся на централизованные события очистки и обновления данных
onMounted(() => {
window.addEventListener('clear-application-data', () => {
console.log('[InterfaceWebSshView] Clearing WebSSH data');
// Очищаем данные при выходе из системы
// InterfaceWebSshView не нуждается в очистке данных
});
window.addEventListener('refresh-application-data', () => {
console.log('[InterfaceWebSshView] Refreshing WebSSH data');
// InterfaceWebSshView не нуждается в обновлении данных
});
});
const tokenBalances = auth.tokenBalances?.value || [];
const isLoadingTokens = false;
</script>

View File

@@ -80,6 +80,20 @@ import { usePermissions } from '@/composables/usePermissions';
import NoAccessModal from '@/components/NoAccessModal.vue';
import wsClient from '@/utils/websocket';
// Подписываемся на централизованные события очистки и обновления данных
onMounted(() => {
window.addEventListener('clear-application-data', () => {
console.log('[SecuritySettingsView] Clearing security data');
// Очищаем данные при выходе из системы
// SecuritySettingsView не нуждается в очистке данных
});
window.addEventListener('refresh-application-data', () => {
console.log('[SecuritySettingsView] Refreshing security data');
// SecuritySettingsView не нуждается в обновлении данных
});
});
// Состояние для отображения/скрытия дополнительных настроек
const showRpcSettings = ref(false);
const showAuthSettings = ref(false);
@@ -88,7 +102,6 @@ const isSaving = ref(false);
const showNoAccessModal = ref(false);
// Получаем контекст авторизации
const { isAdmin } = useAuthContext();
const { canManageSettings } = usePermissions();
// Настройки безопасности

View File

@@ -186,6 +186,20 @@ const { address, isAuthenticated, tokenBalances, checkTokenBalances } = useAuthC
const router = useRouter();
const route = useRoute();
// Подписываемся на централизованные события очистки и обновления данных
onMounted(() => {
window.addEventListener('clear-application-data', () => {
console.log('[CreateProposalView] Clearing DLE proposal data');
// Очищаем данные при выходе из системы
dleInfo.value = null;
});
window.addEventListener('refresh-application-data', () => {
console.log('[CreateProposalView] Refreshing DLE proposal data');
loadDLEInfo(); // Обновляем данные при входе в систему
});
});
// Получаем адрес DLE из URL
const dleAddress = computed(() => {
const address = route.query.address || props.dleAddress;

View File

@@ -154,6 +154,20 @@ const emit = defineEmits(['auth-action-completed']);
const router = useRouter();
const { address } = useAuthContext();
// Подписываемся на централизованные события очистки и обновления данных
onMounted(() => {
window.addEventListener('clear-application-data', () => {
console.log('[DleManagementView] Clearing DLE management data');
// Очищаем данные при выходе из системы
dles.value = [];
});
window.addEventListener('refresh-application-data', () => {
console.log('[DleManagementView] Refreshing DLE management data');
loadDleList(); // Обновляем данные при входе в систему
});
});
// Состояние формы
const isAdding = ref(false);

View File

@@ -110,6 +110,20 @@ const goBackToBlocks = () => {
// Получаем адрес пользователя из контекста аутентификации
const { address: userAddress } = useAuthContext();
// Подписываемся на централизованные события очистки и обновления данных
onMounted(() => {
window.addEventListener('clear-application-data', () => {
console.log('[SettingsView] Clearing DLE settings data');
// Очищаем данные при выходе из системы
dleInfo.value = null;
});
window.addEventListener('refresh-application-data', () => {
console.log('[SettingsView] Refreshing DLE settings data');
loadDLEInfo(); // Обновляем данные при входе в систему
});
});
// Загружаем информацию о DLE
const loadDLEInfo = async () => {
if (!address) {

View File

@@ -14,7 +14,7 @@
<BaseLayout>
<div class="create-table-container">
<h2>Создать новую таблицу</h2>
<form v-if="canEdit" @submit.prevent="handleCreateTable" class="create-table-form">
<form v-if="canEditData" @submit.prevent="handleCreateTable" class="create-table-form">
<label>Название таблицы</label>
<input v-model="newTableName" required placeholder="Введите название" />
<label>Описание</label>
@@ -38,7 +38,7 @@
</template>
<script setup>
import { ref } from 'vue';
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import BaseLayout from '../../components/BaseLayout.vue';
import tablesService from '../../services/tablesService';
@@ -49,8 +49,21 @@ const router = useRouter();
const newTableName = ref('');
const newTableDescription = ref('');
const newTableIsRagSourceId = ref(2);
const { isAdmin } = useAuthContext();
const { canEdit } = usePermissions();
const { canEditData } = usePermissions();
// Подписываемся на централизованные события очистки и обновления данных
onMounted(() => {
window.addEventListener('clear-application-data', () => {
console.log('[CreateTableView] Clearing form data');
// Очищаем данные формы при выходе из системы
form.value = { name: '', description: '' };
});
window.addEventListener('refresh-application-data', () => {
console.log('[CreateTableView] Refreshing form data');
// CreateTableView не нуждается в обновлении данных
});
});
async function handleCreateTable() {
if (!newTableName.value) return;

View File

@@ -16,10 +16,10 @@
<h2>Удалить таблицу?</h2>
<p>Вы уверены, что хотите удалить эту таблицу? Это действие необратимо.</p>
<div class="actions">
<button v-if="canDelete" class="danger" @click="remove">Удалить</button>
<button v-if="canDeleteData" class="danger" @click="remove">Удалить</button>
<button @click="cancel">Отмена</button>
</div>
<div v-if="!canDelete" class="empty-table-placeholder">Нет прав для удаления таблицы</div>
<div v-if="!canDeleteData" class="empty-table-placeholder">Нет прав для удаления таблицы</div>
</div>
</BaseLayout>
</template>
@@ -29,10 +29,24 @@ import BaseLayout from '../../components/BaseLayout.vue';
import axios from 'axios';
import { useAuthContext } from '@/composables/useAuth';
import { usePermissions } from '@/composables/usePermissions';
import { onMounted } from 'vue';
const $route = useRoute();
const router = useRouter();
const { isAdmin } = useAuthContext();
const { canDelete } = usePermissions();
const { canDeleteData } = usePermissions();
// Подписываемся на централизованные события очистки и обновления данных
onMounted(() => {
window.addEventListener('clear-application-data', () => {
console.log('[DeleteTableView] Clearing confirmation data');
// Очищаем данные при выходе из системы
table.value = null;
});
window.addEventListener('refresh-application-data', () => {
console.log('[DeleteTableView] Refreshing confirmation data');
// DeleteTableView не нуждается в обновлении данных
});
});
async function remove() {
await axios.delete(`/tables/${$route.params.id}`);

View File

@@ -17,10 +17,10 @@
<button class="nav-btn" @click="goToTables">Таблицы</button>
<button class="nav-btn" @click="goToCreate">Создать таблицу</button>
<button class="close-btn" @click="closeTable">Закрыть</button>
<button v-if="canEdit" class="action-btn" @click="goToEdit">Редактировать</button>
<button v-if="canDelete" class="danger-btn" @click="goToDelete">Удалить</button>
<button v-if="canEditData" class="action-btn" @click="goToEdit">Редактировать</button>
<button v-if="canDeleteData" class="danger-btn" @click="goToDelete">Удалить</button>
</div>
<UserTableView v-if="canRead" :table-id="Number($route.params.id)" />
<UserTableView v-if="canViewData" :table-id="Number($route.params.id)" />
<div v-else class="empty-table-placeholder">Нет данных для отображения</div>
</div>
</BaseLayout>
@@ -32,10 +32,25 @@ import UserTableView from '../../components/tables/UserTableView.vue';
import { useRoute, useRouter } from 'vue-router';
import { useAuthContext } from '@/composables/useAuth';
import { usePermissions } from '@/composables/usePermissions';
import { onMounted } from 'vue';
const $route = useRoute();
const router = useRouter();
const { isAdmin } = useAuthContext();
const { canRead, canEdit, canDelete } = usePermissions();
const { canViewData, canEditData, canDeleteData } = usePermissions();
// Подписываемся на централизованные события очистки и обновления данных
onMounted(() => {
window.addEventListener('clear-application-data', () => {
console.log('[TableView] Clearing table data');
// Очищаем данные при выходе из системы
tableData.value = [];
columns.value = [];
});
window.addEventListener('refresh-application-data', () => {
console.log('[TableView] Refreshing table data');
loadTableData(); // Обновляем данные при входе в систему
});
});
function closeTable() {
if (window.history.length > 1) {

View File

@@ -15,7 +15,7 @@
<div class="tables-list-block">
<button class="close-btn" @click="goBack">×</button>
<h2>Список таблиц</h2>
<UserTablesList v-if="canRead" />
<UserTablesList v-if="canViewData" />
<div v-else class="empty-table-placeholder">Нет данных для отображения</div>
</div>
</BaseLayout>
@@ -27,9 +27,24 @@ import UserTablesList from '../../components/tables/UserTablesList.vue';
import { useRouter } from 'vue-router';
import { useAuthContext } from '@/composables/useAuth';
import { usePermissions } from '@/composables/usePermissions';
import { onMounted } from 'vue';
const router = useRouter();
const { isAdmin } = useAuthContext();
const { canRead } = usePermissions();
const { canViewData } = usePermissions();
// Подписываемся на централизованные события очистки и обновления данных
onMounted(() => {
window.addEventListener('clear-application-data', () => {
console.log('[TablesListView] Clearing tables data');
// Очищаем данные при выходе из системы
tables.value = [];
});
window.addEventListener('refresh-application-data', () => {
console.log('[TablesListView] Refreshing tables data');
loadTables(); // Обновляем данные при входе в систему
});
});
function goBack() {
router.push({ name: 'crm' });
}