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

This commit is contained in:
2025-07-29 18:07:21 +03:00
parent ce42899afc
commit 0f2270a08a
58 changed files with 5367 additions and 5931 deletions

View File

@@ -6,9 +6,10 @@ http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Основной сервер для легитимных доменов
server {
listen 80;
server_name localhost;
server_name hb3-accelerator.com www.hb3-accelerator.com localhost;
# API прокси (точное совпадение для /api/)
location /api/ {
@@ -32,4 +33,16 @@ http {
proxy_set_header X-Forwarded-Port $server_port;
}
}
# Сервер по умолчанию для блокировки подозрительных доменов
server {
listen 80 default_server;
server_name _;
# Возвращаем 444 (Connection Closed Without Response)
return 444;
# Логируем попытки доступа к подозрительным доменам
access_log /var/log/nginx/suspicious_domains.log;
}
}

View File

@@ -1,3 +1,15 @@
<!--
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>
<div class="ai-queue-monitor">
<div class="monitor-header">

View File

@@ -1,841 +0,0 @@
<!--
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>
<div class="dle-management-modal">
<div class="dle-management-header">
<h2>Ваши DLE</h2>
<button class="close-btn" @click="$emit('close')">×</button>
</div>
<div class="dle-list-section">
<div v-if="dleList.length === 0" class="no-dle-message">
<p>У вас пока нет созданных DLE.</p>
</div>
<div v-else>
<div class="dle-list">
<div v-for="(dle, index) in dleList" :key="index" class="dle-card"
:class="{ 'active': selectedDleIndex === index }"
@click="selectDle(index)">
<div class="dle-card-header">
<h3>{{ dle.name }} ({{ dle.symbol }})</h3>
<button v-if="isAdmin" class="delete-dle-btn" @click.stop="deleteDle(dle, index)" title="Удалить DLE">
<i class="fas fa-trash"></i>
</button>
</div>
<p><strong>Адрес:</strong> {{ shortenAddress(dle.tokenAddress) }}</p>
<p><strong>Местонахождение:</strong> {{ dle.location }}</p>
</div>
</div>
</div>
</div>
<div v-if="selectedDle" class="dle-details-section">
<h2>Управление "{{ selectedDle.name }}"</h2>
<div class="dle-tabs">
<div class="tab-header">
<div class="tab-button" :class="{ 'active': activeTab === 'info' }" @click="activeTab = 'info'">
<i class="fas fa-info-circle"></i> Основная информация
</div>
<div class="tab-button" :class="{ 'active': activeTab === 'proposals' }" @click="activeTab = 'proposals'">
<i class="fas fa-tasks"></i> Предложения
</div>
<div class="tab-button" :class="{ 'active': activeTab === 'governance' }" @click="activeTab = 'governance'">
<i class="fas fa-balance-scale"></i> Управление
</div>
<div class="tab-button" :class="{ 'active': activeTab === 'modules' }" @click="activeTab = 'modules'">
<i class="fas fa-puzzle-piece"></i> Модули
</div>
</div>
<div class="tab-content" v-if="activeTab === 'info'">
<div class="info-card">
<h3>Основная информация</h3>
<div class="info-row">
<span class="info-label">Название:</span>
<span class="info-value">{{ selectedDle.name }}</span>
</div>
<div class="info-row">
<span class="info-label">Символ токена:</span>
<span class="info-value">{{ selectedDle.symbol }}</span>
</div>
<div class="info-row">
<span class="info-label">Местонахождение:</span>
<span class="info-value">{{ selectedDle.location }}</span>
</div>
<div class="info-row">
<span class="info-label">Коды деятельности:</span>
<span class="info-value">{{ selectedDle.isicCodes && selectedDle.isicCodes.length ? selectedDle.isicCodes.join(', ') : 'Не указаны' }}</span>
</div>
<div class="info-row">
<span class="info-label">Дата создания:</span>
<span class="info-value">{{ formatDate(selectedDle.creationTimestamp) }}</span>
</div>
</div>
<div class="contract-cards">
<div class="contract-card">
<h4>Токен управления</h4>
<p class="address">{{ selectedDle.tokenAddress }}</p>
<div class="contract-actions">
<button class="btn btn-sm btn-secondary" @click="copyToClipboard(selectedDle.tokenAddress)">
<i class="fas fa-copy"></i> Копировать адрес
</button>
<button class="btn btn-sm btn-info" @click="viewOnExplorer(selectedDle.tokenAddress)">
<i class="fas fa-external-link-alt"></i> Обзор
</button>
</div>
</div>
<div class="contract-card">
<h4>Таймлок</h4>
<p class="address">{{ selectedDle.timelockAddress }}</p>
<div class="contract-actions">
<button class="btn btn-sm btn-secondary" @click="copyToClipboard(selectedDle.timelockAddress)">
<i class="fas fa-copy"></i> Копировать адрес
</button>
<button class="btn btn-sm btn-info" @click="viewOnExplorer(selectedDle.timelockAddress)">
<i class="fas fa-external-link-alt"></i> Обзор
</button>
</div>
</div>
<div class="contract-card">
<h4>Governor</h4>
<p class="address">{{ selectedDle.governorAddress }}</p>
<div class="contract-actions">
<button class="btn btn-sm btn-secondary" @click="copyToClipboard(selectedDle.governorAddress)">
<i class="fas fa-copy"></i> Копировать адрес
</button>
<button class="btn btn-sm btn-info" @click="viewOnExplorer(selectedDle.governorAddress)">
<i class="fas fa-external-link-alt"></i> Обзор
</button>
</div>
</div>
</div>
</div>
<div class="tab-content" v-if="activeTab === 'proposals'">
<h3>Предложения</h3>
<div class="proposals-actions">
<button class="btn btn-primary" @click="showCreateProposalForm = true">
<i class="fas fa-plus"></i> Создать предложение
</button>
</div>
<div v-if="showCreateProposalForm" class="create-proposal-form">
<h4>Новое предложение</h4>
<div class="form-group">
<label for="proposalTitle">Заголовок:</label>
<input type="text" id="proposalTitle" v-model="newProposal.title" class="form-control">
</div>
<div class="form-group">
<label for="proposalDescription">Описание:</label>
<textarea id="proposalDescription" v-model="newProposal.description" class="form-control" rows="3"></textarea>
</div>
<div class="form-actions">
<button class="btn btn-success" @click="createProposal" :disabled="isCreatingProposal">
<i class="fas fa-paper-plane"></i> {{ isCreatingProposal ? 'Отправка...' : 'Отправить' }}
</button>
<button class="btn btn-secondary" @click="showCreateProposalForm = false">
<i class="fas fa-times"></i> Отмена
</button>
</div>
</div>
<div class="proposals-list">
<p v-if="proposals.length === 0">Предложений пока нет</p>
<div v-else v-for="(proposal, index) in proposals" :key="index" class="proposal-card">
<h4>{{ proposal.title }}</h4>
<p>{{ proposal.description }}</p>
<div class="proposal-status" :class="proposal.status">
{{ getProposalStatusText(proposal.status) }}
</div>
<div class="proposal-actions">
<button class="btn btn-sm btn-primary" @click="voteForProposal(proposal.id, true)" :disabled="!canVote(proposal)">
<i class="fas fa-thumbs-up"></i> За
</button>
<button class="btn btn-sm btn-danger" @click="voteForProposal(proposal.id, false)" :disabled="!canVote(proposal)">
<i class="fas fa-thumbs-down"></i> Против
</button>
</div>
</div>
</div>
</div>
<div class="tab-content" v-if="activeTab === 'governance'">
<h3>Управление</h3>
<div class="governance-info">
<div class="info-card">
<h4>Настройки Governor</h4>
<div class="info-row">
<span class="info-label">Порог предложения:</span>
<span class="info-value">100,000 GT</span>
</div>
<div class="info-row">
<span class="info-label">Кворум:</span>
<span class="info-value">4%</span>
</div>
<div class="info-row">
<span class="info-label">Задержка голосования:</span>
<span class="info-value">1 день</span>
</div>
<div class="info-row">
<span class="info-label">Период голосования:</span>
<span class="info-value">7 дней</span>
</div>
</div>
<div class="info-card">
<h4>Статистика голосований</h4>
<div class="info-row">
<span class="info-label">Всего предложений:</span>
<span class="info-value">{{ proposals.length }}</span>
</div>
<div class="info-row">
<span class="info-label">Активных предложений:</span>
<span class="info-value">{{ getProposalsByStatus('active').length }}</span>
</div>
<div class="info-row">
<span class="info-label">Успешных предложений:</span>
<span class="info-value">{{ getProposalsByStatus('succeeded').length }}</span>
</div>
<div class="info-row">
<span class="info-label">Отклоненных предложений:</span>
<span class="info-value">{{ getProposalsByStatus('defeated').length }}</span>
</div>
</div>
</div>
</div>
<div class="tab-content" v-if="activeTab === 'modules'">
<h3>Подключение модулей</h3>
<p>Здесь вы можете подключить дополнительные модули к вашему DLE.</p>
<div class="modules-list">
<div v-for="(module, index) in availableModules" :key="index" class="module-card">
<h4>{{ module.name }}</h4>
<p>{{ module.description }}</p>
<div class="module-status" :class="{ 'installed': isModuleInstalled(module) }">
{{ isModuleInstalled(module) ? 'Установлен' : 'Доступен' }}
</div>
<div v-if="module.name === 'Прием платежей' && paymentModuleTokens.length > 0" class="payment-tokens-list">
<div v-for="token in paymentModuleTokens" :key="token.address + token.network" class="payment-token-entry">
<span><b>{{ token.name }}</b> ({{ token.network }})</span>
<span style="font-size:0.95em;color:#888">{{ token.address }}</span>
</div>
</div>
<div class="module-actions">
<button v-if="module.name === 'Прием платежей' && !isModuleInstalled(module)" class="btn btn-success" @click="openPaymentTokensModal">
<i class="fas fa-plus"></i> Настроить
</button>
<button v-else-if="module.name === 'Прием платежей' && isModuleInstalled(module)" class="btn btn-danger" @click="uninstallPaymentModule">
<i class="fas fa-trash"></i> Удалить
</button>
<button v-else-if="!isModuleInstalled(module)" class="btn btn-success" @click="installModule(module)">
<i class="fas fa-plus"></i> Установить
</button>
<button v-else class="btn btn-danger" @click="uninstallModule(module)">
<i class="fas fa-trash"></i> Удалить
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<BaseModal
:show="showDeleteModal"
:title="deleteSuccess ? 'Готово' : 'Подтвердите удаление DLE'"
@close="closeDeleteModal"
>
<template #default>
<div style="margin-bottom:18px;">
<span v-if="!deleteSuccess">Удалить DLE <b>«{{ dleToDelete?.name }}»</b>? Это действие необратимо.</span>
<span v-else>DLE успешно удалён</span>
</div>
</template>
<template #actions>
<template v-if="!deleteSuccess">
<button class="modal-ok-btn delete-btn" @click="confirmDeleteDle" :disabled="isDeletingDle">
{{ isDeletingDle ? 'Удаление...' : 'Удалить' }}
</button>
<button class="modal-ok-btn cancel-btn" @click="closeDeleteModal" :disabled="isDeletingDle">Отмена</button>
</template>
<template v-else>
<button class="modal-ok-btn" @click="closeDeleteModal">OK</button>
</template>
</template>
</BaseModal>
<BaseModal
:show="showPaymentTokensModal"
title="Выберите токены для приема платежей"
@close="closePaymentTokensModal"
>
<template #default>
<div v-if="authTokens.length === 0">Нет доступных токенов. Добавьте токены в настройках безопасности.</div>
<div v-else>
<div v-for="token in authTokens" :key="token.address + token.network" class="token-select-row">
<label>
<input type="checkbox" :value="token" v-model="paymentModuleTokens.value" />
<b>{{ token.name }}</b> ({{ token.network }}) <span style="font-size:0.95em;color:#888">{{ token.address }}</span>
</label>
</div>
</div>
</template>
<template #actions>
<button class="modal-ok-btn" @click="closePaymentTokensModal">Отмена</button>
<button class="modal-ok-btn btn-success" @click="closePaymentTokensModal">Сохранить</button>
</template>
</BaseModal>
</div>
</template>
<script setup>
import { ref, defineProps, computed, inject } from 'vue';
import { useAuthContext } from '@/composables/useAuth';
import dleService from '@/services/dleService';
import BaseModal from './NoAccessModal.vue';
const props = defineProps({
dleList: { type: Array, required: true },
selectedDleIndex: { type: Number, default: null }
});
const { isAdmin } = useAuthContext();
const selectedDleIndex = ref(props.selectedDleIndex ?? 0);
const activeTab = ref('info');
const showCreateProposalForm = ref(false);
const newProposal = ref({ title: '', description: '' });
const isCreatingProposal = ref(false);
const proposals = ref([]);
const availableModules = ref([
{
name: 'Контракт на активы',
description: 'Позволяет токенизировать физические активы и управлять ими через DLE.',
installed: false
},
{
name: 'Мультиподпись',
description: 'Добавляет функциональность мультиподписи для повышенной безопасности.',
installed: false
},
{
name: 'Дивиденды',
description: 'Позволяет распределять дивиденды между держателями токенов.',
installed: false
},
{
name: 'Стейкинг',
description: 'Добавляет возможность стейкинга токенов для получения наград.',
installed: false
}
]);
const selectedDle = computed(() => {
if (selectedDleIndex.value !== null && props.dleList.length > selectedDleIndex.value) {
return props.dleList[selectedDleIndex.value];
}
return null;
});
function selectDle(index) {
selectedDleIndex.value = index;
activeTab.value = 'info';
}
function shortenAddress(address) {
if (!address) return '';
return `${address.slice(0, 6)}...${address.slice(-4)}`;
}
function formatDate(timestamp) {
if (!timestamp) return 'N/A';
return new Date(timestamp * 1000).toLocaleString();
}
function copyToClipboard(text) {
navigator.clipboard.writeText(text)
.then(() => {
alert('Адрес скопирован в буфер обмена');
})
.catch(err => {
console.error('Ошибка при копировании текста: ', err);
});
}
function viewOnExplorer(address) {
window.open(`https://sepolia.etherscan.io/address/${address}`, '_blank');
}
function createProposal() {
if (!newProposal.value.title || !newProposal.value.description) {
alert('Пожалуйста, заполните все поля');
return;
}
isCreatingProposal.value = true;
try {
proposals.value.push({
id: Date.now().toString(),
title: newProposal.value.title,
description: newProposal.value.description,
status: 'pending',
votes: { for: 0, against: 0 }
});
showCreateProposalForm.value = false;
newProposal.value = { title: '', description: '' };
alert('Предложение создано!');
} catch (error) {
console.error('Ошибка при создании предложения:', error);
alert('Ошибка при создании предложения');
} finally {
isCreatingProposal.value = false;
}
}
function voteForProposal(proposalId, isFor) {
try {
const proposal = proposals.value.find(p => p.id === proposalId);
if (proposal) {
if (isFor) {
proposal.votes.for += 1;
} else {
proposal.votes.against += 1;
}
if (proposal.votes.for > proposal.votes.against && proposal.votes.for >= 3) {
proposal.status = 'succeeded';
} else if (proposal.votes.against > proposal.votes.for && proposal.votes.against >= 3) {
proposal.status = 'defeated';
} else {
proposal.status = 'active';
}
alert('Ваш голос учтен!');
}
} catch (error) {
console.error('Ошибка при голосовании:', error);
alert('Ошибка при голосовании');
}
}
function canVote(proposal) {
return proposal.status === 'active' || proposal.status === 'pending';
}
function getProposalStatusText(status) {
const statusMap = {
'pending': 'Ожидает',
'active': 'Активно',
'succeeded': 'Принято',
'defeated': 'Отклонено',
'executed': 'Выполнено'
};
return statusMap[status] || status;
}
function getProposalsByStatus(status) {
return proposals.value.filter(p => p.status === status);
}
function installModule(module) {
module.installed = true;
alert(`Модуль "${module.name}" успешно установлен!`);
}
function uninstallModule(module) {
module.installed = false;
alert(`Модуль "${module.name}" удален.`);
}
function isModuleInstalled(module) {
if (typeof module.installed === 'function') return module.installed();
return !!module.installed;
}
const emit = defineEmits(['close', 'dle-updated']);
const showDeleteModal = ref(false);
const dleToDelete = ref(null);
const isDeletingDle = ref(false);
const deleteSuccess = ref(false);
function deleteDle(dle, idx) {
if (!isAdmin.value) return;
dleToDelete.value = dle;
showDeleteModal.value = true;
deleteSuccess.value = false;
}
function closeDeleteModal() {
showDeleteModal.value = false;
dleToDelete.value = null;
isDeletingDle.value = false;
deleteSuccess.value = false;
}
async function confirmDeleteDle() {
if (!dleToDelete.value) return;
isDeletingDle.value = true;
try {
await dleService.deleteDLE(dleToDelete.value.tokenAddress);
deleteSuccess.value = true;
emit('dle-updated');
isDeletingDle.value = false;
} catch (e) {
alert('Ошибка при удалении DLE: ' + (e?.message || e));
isDeletingDle.value = false;
}
}
const authTokens = inject('authTokens', ref([]));
const paymentModuleTokens = ref([]);
const showPaymentTokensModal = ref(false);
function openPaymentTokensModal() {
showPaymentTokensModal.value = true;
}
function closePaymentTokensModal() {
showPaymentTokensModal.value = false;
}
function savePaymentTokens(selected) {
paymentModuleTokens.value = selected;
closePaymentTokensModal();
// Можно добавить сохранение в API, если потребуется
}
function uninstallPaymentModule() {
paymentModuleTokens.value = [];
}
availableModules.value.push({
name: 'Прием платежей',
description: 'Позволяет принимать оплату в выбранных токенах. Можно выбрать один или несколько токенов для приема платежей.',
installed: computed(() => paymentModuleTokens.value.length > 0)
});
</script>
<style scoped>
.dle-management-modal {
background: #fff;
border-radius: 16px;
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
padding: 32px 24px 24px 24px;
width: 100%;
margin-top: 40px;
position: relative;
overflow-x: auto;
}
.dle-management-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.close-btn {
background: none;
border: none;
font-size: 2rem;
cursor: pointer;
color: #bbb;
transition: color 0.2s;
}
.close-btn:hover {
color: #333;
}
.dle-list-section {
margin-bottom: 30px;
}
.dle-list {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-top: 20px;
}
.dle-card {
width: 300px;
padding: 15px;
border: 1px solid #e5e7eb;
border-radius: 10px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
background: #f8f9fa;
cursor: pointer;
transition: all 0.2s ease;
}
.dle-card.active {
border-color: #17a2b8;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.dle-card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.dle-card h3 {
margin-top: 0;
margin-bottom: 10px;
color: #17a2b8;
}
.dle-details-section {
margin-top: 30px;
border-top: 1px solid #e5e7eb;
padding-top: 20px;
}
.no-dle-message {
padding: 20px;
background-color: #f8f9fa;
border-radius: 10px;
text-align: center;
}
.dle-tabs {
display: flex;
flex-direction: column;
gap: 20px;
}
.tab-header {
display: flex;
gap: 20px;
border-bottom: 1px solid #e5e7eb;
}
.tab-button {
padding: 10px 20px;
cursor: pointer;
transition: border-bottom 0.2s;
}
.tab-button.active {
border-bottom: 2px solid #17a2b8;
}
.tab-content {
margin-top: 20px;
}
.info-card {
padding: 20px;
background-color: #f8f9fa;
border-radius: 10px;
}
.info-row {
margin-bottom: 10px;
}
.info-label {
font-weight: bold;
}
.info-value {
margin-left: 10px;
}
.contract-cards {
display: flex;
gap: 24px;
margin-top: 24px;
flex-wrap: wrap;
justify-content: space-between;
}
.contract-card {
flex: 1 1 0;
min-width: 260px;
max-width: 340px;
background: #fff;
border-radius: 18px;
box-shadow: 0 2px 16px rgba(0,0,0,0.07);
padding: 22px 20px 18px 20px;
display: flex;
flex-direction: column;
margin-bottom: 16px;
transition: box-shadow 0.2s, transform 0.2s;
}
.contract-card:hover {
box-shadow: 0 6px 24px rgba(23,162,184,0.13);
transform: translateY(-2px) scale(1.02);
}
.contract-card h4 {
margin: 0 0 10px 0;
font-size: 1.1rem;
font-weight: 600;
color: #17a2b8;
}
.contract-card .address {
font-family: 'Fira Mono', 'Consolas', monospace;
word-break: break-all;
background: #f6f8fa;
border-radius: 6px;
padding: 7px 10px;
font-size: 0.98rem;
margin-bottom: 18px;
color: #222;
}
.contract-actions {
display: flex;
gap: 10px;
width: 100%;
}
.contract-actions .btn {
flex: 1 1 0;
min-width: 0;
font-size: 1rem;
padding: 10px 0;
border-radius: 8px;
font-weight: 500;
box-shadow: none;
border: none;
transition: background 0.18s, color 0.18s;
}
.contract-actions .btn-secondary {
background: #f1f3f6;
color: #888;
}
.contract-actions .btn-secondary:hover {
background: #e2e6ea;
color: #222;
}
.contract-actions .btn-info {
background: #17a2b8;
color: #fff;
}
.contract-actions .btn-info:hover {
background: #148a9d;
}
@media (max-width: 900px) {
.contract-cards {
flex-direction: column;
gap: 16px;
align-items: stretch;
}
.contract-card {
max-width: 100%;
min-width: 0;
}
}
.proposals-actions {
margin-bottom: 20px;
}
.create-proposal-form {
padding: 20px;
background-color: #f8f9fa;
border-radius: 10px;
}
.form-group {
margin-bottom: 10px;
}
.form-group label {
display: block;
margin-bottom: 5px;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 8px;
border: 1px solid #e5e7eb;
border-radius: 5px;
}
.form-actions {
margin-top: 10px;
display: flex;
justify-content: flex-end;
gap: 10px;
}
.proposals-list {
margin-top: 20px;
}
.proposal-card {
padding: 10px;
background-color: #f8f9fa;
border-radius: 5px;
margin-bottom: 10px;
}
.proposal-status {
margin-top: 5px;
padding: 5px 10px;
border-radius: 5px;
}
.proposal-status.pending {
background-color: #ffd700;
}
.proposal-status.active {
background-color: #17a2b8;
color: #fff;
}
.proposal-status.succeeded {
background-color: #28a745;
color: #fff;
}
.proposal-status.defeated {
background-color: #dc3545;
color: #fff;
}
.proposal-actions {
margin-top: 10px;
display: flex;
gap: 10px;
}
.modules-list {
margin-top: 20px;
}
.module-card {
padding: 10px;
background-color: #f8f9fa;
border-radius: 5px;
margin-bottom: 10px;
}
.module-status {
margin-top: 5px;
padding: 5px 10px;
border-radius: 5px;
}
.module-status.installed {
background-color: #28a745;
color: #fff;
}
.module-actions {
margin-top: 10px;
display: flex;
gap: 10px;
}
.delete-dle-btn {
background: none;
border: none;
color: #dc3545;
font-size: 1.2em;
cursor: pointer;
padding: 2px 6px;
border-radius: 4px;
transition: background 0.15s;
}
.delete-dle-btn:hover {
background: #ffeaea;
color: #a71d2a;
}
.delete-btn {
background: #dc3545;
color: #fff;
border: none;
border-radius: 6px;
padding: 0.5rem 2rem;
cursor: pointer;
font-size: 1rem;
transition: background 0.2s;
}
.delete-btn:disabled {
background: #e6a6ad;
cursor: not-allowed;
}
.delete-btn:hover:not(:disabled) {
background: #b52a37;
}
.cancel-btn {
background: #f5f5f5;
color: #333;
border: 1px solid #ccc;
border-radius: 6px;
padding: 0.5rem 2rem;
cursor: pointer;
font-size: 1rem;
transition: background 0.2s;
}
.cancel-btn:disabled {
background: #eee;
color: #aaa;
cursor: not-allowed;
}
.cancel-btn:hover:not(:disabled) {
background: #e0e0e0;
}
.payment-tokens-list {
margin: 10px 0 0 0;
padding: 8px 0 0 0;
border-top: 1px solid #e5e7eb;
}
.payment-token-entry {
font-size: 1.02em;
margin-bottom: 4px;
display: flex;
flex-direction: column;
}
.token-select-row {
margin-bottom: 8px;
text-align: left;
}
.btn-success {
background: #28a745;
color: #fff;
border: none;
border-radius: 6px;
padding: 0.5rem 2rem;
cursor: pointer;
font-size: 1rem;
transition: background 0.2s;
}
.btn-success:hover {
background: #218838;
}
</style>

View File

@@ -171,11 +171,7 @@ const routes = [
name: 'contacts-list',
component: () => import('../views/ContactsView.vue')
},
{
path: '/dle-management',
name: 'dle-management',
component: () => import('../views/DleManagementView.vue')
},
{
path: '/settings/ai/telegram',
name: 'telegram-settings',
@@ -223,10 +219,16 @@ const routes = [
component: () => import('../views/smartcontracts/DleManagementView.vue'),
meta: { requiresAuth: true }
},
{
path: '/management/dle-management',
name: 'management-dle-management',
component: () => import('../views/smartcontracts/DleManagementView.vue'),
meta: { requiresAuth: true }
},
{
path: '/management/proposals',
name: 'management-proposals',
component: () => import('../views/smartcontracts/ProposalsView.vue'),
component: () => import('../views/smartcontracts/DleProposalsView.vue'),
meta: { requiresAuth: true }
},
{
@@ -244,7 +246,13 @@ const routes = [
{
path: '/management/modules',
name: 'management-modules',
component: () => import('../views/smartcontracts/ModulesView.vue'),
component: () => import('../views/smartcontracts/DleModulesView.vue'),
meta: { requiresAuth: true }
},
{
path: '/management/multisig',
name: 'management-multisig',
component: () => import('../views/smartcontracts/DleMultisigView.vue'),
meta: { requiresAuth: true }
},
{

View File

@@ -1,104 +0,0 @@
/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* All rights reserved.
*
* This software is proprietary and confidential.
* Unauthorized copying, modification, or distribution is prohibited.
*
* For licensing inquiries: info@hb3-accelerator.com
* Website: https://hb3-accelerator.com
* GitHub: https://github.com/HB3-ACCELERATOR
*/
import api from '@/api/axios';
/**
* Сервис для работы с DLE (Digital Legal Entity)
*/
class DLEService {
/**
* Получает настройки по умолчанию для создания DLE
* @returns {Promise<Object>} - Настройки по умолчанию
*/
async getDefaultSettings() {
try {
const response = await api.get('/dle/settings');
return response.data.data;
} catch (error) {
console.error('Ошибка при получении настроек DLE:', error);
throw error;
}
}
/**
* Создает новое DLE с указанными параметрами
* @param {Object} dleParams - Параметры для создания DLE
* @returns {Promise<Object>} - Результат создания DLE
*/
async createDLE(dleParams) {
try {
const response = await api.post('/dle', dleParams);
return response.data;
} catch (error) {
console.error('Ошибка при создании DLE:', error);
throw error;
}
}
/**
* Получает список всех DLE
* @returns {Promise<Array>} - Список DLE
*/
async getAllDLEs() {
try {
const response = await api.get('/dle');
// Проверяем и нормализуем поля isicCodes для всех DLE
if (response.data.data && Array.isArray(response.data.data)) {
response.data.data.forEach(dle => {
// Если isicCodes отсутствует или не является массивом, инициализируем пустым массивом
if (!dle.isicCodes || !Array.isArray(dle.isicCodes)) {
dle.isicCodes = [];
}
});
}
return response.data.data;
} catch (error) {
console.error('Ошибка при получении списка DLE:', error);
throw error;
}
}
/**
* Удаляет DLE по адресу токена
* @param {string} tokenAddress - Адрес токена DLE
* @returns {Promise<Object>} - Результат удаления
*/
async deleteDLE(tokenAddress) {
try {
const response = await api.delete(`/dle/${tokenAddress}`);
return response.data;
} catch (error) {
console.error('Ошибка при удалении DLE:', error);
throw error;
}
}
/**
* Удаляет пустое DLE по имени файла
* @param {string} fileName - Имя файла DLE
* @returns {Promise<Object>} - Результат удаления
*/
async deleteEmptyDLE(fileName) {
try {
const response = await api.delete(`/dle/empty/${fileName}`);
return response.data;
} catch (error) {
console.error('Ошибка при удалении пустого DLE:', error);
throw error;
}
}
}
export default new DLEService();

View File

@@ -1,3 +1,15 @@
/**
* 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
*/
/**
* WebSocket сервис для реального времени обновлений
*/

View File

@@ -19,12 +19,7 @@
@auth-action-completed="$emit('auth-action-completed')"
>
<div class="crm-view-container">
<div class="dle-management-block">
<h2>Управление DLE</h2>
<button class="details-btn" @click="goToDleManagement">
Подробнее
</button>
</div>
<div class="crm-contacts-block">
<h2>Контакты</h2>
<button class="details-btn" @click="goToContactsList">
@@ -62,10 +57,8 @@ import { useRouter } from 'vue-router';
import { setToStorage } from '../utils/storage';
import BaseLayout from '../components/BaseLayout.vue';
import eventBus from '../utils/eventBus';
import dleService from '../services/dleService';
import ContactTable from '../components/ContactTable.vue';
import contactsService from '../services/contactsService.js';
import DleManagement from '../components/DleManagement.vue';
// Определяем props
const props = defineProps({
@@ -214,9 +207,7 @@ function goToTables() {
router.push({ name: 'tables-list' });
}
function goToDleManagement() {
router.push({ name: 'dle-management' });
}
function goToContactsList() {
router.push({ name: 'contacts-list' });
@@ -293,24 +284,7 @@ strong {
background: #5a6268;
}
.dle-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;
}
.dle-management-block h2 {
margin: 0;
font-size: 1.4rem;
font-weight: 600;
}
.dle-management-block .details-btn {
margin-top: 0;
}
.crm-contacts-block {
margin: 32px 0 24px 0;

View File

@@ -1,70 +0,0 @@
<!--
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>
<DleManagement
:dle-list="dleList"
:selected-dle-index="selectedDleIndex"
@close="goBack"
@dle-updated="reloadDleList"
class="dle-management-root"
/>
</BaseLayout>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import BaseLayout from '../components/BaseLayout.vue';
import DleManagement from '../components/DleManagement.vue';
import dleService from '../services/dleService';
const dleList = ref([]);
const selectedDleIndex = ref(0);
const router = useRouter();
function goBack() {
if (window.history.length > 1) {
router.back();
} else {
router.push({ name: 'crm' });
}
}
async function reloadDleList() {
dleList.value = await dleService.getAllDLEs() || [];
// Сбросить выбранный индекс, если список изменился
if (dleList.value.length === 0) {
selectedDleIndex.value = 0;
} else if (selectedDleIndex.value >= dleList.value.length) {
selectedDleIndex.value = 0;
}
}
onMounted(async () => {
await reloadDleList();
});
</script>
<style scoped>
.dle-management-root {
background: #fff;
border-radius: 16px;
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
padding: 32px 24px 24px 24px;
width: 100%;
margin-top: 40px;
position: relative;
overflow-x: auto;
}
</style>

View File

@@ -130,7 +130,7 @@ const openModules = () => {
};
const openDle = () => {
router.push('/management/dle');
router.push('/management/dle-management');
};
const openTreasury = () => {

View File

@@ -325,6 +325,20 @@
<small class="form-help">3-10 символов для токена управления (Governance Token)</small>
</div>
<!-- Координаты -->
<div class="form-group">
<label class="form-label" for="coordinates">Координаты (широта, долгота):</label>
<input
type="text"
id="coordinates"
v-model="dleSettings.coordinates"
class="form-control"
placeholder="Например: 55.7558,37.6176"
pattern="^-?\d+\.\d+,-?\d+\.\d+$"
>
<small class="form-help">Координаты в формате "широта,долгота" (например: 55.7558,37.6176)</small>
</div>
<!-- Партнеры и распределение токенов -->
<div class="partners-section">
<h4>Партнеры и распределение токенов</h4>
@@ -761,6 +775,11 @@
<strong>🏢 КПП:</strong> {{ selectedKppInfo.code }} - {{ selectedKppInfo.title }}
</div>
<!-- Координаты -->
<div v-if="dleSettings.coordinates" class="preview-item">
<strong>📍 Координаты:</strong> {{ dleSettings.coordinates }}
</div>
<!-- Кнопка деплоя смарт-контрактов -->
<div class="deploy-section">
<div class="deploy-buttons">
@@ -843,6 +862,7 @@ const dleSettings = reactive({
// Устаревшие поля (для совместимости)
deployNetwork: '', // Заменено на selectedNetworks
privateKey: '', // Заменено на privateKeys объект
coordinates: '', // Координаты для DLE
});
// Состояние UI (минимально необходимое)
@@ -2156,6 +2176,88 @@ const maskedPrivateKey = computed(() => {
const end = dleSettings.privateKey.substring(dleSettings.privateKey.length - 4);
return `${start}...${end}`;
});
// Функция деплоя смарт-контрактов DLE
const deploySmartContracts = async () => {
try {
// Валидация данных
if (!isFormValid.value) {
alert('Пожалуйста, заполните все обязательные поля');
return;
}
// Подготовка данных для деплоя
const deployData = {
// Основная информация DLE
name: dleSettings.name,
symbol: dleSettings.tokenSymbol,
location: dleSettings.addressData.fullAddress || 'Не указан',
coordinates: dleSettings.coordinates || '0,0',
jurisdiction: parseInt(dleSettings.jurisdiction) || 0,
oktmo: dleSettings.selectedOktmo || '',
okvedCodes: dleSettings.selectedOkved || [],
kpp: dleSettings.kppCode || '',
// Настройки кворума
quorumPercentage: dleSettings.governanceQuorum || 51,
// Партнеры и токены
initialPartners: dleSettings.partners.map(p => p.address).filter(addr => addr),
initialAmounts: dleSettings.partners.map(p => p.amount).filter(amount => amount > 0),
// Мульти-чейн настройки
supportedChainIds: dleSettings.selectedNetworks || [],
// Текущая цепочка (будет установлена при деплое)
currentChainId: dleSettings.selectedNetworks[0] || 1
};
console.log('Данные для деплоя DLE:', deployData);
// Вызов API для деплоя
const response = await axios.post('/api/dle-v2', deployData);
if (response.data.success) {
alert('✅ DLE успешно развернут!');
// Сохраняем адрес контракта
dleSettings.predictedAddress = response.data.data?.contractAddress || 'Адрес будет доступен после деплоя';
// Перенаправляем на страницу управления
router.push('/management/dle-management');
} else {
alert('❌ Ошибка при деплое: ' + response.data.error);
}
} catch (error) {
console.error('Ошибка деплоя DLE:', error);
alert('❌ Ошибка при деплое смарт-контракта: ' + error.message);
}
};
// Валидация формы
const isFormValid = computed(() => {
return (
dleSettings.jurisdiction &&
dleSettings.name &&
dleSettings.tokenSymbol ||
dleSettings.tokenStandard !== 'ERC20' ||
dleSettings.partners.length > 0 &&
dleSettings.partners.every(partner => partner.address && partner.amount > 0) &&
dleSettings.governanceQuorum > 0 &&
dleSettings.governanceQuorum <= 100 &&
dleSettings.selectedNetworks.length > 0 &&
// Валидация координат
validateCoordinates(dleSettings.coordinates)
);
});
// Функция валидации координат
const validateCoordinates = (coordinates) => {
if (!coordinates) return true; // Координаты не обязательны
const coordRegex = /^-?\d+\.\d+,-?\d+\.\d+$/;
return coordRegex.test(coordinates);
};
</script>
<style scoped>

View File

@@ -20,12 +20,7 @@
<div class="main-block">
<h3>Блокчейн</h3>
<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>
<button class="details-btn" @click="$router.push('/settings/dle-v2-deploy')">Подробнее</button>
</div>
<div class="main-block">
<h3>Безопасность</h3>

View File

@@ -1,499 +0,0 @@
<!--
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>

View File

@@ -0,0 +1,815 @@
<!--
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>
<div class="dle-modules-management">
<div class="modules-header">
<h3>🧩 Управление модулями</h3>
<button class="btn btn-primary" @click="showAddModuleForm = true">
<i class="fas fa-plus"></i> Добавить модуль
</button>
</div>
<!-- Форма добавления модуля -->
<div v-if="showAddModuleForm" class="add-module-form">
<div class="form-header">
<h4>🧩 Добавить модуль</h4>
<button class="close-btn" @click="showAddModuleForm = false">×</button>
</div>
<div class="form-content">
<!-- Информация о модуле -->
<div class="form-section">
<h5>📋 Информация о модуле</h5>
<div class="form-group">
<label for="moduleId">ID модуля:</label>
<input
type="text"
id="moduleId"
v-model="newModule.moduleId"
class="form-control"
placeholder="TreasuryModule"
>
<small class="form-help">Уникальный идентификатор модуля (например: TreasuryModule)</small>
</div>
<div class="form-group">
<label for="moduleAddress">Адрес модуля:</label>
<input
type="text"
id="moduleAddress"
v-model="newModule.moduleAddress"
class="form-control"
placeholder="0x..."
>
<small class="form-help">Адрес смарт-контракта модуля</small>
</div>
<div class="form-group">
<label for="moduleName">Название модуля:</label>
<input
type="text"
id="moduleName"
v-model="newModule.name"
class="form-control"
placeholder="Казначейство"
>
</div>
<div class="form-group">
<label for="moduleDescription">Описание модуля:</label>
<textarea
id="moduleDescription"
v-model="newModule.description"
class="form-control"
rows="3"
placeholder="Описание функциональности модуля..."
></textarea>
</div>
</div>
<!-- Выбор типа модуля -->
<div class="form-section">
<h5>🔧 Тип модуля</h5>
<div class="module-types">
<div class="form-group">
<label for="moduleType">Выберите тип модуля:</label>
<select id="moduleType" v-model="newModule.type" class="form-control">
<option value="">-- Выберите тип --</option>
<option value="treasury">Казначейство</option>
<option value="voting">Голосование</option>
<option value="communication">Коммуникации</option>
<option value="custom">Пользовательский</option>
</select>
</div>
<!-- Специфичные настройки для казначейства -->
<div v-if="newModule.type === 'treasury'" class="module-settings">
<h6>Настройки казначейства</h6>
<div class="form-group">
<label for="treasuryTokens">Токены для управления:</label>
<select id="treasuryTokens" v-model="newModule.settings.tokens" multiple class="form-control">
<option value="ETH">ETH</option>
<option value="USDT">USDT</option>
<option value="USDC">USDC</option>
<option value="DAI">DAI</option>
</select>
</div>
<div class="form-group">
<label for="treasuryLimit">Лимит операций:</label>
<input
type="number"
id="treasuryLimit"
v-model.number="newModule.settings.limit"
class="form-control"
placeholder="1000"
>
</div>
</div>
<!-- Специфичные настройки для голосования -->
<div v-if="newModule.type === 'voting'" class="module-settings">
<h6>Настройки голосования</h6>
<div class="form-group">
<label for="votingType">Тип голосования:</label>
<select id="votingType" v-model="newModule.settings.votingType" class="form-control">
<option value="simple">Простое большинство</option>
<option value="weighted">Взвешенное голосование</option>
<option value="quadratic">Квадратичное голосование</option>
</select>
</div>
<div class="form-group">
<label for="votingDuration">Длительность голосования (дни):</label>
<input
type="number"
id="votingDuration"
v-model.number="newModule.settings.duration"
class="form-control"
min="1"
max="30"
placeholder="7"
>
</div>
</div>
<!-- Специфичные настройки для коммуникаций -->
<div v-if="newModule.type === 'communication'" class="module-settings">
<h6>Настройки коммуникаций</h6>
<div class="form-group">
<label for="communicationChannels">Каналы связи:</label>
<div class="checkbox-group">
<label><input type="checkbox" v-model="newModule.settings.channels.email"> Email</label>
<label><input type="checkbox" v-model="newModule.settings.channels.telegram"> Telegram</label>
<label><input type="checkbox" v-model="newModule.settings.channels.discord"> Discord</label>
<label><input type="checkbox" v-model="newModule.settings.channels.slack"> Slack</label>
</div>
</div>
</div>
</div>
</div>
<!-- Предварительный просмотр -->
<div class="form-section">
<h5>👁 Предварительный просмотр</h5>
<div class="preview-card">
<div class="preview-item">
<strong>ID модуля:</strong> {{ newModule.moduleId || 'Не указан' }}
</div>
<div class="preview-item">
<strong>Адрес:</strong> {{ shortenAddress(newModule.moduleAddress) || 'Не указан' }}
</div>
<div class="preview-item">
<strong>Название:</strong> {{ newModule.name || 'Не указано' }}
</div>
<div class="preview-item">
<strong>Тип:</strong> {{ getModuleTypeName(newModule.type) || 'Не выбран' }}
</div>
<div class="preview-item">
<strong>Описание:</strong> {{ newModule.description || 'Не указано' }}
</div>
<div v-if="newModule.type" class="preview-item">
<strong>Настройки:</strong> {{ getModuleSettingsPreview() }}
</div>
</div>
</div>
<!-- Действия -->
<div class="form-actions">
<button
class="btn btn-success"
@click="addModule"
:disabled="!isFormValid || isAdding"
>
<i class="fas fa-plus"></i>
{{ isAdding ? 'Добавление...' : 'Добавить модуль' }}
</button>
<button class="btn btn-secondary" @click="resetForm">
<i class="fas fa-undo"></i> Сбросить
</button>
<button class="btn btn-danger" @click="showAddModuleForm = false">
<i class="fas fa-times"></i> Отмена
</button>
</div>
</div>
</div>
<!-- Список модулей -->
<div class="modules-list">
<div class="list-header">
<h4>📋 Установленные модули</h4>
<div class="list-filters">
<select v-model="typeFilter" class="form-control">
<option value="">Все типы</option>
<option value="treasury">Казначейство</option>
<option value="voting">Голосование</option>
<option value="communication">Коммуникации</option>
<option value="custom">Пользовательские</option>
</select>
</div>
</div>
<div v-if="filteredModules.length === 0" class="no-modules">
<p>Установленных модулей пока нет</p>
</div>
<div v-else class="modules-grid">
<div
v-for="module in filteredModules"
:key="module.moduleId"
class="module-card"
:class="module.type"
>
<div class="module-header">
<h5>{{ module.name }}</h5>
<span class="module-type">{{ getModuleTypeName(module.type) }}</span>
</div>
<div class="module-details">
<div class="detail-item">
<strong>ID:</strong> {{ module.moduleId }}
</div>
<div class="detail-item">
<strong>Адрес:</strong> {{ shortenAddress(module.moduleAddress) }}
</div>
<div class="detail-item">
<strong>Описание:</strong> {{ module.description }}
</div>
<div class="detail-item">
<strong>Статус:</strong>
<span class="module-status" :class="{ 'active': module.isActive }">
{{ module.isActive ? 'Активен' : 'Неактивен' }}
</span>
</div>
</div>
<div class="module-actions">
<button
class="btn btn-sm btn-info"
@click="viewModuleDetails(module.moduleId)"
>
<i class="fas fa-eye"></i> Детали
</button>
<button
class="btn btn-sm btn-warning"
@click="configureModule(module.moduleId)"
>
<i class="fas fa-cog"></i> Настроить
</button>
<button
class="btn btn-sm btn-danger"
@click="removeModule(module.moduleId)"
>
<i class="fas fa-trash"></i> Удалить
</button>
</div>
</div>
</div>
</div>
<!-- Доступные модули -->
<div class="available-modules">
<h4>📦 Доступные модули</h4>
<p>Модули, которые можно установить в вашем DLE</p>
<div class="available-modules-grid">
<div
v-for="availableModule in availableModules"
:key="availableModule.id"
class="available-module-card"
>
<div class="module-icon">
<i :class="availableModule.icon"></i>
</div>
<div class="module-info">
<h6>{{ availableModule.name }}</h6>
<p>{{ availableModule.description }}</p>
<div class="module-features">
<span v-for="feature in availableModule.features" :key="feature" class="feature-tag">
{{ feature }}
</span>
</div>
</div>
<div class="module-actions">
<button
class="btn btn-sm btn-success"
@click="installAvailableModule(availableModule)"
>
<i class="fas fa-download"></i> Установить
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useAuthContext } from '@/composables/useAuth';
const props = defineProps({
dleAddress: { type: String, required: true },
dleContract: { type: Object, required: true }
});
const { address } = useAuthContext();
// Состояние формы
const showAddModuleForm = ref(false);
const isAdding = ref(false);
const typeFilter = ref('');
// Новый модуль
const newModule = ref({
moduleId: '',
moduleAddress: '',
name: '',
description: '',
type: '',
settings: {
tokens: [],
limit: 0,
votingType: 'simple',
duration: 7,
channels: {
email: false,
telegram: false,
discord: false,
slack: false
}
}
});
// Установленные модули
const modules = ref([]);
// Доступные модули
const availableModules = ref([
{
id: 'treasury',
name: 'Казначейство',
description: 'Управление финансами DLE, прием и отправка токенов',
icon: 'fas fa-coins',
features: ['Управление токенами', 'Бюджетирование', 'Отчетность'],
type: 'treasury'
},
{
id: 'hierarchical-voting',
name: 'Иерархическое голосование',
description: 'Продвинутая система голосования с иерархией',
icon: 'fas fa-sitemap',
features: ['Иерархия', 'Взвешенное голосование', 'Делегирование'],
type: 'voting'
},
{
id: 'communication',
name: 'Коммуникации',
description: 'Система уведомлений и коммуникаций',
icon: 'fas fa-comments',
features: ['Уведомления', 'Каналы связи', 'Автоматизация'],
type: 'communication'
},
{
id: 'analytics',
name: 'Аналитика',
description: 'Аналитические инструменты для DLE',
icon: 'fas fa-chart-line',
features: ['Статистика', 'Графики', 'Отчеты'],
type: 'custom'
}
]);
// Вычисляемые свойства
const isFormValid = computed(() => {
return (
newModule.value.moduleId &&
newModule.value.moduleAddress &&
newModule.value.name &&
newModule.value.type
);
});
const filteredModules = computed(() => {
if (!typeFilter.value) return modules.value;
return modules.value.filter(m => m.type === typeFilter.value);
});
// Функции
function getModuleTypeName(type) {
const types = {
'treasury': 'Казначейство',
'voting': 'Голосование',
'communication': 'Коммуникации',
'custom': 'Пользовательский'
};
return types[type] || 'Неизвестный тип';
}
function getModuleSettingsPreview() {
const settings = newModule.value.settings;
switch (newModule.value.type) {
case 'treasury':
return `Токены: ${settings.tokens.join(', ') || 'Не выбраны'}, Лимит: ${settings.limit}`;
case 'voting':
return `Тип: ${settings.votingType}, Длительность: ${settings.duration} дней`;
case 'communication':
const channels = Object.entries(settings.channels)
.filter(([_, enabled]) => enabled)
.map(([name, _]) => name);
return `Каналы: ${channels.join(', ') || 'Не выбраны'}`;
default:
return 'Нет настроек';
}
}
function shortenAddress(address) {
if (!address) return '';
return `${address.slice(0, 6)}...${address.slice(-4)}`;
}
// Добавление модуля
async function addModule() {
if (!isFormValid.value) {
alert('Пожалуйста, заполните все обязательные поля');
return;
}
isAdding.value = true;
try {
// Вызов смарт-контракта
const tx = await props.dleContract.addModule(
newModule.value.moduleId,
newModule.value.moduleAddress
);
await tx.wait();
// Обновляем список модулей
await loadModules();
// Сбрасываем форму
resetForm();
showAddModuleForm.value = false;
alert('✅ Модуль успешно добавлен!');
} catch (error) {
console.error('Ошибка при добавлении модуля:', error);
alert('❌ Ошибка при добавлении модуля: ' + error.message);
} finally {
isAdding.value = false;
}
}
// Удаление модуля
async function removeModule(moduleId) {
if (!confirm(`Удалить модуль "${moduleId}"?`)) return;
try {
const tx = await props.dleContract.removeModule(moduleId);
await tx.wait();
await loadModules();
alert('✅ Модуль успешно удален!');
} catch (error) {
console.error('Ошибка при удалении модуля:', error);
alert('❌ Ошибка при удалении модуля: ' + error.message);
}
}
// Установка доступного модуля
async function installAvailableModule(availableModule) {
// Здесь должна быть логика установки модуля
// Например, деплой модуля и добавление в DLE
console.log('Установка модуля:', availableModule);
alert(`Модуль "${availableModule.name}" будет установлен`);
}
// Загрузка модулей
async function loadModules() {
try {
// Здесь должен быть вызов API или смарт-контракта для загрузки модулей
// Пока используем заглушку
modules.value = [];
} catch (error) {
console.error('Ошибка при загрузке модулей:', error);
}
}
function resetForm() {
newModule.value = {
moduleId: '',
moduleAddress: '',
name: '',
description: '',
type: '',
settings: {
tokens: [],
limit: 0,
votingType: 'simple',
duration: 7,
channels: {
email: false,
telegram: false,
discord: false,
slack: false
}
}
};
}
function viewModuleDetails(moduleId) {
// Открыть модальное окно с деталями модуля
console.log('Просмотр деталей модуля:', moduleId);
}
function configureModule(moduleId) {
// Открыть форму настройки модуля
console.log('Настройка модуля:', moduleId);
}
onMounted(() => {
loadModules();
});
</script>
<style scoped>
.dle-modules-management {
padding: 1rem;
}
.modules-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.add-module-form {
background: #f8f9fa;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 2rem;
}
.form-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #666;
}
.form-section {
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid #eee;
}
.form-section:last-child {
border-bottom: none;
}
.form-section h5 {
color: #333;
margin-bottom: 1rem;
}
.module-types {
margin-top: 1rem;
}
.module-settings {
margin-top: 1rem;
padding: 1rem;
background: #f8f9fa;
border-radius: 6px;
}
.module-settings h6 {
color: #333;
margin-bottom: 1rem;
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.checkbox-group label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
}
.preview-card {
background: #fff;
border: 1px solid #e9ecef;
border-radius: 6px;
padding: 1rem;
}
.preview-item {
margin-bottom: 0.5rem;
}
.form-actions {
display: flex;
gap: 1rem;
margin-top: 1.5rem;
}
.modules-list {
margin-top: 2rem;
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.modules-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem;
}
.module-card {
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 1rem;
background: #fff;
}
.module-card.treasury {
border-left: 4px solid #28a745;
}
.module-card.voting {
border-left: 4px solid #007bff;
}
.module-card.communication {
border-left: 4px solid #ffc107;
}
.module-card.custom {
border-left: 4px solid #6c757d;
}
.module-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.module-header h5 {
margin: 0;
color: #333;
}
.module-type {
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 600;
background: #e9ecef;
color: #495057;
}
.module-details {
margin-bottom: 1rem;
}
.detail-item {
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.module-status {
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 600;
}
.module-status.active {
background: #d4edda;
color: #155724;
}
.module-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.no-modules {
text-align: center;
padding: 2rem;
color: #666;
}
.available-modules {
margin-top: 3rem;
}
.available-modules h4 {
margin-bottom: 1rem;
}
.available-modules p {
color: #666;
margin-bottom: 2rem;
}
.available-modules-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
.available-module-card {
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 1rem;
background: #fff;
display: flex;
align-items: center;
gap: 1rem;
}
.module-icon {
width: 50px;
height: 50px;
background: #f8f9fa;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #007bff;
font-size: 1.5rem;
flex-shrink: 0;
}
.module-info {
flex-grow: 1;
}
.module-info h6 {
margin: 0 0 0.5rem 0;
color: #333;
}
.module-info p {
margin: 0 0 0.5rem 0;
font-size: 0.9rem;
color: #666;
}
.module-features {
display: flex;
gap: 0.25rem;
flex-wrap: wrap;
}
.feature-tag {
background: #e9ecef;
color: #495057;
padding: 0.125rem 0.5rem;
border-radius: 12px;
font-size: 0.75rem;
}
.form-help {
font-size: 0.9rem;
color: #666;
margin-top: 0.25rem;
}
</style>

View File

@@ -0,0 +1,711 @@
<!--
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>
<div class="dle-multisig-management">
<div class="multisig-header">
<h3>🔐 Управление мультиподписью</h3>
<button class="btn btn-primary" @click="showCreateForm = true">
<i class="fas fa-plus"></i> Создать операцию
</button>
</div>
<!-- Форма создания мультиподписи -->
<div v-if="showCreateForm" class="create-multisig-form">
<div class="form-header">
<h4>🔐 Новая мультиподпись</h4>
<button class="close-btn" @click="showCreateForm = false">×</button>
</div>
<div class="form-content">
<!-- Описание операции -->
<div class="form-section">
<h5>📝 Описание операции</h5>
<div class="form-group">
<label for="operationDescription">Описание операции:</label>
<textarea
id="operationDescription"
v-model="newOperation.description"
class="form-control"
rows="3"
placeholder="Опишите, что нужно сделать..."
></textarea>
</div>
<div class="form-group">
<label for="operationDuration">Длительность сбора подписей (дни):</label>
<input
type="number"
id="operationDuration"
v-model.number="newOperation.duration"
class="form-control"
min="1"
max="30"
placeholder="7"
>
</div>
</div>
<!-- Тип операции -->
<div class="form-section">
<h5> Тип операции</h5>
<div class="operation-types">
<div class="form-group">
<label for="multisigOperationType">Выберите тип операции:</label>
<select id="multisigOperationType" v-model="newOperation.operationType" class="form-control">
<option value="">-- Выберите тип --</option>
<option value="transfer">Передача токенов</option>
<option value="mint">Минтинг токенов</option>
<option value="burn">Сжигание токенов</option>
<option value="addModule">Добавить модуль</option>
<option value="removeModule">Удалить модуль</option>
<option value="custom">Пользовательская операция</option>
</select>
</div>
<!-- Параметры для передачи токенов -->
<div v-if="newOperation.operationType === 'transfer'" class="operation-params">
<div class="form-group">
<label for="multisigTransferTo">Адрес получателя:</label>
<input
type="text"
id="multisigTransferTo"
v-model="newOperation.operationParams.to"
class="form-control"
placeholder="0x..."
>
</div>
<div class="form-group">
<label for="multisigTransferAmount">Количество токенов:</label>
<input
type="number"
id="multisigTransferAmount"
v-model.number="newOperation.operationParams.amount"
class="form-control"
min="1"
placeholder="100"
>
</div>
</div>
<!-- Параметры для модулей -->
<div v-if="newOperation.operationType === 'addModule' || newOperation.operationType === 'removeModule'" class="operation-params">
<div class="form-group">
<label for="moduleId">ID модуля:</label>
<input
type="text"
id="moduleId"
v-model="newOperation.operationParams.moduleId"
class="form-control"
placeholder="TreasuryModule"
>
</div>
<div v-if="newOperation.operationType === 'addModule'" class="form-group">
<label for="moduleAddress">Адрес модуля:</label>
<input
type="text"
id="moduleAddress"
v-model="newOperation.operationParams.moduleAddress"
class="form-control"
placeholder="0x..."
>
</div>
</div>
<!-- Пользовательская операция -->
<div v-if="newOperation.operationType === 'custom'" class="operation-params">
<div class="form-group">
<label for="customMultisigOperation">Пользовательская операция (hex):</label>
<textarea
id="customMultisigOperation"
v-model="newOperation.operationParams.customData"
class="form-control"
rows="3"
placeholder="0x..."
></textarea>
</div>
</div>
</div>
</div>
<!-- Предварительный просмотр -->
<div class="form-section">
<h5>👁 Предварительный просмотр</h5>
<div class="preview-card">
<div class="preview-item">
<strong>Описание:</strong> {{ newOperation.description || 'Не указано' }}
</div>
<div class="preview-item">
<strong>Длительность:</strong> {{ newOperation.duration || 7 }} дней
</div>
<div class="preview-item">
<strong>Тип операции:</strong> {{ getOperationTypeName(newOperation.operationType) || 'Не выбран' }}
</div>
<div v-if="newOperation.operationType" class="preview-item">
<strong>Параметры:</strong> {{ getOperationParamsPreview() }}
</div>
<div class="preview-item">
<strong>Хеш операции:</strong> {{ getOperationHash() }}
</div>
</div>
</div>
<!-- Действия -->
<div class="form-actions">
<button
class="btn btn-success"
@click="createMultisigOperation"
:disabled="!isFormValid || isCreating"
>
<i class="fas fa-paper-plane"></i>
{{ isCreating ? 'Создание...' : 'Создать операцию' }}
</button>
<button class="btn btn-secondary" @click="resetForm">
<i class="fas fa-undo"></i> Сбросить
</button>
<button class="btn btn-danger" @click="showCreateForm = false">
<i class="fas fa-times"></i> Отмена
</button>
</div>
</div>
</div>
<!-- Список операций мультиподписи -->
<div class="multisig-list">
<div class="list-header">
<h4>📋 Список операций мультиподписи</h4>
<div class="list-filters">
<select v-model="statusFilter" class="form-control">
<option value="">Все статусы</option>
<option value="active">Активные</option>
<option value="pending">Ожидающие</option>
<option value="succeeded">Принятые</option>
<option value="defeated">Отклоненные</option>
<option value="executed">Выполненные</option>
</select>
</div>
</div>
<div v-if="filteredOperations.length === 0" class="no-operations">
<p>Операций мультиподписи пока нет</p>
</div>
<div v-else class="operations-grid">
<div
v-for="operation in filteredOperations"
:key="operation.id"
class="operation-card"
:class="operation.status"
>
<div class="operation-header">
<h5>{{ operation.description }}</h5>
<span class="operation-status" :class="operation.status">
{{ getOperationStatusText(operation.status) }}
</span>
</div>
<div class="operation-details">
<div class="detail-item">
<strong>ID:</strong> #{{ operation.id }}
</div>
<div class="detail-item">
<strong>Создатель:</strong> {{ shortenAddress(operation.initiator) }}
</div>
<div class="detail-item">
<strong>Хеш:</strong> {{ shortenAddress(operation.operationHash) }}
</div>
<div class="detail-item">
<strong>Дедлайн:</strong> {{ formatDate(operation.deadline) }}
</div>
<div class="detail-item">
<strong>Подписи:</strong>
<span class="signatures">
<span class="for">За: {{ operation.forSignatures }}</span>
<span class="against">Против: {{ operation.againstSignatures }}</span>
</span>
</div>
</div>
<div class="operation-actions">
<button
v-if="canSign(operation)"
class="btn btn-sm btn-success"
@click="signOperation(operation.id, true)"
:disabled="hasSigned(operation.id, true)"
>
<i class="fas fa-thumbs-up"></i> Подписать за
</button>
<button
v-if="canSign(operation)"
class="btn btn-sm btn-danger"
@click="signOperation(operation.id, false)"
:disabled="hasSigned(operation.id, false)"
>
<i class="fas fa-thumbs-down"></i> Подписать против
</button>
<button
v-if="canExecute(operation)"
class="btn btn-sm btn-primary"
@click="executeOperation(operation.id)"
>
<i class="fas fa-play"></i> Исполнить
</button>
<button
class="btn btn-sm btn-info"
@click="viewOperationDetails(operation.id)"
>
<i class="fas fa-eye"></i> Детали
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useAuthContext } from '@/composables/useAuth';
const props = defineProps({
dleAddress: { type: String, required: true },
dleContract: { type: Object, required: true }
});
const { address } = useAuthContext();
// Состояние формы
const showCreateForm = ref(false);
const isCreating = ref(false);
const statusFilter = ref('');
// Новая операция
const newOperation = ref({
description: '',
duration: 7,
operationType: '',
operationParams: {
to: '',
from: '',
amount: 0,
moduleId: '',
moduleAddress: '',
customData: ''
}
});
// Операции мультиподписи
const operations = ref([]);
// Вычисляемые свойства
const isFormValid = computed(() => {
return (
newOperation.value.description &&
newOperation.value.duration > 0 &&
newOperation.value.operationType &&
validateOperationParams()
);
});
const filteredOperations = computed(() => {
if (!statusFilter.value) return operations.value;
return operations.value.filter(o => o.status === statusFilter.value);
});
// Функции
function validateOperationParams() {
const params = newOperation.value.operationParams;
switch (newOperation.value.operationType) {
case 'transfer':
case 'mint':
return params.to && params.amount > 0;
case 'burn':
return params.from && params.amount > 0;
case 'addModule':
return params.moduleId && params.moduleAddress;
case 'removeModule':
return params.moduleId;
case 'custom':
return params.customData && params.customData.startsWith('0x');
default:
return false;
}
}
function getOperationTypeName(type) {
const types = {
'transfer': 'Передача токенов',
'mint': 'Минтинг токенов',
'burn': 'Сжигание токенов',
'addModule': 'Добавить модуль',
'removeModule': 'Удалить модуль',
'custom': 'Пользовательская операция'
};
return types[type] || 'Неизвестный тип';
}
function getOperationParamsPreview() {
const params = newOperation.value.operationParams;
switch (newOperation.value.operationType) {
case 'transfer':
return `Кому: ${shortenAddress(params.to)}, Количество: ${params.amount}`;
case 'mint':
return `Кому: ${shortenAddress(params.to)}, Количество: ${params.amount}`;
case 'burn':
return `От: ${shortenAddress(params.from)}, Количество: ${params.amount}`;
case 'addModule':
return `ID: ${params.moduleId}, Адрес: ${shortenAddress(params.moduleAddress)}`;
case 'removeModule':
return `ID: ${params.moduleId}`;
case 'custom':
return `Данные: ${params.customData.substring(0, 20)}...`;
default:
return 'Не указаны';
}
}
function getOperationHash() {
// Генерируем хеш операции на основе параметров
const params = newOperation.value.operationParams;
const operationData = JSON.stringify({
type: newOperation.value.operationType,
params: params
});
// Простой хеш для демонстрации
return '0x' + btoa(operationData).substring(0, 64);
}
function shortenAddress(address) {
if (!address) return '';
return `${address.slice(0, 6)}...${address.slice(-4)}`;
}
function formatDate(timestamp) {
if (!timestamp) return 'N/A';
return new Date(timestamp * 1000).toLocaleString();
}
function getOperationStatusText(status) {
const statusMap = {
'pending': 'Ожидает',
'active': 'Активно',
'succeeded': 'Принято',
'defeated': 'Отклонено',
'executed': 'Выполнено'
};
return statusMap[status] || status;
}
function canSign(operation) {
return operation.status === 'active' && !hasSigned(operation.id);
}
function canExecute(operation) {
return operation.status === 'succeeded' && !operation.executed;
}
function hasSigned(operationId, support = null) {
// Здесь должна быть проверка подписи пользователя
return false;
}
// Создание операции мультиподписи
async function createMultisigOperation() {
if (!isFormValid.value) {
alert('Пожалуйста, заполните все обязательные поля');
return;
}
isCreating.value = true;
try {
// Генерируем хеш операции
const operationHash = getOperationHash();
// Вызов смарт-контракта
const tx = await props.dleContract.createMultiSigOperation(
operationHash,
newOperation.value.duration * 24 * 60 * 60 // конвертируем в секунды
);
await tx.wait();
// Обновляем список операций
await loadOperations();
// Сбрасываем форму
resetForm();
showCreateForm.value = false;
alert('✅ Операция мультиподписи успешно создана!');
} catch (error) {
console.error('Ошибка при создании операции мультиподписи:', error);
alert('❌ Ошибка при создании операции: ' + error.message);
} finally {
isCreating.value = false;
}
}
// Подписание операции
async function signOperation(operationId, support) {
try {
const tx = await props.dleContract.signMultiSigOperation(operationId, support);
await tx.wait();
await loadOperations();
alert('✅ Ваша подпись учтена!');
} catch (error) {
console.error('Ошибка при подписании операции:', error);
alert('❌ Ошибка при подписании: ' + error.message);
}
}
// Исполнение операции
async function executeOperation(operationId) {
try {
const tx = await props.dleContract.executeMultiSigOperation(operationId);
await tx.wait();
await loadOperations();
alert('✅ Операция успешно исполнена!');
} catch (error) {
console.error('Ошибка при исполнении операции:', error);
alert('❌ Ошибка при исполнении операции: ' + error.message);
}
}
// Загрузка операций
async function loadOperations() {
try {
// Здесь должен быть вызов API или смарт-контракта для загрузки операций
// Пока используем заглушку
operations.value = [];
} catch (error) {
console.error('Ошибка при загрузке операций:', error);
}
}
function resetForm() {
newOperation.value = {
description: '',
duration: 7,
operationType: '',
operationParams: {
to: '',
from: '',
amount: 0,
moduleId: '',
moduleAddress: '',
customData: ''
}
};
}
function viewOperationDetails(operationId) {
// Открыть модальное окно с деталями операции
console.log('Просмотр деталей операции:', operationId);
}
onMounted(() => {
loadOperations();
});
</script>
<style scoped>
.dle-multisig-management {
padding: 1rem;
}
.multisig-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.create-multisig-form {
background: #f8f9fa;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 2rem;
}
.form-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #666;
}
.form-section {
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid #eee;
}
.form-section:last-child {
border-bottom: none;
}
.form-section h5 {
color: #333;
margin-bottom: 1rem;
}
.operation-types {
margin-top: 1rem;
}
.operation-params {
margin-top: 1rem;
padding: 1rem;
background: #f8f9fa;
border-radius: 6px;
}
.preview-card {
background: #fff;
border: 1px solid #e9ecef;
border-radius: 6px;
padding: 1rem;
}
.preview-item {
margin-bottom: 0.5rem;
}
.form-actions {
display: flex;
gap: 1rem;
margin-top: 1.5rem;
}
.multisig-list {
margin-top: 2rem;
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.operations-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem;
}
.operation-card {
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 1rem;
background: #fff;
}
.operation-card.active {
border-color: #28a745;
}
.operation-card.succeeded {
border-color: #007bff;
}
.operation-card.defeated {
border-color: #dc3545;
}
.operation-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.operation-header h5 {
margin: 0;
color: #333;
}
.operation-status {
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 600;
}
.operation-status.active {
background: #d4edda;
color: #155724;
}
.operation-status.succeeded {
background: #d1ecf1;
color: #0c5460;
}
.operation-status.defeated {
background: #f8d7da;
color: #721c24;
}
.operation-details {
margin-bottom: 1rem;
}
.detail-item {
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.signatures {
display: flex;
gap: 1rem;
}
.signatures .for {
color: #28a745;
}
.signatures .against {
color: #dc3545;
}
.operation-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.no-operations {
text-align: center;
padding: 2rem;
color: #666;
}
</style>

View File

@@ -0,0 +1,844 @@
<!--
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>
<div class="dle-proposals-management">
<div class="proposals-header">
<h3>🗳 Управление предложениями</h3>
<button class="btn btn-primary" @click="showCreateForm = true">
<i class="fas fa-plus"></i> Создать предложение
</button>
</div>
<!-- Форма создания предложения -->
<div v-if="showCreateForm" class="create-proposal-form">
<div class="form-header">
<h4>📝 Новое предложение</h4>
<button class="close-btn" @click="showCreateForm = false">×</button>
</div>
<div class="form-content">
<!-- Основная информация -->
<div class="form-section">
<h5>📋 Основная информация</h5>
<div class="form-group">
<label for="proposalDescription">Описание предложения:</label>
<textarea
id="proposalDescription"
v-model="newProposal.description"
class="form-control"
rows="3"
placeholder="Опишите, что нужно сделать..."
></textarea>
</div>
<div class="form-group">
<label for="proposalDuration">Длительность голосования (дни):</label>
<input
type="number"
id="proposalDuration"
v-model.number="newProposal.duration"
class="form-control"
min="1"
max="30"
placeholder="7"
>
</div>
</div>
<!-- Выбор цепочки для кворума -->
<div class="form-section">
<h5>🔗 Выбор цепочки для кворума</h5>
<p class="form-help">Выберите цепочку, в которой будет происходить сбор голосов</p>
<div class="chains-grid">
<div
v-for="chain in availableChains"
:key="chain.chainId"
class="chain-option"
:class="{ 'selected': newProposal.governanceChainId === chain.chainId }"
@click="newProposal.governanceChainId = chain.chainId"
>
<div class="chain-info">
<h6>{{ chain.name }}</h6>
<span class="chain-id">Chain ID: {{ chain.chainId }}</span>
<p class="chain-description">{{ chain.description }}</p>
</div>
<div class="chain-status">
<i v-if="newProposal.governanceChainId === chain.chainId" class="fas fa-check"></i>
</div>
</div>
</div>
</div>
<!-- Тип операции -->
<div class="form-section">
<h5> Тип операции</h5>
<div class="operation-types">
<div class="form-group">
<label for="operationType">Выберите тип операции:</label>
<select id="operationType" v-model="newProposal.operationType" class="form-control">
<option value="">-- Выберите тип --</option>
<option value="transfer">Передача токенов</option>
<option value="mint">Минтинг токенов</option>
<option value="burn">Сжигание токенов</option>
<option value="custom">Пользовательская операция</option>
</select>
</div>
<!-- Параметры для передачи токенов -->
<div v-if="newProposal.operationType === 'transfer'" class="operation-params">
<div class="form-group">
<label for="transferTo">Адрес получателя:</label>
<input
type="text"
id="transferTo"
v-model="newProposal.operationParams.to"
class="form-control"
placeholder="0x..."
>
</div>
<div class="form-group">
<label for="transferAmount">Количество токенов:</label>
<input
type="number"
id="transferAmount"
v-model.number="newProposal.operationParams.amount"
class="form-control"
min="1"
placeholder="100"
>
</div>
</div>
<!-- Параметры для минтинга -->
<div v-if="newProposal.operationType === 'mint'" class="operation-params">
<div class="form-group">
<label for="mintTo">Адрес получателя:</label>
<input
type="text"
id="mintTo"
v-model="newProposal.operationParams.to"
class="form-control"
placeholder="0x..."
>
</div>
<div class="form-group">
<label for="mintAmount">Количество токенов:</label>
<input
type="number"
id="mintAmount"
v-model.number="newProposal.operationParams.amount"
class="form-control"
min="1"
placeholder="1000"
>
</div>
</div>
<!-- Параметры для сжигания -->
<div v-if="newProposal.operationType === 'burn'" class="operation-params">
<div class="form-group">
<label for="burnFrom">Адрес владельца:</label>
<input
type="text"
id="burnFrom"
v-model="newProposal.operationParams.from"
class="form-control"
placeholder="0x..."
>
</div>
<div class="form-group">
<label for="burnAmount">Количество токенов:</label>
<input
type="number"
id="burnAmount"
v-model.number="newProposal.operationParams.amount"
class="form-control"
min="1"
placeholder="100"
>
</div>
</div>
<!-- Пользовательская операция -->
<div v-if="newProposal.operationType === 'custom'" class="operation-params">
<div class="form-group">
<label for="customOperation">Пользовательская операция (hex):</label>
<textarea
id="customOperation"
v-model="newProposal.operationParams.customData"
class="form-control"
rows="3"
placeholder="0x..."
></textarea>
</div>
</div>
</div>
</div>
<!-- Предварительный просмотр -->
<div class="form-section">
<h5>👁 Предварительный просмотр</h5>
<div class="preview-card">
<div class="preview-item">
<strong>Описание:</strong> {{ newProposal.description || 'Не указано' }}
</div>
<div class="preview-item">
<strong>Длительность:</strong> {{ newProposal.duration || 7 }} дней
</div>
<div class="preview-item">
<strong>Цепочка для кворума:</strong>
{{ getChainName(newProposal.governanceChainId) || 'Не выбрана' }}
</div>
<div class="preview-item">
<strong>Тип операции:</strong> {{ getOperationTypeName(newProposal.operationType) || 'Не выбран' }}
</div>
<div v-if="newProposal.operationType" class="preview-item">
<strong>Параметры:</strong> {{ getOperationParamsPreview() }}
</div>
</div>
</div>
<!-- Действия -->
<div class="form-actions">
<button
class="btn btn-success"
@click="createProposal"
:disabled="!isFormValid || isCreating"
>
<i class="fas fa-paper-plane"></i>
{{ isCreating ? 'Создание...' : 'Создать предложение' }}
</button>
<button class="btn btn-secondary" @click="resetForm">
<i class="fas fa-undo"></i> Сбросить
</button>
<button class="btn btn-danger" @click="showCreateForm = false">
<i class="fas fa-times"></i> Отмена
</button>
</div>
</div>
</div>
<!-- Список предложений -->
<div class="proposals-list">
<div class="list-header">
<h4>📋 Список предложений</h4>
<div class="list-filters">
<select v-model="statusFilter" class="form-control">
<option value="">Все статусы</option>
<option value="active">Активные</option>
<option value="pending">Ожидающие</option>
<option value="succeeded">Принятые</option>
<option value="defeated">Отклоненные</option>
<option value="executed">Выполненные</option>
</select>
</div>
</div>
<div v-if="filteredProposals.length === 0" class="no-proposals">
<p>Предложений пока нет</p>
</div>
<div v-else class="proposals-grid">
<div
v-for="proposal in filteredProposals"
:key="proposal.id"
class="proposal-card"
:class="proposal.status"
>
<div class="proposal-header">
<h5>{{ proposal.description }}</h5>
<span class="proposal-status" :class="proposal.status">
{{ getProposalStatusText(proposal.status) }}
</span>
</div>
<div class="proposal-details">
<div class="detail-item">
<strong>ID:</strong> #{{ proposal.id }}
</div>
<div class="detail-item">
<strong>Создатель:</strong> {{ shortenAddress(proposal.initiator) }}
</div>
<div class="detail-item">
<strong>Цепочка:</strong> {{ getChainName(proposal.governanceChainId) }}
</div>
<div class="detail-item">
<strong>Дедлайн:</strong> {{ formatDate(proposal.deadline) }}
</div>
<div class="detail-item">
<strong>Голоса:</strong>
<span class="votes">
<span class="for">За: {{ proposal.forVotes }}</span>
<span class="against">Против: {{ proposal.againstVotes }}</span>
</span>
</div>
</div>
<div class="proposal-actions">
<button
v-if="canVote(proposal)"
class="btn btn-sm btn-success"
@click="voteForProposal(proposal.id, true)"
:disabled="hasVoted(proposal.id, true)"
>
<i class="fas fa-thumbs-up"></i> За
</button>
<button
v-if="canVote(proposal)"
class="btn btn-sm btn-danger"
@click="voteForProposal(proposal.id, false)"
:disabled="hasVoted(proposal.id, false)"
>
<i class="fas fa-thumbs-down"></i> Против
</button>
<button
v-if="canExecute(proposal)"
class="btn btn-sm btn-primary"
@click="executeProposal(proposal.id)"
>
<i class="fas fa-play"></i> Исполнить
</button>
<button
class="btn btn-sm btn-info"
@click="viewProposalDetails(proposal.id)"
>
<i class="fas fa-eye"></i> Детали
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useAuthContext } from '@/composables/useAuth';
const props = defineProps({
dleAddress: { type: String, required: true },
dleContract: { type: Object, required: true }
});
const { address } = useAuthContext();
// Состояние формы
const showCreateForm = ref(false);
const isCreating = ref(false);
const statusFilter = ref('');
// Новое предложение
const newProposal = ref({
description: '',
duration: 7,
governanceChainId: null,
operationType: '',
operationParams: {
to: '',
from: '',
amount: 0,
customData: ''
}
});
// Доступные цепочки
const availableChains = ref([
{ chainId: 1, name: 'Ethereum', description: 'Основная сеть Ethereum' },
{ chainId: 137, name: 'Polygon', description: 'Сеть Polygon' },
{ chainId: 56, name: 'BSC', description: 'Binance Smart Chain' },
{ chainId: 42161, name: 'Arbitrum', description: 'Arbitrum One' }
]);
// Предложения
const proposals = ref([]);
// Вычисляемые свойства
const isFormValid = computed(() => {
return (
newProposal.value.description &&
newProposal.value.duration > 0 &&
newProposal.value.governanceChainId &&
newProposal.value.operationType &&
validateOperationParams()
);
});
const filteredProposals = computed(() => {
if (!statusFilter.value) return proposals.value;
return proposals.value.filter(p => p.status === statusFilter.value);
});
// Функции
function validateOperationParams() {
const params = newProposal.value.operationParams;
switch (newProposal.value.operationType) {
case 'transfer':
case 'mint':
return params.to && params.amount > 0;
case 'burn':
return params.from && params.amount > 0;
case 'custom':
return params.customData && params.customData.startsWith('0x');
default:
return false;
}
}
function getChainName(chainId) {
const chain = availableChains.value.find(c => c.chainId === chainId);
return chain ? chain.name : 'Неизвестная сеть';
}
function getOperationTypeName(type) {
const types = {
'transfer': 'Передача токенов',
'mint': 'Минтинг токенов',
'burn': 'Сжигание токенов',
'custom': 'Пользовательская операция'
};
return types[type] || 'Неизвестный тип';
}
function getOperationParamsPreview() {
const params = newProposal.value.operationParams;
switch (newProposal.value.operationType) {
case 'transfer':
return `Кому: ${shortenAddress(params.to)}, Количество: ${params.amount}`;
case 'mint':
return `Кому: ${shortenAddress(params.to)}, Количество: ${params.amount}`;
case 'burn':
return `От: ${shortenAddress(params.from)}, Количество: ${params.amount}`;
case 'custom':
return `Данные: ${params.customData.substring(0, 20)}...`;
default:
return 'Не указаны';
}
}
function shortenAddress(address) {
if (!address) return '';
return `${address.slice(0, 6)}...${address.slice(-4)}`;
}
function formatDate(timestamp) {
if (!timestamp) return 'N/A';
return new Date(timestamp * 1000).toLocaleString();
}
function getProposalStatusText(status) {
const statusMap = {
'pending': 'Ожидает',
'active': 'Активно',
'succeeded': 'Принято',
'defeated': 'Отклонено',
'executed': 'Выполнено'
};
return statusMap[status] || status;
}
function canVote(proposal) {
return proposal.status === 'active' && !hasVoted(proposal.id);
}
function canExecute(proposal) {
return proposal.status === 'succeeded' && !proposal.executed;
}
function hasVoted(proposalId, support = null) {
// Здесь должна быть проверка голосования пользователя
return false;
}
// Создание предложения
async function createProposal() {
if (!isFormValid.value) {
alert('Пожалуйста, заполните все обязательные поля');
return;
}
isCreating.value = true;
try {
// Подготовка данных для смарт-контракта
const operation = encodeOperation();
// Вызов смарт-контракта
const tx = await props.dleContract.createProposal(
newProposal.value.description,
newProposal.value.duration * 24 * 60 * 60, // конвертируем в секунды
operation,
newProposal.value.governanceChainId
);
await tx.wait();
// Обновляем список предложений
await loadProposals();
// Сбрасываем форму
resetForm();
showCreateForm.value = false;
alert('✅ Предложение успешно создано!');
} catch (error) {
console.error('Ошибка при создании предложения:', error);
alert('❌ Ошибка при создании предложения: ' + error.message);
} finally {
isCreating.value = false;
}
}
function encodeOperation() {
const params = newProposal.value.operationParams;
switch (newProposal.value.operationType) {
case 'transfer':
return encodeTransferOperation(params.to, params.amount);
case 'mint':
return encodeMintOperation(params.to, params.amount);
case 'burn':
return encodeBurnOperation(params.from, params.amount);
case 'custom':
return params.customData;
default:
throw new Error('Неизвестный тип операции');
}
}
function encodeTransferOperation(to, amount) {
// Кодируем операцию передачи токенов
const abiCoder = new ethers.AbiCoder();
const selector = '0xa9059cbb'; // transfer(address,uint256)
const data = abiCoder.encode(['address', 'uint256'], [to, amount]);
return selector + data.slice(2);
}
function encodeMintOperation(to, amount) {
// Кодируем операцию минтинга токенов
const abiCoder = new ethers.AbiCoder();
const selector = '0x40c10f19'; // mint(address,uint256)
const data = abiCoder.encode(['address', 'uint256'], [to, amount]);
return selector + data.slice(2);
}
function encodeBurnOperation(from, amount) {
// Кодируем операцию сжигания токенов
const abiCoder = new ethers.AbiCoder();
const selector = '0x42966c68'; // burn(uint256)
const data = abiCoder.encode(['uint256'], [amount]);
return selector + data.slice(2);
}
// Голосование
async function voteForProposal(proposalId, support) {
try {
const tx = await props.dleContract.vote(proposalId, support);
await tx.wait();
await loadProposals();
alert('✅ Ваш голос учтен!');
} catch (error) {
console.error('Ошибка при голосовании:', error);
alert('❌ Ошибка при голосовании: ' + error.message);
}
}
// Исполнение предложения
async function executeProposal(proposalId) {
try {
const tx = await props.dleContract.executeProposal(proposalId);
await tx.wait();
await loadProposals();
alert('✅ Предложение успешно исполнено!');
} catch (error) {
console.error('Ошибка при исполнении предложения:', error);
alert('❌ Ошибка при исполнении предложения: ' + error.message);
}
}
// Загрузка предложений
async function loadProposals() {
try {
// Здесь должен быть вызов API или смарт-контракта для загрузки предложений
// Пока используем заглушку
proposals.value = [];
} catch (error) {
console.error('Ошибка при загрузке предложений:', error);
}
}
function resetForm() {
newProposal.value = {
description: '',
duration: 7,
governanceChainId: null,
operationType: '',
operationParams: {
to: '',
from: '',
amount: 0,
customData: ''
}
};
}
function viewProposalDetails(proposalId) {
// Открыть модальное окно с деталями предложения
console.log('Просмотр деталей предложения:', proposalId);
}
onMounted(() => {
loadProposals();
});
</script>
<style scoped>
.dle-proposals-management {
padding: 1rem;
}
.proposals-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.create-proposal-form {
background: #f8f9fa;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 2rem;
}
.form-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #666;
}
.form-section {
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid #eee;
}
.form-section:last-child {
border-bottom: none;
}
.form-section h5 {
color: #333;
margin-bottom: 1rem;
}
.chains-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
.chain-option {
border: 2px solid #e9ecef;
border-radius: 8px;
padding: 1rem;
cursor: pointer;
transition: all 0.3s ease;
}
.chain-option:hover {
border-color: #007bff;
}
.chain-option.selected {
border-color: #007bff;
background: #f8f9ff;
}
.chain-info h6 {
margin: 0 0 0.5rem 0;
color: #333;
}
.chain-id {
font-size: 0.9rem;
color: #666;
}
.chain-description {
font-size: 0.9rem;
color: #888;
margin: 0.5rem 0 0 0;
}
.chain-status {
text-align: right;
color: #007bff;
}
.operation-types {
margin-top: 1rem;
}
.operation-params {
margin-top: 1rem;
padding: 1rem;
background: #f8f9fa;
border-radius: 6px;
}
.preview-card {
background: #fff;
border: 1px solid #e9ecef;
border-radius: 6px;
padding: 1rem;
}
.preview-item {
margin-bottom: 0.5rem;
}
.form-actions {
display: flex;
gap: 1rem;
margin-top: 1.5rem;
}
.proposals-list {
margin-top: 2rem;
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.proposals-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem;
}
.proposal-card {
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 1rem;
background: #fff;
}
.proposal-card.active {
border-color: #28a745;
}
.proposal-card.succeeded {
border-color: #007bff;
}
.proposal-card.defeated {
border-color: #dc3545;
}
.proposal-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.proposal-header h5 {
margin: 0;
color: #333;
}
.proposal-status {
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 600;
}
.proposal-status.active {
background: #d4edda;
color: #155724;
}
.proposal-status.succeeded {
background: #d1ecf1;
color: #0c5460;
}
.proposal-status.defeated {
background: #f8d7da;
color: #721c24;
}
.proposal-details {
margin-bottom: 1rem;
}
.detail-item {
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.votes {
display: flex;
gap: 1rem;
}
.votes .for {
color: #28a745;
}
.votes .against {
color: #dc3545;
}
.proposal-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.no-proposals {
text-align: center;
padding: 2rem;
color: #666;
}
.form-help {
font-size: 0.9rem;
color: #666;
margin-bottom: 1rem;
}
</style>