ваше сообщение коммита
This commit is contained in:
@@ -1,499 +0,0 @@
|
||||
<!--
|
||||
Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
All rights reserved.
|
||||
|
||||
This software is proprietary and confidential.
|
||||
Unauthorized copying, modification, or distribution is prohibited.
|
||||
|
||||
For licensing inquiries: info@hb3-accelerator.com
|
||||
Website: https://hb3-accelerator.com
|
||||
GitHub: https://github.com/HB3-ACCELERATOR
|
||||
-->
|
||||
|
||||
<template>
|
||||
<BaseLayout
|
||||
:is-authenticated="isAuthenticated"
|
||||
:identities="identities"
|
||||
:token-balances="tokenBalances"
|
||||
:is-loading-tokens="isLoadingTokens"
|
||||
@auth-action-completed="$emit('auth-action-completed')"
|
||||
>
|
||||
<div class="dle-management-container">
|
||||
<!-- Заголовок -->
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<h1>Управление DLE</h1>
|
||||
<p>Интеграция с другими DLE и участие в кворумах</p>
|
||||
</div>
|
||||
<button class="close-btn" @click="router.push('/management')">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Карточки DLE -->
|
||||
<div class="dle-cards">
|
||||
<div
|
||||
v-for="dle in dleList"
|
||||
:key="dle.address"
|
||||
class="dle-card"
|
||||
@click="openDleInterface(dle)"
|
||||
>
|
||||
<div class="dle-card-header">
|
||||
<h3>{{ dle.name }}</h3>
|
||||
<button
|
||||
@click.stop="removeDle(dle.address)"
|
||||
class="remove-btn"
|
||||
title="Удалить DLE"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
<div class="dle-card-content">
|
||||
<p class="dle-address">Адрес: {{ formatAddress(dle.address) }}</p>
|
||||
<p class="dle-location">Местонахождение: {{ dle.location }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Кнопка добавления нового DLE -->
|
||||
<div class="dle-card add-dle-card" @click="showAddDleForm = true">
|
||||
<div class="add-dle-content">
|
||||
<div class="add-icon">+</div>
|
||||
<h3>Добавить DLE</h3>
|
||||
<p>Подключить новый DLE для управления</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно добавления DLE -->
|
||||
<div v-if="showAddDleForm" class="modal-overlay" @click="showAddDleForm = false">
|
||||
<div class="modal-content" @click.stop>
|
||||
<div class="modal-header">
|
||||
<h3>Добавить новый DLE</h3>
|
||||
<button @click="showAddDleForm = false" class="close-btn">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form @submit.prevent="addNewDle" class="add-dle-form">
|
||||
<div class="form-group">
|
||||
<label for="dleName">Название DLE:</label>
|
||||
<input
|
||||
id="dleName"
|
||||
v-model="newDle.name"
|
||||
type="text"
|
||||
placeholder="Введите название DLE"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="dleAddress">Адрес контракта:</label>
|
||||
<input
|
||||
id="dleAddress"
|
||||
v-model="newDle.address"
|
||||
type="text"
|
||||
placeholder="0x..."
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="dleLocation">Местонахождение:</label>
|
||||
<input
|
||||
id="dleLocation"
|
||||
v-model="newDle.location"
|
||||
type="text"
|
||||
placeholder="Страна, город"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" @click="showAddDleForm = false" class="btn-secondary">
|
||||
Отмена
|
||||
</button>
|
||||
<button type="submit" class="btn-primary">
|
||||
Добавить DLE
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps, defineEmits } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import BaseLayout from '../../components/BaseLayout.vue';
|
||||
|
||||
// Определяем props
|
||||
const props = defineProps({
|
||||
isAuthenticated: Boolean,
|
||||
identities: Array,
|
||||
tokenBalances: Object,
|
||||
isLoadingTokens: Boolean
|
||||
});
|
||||
|
||||
// Определяем emits
|
||||
const emit = defineEmits(['auth-action-completed']);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// Состояние
|
||||
const showAddDleForm = ref(false);
|
||||
const newDle = ref({
|
||||
name: '',
|
||||
address: '',
|
||||
location: ''
|
||||
});
|
||||
|
||||
// Список DLE (временные данные для демонстрации)
|
||||
const dleList = ref([
|
||||
{
|
||||
name: 'test2 (test2)',
|
||||
address: '0xef49...dfD8',
|
||||
location: '245000, 中国, 黄山市'
|
||||
},
|
||||
{
|
||||
name: 'My DLE',
|
||||
address: '0x1234...5678',
|
||||
location: '101000, Россия, Москва'
|
||||
}
|
||||
]);
|
||||
|
||||
// Методы
|
||||
const formatAddress = (address) => {
|
||||
if (!address) return '';
|
||||
if (address.length <= 10) return address;
|
||||
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
|
||||
};
|
||||
|
||||
const addNewDle = () => {
|
||||
if (!newDle.value.name || !newDle.value.address || !newDle.value.location) {
|
||||
return;
|
||||
}
|
||||
|
||||
dleList.value.push({
|
||||
name: newDle.value.name,
|
||||
address: newDle.value.address,
|
||||
location: newDle.value.location
|
||||
});
|
||||
|
||||
// Сброс формы
|
||||
newDle.value = {
|
||||
name: '',
|
||||
address: '',
|
||||
location: ''
|
||||
};
|
||||
|
||||
showAddDleForm.value = false;
|
||||
};
|
||||
|
||||
const removeDle = (address) => {
|
||||
dleList.value = dleList.value.filter(dle => dle.address !== address);
|
||||
};
|
||||
|
||||
const openDleInterface = (dle) => {
|
||||
// Здесь будет логика открытия интерфейса DLE
|
||||
// Вариант 1: Новая вкладка с внешним сайтом
|
||||
// window.open(`https://example.com/dle/${dle.address}`, '_blank');
|
||||
|
||||
// Вариант 2: Встроенный интерфейс в текущей вкладке
|
||||
router.push(`/management/dle/${dle.address}`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dle-management-container {
|
||||
padding: 20px;
|
||||
background-color: var(--color-white);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 40px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
color: var(--color-primary);
|
||||
font-size: 2.5rem;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: var(--color-grey-dark);
|
||||
font-size: 1.1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Карточки DLE */
|
||||
.dle-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 2rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.dle-card {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
padding: 1.5rem;
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
min-height: 150px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dle-card:hover {
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.dle-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.dle-card-header h3 {
|
||||
color: var(--color-primary);
|
||||
margin: 0;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.remove-btn:hover {
|
||||
background: #ffebee;
|
||||
}
|
||||
|
||||
.dle-card-content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.dle-address {
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-grey-dark);
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.dle-location {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-grey-dark);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Карточка добавления */
|
||||
.add-dle-card {
|
||||
border: 2px dashed #dee2e6;
|
||||
background: #f8f9fa;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.add-dle-card:hover {
|
||||
border-color: var(--color-primary);
|
||||
background: #f0f8ff;
|
||||
}
|
||||
|
||||
.add-dle-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
font-size: 2rem;
|
||||
color: var(--color-primary);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.add-dle-content h3 {
|
||||
color: var(--color-primary);
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.add-dle-content p {
|
||||
color: var(--color-grey-dark);
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Модальное окно */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: var(--color-grey-dark);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* Форма */
|
||||
.add-dle-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
color: var(--color-grey-dark);
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--color-secondary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--color-secondary-dark);
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.dle-cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dle-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
815
frontend/src/views/smartcontracts/DleModulesView.vue
Normal file
815
frontend/src/views/smartcontracts/DleModulesView.vue
Normal file
@@ -0,0 +1,815 @@
|
||||
<!--
|
||||
Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
All rights reserved.
|
||||
|
||||
This software is proprietary and confidential.
|
||||
Unauthorized copying, modification, or distribution is prohibited.
|
||||
|
||||
For licensing inquiries: info@hb3-accelerator.com
|
||||
Website: https://hb3-accelerator.com
|
||||
GitHub: https://github.com/HB3-ACCELERATOR
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="dle-modules-management">
|
||||
<div class="modules-header">
|
||||
<h3>🧩 Управление модулями</h3>
|
||||
<button class="btn btn-primary" @click="showAddModuleForm = true">
|
||||
<i class="fas fa-plus"></i> Добавить модуль
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Форма добавления модуля -->
|
||||
<div v-if="showAddModuleForm" class="add-module-form">
|
||||
<div class="form-header">
|
||||
<h4>🧩 Добавить модуль</h4>
|
||||
<button class="close-btn" @click="showAddModuleForm = false">×</button>
|
||||
</div>
|
||||
|
||||
<div class="form-content">
|
||||
<!-- Информация о модуле -->
|
||||
<div class="form-section">
|
||||
<h5>📋 Информация о модуле</h5>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="moduleId">ID модуля:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="moduleId"
|
||||
v-model="newModule.moduleId"
|
||||
class="form-control"
|
||||
placeholder="TreasuryModule"
|
||||
>
|
||||
<small class="form-help">Уникальный идентификатор модуля (например: TreasuryModule)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="moduleAddress">Адрес модуля:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="moduleAddress"
|
||||
v-model="newModule.moduleAddress"
|
||||
class="form-control"
|
||||
placeholder="0x..."
|
||||
>
|
||||
<small class="form-help">Адрес смарт-контракта модуля</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="moduleName">Название модуля:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="moduleName"
|
||||
v-model="newModule.name"
|
||||
class="form-control"
|
||||
placeholder="Казначейство"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="moduleDescription">Описание модуля:</label>
|
||||
<textarea
|
||||
id="moduleDescription"
|
||||
v-model="newModule.description"
|
||||
class="form-control"
|
||||
rows="3"
|
||||
placeholder="Описание функциональности модуля..."
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Выбор типа модуля -->
|
||||
<div class="form-section">
|
||||
<h5>🔧 Тип модуля</h5>
|
||||
|
||||
<div class="module-types">
|
||||
<div class="form-group">
|
||||
<label for="moduleType">Выберите тип модуля:</label>
|
||||
<select id="moduleType" v-model="newModule.type" class="form-control">
|
||||
<option value="">-- Выберите тип --</option>
|
||||
<option value="treasury">Казначейство</option>
|
||||
<option value="voting">Голосование</option>
|
||||
<option value="communication">Коммуникации</option>
|
||||
<option value="custom">Пользовательский</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Специфичные настройки для казначейства -->
|
||||
<div v-if="newModule.type === 'treasury'" class="module-settings">
|
||||
<h6>Настройки казначейства</h6>
|
||||
<div class="form-group">
|
||||
<label for="treasuryTokens">Токены для управления:</label>
|
||||
<select id="treasuryTokens" v-model="newModule.settings.tokens" multiple class="form-control">
|
||||
<option value="ETH">ETH</option>
|
||||
<option value="USDT">USDT</option>
|
||||
<option value="USDC">USDC</option>
|
||||
<option value="DAI">DAI</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="treasuryLimit">Лимит операций:</label>
|
||||
<input
|
||||
type="number"
|
||||
id="treasuryLimit"
|
||||
v-model.number="newModule.settings.limit"
|
||||
class="form-control"
|
||||
placeholder="1000"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Специфичные настройки для голосования -->
|
||||
<div v-if="newModule.type === 'voting'" class="module-settings">
|
||||
<h6>Настройки голосования</h6>
|
||||
<div class="form-group">
|
||||
<label for="votingType">Тип голосования:</label>
|
||||
<select id="votingType" v-model="newModule.settings.votingType" class="form-control">
|
||||
<option value="simple">Простое большинство</option>
|
||||
<option value="weighted">Взвешенное голосование</option>
|
||||
<option value="quadratic">Квадратичное голосование</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="votingDuration">Длительность голосования (дни):</label>
|
||||
<input
|
||||
type="number"
|
||||
id="votingDuration"
|
||||
v-model.number="newModule.settings.duration"
|
||||
class="form-control"
|
||||
min="1"
|
||||
max="30"
|
||||
placeholder="7"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Специфичные настройки для коммуникаций -->
|
||||
<div v-if="newModule.type === 'communication'" class="module-settings">
|
||||
<h6>Настройки коммуникаций</h6>
|
||||
<div class="form-group">
|
||||
<label for="communicationChannels">Каналы связи:</label>
|
||||
<div class="checkbox-group">
|
||||
<label><input type="checkbox" v-model="newModule.settings.channels.email"> Email</label>
|
||||
<label><input type="checkbox" v-model="newModule.settings.channels.telegram"> Telegram</label>
|
||||
<label><input type="checkbox" v-model="newModule.settings.channels.discord"> Discord</label>
|
||||
<label><input type="checkbox" v-model="newModule.settings.channels.slack"> Slack</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Предварительный просмотр -->
|
||||
<div class="form-section">
|
||||
<h5>👁️ Предварительный просмотр</h5>
|
||||
<div class="preview-card">
|
||||
<div class="preview-item">
|
||||
<strong>ID модуля:</strong> {{ newModule.moduleId || 'Не указан' }}
|
||||
</div>
|
||||
<div class="preview-item">
|
||||
<strong>Адрес:</strong> {{ shortenAddress(newModule.moduleAddress) || 'Не указан' }}
|
||||
</div>
|
||||
<div class="preview-item">
|
||||
<strong>Название:</strong> {{ newModule.name || 'Не указано' }}
|
||||
</div>
|
||||
<div class="preview-item">
|
||||
<strong>Тип:</strong> {{ getModuleTypeName(newModule.type) || 'Не выбран' }}
|
||||
</div>
|
||||
<div class="preview-item">
|
||||
<strong>Описание:</strong> {{ newModule.description || 'Не указано' }}
|
||||
</div>
|
||||
<div v-if="newModule.type" class="preview-item">
|
||||
<strong>Настройки:</strong> {{ getModuleSettingsPreview() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Действия -->
|
||||
<div class="form-actions">
|
||||
<button
|
||||
class="btn btn-success"
|
||||
@click="addModule"
|
||||
:disabled="!isFormValid || isAdding"
|
||||
>
|
||||
<i class="fas fa-plus"></i>
|
||||
{{ isAdding ? 'Добавление...' : 'Добавить модуль' }}
|
||||
</button>
|
||||
<button class="btn btn-secondary" @click="resetForm">
|
||||
<i class="fas fa-undo"></i> Сбросить
|
||||
</button>
|
||||
<button class="btn btn-danger" @click="showAddModuleForm = false">
|
||||
<i class="fas fa-times"></i> Отмена
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Список модулей -->
|
||||
<div class="modules-list">
|
||||
<div class="list-header">
|
||||
<h4>📋 Установленные модули</h4>
|
||||
<div class="list-filters">
|
||||
<select v-model="typeFilter" class="form-control">
|
||||
<option value="">Все типы</option>
|
||||
<option value="treasury">Казначейство</option>
|
||||
<option value="voting">Голосование</option>
|
||||
<option value="communication">Коммуникации</option>
|
||||
<option value="custom">Пользовательские</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="filteredModules.length === 0" class="no-modules">
|
||||
<p>Установленных модулей пока нет</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="modules-grid">
|
||||
<div
|
||||
v-for="module in filteredModules"
|
||||
:key="module.moduleId"
|
||||
class="module-card"
|
||||
:class="module.type"
|
||||
>
|
||||
<div class="module-header">
|
||||
<h5>{{ module.name }}</h5>
|
||||
<span class="module-type">{{ getModuleTypeName(module.type) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="module-details">
|
||||
<div class="detail-item">
|
||||
<strong>ID:</strong> {{ module.moduleId }}
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<strong>Адрес:</strong> {{ shortenAddress(module.moduleAddress) }}
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<strong>Описание:</strong> {{ module.description }}
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<strong>Статус:</strong>
|
||||
<span class="module-status" :class="{ 'active': module.isActive }">
|
||||
{{ module.isActive ? 'Активен' : 'Неактивен' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="module-actions">
|
||||
<button
|
||||
class="btn btn-sm btn-info"
|
||||
@click="viewModuleDetails(module.moduleId)"
|
||||
>
|
||||
<i class="fas fa-eye"></i> Детали
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-warning"
|
||||
@click="configureModule(module.moduleId)"
|
||||
>
|
||||
<i class="fas fa-cog"></i> Настроить
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-danger"
|
||||
@click="removeModule(module.moduleId)"
|
||||
>
|
||||
<i class="fas fa-trash"></i> Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Доступные модули -->
|
||||
<div class="available-modules">
|
||||
<h4>📦 Доступные модули</h4>
|
||||
<p>Модули, которые можно установить в вашем DLE</p>
|
||||
|
||||
<div class="available-modules-grid">
|
||||
<div
|
||||
v-for="availableModule in availableModules"
|
||||
:key="availableModule.id"
|
||||
class="available-module-card"
|
||||
>
|
||||
<div class="module-icon">
|
||||
<i :class="availableModule.icon"></i>
|
||||
</div>
|
||||
<div class="module-info">
|
||||
<h6>{{ availableModule.name }}</h6>
|
||||
<p>{{ availableModule.description }}</p>
|
||||
<div class="module-features">
|
||||
<span v-for="feature in availableModule.features" :key="feature" class="feature-tag">
|
||||
{{ feature }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-actions">
|
||||
<button
|
||||
class="btn btn-sm btn-success"
|
||||
@click="installAvailableModule(availableModule)"
|
||||
>
|
||||
<i class="fas fa-download"></i> Установить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useAuthContext } from '@/composables/useAuth';
|
||||
|
||||
const props = defineProps({
|
||||
dleAddress: { type: String, required: true },
|
||||
dleContract: { type: Object, required: true }
|
||||
});
|
||||
|
||||
const { address } = useAuthContext();
|
||||
|
||||
// Состояние формы
|
||||
const showAddModuleForm = ref(false);
|
||||
const isAdding = ref(false);
|
||||
const typeFilter = ref('');
|
||||
|
||||
// Новый модуль
|
||||
const newModule = ref({
|
||||
moduleId: '',
|
||||
moduleAddress: '',
|
||||
name: '',
|
||||
description: '',
|
||||
type: '',
|
||||
settings: {
|
||||
tokens: [],
|
||||
limit: 0,
|
||||
votingType: 'simple',
|
||||
duration: 7,
|
||||
channels: {
|
||||
email: false,
|
||||
telegram: false,
|
||||
discord: false,
|
||||
slack: false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Установленные модули
|
||||
const modules = ref([]);
|
||||
|
||||
// Доступные модули
|
||||
const availableModules = ref([
|
||||
{
|
||||
id: 'treasury',
|
||||
name: 'Казначейство',
|
||||
description: 'Управление финансами DLE, прием и отправка токенов',
|
||||
icon: 'fas fa-coins',
|
||||
features: ['Управление токенами', 'Бюджетирование', 'Отчетность'],
|
||||
type: 'treasury'
|
||||
},
|
||||
{
|
||||
id: 'hierarchical-voting',
|
||||
name: 'Иерархическое голосование',
|
||||
description: 'Продвинутая система голосования с иерархией',
|
||||
icon: 'fas fa-sitemap',
|
||||
features: ['Иерархия', 'Взвешенное голосование', 'Делегирование'],
|
||||
type: 'voting'
|
||||
},
|
||||
{
|
||||
id: 'communication',
|
||||
name: 'Коммуникации',
|
||||
description: 'Система уведомлений и коммуникаций',
|
||||
icon: 'fas fa-comments',
|
||||
features: ['Уведомления', 'Каналы связи', 'Автоматизация'],
|
||||
type: 'communication'
|
||||
},
|
||||
{
|
||||
id: 'analytics',
|
||||
name: 'Аналитика',
|
||||
description: 'Аналитические инструменты для DLE',
|
||||
icon: 'fas fa-chart-line',
|
||||
features: ['Статистика', 'Графики', 'Отчеты'],
|
||||
type: 'custom'
|
||||
}
|
||||
]);
|
||||
|
||||
// Вычисляемые свойства
|
||||
const isFormValid = computed(() => {
|
||||
return (
|
||||
newModule.value.moduleId &&
|
||||
newModule.value.moduleAddress &&
|
||||
newModule.value.name &&
|
||||
newModule.value.type
|
||||
);
|
||||
});
|
||||
|
||||
const filteredModules = computed(() => {
|
||||
if (!typeFilter.value) return modules.value;
|
||||
return modules.value.filter(m => m.type === typeFilter.value);
|
||||
});
|
||||
|
||||
// Функции
|
||||
function getModuleTypeName(type) {
|
||||
const types = {
|
||||
'treasury': 'Казначейство',
|
||||
'voting': 'Голосование',
|
||||
'communication': 'Коммуникации',
|
||||
'custom': 'Пользовательский'
|
||||
};
|
||||
return types[type] || 'Неизвестный тип';
|
||||
}
|
||||
|
||||
function getModuleSettingsPreview() {
|
||||
const settings = newModule.value.settings;
|
||||
|
||||
switch (newModule.value.type) {
|
||||
case 'treasury':
|
||||
return `Токены: ${settings.tokens.join(', ') || 'Не выбраны'}, Лимит: ${settings.limit}`;
|
||||
case 'voting':
|
||||
return `Тип: ${settings.votingType}, Длительность: ${settings.duration} дней`;
|
||||
case 'communication':
|
||||
const channels = Object.entries(settings.channels)
|
||||
.filter(([_, enabled]) => enabled)
|
||||
.map(([name, _]) => name);
|
||||
return `Каналы: ${channels.join(', ') || 'Не выбраны'}`;
|
||||
default:
|
||||
return 'Нет настроек';
|
||||
}
|
||||
}
|
||||
|
||||
function shortenAddress(address) {
|
||||
if (!address) return '';
|
||||
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||
}
|
||||
|
||||
// Добавление модуля
|
||||
async function addModule() {
|
||||
if (!isFormValid.value) {
|
||||
alert('Пожалуйста, заполните все обязательные поля');
|
||||
return;
|
||||
}
|
||||
|
||||
isAdding.value = true;
|
||||
|
||||
try {
|
||||
// Вызов смарт-контракта
|
||||
const tx = await props.dleContract.addModule(
|
||||
newModule.value.moduleId,
|
||||
newModule.value.moduleAddress
|
||||
);
|
||||
|
||||
await tx.wait();
|
||||
|
||||
// Обновляем список модулей
|
||||
await loadModules();
|
||||
|
||||
// Сбрасываем форму
|
||||
resetForm();
|
||||
showAddModuleForm.value = false;
|
||||
|
||||
alert('✅ Модуль успешно добавлен!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при добавлении модуля:', error);
|
||||
alert('❌ Ошибка при добавлении модуля: ' + error.message);
|
||||
} finally {
|
||||
isAdding.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Удаление модуля
|
||||
async function removeModule(moduleId) {
|
||||
if (!confirm(`Удалить модуль "${moduleId}"?`)) return;
|
||||
|
||||
try {
|
||||
const tx = await props.dleContract.removeModule(moduleId);
|
||||
await tx.wait();
|
||||
|
||||
await loadModules();
|
||||
alert('✅ Модуль успешно удален!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при удалении модуля:', error);
|
||||
alert('❌ Ошибка при удалении модуля: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Установка доступного модуля
|
||||
async function installAvailableModule(availableModule) {
|
||||
// Здесь должна быть логика установки модуля
|
||||
// Например, деплой модуля и добавление в DLE
|
||||
console.log('Установка модуля:', availableModule);
|
||||
alert(`Модуль "${availableModule.name}" будет установлен`);
|
||||
}
|
||||
|
||||
// Загрузка модулей
|
||||
async function loadModules() {
|
||||
try {
|
||||
// Здесь должен быть вызов API или смарт-контракта для загрузки модулей
|
||||
// Пока используем заглушку
|
||||
modules.value = [];
|
||||
} catch (error) {
|
||||
console.error('Ошибка при загрузке модулей:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
newModule.value = {
|
||||
moduleId: '',
|
||||
moduleAddress: '',
|
||||
name: '',
|
||||
description: '',
|
||||
type: '',
|
||||
settings: {
|
||||
tokens: [],
|
||||
limit: 0,
|
||||
votingType: 'simple',
|
||||
duration: 7,
|
||||
channels: {
|
||||
email: false,
|
||||
telegram: false,
|
||||
discord: false,
|
||||
slack: false
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function viewModuleDetails(moduleId) {
|
||||
// Открыть модальное окно с деталями модуля
|
||||
console.log('Просмотр деталей модуля:', moduleId);
|
||||
}
|
||||
|
||||
function configureModule(moduleId) {
|
||||
// Открыть форму настройки модуля
|
||||
console.log('Настройка модуля:', moduleId);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadModules();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dle-modules-management {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.modules-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.add-module-form {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.form-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.form-section:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.form-section h5 {
|
||||
color: #333;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.module-types {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.module-settings {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.module-settings h6 {
|
||||
color: #333;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.checkbox-group label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.preview-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.modules-list {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.modules-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.module-card {
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.module-card.treasury {
|
||||
border-left: 4px solid #28a745;
|
||||
}
|
||||
|
||||
.module-card.voting {
|
||||
border-left: 4px solid #007bff;
|
||||
}
|
||||
|
||||
.module-card.communication {
|
||||
border-left: 4px solid #ffc107;
|
||||
}
|
||||
|
||||
.module-card.custom {
|
||||
border-left: 4px solid #6c757d;
|
||||
}
|
||||
|
||||
.module-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.module-header h5 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.module-type {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
background: #e9ecef;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.module-details {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.module-status {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.module-status.active {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.module-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.no-modules {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.available-modules {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.available-modules h4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.available-modules p {
|
||||
color: #666;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.available-modules-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.available-module-card {
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.module-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #007bff;
|
||||
font-size: 1.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.module-info {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.module-info h6 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.module-info p {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.module-features {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.feature-tag {
|
||||
background: #e9ecef;
|
||||
color: #495057;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.form-help {
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
711
frontend/src/views/smartcontracts/DleMultisigView.vue
Normal file
711
frontend/src/views/smartcontracts/DleMultisigView.vue
Normal file
@@ -0,0 +1,711 @@
|
||||
<!--
|
||||
Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
All rights reserved.
|
||||
|
||||
This software is proprietary and confidential.
|
||||
Unauthorized copying, modification, or distribution is prohibited.
|
||||
|
||||
For licensing inquiries: info@hb3-accelerator.com
|
||||
Website: https://hb3-accelerator.com
|
||||
GitHub: https://github.com/HB3-ACCELERATOR
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="dle-multisig-management">
|
||||
<div class="multisig-header">
|
||||
<h3>🔐 Управление мультиподписью</h3>
|
||||
<button class="btn btn-primary" @click="showCreateForm = true">
|
||||
<i class="fas fa-plus"></i> Создать операцию
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Форма создания мультиподписи -->
|
||||
<div v-if="showCreateForm" class="create-multisig-form">
|
||||
<div class="form-header">
|
||||
<h4>🔐 Новая мультиподпись</h4>
|
||||
<button class="close-btn" @click="showCreateForm = false">×</button>
|
||||
</div>
|
||||
|
||||
<div class="form-content">
|
||||
<!-- Описание операции -->
|
||||
<div class="form-section">
|
||||
<h5>📝 Описание операции</h5>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="operationDescription">Описание операции:</label>
|
||||
<textarea
|
||||
id="operationDescription"
|
||||
v-model="newOperation.description"
|
||||
class="form-control"
|
||||
rows="3"
|
||||
placeholder="Опишите, что нужно сделать..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="operationDuration">Длительность сбора подписей (дни):</label>
|
||||
<input
|
||||
type="number"
|
||||
id="operationDuration"
|
||||
v-model.number="newOperation.duration"
|
||||
class="form-control"
|
||||
min="1"
|
||||
max="30"
|
||||
placeholder="7"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Тип операции -->
|
||||
<div class="form-section">
|
||||
<h5>⚙️ Тип операции</h5>
|
||||
|
||||
<div class="operation-types">
|
||||
<div class="form-group">
|
||||
<label for="multisigOperationType">Выберите тип операции:</label>
|
||||
<select id="multisigOperationType" v-model="newOperation.operationType" class="form-control">
|
||||
<option value="">-- Выберите тип --</option>
|
||||
<option value="transfer">Передача токенов</option>
|
||||
<option value="mint">Минтинг токенов</option>
|
||||
<option value="burn">Сжигание токенов</option>
|
||||
<option value="addModule">Добавить модуль</option>
|
||||
<option value="removeModule">Удалить модуль</option>
|
||||
<option value="custom">Пользовательская операция</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Параметры для передачи токенов -->
|
||||
<div v-if="newOperation.operationType === 'transfer'" class="operation-params">
|
||||
<div class="form-group">
|
||||
<label for="multisigTransferTo">Адрес получателя:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="multisigTransferTo"
|
||||
v-model="newOperation.operationParams.to"
|
||||
class="form-control"
|
||||
placeholder="0x..."
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="multisigTransferAmount">Количество токенов:</label>
|
||||
<input
|
||||
type="number"
|
||||
id="multisigTransferAmount"
|
||||
v-model.number="newOperation.operationParams.amount"
|
||||
class="form-control"
|
||||
min="1"
|
||||
placeholder="100"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Параметры для модулей -->
|
||||
<div v-if="newOperation.operationType === 'addModule' || newOperation.operationType === 'removeModule'" class="operation-params">
|
||||
<div class="form-group">
|
||||
<label for="moduleId">ID модуля:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="moduleId"
|
||||
v-model="newOperation.operationParams.moduleId"
|
||||
class="form-control"
|
||||
placeholder="TreasuryModule"
|
||||
>
|
||||
</div>
|
||||
<div v-if="newOperation.operationType === 'addModule'" class="form-group">
|
||||
<label for="moduleAddress">Адрес модуля:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="moduleAddress"
|
||||
v-model="newOperation.operationParams.moduleAddress"
|
||||
class="form-control"
|
||||
placeholder="0x..."
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Пользовательская операция -->
|
||||
<div v-if="newOperation.operationType === 'custom'" class="operation-params">
|
||||
<div class="form-group">
|
||||
<label for="customMultisigOperation">Пользовательская операция (hex):</label>
|
||||
<textarea
|
||||
id="customMultisigOperation"
|
||||
v-model="newOperation.operationParams.customData"
|
||||
class="form-control"
|
||||
rows="3"
|
||||
placeholder="0x..."
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Предварительный просмотр -->
|
||||
<div class="form-section">
|
||||
<h5>👁️ Предварительный просмотр</h5>
|
||||
<div class="preview-card">
|
||||
<div class="preview-item">
|
||||
<strong>Описание:</strong> {{ newOperation.description || 'Не указано' }}
|
||||
</div>
|
||||
<div class="preview-item">
|
||||
<strong>Длительность:</strong> {{ newOperation.duration || 7 }} дней
|
||||
</div>
|
||||
<div class="preview-item">
|
||||
<strong>Тип операции:</strong> {{ getOperationTypeName(newOperation.operationType) || 'Не выбран' }}
|
||||
</div>
|
||||
<div v-if="newOperation.operationType" class="preview-item">
|
||||
<strong>Параметры:</strong> {{ getOperationParamsPreview() }}
|
||||
</div>
|
||||
<div class="preview-item">
|
||||
<strong>Хеш операции:</strong> {{ getOperationHash() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Действия -->
|
||||
<div class="form-actions">
|
||||
<button
|
||||
class="btn btn-success"
|
||||
@click="createMultisigOperation"
|
||||
:disabled="!isFormValid || isCreating"
|
||||
>
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
{{ isCreating ? 'Создание...' : 'Создать операцию' }}
|
||||
</button>
|
||||
<button class="btn btn-secondary" @click="resetForm">
|
||||
<i class="fas fa-undo"></i> Сбросить
|
||||
</button>
|
||||
<button class="btn btn-danger" @click="showCreateForm = false">
|
||||
<i class="fas fa-times"></i> Отмена
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Список операций мультиподписи -->
|
||||
<div class="multisig-list">
|
||||
<div class="list-header">
|
||||
<h4>📋 Список операций мультиподписи</h4>
|
||||
<div class="list-filters">
|
||||
<select v-model="statusFilter" class="form-control">
|
||||
<option value="">Все статусы</option>
|
||||
<option value="active">Активные</option>
|
||||
<option value="pending">Ожидающие</option>
|
||||
<option value="succeeded">Принятые</option>
|
||||
<option value="defeated">Отклоненные</option>
|
||||
<option value="executed">Выполненные</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="filteredOperations.length === 0" class="no-operations">
|
||||
<p>Операций мультиподписи пока нет</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="operations-grid">
|
||||
<div
|
||||
v-for="operation in filteredOperations"
|
||||
:key="operation.id"
|
||||
class="operation-card"
|
||||
:class="operation.status"
|
||||
>
|
||||
<div class="operation-header">
|
||||
<h5>{{ operation.description }}</h5>
|
||||
<span class="operation-status" :class="operation.status">
|
||||
{{ getOperationStatusText(operation.status) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="operation-details">
|
||||
<div class="detail-item">
|
||||
<strong>ID:</strong> #{{ operation.id }}
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<strong>Создатель:</strong> {{ shortenAddress(operation.initiator) }}
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<strong>Хеш:</strong> {{ shortenAddress(operation.operationHash) }}
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<strong>Дедлайн:</strong> {{ formatDate(operation.deadline) }}
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<strong>Подписи:</strong>
|
||||
<span class="signatures">
|
||||
<span class="for">За: {{ operation.forSignatures }}</span>
|
||||
<span class="against">Против: {{ operation.againstSignatures }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="operation-actions">
|
||||
<button
|
||||
v-if="canSign(operation)"
|
||||
class="btn btn-sm btn-success"
|
||||
@click="signOperation(operation.id, true)"
|
||||
:disabled="hasSigned(operation.id, true)"
|
||||
>
|
||||
<i class="fas fa-thumbs-up"></i> Подписать за
|
||||
</button>
|
||||
<button
|
||||
v-if="canSign(operation)"
|
||||
class="btn btn-sm btn-danger"
|
||||
@click="signOperation(operation.id, false)"
|
||||
:disabled="hasSigned(operation.id, false)"
|
||||
>
|
||||
<i class="fas fa-thumbs-down"></i> Подписать против
|
||||
</button>
|
||||
<button
|
||||
v-if="canExecute(operation)"
|
||||
class="btn btn-sm btn-primary"
|
||||
@click="executeOperation(operation.id)"
|
||||
>
|
||||
<i class="fas fa-play"></i> Исполнить
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-info"
|
||||
@click="viewOperationDetails(operation.id)"
|
||||
>
|
||||
<i class="fas fa-eye"></i> Детали
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useAuthContext } from '@/composables/useAuth';
|
||||
|
||||
const props = defineProps({
|
||||
dleAddress: { type: String, required: true },
|
||||
dleContract: { type: Object, required: true }
|
||||
});
|
||||
|
||||
const { address } = useAuthContext();
|
||||
|
||||
// Состояние формы
|
||||
const showCreateForm = ref(false);
|
||||
const isCreating = ref(false);
|
||||
const statusFilter = ref('');
|
||||
|
||||
// Новая операция
|
||||
const newOperation = ref({
|
||||
description: '',
|
||||
duration: 7,
|
||||
operationType: '',
|
||||
operationParams: {
|
||||
to: '',
|
||||
from: '',
|
||||
amount: 0,
|
||||
moduleId: '',
|
||||
moduleAddress: '',
|
||||
customData: ''
|
||||
}
|
||||
});
|
||||
|
||||
// Операции мультиподписи
|
||||
const operations = ref([]);
|
||||
|
||||
// Вычисляемые свойства
|
||||
const isFormValid = computed(() => {
|
||||
return (
|
||||
newOperation.value.description &&
|
||||
newOperation.value.duration > 0 &&
|
||||
newOperation.value.operationType &&
|
||||
validateOperationParams()
|
||||
);
|
||||
});
|
||||
|
||||
const filteredOperations = computed(() => {
|
||||
if (!statusFilter.value) return operations.value;
|
||||
return operations.value.filter(o => o.status === statusFilter.value);
|
||||
});
|
||||
|
||||
// Функции
|
||||
function validateOperationParams() {
|
||||
const params = newOperation.value.operationParams;
|
||||
|
||||
switch (newOperation.value.operationType) {
|
||||
case 'transfer':
|
||||
case 'mint':
|
||||
return params.to && params.amount > 0;
|
||||
case 'burn':
|
||||
return params.from && params.amount > 0;
|
||||
case 'addModule':
|
||||
return params.moduleId && params.moduleAddress;
|
||||
case 'removeModule':
|
||||
return params.moduleId;
|
||||
case 'custom':
|
||||
return params.customData && params.customData.startsWith('0x');
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getOperationTypeName(type) {
|
||||
const types = {
|
||||
'transfer': 'Передача токенов',
|
||||
'mint': 'Минтинг токенов',
|
||||
'burn': 'Сжигание токенов',
|
||||
'addModule': 'Добавить модуль',
|
||||
'removeModule': 'Удалить модуль',
|
||||
'custom': 'Пользовательская операция'
|
||||
};
|
||||
return types[type] || 'Неизвестный тип';
|
||||
}
|
||||
|
||||
function getOperationParamsPreview() {
|
||||
const params = newOperation.value.operationParams;
|
||||
|
||||
switch (newOperation.value.operationType) {
|
||||
case 'transfer':
|
||||
return `Кому: ${shortenAddress(params.to)}, Количество: ${params.amount}`;
|
||||
case 'mint':
|
||||
return `Кому: ${shortenAddress(params.to)}, Количество: ${params.amount}`;
|
||||
case 'burn':
|
||||
return `От: ${shortenAddress(params.from)}, Количество: ${params.amount}`;
|
||||
case 'addModule':
|
||||
return `ID: ${params.moduleId}, Адрес: ${shortenAddress(params.moduleAddress)}`;
|
||||
case 'removeModule':
|
||||
return `ID: ${params.moduleId}`;
|
||||
case 'custom':
|
||||
return `Данные: ${params.customData.substring(0, 20)}...`;
|
||||
default:
|
||||
return 'Не указаны';
|
||||
}
|
||||
}
|
||||
|
||||
function getOperationHash() {
|
||||
// Генерируем хеш операции на основе параметров
|
||||
const params = newOperation.value.operationParams;
|
||||
const operationData = JSON.stringify({
|
||||
type: newOperation.value.operationType,
|
||||
params: params
|
||||
});
|
||||
|
||||
// Простой хеш для демонстрации
|
||||
return '0x' + btoa(operationData).substring(0, 64);
|
||||
}
|
||||
|
||||
function shortenAddress(address) {
|
||||
if (!address) return '';
|
||||
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||
}
|
||||
|
||||
function formatDate(timestamp) {
|
||||
if (!timestamp) return 'N/A';
|
||||
return new Date(timestamp * 1000).toLocaleString();
|
||||
}
|
||||
|
||||
function getOperationStatusText(status) {
|
||||
const statusMap = {
|
||||
'pending': 'Ожидает',
|
||||
'active': 'Активно',
|
||||
'succeeded': 'Принято',
|
||||
'defeated': 'Отклонено',
|
||||
'executed': 'Выполнено'
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
}
|
||||
|
||||
function canSign(operation) {
|
||||
return operation.status === 'active' && !hasSigned(operation.id);
|
||||
}
|
||||
|
||||
function canExecute(operation) {
|
||||
return operation.status === 'succeeded' && !operation.executed;
|
||||
}
|
||||
|
||||
function hasSigned(operationId, support = null) {
|
||||
// Здесь должна быть проверка подписи пользователя
|
||||
return false;
|
||||
}
|
||||
|
||||
// Создание операции мультиподписи
|
||||
async function createMultisigOperation() {
|
||||
if (!isFormValid.value) {
|
||||
alert('Пожалуйста, заполните все обязательные поля');
|
||||
return;
|
||||
}
|
||||
|
||||
isCreating.value = true;
|
||||
|
||||
try {
|
||||
// Генерируем хеш операции
|
||||
const operationHash = getOperationHash();
|
||||
|
||||
// Вызов смарт-контракта
|
||||
const tx = await props.dleContract.createMultiSigOperation(
|
||||
operationHash,
|
||||
newOperation.value.duration * 24 * 60 * 60 // конвертируем в секунды
|
||||
);
|
||||
|
||||
await tx.wait();
|
||||
|
||||
// Обновляем список операций
|
||||
await loadOperations();
|
||||
|
||||
// Сбрасываем форму
|
||||
resetForm();
|
||||
showCreateForm.value = false;
|
||||
|
||||
alert('✅ Операция мультиподписи успешно создана!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при создании операции мультиподписи:', error);
|
||||
alert('❌ Ошибка при создании операции: ' + error.message);
|
||||
} finally {
|
||||
isCreating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Подписание операции
|
||||
async function signOperation(operationId, support) {
|
||||
try {
|
||||
const tx = await props.dleContract.signMultiSigOperation(operationId, support);
|
||||
await tx.wait();
|
||||
|
||||
await loadOperations();
|
||||
alert('✅ Ваша подпись учтена!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при подписании операции:', error);
|
||||
alert('❌ Ошибка при подписании: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Исполнение операции
|
||||
async function executeOperation(operationId) {
|
||||
try {
|
||||
const tx = await props.dleContract.executeMultiSigOperation(operationId);
|
||||
await tx.wait();
|
||||
|
||||
await loadOperations();
|
||||
alert('✅ Операция успешно исполнена!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при исполнении операции:', error);
|
||||
alert('❌ Ошибка при исполнении операции: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Загрузка операций
|
||||
async function loadOperations() {
|
||||
try {
|
||||
// Здесь должен быть вызов API или смарт-контракта для загрузки операций
|
||||
// Пока используем заглушку
|
||||
operations.value = [];
|
||||
} catch (error) {
|
||||
console.error('Ошибка при загрузке операций:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
newOperation.value = {
|
||||
description: '',
|
||||
duration: 7,
|
||||
operationType: '',
|
||||
operationParams: {
|
||||
to: '',
|
||||
from: '',
|
||||
amount: 0,
|
||||
moduleId: '',
|
||||
moduleAddress: '',
|
||||
customData: ''
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function viewOperationDetails(operationId) {
|
||||
// Открыть модальное окно с деталями операции
|
||||
console.log('Просмотр деталей операции:', operationId);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadOperations();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dle-multisig-management {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.multisig-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.create-multisig-form {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.form-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.form-section:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.form-section h5 {
|
||||
color: #333;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.operation-types {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.operation-params {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.preview-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.multisig-list {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.operations-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.operation-card {
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.operation-card.active {
|
||||
border-color: #28a745;
|
||||
}
|
||||
|
||||
.operation-card.succeeded {
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.operation-card.defeated {
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
.operation-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.operation-header h5 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.operation-status {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.operation-status.active {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.operation-status.succeeded {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.operation-status.defeated {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.operation-details {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.signatures {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.signatures .for {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.signatures .against {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.operation-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.no-operations {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
844
frontend/src/views/smartcontracts/DleProposalsView.vue
Normal file
844
frontend/src/views/smartcontracts/DleProposalsView.vue
Normal file
@@ -0,0 +1,844 @@
|
||||
<!--
|
||||
Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
All rights reserved.
|
||||
|
||||
This software is proprietary and confidential.
|
||||
Unauthorized copying, modification, or distribution is prohibited.
|
||||
|
||||
For licensing inquiries: info@hb3-accelerator.com
|
||||
Website: https://hb3-accelerator.com
|
||||
GitHub: https://github.com/HB3-ACCELERATOR
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="dle-proposals-management">
|
||||
<div class="proposals-header">
|
||||
<h3>🗳️ Управление предложениями</h3>
|
||||
<button class="btn btn-primary" @click="showCreateForm = true">
|
||||
<i class="fas fa-plus"></i> Создать предложение
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Форма создания предложения -->
|
||||
<div v-if="showCreateForm" class="create-proposal-form">
|
||||
<div class="form-header">
|
||||
<h4>📝 Новое предложение</h4>
|
||||
<button class="close-btn" @click="showCreateForm = false">×</button>
|
||||
</div>
|
||||
|
||||
<div class="form-content">
|
||||
<!-- Основная информация -->
|
||||
<div class="form-section">
|
||||
<h5>📋 Основная информация</h5>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="proposalDescription">Описание предложения:</label>
|
||||
<textarea
|
||||
id="proposalDescription"
|
||||
v-model="newProposal.description"
|
||||
class="form-control"
|
||||
rows="3"
|
||||
placeholder="Опишите, что нужно сделать..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="proposalDuration">Длительность голосования (дни):</label>
|
||||
<input
|
||||
type="number"
|
||||
id="proposalDuration"
|
||||
v-model.number="newProposal.duration"
|
||||
class="form-control"
|
||||
min="1"
|
||||
max="30"
|
||||
placeholder="7"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Выбор цепочки для кворума -->
|
||||
<div class="form-section">
|
||||
<h5>🔗 Выбор цепочки для кворума</h5>
|
||||
<p class="form-help">Выберите цепочку, в которой будет происходить сбор голосов</p>
|
||||
|
||||
<div class="chains-grid">
|
||||
<div
|
||||
v-for="chain in availableChains"
|
||||
:key="chain.chainId"
|
||||
class="chain-option"
|
||||
:class="{ 'selected': newProposal.governanceChainId === chain.chainId }"
|
||||
@click="newProposal.governanceChainId = chain.chainId"
|
||||
>
|
||||
<div class="chain-info">
|
||||
<h6>{{ chain.name }}</h6>
|
||||
<span class="chain-id">Chain ID: {{ chain.chainId }}</span>
|
||||
<p class="chain-description">{{ chain.description }}</p>
|
||||
</div>
|
||||
<div class="chain-status">
|
||||
<i v-if="newProposal.governanceChainId === chain.chainId" class="fas fa-check"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Тип операции -->
|
||||
<div class="form-section">
|
||||
<h5>⚙️ Тип операции</h5>
|
||||
|
||||
<div class="operation-types">
|
||||
<div class="form-group">
|
||||
<label for="operationType">Выберите тип операции:</label>
|
||||
<select id="operationType" v-model="newProposal.operationType" class="form-control">
|
||||
<option value="">-- Выберите тип --</option>
|
||||
<option value="transfer">Передача токенов</option>
|
||||
<option value="mint">Минтинг токенов</option>
|
||||
<option value="burn">Сжигание токенов</option>
|
||||
<option value="custom">Пользовательская операция</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Параметры для передачи токенов -->
|
||||
<div v-if="newProposal.operationType === 'transfer'" class="operation-params">
|
||||
<div class="form-group">
|
||||
<label for="transferTo">Адрес получателя:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="transferTo"
|
||||
v-model="newProposal.operationParams.to"
|
||||
class="form-control"
|
||||
placeholder="0x..."
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="transferAmount">Количество токенов:</label>
|
||||
<input
|
||||
type="number"
|
||||
id="transferAmount"
|
||||
v-model.number="newProposal.operationParams.amount"
|
||||
class="form-control"
|
||||
min="1"
|
||||
placeholder="100"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Параметры для минтинга -->
|
||||
<div v-if="newProposal.operationType === 'mint'" class="operation-params">
|
||||
<div class="form-group">
|
||||
<label for="mintTo">Адрес получателя:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="mintTo"
|
||||
v-model="newProposal.operationParams.to"
|
||||
class="form-control"
|
||||
placeholder="0x..."
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="mintAmount">Количество токенов:</label>
|
||||
<input
|
||||
type="number"
|
||||
id="mintAmount"
|
||||
v-model.number="newProposal.operationParams.amount"
|
||||
class="form-control"
|
||||
min="1"
|
||||
placeholder="1000"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Параметры для сжигания -->
|
||||
<div v-if="newProposal.operationType === 'burn'" class="operation-params">
|
||||
<div class="form-group">
|
||||
<label for="burnFrom">Адрес владельца:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="burnFrom"
|
||||
v-model="newProposal.operationParams.from"
|
||||
class="form-control"
|
||||
placeholder="0x..."
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="burnAmount">Количество токенов:</label>
|
||||
<input
|
||||
type="number"
|
||||
id="burnAmount"
|
||||
v-model.number="newProposal.operationParams.amount"
|
||||
class="form-control"
|
||||
min="1"
|
||||
placeholder="100"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Пользовательская операция -->
|
||||
<div v-if="newProposal.operationType === 'custom'" class="operation-params">
|
||||
<div class="form-group">
|
||||
<label for="customOperation">Пользовательская операция (hex):</label>
|
||||
<textarea
|
||||
id="customOperation"
|
||||
v-model="newProposal.operationParams.customData"
|
||||
class="form-control"
|
||||
rows="3"
|
||||
placeholder="0x..."
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Предварительный просмотр -->
|
||||
<div class="form-section">
|
||||
<h5>👁️ Предварительный просмотр</h5>
|
||||
<div class="preview-card">
|
||||
<div class="preview-item">
|
||||
<strong>Описание:</strong> {{ newProposal.description || 'Не указано' }}
|
||||
</div>
|
||||
<div class="preview-item">
|
||||
<strong>Длительность:</strong> {{ newProposal.duration || 7 }} дней
|
||||
</div>
|
||||
<div class="preview-item">
|
||||
<strong>Цепочка для кворума:</strong>
|
||||
{{ getChainName(newProposal.governanceChainId) || 'Не выбрана' }}
|
||||
</div>
|
||||
<div class="preview-item">
|
||||
<strong>Тип операции:</strong> {{ getOperationTypeName(newProposal.operationType) || 'Не выбран' }}
|
||||
</div>
|
||||
<div v-if="newProposal.operationType" class="preview-item">
|
||||
<strong>Параметры:</strong> {{ getOperationParamsPreview() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Действия -->
|
||||
<div class="form-actions">
|
||||
<button
|
||||
class="btn btn-success"
|
||||
@click="createProposal"
|
||||
:disabled="!isFormValid || isCreating"
|
||||
>
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
{{ isCreating ? 'Создание...' : 'Создать предложение' }}
|
||||
</button>
|
||||
<button class="btn btn-secondary" @click="resetForm">
|
||||
<i class="fas fa-undo"></i> Сбросить
|
||||
</button>
|
||||
<button class="btn btn-danger" @click="showCreateForm = false">
|
||||
<i class="fas fa-times"></i> Отмена
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Список предложений -->
|
||||
<div class="proposals-list">
|
||||
<div class="list-header">
|
||||
<h4>📋 Список предложений</h4>
|
||||
<div class="list-filters">
|
||||
<select v-model="statusFilter" class="form-control">
|
||||
<option value="">Все статусы</option>
|
||||
<option value="active">Активные</option>
|
||||
<option value="pending">Ожидающие</option>
|
||||
<option value="succeeded">Принятые</option>
|
||||
<option value="defeated">Отклоненные</option>
|
||||
<option value="executed">Выполненные</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="filteredProposals.length === 0" class="no-proposals">
|
||||
<p>Предложений пока нет</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="proposals-grid">
|
||||
<div
|
||||
v-for="proposal in filteredProposals"
|
||||
:key="proposal.id"
|
||||
class="proposal-card"
|
||||
:class="proposal.status"
|
||||
>
|
||||
<div class="proposal-header">
|
||||
<h5>{{ proposal.description }}</h5>
|
||||
<span class="proposal-status" :class="proposal.status">
|
||||
{{ getProposalStatusText(proposal.status) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="proposal-details">
|
||||
<div class="detail-item">
|
||||
<strong>ID:</strong> #{{ proposal.id }}
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<strong>Создатель:</strong> {{ shortenAddress(proposal.initiator) }}
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<strong>Цепочка:</strong> {{ getChainName(proposal.governanceChainId) }}
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<strong>Дедлайн:</strong> {{ formatDate(proposal.deadline) }}
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<strong>Голоса:</strong>
|
||||
<span class="votes">
|
||||
<span class="for">За: {{ proposal.forVotes }}</span>
|
||||
<span class="against">Против: {{ proposal.againstVotes }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="proposal-actions">
|
||||
<button
|
||||
v-if="canVote(proposal)"
|
||||
class="btn btn-sm btn-success"
|
||||
@click="voteForProposal(proposal.id, true)"
|
||||
:disabled="hasVoted(proposal.id, true)"
|
||||
>
|
||||
<i class="fas fa-thumbs-up"></i> За
|
||||
</button>
|
||||
<button
|
||||
v-if="canVote(proposal)"
|
||||
class="btn btn-sm btn-danger"
|
||||
@click="voteForProposal(proposal.id, false)"
|
||||
:disabled="hasVoted(proposal.id, false)"
|
||||
>
|
||||
<i class="fas fa-thumbs-down"></i> Против
|
||||
</button>
|
||||
<button
|
||||
v-if="canExecute(proposal)"
|
||||
class="btn btn-sm btn-primary"
|
||||
@click="executeProposal(proposal.id)"
|
||||
>
|
||||
<i class="fas fa-play"></i> Исполнить
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-info"
|
||||
@click="viewProposalDetails(proposal.id)"
|
||||
>
|
||||
<i class="fas fa-eye"></i> Детали
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useAuthContext } from '@/composables/useAuth';
|
||||
|
||||
const props = defineProps({
|
||||
dleAddress: { type: String, required: true },
|
||||
dleContract: { type: Object, required: true }
|
||||
});
|
||||
|
||||
const { address } = useAuthContext();
|
||||
|
||||
// Состояние формы
|
||||
const showCreateForm = ref(false);
|
||||
const isCreating = ref(false);
|
||||
const statusFilter = ref('');
|
||||
|
||||
// Новое предложение
|
||||
const newProposal = ref({
|
||||
description: '',
|
||||
duration: 7,
|
||||
governanceChainId: null,
|
||||
operationType: '',
|
||||
operationParams: {
|
||||
to: '',
|
||||
from: '',
|
||||
amount: 0,
|
||||
customData: ''
|
||||
}
|
||||
});
|
||||
|
||||
// Доступные цепочки
|
||||
const availableChains = ref([
|
||||
{ chainId: 1, name: 'Ethereum', description: 'Основная сеть Ethereum' },
|
||||
{ chainId: 137, name: 'Polygon', description: 'Сеть Polygon' },
|
||||
{ chainId: 56, name: 'BSC', description: 'Binance Smart Chain' },
|
||||
{ chainId: 42161, name: 'Arbitrum', description: 'Arbitrum One' }
|
||||
]);
|
||||
|
||||
// Предложения
|
||||
const proposals = ref([]);
|
||||
|
||||
// Вычисляемые свойства
|
||||
const isFormValid = computed(() => {
|
||||
return (
|
||||
newProposal.value.description &&
|
||||
newProposal.value.duration > 0 &&
|
||||
newProposal.value.governanceChainId &&
|
||||
newProposal.value.operationType &&
|
||||
validateOperationParams()
|
||||
);
|
||||
});
|
||||
|
||||
const filteredProposals = computed(() => {
|
||||
if (!statusFilter.value) return proposals.value;
|
||||
return proposals.value.filter(p => p.status === statusFilter.value);
|
||||
});
|
||||
|
||||
// Функции
|
||||
function validateOperationParams() {
|
||||
const params = newProposal.value.operationParams;
|
||||
|
||||
switch (newProposal.value.operationType) {
|
||||
case 'transfer':
|
||||
case 'mint':
|
||||
return params.to && params.amount > 0;
|
||||
case 'burn':
|
||||
return params.from && params.amount > 0;
|
||||
case 'custom':
|
||||
return params.customData && params.customData.startsWith('0x');
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getChainName(chainId) {
|
||||
const chain = availableChains.value.find(c => c.chainId === chainId);
|
||||
return chain ? chain.name : 'Неизвестная сеть';
|
||||
}
|
||||
|
||||
function getOperationTypeName(type) {
|
||||
const types = {
|
||||
'transfer': 'Передача токенов',
|
||||
'mint': 'Минтинг токенов',
|
||||
'burn': 'Сжигание токенов',
|
||||
'custom': 'Пользовательская операция'
|
||||
};
|
||||
return types[type] || 'Неизвестный тип';
|
||||
}
|
||||
|
||||
function getOperationParamsPreview() {
|
||||
const params = newProposal.value.operationParams;
|
||||
|
||||
switch (newProposal.value.operationType) {
|
||||
case 'transfer':
|
||||
return `Кому: ${shortenAddress(params.to)}, Количество: ${params.amount}`;
|
||||
case 'mint':
|
||||
return `Кому: ${shortenAddress(params.to)}, Количество: ${params.amount}`;
|
||||
case 'burn':
|
||||
return `От: ${shortenAddress(params.from)}, Количество: ${params.amount}`;
|
||||
case 'custom':
|
||||
return `Данные: ${params.customData.substring(0, 20)}...`;
|
||||
default:
|
||||
return 'Не указаны';
|
||||
}
|
||||
}
|
||||
|
||||
function shortenAddress(address) {
|
||||
if (!address) return '';
|
||||
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||
}
|
||||
|
||||
function formatDate(timestamp) {
|
||||
if (!timestamp) return 'N/A';
|
||||
return new Date(timestamp * 1000).toLocaleString();
|
||||
}
|
||||
|
||||
function getProposalStatusText(status) {
|
||||
const statusMap = {
|
||||
'pending': 'Ожидает',
|
||||
'active': 'Активно',
|
||||
'succeeded': 'Принято',
|
||||
'defeated': 'Отклонено',
|
||||
'executed': 'Выполнено'
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
}
|
||||
|
||||
function canVote(proposal) {
|
||||
return proposal.status === 'active' && !hasVoted(proposal.id);
|
||||
}
|
||||
|
||||
function canExecute(proposal) {
|
||||
return proposal.status === 'succeeded' && !proposal.executed;
|
||||
}
|
||||
|
||||
function hasVoted(proposalId, support = null) {
|
||||
// Здесь должна быть проверка голосования пользователя
|
||||
return false;
|
||||
}
|
||||
|
||||
// Создание предложения
|
||||
async function createProposal() {
|
||||
if (!isFormValid.value) {
|
||||
alert('Пожалуйста, заполните все обязательные поля');
|
||||
return;
|
||||
}
|
||||
|
||||
isCreating.value = true;
|
||||
|
||||
try {
|
||||
// Подготовка данных для смарт-контракта
|
||||
const operation = encodeOperation();
|
||||
|
||||
// Вызов смарт-контракта
|
||||
const tx = await props.dleContract.createProposal(
|
||||
newProposal.value.description,
|
||||
newProposal.value.duration * 24 * 60 * 60, // конвертируем в секунды
|
||||
operation,
|
||||
newProposal.value.governanceChainId
|
||||
);
|
||||
|
||||
await tx.wait();
|
||||
|
||||
// Обновляем список предложений
|
||||
await loadProposals();
|
||||
|
||||
// Сбрасываем форму
|
||||
resetForm();
|
||||
showCreateForm.value = false;
|
||||
|
||||
alert('✅ Предложение успешно создано!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при создании предложения:', error);
|
||||
alert('❌ Ошибка при создании предложения: ' + error.message);
|
||||
} finally {
|
||||
isCreating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function encodeOperation() {
|
||||
const params = newProposal.value.operationParams;
|
||||
|
||||
switch (newProposal.value.operationType) {
|
||||
case 'transfer':
|
||||
return encodeTransferOperation(params.to, params.amount);
|
||||
case 'mint':
|
||||
return encodeMintOperation(params.to, params.amount);
|
||||
case 'burn':
|
||||
return encodeBurnOperation(params.from, params.amount);
|
||||
case 'custom':
|
||||
return params.customData;
|
||||
default:
|
||||
throw new Error('Неизвестный тип операции');
|
||||
}
|
||||
}
|
||||
|
||||
function encodeTransferOperation(to, amount) {
|
||||
// Кодируем операцию передачи токенов
|
||||
const abiCoder = new ethers.AbiCoder();
|
||||
const selector = '0xa9059cbb'; // transfer(address,uint256)
|
||||
const data = abiCoder.encode(['address', 'uint256'], [to, amount]);
|
||||
return selector + data.slice(2);
|
||||
}
|
||||
|
||||
function encodeMintOperation(to, amount) {
|
||||
// Кодируем операцию минтинга токенов
|
||||
const abiCoder = new ethers.AbiCoder();
|
||||
const selector = '0x40c10f19'; // mint(address,uint256)
|
||||
const data = abiCoder.encode(['address', 'uint256'], [to, amount]);
|
||||
return selector + data.slice(2);
|
||||
}
|
||||
|
||||
function encodeBurnOperation(from, amount) {
|
||||
// Кодируем операцию сжигания токенов
|
||||
const abiCoder = new ethers.AbiCoder();
|
||||
const selector = '0x42966c68'; // burn(uint256)
|
||||
const data = abiCoder.encode(['uint256'], [amount]);
|
||||
return selector + data.slice(2);
|
||||
}
|
||||
|
||||
// Голосование
|
||||
async function voteForProposal(proposalId, support) {
|
||||
try {
|
||||
const tx = await props.dleContract.vote(proposalId, support);
|
||||
await tx.wait();
|
||||
|
||||
await loadProposals();
|
||||
alert('✅ Ваш голос учтен!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при голосовании:', error);
|
||||
alert('❌ Ошибка при голосовании: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Исполнение предложения
|
||||
async function executeProposal(proposalId) {
|
||||
try {
|
||||
const tx = await props.dleContract.executeProposal(proposalId);
|
||||
await tx.wait();
|
||||
|
||||
await loadProposals();
|
||||
alert('✅ Предложение успешно исполнено!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при исполнении предложения:', error);
|
||||
alert('❌ Ошибка при исполнении предложения: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Загрузка предложений
|
||||
async function loadProposals() {
|
||||
try {
|
||||
// Здесь должен быть вызов API или смарт-контракта для загрузки предложений
|
||||
// Пока используем заглушку
|
||||
proposals.value = [];
|
||||
} catch (error) {
|
||||
console.error('Ошибка при загрузке предложений:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
newProposal.value = {
|
||||
description: '',
|
||||
duration: 7,
|
||||
governanceChainId: null,
|
||||
operationType: '',
|
||||
operationParams: {
|
||||
to: '',
|
||||
from: '',
|
||||
amount: 0,
|
||||
customData: ''
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function viewProposalDetails(proposalId) {
|
||||
// Открыть модальное окно с деталями предложения
|
||||
console.log('Просмотр деталей предложения:', proposalId);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadProposals();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dle-proposals-management {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.proposals-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.create-proposal-form {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.form-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.form-section:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.form-section h5 {
|
||||
color: #333;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.chains-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.chain-option {
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.chain-option:hover {
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.chain-option.selected {
|
||||
border-color: #007bff;
|
||||
background: #f8f9ff;
|
||||
}
|
||||
|
||||
.chain-info h6 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.chain-id {
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.chain-description {
|
||||
font-size: 0.9rem;
|
||||
color: #888;
|
||||
margin: 0.5rem 0 0 0;
|
||||
}
|
||||
|
||||
.chain-status {
|
||||
text-align: right;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.operation-types {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.operation-params {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.preview-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.proposals-list {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.proposals-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.proposal-card {
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.proposal-card.active {
|
||||
border-color: #28a745;
|
||||
}
|
||||
|
||||
.proposal-card.succeeded {
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.proposal-card.defeated {
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
.proposal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.proposal-header h5 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.proposal-status {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.proposal-status.active {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.proposal-status.succeeded {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.proposal-status.defeated {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.proposal-details {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.votes {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.votes .for {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.votes .against {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.proposal-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.no-proposals {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.form-help {
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user