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

This commit is contained in:
2025-09-25 03:02:31 +03:00
parent 792282cd75
commit 7b2f6937c8
34 changed files with 2900 additions and 2570 deletions

View File

@@ -0,0 +1,591 @@
<!--
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="create-proposal-page">
<!-- Заголовок -->
<div class="page-header">
<div class="header-content">
<h1>Создание предложения</h1>
<p v-if="selectedDle">{{ selectedDle.name }} ({{ selectedDle.symbol }}) - {{ selectedDle.dleAddress }}</p>
<p v-else-if="isLoadingDle">Загрузка...</p>
<p v-else>DLE не выбран</p>
</div>
<button class="close-btn" @click="goBackToBlocks">×</button>
</div>
<!-- Блоки операций DLE -->
<div class="operations-blocks">
<div class="blocks-header">
<h4>Типы операций DLE контракта</h4>
<p>Выберите тип операции для создания предложения</p>
</div>
<!-- Информация для неавторизованных пользователей -->
<div v-if="!props.isAuthenticated" class="auth-notice">
<div class="alert alert-info">
<i class="fas fa-info-circle"></i>
<strong>Для создания предложений необходимо авторизоваться в приложении</strong>
<p class="mb-0 mt-2">Подключите кошелек в сайдбаре для создания новых предложений</p>
</div>
</div>
<!-- Блоки операций -->
<div class="operations-grid">
<!-- Управление токенами -->
<div class="operation-category">
<h5>💸 Управление токенами</h5>
<div class="operation-blocks">
<div class="operation-block">
<div class="operation-icon">💸</div>
<h6>Передача токенов</h6>
<p>Перевод токенов DLE другому адресу через governance</p>
<button class="create-btn" @click="openTransferForm" :disabled="!props.isAuthenticated">
Создать
</button>
</div>
</div>
</div>
<!-- Управление модулями -->
<div class="operation-category">
<h5>🔧 Управление модулями</h5>
<div class="operation-blocks">
<div class="operation-block">
<div class="operation-icon"></div>
<h6>Добавить модуль</h6>
<p>Добавление нового модуля в DLE контракт</p>
<button class="create-btn" @click="openAddModuleForm" :disabled="!props.isAuthenticated">
Создать
</button>
</div>
<div class="operation-block">
<div class="operation-icon"></div>
<h6>Удалить модуль</h6>
<p>Удаление существующего модуля из DLE контракта</p>
<button class="create-btn" @click="openRemoveModuleForm" :disabled="!props.isAuthenticated">
Создать
</button>
</div>
</div>
</div>
<!-- Управление сетями -->
<div class="operation-category">
<h5>🌐 Управление сетями</h5>
<div class="operation-blocks">
<div class="operation-block">
<div class="operation-icon"></div>
<h6>Добавить сеть</h6>
<p>Добавление новой поддерживаемой блокчейн сети</p>
<button class="create-btn" @click="openAddChainForm" :disabled="!props.isAuthenticated">
Создать
</button>
</div>
<div class="operation-block">
<div class="operation-icon"></div>
<h6>Удалить сеть</h6>
<p>Удаление поддерживаемой блокчейн сети</p>
<button class="create-btn" @click="openRemoveChainForm" :disabled="!props.isAuthenticated">
Создать
</button>
</div>
</div>
</div>
<!-- Управление настройками DLE -->
<div class="operation-category">
<h5> Настройки DLE</h5>
<div class="operation-blocks">
<div class="operation-block">
<div class="operation-icon">📝</div>
<h6>Обновить данные DLE</h6>
<p>Изменение основной информации о DLE (название, символ, адрес и т.д.)</p>
<button class="create-btn" @click="openUpdateDLEInfoForm" :disabled="!props.isAuthenticated">
Создать
</button>
</div>
<div class="operation-block">
<div class="operation-icon">📊</div>
<h6>Изменить кворум</h6>
<p>Изменение процента голосов, необходимого для принятия решений</p>
<button class="create-btn" @click="openUpdateQuorumForm" :disabled="!props.isAuthenticated">
Создать
</button>
</div>
<div class="operation-block">
<div class="operation-icon"></div>
<h6>Изменить время голосования</h6>
<p>Настройка минимального и максимального времени голосования</p>
<button class="create-btn" @click="openUpdateVotingDurationsForm" :disabled="!props.isAuthenticated">
Создать
</button>
</div>
<div class="operation-block">
<div class="operation-icon">🖼</div>
<h6>Изменить логотип</h6>
<p>Обновление URI логотипа DLE для отображения в блокчейн-сканерах</p>
<button class="create-btn" @click="openSetLogoURIForm" :disabled="!props.isAuthenticated">
Создать
</button>
</div>
</div>
</div>
<!-- Оффчейн операции -->
<div class="operation-category">
<h5>📋 Оффчейн операции</h5>
<div class="operation-blocks">
<div class="operation-block">
<div class="operation-icon">📄</div>
<h6>Оффчейн действие</h6>
<p>Создание предложения для выполнения оффчейн операций в приложении</p>
<button class="create-btn" @click="openOffchainActionForm" :disabled="!props.isAuthenticated">
Создать
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</BaseLayout>
</template>
<script setup>
import { ref, computed, onMounted, defineProps, defineEmits, inject } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { useAuthContext } from '../../composables/useAuth';
import BaseLayout from '../../components/BaseLayout.vue';
import { getDLEInfo, getSupportedChains } from '../../services/dleV2Service.js';
import { createProposal as createProposalAPI } from '../../services/proposalsService.js';
import api from '../../api/axios';
import wsClient from '../../utils/websocket.js';
import { ethers } from 'ethers';
const showTargetChains = computed(() => {
// Для offchain-действий не требуется ончейн исполнение (здесь типы пока ончейн)
// Можно расширить логику при появлении offchain типа
return true;
});
const props = defineProps({
isAuthenticated: Boolean,
identities: Array,
tokenBalances: Object,
isLoadingTokens: Boolean
});
const emit = defineEmits(['auth-action-completed']);
const { address, isAuthenticated, tokenBalances, checkTokenBalances } = useAuthContext();
const router = useRouter();
const route = useRoute();
// Получаем адрес DLE из URL
const dleAddress = computed(() => {
const address = route.query.address || props.dleAddress;
console.log('DLE Address from URL:', address);
return address;
});
// Функция возврата к блокам управления
const goBackToBlocks = () => {
if (dleAddress.value) {
router.push(`/management/dle-blocks?address=${dleAddress.value}`);
} else {
router.push('/management');
}
};
// Состояние DLE
const selectedDle = ref(null);
const isLoadingDle = ref(false);
// Доступные цепочки (загружаются из конфигурации)
const availableChains = ref([]);
// Функции для открытия отдельных форм операций
function openTransferForm() {
// TODO: Открыть форму для передачи токенов
alert('Форма передачи токенов будет реализована');
}
function openAddModuleForm() {
// TODO: Открыть форму для добавления модуля
alert('Форма добавления модуля будет реализована');
}
function openRemoveModuleForm() {
// TODO: Открыть форму для удаления модуля
alert('Форма удаления модуля будет реализована');
}
function openAddChainForm() {
// TODO: Открыть форму для добавления сети
alert('Форма добавления сети будет реализована');
}
function openRemoveChainForm() {
// TODO: Открыть форму для удаления сети
alert('Форма удаления сети будет реализована');
}
function openUpdateDLEInfoForm() {
// TODO: Открыть форму для обновления данных DLE
alert('Форма обновления данных DLE будет реализована');
}
function openUpdateQuorumForm() {
// TODO: Открыть форму для изменения кворума
alert('Форма изменения кворума будет реализована');
}
function openUpdateVotingDurationsForm() {
// TODO: Открыть форму для изменения времени голосования
alert('Форма изменения времени голосования будет реализована');
}
function openSetLogoURIForm() {
// TODO: Открыть форму для изменения логотипа
alert('Форма изменения логотипа будет реализована');
}
function openOffchainActionForm() {
// TODO: Открыть форму для оффчейн действий
alert('Форма оффчейн действий будет реализована');
}
// Функции
async function loadDleData() {
console.log('loadDleData вызвана с адресом:', dleAddress.value);
if (!dleAddress.value) {
console.warn('Адрес DLE не указан');
return;
}
isLoadingDle.value = true;
try {
// Загружаем данные DLE из блокчейна
const response = await api.post('/dle-core/read-dle-info', {
dleAddress: dleAddress.value
});
if (response.data.success) {
selectedDle.value = response.data.data;
console.log('Загружены данные DLE из блокчейна:', selectedDle.value);
} else {
console.error('Ошибка загрузки DLE:', response.data.error);
}
// Загружаем поддерживаемые цепочки
const chainsResponse = await getSupportedChains(dleAddress.value);
availableChains.value = chainsResponse.data?.chains || [];
} catch (error) {
console.error('Ошибка загрузки данных DLE из блокчейна:', error);
} finally {
isLoadingDle.value = false;
}
}
onMounted(async () => {
// Принудительно загружаем токены, если пользователь аутентифицирован
if (isAuthenticated.value && address.value) {
console.log('[CreateProposalView] Принудительная загрузка токенов для адреса:', address.value);
await checkTokenBalances(address.value);
}
// Загрузка данных DLE
if (dleAddress.value) {
loadDleData();
}
});
</script>
<style scoped>
.create-proposal-page {
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: center;
margin-bottom: 30px;
padding-bottom: 15px;
border-bottom: 1px solid #f0f0f0;
}
.header-content {
flex-grow: 1;
}
.page-header h1 {
color: var(--color-primary);
font-size: 2rem;
margin: 0 0 5px 0;
}
.page-header p {
color: var(--color-grey-dark);
font-size: 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;
}
/* Стили для блоков операций */
.operations-blocks {
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
border-radius: 12px;
padding: 2rem;
border: 1px solid #e9ecef;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.blocks-header {
margin-bottom: 2rem;
text-align: center;
}
.blocks-header h4 {
color: var(--color-primary);
margin: 0 0 0.5rem 0;
font-size: 1.5rem;
font-weight: 600;
}
.blocks-header p {
color: #6c757d;
margin: 0;
font-size: 1rem;
}
.auth-notice {
margin-bottom: 2rem;
}
.alert {
padding: 1rem 1.5rem;
border-radius: 8px;
border: 1px solid transparent;
display: flex;
align-items: flex-start;
gap: 0.75rem;
}
.alert-info {
background-color: #d1ecf1;
border-color: #bee5eb;
color: #0c5460;
}
.alert i {
margin-top: 0.25rem;
flex-shrink: 0;
}
.operations-grid {
display: flex;
flex-direction: column;
gap: 2rem;
}
.operation-category {
background: white;
border-radius: 12px;
padding: 1.5rem;
border: 1px solid #e9ecef;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.operation-category h5 {
color: var(--color-primary);
margin: 0 0 1.5rem 0;
font-size: 1.25rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
padding-bottom: 0.75rem;
border-bottom: 2px solid #f0f0f0;
}
.operation-blocks {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
}
.operation-block {
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
border: 2px solid #e9ecef;
border-radius: 12px;
padding: 1.5rem;
text-align: center;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.operation-block::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--color-primary), #20c997);
transform: scaleX(0);
transition: transform 0.3s ease;
}
.operation-block:hover {
border-color: var(--color-primary);
box-shadow: 0 8px 25px rgba(0, 123, 255, 0.15);
transform: translateY(-4px);
}
.operation-block:hover::before {
transform: scaleX(1);
}
.operation-icon {
font-size: 3rem;
margin-bottom: 1rem;
display: block;
}
.operation-block h6 {
color: #333;
margin: 0 0 0.75rem 0;
font-size: 1.1rem;
font-weight: 600;
}
.operation-block p {
color: #666;
margin: 0 0 1.5rem 0;
font-size: 0.9rem;
line-height: 1.5;
}
.create-btn {
background: linear-gradient(135deg, var(--color-primary), #20c997);
color: white;
border: none;
border-radius: 8px;
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
width: 100%;
position: relative;
overflow: hidden;
}
.create-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s ease;
}
.create-btn:hover {
background: linear-gradient(135deg, #0056b3, #1ea085);
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0, 123, 255, 0.3);
}
.create-btn:hover::before {
left: 100%;
}
.create-btn:disabled {
background: #6c757d;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.create-btn:disabled::before {
display: none;
}
/* Адаптивность */
@media (max-width: 768px) {
.operations-blocks {
padding: 1rem;
}
.operation-blocks {
grid-template-columns: 1fr;
}
.operation-block {
padding: 1rem;
}
.operation-icon {
font-size: 2.5rem;
}
.blocks-header h4 {
font-size: 1.25rem;
}
.operation-category h5 {
font-size: 1.1rem;
}
}
</style>

View File

@@ -34,6 +34,14 @@
<div class="management-blocks">
<!-- Первый ряд -->
<div class="blocks-row">
<div class="management-block create-proposal-block">
<h3>Создать предложение</h3>
<p>Универсальная форма для создания новых предложений</p>
<button class="details-btn create-btn" @click="openCreateProposal">
Подробнее
</button>
</div>
<div class="management-block">
<h3>Предложения</h3>
<p>Создание, подписание, выполнение</p>
@@ -45,16 +53,16 @@
<p>Балансы, трансферы, распределение</p>
<button class="details-btn" @click="openTokens">Подробнее</button>
</div>
</div>
<!-- Второй ряд -->
<div class="blocks-row">
<div class="management-block">
<h3>Кворум</h3>
<p>Настройки голосования</p>
<button class="details-btn" @click="openQuorum">Подробнее</button>
</div>
</div>
<!-- Второй ряд -->
<div class="blocks-row">
<div class="management-block">
<h3>Модули DLE</h3>
<p>Установка, настройка, управление</p>
@@ -165,6 +173,14 @@ const openSettings = () => {
}
};
const openCreateProposal = () => {
if (dleAddress.value) {
router.push(`/management/create-proposal?address=${dleAddress.value}`);
} else {
router.push('/management/create-proposal');
}
};
onMounted(() => {
// Если нет адреса DLE, перенаправляем на главную страницу management
if (!dleAddress.value) {
@@ -279,6 +295,32 @@ onMounted(() => {
transform: translateY(-1px);
}
/* Стили для блока создания предложения */
.create-proposal-block {
background: linear-gradient(135deg, #e8f5e8 0%, #f0f8f0 100%);
border: 2px solid #28a745;
}
.create-proposal-block:hover {
border-color: #20c997;
box-shadow: 0 4px 20px rgba(40, 167, 69, 0.15);
}
.create-proposal-block h3 {
color: #28a745;
}
.create-btn {
background: linear-gradient(135deg, #28a745, #20c997);
color: white;
font-weight: 700;
}
.create-btn:hover {
background: linear-gradient(135deg, #218838, #1ea085);
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.3);
}
/* Адаптивность */
@media (max-width: 768px) {
.blocks-row {

View File

@@ -195,367 +195,6 @@
</div>
</div>
<!-- Форма создания предложения (всегда внизу страницы) -->
<div class="create-proposal-form">
<div class="form-header">
<h4>📝 Создание нового предложения</h4>
<!-- Кнопка закрытия больше не нужна -->
</div>
<!-- Информация для неавторизованных пользователей -->
<div v-if="!props.isAuthenticated" class="auth-notice-form">
<div class="alert alert-info">
<i class="fas fa-info-circle"></i>
<strong>Для создания предложений необходимо авторизоваться в приложении</strong>
<p class="mb-0 mt-2">Подключите кошелек в сайдбаре для создания новых предложений</p>
</div>
</div>
<!-- Форма только для авторизованных пользователей -->
<div v-else>
<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>
<!-- Timelock -->
<div class="form-section">
<h5> Timelock</h5>
<div class="form-group-inline">
<label for="timelockHours">Задержка исполнения (часы):</label>
<input id="timelockHours" type="number" min="0" step="1" v-model.number="newProposal.timelockHours" class="form-control small" />
</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" v-if="showTargetChains">
<h5>🎯 Целевые сети для исполнения</h5>
<div class="targets-grid">
<label v-for="chain in availableChains" :key="chain.chainId" class="target-item">
<input type="checkbox" :value="chain.chainId" v-model="newProposal.targetChains" />
<span>{{ chain.name }} ({{ chain.chainId }})</span>
</label>
</div>
<small class="text-muted">Выберите хотя бы одну целевую сеть для исполнения операции.</small>
<div v-if="showTargetChains && newProposal.targetChains.length === 0" class="form-error">
<small class="text-danger"> Необходимо выбрать хотя бы одну целевую сеть</small>
</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="updateDLEInfo">Обновить данные DLE</option>
<option value="updateQuorum">Изменить кворум</option>
<option value="updateChain">Изменить текущую цепочку</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="0x1234567890abcdef1234567890abcdef12345678"
:class="{ 'is-invalid': newProposal.operationParams.to && !validateAddress(newProposal.operationParams.to) }"
>
<small class="form-text text-muted">Введите корректный Ethereum адрес (42 символа, начинается с 0x)</small>
</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"
:class="{ 'is-invalid': newProposal.operationParams.amount <= 0 }"
>
<small class="form-text text-muted">Введите количество токенов для передачи</small>
</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>
<!-- Параметры для обновления данных DLE -->
<div v-if="newProposal.operationType === 'updateDLEInfo'" class="operation-params">
<div class="form-group">
<label for="dleName">Новое название DLE:</label>
<input
type="text"
id="dleName"
v-model="newProposal.operationParams.name"
class="form-control"
placeholder="Новое название"
>
</div>
<div class="form-group">
<label for="dleSymbol">Новый символ токена:</label>
<input
type="text"
id="dleSymbol"
v-model="newProposal.operationParams.symbol"
class="form-control"
placeholder="Новый символ"
>
</div>
<div class="form-group">
<label for="dleLocation">Новое местонахождение:</label>
<input
type="text"
id="dleLocation"
v-model="newProposal.operationParams.location"
class="form-control"
placeholder="Новое местонахождение"
>
</div>
<div class="form-group">
<label for="dleCoordinates">Новые координаты:</label>
<input
type="text"
id="dleCoordinates"
v-model="newProposal.operationParams.coordinates"
class="form-control"
placeholder="44.0422736,43.062124"
>
</div>
<div class="form-group">
<label for="dleJurisdiction">Новая юрисдикция:</label>
<input
type="number"
id="dleJurisdiction"
v-model.number="newProposal.operationParams.jurisdiction"
class="form-control"
placeholder="643"
>
</div>
<div class="form-group">
<label for="dleOktmo">Новый ОКТМО:</label>
<input
type="number"
id="dleOktmo"
v-model.number="newProposal.operationParams.oktmo"
class="form-control"
placeholder="45000000000"
>
</div>
<div class="form-group">
<label for="dleKpp">Новый КПП:</label>
<input
type="number"
id="dleKpp"
v-model.number="newProposal.operationParams.kpp"
class="form-control"
placeholder="770101001"
>
</div>
</div>
<!-- Параметры для изменения кворума -->
<div v-if="newProposal.operationType === 'updateQuorum'" class="operation-params">
<div class="form-group">
<label for="newQuorum">Новый процент кворума:</label>
<input
type="number"
id="newQuorum"
v-model.number="newProposal.operationParams.quorumPercentage"
class="form-control"
min="1"
max="100"
placeholder="51"
>
<small class="form-text text-muted">Процент от общего количества токенов (1-100%)</small>
</div>
</div>
<!-- Параметры для изменения текущей цепочки -->
<div v-if="newProposal.operationType === 'updateChain'" class="operation-params">
<div class="form-group">
<label for="newChainId">Новая текущая цепочка:</label>
<select id="newChainId" v-model="newProposal.operationParams.chainId" class="form-control">
<option value="">-- Выберите цепочку --</option>
<option v-for="chain in availableChains" :key="chain.chainId" :value="chain.chainId">
{{ chain.name }} ({{ chain.chainId }})
</option>
</select>
<small class="form-text text-muted">Выберите новую цепочку для управления DLE</small>
</div>
</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>
</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>
</div> <!-- Закрываем div для авторизованных пользователей -->
</div>
</BaseLayout>
</template>
@@ -564,14 +203,8 @@ import { ref, computed, onMounted, onUnmounted, watch, defineProps, defineEmits,
import { useRouter, useRoute } from 'vue-router';
import { useAuthContext } from '../../composables/useAuth';
import BaseLayout from '../../components/BaseLayout.vue';
import { getDLEInfo, getSupportedChains } from '../../services/dleV2Service.js';
import { getProposals, createProposal as createProposalAPI, voteOnProposal as voteForProposalAPI, executeProposal as executeProposalAPI, decodeProposalData } from '../../services/proposalsService.js';
import { getProposals, voteOnProposal as voteForProposalAPI, executeProposal as executeProposalAPI, decodeProposalData } from '../../services/proposalsService.js';
import api from '../../api/axios';
const showTargetChains = computed(() => {
// Для offchain-действий не требуется ончейн исполнение (здесь типы пока ончейн)
// Можно расширить логику при появлении offchain типа
return true;
});
import wsClient from '../../utils/websocket.js';
import { ethers } from 'ethers';
@@ -916,55 +549,14 @@ const goBackToBlocks = () => {
const selectedDle = ref(null);
const isLoadingDle = ref(false);
// Состояние формы
// const showCreateForm = ref(false); // Больше не нужно - форма всегда видна
const isCreating = ref(false);
// Состояние фильтров
const statusFilter = ref('');
// Новое предложение
const newProposal = ref({
description: '',
duration: 7,
governanceChainId: null,
timelockHours: 0,
targetChains: [],
operationType: '',
operationParams: {
to: '',
from: '',
amount: 0,
customData: '',
name: '',
symbol: '',
location: '',
coordinates: '',
jurisdiction: 0,
oktmo: 0,
kpp: 0,
chainId: ''
}
});
// Доступные цепочки (загружаются из конфигурации)
const availableChains = ref([]);
// Предложения
const proposals = ref([]);
// Вычисляемые свойства
const isFormValid = computed(() => {
return (
newProposal.value.description &&
newProposal.value.duration > 0 &&
newProposal.value.governanceChainId &&
newProposal.value.operationType &&
newProposal.value.timelockHours >= 0 &&
validateOperationParams() &&
validateTargetChains()
);
});
const filteredProposals = computed(() => {
console.log('[Frontend] Фильтрация предложений. Всего:', proposals.value.length);
@@ -1038,11 +630,6 @@ async function loadDleData() {
}));
console.log('[Frontend] Итоговый список предложений:', proposals.value);
// Загружаем поддерживаемые цепочки
const chainsResponse = await getSupportedChains(dleAddress.value);
availableChains.value = chainsResponse.data?.chains || [];
} catch (error) {
console.error('Ошибка загрузки данных DLE из блокчейна:', error);
@@ -1051,51 +638,9 @@ async function loadDleData() {
}
}
function validateOperationParams() {
const params = newProposal.value.operationParams;
switch (newProposal.value.operationType) {
case 'transfer':
case 'mint':
return validateAddress(params.to) && params.amount > 0;
case 'burn':
return validateAddress(params.from) && params.amount > 0;
case 'custom':
return params.customData && params.customData.startsWith('0x') && params.customData.length >= 10;
case 'updateDLEInfo':
return params.name && params.symbol && params.location && params.coordinates && params.jurisdiction && params.oktmo && params.kpp;
case 'updateQuorum':
return params.quorumPercentage >= 1 && params.quorumPercentage <= 100;
case 'updateChain':
return params.chainId && params.chainId !== '';
default:
return false;
}
}
function validateTargetChains() {
// Если показываем целевые сети, то должна быть выбрана хотя бы одна
if (showTargetChains.value) {
return newProposal.value.targetChains.length > 0;
}
return true;
}
function validateAddress(address) {
if (!address) return false;
// Проверяем формат Ethereum адреса
const addressRegex = /^0x[a-fA-F0-9]{40}$/;
return addressRegex.test(address);
}
function getChainName(chainId) {
// Сначала ищем в availableChains
if (Array.isArray(availableChains.value)) {
const chain = availableChains.value.find(c => c.chainId === chainId);
if (chain) return chain.name;
}
// Если не найдено, используем известные chain ID
// Используем известные chain ID
const knownChains = {
1: 'Ethereum Mainnet',
11155111: 'Sepolia Testnet',
@@ -1107,42 +652,6 @@ function getChainName(chainId) {
return knownChains[chainId] || `Chain ID: ${chainId}`;
}
function getOperationTypeName(type) {
const types = {
'transfer': 'Передача токенов',
'mint': 'Минтинг токенов',
'burn': 'Сжигание токенов',
'custom': 'Пользовательская операция',
'updateDLEInfo': 'Обновить данные DLE',
'updateQuorum': 'Изменить кворум',
'updateChain': 'Изменить текущую цепочку'
};
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)}...`;
case 'updateDLEInfo':
return `Название: ${params.name}, Символ: ${params.symbol}, Местонахождение: ${params.location}, Координаты: ${params.coordinates}, Юрисдикция: ${params.jurisdiction}, ОКТМО: ${params.oktmo}, КПП: ${params.kpp}`;
case 'updateQuorum':
return `Процент кворума: ${params.quorumPercentage}%`;
case 'updateChain':
return `Новая цепочка: ${getChainName(params.chainId) || 'Не выбрана'}`;
default:
return 'Не указаны';
}
}
function shortenAddress(address) {
if (!address) return '';
return `${address.slice(0, 6)}...${address.slice(-4)}`;
@@ -1481,153 +990,6 @@ function hasVotedFor(proposalId) {
// Создание предложения
async function createProposal() {
// Проверка авторизации для создания предложений
if (!props.isAuthenticated) {
alert('❌ Для создания предложений необходимо авторизоваться в приложении');
return;
}
if (!isFormValid.value) {
alert('Пожалуйста, заполните все обязательные поля');
return;
}
isCreating.value = true;
try {
// Подготовка данных для смарт-контракта
const operation = encodeOperation();
// Создаем предложение через API
const result = await createProposalAPI(dleAddress.value, {
description: newProposal.value.description,
duration: newProposal.value.duration * 24 * 60 * 60, // конвертируем в секунды
operation: operation,
governanceChainId: newProposal.value.governanceChainId,
targetChains: showTargetChains.value ? newProposal.value.targetChains : [],
timelockDelay: (newProposal.value.timelockHours || 0) * 3600
});
console.log('Предложение создано:', result);
// Отправляем WebSocket уведомление
wsClient.send('proposal_created', {
dleAddress: dleAddress.value,
proposalId: result.proposalId,
txHash: result.txHash
});
// Ждем немного, чтобы блокчейн обработал транзакцию
await new Promise(resolve => setTimeout(resolve, 5000));
// Обновляем список предложений
await loadDleData();
// Отправляем WebSocket уведомление о новом предложении
wsClient.send('proposal_created', {
dleAddress: dleAddress.value,
proposalId: result.proposalId,
txHash: result.txHash
});
// Сбрасываем форму
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;
case 'updateDLEInfo':
return encodeUpdateDLEInfoOperation(params.name, params.symbol, params.location, params.coordinates, params.jurisdiction, params.oktmo, params.kpp);
case 'updateQuorum':
return encodeUpdateQuorumOperation(params.quorumPercentage);
case 'updateChain':
return encodeUpdateChainOperation(params.chainId);
default:
throw new Error('Неизвестный тип операции');
}
}
function encodeTransferOperation(to, amount) {
// Кодировка операции передачи токенов ERC20
const selector = '0xa9059cbb'; // transfer(address,uint256)
const paddedAddress = to.slice(2).padStart(64, '0');
const paddedAmount = BigInt(amount).toString(16).padStart(64, '0');
return selector + paddedAddress + paddedAmount;
}
function encodeMintOperation(to, amount) {
// Кодировка операции минтинга токенов
const selector = '0x40c10f19'; // mint(address,uint256)
const paddedAddress = to.slice(2).padStart(64, '0');
const paddedAmount = BigInt(amount).toString(16).padStart(64, '0');
return selector + paddedAddress + paddedAmount;
}
function encodeBurnOperation(from, amount) {
// Кодировка операции сжигания токенов
const selector = '0x42966c68'; // burn(address,uint256)
const paddedAddress = from.slice(2).padStart(64, '0');
const paddedAmount = BigInt(amount).toString(16).padStart(64, '0');
return selector + paddedAddress + paddedAmount;
}
function encodeUpdateDLEInfoOperation(name, symbol, location, coordinates, jurisdiction, oktmo, kpp) {
// Селектор для _updateDLEInfo(string,string,string,string,uint256,string[],uint256)
const selector = '0x' + ethers.keccak256(ethers.toUtf8Bytes('_updateDLEInfo(string,string,string,string,uint256,string[],uint256)')).slice(0, 10);
// Кодируем параметры
const abiCoder = new ethers.AbiCoder();
const encodedData = abiCoder.encode(
['string', 'string', 'string', 'string', 'uint256', 'string[]', 'uint256'],
[name, symbol, location, coordinates, jurisdiction, [], kpp] // okvedCodes пока пустой массив
);
return selector + encodedData.slice(2);
}
function encodeUpdateQuorumOperation(quorumPercentage) {
// Селектор для _updateQuorumPercentage(uint256)
const selector = '0x' + ethers.keccak256(ethers.toUtf8Bytes('_updateQuorumPercentage(uint256)')).slice(0, 10);
// Кодируем параметр
const abiCoder = new ethers.AbiCoder();
const encodedData = abiCoder.encode(['uint256'], [quorumPercentage]);
return selector + encodedData.slice(2);
}
function encodeUpdateChainOperation(chainId) {
// Селектор для _updateCurrentChainId(uint256)
const selector = '0x' + ethers.keccak256(ethers.toUtf8Bytes('_updateCurrentChainId(uint256)')).slice(0, 10);
// Кодируем параметр
const abiCoder = new ethers.AbiCoder();
const encodedData = abiCoder.encode(['uint256'], [chainId]);
return selector + encodedData.slice(2);
}
// Подпись предложения
async function signProposalLocal(proposalId) {
@@ -2027,28 +1389,6 @@ async function executeProposalLocal(proposalId) {
function resetForm() {
newProposal.value = {
description: '',
duration: 7,
governanceChainId: null,
operationType: '',
operationParams: {
to: '',
from: '',
amount: 0,
customData: '',
name: '',
symbol: '',
location: '',
coordinates: '',
jurisdiction: 0,
oktmo: 0,
kpp: 0,
chainId: ''
}
};
}
// Проверка прав администратора
function hasAdminRights() {
@@ -2315,115 +1655,6 @@ onUnmounted(() => {
font-style: italic;
}
.create-proposal-form {
background: #f8f9fa;
border-radius: 8px;
padding: 1.5rem;
margin-top: 2rem;
border-top: 2px solid #e9ecef;
}
.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;
@@ -2669,42 +1900,4 @@ onUnmounted(() => {
font-weight: 500;
}
/* Стили для ошибок валидации */
.form-error {
margin-top: 0.5rem;
padding: 0.5rem;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 4px;
}
.text-danger {
color: #dc3545 !important;
}
.targets-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.5rem;
margin-bottom: 1rem;
}
.target-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
border: 1px solid #e9ecef;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.target-item:hover {
background-color: #f8f9fa;
}
.target-item input[type="checkbox"] {
margin: 0;
}
</style>