ваше сообщение коммита

This commit is contained in:
2025-07-27 03:30:13 +03:00
parent 057fe6254c
commit 1835632be9
141 changed files with 32514 additions and 6661 deletions

View File

@@ -21,27 +21,34 @@
<div class="crm-view-container">
<div class="dle-management-block">
<h2>Управление DLE</h2>
<button class="btn btn-info" @click="goToDleManagement">
<i class="fas fa-cogs"></i> Подробнее
<button class="details-btn" @click="goToDleManagement">
Подробнее
</button>
</div>
<div class="crm-contacts-block">
<h2>Контакты</h2>
<button class="btn btn-info" @click="goToContactsList">
<i class="fas fa-address-book"></i> Подробнее
<button class="details-btn" @click="goToContactsList">
Подробнее
</button>
</div>
<div class="crm-tables-block">
<h2>Таблицы</h2>
<button class="btn btn-info" @click="goToTables">
<i class="fas fa-table"></i> Подробнее
<button class="details-btn" @click="goToTables">
Подробнее
</button>
</div>
<!-- Новый блок Контент -->
<div class="crm-content-block">
<h2>Контент</h2>
<button class="btn btn-info" @click="goToContent">
<i class="fas fa-file-alt"></i> Подробнее
<button class="details-btn" @click="goToContent">
Подробнее
</button>
</div>
<!-- Новый блок Управление -->
<div class="crm-management-block">
<h2>Управление</h2>
<button class="details-btn" @click="goToManagement">
Подробнее
</button>
</div>
</div>
@@ -218,6 +225,10 @@ function goToContactsList() {
function goToContent() {
router.push({ name: 'content-list' });
}
function goToManagement() {
router.push({ name: 'management' });
}
</script>
<style scoped>
@@ -258,57 +269,28 @@ strong {
to { transform: rotate(360deg); }
}
.btn {
display: inline-block;
font-weight: 400;
text-align: center;
white-space: nowrap;
vertical-align: middle;
user-select: none;
border: 1px solid transparent;
padding: 0.375rem 0.75rem;
font-size: 1rem;
line-height: 1.5;
border-radius: 0.25rem;
transition: all 0.15s ease-in-out;
.details-btn {
background: var(--color-primary);
color: #fff;
border: none;
border-radius: 6px;
padding: 0.5rem 1.5rem;
cursor: pointer;
font-size: 1rem;
transition: background 0.2s;
margin: 0;
}
.btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
line-height: 1.5;
border-radius: 0.2rem;
.details-btn:hover {
background: var(--color-primary-dark);
}
.btn-primary {
color: #fff;
background-color: var(--color-primary);
border-color: var(--color-primary);
.details-btn-secondary {
background: #6c757d;
}
.btn-secondary {
color: #fff;
background-color: var(--color-grey-dark);
border-color: var(--color-grey-dark);
}
.btn-success {
color: #fff;
background-color: #28a745;
border-color: #28a745;
}
.btn-danger {
color: #fff;
background-color: #dc3545;
border-color: #dc3545;
}
.btn-info {
color: #fff;
background-color: #17a2b8;
border-color: #17a2b8;
.details-btn-secondary:hover {
background: #5a6268;
}
.dle-management-block {
@@ -326,9 +308,8 @@ strong {
font-size: 1.4rem;
font-weight: 600;
}
.dle-management-block .btn {
font-size: 1rem;
padding: 8px 18px;
.dle-management-block .details-btn {
margin-top: 0;
}
.crm-contacts-block {
@@ -346,9 +327,8 @@ strong {
font-size: 1.4rem;
font-weight: 600;
}
.crm-contacts-block .btn {
font-size: 1rem;
padding: 8px 18px;
.crm-contacts-block .details-btn {
margin-top: 0;
}
.crm-tables-block {
@@ -366,9 +346,8 @@ strong {
font-size: 1.4rem;
font-weight: 600;
}
.crm-tables-block .btn {
font-size: 1rem;
padding: 8px 18px;
.crm-tables-block .details-btn {
margin-top: 0;
}
.crm-content-block {
@@ -386,8 +365,26 @@ strong {
font-size: 1.4rem;
font-weight: 600;
}
.crm-content-block .btn {
font-size: 1rem;
padding: 8px 18px;
.crm-content-block .details-btn {
margin-top: 0;
}
.crm-management-block {
margin: 32px 0 24px 0;
padding: 24px;
background: #f8fafc;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
display: flex;
align-items: center;
justify-content: space-between;
}
.crm-management-block h2 {
margin: 0;
font-size: 1.4rem;
font-weight: 600;
}
.crm-management-block .details-btn {
margin-top: 0;
}
</style>

View File

@@ -0,0 +1,277 @@
<!--
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="management-container">
<!-- Заголовок -->
<div class="management-header">
<h1>Управление DLE</h1>
<button class="close-btn" @click="router.push('/')">×</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>DLE</h3>
<p>Интеграция с другими DLE, участие в кворумах</p>
<button class="details-btn" @click="openDle">Подробнее</button>
</div>
<div class="management-block">
<h3>Казна</h3>
<p>Управление средствами</p>
<button class="details-btn" @click="openTreasury">Подробнее</button>
</div>
</div>
<!-- Третий ряд -->
<div class="blocks-row">
<div class="management-block">
<h3>Аналитика</h3>
<p>Графики, статистика, отчеты</p>
<button class="details-btn" @click="openAnalytics">Подробнее</button>
</div>
<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 { defineProps, defineEmits } from 'vue';
import { useRouter } from 'vue-router';
import BaseLayout from '../components/BaseLayout.vue';
// Определяем props
const props = defineProps({
isAuthenticated: Boolean,
identities: Array,
tokenBalances: Object,
isLoadingTokens: Boolean
});
// Определяем emits
const emit = defineEmits(['auth-action-completed']);
const router = useRouter();
// Функции для открытия страниц управления
const openProposals = () => {
router.push('/management/proposals');
};
const openTokens = () => {
router.push('/management/tokens');
};
const openQuorum = () => {
router.push('/management/quorum');
};
const openModules = () => {
router.push('/management/modules');
};
const openDle = () => {
router.push('/management/dle');
};
const openTreasury = () => {
router.push('/management/treasury');
};
const openAnalytics = () => {
router.push('/management/analytics');
};
const openHistory = () => {
router.push('/management/history');
};
const openSettings = () => {
router.push('/management/settings');
};
</script>
<style scoped>
.management-container {
padding: 20px;
background-color: var(--color-white);
border-radius: var(--radius-lg);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-top: 20px;
margin-bottom: 20px;
}
.management-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 2px solid #f0f0f0;
}
.management-header h1 {
color: var(--color-primary);
font-size: 2.5rem;
margin: 0;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #666;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s;
}
.close-btn:hover {
background: #f0f0f0;
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: 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);
}
.management-block h3 {
color: var(--color-primary);
margin: 0 0 1rem 0;
font-size: 1.5rem;
font-weight: 600;
}
.management-block p {
color: var(--color-grey-dark);
margin: 0 0 1.5rem 0;
line-height: 1.5;
flex-grow: 1;
}
.details-btn {
background: var(--color-primary);
color: #fff;
border: none;
border-radius: 6px;
padding: 0.75rem 1.5rem;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: background 0.2s;
margin: 0;
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-block {
padding: 1.5rem;
}
.management-block h3 {
font-size: 1.3rem;
}
}
</style>

View File

@@ -19,7 +19,14 @@
@auth-action-completed="$emit('auth-action-completed')"
>
<div class="settings-view-container">
<h1>Настройки</h1>
<div class="page-header">
<h1>{{ pageTitle }}</h1>
<button
v-if="route.name === 'settings-blockchain-dle-deploy' || route.name === 'settings-dle-v2-deploy'"
class="close-btn"
@click="router.push('/settings')"
>×</button>
</div>
<!-- Router view для отображения дочерних компонентов настроек -->
<router-view></router-view>
</div>
@@ -50,6 +57,17 @@ const router = useRouter();
const route = useRoute();
const isLoading = ref(true);
// Вычисляемый заголовок страницы в зависимости от роута
const pageTitle = computed(() => {
if (route.name === 'settings-blockchain-dle-deploy') {
return 'Создать новое DLE (Digital Legal Entity)';
}
if (route.name === 'settings-dle-v2-deploy') {
return 'Создать современное DLE v2 (Digital Legal Entity)';
}
return 'Настройки';
});
// Обработчик события изменения авторизации
const handleAuthEvent = (eventData) => {
console.log('[SettingsView] Получено событие изменения авторизации:', eventData);
@@ -91,9 +109,37 @@ onBeforeUnmount(() => {
}
/* Заголовки */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-lg);
}
h1 {
color: var(--color-dark);
margin-bottom: var(--spacing-lg);
margin: 0;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #666;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s;
}
.close-btn:hover {
background: #f0f0f0;
color: #333;
}
h3 {

View File

@@ -24,6 +24,7 @@
:showApiKey="false"
:showBaseUrl="true"
/>
<OllamaModelManager />
</div>
</BaseLayout>
</template>
@@ -31,6 +32,7 @@
<script setup>
import BaseLayout from '@/components/BaseLayout.vue';
import AIProviderSettings from '@/views/settings/AIProviderSettings.vue';
import OllamaModelManager from '@/components/OllamaModelManager.vue';
import { useRouter } from 'vue-router';
const router = useRouter();

View File

@@ -103,20 +103,41 @@ async function loadSettings() {
async function loadModels() {
try {
const { data } = await axios.get(`/settings/ai-settings/${props.provider}/models`);
let data;
if (props.provider === 'ollama') {
// Для Ollama используем специальный API
const response = await axios.get('/ollama/models');
data = { models: response.data.models.map(m => ({ id: m.name, name: m.name })) };
} else {
// Для других провайдеров используем стандартный API
const response = await axios.get(`/settings/ai-settings/${props.provider}/models`);
data = response.data;
}
models.value = data.models || [];
if (!selectedModel.value && models.value.length) {
const first = models.value[0];
selectedModel.value = first.id || first.name || first;
}
} catch (e) {
console.error('Error loading models:', e);
models.value = [];
}
}
async function loadEmbeddingModels() {
try {
const { data } = await axios.get(`/settings/ai-settings/${props.provider}/models`);
let data;
if (props.provider === 'ollama') {
// Для Ollama используем специальный API
const response = await axios.get('/ollama/models');
data = { models: response.data.models.map(m => ({ id: m.name, name: m.name })) };
} else {
// Для других провайдеров используем стандартный API
const response = await axios.get(`/settings/ai-settings/${props.provider}/models`);
data = response.data;
}
embeddingModels.value = (data.models || []).filter(m => {
const name = m.id || m.name || m;
return name && name.toLowerCase().includes('embed');
@@ -126,6 +147,7 @@ async function loadEmbeddingModels() {
selectedEmbeddingModel.value = first.id || first.name || first;
}
} catch (e) {
console.error('Error loading embedding models:', e);
embeddingModels.value = [];
}
}
@@ -135,13 +157,24 @@ async function onVerify() {
verifyStatus.value = null;
verifyError.value = '';
try {
const { data } = await axios.post(`/settings/ai-settings/${props.provider}/verify`, {
api_key: apiKey.value,
base_url: baseUrl.value,
});
let data;
if (props.provider === 'ollama') {
// Для Ollama используем специальный API
const response = await axios.get('/ollama/status');
data = { success: response.data.connected };
} else {
// Для других провайдеров используем стандартный API
const response = await axios.post(`/settings/ai-settings/${props.provider}/verify`, {
api_key: apiKey.value,
base_url: baseUrl.value,
});
data = response.data;
}
verifyStatus.value = data.success;
if (data.success) {
await loadModels();
await loadEmbeddingModels();
}
} catch (e) {
verifyStatus.value = false;

View File

@@ -396,6 +396,8 @@ const dleDeploymentSettings = reactive({
selectedIsicCodes: [], // <<< Для хранения массива выбранных кодов ISIC
});
// Добавляем переменную useCustomGas для управления отображением пользовательских настроек газа
const useCustomGas = ref(false);
@@ -668,6 +670,8 @@ const deployDLE = async () => {
}
};
const validateDLEForm = () => {
// Проверяем обязательные поля
if (!dleDeploymentSettings.name) {
@@ -1397,4 +1401,6 @@ h3 {
.mb-gt-total {
margin-bottom: 2.5rem;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -22,6 +22,11 @@
<p>Интеграция с блокчейн-сетями, RPC, токены и смарт-контракты.</p>
<button class="details-btn" @click="$router.push('/settings/blockchain')">Подробнее</button>
</div>
<div class="main-block">
<h3>Блокчейн 2</h3>
<p>Современный DLE v2 - единый смарт-контракт с встроенной системой голосования.</p>
<button class="details-btn details-btn-secondary" @click="$router.push('/settings/dle-v2-deploy')">Подробнее</button>
</div>
<div class="main-block">
<h3>Безопасность</h3>
<p>Управление доступом, токенами, аутентификацией и правами.</p>
@@ -32,62 +37,14 @@
<p>Настройки серверов, хостинга и публикации приложения.</p>
<button class="details-btn" @click="$router.push('/settings/interface')">Подробнее</button>
</div>
<div class="main-block copyright-block">
<h3>© Авторские права</h3>
<p>Информация об авторских правах и лицензировании проекта DLE.</p>
<div class="copyright-info">
<p><strong>Автор:</strong> Тарабанов Александр Викторович</p>
<p><strong>Email:</strong> info@hb3-accelerator.com</p>
<p><strong>Сайт:</strong> <a href="https://hb3-accelerator.com" target="_blank">hb3-accelerator.com</a></p>
<p><strong>GitHub:</strong> <a href="https://github.com/HB3-ACCELERATOR" target="_blank">@HB3-ACCELERATOR</a></p>
</div>
<button class="details-btn" @click="showCopyrightModal = true">Подробнее</button>
</div>
</div>
<!-- Модальное окно с информацией об авторских правах -->
<div v-if="showCopyrightModal" class="modal-overlay" @click="showCopyrightModal = false">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h2>© Авторские права DLE</h2>
<button class="close-btn" @click="showCopyrightModal = false">&times;</button>
</div>
<div class="modal-body">
<div class="copyright-section">
<h3>Основная информация</h3>
<p><strong>Автор:</strong> Тарабанов Александр Викторович</p>
<p><strong>Период:</strong> 2024-2025</p>
<p><strong>Статус:</strong> Проприетарное программное обеспечение</p>
</div>
<div class="copyright-section">
<h3>Контакты</h3>
<p><strong>Email:</strong> <a href="mailto:info@hb3-accelerator.com">info@hb3-accelerator.com</a></p>
<p><strong>Сайт:</strong> <a href="https://hb3-accelerator.com" target="_blank">hb3-accelerator.com</a></p>
<p><strong>GitHub:</strong> <a href="https://github.com/HB3-ACCELERATOR" target="_blank">@HB3-ACCELERATOR</a></p>
</div>
<div class="copyright-section">
<h3>Условия использования</h3>
<p> <strong>Разрешено:</strong> Использование в бизнесе для внутренних операций</p>
<p> <strong>Запрещено:</strong> Перепродажа, модификация, копирование без разрешения</p>
<p>📋 <strong>Требуется:</strong> Разрешение автора для коммерческого использования</p>
</div>
<div class="copyright-section">
<h3>Лицензирование</h3>
<p>Для получения коммерческой лицензии обращайтесь к автору по указанным контактам.</p>
<p>Все права защищены. Несанкционированное использование запрещено.</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const showCopyrightModal = ref(false);
// Компонент настроек
</script>
<style scoped>
@@ -107,8 +64,14 @@ const showCopyrightModal = ref(false);
flex-direction: column;
align-items: flex-start;
}
.details-btn {
.button-group {
margin-top: 1.5rem;
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.details-btn {
background: var(--color-primary);
color: #fff;
border: none;
@@ -117,121 +80,25 @@ const showCopyrightModal = ref(false);
cursor: pointer;
font-size: 1rem;
transition: background 0.2s;
margin: 0;
}
.details-btn:hover {
background: var(--color-primary-dark);
}
.copyright-block {
border-left: 4px solid var(--color-primary);
.details-btn-secondary {
background: #6c757d;
}
.copyright-info {
margin: 1rem 0;
padding: 1rem;
background: #f8f9fa;
border-radius: 6px;
font-size: 0.9rem;
.details-btn-secondary:hover {
background: #5a6268;
}
.copyright-info p {
margin: 0.5rem 0;
/* Для блоков без группы кнопок */
.main-block .details-btn:not(.button-group .details-btn) {
margin-top: 1.5rem;
}
.copyright-info a {
color: var(--color-primary);
text-decoration: none;
}
.copyright-info a:hover {
text-decoration: underline;
}
/* Модальное окно */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 12px;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid #eee;
}
.modal-header h2 {
margin: 0;
color: var(--color-primary);
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #666;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.close-btn:hover {
color: #333;
}
.modal-body {
padding: 1.5rem;
}
.copyright-section {
margin-bottom: 2rem;
}
.copyright-section h3 {
color: var(--color-primary);
margin-bottom: 1rem;
font-size: 1.1rem;
}
.copyright-section p {
margin: 0.5rem 0;
line-height: 1.5;
}
.copyright-section a {
color: var(--color-primary);
text-decoration: none;
}
.copyright-section a:hover {
text-decoration: underline;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
</style>

View File

@@ -0,0 +1,705 @@
<!--
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="analytics-container">
<!-- Заголовок -->
<div class="page-header">
<div class="header-content">
<h1>Аналитика</h1>
<p>Графики, статистика и отчеты DLE</p>
</div>
<button class="close-btn" @click="router.push('/management')">×</button>
</div>
<!-- Ключевые метрики -->
<div class="metrics-section">
<h2>Ключевые метрики</h2>
<div class="metrics-grid">
<div class="metric-card">
<h3>Общая стоимость</h3>
<p class="metric-value">${{ totalValue.toLocaleString() }}</p>
<p class="metric-change positive">+{{ valueChange }}% (30д)</p>
</div>
<div class="metric-card">
<h3>Активные участники</h3>
<p class="metric-value">{{ activeParticipants }}</p>
<p class="metric-change positive">+{{ participantsChange }} (30д)</p>
</div>
<div class="metric-card">
<h3>Предложения</h3>
<p class="metric-value">{{ totalProposals }}</p>
<p class="metric-change positive">+{{ proposalsChange }} (30д)</p>
</div>
<div class="metric-card">
<h3>Доходность</h3>
<p class="metric-value">{{ yieldRate }}%</p>
<p class="metric-change positive">+{{ yieldChange }}% (30д)</p>
</div>
</div>
</div>
<!-- Графики -->
<div class="charts-section">
<h2>Графики</h2>
<div class="charts-grid">
<!-- График стоимости токенов -->
<div class="chart-card">
<h3>Стоимость токенов</h3>
<div class="chart-placeholder">
<div class="chart-line">
<div class="chart-point" style="left: 10%; top: 80%"></div>
<div class="chart-point" style="left: 25%; top: 60%"></div>
<div class="chart-point" style="left: 40%; top: 40%"></div>
<div class="chart-point" style="left: 55%; top: 30%"></div>
<div class="chart-point" style="left: 70%; top: 20%"></div>
<div class="chart-point" style="left: 85%; top: 10%"></div>
<div class="chart-point" style="left: 100%; top: 5%"></div>
</div>
</div>
<div class="chart-legend">
<span class="legend-item">
<span class="legend-color" style="background: var(--color-primary)"></span>
Стоимость токена
</span>
</div>
</div>
<!-- График активности -->
<div class="chart-card">
<h3>Активность участников</h3>
<div class="activity-chart">
<div class="activity-bar" style="height: 60%">
<span class="bar-label">Пн</span>
</div>
<div class="activity-bar" style="height: 80%">
<span class="bar-label">Вт</span>
</div>
<div class="activity-bar" style="height: 45%">
<span class="bar-label">Ср</span>
</div>
<div class="activity-bar" style="height: 90%">
<span class="bar-label">Чт</span>
</div>
<div class="activity-bar" style="height: 75%">
<span class="bar-label">Пт</span>
</div>
<div class="activity-bar" style="height: 55%">
<span class="bar-label">Сб</span>
</div>
<div class="activity-bar" style="height: 40%">
<span class="bar-label">Вс</span>
</div>
</div>
<div class="chart-legend">
<span class="legend-item">
<span class="legend-color" style="background: var(--color-secondary)"></span>
Количество операций
</span>
</div>
</div>
</div>
</div>
<!-- Статистика -->
<div class="statistics-section">
<h2>Статистика</h2>
<div class="stats-grid">
<div class="stats-card">
<h3>Распределение токенов</h3>
<div class="distribution-chart">
<div class="pie-segment" style="--percentage: 40; --color: #007bff">
<span class="segment-label">Крупные держатели</span>
<span class="segment-value">40%</span>
</div>
<div class="pie-segment" style="--percentage: 30; --color: #28a745">
<span class="segment-label">Средние держатели</span>
<span class="segment-value">30%</span>
</div>
<div class="pie-segment" style="--percentage: 20; --color: #ffc107">
<span class="segment-label">Малые держатели</span>
<span class="segment-value">20%</span>
</div>
<div class="pie-segment" style="--percentage: 10; --color: #dc3545">
<span class="segment-label">Резерв</span>
<span class="segment-value">10%</span>
</div>
</div>
</div>
<div class="stats-card">
<h3>Топ участников</h3>
<div class="top-participants">
<div
v-for="participant in topParticipants"
:key="participant.address"
class="participant-item"
>
<div class="participant-info">
<span class="participant-rank">#{{ participant.rank }}</span>
<span class="participant-address">{{ formatAddress(participant.address) }}</span>
</div>
<div class="participant-stats">
<span class="participant-balance">{{ participant.balance }} токенов</span>
<span class="participant-percentage">{{ participant.percentage }}%</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Отчеты -->
<div class="reports-section">
<h2>Отчеты</h2>
<div class="reports-grid">
<div class="report-card">
<h3>Ежемесячный отчет</h3>
<p>Подробный анализ деятельности DLE за последний месяц</p>
<div class="report-actions">
<button class="btn-secondary">Просмотреть</button>
<button class="btn-secondary">Скачать PDF</button>
</div>
</div>
<div class="report-card">
<h3>Финансовый отчет</h3>
<p>Анализ финансового состояния и доходности</p>
<div class="report-actions">
<button class="btn-secondary">Просмотреть</button>
<button class="btn-secondary">Скачать PDF</button>
</div>
</div>
<div class="report-card">
<h3>Отчет по предложениям</h3>
<p>Статистика и анализ предложений за период</p>
<div class="report-actions">
<button class="btn-secondary">Просмотреть</button>
<button class="btn-secondary">Скачать PDF</button>
</div>
</div>
<div class="report-card">
<h3>Отчет по активности</h3>
<p>Анализ активности участников и операций</p>
<div class="report-actions">
<button class="btn-secondary">Просмотреть</button>
<button class="btn-secondary">Скачать PDF</button>
</div>
</div>
</div>
</div>
</div>
</BaseLayout>
</template>
<script setup>
import { ref, defineProps, defineEmits } from 'vue';
import { useRouter } from 'vue-router';
import BaseLayout from '../../components/BaseLayout.vue';
// Определяем props
const props = defineProps({
isAuthenticated: Boolean,
identities: Array,
tokenBalances: Object,
isLoadingTokens: Boolean
});
// Определяем emits
const emit = defineEmits(['auth-action-completed']);
const router = useRouter();
// Ключевые метрики
const totalValue = ref(2500000);
const valueChange = ref(12.5);
const activeParticipants = ref(156);
const participantsChange = ref(23);
const totalProposals = ref(45);
const proposalsChange = ref(8);
const yieldRate = ref(8.7);
const yieldChange = ref(1.2);
// Топ участников (временные данные)
const topParticipants = ref([
{
rank: 1,
address: '0x1234567890123456789012345678901234567890',
balance: 2500,
percentage: 25.0
},
{
rank: 2,
address: '0x2345678901234567890123456789012345678901',
balance: 1800,
percentage: 18.0
},
{
rank: 3,
address: '0x3456789012345678901234567890123456789012',
balance: 1200,
percentage: 12.0
},
{
rank: 4,
address: '0x4567890123456789012345678901234567890123',
balance: 800,
percentage: 8.0
},
{
rank: 5,
address: '0x5678901234567890123456789012345678901234',
balance: 600,
percentage: 6.0
}
]);
// Методы
const formatAddress = (address) => {
if (!address) return '';
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
};
</script>
<style scoped>
.analytics-container {
padding: 20px;
background-color: var(--color-white);
border-radius: var(--radius-lg);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-top: 20px;
margin-bottom: 20px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 2px solid #f0f0f0;
}
.header-content {
flex-grow: 1;
}
.page-header h1 {
color: var(--color-primary);
font-size: 2.5rem;
margin: 0 0 10px 0;
}
.page-header p {
color: var(--color-grey-dark);
font-size: 1.1rem;
margin: 0;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #666;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s;
flex-shrink: 0;
}
.close-btn:hover {
background: #f0f0f0;
color: #333;
}
/* Секции */
.metrics-section,
.charts-section,
.statistics-section,
.reports-section {
margin-bottom: 40px;
}
.metrics-section h2,
.charts-section h2,
.statistics-section h2,
.reports-section h2 {
color: var(--color-primary);
margin-bottom: 20px;
font-size: 1.8rem;
}
/* Ключевые метрики */
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.metric-card {
background: #f8f9fa;
padding: 25px;
border-radius: var(--radius-lg);
border-left: 4px solid var(--color-primary);
text-align: center;
}
.metric-card h3 {
color: var(--color-primary);
margin-bottom: 15px;
font-size: 1rem;
text-transform: uppercase;
font-weight: 600;
}
.metric-value {
font-size: 2rem;
font-weight: 700;
margin: 0 0 10px 0;
color: var(--color-primary);
}
.metric-change {
font-size: 1rem;
font-weight: 600;
margin: 0;
}
.metric-change.positive {
color: #28a745;
}
.metric-change.negative {
color: #dc3545;
}
/* Графики */
.charts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 30px;
}
.chart-card {
background: white;
padding: 25px;
border-radius: var(--radius-lg);
border: 1px solid #e9ecef;
}
.chart-card h3 {
color: var(--color-primary);
margin-bottom: 20px;
font-size: 1.3rem;
}
/* График стоимости токенов */
.chart-placeholder {
height: 200px;
background: #f8f9fa;
border-radius: var(--radius-sm);
position: relative;
margin-bottom: 15px;
border: 1px solid #e9ecef;
}
.chart-line {
position: relative;
height: 100%;
width: 100%;
}
.chart-point {
position: absolute;
width: 8px;
height: 8px;
background: var(--color-primary);
border-radius: 50%;
transform: translate(-50%, -50%);
}
.chart-point::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 12px;
height: 12px;
background: rgba(0, 123, 255, 0.2);
border-radius: 50%;
transform: translate(-50%, -50%);
}
/* График активности */
.activity-chart {
height: 200px;
display: flex;
align-items: end;
justify-content: space-between;
gap: 10px;
margin-bottom: 15px;
}
.activity-bar {
background: var(--color-secondary);
border-radius: 4px 4px 0 0;
min-width: 30px;
position: relative;
transition: all 0.3s ease;
}
.activity-bar:hover {
background: var(--color-secondary-dark);
}
.bar-label {
position: absolute;
bottom: -25px;
left: 50%;
transform: translateX(-50%);
font-size: 0.8rem;
color: var(--color-grey-dark);
}
/* Легенда графиков */
.chart-legend {
display: flex;
justify-content: center;
gap: 20px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.9rem;
color: var(--color-grey-dark);
}
.legend-color {
width: 12px;
height: 12px;
border-radius: 2px;
}
/* Статистика */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 30px;
}
.stats-card {
background: white;
padding: 25px;
border-radius: var(--radius-lg);
border: 1px solid #e9ecef;
}
.stats-card h3 {
color: var(--color-primary);
margin-bottom: 20px;
font-size: 1.3rem;
}
/* Круговая диаграмма */
.distribution-chart {
display: flex;
flex-direction: column;
gap: 15px;
}
.pie-segment {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
background: #f8f9fa;
border-radius: var(--radius-sm);
border-left: 4px solid var(--color);
}
.segment-label {
font-weight: 600;
color: var(--color-grey-dark);
}
.segment-value {
font-weight: 700;
color: var(--color);
}
/* Топ участников */
.top-participants {
display: flex;
flex-direction: column;
gap: 15px;
}
.participant-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background: #f8f9fa;
border-radius: var(--radius-sm);
transition: all 0.3s ease;
}
.participant-item:hover {
background: #e9ecef;
}
.participant-info {
display: flex;
align-items: center;
gap: 15px;
}
.participant-rank {
background: var(--color-primary);
color: white;
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.8rem;
font-weight: 600;
}
.participant-address {
font-family: monospace;
font-size: 0.9rem;
color: var(--color-grey-dark);
}
.participant-stats {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 5px;
}
.participant-balance {
font-weight: 600;
color: var(--color-primary);
font-size: 0.9rem;
}
.participant-percentage {
font-size: 0.8rem;
color: var(--color-grey-dark);
}
/* Отчеты */
.reports-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.report-card {
background: white;
padding: 25px;
border-radius: var(--radius-lg);
border: 1px solid #e9ecef;
transition: all 0.3s ease;
}
.report-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.report-card h3 {
color: var(--color-primary);
margin-bottom: 15px;
font-size: 1.2rem;
}
.report-card p {
color: var(--color-grey-dark);
margin-bottom: 20px;
line-height: 1.5;
}
.report-actions {
display: flex;
gap: 10px;
}
/* Кнопки */
.btn-secondary {
background: var(--color-secondary);
color: white;
border: none;
padding: 8px 16px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-secondary:hover {
background: var(--color-secondary-dark);
}
/* Адаптивность */
@media (max-width: 768px) {
.charts-grid {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: 1fr;
}
.reports-grid {
grid-template-columns: 1fr;
}
.report-actions {
flex-direction: column;
}
.participant-item {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.participant-stats {
align-items: flex-start;
}
.metrics-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,499 @@
<!--
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-management-container">
<!-- Заголовок -->
<div class="page-header">
<div class="header-content">
<h1>Управление DLE</h1>
<p>Интеграция с другими DLE и участие в кворумах</p>
</div>
<button class="close-btn" @click="router.push('/management')">×</button>
</div>
<!-- Карточки DLE -->
<div class="dle-cards">
<div
v-for="dle in dleList"
:key="dle.address"
class="dle-card"
@click="openDleInterface(dle)"
>
<div class="dle-card-header">
<h3>{{ dle.name }}</h3>
<button
@click.stop="removeDle(dle.address)"
class="remove-btn"
title="Удалить DLE"
>
🗑
</button>
</div>
<div class="dle-card-content">
<p class="dle-address">Адрес: {{ formatAddress(dle.address) }}</p>
<p class="dle-location">Местонахождение: {{ dle.location }}</p>
</div>
</div>
<!-- Кнопка добавления нового DLE -->
<div class="dle-card add-dle-card" @click="showAddDleForm = true">
<div class="add-dle-content">
<div class="add-icon">+</div>
<h3>Добавить DLE</h3>
<p>Подключить новый DLE для управления</p>
</div>
</div>
</div>
<!-- Модальное окно добавления DLE -->
<div v-if="showAddDleForm" class="modal-overlay" @click="showAddDleForm = false">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>Добавить новый DLE</h3>
<button @click="showAddDleForm = false" class="close-btn"></button>
</div>
<div class="modal-body">
<form @submit.prevent="addNewDle" class="add-dle-form">
<div class="form-group">
<label for="dleName">Название DLE:</label>
<input
id="dleName"
v-model="newDle.name"
type="text"
placeholder="Введите название DLE"
required
>
</div>
<div class="form-group">
<label for="dleAddress">Адрес контракта:</label>
<input
id="dleAddress"
v-model="newDle.address"
type="text"
placeholder="0x..."
required
>
</div>
<div class="form-group">
<label for="dleLocation">Местонахождение:</label>
<input
id="dleLocation"
v-model="newDle.location"
type="text"
placeholder="Страна, город"
required
>
</div>
<div class="form-actions">
<button type="button" @click="showAddDleForm = false" class="btn-secondary">
Отмена
</button>
<button type="submit" class="btn-primary">
Добавить DLE
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</BaseLayout>
</template>
<script setup>
import { ref, defineProps, defineEmits } from 'vue';
import { useRouter } from 'vue-router';
import BaseLayout from '../../components/BaseLayout.vue';
// Определяем props
const props = defineProps({
isAuthenticated: Boolean,
identities: Array,
tokenBalances: Object,
isLoadingTokens: Boolean
});
// Определяем emits
const emit = defineEmits(['auth-action-completed']);
const router = useRouter();
// Состояние
const showAddDleForm = ref(false);
const newDle = ref({
name: '',
address: '',
location: ''
});
// Список DLE (временные данные для демонстрации)
const dleList = ref([
{
name: 'test2 (test2)',
address: '0xef49...dfD8',
location: '245000, 中国, 黄山市'
},
{
name: 'My DLE',
address: '0x1234...5678',
location: '101000, Россия, Москва'
}
]);
// Методы
const formatAddress = (address) => {
if (!address) return '';
if (address.length <= 10) return address;
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
};
const addNewDle = () => {
if (!newDle.value.name || !newDle.value.address || !newDle.value.location) {
return;
}
dleList.value.push({
name: newDle.value.name,
address: newDle.value.address,
location: newDle.value.location
});
// Сброс формы
newDle.value = {
name: '',
address: '',
location: ''
};
showAddDleForm.value = false;
};
const removeDle = (address) => {
dleList.value = dleList.value.filter(dle => dle.address !== address);
};
const openDleInterface = (dle) => {
// Здесь будет логика открытия интерфейса DLE
// Вариант 1: Новая вкладка с внешним сайтом
// window.open(`https://example.com/dle/${dle.address}`, '_blank');
// Вариант 2: Встроенный интерфейс в текущей вкладке
router.push(`/management/dle/${dle.address}`);
};
</script>
<style scoped>
.dle-management-container {
padding: 20px;
background-color: var(--color-white);
border-radius: var(--radius-lg);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-top: 20px;
margin-bottom: 20px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 2px solid #f0f0f0;
}
.header-content {
flex-grow: 1;
}
.page-header h1 {
color: var(--color-primary);
font-size: 2.5rem;
margin: 0 0 10px 0;
}
.page-header p {
color: var(--color-grey-dark);
font-size: 1.1rem;
margin: 0;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #666;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s;
flex-shrink: 0;
}
.close-btn:hover {
background: #f0f0f0;
color: #333;
}
/* Карточки DLE */
.dle-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 2rem;
margin-top: 2rem;
}
.dle-card {
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
padding: 1.5rem;
border: 1px solid #e9ecef;
transition: all 0.3s ease;
cursor: pointer;
min-height: 150px;
display: flex;
flex-direction: column;
}
.dle-card:hover {
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
transform: translateY(-2px);
}
.dle-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.dle-card-header h3 {
color: var(--color-primary);
margin: 0;
font-size: 1.3rem;
font-weight: 600;
}
.remove-btn {
background: none;
border: none;
font-size: 1.2rem;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: background 0.2s;
}
.remove-btn:hover {
background: #ffebee;
}
.dle-card-content {
flex-grow: 1;
}
.dle-address {
font-family: monospace;
font-size: 0.9rem;
color: var(--color-grey-dark);
margin: 0 0 0.5rem 0;
}
.dle-location {
font-size: 0.9rem;
color: var(--color-grey-dark);
margin: 0;
}
/* Карточка добавления */
.add-dle-card {
border: 2px dashed #dee2e6;
background: #f8f9fa;
justify-content: center;
align-items: center;
text-align: center;
}
.add-dle-card:hover {
border-color: var(--color-primary);
background: #f0f8ff;
}
.add-dle-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.add-icon {
font-size: 2rem;
color: var(--color-primary);
font-weight: bold;
}
.add-dle-content h3 {
color: var(--color-primary);
margin: 0;
font-size: 1.2rem;
}
.add-dle-content p {
color: var(--color-grey-dark);
margin: 0;
font-size: 0.9rem;
}
/* Модальное окно */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: var(--radius-lg);
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid #e9ecef;
}
.modal-header h3 {
margin: 0;
color: var(--color-primary);
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--color-grey-dark);
padding: 4px;
}
.modal-body {
padding: 1.5rem;
}
/* Форма */
.add-dle-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-group label {
font-weight: 600;
color: var(--color-grey-dark);
}
.form-group input {
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: var(--radius-sm);
font-size: 1rem;
}
.form-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 1rem;
}
.btn-primary {
background: var(--color-primary);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: background 0.2s;
}
.btn-primary:hover {
background: var(--color-primary-dark);
}
.btn-secondary {
background: var(--color-secondary);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: background 0.2s;
}
.btn-secondary:hover {
background: var(--color-secondary-dark);
}
/* Адаптивность */
@media (max-width: 768px) {
.dle-cards {
grid-template-columns: 1fr;
}
.dle-card {
padding: 1rem;
}
.form-actions {
flex-direction: column;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,766 @@
<!--
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="modules-container">
<!-- Заголовок -->
<div class="page-header">
<div class="header-content">
<h1>Модули DLE</h1>
<p>Установка, настройка и управление модулями</p>
</div>
<button class="close-btn" @click="router.push('/management')">×</button>
</div>
<!-- Доступные модули -->
<div class="available-modules-section">
<h2>Доступные модули</h2>
<div class="modules-grid">
<div
v-for="module in availableModules"
:key="module.id"
class="module-card"
:class="{ 'module-installed': module.installed }"
>
<div class="module-header">
<h3>{{ module.name }}</h3>
<span class="module-version">v{{ module.version }}</span>
</div>
<p class="module-description">{{ module.description }}</p>
<div class="module-features">
<span
v-for="feature in module.features"
:key="feature"
class="feature-tag"
>
{{ feature }}
</span>
</div>
<div class="module-actions">
<button
v-if="!module.installed"
@click="installModule(module.id)"
class="btn-primary"
:disabled="isInstalling"
>
{{ isInstalling ? 'Установка...' : 'Установить' }}
</button>
<button
v-else
@click="openModuleInterface(module)"
class="btn-secondary"
>
Управление
</button>
</div>
</div>
</div>
</div>
<!-- Установленные модули -->
<div class="installed-modules-section">
<h2>Установленные модули</h2>
<div v-if="installedModules.length === 0" class="empty-state">
<p>Нет установленных модулей</p>
</div>
<div v-else class="installed-modules-list">
<div
v-for="module in installedModules"
:key="module.address"
class="installed-module-card"
>
<div class="module-info">
<div class="module-header">
<h3>{{ module.name }}</h3>
<span class="module-status" :class="module.status">
{{ getStatusText(module.status) }}
</span>
</div>
<p class="module-description">{{ module.description }}</p>
<p class="module-address">Адрес: {{ formatAddress(module.address) }}</p>
<p class="module-version">Версия: {{ module.version }}</p>
</div>
<div class="module-actions">
<button @click="openModuleInterface(module)" class="btn-secondary">
Управление
</button>
<button @click="configureModule(module)" class="btn-secondary">
Настройки
</button>
<button @click="uninstallModule(module.address)" class="btn-danger">
Удалить
</button>
</div>
</div>
</div>
</div>
<!-- Модальное окно настройки модуля -->
<div v-if="showConfigModal" class="modal-overlay" @click="showConfigModal = false">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>Настройки модуля {{ selectedModule?.name }}</h3>
<button @click="showConfigModal = false" class="close-btn"></button>
</div>
<div class="modal-body">
<form @submit.prevent="saveModuleConfig" class="config-form">
<div
v-for="setting in selectedModule?.configSettings"
:key="setting.key"
class="form-group"
>
<label :for="setting.key">{{ setting.label }}:</label>
<input
v-if="setting.type === 'text'"
:id="setting.key"
v-model="moduleConfig[setting.key]"
type="text"
:placeholder="setting.placeholder"
required
>
<input
v-else-if="setting.type === 'number'"
:id="setting.key"
v-model="moduleConfig[setting.key]"
type="number"
:min="setting.min"
:max="setting.max"
:placeholder="setting.placeholder"
required
>
<select
v-else-if="setting.type === 'select'"
:id="setting.key"
v-model="moduleConfig[setting.key]"
required
>
<option value="">Выберите значение</option>
<option
v-for="option in setting.options"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
<span class="setting-hint">{{ setting.hint }}</span>
</div>
<div class="form-actions">
<button type="button" @click="showConfigModal = false" class="btn-secondary">
Отмена
</button>
<button type="submit" class="btn-primary" :disabled="isSavingConfig">
{{ isSavingConfig ? 'Сохранение...' : 'Сохранить' }}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</BaseLayout>
</template>
<script setup>
import { ref, defineProps, defineEmits } from 'vue';
import { useRouter } from 'vue-router';
import BaseLayout from '../../components/BaseLayout.vue';
// Определяем props
const props = defineProps({
isAuthenticated: Boolean,
identities: Array,
tokenBalances: Object,
isLoadingTokens: Boolean
});
// Определяем emits
const emit = defineEmits(['auth-action-completed']);
const router = useRouter();
// Состояние
const isInstalling = ref(false);
const isSavingConfig = ref(false);
const showConfigModal = ref(false);
const selectedModule = ref(null);
const moduleConfig = ref({});
// Доступные модули (временные данные)
const availableModules = ref([
{
id: 'treasury',
name: 'Казначейство',
version: '1.0.0',
description: 'Управление средствами и активами DLE',
features: ['Мультивалютность', 'Автоматизация', 'Отчетность'],
installed: true,
configSettings: [
{
key: 'maxWithdrawal',
label: 'Максимальная сумма вывода',
type: 'number',
min: 0,
max: 1000000,
placeholder: '10000',
hint: 'Максимальная сумма для однократного вывода средств'
},
{
key: 'approvalRequired',
label: 'Требуется одобрение',
type: 'select',
options: [
{ value: 'true', label: 'Да' },
{ value: 'false', label: 'Нет' }
],
hint: 'Требуется ли одобрение для операций с казной'
}
]
},
{
id: 'governance',
name: 'Расширенное управление',
version: '2.1.0',
description: 'Дополнительные функции голосования и управления',
features: ['Делегирование', 'Взвешенное голосование', 'Автоматизация'],
installed: false,
configSettings: [
{
key: 'delegationEnabled',
label: 'Включить делегирование',
type: 'select',
options: [
{ value: 'true', label: 'Да' },
{ value: 'false', label: 'Нет' }
],
hint: 'Разрешить делегирование голосов'
}
]
},
{
id: 'compliance',
name: 'Соответствие требованиям',
version: '1.2.0',
description: 'Модуль для обеспечения соответствия нормативным требованиям',
features: ['KYC/AML', 'Отчетность', 'Аудит'],
installed: false,
configSettings: [
{
key: 'kycRequired',
label: 'Требуется KYC',
type: 'select',
options: [
{ value: 'true', label: 'Да' },
{ value: 'false', label: 'Нет' }
],
hint: 'Требуется ли прохождение KYC для участия'
}
]
}
]);
// Установленные модули (временные данные)
const installedModules = ref([
{
name: 'Казначейство',
description: 'Управление средствами и активами DLE',
address: '0x1234567890123456789012345678901234567890',
version: '1.0.0',
status: 'active',
configSettings: availableModules.value[0].configSettings
}
]);
// Методы
const installModule = async (moduleId) => {
if (isInstalling.value) return;
try {
isInstalling.value = true;
// Здесь будет логика установки модуля
console.log('Установка модуля:', moduleId);
// Временная логика
const module = availableModules.value.find(m => m.id === moduleId);
if (module) {
module.installed = true;
// Добавляем в список установленных
installedModules.value.push({
name: module.name,
description: module.description,
address: '0x' + Math.random().toString(16).substr(2, 40),
version: module.version,
status: 'active',
configSettings: module.configSettings
});
}
alert('Модуль успешно установлен!');
} catch (error) {
console.error('Ошибка установки модуля:', error);
alert('Ошибка при установке модуля');
} finally {
isInstalling.value = false;
}
};
const uninstallModule = async (moduleAddress) => {
if (!confirm('Вы уверены, что хотите удалить этот модуль?')) return;
try {
// Здесь будет логика удаления модуля
console.log('Удаление модуля:', moduleAddress);
// Временная логика
installedModules.value = installedModules.value.filter(m => m.address !== moduleAddress);
// Обновляем статус в доступных модулях
const module = availableModules.value.find(m => m.name === 'Казначейство');
if (module) {
module.installed = false;
}
alert('Модуль успешно удален!');
} catch (error) {
console.error('Ошибка удаления модуля:', error);
alert('Ошибка при удалении модуля');
}
};
const configureModule = (module) => {
selectedModule.value = module;
moduleConfig.value = {};
showConfigModal.value = true;
};
const saveModuleConfig = async () => {
if (isSavingConfig.value) return;
try {
isSavingConfig.value = true;
// Здесь будет логика сохранения конфигурации
console.log('Сохранение конфигурации:', moduleConfig.value);
alert('Конфигурация успешно сохранена!');
showConfigModal.value = false;
} catch (error) {
console.error('Ошибка сохранения конфигурации:', error);
alert('Ошибка при сохранении конфигурации');
} finally {
isSavingConfig.value = false;
}
};
const openModuleInterface = (module) => {
// Здесь будет логика открытия интерфейса модуля
console.log('Открытие интерфейса модуля:', module);
alert(`Открытие интерфейса модуля ${module.name}`);
};
const getStatusText = (status) => {
const statusMap = {
'active': 'Активен',
'inactive': 'Неактивен',
'error': 'Ошибка',
'updating': 'Обновляется'
};
return statusMap[status] || status;
};
const formatAddress = (address) => {
if (!address) return '';
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
};
</script>
<style scoped>
.modules-container {
padding: 20px;
background-color: var(--color-white);
border-radius: var(--radius-lg);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-top: 20px;
margin-bottom: 20px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 2px solid #f0f0f0;
}
.header-content {
flex-grow: 1;
}
.page-header h1 {
color: var(--color-primary);
font-size: 2.5rem;
margin: 0 0 10px 0;
}
.page-header p {
color: var(--color-grey-dark);
font-size: 1.1rem;
margin: 0;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #666;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s;
flex-shrink: 0;
}
.close-btn:hover {
background: #f0f0f0;
color: #333;
}
/* Секции */
.available-modules-section,
.installed-modules-section {
margin-bottom: 40px;
}
.available-modules-section h2,
.installed-modules-section h2 {
color: var(--color-primary);
margin-bottom: 20px;
font-size: 1.8rem;
}
/* Сетка модулей */
.modules-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 20px;
}
.module-card {
background: white;
padding: 25px;
border-radius: var(--radius-lg);
border: 1px solid #e9ecef;
transition: all 0.3s ease;
}
.module-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.module-card.module-installed {
border-left: 4px solid #28a745;
background: #f8fff9;
}
.module-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.module-header h3 {
margin: 0;
color: var(--color-primary);
font-size: 1.3rem;
}
.module-version {
background: var(--color-primary);
color: white;
padding: 4px 8px;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 600;
}
.module-description {
color: var(--color-grey-dark);
margin: 0 0 15px 0;
line-height: 1.5;
}
.module-features {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 20px;
}
.feature-tag {
background: #e9ecef;
color: var(--color-grey-dark);
padding: 4px 8px;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 600;
}
.module-actions {
display: flex;
gap: 10px;
}
/* Установленные модули */
.installed-modules-list {
display: grid;
gap: 20px;
}
.installed-module-card {
background: white;
padding: 25px;
border-radius: var(--radius-lg);
border: 1px solid #e9ecef;
transition: all 0.3s ease;
}
.installed-module-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.module-info {
margin-bottom: 20px;
}
.module-status {
padding: 4px 12px;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 600;
}
.module-status.active {
background: #d4edda;
color: #155724;
}
.module-status.inactive {
background: #f8d7da;
color: #721c24;
}
.module-status.error {
background: #f8d7da;
color: #721c24;
}
.module-status.updating {
background: #fff3cd;
color: #856404;
}
.module-address {
font-family: monospace;
font-size: 0.9rem;
color: var(--color-grey-dark);
margin: 8px 0;
}
.module-version {
font-size: 0.9rem;
color: var(--color-grey-dark);
margin: 0;
}
/* Модальное окно */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: var(--radius-lg);
width: 90%;
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #e9ecef;
}
.modal-header h3 {
margin: 0;
color: var(--color-primary);
}
.modal-body {
padding: 20px;
}
/* Форма конфигурации */
.config-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-group label {
font-weight: 600;
color: var(--color-grey-dark);
}
.form-group input,
.form-group select {
padding: 12px;
border: 1px solid #ddd;
border-radius: var(--radius-sm);
font-size: 1rem;
}
.setting-hint {
font-size: 0.8rem;
color: var(--color-grey-dark);
margin-top: 4px;
}
.form-actions {
display: flex;
gap: 15px;
justify-content: flex-end;
margin-top: 20px;
}
/* Кнопки */
.btn-primary {
background: var(--color-primary);
color: white;
border: none;
padding: 10px 20px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-primary:hover:not(:disabled) {
background: var(--color-primary-dark);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-secondary {
background: var(--color-secondary);
color: white;
border: none;
padding: 10px 20px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-secondary:hover {
background: var(--color-secondary-dark);
}
.btn-danger {
background: #dc3545;
color: white;
border: none;
padding: 10px 20px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-danger:hover {
background: #c82333;
}
/* Состояния */
.empty-state {
text-align: center;
padding: 60px;
color: var(--color-grey-dark);
background: #f8f9fa;
border-radius: var(--radius-lg);
border: 2px dashed #dee2e6;
}
.empty-state p {
margin: 0;
font-size: 1.1rem;
}
/* Адаптивность */
@media (max-width: 768px) {
.modules-grid {
grid-template-columns: 1fr;
}
.module-actions {
flex-direction: column;
}
.form-actions {
flex-direction: column;
}
}
</style>

View File

@@ -0,0 +1,611 @@
<!--
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="proposals-container">
<!-- Заголовок -->
<div class="page-header">
<div class="header-content">
<h1>Предложения</h1>
<p>Создание, подписание и выполнение предложений</p>
</div>
<button class="close-btn" @click="router.push('/management')">×</button>
</div>
<!-- Создание нового предложения -->
<div class="create-proposal-section">
<h2>Создать новое предложение</h2>
<form @submit.prevent="createProposal" class="proposal-form">
<div class="form-row">
<div class="form-group">
<label for="operationType">Тип операции:</label>
<select id="operationType" v-model="newProposal.operationType" required>
<option value="">Выберите тип операции</option>
<option value="token_transfer">Перевод токенов</option>
<option value="treasury_operation">Казначейская операция</option>
<option value="module_install">Установка модуля</option>
<option value="parameter_change">Изменение параметров</option>
<option value="emergency_action">Экстренные действия</option>
</select>
</div>
<div class="form-group">
<label for="timelockDelay">Задержка таймлока (часы):</label>
<input
id="timelockDelay"
type="number"
v-model="newProposal.timelockDelay"
min="1"
max="168"
required
>
</div>
</div>
<div class="form-group">
<label for="description">Описание операции:</label>
<textarea
id="description"
v-model="newProposal.description"
placeholder="Опишите детали операции..."
required
></textarea>
</div>
<div class="form-group">
<label>Целевые сети:</label>
<div class="networks-grid">
<label v-for="network in availableNetworks" :key="network.id" class="network-checkbox">
<input
type="checkbox"
:value="network.id"
v-model="newProposal.targetChains"
>
{{ network.name }}
</label>
</div>
</div>
<button type="submit" class="btn-primary" :disabled="isCreatingProposal">
{{ isCreatingProposal ? 'Создание...' : 'Создать предложение' }}
</button>
</form>
</div>
<!-- Активные предложения -->
<div class="proposals-section">
<h2>Активные предложения</h2>
<div v-if="proposals.length === 0" class="empty-state">
<p>Нет активных предложений</p>
</div>
<div v-else class="proposals-list">
<div
v-for="proposal in proposals"
:key="proposal.id"
class="proposal-card"
:class="{ 'proposal-executed': proposal.executed }"
>
<div class="proposal-header">
<h3>Предложение #{{ proposal.id }}</h3>
<span class="proposal-status" :class="getStatusClass(proposal)">
{{ getStatusText(proposal) }}
</span>
</div>
<div class="proposal-details">
<p><strong>Описание:</strong> {{ proposal.description }}</p>
<p><strong>Инициатор:</strong> {{ formatAddress(proposal.initiator) }}</p>
<p><strong>Таймлок:</strong> {{ formatTimestamp(proposal.timelock) }}</p>
<p><strong>Подписи:</strong> {{ proposal.signaturesCount }} / {{ proposal.quorumRequired }}</p>
</div>
<div class="proposal-actions">
<button
v-if="!proposal.hasSigned && !proposal.executed"
@click="signProposal(proposal.id)"
class="btn-secondary"
:disabled="isSigning"
>
Подписать
</button>
<button
v-if="canExecuteProposal(proposal)"
@click="executeProposal(proposal.id)"
class="btn-success"
:disabled="isExecuting"
>
Выполнить
</button>
</div>
</div>
</div>
</div>
</div>
</BaseLayout>
</template>
<script setup>
import { ref, defineProps, defineEmits } from 'vue';
import { useRouter } from 'vue-router';
import BaseLayout from '../../components/BaseLayout.vue';
// Определяем props
const props = defineProps({
isAuthenticated: Boolean,
identities: Array,
tokenBalances: Object,
isLoadingTokens: Boolean
});
// Определяем emits
const emit = defineEmits(['auth-action-completed']);
const router = useRouter();
// Состояние
const isCreatingProposal = ref(false);
const isSigning = ref(false);
const isExecuting = ref(false);
const newProposal = ref({
operationType: '',
description: '',
targetChains: [],
timelockDelay: 24
});
// Доступные сети
const availableNetworks = ref([
{ id: 1, name: 'Ethereum Mainnet' },
{ id: 137, name: 'Polygon' },
{ id: 56, name: 'BSC' },
{ id: 42161, name: 'Arbitrum' }
]);
// Предложения (временные данные)
const proposals = ref([
{
id: 1,
description: 'Перевод 100 токенов партнеру',
initiator: '0x1234567890123456789012345678901234567890',
timelock: Math.floor(Date.now() / 1000) + 3600,
signaturesCount: 5000,
quorumRequired: 5100,
executed: false,
hasSigned: false
}
]);
// Методы
const createProposal = async () => {
if (isCreatingProposal.value) return;
try {
isCreatingProposal.value = true;
// Здесь будет создание предложения в смарт-контракте
console.log('Создание предложения:', newProposal.value);
// Временная логика
const proposal = {
id: proposals.value.length + 1,
description: newProposal.value.description,
initiator: '0x' + Math.random().toString(16).substr(2, 40),
timelock: Math.floor(Date.now() / 1000) + (newProposal.value.timelockDelay * 3600),
signaturesCount: 0,
quorumRequired: 5100,
executed: false,
hasSigned: false
};
proposals.value.push(proposal);
// Сброс формы
newProposal.value = {
operationType: '',
description: '',
targetChains: [],
timelockDelay: 24
};
} catch (error) {
console.error('Ошибка создания предложения:', error);
} finally {
isCreatingProposal.value = false;
}
};
const signProposal = async (proposalId) => {
if (isSigning.value) return;
try {
isSigning.value = true;
// Здесь будет подписание предложения в смарт-контракте
console.log('Подписание предложения:', proposalId);
const proposal = proposals.value.find(p => p.id === proposalId);
if (proposal) {
proposal.signaturesCount += 1000;
proposal.hasSigned = true;
}
} catch (error) {
console.error('Ошибка подписания предложения:', error);
} finally {
isSigning.value = false;
}
};
const executeProposal = async (proposalId) => {
if (isExecuting.value) return;
try {
isExecuting.value = true;
// Здесь будет выполнение предложения в смарт-контракте
console.log('Выполнение предложения:', proposalId);
const proposal = proposals.value.find(p => p.id === proposalId);
if (proposal) {
proposal.executed = true;
}
} catch (error) {
console.error('Ошибка выполнения предложения:', error);
} finally {
isExecuting.value = false;
}
};
const canExecuteProposal = (proposal) => {
return !proposal.executed &&
proposal.signaturesCount >= proposal.quorumRequired &&
Date.now() >= proposal.timelock * 1000;
};
const formatAddress = (address) => {
if (!address) return '';
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
};
const formatTimestamp = (timestamp) => {
return new Date(timestamp * 1000).toLocaleString('ru-RU');
};
const getStatusClass = (proposal) => {
if (proposal.executed) return 'status-executed';
if (proposal.signaturesCount >= proposal.quorumRequired) return 'status-ready';
return 'status-pending';
};
const getStatusText = (proposal) => {
if (proposal.executed) return 'Выполнено';
if (proposal.signaturesCount >= proposal.quorumRequired) return 'Готово к выполнению';
return 'Ожидает подписей';
};
</script>
<style scoped>
.proposals-container {
padding: 20px;
background-color: var(--color-white);
border-radius: var(--radius-lg);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-top: 20px;
margin-bottom: 20px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 2px solid #f0f0f0;
}
.header-content {
flex-grow: 1;
}
.page-header h1 {
color: var(--color-primary);
font-size: 2.5rem;
margin: 0 0 10px 0;
}
.page-header p {
color: var(--color-grey-dark);
font-size: 1.1rem;
margin: 0;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #666;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s;
flex-shrink: 0;
}
.close-btn:hover {
background: #f0f0f0;
color: #333;
}
/* Секции */
.create-proposal-section,
.proposals-section {
margin-bottom: 40px;
}
.create-proposal-section h2,
.proposals-section h2 {
color: var(--color-primary);
margin-bottom: 20px;
font-size: 1.8rem;
}
/* Форма */
.proposal-form {
background: #f8f9fa;
padding: 25px;
border-radius: var(--radius-lg);
border: 1px solid #e9ecef;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 20px;
}
.form-group label {
font-weight: 600;
color: var(--color-grey-dark);
}
.form-group input,
.form-group select,
.form-group textarea {
padding: 12px;
border: 1px solid #ddd;
border-radius: var(--radius-sm);
font-size: 1rem;
}
.form-group textarea {
min-height: 100px;
resize: vertical;
}
.networks-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.network-checkbox {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 8px;
border-radius: var(--radius-sm);
transition: background 0.2s;
}
.network-checkbox:hover {
background: #e9ecef;
}
.network-checkbox input[type="checkbox"] {
width: auto;
}
/* Предложения */
.proposals-list {
display: grid;
gap: 20px;
}
.proposal-card {
background: white;
padding: 25px;
border-radius: var(--radius-lg);
border: 1px solid #e9ecef;
transition: all 0.3s ease;
}
.proposal-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.proposal-card.proposal-executed {
opacity: 0.7;
background: #f8f9fa;
}
.proposal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.proposal-header h3 {
margin: 0;
color: var(--color-primary);
font-size: 1.3rem;
}
.proposal-status {
padding: 6px 16px;
border-radius: 20px;
font-size: 0.9rem;
font-weight: 600;
}
.status-pending {
background: #fff3cd;
color: #856404;
}
.status-ready {
background: #d1ecf1;
color: #0c5460;
}
.status-executed {
background: #d4edda;
color: #155724;
}
.proposal-details {
margin-bottom: 20px;
}
.proposal-details p {
margin: 8px 0;
font-size: 0.95rem;
}
.proposal-actions {
display: flex;
gap: 12px;
}
/* Кнопки */
.btn-primary {
background: var(--color-primary);
color: white;
border: none;
padding: 12px 24px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-primary:hover:not(:disabled) {
background: var(--color-primary-dark);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-secondary {
background: var(--color-secondary);
color: white;
border: none;
padding: 10px 20px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-secondary:hover:not(:disabled) {
background: var(--color-secondary-dark);
}
.btn-secondary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-success {
background: #28a745;
color: white;
border: none;
padding: 10px 20px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-success:hover:not(:disabled) {
background: #218838;
}
.btn-success:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Состояния */
.empty-state {
text-align: center;
padding: 60px;
color: var(--color-grey-dark);
background: #f8f9fa;
border-radius: var(--radius-lg);
border: 2px dashed #dee2e6;
}
.empty-state p {
margin: 0;
font-size: 1.1rem;
}
/* Адаптивность */
@media (max-width: 768px) {
.form-row {
grid-template-columns: 1fr;
}
.proposal-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.proposal-actions {
flex-direction: column;
}
.networks-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,600 @@
<!--
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="quorum-container">
<!-- Заголовок -->
<div class="page-header">
<div class="header-content">
<h1>Кворум</h1>
<p>Настройки голосования и кворума</p>
</div>
<button class="close-btn" @click="router.push('/management')">×</button>
</div>
<!-- Текущие настройки -->
<div class="current-settings-section">
<h2>Текущие настройки</h2>
<div class="settings-grid">
<div class="setting-card">
<h3>Процент кворума</h3>
<p class="setting-value">{{ currentQuorum }}%</p>
<p class="setting-description">Минимальный процент токенов для принятия решения</p>
</div>
<div class="setting-card">
<h3>Задержка голосования</h3>
<p class="setting-value">{{ votingDelay }} блоков</p>
<p class="setting-description">Время между созданием и началом голосования</p>
</div>
<div class="setting-card">
<h3>Период голосования</h3>
<p class="setting-value">{{ votingPeriod }} блоков</p>
<p class="setting-description">Длительность периода голосования</p>
</div>
<div class="setting-card">
<h3>Порог предложений</h3>
<p class="setting-value">{{ proposalThreshold }} токенов</p>
<p class="setting-description">Минимальное количество токенов для создания предложения</p>
</div>
</div>
</div>
<!-- Изменение настроек -->
<div class="change-settings-section">
<h2>Изменить настройки</h2>
<form @submit.prevent="updateSettings" class="settings-form">
<div class="form-row">
<div class="form-group">
<label for="newQuorum">Новый процент кворума:</label>
<input
id="newQuorum"
v-model="newSettings.quorumPercentage"
type="number"
min="1"
max="100"
placeholder="51"
required
>
<span class="input-hint">% (от 1 до 100)</span>
</div>
<div class="form-group">
<label for="newVotingDelay">Новая задержка голосования:</label>
<input
id="newVotingDelay"
v-model="newSettings.votingDelay"
type="number"
min="0"
placeholder="1"
required
>
<span class="input-hint">блоков</span>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="newVotingPeriod">Новый период голосования:</label>
<input
id="newVotingPeriod"
v-model="newSettings.votingPeriod"
type="number"
min="1"
placeholder="45818"
required
>
<span class="input-hint">блоков (~1 неделя)</span>
</div>
<div class="form-group">
<label for="newProposalThreshold">Новый порог предложений:</label>
<input
id="newProposalThreshold"
v-model="newSettings.proposalThreshold"
type="number"
min="0"
placeholder="100"
required
>
<span class="input-hint">токенов</span>
</div>
</div>
<div class="form-group">
<label for="changeReason">Причина изменения:</label>
<textarea
id="changeReason"
v-model="newSettings.reason"
placeholder="Опишите причину изменения настроек..."
rows="4"
required
></textarea>
</div>
<button type="submit" class="btn-primary" :disabled="isUpdating">
{{ isUpdating ? 'Обновление...' : 'Обновить настройки' }}
</button>
</form>
</div>
<!-- История изменений -->
<div class="history-section">
<h2>История изменений</h2>
<div v-if="settingsHistory.length === 0" class="empty-state">
<p>Нет истории изменений настроек</p>
</div>
<div v-else class="history-list">
<div
v-for="change in settingsHistory"
:key="change.id"
class="history-item"
>
<div class="history-header">
<h3>Изменение #{{ change.id }}</h3>
<span class="change-date">{{ formatDate(change.timestamp) }}</span>
</div>
<div class="change-details">
<p><strong>Причина:</strong> {{ change.reason }}</p>
<div class="changes-list">
<div v-if="change.quorumChange" class="change-item">
<span class="change-label">Кворум:</span>
<span class="change-value">{{ change.quorumChange.from }}% {{ change.quorumChange.to }}%</span>
</div>
<div v-if="change.votingDelayChange" class="change-item">
<span class="change-label">Задержка голосования:</span>
<span class="change-value">{{ change.votingDelayChange.from }} {{ change.votingDelayChange.to }} блоков</span>
</div>
<div v-if="change.votingPeriodChange" class="change-item">
<span class="change-label">Период голосования:</span>
<span class="change-value">{{ change.votingPeriodChange.from }} {{ change.votingPeriodChange.to }} блоков</span>
</div>
<div v-if="change.proposalThresholdChange" class="change-item">
<span class="change-label">Порог предложений:</span>
<span class="change-value">{{ change.proposalThresholdChange.from }} {{ change.proposalThresholdChange.to }} токенов</span>
</div>
</div>
</div>
<div class="change-author">
<span>Автор: {{ formatAddress(change.author) }}</span>
</div>
</div>
</div>
</div>
</div>
</BaseLayout>
</template>
<script setup>
import { ref, defineProps, defineEmits } from 'vue';
import { useRouter } from 'vue-router';
import BaseLayout from '../../components/BaseLayout.vue';
// Определяем props
const props = defineProps({
isAuthenticated: Boolean,
identities: Array,
tokenBalances: Object,
isLoadingTokens: Boolean
});
// Определяем emits
const emit = defineEmits(['auth-action-completed']);
const router = useRouter();
// Состояние
const isUpdating = ref(false);
// Текущие настройки
const currentQuorum = ref(51);
const votingDelay = ref(1);
const votingPeriod = ref(45818);
const proposalThreshold = ref(100);
// Новые настройки
const newSettings = ref({
quorumPercentage: '',
votingDelay: '',
votingPeriod: '',
proposalThreshold: '',
reason: ''
});
// История изменений (временные данные)
const settingsHistory = ref([
{
id: 1,
timestamp: Date.now() - 86400000, // 1 день назад
reason: 'Оптимизация параметров голосования для повышения эффективности',
quorumChange: { from: 60, to: 51 },
votingDelayChange: { from: 2, to: 1 },
author: '0x1234567890123456789012345678901234567890'
},
{
id: 2,
timestamp: Date.now() - 604800000, // 1 неделя назад
reason: 'Первоначальная настройка параметров DLE',
quorumChange: { from: 0, to: 60 },
votingDelayChange: { from: 0, to: 2 },
votingPeriodChange: { from: 0, to: 45818 },
proposalThresholdChange: { from: 0, to: 100 },
author: '0x2345678901234567890123456789012345678901'
}
]);
// Методы
const updateSettings = async () => {
if (isUpdating.value) return;
try {
isUpdating.value = true;
// Здесь будет логика обновления настроек в смарт-контракте
console.log('Обновление настроек:', newSettings.value);
// Временная логика
const change = {
id: settingsHistory.value.length + 1,
timestamp: Date.now(),
reason: newSettings.value.reason,
author: '0x' + Math.random().toString(16).substr(2, 40)
};
// Добавляем изменения в историю
if (newSettings.value.quorumPercentage && newSettings.value.quorumPercentage !== currentQuorum.value) {
change.quorumChange = { from: currentQuorum.value, to: parseInt(newSettings.value.quorumPercentage) };
currentQuorum.value = parseInt(newSettings.value.quorumPercentage);
}
if (newSettings.value.votingDelay && newSettings.value.votingDelay !== votingDelay.value) {
change.votingDelayChange = { from: votingDelay.value, to: parseInt(newSettings.value.votingDelay) };
votingDelay.value = parseInt(newSettings.value.votingDelay);
}
if (newSettings.value.votingPeriod && newSettings.value.votingPeriod !== votingPeriod.value) {
change.votingPeriodChange = { from: votingPeriod.value, to: parseInt(newSettings.value.votingPeriod) };
votingPeriod.value = parseInt(newSettings.value.votingPeriod);
}
if (newSettings.value.proposalThreshold && newSettings.value.proposalThreshold !== proposalThreshold.value) {
change.proposalThresholdChange = { from: proposalThreshold.value, to: parseInt(newSettings.value.proposalThreshold) };
proposalThreshold.value = parseInt(newSettings.value.proposalThreshold);
}
settingsHistory.value.unshift(change);
// Сброс формы
newSettings.value = {
quorumPercentage: '',
votingDelay: '',
votingPeriod: '',
proposalThreshold: '',
reason: ''
};
alert('Настройки успешно обновлены!');
} catch (error) {
console.error('Ошибка обновления настроек:', error);
alert('Ошибка при обновлении настроек');
} finally {
isUpdating.value = false;
}
};
const formatAddress = (address) => {
if (!address) return '';
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
};
const formatDate = (timestamp) => {
return new Date(timestamp).toLocaleString('ru-RU');
};
</script>
<style scoped>
.quorum-container {
padding: 20px;
background-color: var(--color-white);
border-radius: var(--radius-lg);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-top: 20px;
margin-bottom: 20px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 2px solid #f0f0f0;
}
.header-content {
flex-grow: 1;
}
.page-header h1 {
color: var(--color-primary);
font-size: 2.5rem;
margin: 0 0 10px 0;
}
.page-header p {
color: var(--color-grey-dark);
font-size: 1.1rem;
margin: 0;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #666;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s;
flex-shrink: 0;
}
.close-btn:hover {
background: #f0f0f0;
color: #333;
}
/* Секции */
.current-settings-section,
.change-settings-section,
.history-section {
margin-bottom: 40px;
}
.current-settings-section h2,
.change-settings-section h2,
.history-section h2 {
color: var(--color-primary);
margin-bottom: 20px;
font-size: 1.8rem;
}
/* Текущие настройки */
.settings-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
}
.setting-card {
background: #f8f9fa;
padding: 25px;
border-radius: var(--radius-lg);
border-left: 4px solid var(--color-primary);
}
.setting-card h3 {
color: var(--color-primary);
margin-bottom: 15px;
font-size: 1.1rem;
font-weight: 600;
}
.setting-value {
font-size: 1.8rem;
font-weight: 700;
margin: 0 0 10px 0;
color: var(--color-primary);
}
.setting-description {
font-size: 0.9rem;
color: var(--color-grey-dark);
margin: 0;
line-height: 1.4;
}
/* Форма настроек */
.settings-form {
background: #f8f9fa;
padding: 25px;
border-radius: var(--radius-lg);
border: 1px solid #e9ecef;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 20px;
}
.form-group label {
font-weight: 600;
color: var(--color-grey-dark);
}
.form-group input,
.form-group textarea {
padding: 12px;
border: 1px solid #ddd;
border-radius: var(--radius-sm);
font-size: 1rem;
}
.form-group textarea {
resize: vertical;
min-height: 100px;
}
.input-hint {
font-size: 0.8rem;
color: var(--color-grey-dark);
margin-top: 4px;
}
/* История изменений */
.history-list {
display: grid;
gap: 20px;
}
.history-item {
background: white;
padding: 25px;
border-radius: var(--radius-lg);
border: 1px solid #e9ecef;
transition: all 0.3s ease;
}
.history-item:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.history-header h3 {
margin: 0;
color: var(--color-primary);
font-size: 1.2rem;
}
.change-date {
font-size: 0.9rem;
color: var(--color-grey-dark);
}
.change-details {
margin-bottom: 15px;
}
.change-details p {
margin: 0 0 15px 0;
font-size: 0.95rem;
}
.changes-list {
display: grid;
gap: 8px;
}
.change-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: #f8f9fa;
border-radius: var(--radius-sm);
}
.change-label {
font-weight: 600;
color: var(--color-grey-dark);
font-size: 0.9rem;
}
.change-value {
font-weight: 600;
color: var(--color-primary);
font-size: 0.9rem;
}
.change-author {
font-size: 0.8rem;
color: var(--color-grey-dark);
font-family: monospace;
}
/* Кнопки */
.btn-primary {
background: var(--color-primary);
color: white;
border: none;
padding: 12px 24px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-primary:hover:not(:disabled) {
background: var(--color-primary-dark);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Состояния */
.empty-state {
text-align: center;
padding: 60px;
color: var(--color-grey-dark);
background: #f8f9fa;
border-radius: var(--radius-lg);
border: 2px dashed #dee2e6;
}
.empty-state p {
margin: 0;
font-size: 1.1rem;
}
/* Адаптивность */
@media (max-width: 768px) {
.form-row {
grid-template-columns: 1fr;
}
.history-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.change-item {
flex-direction: column;
align-items: flex-start;
gap: 5px;
}
.settings-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,835 @@
<!--
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="settings-container">
<!-- Заголовок -->
<div class="page-header">
<div class="header-content">
<h1>Настройки</h1>
<p>Параметры DLE и конфигурация</p>
</div>
<button class="close-btn" @click="router.push('/management')">×</button>
</div>
<!-- Основные настройки -->
<div class="main-settings-section">
<h2>Основные настройки</h2>
<form @submit.prevent="saveMainSettings" class="settings-form">
<div class="form-row">
<div class="form-group">
<label for="dleName">Название DLE:</label>
<input
id="dleName"
v-model="mainSettings.name"
type="text"
placeholder="Введите название DLE"
required
>
</div>
<div class="form-group">
<label for="dleSymbol">Символ токена:</label>
<input
id="dleSymbol"
v-model="mainSettings.symbol"
type="text"
placeholder="MDLE"
maxlength="10"
required
>
</div>
</div>
<div class="form-group">
<label for="dleDescription">Описание:</label>
<textarea
id="dleDescription"
v-model="mainSettings.description"
placeholder="Опишите назначение и деятельность DLE..."
rows="4"
></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="dleLocation">Местонахождение:</label>
<input
id="dleLocation"
v-model="mainSettings.location"
type="text"
placeholder="Страна, город"
required
>
</div>
<div class="form-group">
<label for="dleWebsite">Веб-сайт:</label>
<input
id="dleWebsite"
v-model="mainSettings.website"
type="url"
placeholder="https://example.com"
>
</div>
</div>
<button type="submit" class="btn-primary" :disabled="isSaving">
{{ isSaving ? 'Сохранение...' : 'Сохранить настройки' }}
</button>
</form>
</div>
<!-- Настройки безопасности -->
<div class="security-settings-section">
<h2>Настройки безопасности</h2>
<form @submit.prevent="saveSecuritySettings" class="settings-form">
<div class="form-row">
<div class="form-group">
<label for="minQuorum">Минимальный кворум (%):</label>
<input
id="minQuorum"
v-model="securitySettings.minQuorum"
type="number"
min="1"
max="100"
placeholder="51"
required
>
<span class="input-hint">Минимальный процент токенов для принятия решений</span>
</div>
<div class="form-group">
<label for="maxProposalDuration">Максимальная длительность предложения (дни):</label>
<input
id="maxProposalDuration"
v-model="securitySettings.maxProposalDuration"
type="number"
min="1"
max="365"
placeholder="7"
required
>
<span class="input-hint">Максимальное время жизни предложения</span>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="emergencyThreshold">Порог экстренных действий (%):</label>
<input
id="emergencyThreshold"
v-model="securitySettings.emergencyThreshold"
type="number"
min="1"
max="100"
placeholder="75"
required
>
<span class="input-hint">Процент для экстренных действий</span>
</div>
<div class="form-group">
<label for="timelockDelay">Задержка таймлока (часы):</label>
<input
id="timelockDelay"
v-model="securitySettings.timelockDelay"
type="number"
min="1"
max="168"
placeholder="24"
required
>
<span class="input-hint">Минимальная задержка перед выполнением</span>
</div>
</div>
<div class="form-group">
<label>Дополнительные настройки:</label>
<div class="checkbox-group">
<label class="checkbox-item">
<input
type="checkbox"
v-model="securitySettings.allowDelegation"
>
Разрешить делегирование голосов
</label>
<label class="checkbox-item">
<input
type="checkbox"
v-model="securitySettings.requireKYC"
>
Требовать KYC для участия
</label>
<label class="checkbox-item">
<input
type="checkbox"
v-model="securitySettings.autoExecute"
>
Автоматическое выполнение предложений
</label>
</div>
</div>
<button type="submit" class="btn-primary" :disabled="isSaving">
{{ isSaving ? 'Сохранение...' : 'Сохранить настройки безопасности' }}
</button>
</form>
</div>
<!-- Настройки сети -->
<div class="network-settings-section">
<h2>Настройки сети</h2>
<form @submit.prevent="saveNetworkSettings" class="settings-form">
<div class="form-group">
<label>Поддерживаемые сети:</label>
<div class="networks-grid">
<label
v-for="network in availableNetworks"
:key="network.id"
class="network-checkbox"
>
<input
type="checkbox"
:value="network.id"
v-model="networkSettings.enabledNetworks"
>
<div class="network-info">
<span class="network-name">{{ network.name }}</span>
<span class="network-chain-id">Chain ID: {{ network.chainId }}</span>
</div>
</label>
</div>
</div>
<div class="form-group">
<label for="defaultNetwork">Сеть по умолчанию:</label>
<select id="defaultNetwork" v-model="networkSettings.defaultNetwork" required>
<option value="">Выберите сеть</option>
<option
v-for="network in availableNetworks"
:key="network.id"
:value="network.id"
>
{{ network.name }}
</option>
</select>
</div>
<div class="form-group">
<label for="rpcEndpoint">RPC Endpoint:</label>
<input
id="rpcEndpoint"
v-model="networkSettings.rpcEndpoint"
type="url"
placeholder="https://mainnet.infura.io/v3/YOUR_PROJECT_ID"
>
<span class="input-hint">RPC endpoint для подключения к блокчейну</span>
</div>
<button type="submit" class="btn-primary" :disabled="isSaving">
{{ isSaving ? 'Сохранение...' : 'Сохранить настройки сети' }}
</button>
</form>
</div>
<!-- Резервное копирование -->
<div class="backup-section">
<h2>Резервное копирование</h2>
<div class="backup-actions">
<div class="backup-card">
<h3>Экспорт настроек</h3>
<p>Скачайте файл с настройками DLE для резервного копирования</p>
<button @click="exportSettings" class="btn-secondary">
Экспортировать
</button>
</div>
<div class="backup-card">
<h3>Импорт настроек</h3>
<p>Загрузите файл с настройками для восстановления</p>
<input
ref="importFile"
type="file"
accept=".json"
@change="importSettings"
style="display: none"
>
<button @click="$refs.importFile.click()" class="btn-secondary">
Импортировать
</button>
</div>
</div>
</div>
<!-- Опасная зона -->
<div class="danger-zone-section">
<h2>Опасная зона</h2>
<div class="danger-actions">
<div class="danger-card">
<h3>Сброс настроек</h3>
<p>Вернуть все настройки к значениям по умолчанию</p>
<button @click="resetSettings" class="btn-danger">
Сбросить настройки
</button>
</div>
<div class="danger-card">
<h3>Удаление DLE</h3>
<p>Полное удаление DLE и всех связанных данных</p>
<button @click="deleteDLE" class="btn-danger">
Удалить DLE
</button>
</div>
</div>
</div>
</div>
</BaseLayout>
</template>
<script setup>
import { ref, defineProps, defineEmits } from 'vue';
import { useRouter } from 'vue-router';
import BaseLayout from '../../components/BaseLayout.vue';
// Определяем props
const props = defineProps({
isAuthenticated: Boolean,
identities: Array,
tokenBalances: Object,
isLoadingTokens: Boolean
});
// Определяем emits
const emit = defineEmits(['auth-action-completed']);
const router = useRouter();
// Состояние
const isSaving = ref(false);
// Основные настройки
const mainSettings = ref({
name: 'Мое DLE',
symbol: 'MDLE',
description: 'Цифровое юридическое лицо для управления активами и принятия решений',
location: 'Россия, Москва',
website: 'https://example.com'
});
// Настройки безопасности
const securitySettings = ref({
minQuorum: 51,
maxProposalDuration: 7,
emergencyThreshold: 75,
timelockDelay: 24,
allowDelegation: true,
requireKYC: false,
autoExecute: false
});
// Настройки сети
const networkSettings = ref({
enabledNetworks: [1, 137],
defaultNetwork: 1,
rpcEndpoint: 'https://mainnet.infura.io/v3/YOUR_PROJECT_ID'
});
// Доступные сети
const availableNetworks = ref([
{ id: 1, name: 'Ethereum Mainnet', chainId: 1 },
{ id: 137, name: 'Polygon', chainId: 137 },
{ id: 56, name: 'BSC', chainId: 56 },
{ id: 42161, name: 'Arbitrum', chainId: 42161 },
{ id: 10, name: 'Optimism', chainId: 10 }
]);
// Методы
const saveMainSettings = async () => {
if (isSaving.value) return;
try {
isSaving.value = true;
// Здесь будет логика сохранения основных настроек
console.log('Сохранение основных настроек:', mainSettings.value);
// Временная логика
await new Promise(resolve => setTimeout(resolve, 1000));
alert('Основные настройки успешно сохранены!');
} catch (error) {
console.error('Ошибка сохранения основных настроек:', error);
alert('Ошибка при сохранении настроек');
} finally {
isSaving.value = false;
}
};
const saveSecuritySettings = async () => {
if (isSaving.value) return;
try {
isSaving.value = true;
// Здесь будет логика сохранения настроек безопасности
console.log('Сохранение настроек безопасности:', securitySettings.value);
// Временная логика
await new Promise(resolve => setTimeout(resolve, 1000));
alert('Настройки безопасности успешно сохранены!');
} catch (error) {
console.error('Ошибка сохранения настроек безопасности:', error);
alert('Ошибка при сохранении настроек безопасности');
} finally {
isSaving.value = false;
}
};
const saveNetworkSettings = async () => {
if (isSaving.value) return;
try {
isSaving.value = true;
// Здесь будет логика сохранения настроек сети
console.log('Сохранение настроек сети:', networkSettings.value);
// Временная логика
await new Promise(resolve => setTimeout(resolve, 1000));
alert('Настройки сети успешно сохранены!');
} catch (error) {
console.error('Ошибка сохранения настроек сети:', error);
alert('Ошибка при сохранении настроек сети');
} finally {
isSaving.value = false;
}
};
const exportSettings = () => {
const settings = {
main: mainSettings.value,
security: securitySettings.value,
network: networkSettings.value,
exportDate: new Date().toISOString()
};
const blob = new Blob([JSON.stringify(settings, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `dle-settings-${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
alert('Настройки успешно экспортированы!');
};
const importSettings = (event) => {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const settings = JSON.parse(e.target.result);
if (settings.main) mainSettings.value = settings.main;
if (settings.security) securitySettings.value = settings.security;
if (settings.network) networkSettings.value = settings.network;
alert('Настройки успешно импортированы!');
} catch (error) {
console.error('Ошибка импорта настроек:', error);
alert('Ошибка при импорте настроек');
}
};
reader.readAsText(file);
};
const resetSettings = () => {
if (!confirm('Вы уверены, что хотите сбросить все настройки к значениям по умолчанию?')) {
return;
}
// Сброс к значениям по умолчанию
mainSettings.value = {
name: 'Мое DLE',
symbol: 'MDLE',
description: 'Цифровое юридическое лицо для управления активами и принятия решений',
location: 'Россия, Москва',
website: 'https://example.com'
};
securitySettings.value = {
minQuorum: 51,
maxProposalDuration: 7,
emergencyThreshold: 75,
timelockDelay: 24,
allowDelegation: true,
requireKYC: false,
autoExecute: false
};
networkSettings.value = {
enabledNetworks: [1, 137],
defaultNetwork: 1,
rpcEndpoint: 'https://mainnet.infura.io/v3/YOUR_PROJECT_ID'
};
alert('Настройки сброшены к значениям по умолчанию!');
};
const deleteDLE = () => {
if (!confirm('ВНИМАНИЕ! Это действие необратимо. Вы уверены, что хотите удалить DLE и все связанные данные?')) {
return;
}
if (!confirm('Это действие нельзя отменить. Подтвердите удаление еще раз.')) {
return;
}
// Здесь будет логика удаления DLE
console.log('Удаление DLE...');
alert('DLE будет удален. Это действие может занять некоторое время.');
};
</script>
<style scoped>
.settings-container {
padding: 20px;
background-color: var(--color-white);
border-radius: var(--radius-lg);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-top: 20px;
margin-bottom: 20px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 2px solid #f0f0f0;
}
.header-content {
flex-grow: 1;
}
.page-header h1 {
color: var(--color-primary);
font-size: 2.5rem;
margin: 0 0 10px 0;
}
.page-header p {
color: var(--color-grey-dark);
font-size: 1.1rem;
margin: 0;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #666;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s;
flex-shrink: 0;
}
.close-btn:hover {
background: #f0f0f0;
color: #333;
}
/* Секции */
.main-settings-section,
.security-settings-section,
.network-settings-section,
.backup-section,
.danger-zone-section {
margin-bottom: 40px;
}
.main-settings-section h2,
.security-settings-section h2,
.network-settings-section h2,
.backup-section h2,
.danger-zone-section h2 {
color: var(--color-primary);
margin-bottom: 20px;
font-size: 1.8rem;
}
/* Формы настроек */
.settings-form {
background: #f8f9fa;
padding: 25px;
border-radius: var(--radius-lg);
border: 1px solid #e9ecef;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 20px;
}
.form-group label {
font-weight: 600;
color: var(--color-grey-dark);
}
.form-group input,
.form-group select,
.form-group textarea {
padding: 12px;
border: 1px solid #ddd;
border-radius: var(--radius-sm);
font-size: 1rem;
}
.form-group textarea {
resize: vertical;
min-height: 100px;
}
.input-hint {
font-size: 0.8rem;
color: var(--color-grey-dark);
margin-top: 4px;
}
/* Чекбоксы */
.checkbox-group {
display: grid;
gap: 15px;
}
.checkbox-item {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
padding: 10px;
border-radius: var(--radius-sm);
transition: background 0.2s;
}
.checkbox-item:hover {
background: #e9ecef;
}
.checkbox-item input[type="checkbox"] {
width: auto;
}
/* Сети */
.networks-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
}
.network-checkbox {
display: flex;
align-items: center;
gap: 15px;
cursor: pointer;
padding: 15px;
background: white;
border-radius: var(--radius-sm);
border: 1px solid #e9ecef;
transition: all 0.3s ease;
}
.network-checkbox:hover {
border-color: var(--color-primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.network-checkbox input[type="checkbox"] {
width: auto;
}
.network-info {
display: flex;
flex-direction: column;
gap: 5px;
}
.network-name {
font-weight: 600;
color: var(--color-primary);
}
.network-chain-id {
font-size: 0.8rem;
color: var(--color-grey-dark);
}
/* Резервное копирование */
.backup-actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.backup-card {
background: white;
padding: 25px;
border-radius: var(--radius-lg);
border: 1px solid #e9ecef;
text-align: center;
}
.backup-card h3 {
color: var(--color-primary);
margin-bottom: 15px;
font-size: 1.2rem;
}
.backup-card p {
color: var(--color-grey-dark);
margin-bottom: 20px;
line-height: 1.5;
}
/* Опасная зона */
.danger-actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.danger-card {
background: #fff5f5;
padding: 25px;
border-radius: var(--radius-lg);
border: 1px solid #fed7d7;
text-align: center;
}
.danger-card h3 {
color: #c53030;
margin-bottom: 15px;
font-size: 1.2rem;
}
.danger-card p {
color: var(--color-grey-dark);
margin-bottom: 20px;
line-height: 1.5;
}
/* Кнопки */
.btn-primary {
background: var(--color-primary);
color: white;
border: none;
padding: 12px 24px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-primary:hover:not(:disabled) {
background: var(--color-primary-dark);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-secondary {
background: var(--color-secondary);
color: white;
border: none;
padding: 10px 20px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-secondary:hover {
background: var(--color-secondary-dark);
}
.btn-danger {
background: #dc3545;
color: white;
border: none;
padding: 10px 20px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-danger:hover {
background: #c82333;
}
/* Адаптивность */
@media (max-width: 768px) {
.form-row {
grid-template-columns: 1fr;
}
.networks-grid {
grid-template-columns: 1fr;
}
.backup-actions,
.danger-actions {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,626 @@
<!--
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="tokens-container">
<!-- Заголовок -->
<div class="page-header">
<div class="header-content">
<h1>Токены DLE</h1>
<p>Балансы, трансферы и распределение токенов</p>
</div>
<button class="close-btn" @click="router.push('/management')">×</button>
</div>
<!-- Информация о токенах -->
<div class="token-info-section">
<h2>Информация о токенах</h2>
<div class="token-info-grid">
<div class="info-card">
<h3>Общий запас</h3>
<p class="token-amount">{{ totalSupply }} {{ tokenSymbol }}</p>
</div>
<div class="info-card">
<h3>Ваш баланс</h3>
<p class="token-amount">{{ userBalance }} {{ tokenSymbol }}</p>
</div>
<div class="info-card">
<h3>Кворум</h3>
<p class="token-amount">{{ quorumPercentage }}%</p>
</div>
<div class="info-card">
<h3>Цена токена</h3>
<p class="token-amount">${{ tokenPrice }}</p>
</div>
</div>
</div>
<!-- Трансфер токенов -->
<div class="transfer-section">
<h2>Перевод токенов</h2>
<form @submit.prevent="transferTokens" class="transfer-form">
<div class="form-row">
<div class="form-group">
<label for="recipient">Получатель:</label>
<input
id="recipient"
v-model="transferData.recipient"
type="text"
placeholder="0x..."
required
>
</div>
<div class="form-group">
<label for="amount">Количество токенов:</label>
<input
id="amount"
v-model="transferData.amount"
type="number"
min="0.01"
step="0.01"
placeholder="0.00"
required
>
</div>
</div>
<div class="form-group">
<label for="transferDescription">Описание (опционально):</label>
<textarea
id="transferDescription"
v-model="transferData.description"
placeholder="Укажите причину перевода..."
rows="3"
></textarea>
</div>
<button type="submit" class="btn-primary" :disabled="isTransferring">
{{ isTransferring ? 'Перевод...' : 'Перевести токены' }}
</button>
</form>
</div>
<!-- Распределение токенов -->
<div class="distribution-section">
<h2>Распределение токенов</h2>
<form @submit.prevent="distributeTokens" class="distribution-form">
<div class="form-group">
<label for="distributionType">Тип распределения:</label>
<select id="distributionType" v-model="distributionData.type" required>
<option value="">Выберите тип</option>
<option value="partners">Партнерам</option>
<option value="employees">Сотрудникам</option>
<option value="investors">Инвесторам</option>
<option value="custom">Пользовательское</option>
</select>
</div>
<div class="form-group">
<label>Получатели:</label>
<div class="recipients-list">
<div
v-for="(recipient, index) in distributionData.recipients"
:key="index"
class="recipient-item"
>
<input
v-model="recipient.address"
type="text"
placeholder="Адрес получателя"
required
>
<input
v-model="recipient.amount"
type="number"
placeholder="Количество"
min="0.01"
step="0.01"
required
>
<button
type="button"
@click="removeRecipient(index)"
class="btn-remove"
>
</button>
</div>
</div>
<button
type="button"
@click="addRecipient"
class="btn-secondary"
>
+ Добавить получателя
</button>
</div>
<button type="submit" class="btn-primary" :disabled="isDistributing">
{{ isDistributing ? 'Распределение...' : 'Распределить токены' }}
</button>
</form>
</div>
<!-- Держатели токенов -->
<div class="holders-section">
<h2>Держатели токенов</h2>
<div v-if="tokenHolders.length === 0" class="empty-state">
<p>Нет данных о держателях токенов</p>
</div>
<div v-else class="holders-list">
<div
v-for="holder in tokenHolders"
:key="holder.address"
class="holder-item"
>
<div class="holder-info">
<span class="holder-address">{{ formatAddress(holder.address) }}</span>
<span class="holder-balance">{{ holder.balance }} {{ tokenSymbol }}</span>
</div>
<div class="holder-percentage">
{{ ((holder.balance / totalSupply) * 100).toFixed(2) }}%
</div>
</div>
</div>
</div>
</div>
</BaseLayout>
</template>
<script setup>
import { ref, defineProps, defineEmits } from 'vue';
import { useRouter } from 'vue-router';
import BaseLayout from '../../components/BaseLayout.vue';
// Определяем props
const props = defineProps({
isAuthenticated: Boolean,
identities: Array,
tokenBalances: Object,
isLoadingTokens: Boolean
});
// Определяем emits
const emit = defineEmits(['auth-action-completed']);
const router = useRouter();
// Состояние
const isTransferring = ref(false);
const isDistributing = ref(false);
// Данные токенов
const tokenSymbol = ref('MDLE');
const totalSupply = ref(10000);
const userBalance = ref(1000);
const quorumPercentage = ref(51);
const tokenPrice = ref(1.25);
// Данные трансфера
const transferData = ref({
recipient: '',
amount: '',
description: ''
});
// Данные распределения
const distributionData = ref({
type: '',
recipients: [
{ address: '', amount: '' }
]
});
// Держатели токенов (временные данные)
const tokenHolders = ref([
{ address: '0x1234567890123456789012345678901234567890', balance: 2500 },
{ address: '0x2345678901234567890123456789012345678901', balance: 1800 },
{ address: '0x3456789012345678901234567890123456789012', balance: 1200 },
{ address: '0x4567890123456789012345678901234567890123', balance: 800 },
{ address: '0x5678901234567890123456789012345678901234', balance: 600 }
]);
// Методы
const transferTokens = async () => {
if (isTransferring.value) return;
try {
isTransferring.value = true;
// Здесь будет логика трансфера токенов
console.log('Трансфер токенов:', transferData.value);
// Временная логика
const amount = parseFloat(transferData.value.amount);
if (amount > userBalance.value) {
alert('Недостаточно токенов для перевода');
return;
}
userBalance.value -= amount;
// Сброс формы
transferData.value = {
recipient: '',
amount: '',
description: ''
};
alert('Токены успешно переведены!');
} catch (error) {
console.error('Ошибка трансфера токенов:', error);
alert('Ошибка при переводе токенов');
} finally {
isTransferring.value = false;
}
};
const distributeTokens = async () => {
if (isDistributing.value) return;
try {
isDistributing.value = true;
// Здесь будет логика распределения токенов
console.log('Распределение токенов:', distributionData.value);
// Временная логика
const totalAmount = distributionData.value.recipients.reduce((sum, recipient) => {
return sum + parseFloat(recipient.amount || 0);
}, 0);
if (totalAmount > userBalance.value) {
alert('Недостаточно токенов для распределения');
return;
}
userBalance.value -= totalAmount;
// Сброс формы
distributionData.value = {
type: '',
recipients: [{ address: '', amount: '' }]
};
alert('Токены успешно распределены!');
} catch (error) {
console.error('Ошибка распределения токенов:', error);
alert('Ошибка при распределении токенов');
} finally {
isDistributing.value = false;
}
};
const addRecipient = () => {
distributionData.value.recipients.push({ address: '', amount: '' });
};
const removeRecipient = (index) => {
if (distributionData.value.recipients.length > 1) {
distributionData.value.recipients.splice(index, 1);
}
};
const formatAddress = (address) => {
if (!address) return '';
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
};
</script>
<style scoped>
.tokens-container {
padding: 20px;
background-color: var(--color-white);
border-radius: var(--radius-lg);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-top: 20px;
margin-bottom: 20px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 2px solid #f0f0f0;
}
.header-content {
flex-grow: 1;
}
.page-header h1 {
color: var(--color-primary);
font-size: 2.5rem;
margin: 0 0 10px 0;
}
.page-header p {
color: var(--color-grey-dark);
font-size: 1.1rem;
margin: 0;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #666;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s;
flex-shrink: 0;
}
.close-btn:hover {
background: #f0f0f0;
color: #333;
}
/* Секции */
.token-info-section,
.transfer-section,
.distribution-section,
.holders-section {
margin-bottom: 40px;
}
.token-info-section h2,
.transfer-section h2,
.distribution-section h2,
.holders-section h2 {
color: var(--color-primary);
margin-bottom: 20px;
font-size: 1.8rem;
}
/* Информация о токенах */
.token-info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.info-card {
background: #f8f9fa;
padding: 25px;
border-radius: var(--radius-lg);
border-left: 4px solid var(--color-primary);
text-align: center;
}
.info-card h3 {
color: var(--color-primary);
margin-bottom: 15px;
font-size: 1rem;
text-transform: uppercase;
font-weight: 600;
}
.token-amount {
font-size: 1.8rem;
font-weight: 700;
margin: 0;
color: var(--color-primary);
}
/* Формы */
.transfer-form,
.distribution-form {
background: #f8f9fa;
padding: 25px;
border-radius: var(--radius-lg);
border: 1px solid #e9ecef;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 20px;
}
.form-group label {
font-weight: 600;
color: var(--color-grey-dark);
}
.form-group input,
.form-group select,
.form-group textarea {
padding: 12px;
border: 1px solid #ddd;
border-radius: var(--radius-sm);
font-size: 1rem;
}
.form-group textarea {
resize: vertical;
min-height: 80px;
}
/* Получатели */
.recipients-list {
display: grid;
gap: 15px;
margin-bottom: 15px;
}
.recipient-item {
display: grid;
grid-template-columns: 1fr 1fr auto;
gap: 15px;
align-items: center;
}
.btn-remove {
background: #dc3545;
color: white;
border: none;
border-radius: 50%;
width: 30px;
height: 30px;
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
}
.btn-remove:hover {
background: #c82333;
}
/* Держатели токенов */
.holders-list {
display: grid;
gap: 15px;
}
.holder-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background: white;
border-radius: var(--radius-lg);
border: 1px solid #e9ecef;
transition: all 0.3s ease;
}
.holder-item:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.holder-info {
display: flex;
flex-direction: column;
gap: 5px;
}
.holder-address {
font-family: monospace;
font-size: 0.9rem;
color: var(--color-grey-dark);
}
.holder-balance {
font-size: 1.1rem;
font-weight: 600;
color: var(--color-primary);
}
.holder-percentage {
font-size: 0.9rem;
color: var(--color-grey-dark);
font-weight: 600;
}
/* Кнопки */
.btn-primary {
background: var(--color-primary);
color: white;
border: none;
padding: 12px 24px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-primary:hover:not(:disabled) {
background: var(--color-primary-dark);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-secondary {
background: var(--color-secondary);
color: white;
border: none;
padding: 10px 20px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-secondary:hover {
background: var(--color-secondary-dark);
}
/* Состояния */
.empty-state {
text-align: center;
padding: 60px;
color: var(--color-grey-dark);
background: #f8f9fa;
border-radius: var(--radius-lg);
border: 2px dashed #dee2e6;
}
.empty-state p {
margin: 0;
font-size: 1.1rem;
}
/* Адаптивность */
@media (max-width: 768px) {
.form-row {
grid-template-columns: 1fr;
}
.recipient-item {
grid-template-columns: 1fr;
gap: 10px;
}
.holder-item {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.token-info-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,935 @@
<!--
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="treasury-container">
<!-- Заголовок -->
<div class="page-header">
<div class="header-content">
<h1>Казна</h1>
<p>Управление средствами и активами DLE</p>
</div>
<button class="close-btn" @click="router.push('/management')">×</button>
</div>
<!-- Обзор казны -->
<div class="treasury-overview">
<h2>Обзор казны</h2>
<div class="overview-grid">
<div class="overview-card">
<h3>Общий баланс</h3>
<p class="balance-amount">${{ totalBalance.toLocaleString() }}</p>
<p class="balance-change positive">+${{ dailyChange.toLocaleString() }} (24ч)</p>
</div>
<div class="overview-card">
<h3>Активы</h3>
<p class="balance-amount">{{ assetsCount }}</p>
<p class="balance-description">различных активов</p>
</div>
<div class="overview-card">
<h3>Доходность</h3>
<p class="balance-amount">{{ yieldPercentage }}%</p>
<p class="balance-description">годовая доходность</p>
</div>
<div class="overview-card">
<h3>Риск</h3>
<p class="balance-amount risk-low">Низкий</p>
<p class="balance-description">уровень риска</p>
</div>
</div>
</div>
<!-- Активы -->
<div class="assets-section">
<h2>Активы</h2>
<div class="assets-list">
<div
v-for="asset in assets"
:key="asset.id"
class="asset-card"
>
<div class="asset-info">
<div class="asset-header">
<h3>{{ asset.name }}</h3>
<span class="asset-symbol">{{ asset.symbol }}</span>
</div>
<div class="asset-balance">
<p class="asset-amount">{{ asset.balance }} {{ asset.symbol }}</p>
<p class="asset-value">${{ asset.value.toLocaleString() }}</p>
</div>
<div class="asset-change" :class="asset.change >= 0 ? 'positive' : 'negative'">
{{ asset.change >= 0 ? '+' : '' }}{{ asset.change }}% (24ч)
</div>
</div>
<div class="asset-actions">
<button @click="depositAsset(asset)" class="btn-secondary">
Пополнить
</button>
<button @click="withdrawAsset(asset)" class="btn-secondary">
Вывести
</button>
</div>
</div>
</div>
</div>
<!-- Операции -->
<div class="operations-section">
<h2>Операции</h2>
<div class="operations-tabs">
<button
v-for="tab in operationTabs"
:key="tab.id"
@click="activeTab = tab.id"
class="tab-button"
:class="{ active: activeTab === tab.id }"
>
{{ tab.name }}
</button>
</div>
<!-- Форма депозита -->
<div v-if="activeTab === 'deposit'" class="operation-form">
<form @submit.prevent="performDeposit" class="deposit-form">
<div class="form-row">
<div class="form-group">
<label for="depositAsset">Актив:</label>
<select id="depositAsset" v-model="depositData.asset" required>
<option value="">Выберите актив</option>
<option v-for="asset in assets" :key="asset.id" :value="asset.id">
{{ asset.name }} ({{ asset.symbol }})
</option>
</select>
</div>
<div class="form-group">
<label for="depositAmount">Количество:</label>
<input
id="depositAmount"
v-model="depositData.amount"
type="number"
min="0.000001"
step="0.000001"
placeholder="0.00"
required
>
</div>
</div>
<div class="form-group">
<label for="depositReason">Причина депозита:</label>
<textarea
id="depositReason"
v-model="depositData.reason"
placeholder="Опишите причину депозита..."
rows="3"
></textarea>
</div>
<button type="submit" class="btn-primary" :disabled="isProcessing">
{{ isProcessing ? 'Обработка...' : 'Пополнить казну' }}
</button>
</form>
</div>
<!-- Форма вывода -->
<div v-if="activeTab === 'withdraw'" class="operation-form">
<form @submit.prevent="performWithdraw" class="withdraw-form">
<div class="form-row">
<div class="form-group">
<label for="withdrawAsset">Актив:</label>
<select id="withdrawAsset" v-model="withdrawData.asset" required>
<option value="">Выберите актив</option>
<option v-for="asset in assets" :key="asset.id" :value="asset.id">
{{ asset.name }} ({{ asset.symbol }})
</option>
</select>
</div>
<div class="form-group">
<label for="withdrawAmount">Количество:</label>
<input
id="withdrawAmount"
v-model="withdrawData.amount"
type="number"
min="0.000001"
step="0.000001"
placeholder="0.00"
required
>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="withdrawRecipient">Получатель:</label>
<input
id="withdrawRecipient"
v-model="withdrawData.recipient"
type="text"
placeholder="0x..."
required
>
</div>
<div class="form-group">
<label for="withdrawReason">Причина вывода:</label>
<input
id="withdrawReason"
v-model="withdrawData.reason"
type="text"
placeholder="Опишите причину вывода..."
required
>
</div>
</div>
<button type="submit" class="btn-primary" :disabled="isProcessing">
{{ isProcessing ? 'Обработка...' : 'Вывести из казны' }}
</button>
</form>
</div>
</div>
<!-- История операций -->
<div class="history-section">
<h2>История операций</h2>
<div v-if="operationsHistory.length === 0" class="empty-state">
<p>Нет операций в истории</p>
</div>
<div v-else class="history-list">
<div
v-for="operation in operationsHistory"
:key="operation.id"
class="history-item"
>
<div class="operation-info">
<div class="operation-header">
<h3>{{ operation.type === 'deposit' ? 'Депозит' : 'Вывод' }}</h3>
<span class="operation-date">{{ formatDate(operation.timestamp) }}</span>
</div>
<div class="operation-details">
<p><strong>Актив:</strong> {{ operation.asset }}</p>
<p><strong>Количество:</strong> {{ operation.amount }} {{ operation.symbol }}</p>
<p><strong>Стоимость:</strong> ${{ operation.value.toLocaleString() }}</p>
<p v-if="operation.reason"><strong>Причина:</strong> {{ operation.reason }}</p>
<p v-if="operation.recipient"><strong>Получатель:</strong> {{ formatAddress(operation.recipient) }}</p>
</div>
</div>
<div class="operation-status" :class="operation.status">
{{ getStatusText(operation.status) }}
</div>
</div>
</div>
</div>
</div>
</BaseLayout>
</template>
<script setup>
import { ref, defineProps, defineEmits } from 'vue';
import { useRouter } from 'vue-router';
import BaseLayout from '../../components/BaseLayout.vue';
// Определяем props
const props = defineProps({
isAuthenticated: Boolean,
identities: Array,
tokenBalances: Object,
isLoadingTokens: Boolean
});
// Определяем emits
const emit = defineEmits(['auth-action-completed']);
const router = useRouter();
// Состояние
const isProcessing = ref(false);
const activeTab = ref('deposit');
// Данные казны
const totalBalance = ref(1250000);
const dailyChange = ref(25000);
const assetsCount = ref(5);
const yieldPercentage = ref(8.5);
// Активы (временные данные)
const assets = ref([
{
id: 'eth',
name: 'Ethereum',
symbol: 'ETH',
balance: 125.5,
value: 450000,
change: 2.5
},
{
id: 'usdc',
name: 'USD Coin',
symbol: 'USDC',
balance: 500000,
value: 500000,
change: 0.1
},
{
id: 'btc',
name: 'Bitcoin',
symbol: 'BTC',
balance: 2.5,
value: 150000,
change: -1.2
},
{
id: 'matic',
name: 'Polygon',
symbol: 'MATIC',
balance: 50000,
value: 75000,
change: 5.8
},
{
id: 'link',
name: 'Chainlink',
symbol: 'LINK',
balance: 2500,
value: 75000,
change: 3.2
}
]);
// Вкладки операций
const operationTabs = ref([
{ id: 'deposit', name: 'Депозит' },
{ id: 'withdraw', name: 'Вывод' }
]);
// Данные депозита
const depositData = ref({
asset: '',
amount: '',
reason: ''
});
// Данные вывода
const withdrawData = ref({
asset: '',
amount: '',
recipient: '',
reason: ''
});
// История операций (временные данные)
const operationsHistory = ref([
{
id: 1,
type: 'deposit',
asset: 'Ethereum',
symbol: 'ETH',
amount: 10.5,
value: 37500,
reason: 'Пополнение казны от доходов',
timestamp: Date.now() - 3600000,
status: 'completed'
},
{
id: 2,
type: 'withdraw',
asset: 'USD Coin',
symbol: 'USDC',
amount: 25000,
value: 25000,
reason: 'Выплата партнерам',
recipient: '0x1234567890123456789012345678901234567890',
timestamp: Date.now() - 7200000,
status: 'completed'
}
]);
// Методы
const depositAsset = (asset) => {
depositData.value.asset = asset.id;
activeTab.value = 'deposit';
};
const withdrawAsset = (asset) => {
withdrawData.value.asset = asset.id;
activeTab.value = 'withdraw';
};
const performDeposit = async () => {
if (isProcessing.value) return;
try {
isProcessing.value = true;
// Здесь будет логика депозита
console.log('Депозит:', depositData.value);
// Временная логика
const asset = assets.value.find(a => a.id === depositData.value.asset);
if (asset) {
const amount = parseFloat(depositData.value.amount);
asset.balance += amount;
asset.value = asset.balance * (asset.value / (asset.balance - amount));
totalBalance.value += amount * (asset.value / asset.balance);
}
// Добавляем в историю
operationsHistory.value.unshift({
id: operationsHistory.value.length + 1,
type: 'deposit',
asset: asset.name,
symbol: asset.symbol,
amount: parseFloat(depositData.value.amount),
value: parseFloat(depositData.value.amount) * (asset.value / asset.balance),
reason: depositData.value.reason,
timestamp: Date.now(),
status: 'completed'
});
// Сброс формы
depositData.value = {
asset: '',
amount: '',
reason: ''
};
alert('Депозит успешно выполнен!');
} catch (error) {
console.error('Ошибка депозита:', error);
alert('Ошибка при выполнении депозита');
} finally {
isProcessing.value = false;
}
};
const performWithdraw = async () => {
if (isProcessing.value) return;
try {
isProcessing.value = true;
// Здесь будет логика вывода
console.log('Вывод:', withdrawData.value);
// Временная логика
const asset = assets.value.find(a => a.id === withdrawData.value.asset);
if (asset) {
const amount = parseFloat(withdrawData.value.amount);
if (amount > asset.balance) {
alert('Недостаточно средств для вывода');
return;
}
asset.balance -= amount;
asset.value = asset.balance * (asset.value / (asset.balance + amount));
totalBalance.value -= amount * (asset.value / asset.balance);
}
// Добавляем в историю
operationsHistory.value.unshift({
id: operationsHistory.value.length + 1,
type: 'withdraw',
asset: asset.name,
symbol: asset.symbol,
amount: parseFloat(withdrawData.value.amount),
value: parseFloat(withdrawData.value.amount) * (asset.value / asset.balance),
reason: withdrawData.value.reason,
recipient: withdrawData.value.recipient,
timestamp: Date.now(),
status: 'completed'
});
// Сброс формы
withdrawData.value = {
asset: '',
amount: '',
recipient: '',
reason: ''
};
alert('Вывод успешно выполнен!');
} catch (error) {
console.error('Ошибка вывода:', error);
alert('Ошибка при выполнении вывода');
} finally {
isProcessing.value = false;
}
};
const getStatusText = (status) => {
const statusMap = {
'completed': 'Завершено',
'pending': 'В обработке',
'failed': 'Ошибка'
};
return statusMap[status] || status;
};
const formatAddress = (address) => {
if (!address) return '';
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
};
const formatDate = (timestamp) => {
return new Date(timestamp).toLocaleString('ru-RU');
};
</script>
<style scoped>
.treasury-container {
padding: 20px;
background-color: var(--color-white);
border-radius: var(--radius-lg);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-top: 20px;
margin-bottom: 20px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 2px solid #f0f0f0;
}
.header-content {
flex-grow: 1;
}
.page-header h1 {
color: var(--color-primary);
font-size: 2.5rem;
margin: 0 0 10px 0;
}
.page-header p {
color: var(--color-grey-dark);
font-size: 1.1rem;
margin: 0;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #666;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s;
flex-shrink: 0;
}
.close-btn:hover {
background: #f0f0f0;
color: #333;
}
/* Секции */
.treasury-overview,
.assets-section,
.operations-section,
.history-section {
margin-bottom: 40px;
}
.treasury-overview h2,
.assets-section h2,
.operations-section h2,
.history-section h2 {
color: var(--color-primary);
margin-bottom: 20px;
font-size: 1.8rem;
}
/* Обзор казны */
.overview-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.overview-card {
background: #f8f9fa;
padding: 25px;
border-radius: var(--radius-lg);
border-left: 4px solid var(--color-primary);
text-align: center;
}
.overview-card h3 {
color: var(--color-primary);
margin-bottom: 15px;
font-size: 1rem;
text-transform: uppercase;
font-weight: 600;
}
.balance-amount {
font-size: 2rem;
font-weight: 700;
margin: 0 0 10px 0;
color: var(--color-primary);
}
.balance-change {
font-size: 1rem;
font-weight: 600;
margin: 0;
}
.balance-change.positive {
color: #28a745;
}
.balance-change.negative {
color: #dc3545;
}
.balance-description {
font-size: 0.9rem;
color: var(--color-grey-dark);
margin: 0;
}
.risk-low {
color: #28a745;
}
/* Активы */
.assets-list {
display: grid;
gap: 20px;
}
.asset-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 25px;
background: white;
border-radius: var(--radius-lg);
border: 1px solid #e9ecef;
transition: all 0.3s ease;
}
.asset-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.asset-info {
flex-grow: 1;
}
.asset-header {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 10px;
}
.asset-header h3 {
margin: 0;
color: var(--color-primary);
font-size: 1.2rem;
}
.asset-symbol {
background: var(--color-primary);
color: white;
padding: 4px 8px;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 600;
}
.asset-balance {
margin-bottom: 10px;
}
.asset-amount {
font-size: 1.1rem;
font-weight: 600;
margin: 0 0 5px 0;
color: var(--color-primary);
}
.asset-value {
font-size: 0.9rem;
color: var(--color-grey-dark);
margin: 0;
}
.asset-change {
font-size: 0.9rem;
font-weight: 600;
}
.asset-change.positive {
color: #28a745;
}
.asset-change.negative {
color: #dc3545;
}
.asset-actions {
display: flex;
gap: 10px;
}
/* Операции */
.operations-tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.tab-button {
background: #f8f9fa;
border: 1px solid #e9ecef;
padding: 12px 24px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: all 0.3s ease;
}
.tab-button:hover {
background: #e9ecef;
}
.tab-button.active {
background: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
.operation-form {
background: #f8f9fa;
padding: 25px;
border-radius: var(--radius-lg);
border: 1px solid #e9ecef;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 20px;
}
.form-group label {
font-weight: 600;
color: var(--color-grey-dark);
}
.form-group input,
.form-group select,
.form-group textarea {
padding: 12px;
border: 1px solid #ddd;
border-radius: var(--radius-sm);
font-size: 1rem;
}
.form-group textarea {
resize: vertical;
min-height: 80px;
}
/* История операций */
.history-list {
display: grid;
gap: 20px;
}
.history-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 25px;
background: white;
border-radius: var(--radius-lg);
border: 1px solid #e9ecef;
transition: all 0.3s ease;
}
.history-item:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.operation-info {
flex-grow: 1;
}
.operation-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.operation-header h3 {
margin: 0;
color: var(--color-primary);
font-size: 1.2rem;
}
.operation-date {
font-size: 0.9rem;
color: var(--color-grey-dark);
}
.operation-details p {
margin: 5px 0;
font-size: 0.95rem;
}
.operation-status {
padding: 6px 16px;
border-radius: 20px;
font-size: 0.9rem;
font-weight: 600;
margin-left: 20px;
}
.operation-status.completed {
background: #d4edda;
color: #155724;
}
.operation-status.pending {
background: #fff3cd;
color: #856404;
}
.operation-status.failed {
background: #f8d7da;
color: #721c24;
}
/* Кнопки */
.btn-primary {
background: var(--color-primary);
color: white;
border: none;
padding: 12px 24px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-primary:hover:not(:disabled) {
background: var(--color-primary-dark);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-secondary {
background: var(--color-secondary);
color: white;
border: none;
padding: 10px 20px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-secondary:hover {
background: var(--color-secondary-dark);
}
/* Состояния */
.empty-state {
text-align: center;
padding: 60px;
color: var(--color-grey-dark);
background: #f8f9fa;
border-radius: var(--radius-lg);
border: 2px dashed #dee2e6;
}
.empty-state p {
margin: 0;
font-size: 1.1rem;
}
/* Адаптивность */
@media (max-width: 768px) {
.form-row {
grid-template-columns: 1fr;
}
.asset-card {
flex-direction: column;
align-items: flex-start;
gap: 20px;
}
.asset-actions {
width: 100%;
justify-content: space-between;
}
.history-item {
flex-direction: column;
gap: 15px;
}
.operation-status {
margin-left: 0;
align-self: flex-start;
}
.overview-grid {
grid-template-columns: 1fr;
}
.operations-tabs {
flex-direction: column;
}
}
</style>