ваше сообщение коммита
This commit is contained in:
@@ -156,6 +156,11 @@ const props = defineProps({
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
autoVerifyAfterDeploy: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
@@ -250,8 +255,16 @@ const startDeployment = async () => {
|
||||
try {
|
||||
addLog('🚀 Начинаем асинхронный деплой с WebSocket отслеживанием', 'info');
|
||||
|
||||
// Генерируем deploymentId заранее, чтобы WebSocket сообщения не игнорировались
|
||||
const tempDeploymentId = `deploy_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
|
||||
addLog(`🆔 Временный ID деплоя: ${tempDeploymentId}`, 'info');
|
||||
|
||||
// Начинаем отслеживание сразу с временным ID
|
||||
startDeploymentTracking(tempDeploymentId);
|
||||
|
||||
// Подготовка данных для деплоя
|
||||
const deployData = {
|
||||
deploymentId: tempDeploymentId, // Передаем временный ID в backend
|
||||
name: props.dleData.name,
|
||||
symbol: props.dleData.tokenSymbol,
|
||||
location: props.dleData.addressData?.fullAddress || 'Не указан',
|
||||
@@ -260,7 +273,7 @@ const startDeployment = async () => {
|
||||
oktmo: props.dleData.selectedOktmo || '',
|
||||
okvedCodes: props.dleData.selectedOkved || [],
|
||||
kpp: props.dleData.kppCode || '',
|
||||
quorumPercentage: props.dleData.governanceQuorum || 51,
|
||||
quorumPercentage: props.dleData.governanceQuorum !== undefined ? props.dleData.governanceQuorum : 51,
|
||||
initialPartners: props.dleData.partners.map(p => p.address).filter(addr => addr),
|
||||
initialAmounts: props.dleData.partners.map(p => p.amount).filter(amount => amount > 0),
|
||||
supportedChainIds: props.selectedNetworks.filter(id => id !== null && id !== undefined),
|
||||
@@ -268,7 +281,7 @@ const startDeployment = async () => {
|
||||
logoURI: props.logoURI || '/uploads/logos/default-token.svg',
|
||||
privateKey: props.privateKey,
|
||||
etherscanApiKey: props.etherscanApiKey || '',
|
||||
autoVerifyAfterDeploy: false
|
||||
autoVerifyAfterDeploy: props.autoVerifyAfterDeploy !== undefined ? props.autoVerifyAfterDeploy : false
|
||||
};
|
||||
|
||||
addLog('📤 Отправляем запрос на асинхронный деплой...', 'info');
|
||||
@@ -279,8 +292,11 @@ const startDeployment = async () => {
|
||||
if (response.data.success && response.data.deploymentId) {
|
||||
addLog(`✅ Деплой запущен! ID: ${response.data.deploymentId}`, 'success');
|
||||
|
||||
// Начинаем отслеживание через WebSocket
|
||||
startDeploymentTracking(response.data.deploymentId);
|
||||
// Обновляем deploymentId на реальный от сервера
|
||||
if (response.data.deploymentId !== tempDeploymentId) {
|
||||
addLog(`🔄 Обновляем ID деплоя: ${tempDeploymentId} → ${response.data.deploymentId}`, 'info');
|
||||
startDeploymentTracking(response.data.deploymentId);
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error('Не удалось запустить деплой: ' + (response.data.message || 'неизвестная ошибка'));
|
||||
|
||||
@@ -47,30 +47,19 @@ export function useDeploymentWebSocket() {
|
||||
|
||||
// Обработчик WebSocket сообщений
|
||||
const handleDeploymentUpdate = (data) => {
|
||||
if (data.deploymentId !== deploymentId.value) return;
|
||||
|
||||
console.log('🔄 [DeploymentWebSocket] Получено обновление:', data);
|
||||
console.log('🔄 [DeploymentWebSocket] Текущий deploymentId:', deploymentId.value);
|
||||
console.log('🔄 [DeploymentWebSocket] deploymentId из данных:', data.deploymentId);
|
||||
|
||||
if (data.deploymentId !== deploymentId.value) {
|
||||
console.log('🔄 [DeploymentWebSocket] Игнорируем обновление - не наш deploymentId');
|
||||
return;
|
||||
}
|
||||
|
||||
switch (data.type) {
|
||||
case 'deployment_started':
|
||||
deploymentStatus.value = 'in_progress';
|
||||
isDeploying.value = true;
|
||||
currentStage.value = data.stage || '';
|
||||
addLog(`🚀 ${data.message}`, 'info');
|
||||
break;
|
||||
|
||||
case 'deployment_progress':
|
||||
currentStage.value = data.stage || '';
|
||||
currentNetwork.value = data.network || '';
|
||||
progress.value = data.progress || 0;
|
||||
if (data.message) {
|
||||
addLog(`📊 ${data.message}`, 'info');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'deployment_stage_completed':
|
||||
if (data.message) {
|
||||
addLog(`✅ ${data.message}`, 'success');
|
||||
case 'deployment_log':
|
||||
if (data.log) {
|
||||
addLog(data.log.message, data.log.type || 'info');
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -87,34 +76,6 @@ export function useDeploymentWebSocket() {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'deployment_error':
|
||||
error.value = data.error;
|
||||
if (data.message) {
|
||||
addLog(`❌ ${data.message}`, 'error');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'deployment_completed':
|
||||
deploymentStatus.value = 'completed';
|
||||
isDeploying.value = false;
|
||||
deploymentResult.value = data.result;
|
||||
progress.value = 100;
|
||||
addLog(`🎉 ${data.message}`, 'success');
|
||||
break;
|
||||
|
||||
case 'deployment_failed':
|
||||
deploymentStatus.value = 'failed';
|
||||
isDeploying.value = false;
|
||||
error.value = data.error;
|
||||
addLog(`💥 ${data.message}`, 'error');
|
||||
break;
|
||||
|
||||
case 'deployment_log':
|
||||
if (data.log) {
|
||||
addLog(data.log.message, data.log.type || 'info');
|
||||
}
|
||||
break;
|
||||
|
||||
case undefined:
|
||||
// Обработка событий без типа (прямые обновления)
|
||||
if (data.stage) currentStage.value = data.stage;
|
||||
@@ -135,6 +96,13 @@ export function useDeploymentWebSocket() {
|
||||
console.warn('🤷♂️ [DeploymentWebSocket] Неизвестный тип события:', data.type);
|
||||
}
|
||||
};
|
||||
|
||||
// Подключаемся к WebSocket сразу при инициализации
|
||||
wsClient.connect();
|
||||
if (wsClient && typeof wsClient.subscribe === 'function') {
|
||||
wsClient.subscribe('deployment_update', handleDeploymentUpdate);
|
||||
console.log('🔌 [DeploymentWebSocket] Подключились к WebSocket при инициализации');
|
||||
}
|
||||
|
||||
// Начать отслеживание деплоя
|
||||
const startDeploymentTracking = (id) => {
|
||||
@@ -145,13 +113,7 @@ export function useDeploymentWebSocket() {
|
||||
isDeploying.value = true;
|
||||
clearLogs();
|
||||
|
||||
// Подключаемся к WebSocket обновлениям
|
||||
wsClient.connect();
|
||||
if (wsClient && typeof wsClient.subscribe === 'function') {
|
||||
wsClient.subscribe('deployment_update', handleDeploymentUpdate);
|
||||
} else {
|
||||
console.warn('[DeploymentWebSocket] wsClient.subscribe недоступен');
|
||||
}
|
||||
// WebSocket уже подключен при инициализации
|
||||
|
||||
addLog('🔌 Подключено к WebSocket для получения обновлений деплоя', 'info');
|
||||
};
|
||||
|
||||
@@ -222,6 +222,11 @@ const routes = [
|
||||
name: 'management-proposals',
|
||||
component: () => import('../views/smartcontracts/DleProposalsView.vue')
|
||||
},
|
||||
{
|
||||
path: '/management/create-proposal',
|
||||
name: 'management-create-proposal',
|
||||
component: () => import('../views/smartcontracts/CreateProposalView.vue')
|
||||
},
|
||||
{
|
||||
path: '/management/tokens',
|
||||
name: 'management-tokens',
|
||||
|
||||
@@ -26,7 +26,12 @@ class WebSocketClient {
|
||||
|
||||
connect() {
|
||||
try {
|
||||
this.ws = new WebSocket('ws://localhost:8000/ws');
|
||||
// В Docker окружении используем Vite прокси для WebSocket
|
||||
// Используем относительный путь, чтобы Vite прокси мог перенаправить запрос на backend
|
||||
const wsUrl = window.location.protocol === 'https:'
|
||||
? 'wss://' + window.location.host + '/ws'
|
||||
: 'ws://' + window.location.host + '/ws';
|
||||
this.ws = new WebSocket(wsUrl);
|
||||
|
||||
this.ws.onopen = () => {
|
||||
console.log('[WebSocket] Подключение установлено');
|
||||
@@ -37,13 +42,21 @@ class WebSocketClient {
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('[WebSocket] Получено сообщение:', data);
|
||||
|
||||
// Логируем все deployment_update сообщения для отладки
|
||||
if (data.type === 'deployment_update') {
|
||||
console.log('[WebSocket] Получено deployment_update:', data);
|
||||
console.log('[WebSocket] Данные для обработчика:', data.data);
|
||||
}
|
||||
|
||||
// Вызываем все зарегистрированные обработчики для этого события
|
||||
if (this.listeners.has(data.type)) {
|
||||
console.log(`[WebSocket] Вызываем обработчики для типа: ${data.type}, количество: ${this.listeners.get(data.type).length}`);
|
||||
this.listeners.get(data.type).forEach(callback => {
|
||||
callback(data.data);
|
||||
});
|
||||
} else {
|
||||
console.log(`[WebSocket] Нет обработчиков для типа: ${data.type}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[WebSocket] Ошибка парсинга сообщения:', error);
|
||||
|
||||
@@ -96,16 +96,25 @@
|
||||
</ul>
|
||||
</div>
|
||||
<div class="detail-item" v-else>
|
||||
<strong>Адрес контракта:</strong>
|
||||
<a
|
||||
:href="`https://sepolia.etherscan.io/address/${dle.dleAddress}`"
|
||||
target="_blank"
|
||||
class="address-link"
|
||||
@click.stop
|
||||
>
|
||||
{{ shortenAddress(dle.dleAddress) }}
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</a>
|
||||
<strong>Адреса контрактов:</strong>
|
||||
<div class="addresses-list">
|
||||
<div
|
||||
v-for="network in dle.deployedNetworks || [{ chainId: 11155111, address: dle.dleAddress }]"
|
||||
:key="network.chainId"
|
||||
class="address-item"
|
||||
>
|
||||
<span class="chain-name">{{ getChainName(network.chainId) }}:</span>
|
||||
<a
|
||||
:href="getExplorerUrl(network.chainId, network.address)"
|
||||
target="_blank"
|
||||
class="address-link"
|
||||
@click.stop
|
||||
>
|
||||
{{ shortenAddress(network.address) }}
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<strong>Местоположение:</strong> {{ dle.location }}
|
||||
@@ -124,17 +133,13 @@
|
||||
<strong>Статус:</strong>
|
||||
<span class="status active">Активен</span>
|
||||
</div>
|
||||
<div class="detail-item" v-if="dle.totalSupply">
|
||||
<div class="detail-item">
|
||||
<strong>Общий объем токенов:</strong>
|
||||
<span class="token-supply">{{ parseFloat(dle.totalSupply).toLocaleString() }} {{ dle.symbol }}</span>
|
||||
<span class="token-supply">{{ formatTokenAmount(dle.totalSupply || 0) }} {{ dle.symbol }}</span>
|
||||
</div>
|
||||
<div class="detail-item" v-if="dle.logoURI">
|
||||
<strong>Логотип:</strong>
|
||||
<span class="logo-info">Установлен</span>
|
||||
</div>
|
||||
<div class="detail-item" v-if="dle.creationTimestamp">
|
||||
<div class="detail-item">
|
||||
<strong>Дата создания:</strong>
|
||||
<span class="creation-date">{{ formatTimestamp(dle.creationTimestamp) }}</span>
|
||||
<span class="creation-date">{{ formatTimestamp(dle.creationTimestamp || dle.createdAt) }}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -345,7 +350,18 @@ function getExplorerUrl(chainId, address) {
|
||||
|
||||
function formatTimestamp(timestamp) {
|
||||
if (!timestamp) return '';
|
||||
const date = new Date(timestamp * 1000); // Конвертируем из Unix timestamp
|
||||
|
||||
let date;
|
||||
if (typeof timestamp === 'number') {
|
||||
// Unix timestamp
|
||||
date = new Date(timestamp * 1000);
|
||||
} else if (typeof timestamp === 'string') {
|
||||
// ISO string
|
||||
date = new Date(timestamp);
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
|
||||
return date.toLocaleDateString('ru-RU', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
@@ -355,6 +371,15 @@ function formatTimestamp(timestamp) {
|
||||
});
|
||||
}
|
||||
|
||||
function formatTokenAmount(amount) {
|
||||
if (!amount) return '0';
|
||||
const num = parseFloat(amount);
|
||||
if (num === 0) return '0';
|
||||
|
||||
// Всегда показываем полное число с разделителями тысяч
|
||||
return num.toLocaleString('ru-RU', { maximumFractionDigits: 0 });
|
||||
}
|
||||
|
||||
function openDleOnEtherscan(address) {
|
||||
window.open(`https://sepolia.etherscan.io/address/${address}`, '_blank');
|
||||
}
|
||||
|
||||
@@ -513,18 +513,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Предсказанный адрес DLE - отключено -->
|
||||
<!-- <div v-if="selectedNetworks.length > 0" class="predicted-address-section">
|
||||
<h5>📍 Адрес DLE во всех сетях:</h5>
|
||||
<div class="address-display">
|
||||
<code class="dle-address">{{ predictedAddress || 'Вычисляется...' }}</code>
|
||||
<button v-if="predictedAddress" @click="copyAddress" class="copy-btn" title="Копировать адрес">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
|
||||
|
||||
<!-- Кнопки управления RPC -->
|
||||
<div class="rpc-settings-actions">
|
||||
@@ -631,7 +619,7 @@
|
||||
|
||||
<!-- Требования к балансу -->
|
||||
<div v-if="selectedNetworks.length > 0" class="balance-requirements">
|
||||
<h5>💰 Требования к балансу:</h5>
|
||||
<h5>Требования к балансу:</h5>
|
||||
<div class="balance-grid">
|
||||
<div
|
||||
v-for="network in selectedNetworkDetails"
|
||||
@@ -655,7 +643,7 @@
|
||||
<i class="fas fa-shield-alt"></i>
|
||||
</div>
|
||||
<div class="security-content">
|
||||
<h5>🔒 Рекомендации по безопасности:</h5>
|
||||
<h5>Рекомендации по безопасности:</h5>
|
||||
<ul>
|
||||
<li>Используйте отдельный кошелек только для деплоя DLE</li>
|
||||
<li>Убедитесь, что на кошельке достаточно средств для оплаты газа</li>
|
||||
@@ -697,7 +685,7 @@
|
||||
<h4>Основная информация DLE</h4>
|
||||
|
||||
<div v-if="logoPreviewUrl" class="preview-item">
|
||||
<strong>🎨 Логотип:</strong>
|
||||
<strong>Логотип:</strong>
|
||||
<div style="display: flex; align-items: center; gap: 10px; margin-top: 5px;">
|
||||
<img :src="logoPreviewUrl" alt="Logo preview" style="width: 48px; height: 48px; border-radius: 6px; object-fit: contain; border: 1px solid #e9ecef;" />
|
||||
<span style="color: #666; font-size: 0.9em;">{{ logoFile?.name || 'ENS аватар' || 'Дефолтный логотип' }}</span>
|
||||
@@ -705,11 +693,11 @@
|
||||
</div>
|
||||
|
||||
<div v-if="dleSettings.name" class="preview-item">
|
||||
<strong>📋 Название:</strong> {{ dleSettings.name }}
|
||||
<strong>Название:</strong> {{ dleSettings.name }}
|
||||
</div>
|
||||
|
||||
<div v-if="dleSettings.tokenSymbol" class="preview-item">
|
||||
<strong>🪙 Токен:</strong> {{ dleSettings.tokenSymbol }}
|
||||
<strong>Токен:</strong> {{ dleSettings.tokenSymbol }}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -723,7 +711,7 @@
|
||||
|
||||
<div v-for="(partner, index) in dleSettings.partners" :key="index">
|
||||
<div v-if="partner.address || partner.amount > 1" class="preview-item">
|
||||
<strong>👥 Партнер {{ index + 1 }}:</strong>
|
||||
<strong>Партнер {{ index + 1 }}:</strong>
|
||||
<div class="partner-details">
|
||||
<div v-if="partner.address" class="partner-address">
|
||||
Адрес: {{ partner.address.substring(0, 10) }}...{{ partner.address.substring(partner.address.length - 8) }}
|
||||
@@ -736,11 +724,11 @@
|
||||
</div>
|
||||
|
||||
<div class="preview-item">
|
||||
<strong>💰 Общий эмиссия:</strong> {{ totalTokens }} токенов
|
||||
<strong>Общий эмиссия:</strong> {{ totalTokens }} токенов
|
||||
</div>
|
||||
|
||||
<div class="preview-item">
|
||||
<strong>🗳️ Кворум подписей партнеров:</strong> {{ dleSettings.governanceQuorum }}%
|
||||
<strong>Кворум подписей партнеров:</strong> {{ dleSettings.governanceQuorum }}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -749,11 +737,11 @@
|
||||
<h4>🔗 Мульти-чейн деплой</h4>
|
||||
|
||||
<!-- <div class="preview-item">
|
||||
<strong>📍 Адрес DLE:</strong> {{ predictedAddress || 'Вычисляется...' }}
|
||||
<strong> Адрес DLE:</strong> {{ predictedAddress || 'Вычисляется...' }}
|
||||
</div> -->
|
||||
|
||||
<div class="preview-item">
|
||||
<strong>🌐 Выбранные сети:</strong>
|
||||
<strong>Выбранные сети:</strong>
|
||||
<ul class="networks-list">
|
||||
<li v-for="network in selectedNetworkDetails" :key="network.chainId">
|
||||
{{ network.name }} (Chain ID: {{ network.chainId }}) - ~${{ network.estimatedCost }}
|
||||
@@ -762,7 +750,7 @@
|
||||
</div>
|
||||
|
||||
<div class="preview-item">
|
||||
<strong>💰 Общая стоимость:</strong> ~${{ totalDeployCost.toFixed(2) }}
|
||||
<strong>Общая стоимость:</strong> ~${{ totalDeployCost.toFixed(2) }}
|
||||
</div>
|
||||
|
||||
<!-- Предсказанные адреса скрыты, чтобы не создавать шум при отсутствии данных -->
|
||||
@@ -775,7 +763,7 @@
|
||||
<h4>🔐 Приватный ключ</h4>
|
||||
|
||||
<div class="preview-item">
|
||||
<strong>🔑 Ключ:</strong> ***{{ unifiedPrivateKey.slice(-4) }}
|
||||
<strong>Ключ:</strong> ***{{ unifiedPrivateKey.slice(-4) }}
|
||||
</div>
|
||||
|
||||
<div v-if="keyValidation.unified && keyValidation.unified.isValid" class="preview-item">
|
||||
@@ -830,7 +818,7 @@
|
||||
|
||||
<!-- Координаты -->
|
||||
<div v-if="dleSettings.coordinates" class="preview-item">
|
||||
<strong>📍 Координаты:</strong> {{ dleSettings.coordinates }}
|
||||
<strong>📍Координаты:</strong> {{ dleSettings.coordinates }}
|
||||
</div>
|
||||
|
||||
<!-- Кнопка деплоя смарт-контрактов -->
|
||||
@@ -866,8 +854,8 @@
|
||||
@click="deploySmartContracts"
|
||||
type="button"
|
||||
class="btn btn-primary btn-lg deploy-btn"
|
||||
:disabled="!isFormValid || !canEdit || adminTokenCheck.isLoading || showDeployProgress"
|
||||
:title="`isFormValid: ${isFormValid}, isAdmin: ${adminTokenCheck.isAdmin}, isLoading: ${adminTokenCheck.isLoading}, showDeployProgress: ${showDeployProgress}`"
|
||||
:disabled="!isFormValid || !canEdit || adminTokenCheck.isLoading"
|
||||
:title="`isFormValid: ${isFormValid}, isAdmin: ${adminTokenCheck.isAdmin}, isLoading: ${adminTokenCheck.isLoading}`"
|
||||
>
|
||||
<i class="fas fa-cogs"></i>
|
||||
Поэтапный деплой DLE
|
||||
@@ -877,48 +865,12 @@
|
||||
@click="clearAllData"
|
||||
class="btn btn-danger btn-lg clear-btn"
|
||||
title="Очистить все данные"
|
||||
:disabled="showDeployProgress"
|
||||
:disabled="false"
|
||||
>
|
||||
Удалить все
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Индикатор процесса деплоя -->
|
||||
<div v-if="showDeployProgress" class="deploy-progress">
|
||||
<div class="progress-header">
|
||||
<h4>🚀 Деплой DLE в блокчейне</h4>
|
||||
<p>{{ deployStatus }}</p>
|
||||
</div>
|
||||
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar">
|
||||
<div
|
||||
class="progress-fill"
|
||||
:style="{ width: deployProgress + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
<span class="progress-text">{{ deployProgress }}%</span>
|
||||
</div>
|
||||
|
||||
<div class="progress-steps">
|
||||
<div class="step" :class="{ active: deployProgress >= 10 }">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span>Подготовка данных</span>
|
||||
</div>
|
||||
<div class="step" :class="{ active: deployProgress >= 30 }">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span>Отправка на сервер</span>
|
||||
</div>
|
||||
<div class="step" :class="{ active: deployProgress >= 70 }">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span>Деплой в блокчейне</span>
|
||||
</div>
|
||||
<div class="step" :class="{ active: deployProgress >= 100 }">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span>Завершение</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -939,6 +891,7 @@
|
||||
:dle-data="dleSettings"
|
||||
:logo-uri="getLogoURI()"
|
||||
:etherscan-api-key="etherscanApiKey"
|
||||
:auto-verify-after-deploy="autoVerifyAfterDeploy"
|
||||
@deployment-completed="handleDeploymentCompleted"
|
||||
/>
|
||||
</div>
|
||||
@@ -1113,33 +1066,6 @@ const hasSelectedNetworks = computed(() => {
|
||||
return selectedNetworks.value.length > 0;
|
||||
});
|
||||
|
||||
// Инициализация при смене выбранных сетей
|
||||
// watch(selectedNetworkDetails, (nets) => {
|
||||
// if (nets && nets.length > 0) predictAddresses();
|
||||
// }, { immediate: true });
|
||||
|
||||
// Предсказание адресов (упрощенно через бэкенд) - отключено
|
||||
// async function predictAddresses() {
|
||||
// try {
|
||||
// isPredicting.value = true;
|
||||
// const payload = {
|
||||
// name: dleSettings.name,
|
||||
// symbol: dleSettings.tokenSymbol,
|
||||
// selectedNetworks: selectedNetworkDetails.value.map(n => n.chainId)
|
||||
// };
|
||||
// if (resp.data && resp.data.success && resp.data.data) {
|
||||
// // ожидаем вид { [chainId]: address }
|
||||
// Object.keys(predictedAddresses).forEach(k => delete predictedAddresses[k]);
|
||||
// Object.assign(predictedAddresses, resp.data.data);
|
||||
// }
|
||||
// } catch (e) {
|
||||
// console.error('Ошибка расчета предсказанных адресов:', e);
|
||||
// alert('Не удалось рассчитать предсказанные адреса');
|
||||
// } finally {
|
||||
// isPredicting.value = false;
|
||||
// }
|
||||
// }
|
||||
|
||||
function copyToClipboard(text) {
|
||||
navigator.clipboard?.writeText(text).then(() => {
|
||||
// no-op
|
||||
@@ -1190,10 +1116,6 @@ const selectedOkvedLevel4 = ref('');
|
||||
const currentSelectedOkvedCode = ref('');
|
||||
const currentSelectedOkvedText = ref('');
|
||||
|
||||
// Состояние процесса деплоя
|
||||
const showDeployProgress = ref(false);
|
||||
const deployProgress = ref(0);
|
||||
const deployStatus = ref('');
|
||||
|
||||
// Функция определения уровня ОКВЭД кода
|
||||
const getOkvedLevel = (code) => {
|
||||
@@ -2399,10 +2321,6 @@ watch(unifiedPrivateKey, (newValue) => {
|
||||
// Инициализация
|
||||
onMounted(() => {
|
||||
|
||||
// Сбрасываем состояние деплоя при загрузке страницы
|
||||
showDeployProgress.value = false;
|
||||
deployProgress.value = 0;
|
||||
deployStatus.value = '';
|
||||
|
||||
// Загружаем список стран
|
||||
loadCountries();
|
||||
@@ -2544,7 +2462,6 @@ const deploySmartContracts = async () => {
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка деплоя DLE:', error);
|
||||
showDeployProgress.value = false;
|
||||
alert('❌ Ошибка при деплое смарт-контракта: ' + error.message);
|
||||
}
|
||||
};
|
||||
@@ -2555,10 +2472,6 @@ const startStagedDeployment = async () => {
|
||||
|
||||
// Сначала выполняем стандартный деплой DLE контракта
|
||||
try {
|
||||
// Показываем индикатор процесса
|
||||
showDeployProgress.value = true;
|
||||
deployProgress.value = 10;
|
||||
deployStatus.value = 'Подготовка данных для деплоя DLE...';
|
||||
|
||||
// Подготовка данных для деплоя
|
||||
console.log('DEBUG: dleSettings.selectedNetworks:', dleSettings.selectedNetworks);
|
||||
@@ -2591,7 +2504,7 @@ const startStagedDeployment = async () => {
|
||||
privateKey: unifiedPrivateKey.value,
|
||||
// Верификация через Etherscan V2
|
||||
etherscanApiKey: etherscanApiKey.value,
|
||||
autoVerifyAfterDeploy: false // Отключаем автоверификацию для поэтапного деплоя
|
||||
autoVerifyAfterDeploy: autoVerifyAfterDeploy.value
|
||||
};
|
||||
|
||||
// Обработка логотипа
|
||||
@@ -2617,8 +2530,6 @@ const startStagedDeployment = async () => {
|
||||
console.log('Данные для деплоя DLE:', deployData);
|
||||
|
||||
// Предварительная проверка балансов (через приватный ключ)
|
||||
deployProgress.value = 20;
|
||||
deployStatus.value = 'Проверка баланса во всех выбранных сетях...';
|
||||
try {
|
||||
const pre = await api.post('/dle-v2/precheck', {
|
||||
supportedChainIds: deployData.supportedChainIds,
|
||||
@@ -2630,7 +2541,6 @@ const startStagedDeployment = async () => {
|
||||
if (lacks.length > 0) {
|
||||
const message = `❌ Недостаточно средств в некоторых сетях!`;
|
||||
alert(message);
|
||||
showDeployProgress.value = false;
|
||||
return;
|
||||
}
|
||||
console.log('✅ Проверка балансов пройдена:', preData.summary);
|
||||
@@ -2639,25 +2549,6 @@ const startStagedDeployment = async () => {
|
||||
console.warn('⚠️ Ошибка проверки балансов:', e.message);
|
||||
}
|
||||
|
||||
deployProgress.value = 30;
|
||||
deployStatus.value = 'Компиляция смарт-контрактов...';
|
||||
|
||||
// Автокомпиляция контрактов
|
||||
try {
|
||||
const compileResponse = await api.post('/compile-contracts');
|
||||
console.log('✅ Контракты скомпилированы:', compileResponse.data);
|
||||
} catch (compileError) {
|
||||
console.warn('⚠️ Ошибка автокомпиляции:', compileError.message);
|
||||
}
|
||||
|
||||
deployProgress.value = 40;
|
||||
deployStatus.value = 'Деплой DLE контракта...';
|
||||
|
||||
// Деплой будет выполнен в DeploymentWizard
|
||||
// Здесь только показываем мастер деплоя
|
||||
deployProgress.value = 80;
|
||||
deployStatus.value = 'Запуск мастера деплоя...';
|
||||
|
||||
// Показываем мастер деплоя
|
||||
showDeploymentWizard.value = true;
|
||||
|
||||
@@ -2665,8 +2556,6 @@ const startStagedDeployment = async () => {
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при запуске деплоя:', error);
|
||||
deployStatus.value = `❌ Ошибка: ${error.message}`;
|
||||
deployProgress.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2697,11 +2586,10 @@ const handleDeploymentCompleted = (result) => {
|
||||
console.log('🔍 Валидация формы:', validation);
|
||||
console.log('🔍 selectedNetworks.value:', selectedNetworks.value);
|
||||
console.log('🔍 adminTokenCheck:', adminTokenCheck.value);
|
||||
console.log('🔍 showDeployProgress:', showDeployProgress.value);
|
||||
console.log('🔍 unifiedPrivateKey.value:', unifiedPrivateKey.value);
|
||||
console.log('🔍 keyValidation.unified:', keyValidation.unified);
|
||||
console.log('🔍 dleSettings.coordinates:', dleSettings.coordinates);
|
||||
console.log('🔍 Кнопка должна быть активна:', !(!validation.jurisdiction || !validation.name || !validation.tokenSymbol || !validation.partners || !validation.partnersValid || !validation.quorum || !validation.networks || !validation.privateKey || !validation.keyValid || !validation.coordinates) && adminTokenCheck.value.isAdmin && !adminTokenCheck.value.isLoading && !showDeployProgress.value);
|
||||
console.log('🔍 Кнопка должна быть активна:', !(!validation.jurisdiction || !validation.name || !validation.tokenSymbol || !validation.partners || !validation.partnersValid || !validation.quorum || !validation.networks || !validation.privateKey || !validation.keyValid || !validation.coordinates) && adminTokenCheck.value.isAdmin && !adminTokenCheck.value.isLoading);
|
||||
|
||||
return Boolean(
|
||||
validation.jurisdiction &&
|
||||
@@ -4588,103 +4476,6 @@ async function submitDeploy() {
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
/* Стили для индикатора процесса деплоя */
|
||||
.deploy-progress {
|
||||
margin-top: 2rem;
|
||||
padding: 2rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
animation: fadeIn 0.5s ease;
|
||||
}
|
||||
|
||||
.progress-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.progress-header h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.progress-header p {
|
||||
margin: 0;
|
||||
opacity: 0.9;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
height: 12px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #4ade80 0%, #22c55e 100%);
|
||||
border-radius: 6px;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.progress-steps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
opacity: 0.5;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.step.active {
|
||||
opacity: 1;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.step i {
|
||||
font-size: 1.2rem;
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.step span {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Стили для загрузки картинки токена */
|
||||
.token-image-upload {
|
||||
|
||||
591
frontend/src/views/smartcontracts/CreateProposalView.vue
Normal file
591
frontend/src/views/smartcontracts/CreateProposalView.vue
Normal 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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -64,6 +64,17 @@ export default defineConfig({
|
||||
secure: false,
|
||||
credentials: true,
|
||||
rewrite: (path) => path,
|
||||
configure: (proxy, options) => {
|
||||
proxy.on('error', (err, req, res) => {
|
||||
console.log('WebSocket proxy error:', err.message);
|
||||
});
|
||||
proxy.on('proxyReqWs', (proxyReq, req, socket) => {
|
||||
console.log('WebSocket proxy request to:', req.url);
|
||||
});
|
||||
proxy.on('proxyResWs', (proxyRes, req, socket) => {
|
||||
console.log('WebSocket proxy response:', proxyRes.statusCode);
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
||||
Reference in New Issue
Block a user