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

@@ -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' });
}