ваше сообщение коммита
This commit is contained in:
@@ -13,10 +13,10 @@
|
||||
<template>
|
||||
<div class="contact-table-modal">
|
||||
<div class="contact-table-header">
|
||||
<el-button type="info" :disabled="!selectedIds.length" @click="showBroadcastModal = true" style="margin-right: 1em;">Рассылка</el-button>
|
||||
<el-button type="warning" :disabled="!selectedIds.length" @click="deleteMessagesSelected" style="margin-right: 1em;">Удалить сообщения</el-button>
|
||||
<el-button type="danger" :disabled="!selectedIds.length" @click="deleteSelected" style="margin-right: 1em;">Удалить</el-button>
|
||||
<el-button type="primary" @click="showImportModal = true" 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>
|
||||
<button class="close-btn" @click="$emit('close')">×</button>
|
||||
</div>
|
||||
<el-form :inline="true" class="filters-form" label-position="top">
|
||||
@@ -74,7 +74,7 @@
|
||||
<table class="contact-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" v-model="selectAll" @change="toggleSelectAll" /></th>
|
||||
<th v-if="canEdit || canDelete || canManageSettings"><input type="checkbox" v-model="selectAll" @change="toggleSelectAll" /></th>
|
||||
<th>Имя</th>
|
||||
<th>Email</th>
|
||||
<th>Telegram</th>
|
||||
@@ -85,7 +85,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="contact in contactsArray" :key="contact.id" :class="{ 'new-contact-row': newIds.includes(contact.id) }">
|
||||
<td><input type="checkbox" v-model="selectedIds" :value="contact.id" /></td>
|
||||
<td v-if="canEdit || canDelete || canManageSettings"><input type="checkbox" v-model="selectedIds" :value="contact.id" /></td>
|
||||
<td>{{ contact.name || '-' }}</td>
|
||||
<td>{{ contact.email || '-' }}</td>
|
||||
<td>{{ contact.telegram || '-' }}</td>
|
||||
@@ -112,6 +112,7 @@ import BroadcastModal from './BroadcastModal.vue';
|
||||
import tablesService from '../services/tablesService';
|
||||
import messagesService from '../services/messagesService';
|
||||
import { useTagsWebSocket } from '../composables/useTagsWebSocket';
|
||||
import { usePermissions } from '@/composables/usePermissions';
|
||||
const props = defineProps({
|
||||
contacts: { type: Array, default: () => [] },
|
||||
newContacts: { type: Array, default: () => [] },
|
||||
@@ -123,6 +124,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 { canEdit, canDelete, canManageSettings } = usePermissions();
|
||||
|
||||
// Фильтры
|
||||
const filterSearch = ref('');
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<template>
|
||||
<template v-if="column.type === 'multiselect'">
|
||||
<div v-if="!editing" @click="editing = true" class="tags-cell-view">
|
||||
<div v-if="!editing" @click="canEdit && (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="editing = true" class="tags-cell-view">
|
||||
<div v-if="!editing" @click="canEdit && (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="editing = true" class="tags-cell-view">
|
||||
<div v-if="!editing" @click="canEdit && (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="editing = true">
|
||||
<div v-if="!editing" class="cell-view-value" @click="canEdit && (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="Добавить">
|
||||
@@ -128,8 +128,11 @@ import tablesService from '../../services/tablesService';
|
||||
import { useTablesWebSocket } from '../../composables/useTablesWebSocket';
|
||||
import { useTagsWebSocket } from '../../composables/useTagsWebSocket';
|
||||
import cacheService from '../../services/cacheService';
|
||||
import { usePermissions } from '@/composables/usePermissions';
|
||||
|
||||
const props = defineProps(['rowId', 'column', 'cellValues']);
|
||||
const emit = defineEmits(['update']);
|
||||
const { canEdit } = usePermissions();
|
||||
|
||||
const localValue = ref('');
|
||||
const editing = ref(false);
|
||||
|
||||
@@ -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 type="danger" :disabled="!selectedRows.length" @click="deleteSelectedRows">Удалить выбранные</el-button>
|
||||
<el-button v-if="canEdit" type="danger" :disabled="!selectedRows.length" @click="deleteSelectedRows">Удалить выбранные</el-button>
|
||||
<span v-if="selectedRows.length">Выбрано: {{ selectedRows.length }}</span>
|
||||
<button v-if="isAdmin" class="rebuild-btn" @click="rebuildIndex" :disabled="rebuilding">
|
||||
<button v-if="canEdit" 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 class="col-menu" @click.stop="openColMenu(col, $event)">⋮</button>
|
||||
<button v-if="canEdit" class="col-menu" @click.stop="openColMenu(col, $event)">⋮</button>
|
||||
</template>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
@@ -90,7 +90,7 @@
|
||||
:resizable="false"
|
||||
>
|
||||
<template #header>
|
||||
<button class="add-col-btn" @click.stop="openAddMenu($event)" title="Добавить">
|
||||
<button v-if="canEdit" 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 class="row-menu" @click.stop="openRowMenu(row, $event)">⋮</button>
|
||||
<button v-if="canEdit" 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>
|
||||
@@ -170,6 +170,7 @@ import { ref, onMounted, computed, watch, onUnmounted } from 'vue';
|
||||
import tablesService from '../../services/tablesService';
|
||||
import TableCell from './TableCell.vue';
|
||||
import { useAuthContext } from '@/composables/useAuth';
|
||||
import { usePermissions } from '@/composables/usePermissions';
|
||||
import axios from 'axios';
|
||||
// Импортируем компоненты Element Plus
|
||||
import { ElSelect, ElOption, ElButton } from 'element-plus';
|
||||
@@ -180,6 +181,7 @@ let unsubscribeFromTableUpdate = null;
|
||||
let unsubscribeFromTagsUpdate = null;
|
||||
|
||||
const { isAdmin } = useAuthContext();
|
||||
const { canEdit } = usePermissions();
|
||||
const rebuilding = ref(false);
|
||||
const rebuildStatus = ref(null);
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ const email = ref(null);
|
||||
const processedGuestIds = ref([]);
|
||||
const identities = ref([]);
|
||||
const tokenBalances = ref([]);
|
||||
const userAccessLevel = ref({ level: 'user', tokenCount: 0, hasAccess: false });
|
||||
|
||||
// Функция для обновления списка идентификаторов
|
||||
const updateIdentities = async () => {
|
||||
@@ -95,6 +96,20 @@ const checkTokenBalances = async (address) => {
|
||||
}
|
||||
};
|
||||
|
||||
const checkUserAccessLevel = async (address) => {
|
||||
try {
|
||||
const response = await axios.get(`/auth/access-level/${address}`);
|
||||
if (response.data.success) {
|
||||
userAccessLevel.value = response.data.data;
|
||||
return response.data.data;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error checking user access level:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const updateAuth = async ({
|
||||
authenticated,
|
||||
authType: newAuthType,
|
||||
@@ -140,9 +155,10 @@ const updateAuth = async ({
|
||||
})
|
||||
);
|
||||
|
||||
// Если аутентификация через кошелек, проверяем баланс токенов
|
||||
// Если аутентификация через кошелек, проверяем баланс токенов и уровень доступа
|
||||
if (authenticated && newAuthType === 'wallet' && newAddress) {
|
||||
await checkTokenBalances(newAddress);
|
||||
await checkUserAccessLevel(newAddress);
|
||||
}
|
||||
|
||||
// Обновляем идентификаторы при любом изменении аутентификации
|
||||
@@ -465,6 +481,7 @@ const authApi = {
|
||||
identities,
|
||||
processedGuestIds,
|
||||
tokenBalances,
|
||||
userAccessLevel,
|
||||
updateAuth,
|
||||
checkAuth,
|
||||
disconnect,
|
||||
@@ -475,6 +492,7 @@ const authApi = {
|
||||
linkIdentity,
|
||||
deleteIdentity,
|
||||
checkTokenBalances,
|
||||
checkUserAccessLevel,
|
||||
};
|
||||
|
||||
// === PROVIDE/INJECT HELPERS ===
|
||||
|
||||
105
frontend/src/composables/usePermissions.js
Normal file
105
frontend/src/composables/usePermissions.js
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
import { computed } from 'vue';
|
||||
import { useAuthContext } from './useAuth';
|
||||
|
||||
/**
|
||||
* Composable для работы с правами доступа
|
||||
* @returns {Object} - Объект с функциями для проверки прав доступа
|
||||
*/
|
||||
export function usePermissions() {
|
||||
const { userAccessLevel, isAdmin } = useAuthContext();
|
||||
|
||||
/**
|
||||
* Проверяет, может ли пользователь только читать данные
|
||||
*/
|
||||
const canRead = computed(() => {
|
||||
return userAccessLevel.value && userAccessLevel.value.hasAccess;
|
||||
});
|
||||
|
||||
/**
|
||||
* Проверяет, может ли пользователь редактировать данные
|
||||
*/
|
||||
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;
|
||||
});
|
||||
|
||||
/**
|
||||
* Получает описание текущего уровня доступа
|
||||
*/
|
||||
const getLevelDescription = (level) => {
|
||||
switch (level) {
|
||||
case 'readonly':
|
||||
return 'Только чтение';
|
||||
case 'editor':
|
||||
return 'Редактор';
|
||||
case 'user':
|
||||
default:
|
||||
return 'Пользователь';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Получает CSS класс для уровня доступа
|
||||
*/
|
||||
const getLevelClass = (level) => {
|
||||
switch (level) {
|
||||
case 'readonly':
|
||||
return 'access-readonly';
|
||||
case 'editor':
|
||||
return 'access-editor';
|
||||
case 'user':
|
||||
default:
|
||||
return 'access-user';
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
canRead,
|
||||
canEdit,
|
||||
canDelete,
|
||||
canManageSettings,
|
||||
currentLevel,
|
||||
tokenCount,
|
||||
getLevelDescription,
|
||||
getLevelClass
|
||||
};
|
||||
}
|
||||
@@ -212,6 +212,11 @@ const routes = [
|
||||
name: 'management-dle-management',
|
||||
component: () => import('../views/smartcontracts/DleManagementView.vue')
|
||||
},
|
||||
{
|
||||
path: '/management/dle-blocks',
|
||||
name: 'management-dle-blocks',
|
||||
component: () => import('../views/smartcontracts/DleBlocksManagementView.vue')
|
||||
},
|
||||
{
|
||||
path: '/management/proposals',
|
||||
name: 'management-proposals',
|
||||
@@ -277,6 +282,11 @@ const routes = [
|
||||
name: 'module-deploy-custom',
|
||||
component: () => import('../views/smartcontracts/modules/ModuleDeployFormView.vue')
|
||||
},
|
||||
{
|
||||
path: '/management/modules/deploy/inheritance',
|
||||
name: 'module-deploy-inheritance',
|
||||
component: () => import('../views/smartcontracts/modules/InheritanceModuleDeploy.vue')
|
||||
},
|
||||
// {
|
||||
// path: '/management/multisig',
|
||||
// name: 'management-multisig',
|
||||
|
||||
@@ -555,32 +555,47 @@ export async function deactivateDLE(dleAddress, userAddress) {
|
||||
throw new Error('Подключенный кошелек не совпадает с адресом пользователя');
|
||||
}
|
||||
|
||||
// Сначала проверяем возможность деактивации через API
|
||||
console.log('Проверяем возможность деактивации DLE через API...');
|
||||
const checkResponse = await api.post('/blockchain/deactivate-dle', {
|
||||
dleAddress: dleAddress,
|
||||
userAddress: userAddress
|
||||
});
|
||||
|
||||
if (!checkResponse.data.success) {
|
||||
throw new Error(checkResponse.data.error || 'Не удалось проверить возможность деактивации');
|
||||
}
|
||||
|
||||
console.log('Проверка деактивации прошла успешно, выполняем деактивацию...');
|
||||
|
||||
// ABI для деактивации DLE
|
||||
const dleAbi = [
|
||||
"function deactivate() external",
|
||||
"function balanceOf(address) external view returns (uint256)",
|
||||
"function totalSupply() external view returns (uint256)",
|
||||
"function createDeactivationProposal(string memory _description, uint256 _duration, uint256 _chainId) external returns (uint256)",
|
||||
"function voteDeactivation(uint256 _proposalId, bool _support) external",
|
||||
"function checkDeactivationProposalResult(uint256 _proposalId) public view returns (bool passed, bool quorumReached)",
|
||||
"function executeDeactivationProposal(uint256 _proposalId) external"
|
||||
"function isActive() external view returns (bool)"
|
||||
];
|
||||
|
||||
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
||||
|
||||
// Проверяем, что пользователь имеет токены
|
||||
// Дополнительные проверки перед деактивацией
|
||||
const balance = await dle.balanceOf(userAddress);
|
||||
if (balance <= 0) {
|
||||
throw new Error('Для деактивации DLE необходимо иметь токены');
|
||||
}
|
||||
|
||||
// Проверяем, что DLE не пустой (есть токены)
|
||||
const totalSupply = await dle.totalSupply();
|
||||
if (totalSupply <= 0) {
|
||||
throw new Error('DLE не имеет токенов');
|
||||
}
|
||||
|
||||
// Выполняем деактивацию (функция проверит наличие валидного предложения с кворумом)
|
||||
const isActive = await dle.isActive();
|
||||
if (!isActive) {
|
||||
throw new Error('DLE уже деактивирован');
|
||||
}
|
||||
|
||||
// Выполняем деактивацию
|
||||
console.log('Выполняем деактивацию DLE...');
|
||||
const tx = await dle.deactivate();
|
||||
const receipt = await tx.wait();
|
||||
|
||||
@@ -595,7 +610,25 @@ export async function deactivateDLE(dleAddress, userAddress) {
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка деактивации DLE:', error);
|
||||
throw error;
|
||||
|
||||
// Улучшенная обработка ошибок
|
||||
let errorMessage = 'Ошибка при деактивации DLE';
|
||||
|
||||
if (error.message.includes('execution reverted')) {
|
||||
errorMessage = '❌ Деактивация невозможна: не выполнены условия смарт-контракта. Возможно, требуется голосование участников или DLE уже деактивирован.';
|
||||
} else if (error.message.includes('владелец')) {
|
||||
errorMessage = '❌ Только владелец DLE может его деактивировать';
|
||||
} else if (error.message.includes('кошелек')) {
|
||||
errorMessage = '❌ Необходимо подключить кошелек';
|
||||
} else if (error.message.includes('деактивирован')) {
|
||||
errorMessage = '❌ DLE уже деактивирован';
|
||||
} else if (error.message.includes('токены')) {
|
||||
errorMessage = '❌ Для деактивации DLE необходимо иметь токены';
|
||||
} else {
|
||||
errorMessage = `❌ Ошибка: ${error.message}`;
|
||||
}
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<span>Контакты</span>
|
||||
<span v-if="newContacts.length" class="badge">+{{ newContacts.length }}</span>
|
||||
</div>
|
||||
<ContactTable v-if="isAdmin" :contacts="contacts" :new-contacts="newContacts" :new-messages="newMessages" @markNewAsRead="markContactsAsRead"
|
||||
<ContactTable v-if="canRead" :contacts="contacts" :new-contacts="newContacts" :new-messages="newMessages" @markNewAsRead="markContactsAsRead"
|
||||
:markMessagesAsReadForUser="markMessagesAsReadForUser" :markContactAsRead="markContactAsRead" @close="goBack" />
|
||||
|
||||
<!-- Таблица-заглушка для обычных пользователей -->
|
||||
@@ -92,6 +92,7 @@ import BaseLayout from '../components/BaseLayout.vue';
|
||||
import ContactTable from '../components/ContactTable.vue';
|
||||
import { useContactsAndMessagesWebSocket } from '../composables/useContactsWebSocket';
|
||||
import { useAuthContext } from '@/composables/useAuth';
|
||||
import { usePermissions } from '@/composables/usePermissions';
|
||||
|
||||
const {
|
||||
contacts, newContacts, newMessages,
|
||||
@@ -99,6 +100,7 @@ const {
|
||||
} = useContactsAndMessagesWebSocket();
|
||||
const router = useRouter();
|
||||
const { isAdmin } = useAuthContext();
|
||||
const { canRead } = usePermissions();
|
||||
|
||||
function goBack() {
|
||||
if (window.history.length > 1) {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
:is-loading-tokens="isLoadingTokens"
|
||||
@auth-action-completed="$emit('auth-action-completed')"
|
||||
>
|
||||
<template v-if="auth.isAdmin.value">
|
||||
<template v-if="auth.userAccessLevel.value && auth.userAccessLevel.value.hasAccess">
|
||||
<ChatInterface
|
||||
:messages="messages"
|
||||
:is-loading="isLoading || isConnectingWallet"
|
||||
|
||||
@@ -54,8 +54,7 @@
|
||||
v-for="dle in deployedDles"
|
||||
:key="dle.dleAddress"
|
||||
class="dle-card"
|
||||
:class="{ 'selected': selectedDle && selectedDle.dleAddress === dle.dleAddress }"
|
||||
@click="selectDle(dle)"
|
||||
@click="openDleManagement(dle.dleAddress)"
|
||||
>
|
||||
<div class="dle-header">
|
||||
<div class="dle-title-section">
|
||||
@@ -148,6 +147,7 @@
|
||||
</ul>
|
||||
<button class="details-btn btn-sm" @click.stop="refreshVerification(dle.dleAddress)">Обновить статус</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -157,64 +157,6 @@
|
||||
|
||||
|
||||
|
||||
<!-- Блоки управления выбранным DLE -->
|
||||
<div v-if="selectedDle" class="management-blocks">
|
||||
|
||||
<!-- Первый ряд -->
|
||||
<div class="blocks-row">
|
||||
<div class="management-block">
|
||||
<h3>Предложения</h3>
|
||||
<p>Создание, подписание, выполнение</p>
|
||||
<button class="details-btn" @click="openProposalsWithDle">Подробнее</button>
|
||||
</div>
|
||||
|
||||
<div class="management-block">
|
||||
<h3>Токены DLE</h3>
|
||||
<p>Балансы, трансферы, распределение</p>
|
||||
<button class="details-btn" @click="openTokensWithDle">Подробнее</button>
|
||||
</div>
|
||||
|
||||
<div class="management-block">
|
||||
<h3>Кворум</h3>
|
||||
<p>Настройки голосования</p>
|
||||
<button class="details-btn" @click="openQuorumWithDle">Подробнее</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Второй ряд -->
|
||||
<div class="blocks-row">
|
||||
<div class="management-block">
|
||||
<h3>Модули DLE</h3>
|
||||
<p>Установка, настройка, управление</p>
|
||||
<button class="details-btn" @click="openModulesWithDle">Подробнее</button>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="management-block">
|
||||
<h3>Аналитика</h3>
|
||||
<p>Графики, статистика, отчеты</p>
|
||||
<button class="details-btn" @click="openAnalyticsWithDle">Подробнее</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Третий ряд -->
|
||||
<div class="blocks-row">
|
||||
<div class="management-block">
|
||||
<h3>История</h3>
|
||||
<p>Лог операций, события, транзакции</p>
|
||||
<button class="details-btn" @click="openHistoryWithDle">Подробнее</button>
|
||||
</div>
|
||||
|
||||
<div class="management-block">
|
||||
<h3>Настройки</h3>
|
||||
<p>Параметры DLE, конфигурация</p>
|
||||
<button class="details-btn" @click="openSettingsWithDle">Подробнее</button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
@@ -245,7 +187,6 @@ const router = useRouter();
|
||||
// Состояние для DLE
|
||||
const deployedDles = ref([]);
|
||||
const isLoadingDles = ref(false);
|
||||
const selectedDle = ref(null);
|
||||
const verificationStatuses = ref({}); // { [address]: { address, chains: { [chainId]: { guid, status } } } }
|
||||
let verifyPollTimer = null;
|
||||
|
||||
@@ -431,14 +372,10 @@ function openDleOnEtherscan(address) {
|
||||
}
|
||||
|
||||
function openDleManagement(dleAddress) {
|
||||
// Переход к детальному управлению DLE (если нужно)
|
||||
router.push(`/management/dle-management?address=${dleAddress}`);
|
||||
// Переход к блокам управления DLE
|
||||
router.push(`/management/dle-blocks?address=${dleAddress}`);
|
||||
}
|
||||
|
||||
function selectDle(dle) {
|
||||
selectedDle.value = dle;
|
||||
console.log('Выбран DLE:', dle);
|
||||
}
|
||||
|
||||
async function refreshVerification(address) {
|
||||
try {
|
||||
@@ -474,56 +411,6 @@ async function pollVerifications() {
|
||||
// router.push('/management/multisig');
|
||||
// }
|
||||
|
||||
// Функции с передачей адреса DLE
|
||||
function openProposalsWithDle() {
|
||||
if (selectedDle.value) {
|
||||
router.push(`/management/proposals?address=${selectedDle.value.dleAddress}`);
|
||||
}
|
||||
}
|
||||
|
||||
function openTokensWithDle() {
|
||||
if (selectedDle.value) {
|
||||
router.push(`/management/tokens?address=${selectedDle.value.dleAddress}`);
|
||||
}
|
||||
}
|
||||
|
||||
function openQuorumWithDle() {
|
||||
if (selectedDle.value) {
|
||||
router.push(`/management/quorum?address=${selectedDle.value.dleAddress}`);
|
||||
}
|
||||
}
|
||||
|
||||
function openModulesWithDle() {
|
||||
if (selectedDle.value) {
|
||||
router.push(`/management/modules?address=${selectedDle.value.dleAddress}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function openAnalyticsWithDle() {
|
||||
if (selectedDle.value) {
|
||||
router.push(`/management/analytics?address=${selectedDle.value.dleAddress}`);
|
||||
}
|
||||
}
|
||||
|
||||
function openHistoryWithDle() {
|
||||
if (selectedDle.value) {
|
||||
router.push(`/management/history?address=${selectedDle.value.dleAddress}`);
|
||||
}
|
||||
}
|
||||
|
||||
function openSettingsWithDle() {
|
||||
if (selectedDle.value) {
|
||||
router.push(`/management/settings?address=${selectedDle.value.dleAddress}`);
|
||||
}
|
||||
}
|
||||
|
||||
// function openMultisigWithDle() {
|
||||
// if (selectedDle.value) {
|
||||
// router.push(`/management/multisig?address=${selectedDle.value.dleAddress}`);
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
|
||||
@@ -586,37 +473,6 @@ onBeforeUnmount(() => {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Блоки управления */
|
||||
.management-blocks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.blocks-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.management-block {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
padding: 2rem;
|
||||
min-width: 260px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.management-block:hover {
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Секция деплоированных DLE */
|
||||
.deployed-dles-section {
|
||||
@@ -905,6 +761,7 @@ onBeforeUnmount(() => {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
|
||||
/* Стили для отображения логотипа */
|
||||
.dle-title-section {
|
||||
display: flex;
|
||||
@@ -998,17 +855,6 @@ onBeforeUnmount(() => {
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.blocks-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.management-block {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.management-block h3 {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.partner-info {
|
||||
flex-direction: column;
|
||||
|
||||
@@ -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="isAdmin" class="delete-btn" @click="deleteContact" :disabled="isDeleting">Удалить</button>
|
||||
<button v-if="canDelete" class="delete-btn" @click="deleteContact" :disabled="isDeleting">Удалить</button>
|
||||
<button class="cancel-btn" @click="cancelDelete" :disabled="isDeleting">Отменить</button>
|
||||
</div>
|
||||
<div v-if="!isAdmin" class="empty-table-placeholder">Нет прав для удаления контакта</div>
|
||||
<div v-if="!canDelete" class="empty-table-placeholder">Нет прав для удаления контакта</div>
|
||||
<div v-if="error" class="error">{{ error }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -36,6 +36,7 @@ import { ref, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import contactsService from '../../services/contactsService.js';
|
||||
import { useAuthContext } from '@/composables/useAuth';
|
||||
import { usePermissions } from '@/composables/usePermissions';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -44,6 +45,7 @@ const isLoading = ref(true);
|
||||
const isDeleting = ref(false);
|
||||
const error = ref('');
|
||||
const { isAdmin } = useAuthContext();
|
||||
const { canDelete } = usePermissions();
|
||||
|
||||
function formatDate(date) {
|
||||
if (!date) return '-';
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<template>
|
||||
<BaseLayout>
|
||||
<div v-if="!isAdmin" class="empty-table-placeholder">Нет доступа</div>
|
||||
<div v-if="!canRead" class="empty-table-placeholder">Нет доступа</div>
|
||||
<div v-else class="contact-details-page">
|
||||
<div v-if="isLoading">Загрузка...</div>
|
||||
<div v-else-if="!contact">Контакт не найден</div>
|
||||
@@ -24,7 +24,7 @@
|
||||
<div class="contact-info-block">
|
||||
<div>
|
||||
<strong>Имя:</strong>
|
||||
<template v-if="isAdmin">
|
||||
<template v-if="canEdit">
|
||||
<input v-model="editableName" class="edit-input" @blur="saveName" @keyup.enter="saveName" />
|
||||
<span v-if="isSavingName" class="saving">Сохранение...</span>
|
||||
</template>
|
||||
@@ -41,9 +41,10 @@
|
||||
<div class="selected-langs">
|
||||
<span v-for="lang in selectedLanguages" :key="lang" class="lang-tag">
|
||||
{{ getLanguageLabel(lang) }}
|
||||
<span class="remove-tag" @click="removeLanguage(lang)">×</span>
|
||||
<span v-if="canEdit" class="remove-tag" @click="removeLanguage(lang)">×</span>
|
||||
</span>
|
||||
<input
|
||||
v-if="canEdit"
|
||||
v-model="langInput"
|
||||
@focus="showLangDropdown = true"
|
||||
@input="showLangDropdown = true"
|
||||
@@ -52,7 +53,7 @@
|
||||
placeholder="Добавить язык..."
|
||||
/>
|
||||
</div>
|
||||
<ul v-if="showLangDropdown" class="lang-dropdown">
|
||||
<ul v-if="showLangDropdown && canEdit" class="lang-dropdown">
|
||||
<li
|
||||
v-for="lang in filteredLanguages"
|
||||
:key="lang.value"
|
||||
@@ -71,15 +72,15 @@
|
||||
<strong>Теги пользователя:</strong>
|
||||
<span v-for="tag in userTags" :key="tag.id" class="user-tag">
|
||||
{{ tag.name }}
|
||||
<span class="remove-tag" @click="removeUserTag(tag.id)">×</span>
|
||||
<span v-if="canEdit" class="remove-tag" @click="removeUserTag(tag.id)">×</span>
|
||||
</span>
|
||||
<button class="add-tag-btn" @click="openTagModal">Добавить тег</button>
|
||||
<button v-if="canEdit" 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="isAdmin">
|
||||
<template v-if="canEdit">
|
||||
<el-button
|
||||
v-if="!contact.is_blocked"
|
||||
type="danger"
|
||||
@@ -108,14 +109,14 @@
|
||||
:isLoading="isLoadingMessages"
|
||||
:attachments="chatAttachments"
|
||||
:newMessage="chatNewMessage"
|
||||
:isAdmin="isAdmin"
|
||||
:isAdmin="canEdit"
|
||||
@send-message="handleSendMessage"
|
||||
@update:newMessage="val => chatNewMessage = val"
|
||||
@update:attachments="val => chatAttachments = val"
|
||||
@ai-reply="handleAiReply"
|
||||
/>
|
||||
</div>
|
||||
<el-dialog v-model="showTagModal" title="Добавить тег пользователю">
|
||||
<el-dialog v-if="canEdit" v-model="showTagModal" title="Добавить тег пользователю">
|
||||
<div v-if="allTags.length">
|
||||
<el-select
|
||||
v-model="selectedTags"
|
||||
@@ -158,6 +159,7 @@ import ChatInterface from '../../components/ChatInterface.vue';
|
||||
import contactsService from '../../services/contactsService.js';
|
||||
import messagesService from '../../services/messagesService.js';
|
||||
import { useAuthContext } from '@/composables/useAuth';
|
||||
import { usePermissions } from '@/composables/usePermissions';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import tablesService from '../../services/tablesService';
|
||||
import { useTagsWebSocket } from '../../composables/useTagsWebSocket';
|
||||
@@ -182,6 +184,7 @@ 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);
|
||||
|
||||
@@ -250,6 +253,7 @@ async function loadAllTags() {
|
||||
}
|
||||
|
||||
function openTagModal() {
|
||||
if (!canEdit.value) return;
|
||||
showTagModal.value = true;
|
||||
loadAllTags();
|
||||
}
|
||||
@@ -289,6 +293,7 @@ function getLanguageLabel(val) {
|
||||
return found ? found.label : val;
|
||||
}
|
||||
function addLanguage(lang) {
|
||||
if (!canEdit.value) return;
|
||||
if (!selectedLanguages.value.includes(lang)) {
|
||||
selectedLanguages.value.push(lang);
|
||||
saveLanguages();
|
||||
@@ -297,14 +302,17 @@ function addLanguage(lang) {
|
||||
showLangDropdown.value = false;
|
||||
}
|
||||
function addLanguageFromInput() {
|
||||
if (!canEdit.value) return;
|
||||
const found = filteredLanguages.value[0];
|
||||
if (found) addLanguage(found.value);
|
||||
}
|
||||
function removeLanguage(lang) {
|
||||
if (!canEdit.value) return;
|
||||
selectedLanguages.value = selectedLanguages.value.filter(l => l !== lang);
|
||||
saveLanguages();
|
||||
}
|
||||
function saveLanguages() {
|
||||
if (!canEdit.value) return;
|
||||
isSavingLangs.value = true;
|
||||
contactsService.updateContact(contact.value.id, { language: selectedLanguages.value })
|
||||
.then(() => reloadContact())
|
||||
@@ -529,6 +537,7 @@ async function unblockUser() {
|
||||
|
||||
// --- Теги ---
|
||||
async function createTag() {
|
||||
if (!canEdit.value) return;
|
||||
if (!newTagName.value) return;
|
||||
const tableId = await ensureTagsTable();
|
||||
const table = await tablesService.getTable(tableId);
|
||||
@@ -588,6 +597,7 @@ async function loadUserTags() {
|
||||
|
||||
// После добавления/удаления тегов всегда обновляем userTags
|
||||
async function addTagsToUser() {
|
||||
if (!canEdit.value) return;
|
||||
if (!contact.value || !contact.value.id) return;
|
||||
if (!selectedTags.value || selectedTags.value.length === 0) return;
|
||||
try {
|
||||
@@ -601,6 +611,7 @@ async function addTagsToUser() {
|
||||
}
|
||||
|
||||
async function removeUserTag(tagId) {
|
||||
if (!canEdit.value) return;
|
||||
if (!contact.value || !contact.value.id) return;
|
||||
try {
|
||||
await contactsService.removeTagFromContact(contact.value.id, tagId);
|
||||
|
||||
@@ -101,6 +101,80 @@
|
||||
{{ em.from_email }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<!-- Настройки RAG поиска -->
|
||||
<div class="rag-search-settings">
|
||||
<h3>Настройки RAG поиска</h3>
|
||||
|
||||
<!-- Метод поиска -->
|
||||
<label>Метод поиска</label>
|
||||
<select v-model="ragSettings.searchMethod">
|
||||
<option value="semantic">Только семантический поиск</option>
|
||||
<option value="keyword">Только поиск по ключевым словам</option>
|
||||
<option value="hybrid">Гибридный поиск</option>
|
||||
</select>
|
||||
|
||||
<!-- Количество результатов -->
|
||||
<label>Максимальное количество результатов поиска</label>
|
||||
<input type="number" v-model="ragSettings.maxResults" min="1" max="20" />
|
||||
|
||||
<!-- Порог релевантности -->
|
||||
<label>Порог релевантности ({{ ragSettings.relevanceThreshold }})</label>
|
||||
<input type="range" v-model="ragSettings.relevanceThreshold"
|
||||
min="0.01" max="1.0" step="0.01" />
|
||||
|
||||
<!-- Настройки извлечения ключевых слов -->
|
||||
<div class="keyword-settings">
|
||||
<h4>Извлечение ключевых слов</h4>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="ragSettings.keywordExtraction.enabled" />
|
||||
Включить извлечение ключевых слов
|
||||
</label>
|
||||
|
||||
<label>Минимальная длина слова</label>
|
||||
<input type="number" v-model="ragSettings.keywordExtraction.minWordLength"
|
||||
min="2" max="10" />
|
||||
|
||||
<label>Максимальное количество ключевых слов</label>
|
||||
<input type="number" v-model="ragSettings.keywordExtraction.maxKeywords"
|
||||
min="5" max="20" />
|
||||
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="ragSettings.keywordExtraction.removeStopWords" />
|
||||
Удалять стоп-слова
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Веса для гибридного поиска -->
|
||||
<div v-if="ragSettings.searchMethod === 'hybrid'" class="search-weights">
|
||||
<h4>Веса поиска</h4>
|
||||
<label>Семантический поиск: {{ ragSettings.searchWeights.semantic }}%</label>
|
||||
<input type="range" v-model="ragSettings.searchWeights.semantic"
|
||||
min="0" max="100" />
|
||||
|
||||
<label>Поиск по ключевым словам: {{ ragSettings.searchWeights.keyword }}%</label>
|
||||
<input type="range" v-model="ragSettings.searchWeights.keyword"
|
||||
min="0" max="100" />
|
||||
</div>
|
||||
|
||||
<!-- Дополнительные настройки -->
|
||||
<div class="advanced-settings">
|
||||
<h4>Дополнительные настройки</h4>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="ragSettings.advanced.enableFuzzySearch" />
|
||||
Нечеткий поиск
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="ragSettings.advanced.enableStemming" />
|
||||
Стемминг слов
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="ragSettings.advanced.enableSynonyms" />
|
||||
Поиск синонимов
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button type="submit">Сохранить</button>
|
||||
<button type="button" @click="goBack">Отмена</button>
|
||||
@@ -143,6 +217,29 @@ const placeholders = ref([]);
|
||||
const editingPlaceholder = ref(null);
|
||||
const editingPlaceholderValue = ref('');
|
||||
|
||||
// Настройки RAG поиска
|
||||
const ragSettings = ref({
|
||||
searchMethod: 'hybrid',
|
||||
maxResults: 5,
|
||||
relevanceThreshold: 0.1,
|
||||
keywordExtraction: {
|
||||
enabled: true,
|
||||
minWordLength: 3,
|
||||
maxKeywords: 10,
|
||||
removeStopWords: true,
|
||||
language: 'ru'
|
||||
},
|
||||
searchWeights: {
|
||||
semantic: 70,
|
||||
keyword: 30
|
||||
},
|
||||
advanced: {
|
||||
enableFuzzySearch: true,
|
||||
enableStemming: true,
|
||||
enableSynonyms: false
|
||||
}
|
||||
});
|
||||
|
||||
async function loadUserTables() {
|
||||
const { data } = await axios.get('/tables');
|
||||
userTables.value = Array.isArray(data) ? data : [];
|
||||
@@ -165,7 +262,14 @@ async function loadSettings() {
|
||||
}
|
||||
|
||||
settings.value = settingsData;
|
||||
|
||||
// Загружаем настройки RAG, если они есть
|
||||
if (data.settings.ragSettings) {
|
||||
ragSettings.value = { ...ragSettings.value, ...data.settings.ragSettings };
|
||||
}
|
||||
|
||||
console.log('[AiAssistantSettings] Loaded settings:', settings.value);
|
||||
console.log('[AiAssistantSettings] Loaded RAG settings:', ragSettings.value);
|
||||
}
|
||||
}
|
||||
async function loadTelegramBots() {
|
||||
@@ -225,7 +329,11 @@ async function saveSettings() {
|
||||
settingsToSave.selected_rag_tables = [settingsToSave.selected_rag_tables];
|
||||
}
|
||||
|
||||
// Добавляем настройки RAG
|
||||
settingsToSave.ragSettings = ragSettings.value;
|
||||
|
||||
console.log('[AiAssistantSettings] Saving settings:', settingsToSave);
|
||||
console.log('[AiAssistantSettings] Saving RAG settings:', ragSettings.value);
|
||||
await axios.put('/settings/ai-assistant', settingsToSave);
|
||||
goBack();
|
||||
}
|
||||
@@ -411,4 +519,63 @@ button[type="button"] {
|
||||
font-size: 1em;
|
||||
margin: 0.7em 0;
|
||||
}
|
||||
|
||||
/* Стили для настроек RAG поиска */
|
||||
.rag-search-settings {
|
||||
margin: 2rem 0;
|
||||
padding: 1.5rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.rag-search-settings h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1.5rem;
|
||||
color: #333;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.keyword-settings, .search-weights, .advanced-settings {
|
||||
margin: 1rem 0;
|
||||
padding: 1rem;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.keyword-settings h4, .search-weights h4, .advanced-settings h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
color: #555;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.search-weights input[type="range"] {
|
||||
width: 100%;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin: 0.5rem 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.checkbox-label input[type="checkbox"] {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.rag-search-settings input[type="range"] {
|
||||
width: 100%;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.rag-search-settings input[type="number"] {
|
||||
width: 100px;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -70,6 +70,7 @@
|
||||
import { ref } from 'vue';
|
||||
import AIProviderSettings from './AIProviderSettings.vue';
|
||||
import { useAuthContext } from '@/composables/useAuth';
|
||||
import { usePermissions } from '@/composables/usePermissions';
|
||||
import NoAccessModal from '@/components/NoAccessModal.vue';
|
||||
|
||||
const showProvider = ref(null);
|
||||
@@ -80,6 +81,7 @@ const showAiAssistantSettings = ref(false);
|
||||
const showNoAccessModal = ref(false);
|
||||
|
||||
const { isAdmin } = useAuthContext();
|
||||
const { canManageSettings } = usePermissions();
|
||||
|
||||
const providerLabels = {
|
||||
openai: {
|
||||
@@ -117,7 +119,7 @@ const providerLabels = {
|
||||
};
|
||||
|
||||
function goTo(path) {
|
||||
if (!isAdmin.value) {
|
||||
if (!canManageSettings.value) {
|
||||
showNoAccessModal.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -13,17 +13,31 @@
|
||||
<template>
|
||||
<div class="auth-tokens-settings">
|
||||
<h4>Токены аутентификации</h4>
|
||||
|
||||
<!-- Отображение текущего уровня доступа -->
|
||||
<div v-if="userAccessLevel && userAccessLevel.hasAccess" class="access-level-info">
|
||||
<div class="access-level-badge" :class="getLevelClass(userAccessLevel.level)">
|
||||
<i class="fas fa-shield-alt"></i>
|
||||
<span>{{ getLevelDescription(userAccessLevel.level) }}</span>
|
||||
<span class="token-count">({{ userAccessLevel.tokenCount }} токен{{ userAccessLevel.tokenCount === 1 ? '' : userAccessLevel.tokenCount < 5 ? 'а' : 'ов' }})</span>
|
||||
</div>
|
||||
<div class="access-level-description">
|
||||
{{ getAccessLevelDescription(userAccessLevel.level) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="authTokens.length > 0" class="tokens-list">
|
||||
<div v-for="(token, index) in authTokens" :key="token.address + token.network" class="token-entry">
|
||||
<span><strong>Название:</strong> {{ token.name }}</span>
|
||||
<span><strong>Адрес:</strong> {{ token.address }}</span>
|
||||
<span><strong>Сеть:</strong> {{ getNetworkLabel(token.network) }}</span>
|
||||
<span><strong>Мин. баланс:</strong> {{ token.minBalance }}</span>
|
||||
<span><strong>Read-Only:</strong> {{ token.readonlyThreshold || 1 }} токен{{ token.readonlyThreshold === 1 ? '' : 'а' }}</span>
|
||||
<span><strong>Editor:</strong> {{ token.editorThreshold || 2 }} токен{{ token.editorThreshold === 1 ? '' : token.editorThreshold < 5 ? 'а' : 'ов' }}</span>
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
:class="isAdmin ? 'btn-danger' : 'btn-secondary'"
|
||||
@click="isAdmin ? removeToken(index) : null"
|
||||
:disabled="!isAdmin"
|
||||
:class="canEdit ? 'btn-danger' : 'btn-secondary'"
|
||||
@click="canEdit ? removeToken(index) : null"
|
||||
:disabled="!canEdit"
|
||||
>
|
||||
Удалить
|
||||
</button>
|
||||
@@ -39,7 +53,7 @@
|
||||
v-model="newToken.name"
|
||||
class="form-control"
|
||||
placeholder="test2"
|
||||
:disabled="!isAdmin"
|
||||
:disabled="!canEdit"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -49,12 +63,12 @@
|
||||
v-model="newToken.address"
|
||||
class="form-control"
|
||||
placeholder="0x..."
|
||||
:disabled="!isAdmin"
|
||||
:disabled="!canEdit"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Сеть:</label>
|
||||
<select v-model="newToken.network" class="form-control" :disabled="!isAdmin">
|
||||
<select v-model="newToken.network" class="form-control" :disabled="!canEdit">
|
||||
<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">
|
||||
@@ -70,14 +84,43 @@
|
||||
v-model.number="newToken.minBalance"
|
||||
class="form-control"
|
||||
placeholder="0"
|
||||
:disabled="!isAdmin"
|
||||
:disabled="!canEdit"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Настройки прав доступа -->
|
||||
<div class="access-settings">
|
||||
<h6>Настройки прав доступа</h6>
|
||||
<div class="form-group">
|
||||
<label>Минимум токенов для Read-Only доступа:</label>
|
||||
<input
|
||||
type="number"
|
||||
v-model="newToken.readonlyThreshold"
|
||||
class="form-control"
|
||||
placeholder="1"
|
||||
min="1"
|
||||
:disabled="!canEdit"
|
||||
>
|
||||
<small class="form-text">Количество токенов для получения прав только на чтение</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Минимум токенов для Editor доступа:</label>
|
||||
<input
|
||||
type="number"
|
||||
v-model="newToken.editorThreshold"
|
||||
class="form-control"
|
||||
placeholder="2"
|
||||
min="2"
|
||||
:disabled="!canEdit"
|
||||
>
|
||||
<small class="form-text">Количество токенов для получения прав на редактирование и удаление</small>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="btn"
|
||||
:class="isAdmin ? 'btn-secondary' : 'btn-secondary'"
|
||||
@click="isAdmin ? addToken() : null"
|
||||
:disabled="!isAdmin"
|
||||
:class="canEdit ? 'btn-primary' : 'btn-secondary'"
|
||||
@click="canEdit ? addToken() : null"
|
||||
:disabled="!canEdit"
|
||||
>
|
||||
Добавить токен
|
||||
</button>
|
||||
@@ -86,36 +129,58 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive } from 'vue';
|
||||
import { reactive, computed } from 'vue';
|
||||
import useBlockchainNetworks from '@/composables/useBlockchainNetworks';
|
||||
import api from '@/api/axios';
|
||||
import { useAuthContext } from '@/composables/useAuth';
|
||||
import { usePermissions } from '@/composables/usePermissions';
|
||||
import eventBus from '@/utils/eventBus';
|
||||
const props = defineProps({
|
||||
authTokens: { type: Array, required: true }
|
||||
});
|
||||
const emit = defineEmits(['update']);
|
||||
const newToken = reactive({ name: '', address: '', network: '', minBalance: 0 });
|
||||
const newToken = reactive({
|
||||
name: '',
|
||||
address: '',
|
||||
network: '',
|
||||
minBalance: 0,
|
||||
readonlyThreshold: 1,
|
||||
editorThreshold: 2
|
||||
});
|
||||
|
||||
const { networkGroups, networks } = useBlockchainNetworks();
|
||||
const { isAdmin, checkTokenBalances, address, checkAuth } = useAuthContext();
|
||||
const { isAdmin, checkTokenBalances, address, checkAuth, userAccessLevel, checkUserAccessLevel } = useAuthContext();
|
||||
const { canEdit, getLevelClass, getLevelDescription } = usePermissions();
|
||||
|
||||
async function addToken() {
|
||||
if (!newToken.name || !newToken.address || !newToken.network) {
|
||||
alert('Все поля обязательны');
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenData = {
|
||||
name: newToken.name,
|
||||
address: newToken.address,
|
||||
network: newToken.network,
|
||||
minBalance: Number(newToken.minBalance) || 0,
|
||||
readonlyThreshold: newToken.readonlyThreshold !== null && newToken.readonlyThreshold !== undefined && newToken.readonlyThreshold !== '' ? Number(newToken.readonlyThreshold) : 1,
|
||||
editorThreshold: newToken.editorThreshold !== null && newToken.editorThreshold !== undefined && newToken.editorThreshold !== '' ? Number(newToken.editorThreshold) : 2
|
||||
};
|
||||
|
||||
console.log('[AuthTokensSettings] Отправляем данные токена:', tokenData);
|
||||
console.log('[AuthTokensSettings] newToken объект:', newToken);
|
||||
console.log('[AuthTokensSettings] newToken.readonlyThreshold:', newToken.readonlyThreshold, 'тип:', typeof newToken.readonlyThreshold);
|
||||
console.log('[AuthTokensSettings] newToken.editorThreshold:', newToken.editorThreshold, 'тип:', typeof newToken.editorThreshold);
|
||||
|
||||
try {
|
||||
await api.post('/settings/auth-token', {
|
||||
...newToken,
|
||||
minBalance: Number(newToken.minBalance) || 0
|
||||
});
|
||||
await api.post('/settings/auth-token', tokenData);
|
||||
|
||||
// После добавления токена перепроверяем баланс пользователя и обновляем состояние аутентификации
|
||||
try {
|
||||
if (address.value) {
|
||||
await checkTokenBalances(address.value);
|
||||
console.log('[AuthTokensSettings] Баланс токенов перепроверен после добавления');
|
||||
await checkUserAccessLevel(address.value);
|
||||
console.log('[AuthTokensSettings] Баланс токенов и уровень доступа перепроверены после добавления');
|
||||
}
|
||||
|
||||
// Обновляем состояние аутентификации чтобы отразить изменения роли
|
||||
@@ -138,6 +203,8 @@ async function addToken() {
|
||||
newToken.address = '';
|
||||
newToken.network = '';
|
||||
newToken.minBalance = 0;
|
||||
newToken.readonlyThreshold = 1;
|
||||
newToken.editorThreshold = 2;
|
||||
} catch (e) {
|
||||
alert('Ошибка при добавлении токена: ' + (e.response?.data?.error || e.message));
|
||||
}
|
||||
@@ -159,7 +226,8 @@ async function removeToken(index) {
|
||||
try {
|
||||
if (address.value) {
|
||||
await checkTokenBalances(address.value);
|
||||
console.log('[AuthTokensSettings] Баланс токенов перепроверен после удаления');
|
||||
await checkUserAccessLevel(address.value);
|
||||
console.log('[AuthTokensSettings] Баланс токенов и уровень доступа перепроверены после удаления');
|
||||
}
|
||||
|
||||
// Обновляем состояние аутентификации чтобы отразить изменения роли
|
||||
@@ -188,13 +256,118 @@ function getNetworkLabel(networkId) {
|
||||
const found = networks.value.find(n => n.value === networkId);
|
||||
return found ? found.label : networkId;
|
||||
}
|
||||
|
||||
|
||||
function getAccessLevelDescription(level) {
|
||||
switch (level) {
|
||||
case 'readonly':
|
||||
return 'Можете просматривать данные, но не можете редактировать или удалять';
|
||||
case 'editor':
|
||||
return 'Можете просматривать, редактировать и удалять данные';
|
||||
case 'user':
|
||||
default:
|
||||
return 'Базовые права пользователя';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tokens-list { margin-bottom: 1rem; }
|
||||
.token-entry { display: flex; gap: 1rem; align-items: center; margin-bottom: 0.5rem; }
|
||||
.token-entry {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
padding: 0.75rem;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.token-entry span {
|
||||
font-size: 0.875rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.add-token-form { margin-top: 1rem; }
|
||||
|
||||
/* Стили для секции настроек прав доступа */
|
||||
.access-settings {
|
||||
margin-top: 1.5rem;
|
||||
padding: 1rem;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.access-settings h6 {
|
||||
margin-bottom: 1rem;
|
||||
color: #495057;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-text {
|
||||
display: block;
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* Стили для отображения уровня доступа */
|
||||
.access-level-info {
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1rem;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #007bff;
|
||||
}
|
||||
|
||||
.access-level-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.access-level-badge i {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.access-readonly {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
border: 1px solid #ffeaa7;
|
||||
}
|
||||
|
||||
.access-editor {
|
||||
background-color: #d1ecf1;
|
||||
color: #0c5460;
|
||||
border: 1px solid #bee5eb;
|
||||
}
|
||||
|
||||
.access-user {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.token-count {
|
||||
font-weight: 400;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.access-level-description {
|
||||
font-size: 0.85rem;
|
||||
color: #6c757d;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* Стили для неактивных кнопок */
|
||||
.btn[disabled], .btn:disabled {
|
||||
background: #e0e0e0 !important;
|
||||
|
||||
@@ -858,7 +858,7 @@
|
||||
@click="deploySmartContracts"
|
||||
type="button"
|
||||
class="btn btn-primary btn-lg deploy-btn"
|
||||
:disabled="!isFormValid || !adminTokenCheck.isAdmin || adminTokenCheck.isLoading || showDeployProgress"
|
||||
:disabled="!isFormValid || !canEdit || adminTokenCheck.isLoading || showDeployProgress"
|
||||
:title="`isFormValid: ${isFormValid}, isAdmin: ${adminTokenCheck.isAdmin}, isLoading: ${adminTokenCheck.isLoading}, showDeployProgress: ${showDeployProgress}`"
|
||||
>
|
||||
<i class="fas fa-cogs"></i>
|
||||
@@ -941,6 +941,7 @@
|
||||
import { reactive, ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAuthContext } from '@/composables/useAuth';
|
||||
import { usePermissions } from '@/composables/usePermissions';
|
||||
import api from '@/api/axios';
|
||||
import DeploymentWizard from '@/components/deployment/DeploymentWizard.vue';
|
||||
|
||||
@@ -959,6 +960,7 @@ function normalizePrivateKey(raw) {
|
||||
|
||||
// Получаем контекст авторизации для адреса кошелька
|
||||
const { address, isAdmin } = useAuthContext();
|
||||
const { canEdit } = usePermissions();
|
||||
|
||||
// Состояние для проверки админских токенов
|
||||
const adminTokenCheck = ref({
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
</div>
|
||||
<button
|
||||
class="btn-primary"
|
||||
@click="isAdmin ? goToAkashDetails() : null"
|
||||
:disabled="!isAdmin"
|
||||
@click="canManageSettings ? goToAkashDetails() : null"
|
||||
:disabled="!canManageSettings"
|
||||
>
|
||||
Подробнее
|
||||
</button>
|
||||
@@ -54,8 +54,8 @@
|
||||
</div>
|
||||
<button
|
||||
class="btn-primary"
|
||||
@click="isAdmin ? goToFluxDetails() : null"
|
||||
:disabled="!isAdmin"
|
||||
@click="canManageSettings ? goToFluxDetails() : null"
|
||||
:disabled="!canManageSettings"
|
||||
>
|
||||
Подробнее
|
||||
</button>
|
||||
@@ -91,10 +91,12 @@
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAuthContext } from '@/composables/useAuth';
|
||||
import { usePermissions } from '@/composables/usePermissions';
|
||||
import NoAccessModal from '@/components/NoAccessModal.vue';
|
||||
import { ref } from 'vue';
|
||||
const router = useRouter();
|
||||
const { isAdmin } = useAuthContext();
|
||||
const { canManageSettings } = usePermissions();
|
||||
const goBack = () => router.push('/settings');
|
||||
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@ import RpcProvidersSettings from './RpcProvidersSettings.vue';
|
||||
import AuthTokensSettings from './AuthTokensSettings.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAuthContext } from '@/composables/useAuth';
|
||||
import { usePermissions } from '@/composables/usePermissions';
|
||||
import NoAccessModal from '@/components/NoAccessModal.vue';
|
||||
import wsClient from '@/utils/websocket';
|
||||
|
||||
@@ -88,6 +89,7 @@ const showNoAccessModal = ref(false);
|
||||
|
||||
// Получаем контекст авторизации
|
||||
const { isAdmin } = useAuthContext();
|
||||
const { canManageSettings } = usePermissions();
|
||||
|
||||
// Настройки безопасности
|
||||
const securitySettings = reactive({
|
||||
@@ -168,7 +170,9 @@ const loadSettings = async () => {
|
||||
name: token.name,
|
||||
address: token.address,
|
||||
network: token.network,
|
||||
minBalance: token.min_balance
|
||||
minBalance: token.min_balance,
|
||||
readonlyThreshold: token.readonly_threshold || 1,
|
||||
editorThreshold: token.editor_threshold || 2
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -331,11 +335,11 @@ provide('networks', networks);
|
||||
|
||||
// Функция для обработки клика по кнопке "Подробнее" для RPC провайдеров
|
||||
const handleRpcDetailsClick = () => {
|
||||
if (isAdmin.value) {
|
||||
// Если администратор - показываем детали RPC
|
||||
if (canManageSettings.value) {
|
||||
// Если есть права на управление настройками - показываем детали RPC
|
||||
showRpcSettings.value = !showRpcSettings.value;
|
||||
} else {
|
||||
// Если обычный пользователь - показываем модальное окно с ограничением доступа
|
||||
// Если нет прав - показываем модальное окно с ограничением доступа
|
||||
showNoAccessModal.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<p v-else-if="isLoadingDle">Загрузка...</p>
|
||||
<p v-else>Подробная аналитика и статистика DLE</p>
|
||||
</div>
|
||||
<button class="close-btn" @click="router.push('/management')">×</button>
|
||||
<button class="close-btn" @click="goBackToBlocks">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Основная информация -->
|
||||
@@ -252,6 +252,15 @@ const route = useRoute();
|
||||
// Получаем адрес DLE из URL параметров
|
||||
const dleAddress = ref(route.query.address || '');
|
||||
|
||||
// Функция возврата к блокам управления
|
||||
const goBackToBlocks = () => {
|
||||
if (dleAddress.value) {
|
||||
router.push(`/management/dle-blocks?address=${dleAddress.value}`);
|
||||
} else {
|
||||
router.push('/management');
|
||||
}
|
||||
};
|
||||
|
||||
// Состояние
|
||||
const selectedDle = ref(null);
|
||||
const isLoadingDle = ref(false);
|
||||
|
||||
298
frontend/src/views/smartcontracts/DleBlocksManagementView.vue
Normal file
298
frontend/src/views/smartcontracts/DleBlocksManagementView.vue
Normal file
@@ -0,0 +1,298 @@
|
||||
<!--
|
||||
Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
All rights reserved.
|
||||
|
||||
This software is proprietary and confidential.
|
||||
Unauthorized copying, modification, or distribution is prohibited.
|
||||
|
||||
For licensing inquiries: info@hb3-accelerator.com
|
||||
Website: https://hb3-accelerator.com
|
||||
GitHub: https://github.com/HB3-ACCELERATOR
|
||||
-->
|
||||
|
||||
<template>
|
||||
<BaseLayout
|
||||
:is-authenticated="isAuthenticated"
|
||||
:identities="identities"
|
||||
:token-balances="tokenBalances"
|
||||
:is-loading-tokens="isLoadingTokens"
|
||||
@auth-action-completed="$emit('auth-action-completed')"
|
||||
>
|
||||
<div class="dle-blocks-management">
|
||||
<!-- Заголовок -->
|
||||
<div class="management-header">
|
||||
<div class="header-content">
|
||||
<h1>Управление DLE</h1>
|
||||
<p v-if="dleAddress" class="dle-address">
|
||||
<strong>DLE:</strong> {{ dleAddress }}
|
||||
</p>
|
||||
</div>
|
||||
<button class="close-btn" @click="router.push('/management')">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Блоки управления -->
|
||||
<div class="management-blocks">
|
||||
<!-- Первый ряд -->
|
||||
<div class="blocks-row">
|
||||
<div class="management-block">
|
||||
<h3>Предложения</h3>
|
||||
<p>Создание, подписание, выполнение</p>
|
||||
<button class="details-btn" @click="openProposals">Подробнее</button>
|
||||
</div>
|
||||
|
||||
<div class="management-block">
|
||||
<h3>Токены DLE</h3>
|
||||
<p>Балансы, трансферы, распределение</p>
|
||||
<button class="details-btn" @click="openTokens">Подробнее</button>
|
||||
</div>
|
||||
|
||||
<div class="management-block">
|
||||
<h3>Кворум</h3>
|
||||
<p>Настройки голосования</p>
|
||||
<button class="details-btn" @click="openQuorum">Подробнее</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Второй ряд -->
|
||||
<div class="blocks-row">
|
||||
<div class="management-block">
|
||||
<h3>Модули DLE</h3>
|
||||
<p>Установка, настройка, управление</p>
|
||||
<button class="details-btn" @click="openModules">Подробнее</button>
|
||||
</div>
|
||||
|
||||
<div class="management-block">
|
||||
<h3>Аналитика</h3>
|
||||
<p>Графики, статистика, отчеты</p>
|
||||
<button class="details-btn" @click="openAnalytics">Подробнее</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Третий ряд -->
|
||||
<div class="blocks-row">
|
||||
<div class="management-block">
|
||||
<h3>История</h3>
|
||||
<p>Лог операций, события, транзакции</p>
|
||||
<button class="details-btn" @click="openHistory">Подробнее</button>
|
||||
</div>
|
||||
|
||||
<div class="management-block">
|
||||
<h3>Настройки</h3>
|
||||
<p>Параметры DLE, конфигурация</p>
|
||||
<button class="details-btn" @click="openSettings">Подробнее</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import BaseLayout from '../../components/BaseLayout.vue';
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
isAuthenticated: Boolean,
|
||||
identities: Array,
|
||||
tokenBalances: Object,
|
||||
isLoadingTokens: Boolean
|
||||
});
|
||||
|
||||
const emit = defineEmits(['auth-action-completed']);
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// Получаем адрес DLE из query параметров
|
||||
const dleAddress = computed(() => route.query.address || null);
|
||||
|
||||
// Функции для открытия страниц управления
|
||||
const openProposals = () => {
|
||||
if (dleAddress.value) {
|
||||
router.push(`/management/proposals?address=${dleAddress.value}`);
|
||||
} else {
|
||||
router.push('/management/proposals');
|
||||
}
|
||||
};
|
||||
|
||||
const openTokens = () => {
|
||||
if (dleAddress.value) {
|
||||
router.push(`/management/tokens?address=${dleAddress.value}`);
|
||||
} else {
|
||||
router.push('/management/tokens');
|
||||
}
|
||||
};
|
||||
|
||||
const openQuorum = () => {
|
||||
if (dleAddress.value) {
|
||||
router.push(`/management/quorum?address=${dleAddress.value}`);
|
||||
} else {
|
||||
router.push('/management/quorum');
|
||||
}
|
||||
};
|
||||
|
||||
const openModules = () => {
|
||||
if (dleAddress.value) {
|
||||
router.push(`/management/modules?address=${dleAddress.value}`);
|
||||
} else {
|
||||
router.push('/management/modules');
|
||||
}
|
||||
};
|
||||
|
||||
const openAnalytics = () => {
|
||||
if (dleAddress.value) {
|
||||
router.push(`/management/analytics?address=${dleAddress.value}`);
|
||||
} else {
|
||||
router.push('/management/analytics');
|
||||
}
|
||||
};
|
||||
|
||||
const openHistory = () => {
|
||||
if (dleAddress.value) {
|
||||
router.push(`/management/history?address=${dleAddress.value}`);
|
||||
} else {
|
||||
router.push('/management/history');
|
||||
}
|
||||
};
|
||||
|
||||
const openSettings = () => {
|
||||
if (dleAddress.value) {
|
||||
router.push(`/management/settings?address=${dleAddress.value}`);
|
||||
} else {
|
||||
router.push('/management/settings');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// Если нет адреса DLE, перенаправляем на главную страницу management
|
||||
if (!dleAddress.value) {
|
||||
router.push('/management');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dle-blocks-management {
|
||||
padding: 20px;
|
||||
background-color: var(--color-white);
|
||||
border-radius: var(--radius-lg);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.management-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
}
|
||||
|
||||
.header-content h1 {
|
||||
margin: 0;
|
||||
color: var(--color-primary);
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dle-address {
|
||||
margin: 0.5rem 0 0 0;
|
||||
color: #666;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 2rem;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #f8f9fa;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.management-blocks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.blocks-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.management-block {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.management-block:hover {
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
|
||||
transform: translateY(-2px);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.management-block h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: var(--color-primary);
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.management-block p {
|
||||
margin: 0 0 1.5rem 0;
|
||||
color: #666;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.details-btn {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.details-btn:hover {
|
||||
background: var(--color-primary-dark);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.blocks-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.management-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.header-content h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -27,7 +27,7 @@
|
||||
<p v-else-if="isLoadingDle">Загрузка...</p>
|
||||
<p v-else>DLE не выбран</p>
|
||||
</div>
|
||||
<button class="close-btn" @click="router.push('/management')">×</button>
|
||||
<button class="close-btn" @click="goBackToBlocks">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Фильтры и управление -->
|
||||
@@ -900,6 +900,15 @@ const dleAddress = computed(() => {
|
||||
return address;
|
||||
});
|
||||
|
||||
// Функция возврата к блокам управления
|
||||
const goBackToBlocks = () => {
|
||||
if (dleAddress.value) {
|
||||
router.push(`/management/dle-blocks?address=${dleAddress.value}`);
|
||||
} else {
|
||||
router.push('/management');
|
||||
}
|
||||
};
|
||||
|
||||
// Состояние DLE
|
||||
const selectedDle = ref(null);
|
||||
const isLoadingDle = ref(false);
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<p v-else-if="isLoadingDle">Загрузка...</p>
|
||||
<p v-else>Лог операций, события и транзакции DLE</p>
|
||||
</div>
|
||||
<button class="close-btn" @click="router.push('/management')">×</button>
|
||||
<button class="close-btn" @click="goBackToBlocks">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Фильтры -->
|
||||
@@ -281,6 +281,15 @@ const route = useRoute();
|
||||
// Получаем адрес DLE из URL параметров
|
||||
const dleAddress = ref(route.query.address || '');
|
||||
|
||||
// Функция возврата к блокам управления
|
||||
const goBackToBlocks = () => {
|
||||
if (dleAddress.value) {
|
||||
router.push(`/management/dle-blocks?address=${dleAddress.value}`);
|
||||
} else {
|
||||
router.push('/management');
|
||||
}
|
||||
};
|
||||
|
||||
// Состояние
|
||||
const selectedDle = ref(null);
|
||||
const isLoadingDle = ref(false);
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<p v-else-if="isLoadingDle">Загрузка...</p>
|
||||
<p v-else>DLE не выбран</p>
|
||||
</div>
|
||||
<button class="close-btn" @click="router.push('/management')">×</button>
|
||||
<button class="close-btn" @click="goBackToBlocks">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Информация о модулях -->
|
||||
@@ -665,6 +665,20 @@ const emit = defineEmits(['auth-action-completed']);
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// Получаем адрес DLE из URL
|
||||
const dleAddress = computed(() => {
|
||||
return route.query.address;
|
||||
});
|
||||
|
||||
// Функция возврата к блокам управления
|
||||
const goBackToBlocks = () => {
|
||||
if (dleAddress.value) {
|
||||
router.push(`/management/dle-blocks?address=${dleAddress.value}`);
|
||||
} else {
|
||||
router.push('/management');
|
||||
}
|
||||
};
|
||||
|
||||
// Состояние
|
||||
const selectedDle = ref(null);
|
||||
const isLoadingDle = ref(false);
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<h1>Кворум</h1>
|
||||
<p>Настройки голосования и кворума</p>
|
||||
</div>
|
||||
<button class="close-btn" @click="router.push('/management')">×</button>
|
||||
<button class="close-btn" @click="goBackToBlocks">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Текущие настройки -->
|
||||
@@ -181,8 +181,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps, defineEmits } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ref, computed, defineProps, defineEmits } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import BaseLayout from '../../components/BaseLayout.vue';
|
||||
import { getGovernanceParams } from '../../services/dleV2Service.js';
|
||||
import { getQuorumAt, getVotingPowerAt } from '../../services/proposalsService.js';
|
||||
@@ -199,6 +199,21 @@ const props = defineProps({
|
||||
const emit = defineEmits(['auth-action-completed']);
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// Получаем адрес DLE из URL
|
||||
const dleAddress = computed(() => {
|
||||
return route.query.address;
|
||||
});
|
||||
|
||||
// Функция возврата к блокам управления
|
||||
const goBackToBlocks = () => {
|
||||
if (dleAddress.value) {
|
||||
router.push(`/management/dle-blocks?address=${dleAddress.value}`);
|
||||
} else {
|
||||
router.push('/management');
|
||||
}
|
||||
};
|
||||
|
||||
// Состояние
|
||||
const isUpdating = ref(false);
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<p v-else-if="address">Загрузка...</p>
|
||||
<p v-else>DLE не выбран</p>
|
||||
</div>
|
||||
<button class="close-btn" @click="router.push('/management')">×</button>
|
||||
<button class="close-btn" @click="goBackToBlocks">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Основной контент -->
|
||||
@@ -39,6 +39,15 @@
|
||||
</div>
|
||||
<div class="danger-content">
|
||||
<p>Полное удаление DLE и всех связанных данных. Это действие необратимо.</p>
|
||||
<div class="warning-info">
|
||||
<h4>⚠️ Важно:</h4>
|
||||
<ul>
|
||||
<li>Для деактивации DLE необходимо иметь токены</li>
|
||||
<li>Может потребоваться голосование участников</li>
|
||||
<li>DLE должен быть активен</li>
|
||||
<li>Только владелец токенов может инициировать деактивацию</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button @click="deleteDLE" class="btn-danger" :disabled="isLoading">
|
||||
{{ isLoading ? 'Загрузка...' : 'Удалить DLE' }}
|
||||
</button>
|
||||
@@ -50,7 +59,7 @@
|
||||
<div v-if="!address" class="no-dle-card">
|
||||
<h3>DLE не выбран</h3>
|
||||
<p>Для управления настройками необходимо выбрать DLE</p>
|
||||
<button @click="router.push('/management')" class="btn-primary">
|
||||
<button @click="goBackToBlocks" class="btn-primary">
|
||||
Вернуться к списку DLE
|
||||
</button>
|
||||
</div>
|
||||
@@ -89,6 +98,15 @@ const isLoading = ref(false);
|
||||
// Получаем адрес DLE из URL параметров
|
||||
const address = route.query.address || props.dleAddress;
|
||||
|
||||
// Функция возврата к блокам управления
|
||||
const goBackToBlocks = () => {
|
||||
if (address) {
|
||||
router.push(`/management/dle-blocks?address=${address}`);
|
||||
} else {
|
||||
router.push('/management');
|
||||
}
|
||||
};
|
||||
|
||||
// Получаем адрес пользователя из контекста аутентификации
|
||||
const { address: userAddress } = useAuthContext();
|
||||
|
||||
@@ -167,20 +185,26 @@ const deleteDLE = async () => {
|
||||
|
||||
alert(`✅ DLE ${dleInfo.value?.name || address} успешно деактивирован!\n\nТранзакция: ${result.txHash}`);
|
||||
|
||||
// Перенаправляем на главную страницу управления
|
||||
router.push('/management');
|
||||
// Перенаправляем на страницу блоков управления
|
||||
goBackToBlocks();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при деактивации DLE:', error);
|
||||
|
||||
let errorMessage = 'Ошибка при деактивации DLE';
|
||||
|
||||
if (error.message.includes('владелец')) {
|
||||
if (error.message.includes('execution reverted')) {
|
||||
errorMessage = '❌ Деактивация невозможна: не выполнены условия смарт-контракта. Возможно, требуется голосование участников или DLE уже деактивирован.';
|
||||
} else if (error.message.includes('владелец')) {
|
||||
errorMessage = '❌ Только владелец DLE может его деактивировать';
|
||||
} else if (error.message.includes('кошелек')) {
|
||||
errorMessage = '❌ Необходимо подключить кошелек';
|
||||
} else if (error.message.includes('деактивирован')) {
|
||||
errorMessage = '❌ DLE уже деактивирован';
|
||||
} else if (error.message.includes('токены')) {
|
||||
errorMessage = '❌ Для деактивации DLE необходимо иметь токены';
|
||||
} else if (error.message.includes('условия смарт-контракта')) {
|
||||
errorMessage = error.message; // Используем сообщение из dle-contract.js
|
||||
} else {
|
||||
errorMessage = `❌ Ошибка: ${error.message}`;
|
||||
}
|
||||
@@ -345,4 +369,31 @@ onMounted(() => {
|
||||
line-height: 1.5;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Стили для блока предупреждения */
|
||||
.warning-info {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.warning-info h4 {
|
||||
color: #856404;
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.warning-info ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.warning-info li {
|
||||
margin-bottom: 5px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
@@ -35,7 +35,7 @@
|
||||
<span>DLE не выбран</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="close-btn" @click="router.push('/management')">×</button>
|
||||
<button class="close-btn" @click="goBackToBlocks">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Информация о токенах -->
|
||||
@@ -182,6 +182,15 @@ const dleAddress = computed(() => {
|
||||
return address;
|
||||
});
|
||||
|
||||
// Функция возврата к блокам управления
|
||||
const goBackToBlocks = () => {
|
||||
if (dleAddress.value) {
|
||||
router.push(`/management/dle-blocks?address=${dleAddress.value}`);
|
||||
} else {
|
||||
router.push('/management');
|
||||
}
|
||||
};
|
||||
|
||||
// Состояние DLE
|
||||
const selectedDle = ref(null);
|
||||
const isLoadingDle = ref(false);
|
||||
|
||||
@@ -49,11 +49,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Форма деплоя модуля во всех сетях -->
|
||||
<div class="deploy-form">
|
||||
<!-- Форма деплоя модуля администратором -->
|
||||
<div v-if="canManageSettings" class="deploy-form">
|
||||
<div class="form-header">
|
||||
<h3>🌐 Деплой DLEReader во всех сетях</h3>
|
||||
<p>Деплой API модуля для чтения данных во всех 4 сетях одновременно</p>
|
||||
<h3>🔧 Деплой DLEReader администратором</h3>
|
||||
<p>Администратор деплоит модуль, затем создает предложение для добавления в DLE</p>
|
||||
</div>
|
||||
|
||||
<div class="form-content">
|
||||
@@ -85,22 +85,39 @@
|
||||
<h4>⚙️ Настройки DLEReader:</h4>
|
||||
|
||||
<div class="settings-form">
|
||||
<div class="form-group">
|
||||
<label for="chainId">ID сети:</label>
|
||||
<select
|
||||
id="chainId"
|
||||
v-model="moduleSettings.chainId"
|
||||
class="form-control"
|
||||
required
|
||||
>
|
||||
<option value="11155111">Sepolia (11155111)</option>
|
||||
<option value="17000">Holesky (17000)</option>
|
||||
<option value="421614">Arbitrum Sepolia (421614)</option>
|
||||
<option value="84532">Base Sepolia (84532)</option>
|
||||
</select>
|
||||
<small class="form-help">ID сети для деплоя модуля</small>
|
||||
<!-- Поля администратора -->
|
||||
<div class="admin-section">
|
||||
<h5>🔐 Настройки администратора:</h5>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="adminPrivateKey">Приватный ключ администратора:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="adminPrivateKey"
|
||||
v-model="moduleSettings.adminPrivateKey"
|
||||
class="form-control"
|
||||
placeholder="0x..."
|
||||
required
|
||||
>
|
||||
<small class="form-help">Приватный ключ для деплоя модуля (администратор платит газ)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="etherscanApiKey">Etherscan API ключ:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="etherscanApiKey"
|
||||
v-model="moduleSettings.etherscanApiKey"
|
||||
class="form-control"
|
||||
placeholder="YourAPIKey..."
|
||||
>
|
||||
<small class="form-help">API ключ для автоматической верификации контрактов</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="simple-info">
|
||||
<h5>📋 Информация о DLEReader:</h5>
|
||||
<div class="info-text">
|
||||
@@ -122,12 +139,17 @@
|
||||
<button
|
||||
class="btn btn-primary btn-large deploy-module"
|
||||
@click="deployDLEReader"
|
||||
:disabled="isDeploying || !dleAddress"
|
||||
:disabled="isDeploying || !dleAddress || !isFormValid"
|
||||
>
|
||||
<i class="fas fa-rocket" :class="{ 'fa-spin': isDeploying }"></i>
|
||||
{{ isDeploying ? 'Деплой модуля...' : 'Деплой DLEReader' }}
|
||||
</button>
|
||||
|
||||
<div v-if="!isFormValid && !isDeploying" class="form-validation-info">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<span>Заполните приватный ключ и API ключ для деплоя</span>
|
||||
</div>
|
||||
|
||||
<div v-if="deploymentProgress" class="deployment-progress">
|
||||
<div class="progress-info">
|
||||
<span>{{ deploymentProgress.message }}</span>
|
||||
@@ -141,14 +163,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Сообщение для пользователей без прав доступа -->
|
||||
<div v-if="!canManageSettings" class="no-access-message">
|
||||
<div class="message-content">
|
||||
<h3>🔒 Нет прав доступа</h3>
|
||||
<p>У вас нет прав для деплоя смарт-контрактов. Только пользователи с ролью Editor могут выполнять деплой.</p>
|
||||
<button class="btn btn-secondary" @click="router.push('/management/modules')">
|
||||
← Вернуться к модулям
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import BaseLayout from '../../../components/BaseLayout.vue';
|
||||
import { usePermissions } from '@/composables/usePermissions';
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
@@ -162,6 +196,7 @@ const emit = defineEmits(['auth-action-completed']);
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const { canEdit, canManageSettings } = usePermissions();
|
||||
|
||||
// Состояние
|
||||
const isLoading = ref(false);
|
||||
@@ -171,12 +206,23 @@ const deploymentProgress = ref(null);
|
||||
|
||||
// Настройки модуля
|
||||
const moduleSettings = ref({
|
||||
// Единственный параметр - ID сети
|
||||
chainId: 11155111
|
||||
// Поля администратора
|
||||
adminPrivateKey: '',
|
||||
etherscanApiKey: ''
|
||||
});
|
||||
|
||||
// Проверка валидности формы
|
||||
const isFormValid = computed(() => {
|
||||
return moduleSettings.value.adminPrivateKey && moduleSettings.value.etherscanApiKey;
|
||||
});
|
||||
|
||||
// Функция деплоя DLEReader
|
||||
async function deployDLEReader() {
|
||||
if (!canManageSettings.value) {
|
||||
alert('У вас нет прав для деплоя смарт-контрактов');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isDeploying.value = true;
|
||||
deploymentProgress.value = {
|
||||
@@ -186,8 +232,8 @@ async function deployDLEReader() {
|
||||
|
||||
console.log('[DLEReaderDeployView] Начинаем деплой DLEReader для DLE:', dleAddress.value);
|
||||
|
||||
// Вызываем API для деплоя модуля во всех сетях
|
||||
const response = await fetch('/api/dle-modules/deploy-reader', {
|
||||
// Вызываем API для деплоя модуля администратором
|
||||
const response = await fetch('/api/dle-modules/deploy-reader-admin', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -195,9 +241,11 @@ async function deployDLEReader() {
|
||||
body: JSON.stringify({
|
||||
dleAddress: dleAddress.value,
|
||||
moduleType: 'reader',
|
||||
adminPrivateKey: moduleSettings.value.adminPrivateKey,
|
||||
etherscanApiKey: moduleSettings.value.etherscanApiKey,
|
||||
settings: {
|
||||
// Единственный параметр - ID сети
|
||||
chainId: moduleSettings.value.chainId
|
||||
// Используем настройки по умолчанию
|
||||
useDefaultSettings: true
|
||||
}
|
||||
})
|
||||
});
|
||||
@@ -217,12 +265,31 @@ async function deployDLEReader() {
|
||||
percentage: 100
|
||||
};
|
||||
|
||||
alert('✅ Деплой DLEReader запущен во всех сетях!');
|
||||
// Показываем детальную информацию о деплое
|
||||
const deployInfo = result.data || {};
|
||||
const deployedAddresses = deployInfo.addresses || [];
|
||||
|
||||
let successMessage = '✅ DLEReader успешно задеплоен!\n\n';
|
||||
successMessage += `📊 Детали деплоя:\n`;
|
||||
successMessage += `• DLE: ${dleAddress.value}\n`;
|
||||
successMessage += `• Тип модуля: DLEReader\n`;
|
||||
successMessage += `• Адрес модуля: ${deployInfo.moduleAddress || 'Не указан'}\n`;
|
||||
|
||||
if (deployedAddresses.length > 0) {
|
||||
successMessage += `\n🌐 Задеплоенные адреса:\n`;
|
||||
deployedAddresses.forEach((addr, index) => {
|
||||
successMessage += `${index + 1}. ${addr.network}: ${addr.address}\n`;
|
||||
});
|
||||
}
|
||||
|
||||
successMessage += `\n📝 Следующий шаг: Создайте предложение для добавления модуля в DLE через governance.`;
|
||||
|
||||
alert(successMessage);
|
||||
|
||||
// Перенаправляем обратно к модулям
|
||||
setTimeout(() => {
|
||||
router.push(`/management/modules?address=${dleAddress.value}`);
|
||||
}, 2000);
|
||||
}, 3000);
|
||||
|
||||
} else {
|
||||
throw new Error(result.error || 'Неизвестная ошибка');
|
||||
@@ -440,6 +507,22 @@ onMounted(() => {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Секция администратора */
|
||||
.admin-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 20px;
|
||||
background: #fff3cd;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid #ffeaa7;
|
||||
}
|
||||
|
||||
.admin-section h5 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #856404;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Простая информация */
|
||||
.simple-info {
|
||||
margin-top: 20px;
|
||||
@@ -582,4 +665,43 @@ onMounted(() => {
|
||||
color: #666;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* Сообщение об отсутствии прав доступа */
|
||||
.no-access-message {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 30px;
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message-content h3 {
|
||||
color: #856404;
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
.message-content p {
|
||||
color: #856404;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.1em;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.message-content .btn {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.message-content .btn:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -49,11 +49,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Форма деплоя модуля во всех сетях -->
|
||||
<div class="deploy-form">
|
||||
<!-- Форма деплоя модуля администратором -->
|
||||
<div v-if="canManageSettings" class="deploy-form">
|
||||
<div class="form-header">
|
||||
<h3>🌐 Деплой TimelockModule во всех сетях</h3>
|
||||
<p>Деплой модуля временных задержек во всех 4 сетях одновременно</p>
|
||||
<h3>🔧 Деплой TimelockModule администратором</h3>
|
||||
<p>Администратор деплоит модуль, затем создает предложение для добавления в DLE</p>
|
||||
</div>
|
||||
|
||||
<div class="form-content">
|
||||
@@ -85,193 +85,49 @@
|
||||
<h4>⚙️ Настройки TimelockModule:</h4>
|
||||
|
||||
<div class="settings-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="chainId">ID сети:</label>
|
||||
<select
|
||||
id="chainId"
|
||||
v-model="moduleSettings.chainId"
|
||||
class="form-control"
|
||||
required
|
||||
>
|
||||
<option value="11155111">Sepolia (11155111)</option>
|
||||
<option value="17000">Holesky (17000)</option>
|
||||
<option value="421614">Arbitrum Sepolia (421614)</option>
|
||||
<option value="84532">Base Sepolia (84532)</option>
|
||||
</select>
|
||||
<small class="form-help">ID сети для деплоя модуля</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="defaultDelay">Стандартная задержка (дни):</label>
|
||||
<input
|
||||
type="number"
|
||||
id="defaultDelay"
|
||||
v-model="moduleSettings.defaultDelay"
|
||||
class="form-control"
|
||||
min="1"
|
||||
max="30"
|
||||
placeholder="2"
|
||||
>
|
||||
<small class="form-help">Стандартная задержка для операций (1-30 дней)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="emergencyDelay">Экстренная задержка (минуты):</label>
|
||||
<input
|
||||
type="number"
|
||||
id="emergencyDelay"
|
||||
v-model="moduleSettings.emergencyDelay"
|
||||
class="form-control"
|
||||
min="5"
|
||||
max="1440"
|
||||
placeholder="30"
|
||||
>
|
||||
<small class="form-help">Экстренная задержка для критических операций (5-1440 минут)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="maxDelay">Максимальная задержка (дни):</label>
|
||||
<input
|
||||
type="number"
|
||||
id="maxDelay"
|
||||
v-model="moduleSettings.maxDelay"
|
||||
class="form-control"
|
||||
min="1"
|
||||
max="365"
|
||||
placeholder="30"
|
||||
>
|
||||
<small class="form-help">Максимальная задержка для операций (1-365 дней)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="minDelay">Минимальная задержка (часы):</label>
|
||||
<input
|
||||
type="number"
|
||||
id="minDelay"
|
||||
v-model="moduleSettings.minDelay"
|
||||
class="form-control"
|
||||
min="1"
|
||||
max="720"
|
||||
placeholder="24"
|
||||
>
|
||||
<small class="form-help">Минимальная задержка для операций (1-720 часов)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="maxOperations">Максимум операций в очереди:</label>
|
||||
<input
|
||||
type="number"
|
||||
id="maxOperations"
|
||||
v-model="moduleSettings.maxOperations"
|
||||
class="form-control"
|
||||
min="10"
|
||||
max="1000"
|
||||
placeholder="100"
|
||||
>
|
||||
<small class="form-help">Максимальное количество операций в очереди (10-1000)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Дополнительные настройки таймлока -->
|
||||
<div class="advanced-settings">
|
||||
<h5>🔧 Дополнительные настройки таймлока:</h5>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="criticalOperations">Критические операции (JSON формат):</label>
|
||||
<textarea
|
||||
id="criticalOperations"
|
||||
v-model="moduleSettings.criticalOperations"
|
||||
class="form-control"
|
||||
rows="3"
|
||||
placeholder='["0x12345678", "0x87654321"]'
|
||||
></textarea>
|
||||
<small class="form-help">Селекторы функций, которые считаются критическими (JSON массив)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="emergencyOperations">Экстренные операции (JSON формат):</label>
|
||||
<textarea
|
||||
id="emergencyOperations"
|
||||
v-model="moduleSettings.emergencyOperations"
|
||||
class="form-control"
|
||||
rows="3"
|
||||
placeholder='["0xabcdef12", "0x21fedcba"]'
|
||||
></textarea>
|
||||
<small class="form-help">Селекторы функций для экстренных операций (JSON массив)</small>
|
||||
</div>
|
||||
<!-- Поля администратора -->
|
||||
<div class="admin-section">
|
||||
<h5>🔐 Настройки администратора:</h5>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="operationDelays">Задержки для операций (JSON формат):</label>
|
||||
<textarea
|
||||
id="operationDelays"
|
||||
v-model="moduleSettings.operationDelays"
|
||||
class="form-control"
|
||||
rows="4"
|
||||
placeholder='{"0x12345678": 86400, "0x87654321": 172800}'
|
||||
></textarea>
|
||||
<small class="form-help">Кастомные задержки для конкретных операций (селектор => секунды)</small>
|
||||
<label for="adminPrivateKey">Приватный ключ администратора:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="adminPrivateKey"
|
||||
v-model="moduleSettings.adminPrivateKey"
|
||||
class="form-control"
|
||||
placeholder="0x..."
|
||||
required
|
||||
>
|
||||
<small class="form-help">Приватный ключ для деплоя модуля (администратор платит газ)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="autoExecuteEnabled">Автоисполнение включено:</label>
|
||||
<select
|
||||
id="autoExecuteEnabled"
|
||||
v-model="moduleSettings.autoExecuteEnabled"
|
||||
<label for="etherscanApiKey">Etherscan API ключ:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="etherscanApiKey"
|
||||
v-model="moduleSettings.etherscanApiKey"
|
||||
class="form-control"
|
||||
placeholder="YourAPIKey..."
|
||||
>
|
||||
<option value="true">Включено</option>
|
||||
<option value="false">Отключено</option>
|
||||
</select>
|
||||
<small class="form-help">Автоматическое исполнение операций после истечения задержки</small>
|
||||
<small class="form-help">API ключ для автоматической верификации контрактов</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="cancellationWindow">Окно отмены (часы):</label>
|
||||
<input
|
||||
type="number"
|
||||
id="cancellationWindow"
|
||||
v-model="moduleSettings.cancellationWindow"
|
||||
class="form-control"
|
||||
min="1"
|
||||
max="168"
|
||||
placeholder="24"
|
||||
>
|
||||
<small class="form-help">Время, в течение которого можно отменить операцию (1-168 часов)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="executionWindow">Окно исполнения (часы):</label>
|
||||
<input
|
||||
type="number"
|
||||
id="executionWindow"
|
||||
v-model="moduleSettings.executionWindow"
|
||||
class="form-control"
|
||||
min="1"
|
||||
max="168"
|
||||
placeholder="48"
|
||||
>
|
||||
<small class="form-help">Время, в течение которого можно исполнить операцию (1-168 часов)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="timelockDescription">Описание таймлока:</label>
|
||||
<textarea
|
||||
id="timelockDescription"
|
||||
v-model="moduleSettings.timelockDescription"
|
||||
class="form-control"
|
||||
rows="2"
|
||||
placeholder="Описание таймлока DLE для безопасности операций..."
|
||||
></textarea>
|
||||
<small class="form-help">Описание таймлока для документации</small>
|
||||
</div>
|
||||
|
||||
<div class="simple-info">
|
||||
<h5>📋 Информация о TimelockModule:</h5>
|
||||
<div class="info-text">
|
||||
<p><strong>TimelockModule</strong> будет задеплоен с настройками по умолчанию:</p>
|
||||
<ul>
|
||||
<li>✅ Стандартная задержка: 2 дня</li>
|
||||
<li>✅ Экстренная задержка: 30 минут</li>
|
||||
<li>✅ Автоматическое исполнение операций</li>
|
||||
<li>✅ Готовые настройки безопасности</li>
|
||||
</ul>
|
||||
<p><em>После деплоя настройки можно будет изменить через governance.</em></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -282,12 +138,17 @@
|
||||
<button
|
||||
class="btn btn-primary btn-large deploy-module"
|
||||
@click="deployTimelockModule"
|
||||
:disabled="isDeploying || !dleAddress"
|
||||
:disabled="isDeploying || !dleAddress || !isFormValid"
|
||||
>
|
||||
<i class="fas fa-rocket" :class="{ 'fa-spin': isDeploying }"></i>
|
||||
{{ isDeploying ? 'Деплой модуля...' : 'Деплой TimelockModule' }}
|
||||
</button>
|
||||
|
||||
<div v-if="!isFormValid && !isDeploying" class="form-validation-info">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<span>Заполните приватный ключ и API ключ для деплоя</span>
|
||||
</div>
|
||||
|
||||
<div v-if="deploymentProgress" class="deployment-progress">
|
||||
<div class="progress-info">
|
||||
<span>{{ deploymentProgress.message }}</span>
|
||||
@@ -301,14 +162,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Сообщение для пользователей без прав доступа -->
|
||||
<div v-if="!canManageSettings" class="no-access-message">
|
||||
<div class="message-content">
|
||||
<h3>🔒 Нет прав доступа</h3>
|
||||
<p>У вас нет прав для деплоя смарт-контрактов. Только пользователи с ролью Editor могут выполнять деплой.</p>
|
||||
<button class="btn btn-secondary" @click="router.push('/management/modules')">
|
||||
← Вернуться к модулям
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, defineEmits, ref, onMounted } from 'vue';
|
||||
import { defineProps, defineEmits, ref, computed, onMounted } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import BaseLayout from '../../../components/BaseLayout.vue';
|
||||
import { usePermissions } from '@/composables/usePermissions';
|
||||
|
||||
// Определяем props
|
||||
const props = defineProps({
|
||||
@@ -323,6 +196,7 @@ const emit = defineEmits(['auth-action-completed']);
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const { canEdit, canManageSettings } = usePermissions();
|
||||
|
||||
// Состояние
|
||||
const isLoading = ref(false);
|
||||
@@ -332,26 +206,23 @@ const deploymentProgress = ref(null);
|
||||
|
||||
// Настройки модуля
|
||||
const moduleSettings = ref({
|
||||
// Основные параметры
|
||||
chainId: 11155111,
|
||||
defaultDelay: 2, // days
|
||||
emergencyDelay: 30, // minutes
|
||||
maxDelay: 30, // days
|
||||
minDelay: 24, // hours
|
||||
|
||||
// Дополнительные настройки
|
||||
maxOperations: 100,
|
||||
criticalOperations: '',
|
||||
emergencyOperations: '',
|
||||
operationDelays: '',
|
||||
autoExecuteEnabled: 'true',
|
||||
cancellationWindow: 24, // hours
|
||||
executionWindow: 48, // hours
|
||||
timelockDescription: ''
|
||||
// Поля администратора
|
||||
adminPrivateKey: '',
|
||||
etherscanApiKey: ''
|
||||
});
|
||||
|
||||
// Проверка валидности формы
|
||||
const isFormValid = computed(() => {
|
||||
return moduleSettings.value.adminPrivateKey && moduleSettings.value.etherscanApiKey;
|
||||
});
|
||||
|
||||
// Функция деплоя TimelockModule
|
||||
async function deployTimelockModule() {
|
||||
if (!canManageSettings.value) {
|
||||
alert('У вас нет прав для деплоя смарт-контрактов');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isDeploying.value = true;
|
||||
deploymentProgress.value = {
|
||||
@@ -361,8 +232,8 @@ async function deployTimelockModule() {
|
||||
|
||||
console.log('[TimelockModuleDeployView] Начинаем деплой TimelockModule для DLE:', dleAddress.value);
|
||||
|
||||
// Вызываем API для деплоя модуля во всех сетях
|
||||
const response = await fetch('/api/dle-modules/deploy-timelock', {
|
||||
// Вызываем API для деплоя модуля администратором
|
||||
const response = await fetch('/api/dle-modules/deploy-timelock-admin', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -370,23 +241,11 @@ async function deployTimelockModule() {
|
||||
body: JSON.stringify({
|
||||
dleAddress: dleAddress.value,
|
||||
moduleType: 'timelock',
|
||||
adminPrivateKey: moduleSettings.value.adminPrivateKey,
|
||||
etherscanApiKey: moduleSettings.value.etherscanApiKey,
|
||||
settings: {
|
||||
// Основные параметры
|
||||
chainId: moduleSettings.value.chainId,
|
||||
defaultDelay: moduleSettings.value.defaultDelay * 24 * 60 * 60, // конвертируем дни в секунды
|
||||
emergencyDelay: moduleSettings.value.emergencyDelay * 60, // конвертируем минуты в секунды
|
||||
maxDelay: moduleSettings.value.maxDelay * 24 * 60 * 60, // конвертируем дни в секунды
|
||||
minDelay: moduleSettings.value.minDelay * 60 * 60, // конвертируем часы в секунды
|
||||
|
||||
// Дополнительные настройки
|
||||
maxOperations: parseInt(moduleSettings.value.maxOperations),
|
||||
criticalOperations: moduleSettings.value.criticalOperations ? JSON.parse(moduleSettings.value.criticalOperations) : [],
|
||||
emergencyOperations: moduleSettings.value.emergencyOperations ? JSON.parse(moduleSettings.value.emergencyOperations) : [],
|
||||
operationDelays: moduleSettings.value.operationDelays ? JSON.parse(moduleSettings.value.operationDelays) : {},
|
||||
autoExecuteEnabled: moduleSettings.value.autoExecuteEnabled === 'true',
|
||||
cancellationWindow: moduleSettings.value.cancellationWindow * 60 * 60, // конвертируем часы в секунды
|
||||
executionWindow: moduleSettings.value.executionWindow * 60 * 60, // конвертируем часы в секунды
|
||||
timelockDescription: moduleSettings.value.timelockDescription
|
||||
// Используем настройки по умолчанию
|
||||
useDefaultSettings: true
|
||||
}
|
||||
})
|
||||
});
|
||||
@@ -406,12 +265,31 @@ async function deployTimelockModule() {
|
||||
percentage: 100
|
||||
};
|
||||
|
||||
alert('✅ Деплой TimelockModule запущен во всех сетях!');
|
||||
// Показываем детальную информацию о деплое
|
||||
const deployInfo = result.data || {};
|
||||
const deployedAddresses = deployInfo.addresses || [];
|
||||
|
||||
let successMessage = '✅ TimelockModule успешно задеплоен!\n\n';
|
||||
successMessage += `📊 Детали деплоя:\n`;
|
||||
successMessage += `• DLE: ${dleAddress.value}\n`;
|
||||
successMessage += `• Тип модуля: TimelockModule\n`;
|
||||
successMessage += `• Адрес модуля: ${deployInfo.moduleAddress || 'Не указан'}\n`;
|
||||
|
||||
if (deployedAddresses.length > 0) {
|
||||
successMessage += `\n🌐 Задеплоенные адреса:\n`;
|
||||
deployedAddresses.forEach((addr, index) => {
|
||||
successMessage += `${index + 1}. ${addr.network}: ${addr.address}\n`;
|
||||
});
|
||||
}
|
||||
|
||||
successMessage += `\n📝 Следующий шаг: Создайте предложение для добавления модуля в DLE через governance.`;
|
||||
|
||||
alert(successMessage);
|
||||
|
||||
// Перенаправляем обратно к модулям
|
||||
setTimeout(() => {
|
||||
router.push(`/management/modules?address=${dleAddress.value}`);
|
||||
}, 2000);
|
||||
}, 3000);
|
||||
|
||||
} else {
|
||||
throw new Error(result.error || 'Неизвестная ошибка');
|
||||
@@ -569,6 +447,22 @@ onMounted(() => {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Секция администратора */
|
||||
.admin-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 20px;
|
||||
background: #fff3cd;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid #ffeaa7;
|
||||
}
|
||||
|
||||
.admin-section h5 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #856404;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Дополнительные настройки */
|
||||
.advanced-settings {
|
||||
margin-top: 20px;
|
||||
@@ -725,6 +619,45 @@ onMounted(() => {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Сообщение об отсутствии прав доступа */
|
||||
.no-access-message {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 30px;
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message-content h3 {
|
||||
color: #856404;
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
.message-content p {
|
||||
color: #856404;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.1em;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.message-content .btn {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.message-content .btn:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.info-grid {
|
||||
|
||||
@@ -49,11 +49,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Форма деплоя модуля во всех сетях -->
|
||||
<div class="deploy-form">
|
||||
<!-- Форма деплоя модуля администратором -->
|
||||
<div v-if="canManageSettings" class="deploy-form">
|
||||
<div class="form-header">
|
||||
<h3>🌐 Деплой TreasuryModule во всех сетях</h3>
|
||||
<p>Деплой модуля казначейства во всех 4 сетях одновременно</p>
|
||||
<p>Администратор деплоит модуль во всех 4 сетях одновременно, затем создает предложение для добавления в DLE</p>
|
||||
</div>
|
||||
|
||||
<div class="form-content">
|
||||
@@ -85,203 +85,52 @@
|
||||
<h4>⚙️ Настройки TreasuryModule:</h4>
|
||||
|
||||
<div class="settings-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="emergencyAdmin">Адрес экстренного администратора:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="emergencyAdmin"
|
||||
v-model="moduleSettings.emergencyAdmin"
|
||||
class="form-control"
|
||||
placeholder="0x..."
|
||||
required
|
||||
>
|
||||
<small class="form-help">Адрес экстренного администратора для управления модулем</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="chainId">ID сети:</label>
|
||||
<select
|
||||
id="chainId"
|
||||
v-model="moduleSettings.chainId"
|
||||
class="form-control"
|
||||
required
|
||||
>
|
||||
<option value="11155111">Sepolia (11155111)</option>
|
||||
<option value="17000">Holesky (17000)</option>
|
||||
<option value="421614">Arbitrum Sepolia (421614)</option>
|
||||
<option value="84532">Base Sepolia (84532)</option>
|
||||
</select>
|
||||
<small class="form-help">ID сети для деплоя модуля</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="defaultDelay">Стандартная задержка (часы):</label>
|
||||
<input
|
||||
type="number"
|
||||
id="defaultDelay"
|
||||
v-model="moduleSettings.defaultDelay"
|
||||
class="form-control"
|
||||
min="1"
|
||||
max="720"
|
||||
placeholder="24"
|
||||
>
|
||||
<small class="form-help">Стандартная задержка для операций (1-720 часов)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="emergencyDelay">Экстренная задержка (минуты):</label>
|
||||
<input
|
||||
type="number"
|
||||
id="emergencyDelay"
|
||||
v-model="moduleSettings.emergencyDelay"
|
||||
class="form-control"
|
||||
min="5"
|
||||
max="1440"
|
||||
placeholder="30"
|
||||
>
|
||||
<small class="form-help">Экстренная задержка для критических операций (5-1440 минут)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="supportedTokens">Поддерживаемые токены (адреса через запятую):</label>
|
||||
<textarea
|
||||
id="supportedTokens"
|
||||
v-model="moduleSettings.supportedTokens"
|
||||
class="form-control"
|
||||
rows="3"
|
||||
placeholder="0x1234..., 0x5678..., 0x9abc..."
|
||||
></textarea>
|
||||
<small class="form-help">Адреса ERC20 токенов, которые будет поддерживать казначейство (через запятую)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="gasPaymentTokens">Токены для оплаты газа (адреса через запятую):</label>
|
||||
<textarea
|
||||
id="gasPaymentTokens"
|
||||
v-model="moduleSettings.gasPaymentTokens"
|
||||
class="form-control"
|
||||
rows="2"
|
||||
placeholder="0x1234..., 0x5678..."
|
||||
></textarea>
|
||||
<small class="form-help">Токены, которыми можно оплачивать газ (через запятую)</small>
|
||||
</div>
|
||||
|
||||
<!-- Дополнительные настройки казны -->
|
||||
<div class="advanced-settings">
|
||||
<h5>🔧 Дополнительные настройки казны:</h5>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="paymasterAddress">Адрес Paymaster:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="paymasterAddress"
|
||||
v-model="moduleSettings.paymasterAddress"
|
||||
class="form-control"
|
||||
placeholder="0x..."
|
||||
>
|
||||
<small class="form-help">Адрес Paymaster для ERC-4337 (оплата газа любым токеном)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="maxBatchTransfers">Максимум batch переводов:</label>
|
||||
<input
|
||||
type="number"
|
||||
id="maxBatchTransfers"
|
||||
v-model="moduleSettings.maxBatchTransfers"
|
||||
class="form-control"
|
||||
min="1"
|
||||
max="100"
|
||||
placeholder="50"
|
||||
>
|
||||
<small class="form-help">Максимальное количество переводов в batch операции (1-100)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="gasTokenRates">Курсы токенов для газа (JSON формат):</label>
|
||||
<textarea
|
||||
id="gasTokenRates"
|
||||
v-model="moduleSettings.gasTokenRates"
|
||||
class="form-control"
|
||||
rows="3"
|
||||
placeholder='{"0x1234...": "1000000000000000000", "0x5678...": "2000000000000000000"}'
|
||||
></textarea>
|
||||
<small class="form-help">Курсы обмена токенов на нативную монету (JSON формат)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="emergencyThreshold">Порог экстренных операций (ETH):</label>
|
||||
<input
|
||||
type="number"
|
||||
id="emergencyThreshold"
|
||||
v-model="moduleSettings.emergencyThreshold"
|
||||
class="form-control"
|
||||
min="0"
|
||||
step="0.001"
|
||||
placeholder="1.0"
|
||||
>
|
||||
<small class="form-help">Порог для экстренных операций в ETH</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="initialTokens">Начальные токены для добавления (JSON формат):</label>
|
||||
<textarea
|
||||
id="initialTokens"
|
||||
v-model="moduleSettings.initialTokens"
|
||||
class="form-control"
|
||||
rows="4"
|
||||
placeholder='[{"address": "0x1234...", "symbol": "USDC", "decimals": 6}, {"address": "0x5678...", "symbol": "USDT", "decimals": 6}]'
|
||||
></textarea>
|
||||
<small class="form-help">Токены для автоматического добавления при деплое (JSON массив)</small>
|
||||
</div>
|
||||
<!-- Поля администратора -->
|
||||
<div class="admin-section">
|
||||
<h5>🔐 Настройки администратора:</h5>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="autoRefreshBalances">Автообновление балансов:</label>
|
||||
<select
|
||||
id="autoRefreshBalances"
|
||||
v-model="moduleSettings.autoRefreshBalances"
|
||||
<label for="adminPrivateKey">Приватный ключ администратора:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="adminPrivateKey"
|
||||
v-model="moduleSettings.adminPrivateKey"
|
||||
class="form-control"
|
||||
placeholder="0x..."
|
||||
required
|
||||
>
|
||||
<option value="true">Включено</option>
|
||||
<option value="false">Отключено</option>
|
||||
</select>
|
||||
<small class="form-help">Автоматическое обновление балансов токенов</small>
|
||||
<small class="form-help">Приватный ключ для деплоя модуля (администратор платит газ)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="batchTransferEnabled">Batch переводы включены:</label>
|
||||
<select
|
||||
id="batchTransferEnabled"
|
||||
v-model="moduleSettings.batchTransferEnabled"
|
||||
<label for="etherscanApiKey">Etherscan API ключ:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="etherscanApiKey"
|
||||
v-model="moduleSettings.etherscanApiKey"
|
||||
class="form-control"
|
||||
placeholder="YourAPIKey..."
|
||||
>
|
||||
<option value="true">Включено</option>
|
||||
<option value="false">Отключено</option>
|
||||
</select>
|
||||
<small class="form-help">Разрешить batch операции переводов</small>
|
||||
<small class="form-help">API ключ для автоматической верификации контрактов</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="treasuryDescription">Описание казны:</label>
|
||||
<textarea
|
||||
id="treasuryDescription"
|
||||
v-model="moduleSettings.treasuryDescription"
|
||||
class="form-control"
|
||||
rows="2"
|
||||
placeholder="Описание казны DLE для управления финансами..."
|
||||
></textarea>
|
||||
<small class="form-help">Описание казны для документации</small>
|
||||
</div>
|
||||
|
||||
<div class="simple-info">
|
||||
<h5>📋 Информация о TreasuryModule:</h5>
|
||||
<div class="info-text">
|
||||
<p><strong>TreasuryModule</strong> будет задеплоен с настройками по умолчанию:</p>
|
||||
<ul>
|
||||
<li>✅ Поддержка ETH и основных ERC20 токенов</li>
|
||||
<li>✅ Стандартные задержки для безопасности</li>
|
||||
<li>✅ Автоматическая настройка для всех сетей</li>
|
||||
<li>✅ Готовые настройки безопасности</li>
|
||||
</ul>
|
||||
<p><em>После деплоя настройки можно будет изменить через governance.</em></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -290,12 +139,17 @@
|
||||
<button
|
||||
class="btn btn-primary btn-large deploy-module"
|
||||
@click="deployTreasuryModule"
|
||||
:disabled="isDeploying || !dleAddress"
|
||||
:disabled="isDeploying || !dleAddress || !isFormValid"
|
||||
>
|
||||
<i class="fas fa-rocket" :class="{ 'fa-spin': isDeploying }"></i>
|
||||
{{ isDeploying ? 'Деплой модуля...' : 'Деплой TreasuryModule' }}
|
||||
{{ isDeploying ? 'Деплой во всех сетях...' : 'Деплой TreasuryModule во всех сетях' }}
|
||||
</button>
|
||||
|
||||
<div v-if="!isFormValid && !isDeploying" class="form-validation-info">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<span>Заполните приватный ключ и API ключ для деплоя</span>
|
||||
</div>
|
||||
|
||||
<div v-if="deploymentProgress" class="deployment-progress">
|
||||
<div class="progress-info">
|
||||
<span>{{ deploymentProgress.message }}</span>
|
||||
@@ -309,14 +163,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Сообщение для пользователей без прав доступа -->
|
||||
<div v-if="!canManageSettings" class="no-access-message">
|
||||
<div class="message-content">
|
||||
<h3>🔒 Нет прав доступа</h3>
|
||||
<p>У вас нет прав для деплоя смарт-контрактов. Только пользователи с ролью Editor могут выполнять деплой.</p>
|
||||
<button class="btn btn-secondary" @click="router.push('/management/modules')">
|
||||
← Вернуться к модулям
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, defineEmits, ref, onMounted } from 'vue';
|
||||
import { defineProps, defineEmits, ref, computed, onMounted } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import BaseLayout from '../../../components/BaseLayout.vue';
|
||||
import { usePermissions } from '@/composables/usePermissions';
|
||||
|
||||
// Определяем props
|
||||
const props = defineProps({
|
||||
@@ -331,6 +197,7 @@ const emit = defineEmits(['auth-action-completed']);
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const { canEdit, canManageSettings } = usePermissions();
|
||||
|
||||
// Состояние
|
||||
const isLoading = ref(false);
|
||||
@@ -340,29 +207,23 @@ const deploymentProgress = ref(null);
|
||||
|
||||
// Настройки модуля
|
||||
const moduleSettings = ref({
|
||||
// Основные параметры
|
||||
emergencyAdmin: '',
|
||||
chainId: 11155111,
|
||||
defaultDelay: 24, // hours
|
||||
emergencyDelay: 30, // minutes
|
||||
|
||||
// Токены
|
||||
supportedTokens: '',
|
||||
gasPaymentTokens: '',
|
||||
initialTokens: '',
|
||||
|
||||
// Дополнительные настройки
|
||||
paymasterAddress: '',
|
||||
maxBatchTransfers: 50,
|
||||
gasTokenRates: '',
|
||||
emergencyThreshold: 1.0,
|
||||
autoRefreshBalances: 'true',
|
||||
batchTransferEnabled: 'true',
|
||||
treasuryDescription: ''
|
||||
// Поля администратора
|
||||
adminPrivateKey: '',
|
||||
etherscanApiKey: ''
|
||||
});
|
||||
|
||||
// Проверка валидности формы
|
||||
const isFormValid = computed(() => {
|
||||
return moduleSettings.value.adminPrivateKey && moduleSettings.value.etherscanApiKey;
|
||||
});
|
||||
|
||||
// Функция деплоя TreasuryModule
|
||||
async function deployTreasuryModule() {
|
||||
if (!canManageSettings.value) {
|
||||
alert('У вас нет прав для деплоя смарт-контрактов');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isDeploying.value = true;
|
||||
deploymentProgress.value = {
|
||||
@@ -372,8 +233,8 @@ async function deployTreasuryModule() {
|
||||
|
||||
console.log('[TreasuryModuleDeployView] Начинаем деплой TreasuryModule для DLE:', dleAddress.value);
|
||||
|
||||
// Вызываем API для деплоя модуля во всех сетях
|
||||
const response = await fetch('/api/dle-modules/deploy-treasury', {
|
||||
// Вызываем API для деплоя модуля администратором
|
||||
const response = await fetch('/api/dle-modules/deploy-treasury-admin', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -381,26 +242,11 @@ async function deployTreasuryModule() {
|
||||
body: JSON.stringify({
|
||||
dleAddress: dleAddress.value,
|
||||
moduleType: 'treasury',
|
||||
adminPrivateKey: moduleSettings.value.adminPrivateKey,
|
||||
etherscanApiKey: moduleSettings.value.etherscanApiKey,
|
||||
settings: {
|
||||
// Основные параметры
|
||||
emergencyAdmin: moduleSettings.value.emergencyAdmin,
|
||||
chainId: moduleSettings.value.chainId,
|
||||
defaultDelay: moduleSettings.value.defaultDelay,
|
||||
emergencyDelay: moduleSettings.value.emergencyDelay,
|
||||
|
||||
// Токены
|
||||
supportedTokens: moduleSettings.value.supportedTokens.split(',').map(addr => addr.trim()).filter(addr => addr),
|
||||
gasPaymentTokens: moduleSettings.value.gasPaymentTokens.split(',').map(addr => addr.trim()).filter(addr => addr),
|
||||
initialTokens: moduleSettings.value.initialTokens ? JSON.parse(moduleSettings.value.initialTokens) : [],
|
||||
|
||||
// Дополнительные настройки
|
||||
paymasterAddress: moduleSettings.value.paymasterAddress,
|
||||
maxBatchTransfers: parseInt(moduleSettings.value.maxBatchTransfers),
|
||||
gasTokenRates: moduleSettings.value.gasTokenRates ? JSON.parse(moduleSettings.value.gasTokenRates) : {},
|
||||
emergencyThreshold: parseFloat(moduleSettings.value.emergencyThreshold),
|
||||
autoRefreshBalances: moduleSettings.value.autoRefreshBalances === 'true',
|
||||
batchTransferEnabled: moduleSettings.value.batchTransferEnabled === 'true',
|
||||
treasuryDescription: moduleSettings.value.treasuryDescription
|
||||
// Используем настройки по умолчанию
|
||||
useDefaultSettings: true
|
||||
}
|
||||
})
|
||||
});
|
||||
@@ -420,12 +266,31 @@ async function deployTreasuryModule() {
|
||||
percentage: 100
|
||||
};
|
||||
|
||||
alert('✅ Деплой TreasuryModule запущен во всех сетях!');
|
||||
// Показываем детальную информацию о деплое
|
||||
const deployInfo = result.data || {};
|
||||
const deployedAddresses = deployInfo.addresses || [];
|
||||
|
||||
let successMessage = '✅ TreasuryModule успешно задеплоен во всех сетях!\n\n';
|
||||
successMessage += `📊 Детали деплоя:\n`;
|
||||
successMessage += `• DLE: ${dleAddress.value}\n`;
|
||||
successMessage += `• Тип модуля: TreasuryModule\n`;
|
||||
successMessage += `• Сети: Sepolia, Holesky, Arbitrum Sepolia, Base Sepolia\n`;
|
||||
|
||||
if (deployedAddresses.length > 0) {
|
||||
successMessage += `\n🌐 Задеплоенные адреса:\n`;
|
||||
deployedAddresses.forEach((addr, index) => {
|
||||
successMessage += `${index + 1}. ${addr.network}: ${addr.address}\n`;
|
||||
});
|
||||
}
|
||||
|
||||
successMessage += `\n📝 Следующий шаг: Создайте предложение для добавления модуля в DLE через governance.`;
|
||||
|
||||
alert(successMessage);
|
||||
|
||||
// Перенаправляем обратно к модулям
|
||||
setTimeout(() => {
|
||||
router.push(`/management/modules?address=${dleAddress.value}`);
|
||||
}, 2000);
|
||||
}, 3000);
|
||||
|
||||
} else {
|
||||
throw new Error(result.error || 'Неизвестная ошибка');
|
||||
@@ -583,6 +448,176 @@ onMounted(() => {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Секция администратора */
|
||||
.admin-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 20px;
|
||||
background: #fff3cd;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid #ffeaa7;
|
||||
}
|
||||
|
||||
.admin-section h5 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #856404;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Простая информация */
|
||||
.simple-info {
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.simple-info h5 {
|
||||
margin: 0 0 15px 0;
|
||||
color: var(--color-primary);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.info-text ul {
|
||||
margin: 10px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.info-text li {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.token-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.token-input-wrapper {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.token-input {
|
||||
width: 100%;
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
.token-input.is-valid {
|
||||
border-color: #28a745;
|
||||
box-shadow: 0 0 0 2px rgba(40, 167, 69, 0.1);
|
||||
}
|
||||
|
||||
.token-input.is-invalid {
|
||||
border-color: #dc3545;
|
||||
box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.1);
|
||||
}
|
||||
|
||||
.validation-icon {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.validation-icon.valid {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.validation-icon.invalid {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.validation-icon.loading {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.remove-token {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: #dc3545;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.remove-token:hover {
|
||||
background: #c82333;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.remove-token:disabled {
|
||||
background: #6c757d;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.add-token {
|
||||
margin-top: 15px;
|
||||
width: 100%;
|
||||
padding: 12px 20px;
|
||||
border: 2px dashed #28a745;
|
||||
background: #f8f9fa;
|
||||
color: #28a745;
|
||||
border-radius: var(--radius-sm);
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.add-token:hover {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border-color: #28a745;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(40, 167, 69, 0.2);
|
||||
}
|
||||
|
||||
/* Сообщение валидации */
|
||||
.form-validation-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 15px;
|
||||
padding: 10px 15px;
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: var(--radius-sm);
|
||||
color: #856404;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-validation-info i {
|
||||
color: #f39c12;
|
||||
}
|
||||
|
||||
/* Дополнительные настройки */
|
||||
.advanced-settings {
|
||||
margin-top: 20px;
|
||||
@@ -797,6 +832,45 @@ onMounted(() => {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Сообщение об отсутствии прав доступа */
|
||||
.no-access-message {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 30px;
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message-content h3 {
|
||||
color: #856404;
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
.message-content p {
|
||||
color: #856404;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.1em;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.message-content .btn {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.message-content .btn:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.info-grid {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<BaseLayout>
|
||||
<div class="create-table-container">
|
||||
<h2>Создать новую таблицу</h2>
|
||||
<form v-if="isAdmin" @submit.prevent="handleCreateTable" class="create-table-form">
|
||||
<form v-if="canEdit" @submit.prevent="handleCreateTable" class="create-table-form">
|
||||
<label>Название таблицы</label>
|
||||
<input v-model="newTableName" required placeholder="Введите название" />
|
||||
<label>Описание</label>
|
||||
@@ -29,7 +29,10 @@
|
||||
<button type="button" @click="goBack">Отмена</button>
|
||||
</div>
|
||||
</form>
|
||||
<div v-else class="empty-table-placeholder">Нет прав для создания таблицы</div>
|
||||
<div v-else class="empty-table-placeholder">
|
||||
<p>Нет прав для создания таблицы</p>
|
||||
<button type="button" @click="goBack" class="btn btn-primary">Назад</button>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
@@ -40,12 +43,14 @@ import { useRouter } from 'vue-router';
|
||||
import BaseLayout from '../../components/BaseLayout.vue';
|
||||
import tablesService from '../../services/tablesService';
|
||||
import { useAuthContext } from '@/composables/useAuth';
|
||||
import { usePermissions } from '@/composables/usePermissions';
|
||||
|
||||
const router = useRouter();
|
||||
const newTableName = ref('');
|
||||
const newTableDescription = ref('');
|
||||
const newTableIsRagSourceId = ref(2);
|
||||
const { isAdmin } = useAuthContext();
|
||||
const { canEdit } = usePermissions();
|
||||
|
||||
async function handleCreateTable() {
|
||||
if (!newTableName.value) return;
|
||||
@@ -128,4 +133,30 @@ function goBack() {
|
||||
.form-actions button[type="button"]:hover {
|
||||
background: #d5d5d5;
|
||||
}
|
||||
|
||||
.empty-table-placeholder {
|
||||
text-align: center;
|
||||
padding: 2em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.empty-table-placeholder p {
|
||||
margin-bottom: 1.5em;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.empty-table-placeholder .btn {
|
||||
background: #2ecc40;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0.5em 1.2em;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.empty-table-placeholder .btn:hover {
|
||||
background: #27ae38;
|
||||
}
|
||||
</style>
|
||||
@@ -16,10 +16,10 @@
|
||||
<h2>Удалить таблицу?</h2>
|
||||
<p>Вы уверены, что хотите удалить эту таблицу? Это действие необратимо.</p>
|
||||
<div class="actions">
|
||||
<button v-if="isAdmin" class="danger" @click="remove">Удалить</button>
|
||||
<button v-if="canDelete" class="danger" @click="remove">Удалить</button>
|
||||
<button @click="cancel">Отмена</button>
|
||||
</div>
|
||||
<div v-if="!isAdmin" class="empty-table-placeholder">Нет прав для удаления таблицы</div>
|
||||
<div v-if="!canDelete" class="empty-table-placeholder">Нет прав для удаления таблицы</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
@@ -28,9 +28,11 @@ import { useRoute, useRouter } from 'vue-router';
|
||||
import BaseLayout from '../../components/BaseLayout.vue';
|
||||
import axios from 'axios';
|
||||
import { useAuthContext } from '@/composables/useAuth';
|
||||
import { usePermissions } from '@/composables/usePermissions';
|
||||
const $route = useRoute();
|
||||
const router = useRouter();
|
||||
const { isAdmin } = useAuthContext();
|
||||
const { canDelete } = usePermissions();
|
||||
|
||||
async function remove() {
|
||||
await axios.delete(`/tables/${$route.params.id}`);
|
||||
|
||||
@@ -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="isAdmin" class="action-btn" @click="goToEdit">Редактировать</button>
|
||||
<button v-if="isAdmin" class="danger-btn" @click="goToDelete">Удалить</button>
|
||||
<button v-if="canEdit" class="action-btn" @click="goToEdit">Редактировать</button>
|
||||
<button v-if="canDelete" class="danger-btn" @click="goToDelete">Удалить</button>
|
||||
</div>
|
||||
<UserTableView v-if="isAdmin" :table-id="Number($route.params.id)" />
|
||||
<UserTableView v-if="canRead" :table-id="Number($route.params.id)" />
|
||||
<div v-else class="empty-table-placeholder">Нет данных для отображения</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
@@ -31,9 +31,11 @@ import BaseLayout from '../../components/BaseLayout.vue';
|
||||
import UserTableView from '../../components/tables/UserTableView.vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useAuthContext } from '@/composables/useAuth';
|
||||
import { usePermissions } from '@/composables/usePermissions';
|
||||
const $route = useRoute();
|
||||
const router = useRouter();
|
||||
const { isAdmin } = useAuthContext();
|
||||
const { canRead, canEdit, canDelete } = usePermissions();
|
||||
|
||||
function closeTable() {
|
||||
if (window.history.length > 1) {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<div class="tables-list-block">
|
||||
<button class="close-btn" @click="goBack">×</button>
|
||||
<h2>Список таблиц</h2>
|
||||
<UserTablesList v-if="isAdmin" />
|
||||
<UserTablesList v-if="canRead" />
|
||||
<div v-else class="empty-table-placeholder">Нет данных для отображения</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
@@ -26,8 +26,10 @@ import BaseLayout from '../../components/BaseLayout.vue';
|
||||
import UserTablesList from '../../components/tables/UserTablesList.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAuthContext } from '@/composables/useAuth';
|
||||
import { usePermissions } from '@/composables/usePermissions';
|
||||
const router = useRouter();
|
||||
const { isAdmin } = useAuthContext();
|
||||
const { canRead } = usePermissions();
|
||||
function goBack() {
|
||||
router.push({ name: 'crm' });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user