ваше сообщение коммита
This commit is contained in:
@@ -32,7 +32,7 @@ api.interceptors.request.use(
|
||||
);
|
||||
|
||||
// Добавляем перехватчик ответов для обработки ошибок
|
||||
axios.interceptors.response.use(
|
||||
api.interceptors.response.use(
|
||||
(response) => {
|
||||
// Проверяем, что ответ действительно JSON
|
||||
if (response.headers['content-type'] &&
|
||||
|
||||
525
frontend/src/components/AIQueueMonitor.vue
Normal file
525
frontend/src/components/AIQueueMonitor.vue
Normal file
@@ -0,0 +1,525 @@
|
||||
<template>
|
||||
<div class="ai-queue-monitor">
|
||||
<div class="monitor-header">
|
||||
<h3>Мониторинг AI Очереди</h3>
|
||||
<div class="refresh-controls">
|
||||
<button @click="refreshStats" :disabled="loading" class="btn-refresh">
|
||||
<i class="fas fa-sync-alt" :class="{ 'fa-spin': loading }"></i>
|
||||
Обновить
|
||||
</button>
|
||||
<label class="auto-refresh">
|
||||
<input type="checkbox" v-model="autoRefresh" />
|
||||
Автообновление
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<!-- Основная статистика -->
|
||||
<div class="stat-card">
|
||||
<div class="stat-title">Статус очереди</div>
|
||||
<div class="stat-value" :class="queueStatusClass">
|
||||
{{ queueStatus }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-title">Задач в очереди</div>
|
||||
<div class="stat-value">{{ stats.currentQueueSize }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-title">Выполняется</div>
|
||||
<div class="stat-value">{{ stats.runningTasks }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-title">Успешность</div>
|
||||
<div class="stat-value" :class="successRateClass">
|
||||
{{ successRate }}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-title">Среднее время</div>
|
||||
<div class="stat-value">{{ averageTime }}с</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-title">Всего обработано</div>
|
||||
<div class="stat-value">{{ stats.totalProcessed }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Детальная информация -->
|
||||
<div class="detailed-stats">
|
||||
<h4>Детальная статистика</h4>
|
||||
<div class="stats-table">
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Всего задач:</span>
|
||||
<span class="stat-value">{{ stats.totalProcessed }}</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Успешных:</span>
|
||||
<span class="stat-value success">{{ stats.totalProcessed - stats.totalFailed }}</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Ошибок:</span>
|
||||
<span class="stat-value error">{{ stats.totalFailed }}</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Среднее время обработки:</span>
|
||||
<span class="stat-value">{{ Math.round(stats.averageProcessingTime) }}мс</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Последняя обработка:</span>
|
||||
<span class="stat-value">{{ lastProcessedTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Управление очередью (только для админов) -->
|
||||
<div v-if="isAdmin" class="queue-controls">
|
||||
<h4>Управление очередью</h4>
|
||||
<div class="control-buttons">
|
||||
<button @click="controlQueue('pause')" class="btn-control btn-pause">
|
||||
<i class="fas fa-pause"></i>
|
||||
Пауза
|
||||
</button>
|
||||
<button @click="controlQueue('resume')" class="btn-control btn-resume">
|
||||
<i class="fas fa-play"></i>
|
||||
Возобновить
|
||||
</button>
|
||||
<button @click="controlQueue('clear')" class="btn-control btn-clear">
|
||||
<i class="fas fa-trash"></i>
|
||||
Очистить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- График производительности -->
|
||||
<div class="performance-chart">
|
||||
<h4>Производительность</h4>
|
||||
<div class="chart-container">
|
||||
<canvas ref="performanceChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import axios from 'axios'
|
||||
import Chart from 'chart.js/auto'
|
||||
|
||||
export default {
|
||||
name: 'AIQueueMonitor',
|
||||
props: {
|
||||
isAdmin: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const stats = ref({
|
||||
totalProcessed: 0,
|
||||
totalFailed: 0,
|
||||
averageProcessingTime: 0,
|
||||
currentQueueSize: 0,
|
||||
runningTasks: 0,
|
||||
lastProcessedAt: null,
|
||||
isInitialized: false
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const autoRefresh = ref(true)
|
||||
const refreshInterval = ref(null)
|
||||
const performanceChart = ref(null)
|
||||
const chartInstance = ref(null)
|
||||
|
||||
// Вычисляемые свойства
|
||||
const queueStatus = computed(() => {
|
||||
if (!stats.value.isInitialized) return 'Не инициализирована'
|
||||
if (stats.value.currentQueueSize === 0 && stats.value.runningTasks === 0) return 'Пуста'
|
||||
if (stats.value.runningTasks > 0) return 'Работает'
|
||||
return 'Ожидает'
|
||||
})
|
||||
|
||||
const queueStatusClass = computed(() => {
|
||||
if (!stats.value.isInitialized) return 'status-error'
|
||||
if (stats.value.currentQueueSize === 0 && stats.value.runningTasks === 0) return 'status-idle'
|
||||
if (stats.value.runningTasks > 0) return 'status-active'
|
||||
return 'status-waiting'
|
||||
})
|
||||
|
||||
const successRate = computed(() => {
|
||||
if (stats.value.totalProcessed === 0) return 0
|
||||
return Math.round(((stats.value.totalProcessed - stats.value.totalFailed) / stats.value.totalProcessed) * 100)
|
||||
})
|
||||
|
||||
const successRateClass = computed(() => {
|
||||
if (successRate.value >= 95) return 'success'
|
||||
if (successRate.value >= 80) return 'warning'
|
||||
return 'error'
|
||||
})
|
||||
|
||||
const averageTime = computed(() => {
|
||||
return Math.round(stats.value.averageProcessingTime / 1000)
|
||||
})
|
||||
|
||||
const lastProcessedTime = computed(() => {
|
||||
if (!stats.value.lastProcessedAt) return 'Нет данных'
|
||||
return new Date(stats.value.lastProcessedAt).toLocaleString('ru-RU')
|
||||
})
|
||||
|
||||
// Методы
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await axios.get('/api/ai-queue/stats')
|
||||
if (response.data.success) {
|
||||
stats.value = response.data.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching queue stats:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const refreshStats = () => {
|
||||
fetchStats()
|
||||
}
|
||||
|
||||
const controlQueue = async (action) => {
|
||||
try {
|
||||
const response = await axios.post('/api/ai-queue/control', { action })
|
||||
if (response.data.success) {
|
||||
await fetchStats()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error controlling queue (${action}):`, error)
|
||||
}
|
||||
}
|
||||
|
||||
const initChart = () => {
|
||||
const ctx = performanceChart.value.getContext('2d')
|
||||
chartInstance.value = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'Время обработки (мс)',
|
||||
data: [],
|
||||
borderColor: 'rgb(75, 192, 192)',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||
tension: 0.1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const updateChart = () => {
|
||||
if (chartInstance.value) {
|
||||
const now = new Date().toLocaleTimeString('ru-RU')
|
||||
chartInstance.value.data.labels.push(now)
|
||||
chartInstance.value.data.datasets[0].data.push(stats.value.averageProcessingTime)
|
||||
|
||||
// Ограничиваем количество точек на графике
|
||||
if (chartInstance.value.data.labels.length > 20) {
|
||||
chartInstance.value.data.labels.shift()
|
||||
chartInstance.value.data.datasets[0].data.shift()
|
||||
}
|
||||
|
||||
chartInstance.value.update()
|
||||
}
|
||||
}
|
||||
|
||||
// Наблюдатели
|
||||
watch(autoRefresh, (newValue) => {
|
||||
if (newValue) {
|
||||
refreshInterval.value = setInterval(fetchStats, 5000)
|
||||
} else {
|
||||
if (refreshInterval.value) {
|
||||
clearInterval(refreshInterval.value)
|
||||
refreshInterval.value = null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Жизненный цикл
|
||||
onMounted(() => {
|
||||
fetchStats()
|
||||
initChart()
|
||||
|
||||
if (autoRefresh.value) {
|
||||
refreshInterval.value = setInterval(fetchStats, 5000)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (refreshInterval.value) {
|
||||
clearInterval(refreshInterval.value)
|
||||
}
|
||||
if (chartInstance.value) {
|
||||
chartInstance.value.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
stats,
|
||||
loading,
|
||||
autoRefresh,
|
||||
performanceChart,
|
||||
queueStatus,
|
||||
queueStatusClass,
|
||||
successRate,
|
||||
successRateClass,
|
||||
averageTime,
|
||||
lastProcessedTime,
|
||||
refreshStats,
|
||||
controlQueue
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ai-queue-monitor {
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.monitor-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.monitor-header h3 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.refresh-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.btn-refresh {
|
||||
padding: 8px 16px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-refresh:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.btn-refresh:disabled {
|
||||
background: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.auto-refresh {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-title {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.status-waiting {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.status-idle {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.detailed-stats {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.detailed-stats h4 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.stats-table {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.stat-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.queue-controls {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.queue-controls h4 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn-control {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-pause {
|
||||
background: #ffc107;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-pause:hover {
|
||||
background: #e0a800;
|
||||
}
|
||||
|
||||
.btn-resume {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-resume:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.btn-clear {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-clear:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
.performance-chart {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.performance-chart h4 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 300px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.monitor-header {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
903
frontend/src/components/EmbeddedDLEInterface.vue
Normal file
903
frontend/src/components/EmbeddedDLEInterface.vue
Normal file
@@ -0,0 +1,903 @@
|
||||
<!--
|
||||
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="embedded-dle-interface">
|
||||
<!-- Заголовок встраиваемого интерфейса -->
|
||||
<div class="embedded-header">
|
||||
<div class="embedded-title">
|
||||
<h3>{{ dleInfo.name }}</h3>
|
||||
<span class="embedded-subtitle">Встраиваемый интерфейс управления</span>
|
||||
</div>
|
||||
<div class="embedded-status">
|
||||
<span class="status-indicator" :class="connectionStatus">
|
||||
{{ getStatusText() }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Информация о DLE -->
|
||||
<div class="embedded-info">
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<label>Адрес DLE:</label>
|
||||
<span class="address">{{ formatAddress(dleAddress) }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>Ваш баланс токенов:</label>
|
||||
<span class="balance">{{ userTokenBalance }} {{ dleInfo.symbol }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>Общий запас:</label>
|
||||
<span>{{ totalSupply }} {{ dleInfo.symbol }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>Кворум:</label>
|
||||
<span>{{ quorumPercentage }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Быстрые действия -->
|
||||
<div class="quick-actions">
|
||||
<h4>Быстрые действия</h4>
|
||||
<div class="actions-grid">
|
||||
<button
|
||||
@click="createProposal"
|
||||
class="action-btn action-primary"
|
||||
:disabled="!canCreateProposal"
|
||||
>
|
||||
<i class="icon">📝</i>
|
||||
<span>Создать предложение</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="viewProposals"
|
||||
class="action-btn action-secondary"
|
||||
>
|
||||
<i class="icon">📋</i>
|
||||
<span>Просмотр предложений</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="viewTokenHolders"
|
||||
class="action-btn action-secondary"
|
||||
>
|
||||
<i class="icon">👥</i>
|
||||
<span>Держатели токенов</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="viewTreasury"
|
||||
class="action-btn action-secondary"
|
||||
>
|
||||
<i class="icon">💰</i>
|
||||
<span>Казначейство</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Активные предложения -->
|
||||
<div class="active-proposals">
|
||||
<h4>Активные предложения</h4>
|
||||
<div v-if="proposals.length === 0" class="empty-state">
|
||||
<p>Нет активных предложений</p>
|
||||
</div>
|
||||
<div v-else class="proposals-list">
|
||||
<div
|
||||
v-for="proposal in proposals.slice(0, 3)"
|
||||
:key="proposal.id"
|
||||
class="proposal-item"
|
||||
:class="{ 'proposal-signed': proposal.hasSigned }"
|
||||
>
|
||||
<div class="proposal-content">
|
||||
<div class="proposal-title">
|
||||
<span class="proposal-id">#{{ proposal.id }}</span>
|
||||
<span class="proposal-status" :class="getProposalStatusClass(proposal)">
|
||||
{{ getProposalStatusText(proposal) }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="proposal-description">{{ proposal.description }}</p>
|
||||
<div class="proposal-meta">
|
||||
<span>Подписи: {{ proposal.signaturesCount }}/{{ proposal.quorumRequired }}</span>
|
||||
<span>До: {{ formatTimeLeft(proposal.timelock) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="proposal-actions">
|
||||
<button
|
||||
v-if="!proposal.hasSigned && !proposal.executed"
|
||||
@click="signProposal(proposal.id)"
|
||||
class="btn-sign"
|
||||
:disabled="isSigning"
|
||||
>
|
||||
Подписать
|
||||
</button>
|
||||
<button
|
||||
v-if="canExecuteProposal(proposal)"
|
||||
@click="executeProposal(proposal.id)"
|
||||
class="btn-execute"
|
||||
:disabled="isExecuting"
|
||||
>
|
||||
Выполнить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="proposals.length > 3" class="view-more">
|
||||
<button @click="viewAllProposals" class="btn-view-more">
|
||||
Показать все предложения ({{ proposals.length }})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Статистика -->
|
||||
<div class="embedded-stats">
|
||||
<h4>Статистика DLE</h4>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ proposals.length }}</div>
|
||||
<div class="stat-label">Всего предложений</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ executedProposals }}</div>
|
||||
<div class="stat-label">Выполнено</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ tokenHolders.length }}</div>
|
||||
<div class="stat-label">Держателей токенов</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ installedModules.length }}</div>
|
||||
<div class="stat-label">Установленных модулей</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Уведомления -->
|
||||
<div v-if="notifications.length > 0" class="notifications">
|
||||
<h4>Уведомления</h4>
|
||||
<div class="notifications-list">
|
||||
<div
|
||||
v-for="notification in notifications"
|
||||
:key="notification.id"
|
||||
class="notification-item"
|
||||
:class="notification.type"
|
||||
>
|
||||
<div class="notification-content">
|
||||
<span class="notification-title">{{ notification.title }}</span>
|
||||
<p class="notification-message">{{ notification.message }}</p>
|
||||
<span class="notification-time">{{ formatTime(notification.timestamp) }}</span>
|
||||
</div>
|
||||
<button @click="dismissNotification(notification.id)" class="btn-dismiss">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed, defineProps, defineEmits } from 'vue';
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
dleAddress: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
dleInfo: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
name: 'Неизвестное DLE',
|
||||
symbol: 'TOKEN',
|
||||
location: 'Не указано'
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits([
|
||||
'proposal-created',
|
||||
'proposal-signed',
|
||||
'proposal-executed',
|
||||
'interface-action'
|
||||
]);
|
||||
|
||||
// Состояние
|
||||
const connectionStatus = ref('connecting');
|
||||
const userTokenBalance = ref(0);
|
||||
const totalSupply = ref(0);
|
||||
const quorumPercentage = ref(51);
|
||||
const proposals = ref([]);
|
||||
const tokenHolders = ref([]);
|
||||
const installedModules = ref([]);
|
||||
const notifications = ref([]);
|
||||
|
||||
// Состояние загрузки
|
||||
const isSigning = ref(false);
|
||||
const isExecuting = ref(false);
|
||||
|
||||
// Вычисляемые свойства
|
||||
const executedProposals = computed(() => {
|
||||
return proposals.value.filter(p => p.executed).length;
|
||||
});
|
||||
|
||||
const canCreateProposal = computed(() => {
|
||||
return userTokenBalance.value > 0 && connectionStatus.value === 'connected';
|
||||
});
|
||||
|
||||
// Методы
|
||||
const connectToDLE = async () => {
|
||||
try {
|
||||
connectionStatus.value = 'connecting';
|
||||
|
||||
// Здесь будет подключение к DLE через Web3
|
||||
console.log('Подключение к DLE:', props.dleAddress);
|
||||
|
||||
// Имитация подключения
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Загрузка данных
|
||||
await loadDLEData();
|
||||
|
||||
connectionStatus.value = 'connected';
|
||||
} catch (error) {
|
||||
console.error('Ошибка подключения к DLE:', error);
|
||||
connectionStatus.value = 'error';
|
||||
}
|
||||
};
|
||||
|
||||
const loadDLEData = async () => {
|
||||
try {
|
||||
// Загрузка баланса токенов пользователя
|
||||
userTokenBalance.value = 500; // Временные данные
|
||||
|
||||
// Загрузка общего запаса
|
||||
totalSupply.value = 10000;
|
||||
|
||||
// Загрузка предложений
|
||||
proposals.value = [
|
||||
{
|
||||
id: 1,
|
||||
description: 'Перевод 100 токенов партнеру',
|
||||
timelock: Math.floor(Date.now() / 1000) + 3600,
|
||||
signaturesCount: 3000,
|
||||
quorumRequired: 5100,
|
||||
executed: false,
|
||||
hasSigned: false
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
description: 'Установка нового модуля казначейства',
|
||||
timelock: Math.floor(Date.now() / 1000) + 7200,
|
||||
signaturesCount: 5100,
|
||||
quorumRequired: 5100,
|
||||
executed: false,
|
||||
hasSigned: true
|
||||
}
|
||||
];
|
||||
|
||||
// Загрузка держателей токенов
|
||||
tokenHolders.value = [
|
||||
{ address: '0x1234...5678', balance: 2000 },
|
||||
{ address: '0x8765...4321', balance: 1500 },
|
||||
{ address: '0xabcd...efgh', balance: 1000 }
|
||||
];
|
||||
|
||||
// Загрузка модулей
|
||||
installedModules.value = [
|
||||
{ name: 'Казначейство', address: '0x1111...2222' },
|
||||
{ name: 'Коммуникации', address: '0x3333...4444' }
|
||||
];
|
||||
|
||||
// Загрузка уведомлений
|
||||
notifications.value = [
|
||||
{
|
||||
id: 1,
|
||||
type: 'info',
|
||||
title: 'Новое предложение',
|
||||
message: 'Создано предложение #3 для установки модуля',
|
||||
timestamp: Date.now() - 3600000
|
||||
}
|
||||
];
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки данных DLE:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const createProposal = () => {
|
||||
emit('interface-action', {
|
||||
action: 'create-proposal',
|
||||
dleAddress: props.dleAddress
|
||||
});
|
||||
};
|
||||
|
||||
const viewProposals = () => {
|
||||
emit('interface-action', {
|
||||
action: 'view-proposals',
|
||||
dleAddress: props.dleAddress
|
||||
});
|
||||
};
|
||||
|
||||
const viewTokenHolders = () => {
|
||||
emit('interface-action', {
|
||||
action: 'view-token-holders',
|
||||
dleAddress: props.dleAddress
|
||||
});
|
||||
};
|
||||
|
||||
const viewTreasury = () => {
|
||||
emit('interface-action', {
|
||||
action: 'view-treasury',
|
||||
dleAddress: props.dleAddress
|
||||
});
|
||||
};
|
||||
|
||||
const signProposal = async (proposalId) => {
|
||||
if (isSigning.value) return;
|
||||
|
||||
try {
|
||||
isSigning.value = true;
|
||||
|
||||
// Здесь будет подписание предложения
|
||||
console.log('Подписание предложения:', proposalId);
|
||||
|
||||
const proposal = proposals.value.find(p => p.id === proposalId);
|
||||
if (proposal) {
|
||||
proposal.signaturesCount += userTokenBalance.value;
|
||||
proposal.hasSigned = true;
|
||||
}
|
||||
|
||||
emit('proposal-signed', { proposalId, dleAddress: props.dleAddress });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка подписания предложения:', error);
|
||||
} finally {
|
||||
isSigning.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const executeProposal = async (proposalId) => {
|
||||
if (isExecuting.value) return;
|
||||
|
||||
try {
|
||||
isExecuting.value = true;
|
||||
|
||||
// Здесь будет выполнение предложения
|
||||
console.log('Выполнение предложения:', proposalId);
|
||||
|
||||
const proposal = proposals.value.find(p => p.id === proposalId);
|
||||
if (proposal) {
|
||||
proposal.executed = true;
|
||||
}
|
||||
|
||||
emit('proposal-executed', { proposalId, dleAddress: props.dleAddress });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка выполнения предложения:', error);
|
||||
} finally {
|
||||
isExecuting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const viewAllProposals = () => {
|
||||
emit('interface-action', {
|
||||
action: 'view-all-proposals',
|
||||
dleAddress: props.dleAddress
|
||||
});
|
||||
};
|
||||
|
||||
const dismissNotification = (notificationId) => {
|
||||
notifications.value = notifications.value.filter(n => n.id !== notificationId);
|
||||
};
|
||||
|
||||
// Утилиты
|
||||
const formatAddress = (address) => {
|
||||
if (!address) return '';
|
||||
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
|
||||
};
|
||||
|
||||
const formatTimeLeft = (timestamp) => {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const diff = timestamp - now;
|
||||
|
||||
if (diff <= 0) return 'Истекло';
|
||||
|
||||
const hours = Math.floor(diff / 3600);
|
||||
const minutes = Math.floor((diff % 3600) / 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}ч ${minutes}м`;
|
||||
}
|
||||
return `${minutes}м`;
|
||||
};
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
return new Date(timestamp).toLocaleString('ru-RU');
|
||||
};
|
||||
|
||||
const getStatusText = () => {
|
||||
switch (connectionStatus.value) {
|
||||
case 'connecting': return 'Подключение...';
|
||||
case 'connected': return 'Подключено';
|
||||
case 'error': return 'Ошибка подключения';
|
||||
default: return 'Неизвестно';
|
||||
}
|
||||
};
|
||||
|
||||
const canExecuteProposal = (proposal) => {
|
||||
return !proposal.executed &&
|
||||
proposal.signaturesCount >= proposal.quorumRequired &&
|
||||
Date.now() >= proposal.timelock * 1000;
|
||||
};
|
||||
|
||||
const getProposalStatusClass = (proposal) => {
|
||||
if (proposal.executed) return 'status-executed';
|
||||
if (proposal.signaturesCount >= proposal.quorumRequired) return 'status-ready';
|
||||
return 'status-pending';
|
||||
};
|
||||
|
||||
const getProposalStatusText = (proposal) => {
|
||||
if (proposal.executed) return 'Выполнено';
|
||||
if (proposal.signaturesCount >= proposal.quorumRequired) return 'Готово';
|
||||
return 'Ожидает';
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
connectToDLE();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.embedded-dle-interface {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid #e9ecef;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Заголовок */
|
||||
.embedded-header {
|
||||
background: linear-gradient(135deg, var(--color-primary), var(--color-primary-dark));
|
||||
color: white;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.embedded-title h3 {
|
||||
margin: 0 0 5px 0;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.embedded-subtitle {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-indicator.connecting {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.status-indicator.connected {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-indicator.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
/* Информация */
|
||||
.embedded-info {
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.info-item label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-grey-dark);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.info-item span {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.address {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.balance {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Быстрые действия */
|
||||
.quick-actions {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.quick-actions h4 {
|
||||
margin: 0 0 15px 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.actions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 15px;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: var(--radius-md);
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.action-btn:hover:not(:disabled) {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.action-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-primary {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.action-primary:hover:not(:disabled) {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.action-btn span {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Предложения */
|
||||
.active-proposals {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.active-proposals h4 {
|
||||
margin: 0 0 15px 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.proposals-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.proposal-item {
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 15px;
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.proposal-item:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.proposal-item.proposal-signed {
|
||||
border-left: 4px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.proposal-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.proposal-id {
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.proposal-status {
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.status-ready {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.status-executed {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.proposal-description {
|
||||
margin: 8px 0;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.proposal-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-grey-dark);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.proposal-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-sign, .btn-execute {
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-sign {
|
||||
background: var(--color-secondary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-sign:hover:not(:disabled) {
|
||||
background: var(--color-secondary-dark);
|
||||
}
|
||||
|
||||
.btn-execute {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-execute:hover:not(:disabled) {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.btn-sign:disabled, .btn-execute:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.view-more {
|
||||
margin-top: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-view-more {
|
||||
background: none;
|
||||
border: 1px solid var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-view-more:hover {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Статистика */
|
||||
.embedded-stats {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.embedded-stats h4 {
|
||||
margin: 0 0 15px 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-grey-dark);
|
||||
}
|
||||
|
||||
/* Уведомления */
|
||||
.notifications {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.notifications h4 {
|
||||
margin: 0 0 15px 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.notifications-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.notification-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 12px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.notification-item.info {
|
||||
background: #d1ecf1;
|
||||
border-color: #bee5eb;
|
||||
}
|
||||
|
||||
.notification-item.warning {
|
||||
background: #fff3cd;
|
||||
border-color: #ffeaa7;
|
||||
}
|
||||
|
||||
.notification-item.error {
|
||||
background: #f8d7da;
|
||||
border-color: #f5c6cb;
|
||||
}
|
||||
|
||||
.notification-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.notification-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.notification-message {
|
||||
font-size: 0.8rem;
|
||||
margin: 4px 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.notification-time {
|
||||
font-size: 0.7rem;
|
||||
color: var(--color-grey-dark);
|
||||
}
|
||||
|
||||
.btn-dismiss {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-grey-dark);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
padding: 0;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.btn-dismiss:hover {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
/* Состояния */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
color: var(--color-grey-dark);
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.embedded-header {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.actions-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.proposal-meta {
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.proposal-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
435
frontend/src/components/OllamaModelManager.vue
Normal file
435
frontend/src/components/OllamaModelManager.vue
Normal file
@@ -0,0 +1,435 @@
|
||||
<!--
|
||||
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="ollama-model-manager">
|
||||
<h3>Управление локальными моделями</h3>
|
||||
|
||||
<!-- Статус подключения -->
|
||||
<div class="connection-status">
|
||||
<div class="status-indicator" :class="{ connected: isConnected }">
|
||||
{{ isConnected ? '🟢 Подключено' : '🔴 Не подключено' }}
|
||||
</div>
|
||||
<button @click="checkConnection" :disabled="checking">
|
||||
{{ checking ? 'Проверка...' : 'Проверить подключение' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Установленные модели -->
|
||||
<div class="installed-models" v-if="isConnected">
|
||||
<h4>Установленные модели</h4>
|
||||
<div v-if="installedModels.length === 0" class="no-models">
|
||||
Нет установленных моделей
|
||||
</div>
|
||||
<div v-else class="models-list">
|
||||
<div v-for="model in installedModels" :key="model.name" class="model-item">
|
||||
<div class="model-info">
|
||||
<div class="model-name">{{ model.name }}</div>
|
||||
<div class="model-size">{{ formatSize(model.size) }}</div>
|
||||
<div class="model-modified">{{ formatDate(model.modified) }}</div>
|
||||
</div>
|
||||
<div class="model-actions">
|
||||
<button @click="removeModel(model.name)" :disabled="removing === model.name" class="remove-btn">
|
||||
{{ removing === model.name ? 'Удаление...' : 'Удалить' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Поиск и установка моделей -->
|
||||
<div class="model-search" v-if="isConnected">
|
||||
<h4>Установить новую модель</h4>
|
||||
<div class="search-form">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
placeholder="Введите название модели (например: qwen2.5:7b, llama2:7b)"
|
||||
@keyup.enter="searchModels"
|
||||
/>
|
||||
<button @click="searchModels" :disabled="searching || !searchQuery">
|
||||
{{ searching ? 'Поиск...' : 'Найти' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Популярные модели -->
|
||||
<div class="popular-models">
|
||||
<h5>Популярные модели:</h5>
|
||||
<div class="popular-list">
|
||||
<button
|
||||
v-for="model in popularModels"
|
||||
:key="model"
|
||||
@click="installModel(model)"
|
||||
:disabled="installing === model"
|
||||
class="popular-model-btn"
|
||||
>
|
||||
{{ installing === model ? 'Установка...' : model }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Инструкции -->
|
||||
<div class="instructions" v-if="!isConnected">
|
||||
<h4>Как установить Ollama</h4>
|
||||
<div class="instruction-steps">
|
||||
<div class="step">
|
||||
<strong>1. Установите Ollama:</strong>
|
||||
<a href="https://ollama.ai/download" target="_blank" class="download-link">
|
||||
Скачать с официального сайта
|
||||
</a>
|
||||
</div>
|
||||
<div class="step">
|
||||
<strong>2. Запустите Ollama:</strong>
|
||||
<code>ollama serve</code>
|
||||
</div>
|
||||
<div class="step">
|
||||
<strong>3. Установите модель:</strong>
|
||||
<code>ollama pull qwen2.5:7b</code>
|
||||
</div>
|
||||
<div class="step">
|
||||
<strong>4. Проверьте подключение:</strong>
|
||||
Нажмите кнопку "Проверить подключение" выше
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import axios from 'axios';
|
||||
|
||||
const isConnected = ref(false);
|
||||
const checking = ref(false);
|
||||
const installedModels = ref([]);
|
||||
const searchQuery = ref('');
|
||||
const searching = ref(false);
|
||||
const installing = ref('');
|
||||
const removing = ref('');
|
||||
|
||||
const popularModels = [
|
||||
'qwen2.5:7b',
|
||||
'llama2:7b',
|
||||
'mistral:7b',
|
||||
'codellama:7b',
|
||||
'llama2:13b',
|
||||
'qwen2.5:14b'
|
||||
];
|
||||
|
||||
// Проверка подключения к Ollama
|
||||
async function checkConnection() {
|
||||
checking.value = true;
|
||||
try {
|
||||
const response = await axios.get('/ollama/status');
|
||||
isConnected.value = response.data.connected;
|
||||
if (isConnected.value) {
|
||||
await loadInstalledModels();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка проверки подключения:', error);
|
||||
isConnected.value = false;
|
||||
} finally {
|
||||
checking.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Загрузка установленных моделей
|
||||
async function loadInstalledModels() {
|
||||
try {
|
||||
const response = await axios.get('/ollama/models');
|
||||
installedModels.value = response.data.models || [];
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки моделей:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Поиск моделей
|
||||
async function searchModels() {
|
||||
if (!searchQuery.value.trim()) return;
|
||||
|
||||
searching.value = true;
|
||||
try {
|
||||
// Здесь можно добавить поиск моделей в реестре Ollama
|
||||
// Пока просто устанавливаем модель напрямую
|
||||
await installModel(searchQuery.value.trim());
|
||||
} catch (error) {
|
||||
console.error('Ошибка поиска моделей:', error);
|
||||
} finally {
|
||||
searching.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Установка модели
|
||||
async function installModel(modelName) {
|
||||
installing.value = modelName;
|
||||
try {
|
||||
await axios.post('/ollama/install', { model: modelName });
|
||||
await loadInstalledModels();
|
||||
searchQuery.value = '';
|
||||
} catch (error) {
|
||||
console.error('Ошибка установки модели:', error);
|
||||
} finally {
|
||||
installing.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Удаление модели
|
||||
async function removeModel(modelName) {
|
||||
removing.value = modelName;
|
||||
try {
|
||||
await axios.delete(`/ollama/models/${encodeURIComponent(modelName)}`);
|
||||
await loadInstalledModels();
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления модели:', error);
|
||||
} finally {
|
||||
removing.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Форматирование размера
|
||||
function formatSize(bytes) {
|
||||
if (bytes === 0) return '0 Б';
|
||||
const k = 1024;
|
||||
const sizes = ['Б', 'КБ', 'МБ', 'ГБ'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
// Форматирование даты
|
||||
function formatDate(dateString) {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('ru-RU', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkConnection();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ollama-model-manager {
|
||||
background: #fff;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-indicator.connected {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.status-indicator:not(.connected) {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.installed-models {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.no-models {
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.models-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.model-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.model-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.model-name {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.model-size, .model-modified {
|
||||
font-size: 0.9em;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.remove-btn:hover:not(:disabled) {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
.remove-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.model-search {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-form input {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.search-form button {
|
||||
padding: 10px 20px;
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-form button:hover:not(:disabled) {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.search-form button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.popular-models h5 {
|
||||
margin-bottom: 10px;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.popular-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.popular-model-btn {
|
||||
padding: 8px 16px;
|
||||
background: #e9ecef;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.popular-model-btn:hover:not(:disabled) {
|
||||
background: #dee2e6;
|
||||
border-color: #adb5bd;
|
||||
}
|
||||
|
||||
.popular-model-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.instruction-steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.step code {
|
||||
background: #e9ecef;
|
||||
padding: 5px 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.download-link {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.download-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
h3, h4, h5 {
|
||||
margin-bottom: 15px;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1em;
|
||||
}
|
||||
</style>
|
||||
@@ -14,6 +14,7 @@ import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
|
||||
import api from '../api/axios';
|
||||
import { getFromStorage, setToStorage, removeFromStorage } from '../utils/storage';
|
||||
import { generateUniqueId } from '../utils/helpers';
|
||||
import websocketService from '../services/websocketService';
|
||||
|
||||
function initGuestId() {
|
||||
let id = getFromStorage('guestId', '');
|
||||
@@ -396,11 +397,72 @@ export function useChat(auth) {
|
||||
hasUserSentMessage.value = false;
|
||||
newMessage.value = '';
|
||||
attachments.value = [];
|
||||
// Отключаем WebSocket
|
||||
cleanupWebSocket();
|
||||
// Гостевые данные очищаются при успешной аутентификации в loadMessages
|
||||
// или если пользователь сам очистит localStorage
|
||||
} else if (isAuth && !wasAuth) { // Если пользователь вошел
|
||||
console.log('[useChat] Пользователь вошел, подключаем WebSocket.');
|
||||
// Отложенное подключение, чтобы дождаться загрузки данных пользователя
|
||||
setTimeout(() => setupChatWebSocket(), 100);
|
||||
}
|
||||
});
|
||||
|
||||
// Отслеживаем загрузку данных пользователя для подключения WebSocket
|
||||
watch(() => auth.user?.value, (newUser, oldUser) => {
|
||||
if (newUser && newUser.id && auth.isAuthenticated.value) {
|
||||
console.log('[useChat] Данные пользователя загружены, подключаем WebSocket:', newUser.id);
|
||||
setupChatWebSocket();
|
||||
}
|
||||
}, { immediate: false });
|
||||
|
||||
// --- WebSocket для real-time сообщений ---
|
||||
function setupChatWebSocket() {
|
||||
// Подключаемся к WebSocket только если пользователь аутентифицирован
|
||||
if (auth.isAuthenticated.value && auth.user && auth.user.value && auth.user.value.id) {
|
||||
console.log('[useChat] Подключение к WebSocket для пользователя:', auth.user.value.id);
|
||||
websocketService.connect(auth.user.value.id);
|
||||
|
||||
// Подписываемся на события
|
||||
websocketService.on('chat-message', (message) => {
|
||||
console.log('[useChat] Получено новое сообщение через WebSocket:', message);
|
||||
// Проверяем, что сообщение не дублируется
|
||||
const existingMessage = messages.value.find(m => m.id === message.id);
|
||||
if (!existingMessage) {
|
||||
messages.value.push(message);
|
||||
}
|
||||
});
|
||||
|
||||
websocketService.on('conversation-updated', (conversationId) => {
|
||||
console.log('[useChat] Обновление диалога через WebSocket:', conversationId);
|
||||
// Можно добавить логику обновления списка диалогов
|
||||
});
|
||||
|
||||
websocketService.on('connected', () => {
|
||||
console.log('[useChat] WebSocket подключен');
|
||||
});
|
||||
|
||||
websocketService.on('disconnected', () => {
|
||||
console.log('[useChat] WebSocket отключен');
|
||||
});
|
||||
|
||||
websocketService.on('error', (error) => {
|
||||
console.error('[useChat] WebSocket ошибка:', error);
|
||||
});
|
||||
} else {
|
||||
console.log('[useChat] WebSocket не подключен: пользователь не аутентифицирован или данные не загружены');
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupWebSocket() {
|
||||
websocketService.off('chat-message');
|
||||
websocketService.off('conversation-updated');
|
||||
websocketService.off('connected');
|
||||
websocketService.off('disconnected');
|
||||
websocketService.off('error');
|
||||
websocketService.disconnect();
|
||||
}
|
||||
|
||||
// --- Инициализация ---
|
||||
onMounted(() => {
|
||||
if (!auth.isAuthenticated.value && guestId.value) {
|
||||
@@ -408,33 +470,13 @@ export function useChat(auth) {
|
||||
} else if (auth.isAuthenticated.value) {
|
||||
loadMessages({ initial: true });
|
||||
}
|
||||
// Добавляем слушатель для возможности принудительной перезагрузки
|
||||
// window.addEventListener('load-chat-history', () => loadMessages({ initial: true }));
|
||||
});
|
||||
|
||||
// --- WebSocket для real-time сообщений ---
|
||||
let ws = null;
|
||||
function setupChatWebSocket() {
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
ws = new WebSocket(`${wsProtocol}://${window.location.host}/ws`);
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.type === 'chat-message' && data.message) {
|
||||
// Проверяем, что сообщение для текущего пользователя/диалога
|
||||
// (можно доработать фильтрацию по conversation_id, user_id и т.д.)
|
||||
messages.value.push(data.message);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[useChat] Ошибка обработки chat-message по WebSocket:', e);
|
||||
}
|
||||
};
|
||||
}
|
||||
onMounted(() => {
|
||||
|
||||
// Подключаем WebSocket если пользователь уже аутентифицирован
|
||||
setupChatWebSocket();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (ws) ws.close();
|
||||
cleanupWebSocket();
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -55,6 +55,16 @@ const routes = [
|
||||
name: 'settings-blockchain',
|
||||
component: SettingsBlockchainView,
|
||||
},
|
||||
{
|
||||
path: 'blockchain/dle-deploy',
|
||||
name: 'settings-blockchain-dle-deploy',
|
||||
component: () => import('../views/settings/BlockchainSettingsView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'dle-v2-deploy',
|
||||
name: 'settings-dle-v2-deploy',
|
||||
component: () => import('../views/settings/DleDeployFormView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'security',
|
||||
name: 'settings-security',
|
||||
@@ -201,6 +211,66 @@ const routes = [
|
||||
name: 'page-edit',
|
||||
component: () => import('../views/content/PageEditView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/management',
|
||||
name: 'management',
|
||||
component: () => import('../views/ManagementView.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/management/dle',
|
||||
name: 'management-dle',
|
||||
component: () => import('../views/smartcontracts/DleManagementView.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/management/proposals',
|
||||
name: 'management-proposals',
|
||||
component: () => import('../views/smartcontracts/ProposalsView.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/management/tokens',
|
||||
name: 'management-tokens',
|
||||
component: () => import('../views/smartcontracts/TokensView.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/management/quorum',
|
||||
name: 'management-quorum',
|
||||
component: () => import('../views/smartcontracts/QuorumView.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/management/modules',
|
||||
name: 'management-modules',
|
||||
component: () => import('../views/smartcontracts/ModulesView.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/management/treasury',
|
||||
name: 'management-treasury',
|
||||
component: () => import('../views/smartcontracts/TreasuryView.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/management/analytics',
|
||||
name: 'management-analytics',
|
||||
component: () => import('../views/smartcontracts/AnalyticsView.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/management/history',
|
||||
name: 'management-history',
|
||||
component: () => import('../views/smartcontracts/HistoryView.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/management/settings',
|
||||
name: 'management-settings',
|
||||
component: () => import('../views/smartcontracts/SettingsView.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
85
frontend/src/services/dleV2Service.js
Normal file
85
frontend/src/services/dleV2Service.js
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
/**
|
||||
* Сервис для работы с DLE v2 (Digital Legal Entity)
|
||||
* Современный подход с единым контрактом
|
||||
*/
|
||||
class DLEV2Service {
|
||||
/**
|
||||
* Создает новое DLE v2
|
||||
* @param {Object} dleParams - Параметры DLE
|
||||
* @returns {Promise<Object>} - Результат создания
|
||||
*/
|
||||
async createDLE(dleParams) {
|
||||
try {
|
||||
const response = await axios.post('/api/dle-v2', dleParams);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при создании DLE v2:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает список всех DLE v2
|
||||
* @returns {Promise<Array>} - Список DLE v2
|
||||
*/
|
||||
async getAllDLEs() {
|
||||
try {
|
||||
const response = await axios.get('/api/dle-v2');
|
||||
return response.data.data || [];
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении списка DLE v2:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает настройки по умолчанию для DLE v2
|
||||
* @returns {Promise<Object>} - Настройки по умолчанию
|
||||
*/
|
||||
async getDefaults() {
|
||||
try {
|
||||
const response = await axios.get('/api/dle-v2/defaults');
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении настроек по умолчанию DLE v2:', error);
|
||||
return {
|
||||
votingDelay: 1,
|
||||
votingPeriod: 45818,
|
||||
proposalThreshold: '100000',
|
||||
quorumPercentage: 4,
|
||||
minTimelockDelay: 2
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаляет DLE v2 по адресу
|
||||
* @param {string} dleAddress - Адрес DLE
|
||||
* @returns {Promise<Object>} - Результат удаления
|
||||
*/
|
||||
async deleteDLE(dleAddress) {
|
||||
try {
|
||||
const response = await axios.delete(`/api/dle-v2/${dleAddress}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при удалении DLE v2:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new DLEV2Service();
|
||||
@@ -49,6 +49,7 @@ export async function connectWithWallet() {
|
||||
const origin = window.location.origin;
|
||||
const statement = 'Sign in with Ethereum to the app.';
|
||||
|
||||
const issuedAt = new Date().toISOString();
|
||||
const siweMessage = new SiweMessage({
|
||||
domain,
|
||||
address,
|
||||
@@ -57,11 +58,23 @@ export async function connectWithWallet() {
|
||||
version: '1',
|
||||
chainId: 1,
|
||||
nonce,
|
||||
issuedAt,
|
||||
resources: [`${origin}/api/auth/verify`],
|
||||
});
|
||||
|
||||
const message = siweMessage.prepareMessage();
|
||||
console.log('SIWE message:', message);
|
||||
console.log('SIWE message details:', {
|
||||
domain,
|
||||
address,
|
||||
statement,
|
||||
uri: origin,
|
||||
version: '1',
|
||||
chainId: 1,
|
||||
nonce,
|
||||
issuedAt,
|
||||
resources: [`${origin}/api/auth/verify`],
|
||||
});
|
||||
|
||||
// Запрашиваем подпись
|
||||
console.log('Requesting signature...');
|
||||
@@ -75,9 +88,10 @@ export async function connectWithWallet() {
|
||||
// Отправляем подпись на сервер для верификации
|
||||
console.log('Sending verification request...');
|
||||
const verificationResponse = await axios.post('/auth/verify', {
|
||||
message,
|
||||
signature,
|
||||
address,
|
||||
nonce,
|
||||
issuedAt,
|
||||
});
|
||||
|
||||
console.log('Verification response:', verificationResponse.data);
|
||||
|
||||
192
frontend/src/services/websocketService.js
Normal file
192
frontend/src/services/websocketService.js
Normal file
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* WebSocket сервис для реального времени обновлений
|
||||
*/
|
||||
|
||||
class WebSocketService {
|
||||
constructor() {
|
||||
this.ws = null;
|
||||
this.isConnected = false;
|
||||
this.reconnectAttempts = 0;
|
||||
this.maxReconnectAttempts = 5;
|
||||
this.reconnectDelay = 1000; // 1 секунда
|
||||
this.listeners = new Map();
|
||||
this.userId = null;
|
||||
}
|
||||
|
||||
// Подключение к WebSocket серверу
|
||||
connect(userId = null) {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
console.log('🔌 [WebSocket] Уже подключен');
|
||||
return;
|
||||
}
|
||||
|
||||
this.userId = userId;
|
||||
|
||||
try {
|
||||
// Определяем WebSocket URL
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
// В Docker окружении backend работает на порту 8000
|
||||
const backendHost = window.location.hostname + ':8000';
|
||||
const wsUrl = `${protocol}//${backendHost}/ws`;
|
||||
|
||||
console.log('🔌 [WebSocket] Подключение к:', wsUrl);
|
||||
|
||||
this.ws = new WebSocket(wsUrl);
|
||||
|
||||
this.ws.onopen = () => {
|
||||
console.log('✅ [WebSocket] Подключение установлено');
|
||||
this.isConnected = true;
|
||||
this.reconnectAttempts = 0;
|
||||
|
||||
// Аутентификация пользователя
|
||||
if (this.userId) {
|
||||
this.send({
|
||||
type: 'auth',
|
||||
userId: this.userId
|
||||
});
|
||||
}
|
||||
|
||||
this.emit('connected');
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('📨 [WebSocket] Получено сообщение:', data);
|
||||
this.handleMessage(data);
|
||||
} catch (error) {
|
||||
console.error('❌ [WebSocket] Ошибка парсинга сообщения:', error);
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onclose = (event) => {
|
||||
console.log('🔌 [WebSocket] Соединение закрыто:', event.code, event.reason);
|
||||
this.isConnected = false;
|
||||
this.emit('disconnected', event);
|
||||
|
||||
// Попытка переподключения
|
||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.reconnectAttempts++;
|
||||
console.log(`🔄 [WebSocket] Попытка переподключения ${this.reconnectAttempts}/${this.maxReconnectAttempts}`);
|
||||
|
||||
setTimeout(() => {
|
||||
this.connect(this.userId);
|
||||
}, this.reconnectDelay * this.reconnectAttempts);
|
||||
} else {
|
||||
console.error('❌ [WebSocket] Превышено максимальное количество попыток переподключения');
|
||||
this.emit('reconnect-failed');
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onerror = (error) => {
|
||||
console.error('❌ [WebSocket] Ошибка соединения:', error);
|
||||
this.emit('error', error);
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ [WebSocket] Ошибка создания соединения:', error);
|
||||
this.emit('error', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Отправка сообщения
|
||||
send(data) {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(JSON.stringify(data));
|
||||
} else {
|
||||
console.warn('⚠️ [WebSocket] Соединение не установлено, сообщение не отправлено:', data);
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка входящих сообщений
|
||||
handleMessage(data) {
|
||||
switch (data.type) {
|
||||
case 'auth-success':
|
||||
console.log('✅ [WebSocket] Аутентификация успешна для пользователя:', data.userId);
|
||||
this.emit('auth-success', data);
|
||||
break;
|
||||
|
||||
case 'chat-message':
|
||||
console.log('💬 [WebSocket] Новое сообщение чата:', data.message);
|
||||
this.emit('chat-message', data.message);
|
||||
break;
|
||||
|
||||
case 'conversation-updated':
|
||||
console.log('📝 [WebSocket] Обновление диалога:', data.conversationId);
|
||||
this.emit('conversation-updated', data.conversationId);
|
||||
break;
|
||||
|
||||
case 'messages-updated':
|
||||
console.log('📨 [WebSocket] Обновление сообщений');
|
||||
this.emit('messages-updated');
|
||||
break;
|
||||
|
||||
case 'contacts-updated':
|
||||
console.log('👥 [WebSocket] Обновление контактов');
|
||||
this.emit('contacts-updated');
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('❓ [WebSocket] Неизвестный тип сообщения:', data.type);
|
||||
this.emit('unknown-message', data);
|
||||
}
|
||||
}
|
||||
|
||||
// Подписка на события
|
||||
on(event, callback) {
|
||||
if (!this.listeners.has(event)) {
|
||||
this.listeners.set(event, []);
|
||||
}
|
||||
this.listeners.get(event).push(callback);
|
||||
}
|
||||
|
||||
// Отписка от событий
|
||||
off(event, callback) {
|
||||
if (this.listeners.has(event)) {
|
||||
const callbacks = this.listeners.get(event);
|
||||
const index = callbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
callbacks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Эмиссия событий
|
||||
emit(event, data) {
|
||||
if (this.listeners.has(event)) {
|
||||
this.listeners.get(event).forEach(callback => {
|
||||
try {
|
||||
callback(data);
|
||||
} catch (error) {
|
||||
console.error(`❌ [WebSocket] Ошибка в обработчике события ${event}:`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Отключение
|
||||
disconnect() {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
this.isConnected = false;
|
||||
this.listeners.clear();
|
||||
console.log('🔌 [WebSocket] Отключен');
|
||||
}
|
||||
|
||||
// Получение статуса соединения
|
||||
getStatus() {
|
||||
return {
|
||||
isConnected: this.isConnected,
|
||||
readyState: this.ws ? this.ws.readyState : null,
|
||||
reconnectAttempts: this.reconnectAttempts,
|
||||
userId: this.userId
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем единственный экземпляр
|
||||
const websocketService = new WebSocketService();
|
||||
|
||||
export default websocketService;
|
||||
@@ -10,7 +10,7 @@
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
import axios from '../api/axios';
|
||||
import axios from 'axios';
|
||||
import { ethers } from 'ethers';
|
||||
import { SiweMessage } from 'siwe';
|
||||
|
||||
@@ -100,10 +100,15 @@ export const connectWallet = async () => {
|
||||
|
||||
// Отправляем верификацию на сервер
|
||||
console.log('Sending verification request...');
|
||||
const verifyResponse = await axios.post('/auth/verify', {
|
||||
const requestData = {
|
||||
address: normalizedAddress,
|
||||
signature,
|
||||
nonce,
|
||||
};
|
||||
console.log('Request data:', requestData);
|
||||
|
||||
const verifyResponse = await axios.post('/api/auth/verify', requestData, {
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
// Обновляем интерфейс для отображения подключенного состояния
|
||||
|
||||
@@ -21,27 +21,34 @@
|
||||
<div class="crm-view-container">
|
||||
<div class="dle-management-block">
|
||||
<h2>Управление DLE</h2>
|
||||
<button class="btn btn-info" @click="goToDleManagement">
|
||||
<i class="fas fa-cogs"></i> Подробнее
|
||||
<button class="details-btn" @click="goToDleManagement">
|
||||
Подробнее
|
||||
</button>
|
||||
</div>
|
||||
<div class="crm-contacts-block">
|
||||
<h2>Контакты</h2>
|
||||
<button class="btn btn-info" @click="goToContactsList">
|
||||
<i class="fas fa-address-book"></i> Подробнее
|
||||
<button class="details-btn" @click="goToContactsList">
|
||||
Подробнее
|
||||
</button>
|
||||
</div>
|
||||
<div class="crm-tables-block">
|
||||
<h2>Таблицы</h2>
|
||||
<button class="btn btn-info" @click="goToTables">
|
||||
<i class="fas fa-table"></i> Подробнее
|
||||
<button class="details-btn" @click="goToTables">
|
||||
Подробнее
|
||||
</button>
|
||||
</div>
|
||||
<!-- Новый блок Контент -->
|
||||
<div class="crm-content-block">
|
||||
<h2>Контент</h2>
|
||||
<button class="btn btn-info" @click="goToContent">
|
||||
<i class="fas fa-file-alt"></i> Подробнее
|
||||
<button class="details-btn" @click="goToContent">
|
||||
Подробнее
|
||||
</button>
|
||||
</div>
|
||||
<!-- Новый блок Управление -->
|
||||
<div class="crm-management-block">
|
||||
<h2>Управление</h2>
|
||||
<button class="details-btn" @click="goToManagement">
|
||||
Подробнее
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -218,6 +225,10 @@ function goToContactsList() {
|
||||
function goToContent() {
|
||||
router.push({ name: 'content-list' });
|
||||
}
|
||||
|
||||
function goToManagement() {
|
||||
router.push({ name: 'management' });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -258,57 +269,28 @@ strong {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
user-select: none;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
border-radius: 0.25rem;
|
||||
transition: all 0.15s ease-in-out;
|
||||
.details-btn {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background 0.2s;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
border-radius: 0.2rem;
|
||||
.details-btn:hover {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
.details-btn-secondary {
|
||||
background: #6c757d;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
color: #fff;
|
||||
background-color: var(--color-grey-dark);
|
||||
border-color: var(--color-grey-dark);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
color: #fff;
|
||||
background-color: #28a745;
|
||||
border-color: #28a745;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
color: #fff;
|
||||
background-color: #dc3545;
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
color: #fff;
|
||||
background-color: #17a2b8;
|
||||
border-color: #17a2b8;
|
||||
.details-btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
.dle-management-block {
|
||||
@@ -326,9 +308,8 @@ strong {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.dle-management-block .btn {
|
||||
font-size: 1rem;
|
||||
padding: 8px 18px;
|
||||
.dle-management-block .details-btn {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.crm-contacts-block {
|
||||
@@ -346,9 +327,8 @@ strong {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.crm-contacts-block .btn {
|
||||
font-size: 1rem;
|
||||
padding: 8px 18px;
|
||||
.crm-contacts-block .details-btn {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.crm-tables-block {
|
||||
@@ -366,9 +346,8 @@ strong {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.crm-tables-block .btn {
|
||||
font-size: 1rem;
|
||||
padding: 8px 18px;
|
||||
.crm-tables-block .details-btn {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.crm-content-block {
|
||||
@@ -386,8 +365,26 @@ strong {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.crm-content-block .btn {
|
||||
font-size: 1rem;
|
||||
padding: 8px 18px;
|
||||
.crm-content-block .details-btn {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.crm-management-block {
|
||||
margin: 32px 0 24px 0;
|
||||
padding: 24px;
|
||||
background: #f8fafc;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.crm-management-block h2 {
|
||||
margin: 0;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.crm-management-block .details-btn {
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
277
frontend/src/views/ManagementView.vue
Normal file
277
frontend/src/views/ManagementView.vue
Normal file
@@ -0,0 +1,277 @@
|
||||
<!--
|
||||
Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
All rights reserved.
|
||||
|
||||
This software is proprietary and confidential.
|
||||
Unauthorized copying, modification, or distribution is prohibited.
|
||||
|
||||
For licensing inquiries: info@hb3-accelerator.com
|
||||
Website: https://hb3-accelerator.com
|
||||
GitHub: https://github.com/HB3-ACCELERATOR
|
||||
-->
|
||||
|
||||
<template>
|
||||
<BaseLayout
|
||||
:is-authenticated="isAuthenticated"
|
||||
:identities="identities"
|
||||
:token-balances="tokenBalances"
|
||||
:is-loading-tokens="isLoadingTokens"
|
||||
@auth-action-completed="$emit('auth-action-completed')"
|
||||
>
|
||||
<div class="management-container">
|
||||
<!-- Заголовок -->
|
||||
<div class="management-header">
|
||||
<h1>Управление DLE</h1>
|
||||
<button class="close-btn" @click="router.push('/')">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Блоки управления -->
|
||||
<div class="management-blocks">
|
||||
<!-- Первый ряд -->
|
||||
<div class="blocks-row">
|
||||
<div class="management-block">
|
||||
<h3>Предложения</h3>
|
||||
<p>Создание, подписание, выполнение</p>
|
||||
<button class="details-btn" @click="openProposals">Подробнее</button>
|
||||
</div>
|
||||
|
||||
<div class="management-block">
|
||||
<h3>Токены DLE</h3>
|
||||
<p>Балансы, трансферы, распределение</p>
|
||||
<button class="details-btn" @click="openTokens">Подробнее</button>
|
||||
</div>
|
||||
|
||||
<div class="management-block">
|
||||
<h3>Кворум</h3>
|
||||
<p>Настройки голосования</p>
|
||||
<button class="details-btn" @click="openQuorum">Подробнее</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Второй ряд -->
|
||||
<div class="blocks-row">
|
||||
<div class="management-block">
|
||||
<h3>Модули DLE</h3>
|
||||
<p>Установка, настройка, управление</p>
|
||||
<button class="details-btn" @click="openModules">Подробнее</button>
|
||||
</div>
|
||||
|
||||
<div class="management-block">
|
||||
<h3>DLE</h3>
|
||||
<p>Интеграция с другими DLE, участие в кворумах</p>
|
||||
<button class="details-btn" @click="openDle">Подробнее</button>
|
||||
</div>
|
||||
|
||||
<div class="management-block">
|
||||
<h3>Казна</h3>
|
||||
<p>Управление средствами</p>
|
||||
<button class="details-btn" @click="openTreasury">Подробнее</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Третий ряд -->
|
||||
<div class="blocks-row">
|
||||
<div class="management-block">
|
||||
<h3>Аналитика</h3>
|
||||
<p>Графики, статистика, отчеты</p>
|
||||
<button class="details-btn" @click="openAnalytics">Подробнее</button>
|
||||
</div>
|
||||
|
||||
<div class="management-block">
|
||||
<h3>История</h3>
|
||||
<p>Лог операций, события, транзакции</p>
|
||||
<button class="details-btn" @click="openHistory">Подробнее</button>
|
||||
</div>
|
||||
|
||||
<div class="management-block">
|
||||
<h3>Настройки</h3>
|
||||
<p>Параметры DLE, конфигурация</p>
|
||||
<button class="details-btn" @click="openSettings">Подробнее</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, defineEmits } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import BaseLayout from '../components/BaseLayout.vue';
|
||||
|
||||
// Определяем props
|
||||
const props = defineProps({
|
||||
isAuthenticated: Boolean,
|
||||
identities: Array,
|
||||
tokenBalances: Object,
|
||||
isLoadingTokens: Boolean
|
||||
});
|
||||
|
||||
// Определяем emits
|
||||
const emit = defineEmits(['auth-action-completed']);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// Функции для открытия страниц управления
|
||||
const openProposals = () => {
|
||||
router.push('/management/proposals');
|
||||
};
|
||||
|
||||
const openTokens = () => {
|
||||
router.push('/management/tokens');
|
||||
};
|
||||
|
||||
const openQuorum = () => {
|
||||
router.push('/management/quorum');
|
||||
};
|
||||
|
||||
const openModules = () => {
|
||||
router.push('/management/modules');
|
||||
};
|
||||
|
||||
const openDle = () => {
|
||||
router.push('/management/dle');
|
||||
};
|
||||
|
||||
const openTreasury = () => {
|
||||
router.push('/management/treasury');
|
||||
};
|
||||
|
||||
const openAnalytics = () => {
|
||||
router.push('/management/analytics');
|
||||
};
|
||||
|
||||
const openHistory = () => {
|
||||
router.push('/management/history');
|
||||
};
|
||||
|
||||
const openSettings = () => {
|
||||
router.push('/management/settings');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.management-container {
|
||||
padding: 20px;
|
||||
background-color: var(--color-white);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.management-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 40px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.management-header h1 {
|
||||
color: var(--color-primary);
|
||||
font-size: 2.5rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Блоки управления */
|
||||
.management-blocks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.blocks-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.management-block {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
padding: 2rem;
|
||||
min-width: 260px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.management-block:hover {
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.management-block h3 {
|
||||
color: var(--color-primary);
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.management-block p {
|
||||
color: var(--color-grey-dark);
|
||||
margin: 0 0 1.5rem 0;
|
||||
line-height: 1.5;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.details-btn {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
transition: background 0.2s;
|
||||
margin: 0;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.details-btn:hover {
|
||||
background: var(--color-primary-dark);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.blocks-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.management-block {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.management-block h3 {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -19,7 +19,14 @@
|
||||
@auth-action-completed="$emit('auth-action-completed')"
|
||||
>
|
||||
<div class="settings-view-container">
|
||||
<h1>Настройки</h1>
|
||||
<div class="page-header">
|
||||
<h1>{{ pageTitle }}</h1>
|
||||
<button
|
||||
v-if="route.name === 'settings-blockchain-dle-deploy' || route.name === 'settings-dle-v2-deploy'"
|
||||
class="close-btn"
|
||||
@click="router.push('/settings')"
|
||||
>×</button>
|
||||
</div>
|
||||
<!-- Router view для отображения дочерних компонентов настроек -->
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
@@ -50,6 +57,17 @@ const router = useRouter();
|
||||
const route = useRoute();
|
||||
const isLoading = ref(true);
|
||||
|
||||
// Вычисляемый заголовок страницы в зависимости от роута
|
||||
const pageTitle = computed(() => {
|
||||
if (route.name === 'settings-blockchain-dle-deploy') {
|
||||
return 'Создать новое DLE (Digital Legal Entity)';
|
||||
}
|
||||
if (route.name === 'settings-dle-v2-deploy') {
|
||||
return 'Создать современное DLE v2 (Digital Legal Entity)';
|
||||
}
|
||||
return 'Настройки';
|
||||
});
|
||||
|
||||
// Обработчик события изменения авторизации
|
||||
const handleAuthEvent = (eventData) => {
|
||||
console.log('[SettingsView] Получено событие изменения авторизации:', eventData);
|
||||
@@ -91,9 +109,37 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
/* Заголовки */
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--color-dark);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
h3 {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
:showApiKey="false"
|
||||
:showBaseUrl="true"
|
||||
/>
|
||||
<OllamaModelManager />
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
@@ -31,6 +32,7 @@
|
||||
<script setup>
|
||||
import BaseLayout from '@/components/BaseLayout.vue';
|
||||
import AIProviderSettings from '@/views/settings/AIProviderSettings.vue';
|
||||
import OllamaModelManager from '@/components/OllamaModelManager.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@@ -103,20 +103,41 @@ async function loadSettings() {
|
||||
|
||||
async function loadModels() {
|
||||
try {
|
||||
const { data } = await axios.get(`/settings/ai-settings/${props.provider}/models`);
|
||||
let data;
|
||||
if (props.provider === 'ollama') {
|
||||
// Для Ollama используем специальный API
|
||||
const response = await axios.get('/ollama/models');
|
||||
data = { models: response.data.models.map(m => ({ id: m.name, name: m.name })) };
|
||||
} else {
|
||||
// Для других провайдеров используем стандартный API
|
||||
const response = await axios.get(`/settings/ai-settings/${props.provider}/models`);
|
||||
data = response.data;
|
||||
}
|
||||
|
||||
models.value = data.models || [];
|
||||
if (!selectedModel.value && models.value.length) {
|
||||
const first = models.value[0];
|
||||
selectedModel.value = first.id || first.name || first;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading models:', e);
|
||||
models.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function loadEmbeddingModels() {
|
||||
try {
|
||||
const { data } = await axios.get(`/settings/ai-settings/${props.provider}/models`);
|
||||
let data;
|
||||
if (props.provider === 'ollama') {
|
||||
// Для Ollama используем специальный API
|
||||
const response = await axios.get('/ollama/models');
|
||||
data = { models: response.data.models.map(m => ({ id: m.name, name: m.name })) };
|
||||
} else {
|
||||
// Для других провайдеров используем стандартный API
|
||||
const response = await axios.get(`/settings/ai-settings/${props.provider}/models`);
|
||||
data = response.data;
|
||||
}
|
||||
|
||||
embeddingModels.value = (data.models || []).filter(m => {
|
||||
const name = m.id || m.name || m;
|
||||
return name && name.toLowerCase().includes('embed');
|
||||
@@ -126,6 +147,7 @@ async function loadEmbeddingModels() {
|
||||
selectedEmbeddingModel.value = first.id || first.name || first;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading embedding models:', e);
|
||||
embeddingModels.value = [];
|
||||
}
|
||||
}
|
||||
@@ -135,13 +157,24 @@ async function onVerify() {
|
||||
verifyStatus.value = null;
|
||||
verifyError.value = '';
|
||||
try {
|
||||
const { data } = await axios.post(`/settings/ai-settings/${props.provider}/verify`, {
|
||||
api_key: apiKey.value,
|
||||
base_url: baseUrl.value,
|
||||
});
|
||||
let data;
|
||||
if (props.provider === 'ollama') {
|
||||
// Для Ollama используем специальный API
|
||||
const response = await axios.get('/ollama/status');
|
||||
data = { success: response.data.connected };
|
||||
} else {
|
||||
// Для других провайдеров используем стандартный API
|
||||
const response = await axios.post(`/settings/ai-settings/${props.provider}/verify`, {
|
||||
api_key: apiKey.value,
|
||||
base_url: baseUrl.value,
|
||||
});
|
||||
data = response.data;
|
||||
}
|
||||
|
||||
verifyStatus.value = data.success;
|
||||
if (data.success) {
|
||||
await loadModels();
|
||||
await loadEmbeddingModels();
|
||||
}
|
||||
} catch (e) {
|
||||
verifyStatus.value = false;
|
||||
|
||||
@@ -396,6 +396,8 @@ const dleDeploymentSettings = reactive({
|
||||
selectedIsicCodes: [], // <<< Для хранения массива выбранных кодов ISIC
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Добавляем переменную useCustomGas для управления отображением пользовательских настроек газа
|
||||
const useCustomGas = ref(false);
|
||||
|
||||
@@ -668,6 +670,8 @@ const deployDLE = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
const validateDLEForm = () => {
|
||||
// Проверяем обязательные поля
|
||||
if (!dleDeploymentSettings.name) {
|
||||
@@ -1397,4 +1401,6 @@ h3 {
|
||||
.mb-gt-total {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
3799
frontend/src/views/settings/DleDeployFormView.vue
Normal file
3799
frontend/src/views/settings/DleDeployFormView.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,11 @@
|
||||
<p>Интеграция с блокчейн-сетями, RPC, токены и смарт-контракты.</p>
|
||||
<button class="details-btn" @click="$router.push('/settings/blockchain')">Подробнее</button>
|
||||
</div>
|
||||
<div class="main-block">
|
||||
<h3>Блокчейн 2</h3>
|
||||
<p>Современный DLE v2 - единый смарт-контракт с встроенной системой голосования.</p>
|
||||
<button class="details-btn details-btn-secondary" @click="$router.push('/settings/dle-v2-deploy')">Подробнее</button>
|
||||
</div>
|
||||
<div class="main-block">
|
||||
<h3>Безопасность</h3>
|
||||
<p>Управление доступом, токенами, аутентификацией и правами.</p>
|
||||
@@ -32,62 +37,14 @@
|
||||
<p>Настройки серверов, хостинга и публикации приложения.</p>
|
||||
<button class="details-btn" @click="$router.push('/settings/interface')">Подробнее</button>
|
||||
</div>
|
||||
<div class="main-block copyright-block">
|
||||
<h3>© Авторские права</h3>
|
||||
<p>Информация об авторских правах и лицензировании проекта DLE.</p>
|
||||
<div class="copyright-info">
|
||||
<p><strong>Автор:</strong> Тарабанов Александр Викторович</p>
|
||||
<p><strong>Email:</strong> info@hb3-accelerator.com</p>
|
||||
<p><strong>Сайт:</strong> <a href="https://hb3-accelerator.com" target="_blank">hb3-accelerator.com</a></p>
|
||||
<p><strong>GitHub:</strong> <a href="https://github.com/HB3-ACCELERATOR" target="_blank">@HB3-ACCELERATOR</a></p>
|
||||
</div>
|
||||
<button class="details-btn" @click="showCopyrightModal = true">Подробнее</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно с информацией об авторских правах -->
|
||||
<div v-if="showCopyrightModal" class="modal-overlay" @click="showCopyrightModal = false">
|
||||
<div class="modal-content" @click.stop>
|
||||
<div class="modal-header">
|
||||
<h2>© Авторские права DLE</h2>
|
||||
<button class="close-btn" @click="showCopyrightModal = false">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="copyright-section">
|
||||
<h3>Основная информация</h3>
|
||||
<p><strong>Автор:</strong> Тарабанов Александр Викторович</p>
|
||||
<p><strong>Период:</strong> 2024-2025</p>
|
||||
<p><strong>Статус:</strong> Проприетарное программное обеспечение</p>
|
||||
</div>
|
||||
|
||||
<div class="copyright-section">
|
||||
<h3>Контакты</h3>
|
||||
<p><strong>Email:</strong> <a href="mailto:info@hb3-accelerator.com">info@hb3-accelerator.com</a></p>
|
||||
<p><strong>Сайт:</strong> <a href="https://hb3-accelerator.com" target="_blank">hb3-accelerator.com</a></p>
|
||||
<p><strong>GitHub:</strong> <a href="https://github.com/HB3-ACCELERATOR" target="_blank">@HB3-ACCELERATOR</a></p>
|
||||
</div>
|
||||
|
||||
<div class="copyright-section">
|
||||
<h3>Условия использования</h3>
|
||||
<p>✅ <strong>Разрешено:</strong> Использование в бизнесе для внутренних операций</p>
|
||||
<p>❌ <strong>Запрещено:</strong> Перепродажа, модификация, копирование без разрешения</p>
|
||||
<p>📋 <strong>Требуется:</strong> Разрешение автора для коммерческого использования</p>
|
||||
</div>
|
||||
|
||||
<div class="copyright-section">
|
||||
<h3>Лицензирование</h3>
|
||||
<p>Для получения коммерческой лицензии обращайтесь к автору по указанным контактам.</p>
|
||||
<p>Все права защищены. Несанкционированное использование запрещено.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const showCopyrightModal = ref(false);
|
||||
// Компонент настроек
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -107,8 +64,14 @@ const showCopyrightModal = ref(false);
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.details-btn {
|
||||
.button-group {
|
||||
margin-top: 1.5rem;
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.details-btn {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
@@ -117,121 +80,25 @@ const showCopyrightModal = ref(false);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background 0.2s;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.details-btn:hover {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.copyright-block {
|
||||
border-left: 4px solid var(--color-primary);
|
||||
.details-btn-secondary {
|
||||
background: #6c757d;
|
||||
}
|
||||
|
||||
.copyright-info {
|
||||
margin: 1rem 0;
|
||||
padding: 1rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
.details-btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
.copyright-info p {
|
||||
margin: 0.5rem 0;
|
||||
/* Для блоков без группы кнопок */
|
||||
.main-block .details-btn:not(.button-group .details-btn) {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.copyright-info a {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.copyright-info a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Модальное окно */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
margin: 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.copyright-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.copyright-section h3 {
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.copyright-section p {
|
||||
margin: 0.5rem 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.copyright-section a {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.copyright-section a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
705
frontend/src/views/smartcontracts/AnalyticsView.vue
Normal file
705
frontend/src/views/smartcontracts/AnalyticsView.vue
Normal file
@@ -0,0 +1,705 @@
|
||||
<!--
|
||||
Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
All rights reserved.
|
||||
|
||||
This software is proprietary and confidential.
|
||||
Unauthorized copying, modification, or distribution is prohibited.
|
||||
|
||||
For licensing inquiries: info@hb3-accelerator.com
|
||||
Website: https://hb3-accelerator.com
|
||||
GitHub: https://github.com/HB3-ACCELERATOR
|
||||
-->
|
||||
|
||||
<template>
|
||||
<BaseLayout
|
||||
:is-authenticated="isAuthenticated"
|
||||
:identities="identities"
|
||||
:token-balances="tokenBalances"
|
||||
:is-loading-tokens="isLoadingTokens"
|
||||
@auth-action-completed="$emit('auth-action-completed')"
|
||||
>
|
||||
<div class="analytics-container">
|
||||
<!-- Заголовок -->
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<h1>Аналитика</h1>
|
||||
<p>Графики, статистика и отчеты DLE</p>
|
||||
</div>
|
||||
<button class="close-btn" @click="router.push('/management')">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Ключевые метрики -->
|
||||
<div class="metrics-section">
|
||||
<h2>Ключевые метрики</h2>
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-card">
|
||||
<h3>Общая стоимость</h3>
|
||||
<p class="metric-value">${{ totalValue.toLocaleString() }}</p>
|
||||
<p class="metric-change positive">+{{ valueChange }}% (30д)</p>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<h3>Активные участники</h3>
|
||||
<p class="metric-value">{{ activeParticipants }}</p>
|
||||
<p class="metric-change positive">+{{ participantsChange }} (30д)</p>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<h3>Предложения</h3>
|
||||
<p class="metric-value">{{ totalProposals }}</p>
|
||||
<p class="metric-change positive">+{{ proposalsChange }} (30д)</p>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<h3>Доходность</h3>
|
||||
<p class="metric-value">{{ yieldRate }}%</p>
|
||||
<p class="metric-change positive">+{{ yieldChange }}% (30д)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Графики -->
|
||||
<div class="charts-section">
|
||||
<h2>Графики</h2>
|
||||
<div class="charts-grid">
|
||||
<!-- График стоимости токенов -->
|
||||
<div class="chart-card">
|
||||
<h3>Стоимость токенов</h3>
|
||||
<div class="chart-placeholder">
|
||||
<div class="chart-line">
|
||||
<div class="chart-point" style="left: 10%; top: 80%"></div>
|
||||
<div class="chart-point" style="left: 25%; top: 60%"></div>
|
||||
<div class="chart-point" style="left: 40%; top: 40%"></div>
|
||||
<div class="chart-point" style="left: 55%; top: 30%"></div>
|
||||
<div class="chart-point" style="left: 70%; top: 20%"></div>
|
||||
<div class="chart-point" style="left: 85%; top: 10%"></div>
|
||||
<div class="chart-point" style="left: 100%; top: 5%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item">
|
||||
<span class="legend-color" style="background: var(--color-primary)"></span>
|
||||
Стоимость токена
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- График активности -->
|
||||
<div class="chart-card">
|
||||
<h3>Активность участников</h3>
|
||||
<div class="activity-chart">
|
||||
<div class="activity-bar" style="height: 60%">
|
||||
<span class="bar-label">Пн</span>
|
||||
</div>
|
||||
<div class="activity-bar" style="height: 80%">
|
||||
<span class="bar-label">Вт</span>
|
||||
</div>
|
||||
<div class="activity-bar" style="height: 45%">
|
||||
<span class="bar-label">Ср</span>
|
||||
</div>
|
||||
<div class="activity-bar" style="height: 90%">
|
||||
<span class="bar-label">Чт</span>
|
||||
</div>
|
||||
<div class="activity-bar" style="height: 75%">
|
||||
<span class="bar-label">Пт</span>
|
||||
</div>
|
||||
<div class="activity-bar" style="height: 55%">
|
||||
<span class="bar-label">Сб</span>
|
||||
</div>
|
||||
<div class="activity-bar" style="height: 40%">
|
||||
<span class="bar-label">Вс</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item">
|
||||
<span class="legend-color" style="background: var(--color-secondary)"></span>
|
||||
Количество операций
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Статистика -->
|
||||
<div class="statistics-section">
|
||||
<h2>Статистика</h2>
|
||||
<div class="stats-grid">
|
||||
<div class="stats-card">
|
||||
<h3>Распределение токенов</h3>
|
||||
<div class="distribution-chart">
|
||||
<div class="pie-segment" style="--percentage: 40; --color: #007bff">
|
||||
<span class="segment-label">Крупные держатели</span>
|
||||
<span class="segment-value">40%</span>
|
||||
</div>
|
||||
<div class="pie-segment" style="--percentage: 30; --color: #28a745">
|
||||
<span class="segment-label">Средние держатели</span>
|
||||
<span class="segment-value">30%</span>
|
||||
</div>
|
||||
<div class="pie-segment" style="--percentage: 20; --color: #ffc107">
|
||||
<span class="segment-label">Малые держатели</span>
|
||||
<span class="segment-value">20%</span>
|
||||
</div>
|
||||
<div class="pie-segment" style="--percentage: 10; --color: #dc3545">
|
||||
<span class="segment-label">Резерв</span>
|
||||
<span class="segment-value">10%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-card">
|
||||
<h3>Топ участников</h3>
|
||||
<div class="top-participants">
|
||||
<div
|
||||
v-for="participant in topParticipants"
|
||||
:key="participant.address"
|
||||
class="participant-item"
|
||||
>
|
||||
<div class="participant-info">
|
||||
<span class="participant-rank">#{{ participant.rank }}</span>
|
||||
<span class="participant-address">{{ formatAddress(participant.address) }}</span>
|
||||
</div>
|
||||
<div class="participant-stats">
|
||||
<span class="participant-balance">{{ participant.balance }} токенов</span>
|
||||
<span class="participant-percentage">{{ participant.percentage }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Отчеты -->
|
||||
<div class="reports-section">
|
||||
<h2>Отчеты</h2>
|
||||
<div class="reports-grid">
|
||||
<div class="report-card">
|
||||
<h3>Ежемесячный отчет</h3>
|
||||
<p>Подробный анализ деятельности DLE за последний месяц</p>
|
||||
<div class="report-actions">
|
||||
<button class="btn-secondary">Просмотреть</button>
|
||||
<button class="btn-secondary">Скачать PDF</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="report-card">
|
||||
<h3>Финансовый отчет</h3>
|
||||
<p>Анализ финансового состояния и доходности</p>
|
||||
<div class="report-actions">
|
||||
<button class="btn-secondary">Просмотреть</button>
|
||||
<button class="btn-secondary">Скачать PDF</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="report-card">
|
||||
<h3>Отчет по предложениям</h3>
|
||||
<p>Статистика и анализ предложений за период</p>
|
||||
<div class="report-actions">
|
||||
<button class="btn-secondary">Просмотреть</button>
|
||||
<button class="btn-secondary">Скачать PDF</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="report-card">
|
||||
<h3>Отчет по активности</h3>
|
||||
<p>Анализ активности участников и операций</p>
|
||||
<div class="report-actions">
|
||||
<button class="btn-secondary">Просмотреть</button>
|
||||
<button class="btn-secondary">Скачать PDF</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps, defineEmits } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import BaseLayout from '../../components/BaseLayout.vue';
|
||||
|
||||
// Определяем props
|
||||
const props = defineProps({
|
||||
isAuthenticated: Boolean,
|
||||
identities: Array,
|
||||
tokenBalances: Object,
|
||||
isLoadingTokens: Boolean
|
||||
});
|
||||
|
||||
// Определяем emits
|
||||
const emit = defineEmits(['auth-action-completed']);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// Ключевые метрики
|
||||
const totalValue = ref(2500000);
|
||||
const valueChange = ref(12.5);
|
||||
const activeParticipants = ref(156);
|
||||
const participantsChange = ref(23);
|
||||
const totalProposals = ref(45);
|
||||
const proposalsChange = ref(8);
|
||||
const yieldRate = ref(8.7);
|
||||
const yieldChange = ref(1.2);
|
||||
|
||||
// Топ участников (временные данные)
|
||||
const topParticipants = ref([
|
||||
{
|
||||
rank: 1,
|
||||
address: '0x1234567890123456789012345678901234567890',
|
||||
balance: 2500,
|
||||
percentage: 25.0
|
||||
},
|
||||
{
|
||||
rank: 2,
|
||||
address: '0x2345678901234567890123456789012345678901',
|
||||
balance: 1800,
|
||||
percentage: 18.0
|
||||
},
|
||||
{
|
||||
rank: 3,
|
||||
address: '0x3456789012345678901234567890123456789012',
|
||||
balance: 1200,
|
||||
percentage: 12.0
|
||||
},
|
||||
{
|
||||
rank: 4,
|
||||
address: '0x4567890123456789012345678901234567890123',
|
||||
balance: 800,
|
||||
percentage: 8.0
|
||||
},
|
||||
{
|
||||
rank: 5,
|
||||
address: '0x5678901234567890123456789012345678901234',
|
||||
balance: 600,
|
||||
percentage: 6.0
|
||||
}
|
||||
]);
|
||||
|
||||
// Методы
|
||||
const formatAddress = (address) => {
|
||||
if (!address) return '';
|
||||
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.analytics-container {
|
||||
padding: 20px;
|
||||
background-color: var(--color-white);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 40px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
color: var(--color-primary);
|
||||
font-size: 2.5rem;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: var(--color-grey-dark);
|
||||
font-size: 1.1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Секции */
|
||||
.metrics-section,
|
||||
.charts-section,
|
||||
.statistics-section,
|
||||
.reports-section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.metrics-section h2,
|
||||
.charts-section h2,
|
||||
.statistics-section h2,
|
||||
.reports-section h2 {
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
/* Ключевые метрики */
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background: #f8f9fa;
|
||||
padding: 25px;
|
||||
border-radius: var(--radius-lg);
|
||||
border-left: 4px solid var(--color-primary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metric-card h3 {
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 15px;
|
||||
font-size: 1rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 10px 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.metric-change {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.metric-change.positive {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.metric-change.negative {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
/* Графики */
|
||||
.charts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.chart-card h3 {
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
/* График стоимости токенов */
|
||||
.chart-placeholder {
|
||||
height: 200px;
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-sm);
|
||||
position: relative;
|
||||
margin-bottom: 15px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.chart-line {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chart-point {
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--color-primary);
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.chart-point::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: rgba(0, 123, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
/* График активности */
|
||||
.activity-chart {
|
||||
height: 200px;
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.activity-bar {
|
||||
background: var(--color-secondary);
|
||||
border-radius: 4px 4px 0 0;
|
||||
min-width: 30px;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.activity-bar:hover {
|
||||
background: var(--color-secondary-dark);
|
||||
}
|
||||
|
||||
.bar-label {
|
||||
position: absolute;
|
||||
bottom: -25px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-grey-dark);
|
||||
}
|
||||
|
||||
/* Легенда графиков */
|
||||
.chart-legend {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-grey-dark);
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Статистика */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.stats-card h3 {
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
/* Круговая диаграмма */
|
||||
.distribution-chart {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.pie-segment {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-sm);
|
||||
border-left: 4px solid var(--color);
|
||||
}
|
||||
|
||||
.segment-label {
|
||||
font-weight: 600;
|
||||
color: var(--color-grey-dark);
|
||||
}
|
||||
|
||||
.segment-value {
|
||||
font-weight: 700;
|
||||
color: var(--color);
|
||||
}
|
||||
|
||||
/* Топ участников */
|
||||
.top-participants {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.participant-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.participant-item:hover {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.participant-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.participant-rank {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.participant-address {
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-grey-dark);
|
||||
}
|
||||
|
||||
.participant-stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.participant-balance {
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.participant-percentage {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-grey-dark);
|
||||
}
|
||||
|
||||
/* Отчеты */
|
||||
.reports-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.report-card {
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.report-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.report-card h3 {
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.report-card p {
|
||||
color: var(--color-grey-dark);
|
||||
margin-bottom: 20px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.report-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Кнопки */
|
||||
.btn-secondary {
|
||||
background: var(--color-secondary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--color-secondary-dark);
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.charts-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.reports-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.report-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.participant-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.participant-stats {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
499
frontend/src/views/smartcontracts/DleManagementView.vue
Normal file
499
frontend/src/views/smartcontracts/DleManagementView.vue
Normal file
@@ -0,0 +1,499 @@
|
||||
<!--
|
||||
Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
All rights reserved.
|
||||
|
||||
This software is proprietary and confidential.
|
||||
Unauthorized copying, modification, or distribution is prohibited.
|
||||
|
||||
For licensing inquiries: info@hb3-accelerator.com
|
||||
Website: https://hb3-accelerator.com
|
||||
GitHub: https://github.com/HB3-ACCELERATOR
|
||||
-->
|
||||
|
||||
<template>
|
||||
<BaseLayout
|
||||
:is-authenticated="isAuthenticated"
|
||||
:identities="identities"
|
||||
:token-balances="tokenBalances"
|
||||
:is-loading-tokens="isLoadingTokens"
|
||||
@auth-action-completed="$emit('auth-action-completed')"
|
||||
>
|
||||
<div class="dle-management-container">
|
||||
<!-- Заголовок -->
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<h1>Управление DLE</h1>
|
||||
<p>Интеграция с другими DLE и участие в кворумах</p>
|
||||
</div>
|
||||
<button class="close-btn" @click="router.push('/management')">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Карточки DLE -->
|
||||
<div class="dle-cards">
|
||||
<div
|
||||
v-for="dle in dleList"
|
||||
:key="dle.address"
|
||||
class="dle-card"
|
||||
@click="openDleInterface(dle)"
|
||||
>
|
||||
<div class="dle-card-header">
|
||||
<h3>{{ dle.name }}</h3>
|
||||
<button
|
||||
@click.stop="removeDle(dle.address)"
|
||||
class="remove-btn"
|
||||
title="Удалить DLE"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
<div class="dle-card-content">
|
||||
<p class="dle-address">Адрес: {{ formatAddress(dle.address) }}</p>
|
||||
<p class="dle-location">Местонахождение: {{ dle.location }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Кнопка добавления нового DLE -->
|
||||
<div class="dle-card add-dle-card" @click="showAddDleForm = true">
|
||||
<div class="add-dle-content">
|
||||
<div class="add-icon">+</div>
|
||||
<h3>Добавить DLE</h3>
|
||||
<p>Подключить новый DLE для управления</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно добавления DLE -->
|
||||
<div v-if="showAddDleForm" class="modal-overlay" @click="showAddDleForm = false">
|
||||
<div class="modal-content" @click.stop>
|
||||
<div class="modal-header">
|
||||
<h3>Добавить новый DLE</h3>
|
||||
<button @click="showAddDleForm = false" class="close-btn">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form @submit.prevent="addNewDle" class="add-dle-form">
|
||||
<div class="form-group">
|
||||
<label for="dleName">Название DLE:</label>
|
||||
<input
|
||||
id="dleName"
|
||||
v-model="newDle.name"
|
||||
type="text"
|
||||
placeholder="Введите название DLE"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="dleAddress">Адрес контракта:</label>
|
||||
<input
|
||||
id="dleAddress"
|
||||
v-model="newDle.address"
|
||||
type="text"
|
||||
placeholder="0x..."
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="dleLocation">Местонахождение:</label>
|
||||
<input
|
||||
id="dleLocation"
|
||||
v-model="newDle.location"
|
||||
type="text"
|
||||
placeholder="Страна, город"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" @click="showAddDleForm = false" class="btn-secondary">
|
||||
Отмена
|
||||
</button>
|
||||
<button type="submit" class="btn-primary">
|
||||
Добавить DLE
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps, defineEmits } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import BaseLayout from '../../components/BaseLayout.vue';
|
||||
|
||||
// Определяем props
|
||||
const props = defineProps({
|
||||
isAuthenticated: Boolean,
|
||||
identities: Array,
|
||||
tokenBalances: Object,
|
||||
isLoadingTokens: Boolean
|
||||
});
|
||||
|
||||
// Определяем emits
|
||||
const emit = defineEmits(['auth-action-completed']);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// Состояние
|
||||
const showAddDleForm = ref(false);
|
||||
const newDle = ref({
|
||||
name: '',
|
||||
address: '',
|
||||
location: ''
|
||||
});
|
||||
|
||||
// Список DLE (временные данные для демонстрации)
|
||||
const dleList = ref([
|
||||
{
|
||||
name: 'test2 (test2)',
|
||||
address: '0xef49...dfD8',
|
||||
location: '245000, 中国, 黄山市'
|
||||
},
|
||||
{
|
||||
name: 'My DLE',
|
||||
address: '0x1234...5678',
|
||||
location: '101000, Россия, Москва'
|
||||
}
|
||||
]);
|
||||
|
||||
// Методы
|
||||
const formatAddress = (address) => {
|
||||
if (!address) return '';
|
||||
if (address.length <= 10) return address;
|
||||
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
|
||||
};
|
||||
|
||||
const addNewDle = () => {
|
||||
if (!newDle.value.name || !newDle.value.address || !newDle.value.location) {
|
||||
return;
|
||||
}
|
||||
|
||||
dleList.value.push({
|
||||
name: newDle.value.name,
|
||||
address: newDle.value.address,
|
||||
location: newDle.value.location
|
||||
});
|
||||
|
||||
// Сброс формы
|
||||
newDle.value = {
|
||||
name: '',
|
||||
address: '',
|
||||
location: ''
|
||||
};
|
||||
|
||||
showAddDleForm.value = false;
|
||||
};
|
||||
|
||||
const removeDle = (address) => {
|
||||
dleList.value = dleList.value.filter(dle => dle.address !== address);
|
||||
};
|
||||
|
||||
const openDleInterface = (dle) => {
|
||||
// Здесь будет логика открытия интерфейса DLE
|
||||
// Вариант 1: Новая вкладка с внешним сайтом
|
||||
// window.open(`https://example.com/dle/${dle.address}`, '_blank');
|
||||
|
||||
// Вариант 2: Встроенный интерфейс в текущей вкладке
|
||||
router.push(`/management/dle/${dle.address}`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dle-management-container {
|
||||
padding: 20px;
|
||||
background-color: var(--color-white);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 40px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
color: var(--color-primary);
|
||||
font-size: 2.5rem;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: var(--color-grey-dark);
|
||||
font-size: 1.1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Карточки DLE */
|
||||
.dle-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 2rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.dle-card {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
padding: 1.5rem;
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
min-height: 150px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dle-card:hover {
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.dle-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.dle-card-header h3 {
|
||||
color: var(--color-primary);
|
||||
margin: 0;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.remove-btn:hover {
|
||||
background: #ffebee;
|
||||
}
|
||||
|
||||
.dle-card-content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.dle-address {
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-grey-dark);
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.dle-location {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-grey-dark);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Карточка добавления */
|
||||
.add-dle-card {
|
||||
border: 2px dashed #dee2e6;
|
||||
background: #f8f9fa;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.add-dle-card:hover {
|
||||
border-color: var(--color-primary);
|
||||
background: #f0f8ff;
|
||||
}
|
||||
|
||||
.add-dle-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
font-size: 2rem;
|
||||
color: var(--color-primary);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.add-dle-content h3 {
|
||||
color: var(--color-primary);
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.add-dle-content p {
|
||||
color: var(--color-grey-dark);
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Модальное окно */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: var(--color-grey-dark);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* Форма */
|
||||
.add-dle-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
color: var(--color-grey-dark);
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--color-secondary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--color-secondary-dark);
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.dle-cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dle-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1058
frontend/src/views/smartcontracts/HistoryView.vue
Normal file
1058
frontend/src/views/smartcontracts/HistoryView.vue
Normal file
File diff suppressed because it is too large
Load Diff
766
frontend/src/views/smartcontracts/ModulesView.vue
Normal file
766
frontend/src/views/smartcontracts/ModulesView.vue
Normal file
@@ -0,0 +1,766 @@
|
||||
<!--
|
||||
Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
All rights reserved.
|
||||
|
||||
This software is proprietary and confidential.
|
||||
Unauthorized copying, modification, or distribution is prohibited.
|
||||
|
||||
For licensing inquiries: info@hb3-accelerator.com
|
||||
Website: https://hb3-accelerator.com
|
||||
GitHub: https://github.com/HB3-ACCELERATOR
|
||||
-->
|
||||
|
||||
<template>
|
||||
<BaseLayout
|
||||
:is-authenticated="isAuthenticated"
|
||||
:identities="identities"
|
||||
:token-balances="tokenBalances"
|
||||
:is-loading-tokens="isLoadingTokens"
|
||||
@auth-action-completed="$emit('auth-action-completed')"
|
||||
>
|
||||
<div class="modules-container">
|
||||
<!-- Заголовок -->
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<h1>Модули DLE</h1>
|
||||
<p>Установка, настройка и управление модулями</p>
|
||||
</div>
|
||||
<button class="close-btn" @click="router.push('/management')">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Доступные модули -->
|
||||
<div class="available-modules-section">
|
||||
<h2>Доступные модули</h2>
|
||||
<div class="modules-grid">
|
||||
<div
|
||||
v-for="module in availableModules"
|
||||
:key="module.id"
|
||||
class="module-card"
|
||||
:class="{ 'module-installed': module.installed }"
|
||||
>
|
||||
<div class="module-header">
|
||||
<h3>{{ module.name }}</h3>
|
||||
<span class="module-version">v{{ module.version }}</span>
|
||||
</div>
|
||||
<p class="module-description">{{ module.description }}</p>
|
||||
<div class="module-features">
|
||||
<span
|
||||
v-for="feature in module.features"
|
||||
:key="feature"
|
||||
class="feature-tag"
|
||||
>
|
||||
{{ feature }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="module-actions">
|
||||
<button
|
||||
v-if="!module.installed"
|
||||
@click="installModule(module.id)"
|
||||
class="btn-primary"
|
||||
:disabled="isInstalling"
|
||||
>
|
||||
{{ isInstalling ? 'Установка...' : 'Установить' }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
@click="openModuleInterface(module)"
|
||||
class="btn-secondary"
|
||||
>
|
||||
Управление
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Установленные модули -->
|
||||
<div class="installed-modules-section">
|
||||
<h2>Установленные модули</h2>
|
||||
<div v-if="installedModules.length === 0" class="empty-state">
|
||||
<p>Нет установленных модулей</p>
|
||||
</div>
|
||||
<div v-else class="installed-modules-list">
|
||||
<div
|
||||
v-for="module in installedModules"
|
||||
:key="module.address"
|
||||
class="installed-module-card"
|
||||
>
|
||||
<div class="module-info">
|
||||
<div class="module-header">
|
||||
<h3>{{ module.name }}</h3>
|
||||
<span class="module-status" :class="module.status">
|
||||
{{ getStatusText(module.status) }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="module-description">{{ module.description }}</p>
|
||||
<p class="module-address">Адрес: {{ formatAddress(module.address) }}</p>
|
||||
<p class="module-version">Версия: {{ module.version }}</p>
|
||||
</div>
|
||||
<div class="module-actions">
|
||||
<button @click="openModuleInterface(module)" class="btn-secondary">
|
||||
Управление
|
||||
</button>
|
||||
<button @click="configureModule(module)" class="btn-secondary">
|
||||
Настройки
|
||||
</button>
|
||||
<button @click="uninstallModule(module.address)" class="btn-danger">
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно настройки модуля -->
|
||||
<div v-if="showConfigModal" class="modal-overlay" @click="showConfigModal = false">
|
||||
<div class="modal-content" @click.stop>
|
||||
<div class="modal-header">
|
||||
<h3>Настройки модуля {{ selectedModule?.name }}</h3>
|
||||
<button @click="showConfigModal = false" class="close-btn">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form @submit.prevent="saveModuleConfig" class="config-form">
|
||||
<div
|
||||
v-for="setting in selectedModule?.configSettings"
|
||||
:key="setting.key"
|
||||
class="form-group"
|
||||
>
|
||||
<label :for="setting.key">{{ setting.label }}:</label>
|
||||
<input
|
||||
v-if="setting.type === 'text'"
|
||||
:id="setting.key"
|
||||
v-model="moduleConfig[setting.key]"
|
||||
type="text"
|
||||
:placeholder="setting.placeholder"
|
||||
required
|
||||
>
|
||||
<input
|
||||
v-else-if="setting.type === 'number'"
|
||||
:id="setting.key"
|
||||
v-model="moduleConfig[setting.key]"
|
||||
type="number"
|
||||
:min="setting.min"
|
||||
:max="setting.max"
|
||||
:placeholder="setting.placeholder"
|
||||
required
|
||||
>
|
||||
<select
|
||||
v-else-if="setting.type === 'select'"
|
||||
:id="setting.key"
|
||||
v-model="moduleConfig[setting.key]"
|
||||
required
|
||||
>
|
||||
<option value="">Выберите значение</option>
|
||||
<option
|
||||
v-for="option in setting.options"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
<span class="setting-hint">{{ setting.hint }}</span>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" @click="showConfigModal = false" class="btn-secondary">
|
||||
Отмена
|
||||
</button>
|
||||
<button type="submit" class="btn-primary" :disabled="isSavingConfig">
|
||||
{{ isSavingConfig ? 'Сохранение...' : 'Сохранить' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps, defineEmits } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import BaseLayout from '../../components/BaseLayout.vue';
|
||||
|
||||
// Определяем props
|
||||
const props = defineProps({
|
||||
isAuthenticated: Boolean,
|
||||
identities: Array,
|
||||
tokenBalances: Object,
|
||||
isLoadingTokens: Boolean
|
||||
});
|
||||
|
||||
// Определяем emits
|
||||
const emit = defineEmits(['auth-action-completed']);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// Состояние
|
||||
const isInstalling = ref(false);
|
||||
const isSavingConfig = ref(false);
|
||||
const showConfigModal = ref(false);
|
||||
const selectedModule = ref(null);
|
||||
const moduleConfig = ref({});
|
||||
|
||||
// Доступные модули (временные данные)
|
||||
const availableModules = ref([
|
||||
{
|
||||
id: 'treasury',
|
||||
name: 'Казначейство',
|
||||
version: '1.0.0',
|
||||
description: 'Управление средствами и активами DLE',
|
||||
features: ['Мультивалютность', 'Автоматизация', 'Отчетность'],
|
||||
installed: true,
|
||||
configSettings: [
|
||||
{
|
||||
key: 'maxWithdrawal',
|
||||
label: 'Максимальная сумма вывода',
|
||||
type: 'number',
|
||||
min: 0,
|
||||
max: 1000000,
|
||||
placeholder: '10000',
|
||||
hint: 'Максимальная сумма для однократного вывода средств'
|
||||
},
|
||||
{
|
||||
key: 'approvalRequired',
|
||||
label: 'Требуется одобрение',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: 'true', label: 'Да' },
|
||||
{ value: 'false', label: 'Нет' }
|
||||
],
|
||||
hint: 'Требуется ли одобрение для операций с казной'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'governance',
|
||||
name: 'Расширенное управление',
|
||||
version: '2.1.0',
|
||||
description: 'Дополнительные функции голосования и управления',
|
||||
features: ['Делегирование', 'Взвешенное голосование', 'Автоматизация'],
|
||||
installed: false,
|
||||
configSettings: [
|
||||
{
|
||||
key: 'delegationEnabled',
|
||||
label: 'Включить делегирование',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: 'true', label: 'Да' },
|
||||
{ value: 'false', label: 'Нет' }
|
||||
],
|
||||
hint: 'Разрешить делегирование голосов'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'compliance',
|
||||
name: 'Соответствие требованиям',
|
||||
version: '1.2.0',
|
||||
description: 'Модуль для обеспечения соответствия нормативным требованиям',
|
||||
features: ['KYC/AML', 'Отчетность', 'Аудит'],
|
||||
installed: false,
|
||||
configSettings: [
|
||||
{
|
||||
key: 'kycRequired',
|
||||
label: 'Требуется KYC',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: 'true', label: 'Да' },
|
||||
{ value: 'false', label: 'Нет' }
|
||||
],
|
||||
hint: 'Требуется ли прохождение KYC для участия'
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
// Установленные модули (временные данные)
|
||||
const installedModules = ref([
|
||||
{
|
||||
name: 'Казначейство',
|
||||
description: 'Управление средствами и активами DLE',
|
||||
address: '0x1234567890123456789012345678901234567890',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
configSettings: availableModules.value[0].configSettings
|
||||
}
|
||||
]);
|
||||
|
||||
// Методы
|
||||
const installModule = async (moduleId) => {
|
||||
if (isInstalling.value) return;
|
||||
|
||||
try {
|
||||
isInstalling.value = true;
|
||||
|
||||
// Здесь будет логика установки модуля
|
||||
console.log('Установка модуля:', moduleId);
|
||||
|
||||
// Временная логика
|
||||
const module = availableModules.value.find(m => m.id === moduleId);
|
||||
if (module) {
|
||||
module.installed = true;
|
||||
|
||||
// Добавляем в список установленных
|
||||
installedModules.value.push({
|
||||
name: module.name,
|
||||
description: module.description,
|
||||
address: '0x' + Math.random().toString(16).substr(2, 40),
|
||||
version: module.version,
|
||||
status: 'active',
|
||||
configSettings: module.configSettings
|
||||
});
|
||||
}
|
||||
|
||||
alert('Модуль успешно установлен!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка установки модуля:', error);
|
||||
alert('Ошибка при установке модуля');
|
||||
} finally {
|
||||
isInstalling.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const uninstallModule = async (moduleAddress) => {
|
||||
if (!confirm('Вы уверены, что хотите удалить этот модуль?')) return;
|
||||
|
||||
try {
|
||||
// Здесь будет логика удаления модуля
|
||||
console.log('Удаление модуля:', moduleAddress);
|
||||
|
||||
// Временная логика
|
||||
installedModules.value = installedModules.value.filter(m => m.address !== moduleAddress);
|
||||
|
||||
// Обновляем статус в доступных модулях
|
||||
const module = availableModules.value.find(m => m.name === 'Казначейство');
|
||||
if (module) {
|
||||
module.installed = false;
|
||||
}
|
||||
|
||||
alert('Модуль успешно удален!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления модуля:', error);
|
||||
alert('Ошибка при удалении модуля');
|
||||
}
|
||||
};
|
||||
|
||||
const configureModule = (module) => {
|
||||
selectedModule.value = module;
|
||||
moduleConfig.value = {};
|
||||
showConfigModal.value = true;
|
||||
};
|
||||
|
||||
const saveModuleConfig = async () => {
|
||||
if (isSavingConfig.value) return;
|
||||
|
||||
try {
|
||||
isSavingConfig.value = true;
|
||||
|
||||
// Здесь будет логика сохранения конфигурации
|
||||
console.log('Сохранение конфигурации:', moduleConfig.value);
|
||||
|
||||
alert('Конфигурация успешно сохранена!');
|
||||
showConfigModal.value = false;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка сохранения конфигурации:', error);
|
||||
alert('Ошибка при сохранении конфигурации');
|
||||
} finally {
|
||||
isSavingConfig.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const openModuleInterface = (module) => {
|
||||
// Здесь будет логика открытия интерфейса модуля
|
||||
console.log('Открытие интерфейса модуля:', module);
|
||||
alert(`Открытие интерфейса модуля ${module.name}`);
|
||||
};
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
'active': 'Активен',
|
||||
'inactive': 'Неактивен',
|
||||
'error': 'Ошибка',
|
||||
'updating': 'Обновляется'
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
};
|
||||
|
||||
const formatAddress = (address) => {
|
||||
if (!address) return '';
|
||||
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modules-container {
|
||||
padding: 20px;
|
||||
background-color: var(--color-white);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 40px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
color: var(--color-primary);
|
||||
font-size: 2.5rem;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: var(--color-grey-dark);
|
||||
font-size: 1.1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Секции */
|
||||
.available-modules-section,
|
||||
.installed-modules-section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.available-modules-section h2,
|
||||
.installed-modules-section h2 {
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
/* Сетка модулей */
|
||||
.modules-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.module-card {
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.module-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.module-card.module-installed {
|
||||
border-left: 4px solid #28a745;
|
||||
background: #f8fff9;
|
||||
}
|
||||
|
||||
.module-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.module-header h3 {
|
||||
margin: 0;
|
||||
color: var(--color-primary);
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.module-version {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.module-description {
|
||||
color: var(--color-grey-dark);
|
||||
margin: 0 0 15px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.module-features {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.feature-tag {
|
||||
background: #e9ecef;
|
||||
color: var(--color-grey-dark);
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.module-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Установленные модули */
|
||||
.installed-modules-list {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.installed-module-card {
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.installed-module-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.module-info {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.module-status {
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.module-status.active {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.module-status.inactive {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.module-status.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.module-status.updating {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.module-address {
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-grey-dark);
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.module-version {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-grey-dark);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Модальное окно */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Форма конфигурации */
|
||||
.config-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
color: var(--color-grey-dark);
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.setting-hint {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-grey-dark);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Кнопки */
|
||||
.btn-primary {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--color-secondary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--color-secondary-dark);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
/* Состояния */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px;
|
||||
color: var(--color-grey-dark);
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 2px dashed #dee2e6;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.modules-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.module-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
611
frontend/src/views/smartcontracts/ProposalsView.vue
Normal file
611
frontend/src/views/smartcontracts/ProposalsView.vue
Normal file
@@ -0,0 +1,611 @@
|
||||
<!--
|
||||
Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
All rights reserved.
|
||||
|
||||
This software is proprietary and confidential.
|
||||
Unauthorized copying, modification, or distribution is prohibited.
|
||||
|
||||
For licensing inquiries: info@hb3-accelerator.com
|
||||
Website: https://hb3-accelerator.com
|
||||
GitHub: https://github.com/HB3-ACCELERATOR
|
||||
-->
|
||||
|
||||
<template>
|
||||
<BaseLayout
|
||||
:is-authenticated="isAuthenticated"
|
||||
:identities="identities"
|
||||
:token-balances="tokenBalances"
|
||||
:is-loading-tokens="isLoadingTokens"
|
||||
@auth-action-completed="$emit('auth-action-completed')"
|
||||
>
|
||||
<div class="proposals-container">
|
||||
<!-- Заголовок -->
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<h1>Предложения</h1>
|
||||
<p>Создание, подписание и выполнение предложений</p>
|
||||
</div>
|
||||
<button class="close-btn" @click="router.push('/management')">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Создание нового предложения -->
|
||||
<div class="create-proposal-section">
|
||||
<h2>Создать новое предложение</h2>
|
||||
<form @submit.prevent="createProposal" class="proposal-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="operationType">Тип операции:</label>
|
||||
<select id="operationType" v-model="newProposal.operationType" required>
|
||||
<option value="">Выберите тип операции</option>
|
||||
<option value="token_transfer">Перевод токенов</option>
|
||||
<option value="treasury_operation">Казначейская операция</option>
|
||||
<option value="module_install">Установка модуля</option>
|
||||
<option value="parameter_change">Изменение параметров</option>
|
||||
<option value="emergency_action">Экстренные действия</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="timelockDelay">Задержка таймлока (часы):</label>
|
||||
<input
|
||||
id="timelockDelay"
|
||||
type="number"
|
||||
v-model="newProposal.timelockDelay"
|
||||
min="1"
|
||||
max="168"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Описание операции:</label>
|
||||
<textarea
|
||||
id="description"
|
||||
v-model="newProposal.description"
|
||||
placeholder="Опишите детали операции..."
|
||||
required
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Целевые сети:</label>
|
||||
<div class="networks-grid">
|
||||
<label v-for="network in availableNetworks" :key="network.id" class="network-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="network.id"
|
||||
v-model="newProposal.targetChains"
|
||||
>
|
||||
{{ network.name }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary" :disabled="isCreatingProposal">
|
||||
{{ isCreatingProposal ? 'Создание...' : 'Создать предложение' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Активные предложения -->
|
||||
<div class="proposals-section">
|
||||
<h2>Активные предложения</h2>
|
||||
<div v-if="proposals.length === 0" class="empty-state">
|
||||
<p>Нет активных предложений</p>
|
||||
</div>
|
||||
<div v-else class="proposals-list">
|
||||
<div
|
||||
v-for="proposal in proposals"
|
||||
:key="proposal.id"
|
||||
class="proposal-card"
|
||||
:class="{ 'proposal-executed': proposal.executed }"
|
||||
>
|
||||
<div class="proposal-header">
|
||||
<h3>Предложение #{{ proposal.id }}</h3>
|
||||
<span class="proposal-status" :class="getStatusClass(proposal)">
|
||||
{{ getStatusText(proposal) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="proposal-details">
|
||||
<p><strong>Описание:</strong> {{ proposal.description }}</p>
|
||||
<p><strong>Инициатор:</strong> {{ formatAddress(proposal.initiator) }}</p>
|
||||
<p><strong>Таймлок:</strong> {{ formatTimestamp(proposal.timelock) }}</p>
|
||||
<p><strong>Подписи:</strong> {{ proposal.signaturesCount }} / {{ proposal.quorumRequired }}</p>
|
||||
</div>
|
||||
|
||||
<div class="proposal-actions">
|
||||
<button
|
||||
v-if="!proposal.hasSigned && !proposal.executed"
|
||||
@click="signProposal(proposal.id)"
|
||||
class="btn-secondary"
|
||||
:disabled="isSigning"
|
||||
>
|
||||
Подписать
|
||||
</button>
|
||||
<button
|
||||
v-if="canExecuteProposal(proposal)"
|
||||
@click="executeProposal(proposal.id)"
|
||||
class="btn-success"
|
||||
:disabled="isExecuting"
|
||||
>
|
||||
Выполнить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps, defineEmits } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import BaseLayout from '../../components/BaseLayout.vue';
|
||||
|
||||
// Определяем props
|
||||
const props = defineProps({
|
||||
isAuthenticated: Boolean,
|
||||
identities: Array,
|
||||
tokenBalances: Object,
|
||||
isLoadingTokens: Boolean
|
||||
});
|
||||
|
||||
// Определяем emits
|
||||
const emit = defineEmits(['auth-action-completed']);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// Состояние
|
||||
const isCreatingProposal = ref(false);
|
||||
const isSigning = ref(false);
|
||||
const isExecuting = ref(false);
|
||||
|
||||
const newProposal = ref({
|
||||
operationType: '',
|
||||
description: '',
|
||||
targetChains: [],
|
||||
timelockDelay: 24
|
||||
});
|
||||
|
||||
// Доступные сети
|
||||
const availableNetworks = ref([
|
||||
{ id: 1, name: 'Ethereum Mainnet' },
|
||||
{ id: 137, name: 'Polygon' },
|
||||
{ id: 56, name: 'BSC' },
|
||||
{ id: 42161, name: 'Arbitrum' }
|
||||
]);
|
||||
|
||||
// Предложения (временные данные)
|
||||
const proposals = ref([
|
||||
{
|
||||
id: 1,
|
||||
description: 'Перевод 100 токенов партнеру',
|
||||
initiator: '0x1234567890123456789012345678901234567890',
|
||||
timelock: Math.floor(Date.now() / 1000) + 3600,
|
||||
signaturesCount: 5000,
|
||||
quorumRequired: 5100,
|
||||
executed: false,
|
||||
hasSigned: false
|
||||
}
|
||||
]);
|
||||
|
||||
// Методы
|
||||
const createProposal = async () => {
|
||||
if (isCreatingProposal.value) return;
|
||||
|
||||
try {
|
||||
isCreatingProposal.value = true;
|
||||
|
||||
// Здесь будет создание предложения в смарт-контракте
|
||||
console.log('Создание предложения:', newProposal.value);
|
||||
|
||||
// Временная логика
|
||||
const proposal = {
|
||||
id: proposals.value.length + 1,
|
||||
description: newProposal.value.description,
|
||||
initiator: '0x' + Math.random().toString(16).substr(2, 40),
|
||||
timelock: Math.floor(Date.now() / 1000) + (newProposal.value.timelockDelay * 3600),
|
||||
signaturesCount: 0,
|
||||
quorumRequired: 5100,
|
||||
executed: false,
|
||||
hasSigned: false
|
||||
};
|
||||
|
||||
proposals.value.push(proposal);
|
||||
|
||||
// Сброс формы
|
||||
newProposal.value = {
|
||||
operationType: '',
|
||||
description: '',
|
||||
targetChains: [],
|
||||
timelockDelay: 24
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка создания предложения:', error);
|
||||
} finally {
|
||||
isCreatingProposal.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const signProposal = async (proposalId) => {
|
||||
if (isSigning.value) return;
|
||||
|
||||
try {
|
||||
isSigning.value = true;
|
||||
|
||||
// Здесь будет подписание предложения в смарт-контракте
|
||||
console.log('Подписание предложения:', proposalId);
|
||||
|
||||
const proposal = proposals.value.find(p => p.id === proposalId);
|
||||
if (proposal) {
|
||||
proposal.signaturesCount += 1000;
|
||||
proposal.hasSigned = true;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка подписания предложения:', error);
|
||||
} finally {
|
||||
isSigning.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const executeProposal = async (proposalId) => {
|
||||
if (isExecuting.value) return;
|
||||
|
||||
try {
|
||||
isExecuting.value = true;
|
||||
|
||||
// Здесь будет выполнение предложения в смарт-контракте
|
||||
console.log('Выполнение предложения:', proposalId);
|
||||
|
||||
const proposal = proposals.value.find(p => p.id === proposalId);
|
||||
if (proposal) {
|
||||
proposal.executed = true;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка выполнения предложения:', error);
|
||||
} finally {
|
||||
isExecuting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const canExecuteProposal = (proposal) => {
|
||||
return !proposal.executed &&
|
||||
proposal.signaturesCount >= proposal.quorumRequired &&
|
||||
Date.now() >= proposal.timelock * 1000;
|
||||
};
|
||||
|
||||
const formatAddress = (address) => {
|
||||
if (!address) return '';
|
||||
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
|
||||
};
|
||||
|
||||
const formatTimestamp = (timestamp) => {
|
||||
return new Date(timestamp * 1000).toLocaleString('ru-RU');
|
||||
};
|
||||
|
||||
const getStatusClass = (proposal) => {
|
||||
if (proposal.executed) return 'status-executed';
|
||||
if (proposal.signaturesCount >= proposal.quorumRequired) return 'status-ready';
|
||||
return 'status-pending';
|
||||
};
|
||||
|
||||
const getStatusText = (proposal) => {
|
||||
if (proposal.executed) return 'Выполнено';
|
||||
if (proposal.signaturesCount >= proposal.quorumRequired) return 'Готово к выполнению';
|
||||
return 'Ожидает подписей';
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.proposals-container {
|
||||
padding: 20px;
|
||||
background-color: var(--color-white);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 40px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
color: var(--color-primary);
|
||||
font-size: 2.5rem;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: var(--color-grey-dark);
|
||||
font-size: 1.1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Секции */
|
||||
.create-proposal-section,
|
||||
.proposals-section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.create-proposal-section h2,
|
||||
.proposals-section h2 {
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
/* Форма */
|
||||
.proposal-form {
|
||||
background: #f8f9fa;
|
||||
padding: 25px;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
color: var(--color-grey-dark);
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.networks-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.network-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.network-checkbox:hover {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.network-checkbox input[type="checkbox"] {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Предложения */
|
||||
.proposals-list {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.proposal-card {
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.proposal-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.proposal-card.proposal-executed {
|
||||
opacity: 0.7;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.proposal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.proposal-header h3 {
|
||||
margin: 0;
|
||||
color: var(--color-primary);
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.proposal-status {
|
||||
padding: 6px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.status-ready {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.status-executed {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.proposal-details {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.proposal-details p {
|
||||
margin: 8px 0;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.proposal-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Кнопки */
|
||||
.btn-primary {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--color-secondary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: var(--color-secondary-dark);
|
||||
}
|
||||
|
||||
.btn-secondary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-success:hover:not(:disabled) {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.btn-success:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Состояния */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px;
|
||||
color: var(--color-grey-dark);
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 2px dashed #dee2e6;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.proposal-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.proposal-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.networks-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
600
frontend/src/views/smartcontracts/QuorumView.vue
Normal file
600
frontend/src/views/smartcontracts/QuorumView.vue
Normal file
@@ -0,0 +1,600 @@
|
||||
<!--
|
||||
Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
All rights reserved.
|
||||
|
||||
This software is proprietary and confidential.
|
||||
Unauthorized copying, modification, or distribution is prohibited.
|
||||
|
||||
For licensing inquiries: info@hb3-accelerator.com
|
||||
Website: https://hb3-accelerator.com
|
||||
GitHub: https://github.com/HB3-ACCELERATOR
|
||||
-->
|
||||
|
||||
<template>
|
||||
<BaseLayout
|
||||
:is-authenticated="isAuthenticated"
|
||||
:identities="identities"
|
||||
:token-balances="tokenBalances"
|
||||
:is-loading-tokens="isLoadingTokens"
|
||||
@auth-action-completed="$emit('auth-action-completed')"
|
||||
>
|
||||
<div class="quorum-container">
|
||||
<!-- Заголовок -->
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<h1>Кворум</h1>
|
||||
<p>Настройки голосования и кворума</p>
|
||||
</div>
|
||||
<button class="close-btn" @click="router.push('/management')">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Текущие настройки -->
|
||||
<div class="current-settings-section">
|
||||
<h2>Текущие настройки</h2>
|
||||
<div class="settings-grid">
|
||||
<div class="setting-card">
|
||||
<h3>Процент кворума</h3>
|
||||
<p class="setting-value">{{ currentQuorum }}%</p>
|
||||
<p class="setting-description">Минимальный процент токенов для принятия решения</p>
|
||||
</div>
|
||||
<div class="setting-card">
|
||||
<h3>Задержка голосования</h3>
|
||||
<p class="setting-value">{{ votingDelay }} блоков</p>
|
||||
<p class="setting-description">Время между созданием и началом голосования</p>
|
||||
</div>
|
||||
<div class="setting-card">
|
||||
<h3>Период голосования</h3>
|
||||
<p class="setting-value">{{ votingPeriod }} блоков</p>
|
||||
<p class="setting-description">Длительность периода голосования</p>
|
||||
</div>
|
||||
<div class="setting-card">
|
||||
<h3>Порог предложений</h3>
|
||||
<p class="setting-value">{{ proposalThreshold }} токенов</p>
|
||||
<p class="setting-description">Минимальное количество токенов для создания предложения</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Изменение настроек -->
|
||||
<div class="change-settings-section">
|
||||
<h2>Изменить настройки</h2>
|
||||
<form @submit.prevent="updateSettings" class="settings-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="newQuorum">Новый процент кворума:</label>
|
||||
<input
|
||||
id="newQuorum"
|
||||
v-model="newSettings.quorumPercentage"
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
placeholder="51"
|
||||
required
|
||||
>
|
||||
<span class="input-hint">% (от 1 до 100)</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="newVotingDelay">Новая задержка голосования:</label>
|
||||
<input
|
||||
id="newVotingDelay"
|
||||
v-model="newSettings.votingDelay"
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="1"
|
||||
required
|
||||
>
|
||||
<span class="input-hint">блоков</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="newVotingPeriod">Новый период голосования:</label>
|
||||
<input
|
||||
id="newVotingPeriod"
|
||||
v-model="newSettings.votingPeriod"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="45818"
|
||||
required
|
||||
>
|
||||
<span class="input-hint">блоков (~1 неделя)</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="newProposalThreshold">Новый порог предложений:</label>
|
||||
<input
|
||||
id="newProposalThreshold"
|
||||
v-model="newSettings.proposalThreshold"
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="100"
|
||||
required
|
||||
>
|
||||
<span class="input-hint">токенов</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="changeReason">Причина изменения:</label>
|
||||
<textarea
|
||||
id="changeReason"
|
||||
v-model="newSettings.reason"
|
||||
placeholder="Опишите причину изменения настроек..."
|
||||
rows="4"
|
||||
required
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary" :disabled="isUpdating">
|
||||
{{ isUpdating ? 'Обновление...' : 'Обновить настройки' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- История изменений -->
|
||||
<div class="history-section">
|
||||
<h2>История изменений</h2>
|
||||
<div v-if="settingsHistory.length === 0" class="empty-state">
|
||||
<p>Нет истории изменений настроек</p>
|
||||
</div>
|
||||
<div v-else class="history-list">
|
||||
<div
|
||||
v-for="change in settingsHistory"
|
||||
:key="change.id"
|
||||
class="history-item"
|
||||
>
|
||||
<div class="history-header">
|
||||
<h3>Изменение #{{ change.id }}</h3>
|
||||
<span class="change-date">{{ formatDate(change.timestamp) }}</span>
|
||||
</div>
|
||||
<div class="change-details">
|
||||
<p><strong>Причина:</strong> {{ change.reason }}</p>
|
||||
<div class="changes-list">
|
||||
<div v-if="change.quorumChange" class="change-item">
|
||||
<span class="change-label">Кворум:</span>
|
||||
<span class="change-value">{{ change.quorumChange.from }}% → {{ change.quorumChange.to }}%</span>
|
||||
</div>
|
||||
<div v-if="change.votingDelayChange" class="change-item">
|
||||
<span class="change-label">Задержка голосования:</span>
|
||||
<span class="change-value">{{ change.votingDelayChange.from }} → {{ change.votingDelayChange.to }} блоков</span>
|
||||
</div>
|
||||
<div v-if="change.votingPeriodChange" class="change-item">
|
||||
<span class="change-label">Период голосования:</span>
|
||||
<span class="change-value">{{ change.votingPeriodChange.from }} → {{ change.votingPeriodChange.to }} блоков</span>
|
||||
</div>
|
||||
<div v-if="change.proposalThresholdChange" class="change-item">
|
||||
<span class="change-label">Порог предложений:</span>
|
||||
<span class="change-value">{{ change.proposalThresholdChange.from }} → {{ change.proposalThresholdChange.to }} токенов</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="change-author">
|
||||
<span>Автор: {{ formatAddress(change.author) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps, defineEmits } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import BaseLayout from '../../components/BaseLayout.vue';
|
||||
|
||||
// Определяем props
|
||||
const props = defineProps({
|
||||
isAuthenticated: Boolean,
|
||||
identities: Array,
|
||||
tokenBalances: Object,
|
||||
isLoadingTokens: Boolean
|
||||
});
|
||||
|
||||
// Определяем emits
|
||||
const emit = defineEmits(['auth-action-completed']);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// Состояние
|
||||
const isUpdating = ref(false);
|
||||
|
||||
// Текущие настройки
|
||||
const currentQuorum = ref(51);
|
||||
const votingDelay = ref(1);
|
||||
const votingPeriod = ref(45818);
|
||||
const proposalThreshold = ref(100);
|
||||
|
||||
// Новые настройки
|
||||
const newSettings = ref({
|
||||
quorumPercentage: '',
|
||||
votingDelay: '',
|
||||
votingPeriod: '',
|
||||
proposalThreshold: '',
|
||||
reason: ''
|
||||
});
|
||||
|
||||
// История изменений (временные данные)
|
||||
const settingsHistory = ref([
|
||||
{
|
||||
id: 1,
|
||||
timestamp: Date.now() - 86400000, // 1 день назад
|
||||
reason: 'Оптимизация параметров голосования для повышения эффективности',
|
||||
quorumChange: { from: 60, to: 51 },
|
||||
votingDelayChange: { from: 2, to: 1 },
|
||||
author: '0x1234567890123456789012345678901234567890'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
timestamp: Date.now() - 604800000, // 1 неделя назад
|
||||
reason: 'Первоначальная настройка параметров DLE',
|
||||
quorumChange: { from: 0, to: 60 },
|
||||
votingDelayChange: { from: 0, to: 2 },
|
||||
votingPeriodChange: { from: 0, to: 45818 },
|
||||
proposalThresholdChange: { from: 0, to: 100 },
|
||||
author: '0x2345678901234567890123456789012345678901'
|
||||
}
|
||||
]);
|
||||
|
||||
// Методы
|
||||
const updateSettings = async () => {
|
||||
if (isUpdating.value) return;
|
||||
|
||||
try {
|
||||
isUpdating.value = true;
|
||||
|
||||
// Здесь будет логика обновления настроек в смарт-контракте
|
||||
console.log('Обновление настроек:', newSettings.value);
|
||||
|
||||
// Временная логика
|
||||
const change = {
|
||||
id: settingsHistory.value.length + 1,
|
||||
timestamp: Date.now(),
|
||||
reason: newSettings.value.reason,
|
||||
author: '0x' + Math.random().toString(16).substr(2, 40)
|
||||
};
|
||||
|
||||
// Добавляем изменения в историю
|
||||
if (newSettings.value.quorumPercentage && newSettings.value.quorumPercentage !== currentQuorum.value) {
|
||||
change.quorumChange = { from: currentQuorum.value, to: parseInt(newSettings.value.quorumPercentage) };
|
||||
currentQuorum.value = parseInt(newSettings.value.quorumPercentage);
|
||||
}
|
||||
|
||||
if (newSettings.value.votingDelay && newSettings.value.votingDelay !== votingDelay.value) {
|
||||
change.votingDelayChange = { from: votingDelay.value, to: parseInt(newSettings.value.votingDelay) };
|
||||
votingDelay.value = parseInt(newSettings.value.votingDelay);
|
||||
}
|
||||
|
||||
if (newSettings.value.votingPeriod && newSettings.value.votingPeriod !== votingPeriod.value) {
|
||||
change.votingPeriodChange = { from: votingPeriod.value, to: parseInt(newSettings.value.votingPeriod) };
|
||||
votingPeriod.value = parseInt(newSettings.value.votingPeriod);
|
||||
}
|
||||
|
||||
if (newSettings.value.proposalThreshold && newSettings.value.proposalThreshold !== proposalThreshold.value) {
|
||||
change.proposalThresholdChange = { from: proposalThreshold.value, to: parseInt(newSettings.value.proposalThreshold) };
|
||||
proposalThreshold.value = parseInt(newSettings.value.proposalThreshold);
|
||||
}
|
||||
|
||||
settingsHistory.value.unshift(change);
|
||||
|
||||
// Сброс формы
|
||||
newSettings.value = {
|
||||
quorumPercentage: '',
|
||||
votingDelay: '',
|
||||
votingPeriod: '',
|
||||
proposalThreshold: '',
|
||||
reason: ''
|
||||
};
|
||||
|
||||
alert('Настройки успешно обновлены!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка обновления настроек:', error);
|
||||
alert('Ошибка при обновлении настроек');
|
||||
} finally {
|
||||
isUpdating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const formatAddress = (address) => {
|
||||
if (!address) return '';
|
||||
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
|
||||
};
|
||||
|
||||
const formatDate = (timestamp) => {
|
||||
return new Date(timestamp).toLocaleString('ru-RU');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.quorum-container {
|
||||
padding: 20px;
|
||||
background-color: var(--color-white);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 40px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
color: var(--color-primary);
|
||||
font-size: 2.5rem;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: var(--color-grey-dark);
|
||||
font-size: 1.1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Секции */
|
||||
.current-settings-section,
|
||||
.change-settings-section,
|
||||
.history-section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.current-settings-section h2,
|
||||
.change-settings-section h2,
|
||||
.history-section h2 {
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
/* Текущие настройки */
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.setting-card {
|
||||
background: #f8f9fa;
|
||||
padding: 25px;
|
||||
border-radius: var(--radius-lg);
|
||||
border-left: 4px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.setting-card h3 {
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.setting-value {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 10px 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.setting-description {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-grey-dark);
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Форма настроек */
|
||||
.settings-form {
|
||||
background: #f8f9fa;
|
||||
padding: 25px;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
color: var(--color-grey-dark);
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.input-hint {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-grey-dark);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* История изменений */
|
||||
.history-list {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.history-item:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.history-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.history-header h3 {
|
||||
margin: 0;
|
||||
color: var(--color-primary);
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.change-date {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-grey-dark);
|
||||
}
|
||||
|
||||
.change-details {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.change-details p {
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.changes-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.change-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.change-label {
|
||||
font-weight: 600;
|
||||
color: var(--color-grey-dark);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.change-value {
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.change-author {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-grey-dark);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* Кнопки */
|
||||
.btn-primary {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Состояния */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px;
|
||||
color: var(--color-grey-dark);
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 2px dashed #dee2e6;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.history-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.change-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
835
frontend/src/views/smartcontracts/SettingsView.vue
Normal file
835
frontend/src/views/smartcontracts/SettingsView.vue
Normal file
@@ -0,0 +1,835 @@
|
||||
<!--
|
||||
Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
All rights reserved.
|
||||
|
||||
This software is proprietary and confidential.
|
||||
Unauthorized copying, modification, or distribution is prohibited.
|
||||
|
||||
For licensing inquiries: info@hb3-accelerator.com
|
||||
Website: https://hb3-accelerator.com
|
||||
GitHub: https://github.com/HB3-ACCELERATOR
|
||||
-->
|
||||
|
||||
<template>
|
||||
<BaseLayout
|
||||
:is-authenticated="isAuthenticated"
|
||||
:identities="identities"
|
||||
:token-balances="tokenBalances"
|
||||
:is-loading-tokens="isLoadingTokens"
|
||||
@auth-action-completed="$emit('auth-action-completed')"
|
||||
>
|
||||
<div class="settings-container">
|
||||
<!-- Заголовок -->
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<h1>Настройки</h1>
|
||||
<p>Параметры DLE и конфигурация</p>
|
||||
</div>
|
||||
<button class="close-btn" @click="router.push('/management')">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Основные настройки -->
|
||||
<div class="main-settings-section">
|
||||
<h2>Основные настройки</h2>
|
||||
<form @submit.prevent="saveMainSettings" class="settings-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="dleName">Название DLE:</label>
|
||||
<input
|
||||
id="dleName"
|
||||
v-model="mainSettings.name"
|
||||
type="text"
|
||||
placeholder="Введите название DLE"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="dleSymbol">Символ токена:</label>
|
||||
<input
|
||||
id="dleSymbol"
|
||||
v-model="mainSettings.symbol"
|
||||
type="text"
|
||||
placeholder="MDLE"
|
||||
maxlength="10"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="dleDescription">Описание:</label>
|
||||
<textarea
|
||||
id="dleDescription"
|
||||
v-model="mainSettings.description"
|
||||
placeholder="Опишите назначение и деятельность DLE..."
|
||||
rows="4"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="dleLocation">Местонахождение:</label>
|
||||
<input
|
||||
id="dleLocation"
|
||||
v-model="mainSettings.location"
|
||||
type="text"
|
||||
placeholder="Страна, город"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="dleWebsite">Веб-сайт:</label>
|
||||
<input
|
||||
id="dleWebsite"
|
||||
v-model="mainSettings.website"
|
||||
type="url"
|
||||
placeholder="https://example.com"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary" :disabled="isSaving">
|
||||
{{ isSaving ? 'Сохранение...' : 'Сохранить настройки' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Настройки безопасности -->
|
||||
<div class="security-settings-section">
|
||||
<h2>Настройки безопасности</h2>
|
||||
<form @submit.prevent="saveSecuritySettings" class="settings-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="minQuorum">Минимальный кворум (%):</label>
|
||||
<input
|
||||
id="minQuorum"
|
||||
v-model="securitySettings.minQuorum"
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
placeholder="51"
|
||||
required
|
||||
>
|
||||
<span class="input-hint">Минимальный процент токенов для принятия решений</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="maxProposalDuration">Максимальная длительность предложения (дни):</label>
|
||||
<input
|
||||
id="maxProposalDuration"
|
||||
v-model="securitySettings.maxProposalDuration"
|
||||
type="number"
|
||||
min="1"
|
||||
max="365"
|
||||
placeholder="7"
|
||||
required
|
||||
>
|
||||
<span class="input-hint">Максимальное время жизни предложения</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="emergencyThreshold">Порог экстренных действий (%):</label>
|
||||
<input
|
||||
id="emergencyThreshold"
|
||||
v-model="securitySettings.emergencyThreshold"
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
placeholder="75"
|
||||
required
|
||||
>
|
||||
<span class="input-hint">Процент для экстренных действий</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="timelockDelay">Задержка таймлока (часы):</label>
|
||||
<input
|
||||
id="timelockDelay"
|
||||
v-model="securitySettings.timelockDelay"
|
||||
type="number"
|
||||
min="1"
|
||||
max="168"
|
||||
placeholder="24"
|
||||
required
|
||||
>
|
||||
<span class="input-hint">Минимальная задержка перед выполнением</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Дополнительные настройки:</label>
|
||||
<div class="checkbox-group">
|
||||
<label class="checkbox-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="securitySettings.allowDelegation"
|
||||
>
|
||||
Разрешить делегирование голосов
|
||||
</label>
|
||||
<label class="checkbox-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="securitySettings.requireKYC"
|
||||
>
|
||||
Требовать KYC для участия
|
||||
</label>
|
||||
<label class="checkbox-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="securitySettings.autoExecute"
|
||||
>
|
||||
Автоматическое выполнение предложений
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary" :disabled="isSaving">
|
||||
{{ isSaving ? 'Сохранение...' : 'Сохранить настройки безопасности' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Настройки сети -->
|
||||
<div class="network-settings-section">
|
||||
<h2>Настройки сети</h2>
|
||||
<form @submit.prevent="saveNetworkSettings" class="settings-form">
|
||||
<div class="form-group">
|
||||
<label>Поддерживаемые сети:</label>
|
||||
<div class="networks-grid">
|
||||
<label
|
||||
v-for="network in availableNetworks"
|
||||
:key="network.id"
|
||||
class="network-checkbox"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="network.id"
|
||||
v-model="networkSettings.enabledNetworks"
|
||||
>
|
||||
<div class="network-info">
|
||||
<span class="network-name">{{ network.name }}</span>
|
||||
<span class="network-chain-id">Chain ID: {{ network.chainId }}</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="defaultNetwork">Сеть по умолчанию:</label>
|
||||
<select id="defaultNetwork" v-model="networkSettings.defaultNetwork" required>
|
||||
<option value="">Выберите сеть</option>
|
||||
<option
|
||||
v-for="network in availableNetworks"
|
||||
:key="network.id"
|
||||
:value="network.id"
|
||||
>
|
||||
{{ network.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="rpcEndpoint">RPC Endpoint:</label>
|
||||
<input
|
||||
id="rpcEndpoint"
|
||||
v-model="networkSettings.rpcEndpoint"
|
||||
type="url"
|
||||
placeholder="https://mainnet.infura.io/v3/YOUR_PROJECT_ID"
|
||||
>
|
||||
<span class="input-hint">RPC endpoint для подключения к блокчейну</span>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary" :disabled="isSaving">
|
||||
{{ isSaving ? 'Сохранение...' : 'Сохранить настройки сети' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Резервное копирование -->
|
||||
<div class="backup-section">
|
||||
<h2>Резервное копирование</h2>
|
||||
<div class="backup-actions">
|
||||
<div class="backup-card">
|
||||
<h3>Экспорт настроек</h3>
|
||||
<p>Скачайте файл с настройками DLE для резервного копирования</p>
|
||||
<button @click="exportSettings" class="btn-secondary">
|
||||
Экспортировать
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="backup-card">
|
||||
<h3>Импорт настроек</h3>
|
||||
<p>Загрузите файл с настройками для восстановления</p>
|
||||
<input
|
||||
ref="importFile"
|
||||
type="file"
|
||||
accept=".json"
|
||||
@change="importSettings"
|
||||
style="display: none"
|
||||
>
|
||||
<button @click="$refs.importFile.click()" class="btn-secondary">
|
||||
Импортировать
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Опасная зона -->
|
||||
<div class="danger-zone-section">
|
||||
<h2>Опасная зона</h2>
|
||||
<div class="danger-actions">
|
||||
<div class="danger-card">
|
||||
<h3>Сброс настроек</h3>
|
||||
<p>Вернуть все настройки к значениям по умолчанию</p>
|
||||
<button @click="resetSettings" class="btn-danger">
|
||||
Сбросить настройки
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="danger-card">
|
||||
<h3>Удаление DLE</h3>
|
||||
<p>Полное удаление DLE и всех связанных данных</p>
|
||||
<button @click="deleteDLE" class="btn-danger">
|
||||
Удалить DLE
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps, defineEmits } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import BaseLayout from '../../components/BaseLayout.vue';
|
||||
|
||||
// Определяем props
|
||||
const props = defineProps({
|
||||
isAuthenticated: Boolean,
|
||||
identities: Array,
|
||||
tokenBalances: Object,
|
||||
isLoadingTokens: Boolean
|
||||
});
|
||||
|
||||
// Определяем emits
|
||||
const emit = defineEmits(['auth-action-completed']);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// Состояние
|
||||
const isSaving = ref(false);
|
||||
|
||||
// Основные настройки
|
||||
const mainSettings = ref({
|
||||
name: 'Мое DLE',
|
||||
symbol: 'MDLE',
|
||||
description: 'Цифровое юридическое лицо для управления активами и принятия решений',
|
||||
location: 'Россия, Москва',
|
||||
website: 'https://example.com'
|
||||
});
|
||||
|
||||
// Настройки безопасности
|
||||
const securitySettings = ref({
|
||||
minQuorum: 51,
|
||||
maxProposalDuration: 7,
|
||||
emergencyThreshold: 75,
|
||||
timelockDelay: 24,
|
||||
allowDelegation: true,
|
||||
requireKYC: false,
|
||||
autoExecute: false
|
||||
});
|
||||
|
||||
// Настройки сети
|
||||
const networkSettings = ref({
|
||||
enabledNetworks: [1, 137],
|
||||
defaultNetwork: 1,
|
||||
rpcEndpoint: 'https://mainnet.infura.io/v3/YOUR_PROJECT_ID'
|
||||
});
|
||||
|
||||
// Доступные сети
|
||||
const availableNetworks = ref([
|
||||
{ id: 1, name: 'Ethereum Mainnet', chainId: 1 },
|
||||
{ id: 137, name: 'Polygon', chainId: 137 },
|
||||
{ id: 56, name: 'BSC', chainId: 56 },
|
||||
{ id: 42161, name: 'Arbitrum', chainId: 42161 },
|
||||
{ id: 10, name: 'Optimism', chainId: 10 }
|
||||
]);
|
||||
|
||||
// Методы
|
||||
const saveMainSettings = async () => {
|
||||
if (isSaving.value) return;
|
||||
|
||||
try {
|
||||
isSaving.value = true;
|
||||
|
||||
// Здесь будет логика сохранения основных настроек
|
||||
console.log('Сохранение основных настроек:', mainSettings.value);
|
||||
|
||||
// Временная логика
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
alert('Основные настройки успешно сохранены!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка сохранения основных настроек:', error);
|
||||
alert('Ошибка при сохранении настроек');
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const saveSecuritySettings = async () => {
|
||||
if (isSaving.value) return;
|
||||
|
||||
try {
|
||||
isSaving.value = true;
|
||||
|
||||
// Здесь будет логика сохранения настроек безопасности
|
||||
console.log('Сохранение настроек безопасности:', securitySettings.value);
|
||||
|
||||
// Временная логика
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
alert('Настройки безопасности успешно сохранены!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка сохранения настроек безопасности:', error);
|
||||
alert('Ошибка при сохранении настроек безопасности');
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const saveNetworkSettings = async () => {
|
||||
if (isSaving.value) return;
|
||||
|
||||
try {
|
||||
isSaving.value = true;
|
||||
|
||||
// Здесь будет логика сохранения настроек сети
|
||||
console.log('Сохранение настроек сети:', networkSettings.value);
|
||||
|
||||
// Временная логика
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
alert('Настройки сети успешно сохранены!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка сохранения настроек сети:', error);
|
||||
alert('Ошибка при сохранении настроек сети');
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const exportSettings = () => {
|
||||
const settings = {
|
||||
main: mainSettings.value,
|
||||
security: securitySettings.value,
|
||||
network: networkSettings.value,
|
||||
exportDate: new Date().toISOString()
|
||||
};
|
||||
|
||||
const blob = new Blob([JSON.stringify(settings, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `dle-settings-${new Date().toISOString().split('T')[0]}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
alert('Настройки успешно экспортированы!');
|
||||
};
|
||||
|
||||
const importSettings = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const settings = JSON.parse(e.target.result);
|
||||
|
||||
if (settings.main) mainSettings.value = settings.main;
|
||||
if (settings.security) securitySettings.value = settings.security;
|
||||
if (settings.network) networkSettings.value = settings.network;
|
||||
|
||||
alert('Настройки успешно импортированы!');
|
||||
} catch (error) {
|
||||
console.error('Ошибка импорта настроек:', error);
|
||||
alert('Ошибка при импорте настроек');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
const resetSettings = () => {
|
||||
if (!confirm('Вы уверены, что хотите сбросить все настройки к значениям по умолчанию?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Сброс к значениям по умолчанию
|
||||
mainSettings.value = {
|
||||
name: 'Мое DLE',
|
||||
symbol: 'MDLE',
|
||||
description: 'Цифровое юридическое лицо для управления активами и принятия решений',
|
||||
location: 'Россия, Москва',
|
||||
website: 'https://example.com'
|
||||
};
|
||||
|
||||
securitySettings.value = {
|
||||
minQuorum: 51,
|
||||
maxProposalDuration: 7,
|
||||
emergencyThreshold: 75,
|
||||
timelockDelay: 24,
|
||||
allowDelegation: true,
|
||||
requireKYC: false,
|
||||
autoExecute: false
|
||||
};
|
||||
|
||||
networkSettings.value = {
|
||||
enabledNetworks: [1, 137],
|
||||
defaultNetwork: 1,
|
||||
rpcEndpoint: 'https://mainnet.infura.io/v3/YOUR_PROJECT_ID'
|
||||
};
|
||||
|
||||
alert('Настройки сброшены к значениям по умолчанию!');
|
||||
};
|
||||
|
||||
const deleteDLE = () => {
|
||||
if (!confirm('ВНИМАНИЕ! Это действие необратимо. Вы уверены, что хотите удалить DLE и все связанные данные?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm('Это действие нельзя отменить. Подтвердите удаление еще раз.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Здесь будет логика удаления DLE
|
||||
console.log('Удаление DLE...');
|
||||
alert('DLE будет удален. Это действие может занять некоторое время.');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.settings-container {
|
||||
padding: 20px;
|
||||
background-color: var(--color-white);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 40px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
color: var(--color-primary);
|
||||
font-size: 2.5rem;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: var(--color-grey-dark);
|
||||
font-size: 1.1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Секции */
|
||||
.main-settings-section,
|
||||
.security-settings-section,
|
||||
.network-settings-section,
|
||||
.backup-section,
|
||||
.danger-zone-section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.main-settings-section h2,
|
||||
.security-settings-section h2,
|
||||
.network-settings-section h2,
|
||||
.backup-section h2,
|
||||
.danger-zone-section h2 {
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
/* Формы настроек */
|
||||
.settings-form {
|
||||
background: #f8f9fa;
|
||||
padding: 25px;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
color: var(--color-grey-dark);
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.input-hint {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-grey-dark);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Чекбоксы */
|
||||
.checkbox-group {
|
||||
display: grid;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
padding: 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.checkbox-item:hover {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.checkbox-item input[type="checkbox"] {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Сети */
|
||||
.networks-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.network-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
cursor: pointer;
|
||||
padding: 15px;
|
||||
background: white;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.network-checkbox:hover {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.network-checkbox input[type="checkbox"] {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.network-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.network-name {
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.network-chain-id {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-grey-dark);
|
||||
}
|
||||
|
||||
/* Резервное копирование */
|
||||
.backup-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.backup-card {
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid #e9ecef;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.backup-card h3 {
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.backup-card p {
|
||||
color: var(--color-grey-dark);
|
||||
margin-bottom: 20px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Опасная зона */
|
||||
.danger-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.danger-card {
|
||||
background: #fff5f5;
|
||||
padding: 25px;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid #fed7d7;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.danger-card h3 {
|
||||
color: #c53030;
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.danger-card p {
|
||||
color: var(--color-grey-dark);
|
||||
margin-bottom: 20px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Кнопки */
|
||||
.btn-primary {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--color-secondary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--color-secondary-dark);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.networks-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.backup-actions,
|
||||
.danger-actions {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
626
frontend/src/views/smartcontracts/TokensView.vue
Normal file
626
frontend/src/views/smartcontracts/TokensView.vue
Normal file
@@ -0,0 +1,626 @@
|
||||
<!--
|
||||
Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
All rights reserved.
|
||||
|
||||
This software is proprietary and confidential.
|
||||
Unauthorized copying, modification, or distribution is prohibited.
|
||||
|
||||
For licensing inquiries: info@hb3-accelerator.com
|
||||
Website: https://hb3-accelerator.com
|
||||
GitHub: https://github.com/HB3-ACCELERATOR
|
||||
-->
|
||||
|
||||
<template>
|
||||
<BaseLayout
|
||||
:is-authenticated="isAuthenticated"
|
||||
:identities="identities"
|
||||
:token-balances="tokenBalances"
|
||||
:is-loading-tokens="isLoadingTokens"
|
||||
@auth-action-completed="$emit('auth-action-completed')"
|
||||
>
|
||||
<div class="tokens-container">
|
||||
<!-- Заголовок -->
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<h1>Токены DLE</h1>
|
||||
<p>Балансы, трансферы и распределение токенов</p>
|
||||
</div>
|
||||
<button class="close-btn" @click="router.push('/management')">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Информация о токенах -->
|
||||
<div class="token-info-section">
|
||||
<h2>Информация о токенах</h2>
|
||||
<div class="token-info-grid">
|
||||
<div class="info-card">
|
||||
<h3>Общий запас</h3>
|
||||
<p class="token-amount">{{ totalSupply }} {{ tokenSymbol }}</p>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<h3>Ваш баланс</h3>
|
||||
<p class="token-amount">{{ userBalance }} {{ tokenSymbol }}</p>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<h3>Кворум</h3>
|
||||
<p class="token-amount">{{ quorumPercentage }}%</p>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<h3>Цена токена</h3>
|
||||
<p class="token-amount">${{ tokenPrice }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Трансфер токенов -->
|
||||
<div class="transfer-section">
|
||||
<h2>Перевод токенов</h2>
|
||||
<form @submit.prevent="transferTokens" class="transfer-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="recipient">Получатель:</label>
|
||||
<input
|
||||
id="recipient"
|
||||
v-model="transferData.recipient"
|
||||
type="text"
|
||||
placeholder="0x..."
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="amount">Количество токенов:</label>
|
||||
<input
|
||||
id="amount"
|
||||
v-model="transferData.amount"
|
||||
type="number"
|
||||
min="0.01"
|
||||
step="0.01"
|
||||
placeholder="0.00"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="transferDescription">Описание (опционально):</label>
|
||||
<textarea
|
||||
id="transferDescription"
|
||||
v-model="transferData.description"
|
||||
placeholder="Укажите причину перевода..."
|
||||
rows="3"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary" :disabled="isTransferring">
|
||||
{{ isTransferring ? 'Перевод...' : 'Перевести токены' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Распределение токенов -->
|
||||
<div class="distribution-section">
|
||||
<h2>Распределение токенов</h2>
|
||||
<form @submit.prevent="distributeTokens" class="distribution-form">
|
||||
<div class="form-group">
|
||||
<label for="distributionType">Тип распределения:</label>
|
||||
<select id="distributionType" v-model="distributionData.type" required>
|
||||
<option value="">Выберите тип</option>
|
||||
<option value="partners">Партнерам</option>
|
||||
<option value="employees">Сотрудникам</option>
|
||||
<option value="investors">Инвесторам</option>
|
||||
<option value="custom">Пользовательское</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Получатели:</label>
|
||||
<div class="recipients-list">
|
||||
<div
|
||||
v-for="(recipient, index) in distributionData.recipients"
|
||||
:key="index"
|
||||
class="recipient-item"
|
||||
>
|
||||
<input
|
||||
v-model="recipient.address"
|
||||
type="text"
|
||||
placeholder="Адрес получателя"
|
||||
required
|
||||
>
|
||||
<input
|
||||
v-model="recipient.amount"
|
||||
type="number"
|
||||
placeholder="Количество"
|
||||
min="0.01"
|
||||
step="0.01"
|
||||
required
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@click="removeRecipient(index)"
|
||||
class="btn-remove"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="addRecipient"
|
||||
class="btn-secondary"
|
||||
>
|
||||
+ Добавить получателя
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary" :disabled="isDistributing">
|
||||
{{ isDistributing ? 'Распределение...' : 'Распределить токены' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Держатели токенов -->
|
||||
<div class="holders-section">
|
||||
<h2>Держатели токенов</h2>
|
||||
<div v-if="tokenHolders.length === 0" class="empty-state">
|
||||
<p>Нет данных о держателях токенов</p>
|
||||
</div>
|
||||
<div v-else class="holders-list">
|
||||
<div
|
||||
v-for="holder in tokenHolders"
|
||||
:key="holder.address"
|
||||
class="holder-item"
|
||||
>
|
||||
<div class="holder-info">
|
||||
<span class="holder-address">{{ formatAddress(holder.address) }}</span>
|
||||
<span class="holder-balance">{{ holder.balance }} {{ tokenSymbol }}</span>
|
||||
</div>
|
||||
<div class="holder-percentage">
|
||||
{{ ((holder.balance / totalSupply) * 100).toFixed(2) }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps, defineEmits } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import BaseLayout from '../../components/BaseLayout.vue';
|
||||
|
||||
// Определяем props
|
||||
const props = defineProps({
|
||||
isAuthenticated: Boolean,
|
||||
identities: Array,
|
||||
tokenBalances: Object,
|
||||
isLoadingTokens: Boolean
|
||||
});
|
||||
|
||||
// Определяем emits
|
||||
const emit = defineEmits(['auth-action-completed']);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// Состояние
|
||||
const isTransferring = ref(false);
|
||||
const isDistributing = ref(false);
|
||||
|
||||
// Данные токенов
|
||||
const tokenSymbol = ref('MDLE');
|
||||
const totalSupply = ref(10000);
|
||||
const userBalance = ref(1000);
|
||||
const quorumPercentage = ref(51);
|
||||
const tokenPrice = ref(1.25);
|
||||
|
||||
// Данные трансфера
|
||||
const transferData = ref({
|
||||
recipient: '',
|
||||
amount: '',
|
||||
description: ''
|
||||
});
|
||||
|
||||
// Данные распределения
|
||||
const distributionData = ref({
|
||||
type: '',
|
||||
recipients: [
|
||||
{ address: '', amount: '' }
|
||||
]
|
||||
});
|
||||
|
||||
// Держатели токенов (временные данные)
|
||||
const tokenHolders = ref([
|
||||
{ address: '0x1234567890123456789012345678901234567890', balance: 2500 },
|
||||
{ address: '0x2345678901234567890123456789012345678901', balance: 1800 },
|
||||
{ address: '0x3456789012345678901234567890123456789012', balance: 1200 },
|
||||
{ address: '0x4567890123456789012345678901234567890123', balance: 800 },
|
||||
{ address: '0x5678901234567890123456789012345678901234', balance: 600 }
|
||||
]);
|
||||
|
||||
// Методы
|
||||
const transferTokens = async () => {
|
||||
if (isTransferring.value) return;
|
||||
|
||||
try {
|
||||
isTransferring.value = true;
|
||||
|
||||
// Здесь будет логика трансфера токенов
|
||||
console.log('Трансфер токенов:', transferData.value);
|
||||
|
||||
// Временная логика
|
||||
const amount = parseFloat(transferData.value.amount);
|
||||
if (amount > userBalance.value) {
|
||||
alert('Недостаточно токенов для перевода');
|
||||
return;
|
||||
}
|
||||
|
||||
userBalance.value -= amount;
|
||||
|
||||
// Сброс формы
|
||||
transferData.value = {
|
||||
recipient: '',
|
||||
amount: '',
|
||||
description: ''
|
||||
};
|
||||
|
||||
alert('Токены успешно переведены!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка трансфера токенов:', error);
|
||||
alert('Ошибка при переводе токенов');
|
||||
} finally {
|
||||
isTransferring.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const distributeTokens = async () => {
|
||||
if (isDistributing.value) return;
|
||||
|
||||
try {
|
||||
isDistributing.value = true;
|
||||
|
||||
// Здесь будет логика распределения токенов
|
||||
console.log('Распределение токенов:', distributionData.value);
|
||||
|
||||
// Временная логика
|
||||
const totalAmount = distributionData.value.recipients.reduce((sum, recipient) => {
|
||||
return sum + parseFloat(recipient.amount || 0);
|
||||
}, 0);
|
||||
|
||||
if (totalAmount > userBalance.value) {
|
||||
alert('Недостаточно токенов для распределения');
|
||||
return;
|
||||
}
|
||||
|
||||
userBalance.value -= totalAmount;
|
||||
|
||||
// Сброс формы
|
||||
distributionData.value = {
|
||||
type: '',
|
||||
recipients: [{ address: '', amount: '' }]
|
||||
};
|
||||
|
||||
alert('Токены успешно распределены!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка распределения токенов:', error);
|
||||
alert('Ошибка при распределении токенов');
|
||||
} finally {
|
||||
isDistributing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const addRecipient = () => {
|
||||
distributionData.value.recipients.push({ address: '', amount: '' });
|
||||
};
|
||||
|
||||
const removeRecipient = (index) => {
|
||||
if (distributionData.value.recipients.length > 1) {
|
||||
distributionData.value.recipients.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
const formatAddress = (address) => {
|
||||
if (!address) return '';
|
||||
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tokens-container {
|
||||
padding: 20px;
|
||||
background-color: var(--color-white);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 40px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
color: var(--color-primary);
|
||||
font-size: 2.5rem;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: var(--color-grey-dark);
|
||||
font-size: 1.1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Секции */
|
||||
.token-info-section,
|
||||
.transfer-section,
|
||||
.distribution-section,
|
||||
.holders-section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.token-info-section h2,
|
||||
.transfer-section h2,
|
||||
.distribution-section h2,
|
||||
.holders-section h2 {
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
/* Информация о токенах */
|
||||
.token-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: #f8f9fa;
|
||||
padding: 25px;
|
||||
border-radius: var(--radius-lg);
|
||||
border-left: 4px solid var(--color-primary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info-card h3 {
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 15px;
|
||||
font-size: 1rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.token-amount {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Формы */
|
||||
.transfer-form,
|
||||
.distribution-form {
|
||||
background: #f8f9fa;
|
||||
padding: 25px;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
color: var(--color-grey-dark);
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
/* Получатели */
|
||||
.recipients-list {
|
||||
display: grid;
|
||||
gap: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.recipient-item {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr auto;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-remove {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-remove:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
/* Держатели токенов */
|
||||
.holders-list {
|
||||
display: grid;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.holder-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.holder-item:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.holder-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.holder-address {
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-grey-dark);
|
||||
}
|
||||
|
||||
.holder-balance {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.holder-percentage {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-grey-dark);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Кнопки */
|
||||
.btn-primary {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--color-secondary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--color-secondary-dark);
|
||||
}
|
||||
|
||||
/* Состояния */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px;
|
||||
color: var(--color-grey-dark);
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 2px dashed #dee2e6;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.recipient-item {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.holder-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.token-info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
935
frontend/src/views/smartcontracts/TreasuryView.vue
Normal file
935
frontend/src/views/smartcontracts/TreasuryView.vue
Normal file
@@ -0,0 +1,935 @@
|
||||
<!--
|
||||
Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
All rights reserved.
|
||||
|
||||
This software is proprietary and confidential.
|
||||
Unauthorized copying, modification, or distribution is prohibited.
|
||||
|
||||
For licensing inquiries: info@hb3-accelerator.com
|
||||
Website: https://hb3-accelerator.com
|
||||
GitHub: https://github.com/HB3-ACCELERATOR
|
||||
-->
|
||||
|
||||
<template>
|
||||
<BaseLayout
|
||||
:is-authenticated="isAuthenticated"
|
||||
:identities="identities"
|
||||
:token-balances="tokenBalances"
|
||||
:is-loading-tokens="isLoadingTokens"
|
||||
@auth-action-completed="$emit('auth-action-completed')"
|
||||
>
|
||||
<div class="treasury-container">
|
||||
<!-- Заголовок -->
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<h1>Казна</h1>
|
||||
<p>Управление средствами и активами DLE</p>
|
||||
</div>
|
||||
<button class="close-btn" @click="router.push('/management')">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Обзор казны -->
|
||||
<div class="treasury-overview">
|
||||
<h2>Обзор казны</h2>
|
||||
<div class="overview-grid">
|
||||
<div class="overview-card">
|
||||
<h3>Общий баланс</h3>
|
||||
<p class="balance-amount">${{ totalBalance.toLocaleString() }}</p>
|
||||
<p class="balance-change positive">+${{ dailyChange.toLocaleString() }} (24ч)</p>
|
||||
</div>
|
||||
<div class="overview-card">
|
||||
<h3>Активы</h3>
|
||||
<p class="balance-amount">{{ assetsCount }}</p>
|
||||
<p class="balance-description">различных активов</p>
|
||||
</div>
|
||||
<div class="overview-card">
|
||||
<h3>Доходность</h3>
|
||||
<p class="balance-amount">{{ yieldPercentage }}%</p>
|
||||
<p class="balance-description">годовая доходность</p>
|
||||
</div>
|
||||
<div class="overview-card">
|
||||
<h3>Риск</h3>
|
||||
<p class="balance-amount risk-low">Низкий</p>
|
||||
<p class="balance-description">уровень риска</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Активы -->
|
||||
<div class="assets-section">
|
||||
<h2>Активы</h2>
|
||||
<div class="assets-list">
|
||||
<div
|
||||
v-for="asset in assets"
|
||||
:key="asset.id"
|
||||
class="asset-card"
|
||||
>
|
||||
<div class="asset-info">
|
||||
<div class="asset-header">
|
||||
<h3>{{ asset.name }}</h3>
|
||||
<span class="asset-symbol">{{ asset.symbol }}</span>
|
||||
</div>
|
||||
<div class="asset-balance">
|
||||
<p class="asset-amount">{{ asset.balance }} {{ asset.symbol }}</p>
|
||||
<p class="asset-value">${{ asset.value.toLocaleString() }}</p>
|
||||
</div>
|
||||
<div class="asset-change" :class="asset.change >= 0 ? 'positive' : 'negative'">
|
||||
{{ asset.change >= 0 ? '+' : '' }}{{ asset.change }}% (24ч)
|
||||
</div>
|
||||
</div>
|
||||
<div class="asset-actions">
|
||||
<button @click="depositAsset(asset)" class="btn-secondary">
|
||||
Пополнить
|
||||
</button>
|
||||
<button @click="withdrawAsset(asset)" class="btn-secondary">
|
||||
Вывести
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Операции -->
|
||||
<div class="operations-section">
|
||||
<h2>Операции</h2>
|
||||
<div class="operations-tabs">
|
||||
<button
|
||||
v-for="tab in operationTabs"
|
||||
:key="tab.id"
|
||||
@click="activeTab = tab.id"
|
||||
class="tab-button"
|
||||
:class="{ active: activeTab === tab.id }"
|
||||
>
|
||||
{{ tab.name }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Форма депозита -->
|
||||
<div v-if="activeTab === 'deposit'" class="operation-form">
|
||||
<form @submit.prevent="performDeposit" class="deposit-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="depositAsset">Актив:</label>
|
||||
<select id="depositAsset" v-model="depositData.asset" required>
|
||||
<option value="">Выберите актив</option>
|
||||
<option v-for="asset in assets" :key="asset.id" :value="asset.id">
|
||||
{{ asset.name }} ({{ asset.symbol }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="depositAmount">Количество:</label>
|
||||
<input
|
||||
id="depositAmount"
|
||||
v-model="depositData.amount"
|
||||
type="number"
|
||||
min="0.000001"
|
||||
step="0.000001"
|
||||
placeholder="0.00"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="depositReason">Причина депозита:</label>
|
||||
<textarea
|
||||
id="depositReason"
|
||||
v-model="depositData.reason"
|
||||
placeholder="Опишите причину депозита..."
|
||||
rows="3"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary" :disabled="isProcessing">
|
||||
{{ isProcessing ? 'Обработка...' : 'Пополнить казну' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Форма вывода -->
|
||||
<div v-if="activeTab === 'withdraw'" class="operation-form">
|
||||
<form @submit.prevent="performWithdraw" class="withdraw-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="withdrawAsset">Актив:</label>
|
||||
<select id="withdrawAsset" v-model="withdrawData.asset" required>
|
||||
<option value="">Выберите актив</option>
|
||||
<option v-for="asset in assets" :key="asset.id" :value="asset.id">
|
||||
{{ asset.name }} ({{ asset.symbol }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="withdrawAmount">Количество:</label>
|
||||
<input
|
||||
id="withdrawAmount"
|
||||
v-model="withdrawData.amount"
|
||||
type="number"
|
||||
min="0.000001"
|
||||
step="0.000001"
|
||||
placeholder="0.00"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="withdrawRecipient">Получатель:</label>
|
||||
<input
|
||||
id="withdrawRecipient"
|
||||
v-model="withdrawData.recipient"
|
||||
type="text"
|
||||
placeholder="0x..."
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="withdrawReason">Причина вывода:</label>
|
||||
<input
|
||||
id="withdrawReason"
|
||||
v-model="withdrawData.reason"
|
||||
type="text"
|
||||
placeholder="Опишите причину вывода..."
|
||||
required
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary" :disabled="isProcessing">
|
||||
{{ isProcessing ? 'Обработка...' : 'Вывести из казны' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- История операций -->
|
||||
<div class="history-section">
|
||||
<h2>История операций</h2>
|
||||
<div v-if="operationsHistory.length === 0" class="empty-state">
|
||||
<p>Нет операций в истории</p>
|
||||
</div>
|
||||
<div v-else class="history-list">
|
||||
<div
|
||||
v-for="operation in operationsHistory"
|
||||
:key="operation.id"
|
||||
class="history-item"
|
||||
>
|
||||
<div class="operation-info">
|
||||
<div class="operation-header">
|
||||
<h3>{{ operation.type === 'deposit' ? 'Депозит' : 'Вывод' }}</h3>
|
||||
<span class="operation-date">{{ formatDate(operation.timestamp) }}</span>
|
||||
</div>
|
||||
<div class="operation-details">
|
||||
<p><strong>Актив:</strong> {{ operation.asset }}</p>
|
||||
<p><strong>Количество:</strong> {{ operation.amount }} {{ operation.symbol }}</p>
|
||||
<p><strong>Стоимость:</strong> ${{ operation.value.toLocaleString() }}</p>
|
||||
<p v-if="operation.reason"><strong>Причина:</strong> {{ operation.reason }}</p>
|
||||
<p v-if="operation.recipient"><strong>Получатель:</strong> {{ formatAddress(operation.recipient) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="operation-status" :class="operation.status">
|
||||
{{ getStatusText(operation.status) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps, defineEmits } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import BaseLayout from '../../components/BaseLayout.vue';
|
||||
|
||||
// Определяем props
|
||||
const props = defineProps({
|
||||
isAuthenticated: Boolean,
|
||||
identities: Array,
|
||||
tokenBalances: Object,
|
||||
isLoadingTokens: Boolean
|
||||
});
|
||||
|
||||
// Определяем emits
|
||||
const emit = defineEmits(['auth-action-completed']);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// Состояние
|
||||
const isProcessing = ref(false);
|
||||
const activeTab = ref('deposit');
|
||||
|
||||
// Данные казны
|
||||
const totalBalance = ref(1250000);
|
||||
const dailyChange = ref(25000);
|
||||
const assetsCount = ref(5);
|
||||
const yieldPercentage = ref(8.5);
|
||||
|
||||
// Активы (временные данные)
|
||||
const assets = ref([
|
||||
{
|
||||
id: 'eth',
|
||||
name: 'Ethereum',
|
||||
symbol: 'ETH',
|
||||
balance: 125.5,
|
||||
value: 450000,
|
||||
change: 2.5
|
||||
},
|
||||
{
|
||||
id: 'usdc',
|
||||
name: 'USD Coin',
|
||||
symbol: 'USDC',
|
||||
balance: 500000,
|
||||
value: 500000,
|
||||
change: 0.1
|
||||
},
|
||||
{
|
||||
id: 'btc',
|
||||
name: 'Bitcoin',
|
||||
symbol: 'BTC',
|
||||
balance: 2.5,
|
||||
value: 150000,
|
||||
change: -1.2
|
||||
},
|
||||
{
|
||||
id: 'matic',
|
||||
name: 'Polygon',
|
||||
symbol: 'MATIC',
|
||||
balance: 50000,
|
||||
value: 75000,
|
||||
change: 5.8
|
||||
},
|
||||
{
|
||||
id: 'link',
|
||||
name: 'Chainlink',
|
||||
symbol: 'LINK',
|
||||
balance: 2500,
|
||||
value: 75000,
|
||||
change: 3.2
|
||||
}
|
||||
]);
|
||||
|
||||
// Вкладки операций
|
||||
const operationTabs = ref([
|
||||
{ id: 'deposit', name: 'Депозит' },
|
||||
{ id: 'withdraw', name: 'Вывод' }
|
||||
]);
|
||||
|
||||
// Данные депозита
|
||||
const depositData = ref({
|
||||
asset: '',
|
||||
amount: '',
|
||||
reason: ''
|
||||
});
|
||||
|
||||
// Данные вывода
|
||||
const withdrawData = ref({
|
||||
asset: '',
|
||||
amount: '',
|
||||
recipient: '',
|
||||
reason: ''
|
||||
});
|
||||
|
||||
// История операций (временные данные)
|
||||
const operationsHistory = ref([
|
||||
{
|
||||
id: 1,
|
||||
type: 'deposit',
|
||||
asset: 'Ethereum',
|
||||
symbol: 'ETH',
|
||||
amount: 10.5,
|
||||
value: 37500,
|
||||
reason: 'Пополнение казны от доходов',
|
||||
timestamp: Date.now() - 3600000,
|
||||
status: 'completed'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'withdraw',
|
||||
asset: 'USD Coin',
|
||||
symbol: 'USDC',
|
||||
amount: 25000,
|
||||
value: 25000,
|
||||
reason: 'Выплата партнерам',
|
||||
recipient: '0x1234567890123456789012345678901234567890',
|
||||
timestamp: Date.now() - 7200000,
|
||||
status: 'completed'
|
||||
}
|
||||
]);
|
||||
|
||||
// Методы
|
||||
const depositAsset = (asset) => {
|
||||
depositData.value.asset = asset.id;
|
||||
activeTab.value = 'deposit';
|
||||
};
|
||||
|
||||
const withdrawAsset = (asset) => {
|
||||
withdrawData.value.asset = asset.id;
|
||||
activeTab.value = 'withdraw';
|
||||
};
|
||||
|
||||
const performDeposit = async () => {
|
||||
if (isProcessing.value) return;
|
||||
|
||||
try {
|
||||
isProcessing.value = true;
|
||||
|
||||
// Здесь будет логика депозита
|
||||
console.log('Депозит:', depositData.value);
|
||||
|
||||
// Временная логика
|
||||
const asset = assets.value.find(a => a.id === depositData.value.asset);
|
||||
if (asset) {
|
||||
const amount = parseFloat(depositData.value.amount);
|
||||
asset.balance += amount;
|
||||
asset.value = asset.balance * (asset.value / (asset.balance - amount));
|
||||
totalBalance.value += amount * (asset.value / asset.balance);
|
||||
}
|
||||
|
||||
// Добавляем в историю
|
||||
operationsHistory.value.unshift({
|
||||
id: operationsHistory.value.length + 1,
|
||||
type: 'deposit',
|
||||
asset: asset.name,
|
||||
symbol: asset.symbol,
|
||||
amount: parseFloat(depositData.value.amount),
|
||||
value: parseFloat(depositData.value.amount) * (asset.value / asset.balance),
|
||||
reason: depositData.value.reason,
|
||||
timestamp: Date.now(),
|
||||
status: 'completed'
|
||||
});
|
||||
|
||||
// Сброс формы
|
||||
depositData.value = {
|
||||
asset: '',
|
||||
amount: '',
|
||||
reason: ''
|
||||
};
|
||||
|
||||
alert('Депозит успешно выполнен!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка депозита:', error);
|
||||
alert('Ошибка при выполнении депозита');
|
||||
} finally {
|
||||
isProcessing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const performWithdraw = async () => {
|
||||
if (isProcessing.value) return;
|
||||
|
||||
try {
|
||||
isProcessing.value = true;
|
||||
|
||||
// Здесь будет логика вывода
|
||||
console.log('Вывод:', withdrawData.value);
|
||||
|
||||
// Временная логика
|
||||
const asset = assets.value.find(a => a.id === withdrawData.value.asset);
|
||||
if (asset) {
|
||||
const amount = parseFloat(withdrawData.value.amount);
|
||||
if (amount > asset.balance) {
|
||||
alert('Недостаточно средств для вывода');
|
||||
return;
|
||||
}
|
||||
|
||||
asset.balance -= amount;
|
||||
asset.value = asset.balance * (asset.value / (asset.balance + amount));
|
||||
totalBalance.value -= amount * (asset.value / asset.balance);
|
||||
}
|
||||
|
||||
// Добавляем в историю
|
||||
operationsHistory.value.unshift({
|
||||
id: operationsHistory.value.length + 1,
|
||||
type: 'withdraw',
|
||||
asset: asset.name,
|
||||
symbol: asset.symbol,
|
||||
amount: parseFloat(withdrawData.value.amount),
|
||||
value: parseFloat(withdrawData.value.amount) * (asset.value / asset.balance),
|
||||
reason: withdrawData.value.reason,
|
||||
recipient: withdrawData.value.recipient,
|
||||
timestamp: Date.now(),
|
||||
status: 'completed'
|
||||
});
|
||||
|
||||
// Сброс формы
|
||||
withdrawData.value = {
|
||||
asset: '',
|
||||
amount: '',
|
||||
recipient: '',
|
||||
reason: ''
|
||||
};
|
||||
|
||||
alert('Вывод успешно выполнен!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка вывода:', error);
|
||||
alert('Ошибка при выполнении вывода');
|
||||
} finally {
|
||||
isProcessing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
'completed': 'Завершено',
|
||||
'pending': 'В обработке',
|
||||
'failed': 'Ошибка'
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
};
|
||||
|
||||
const formatAddress = (address) => {
|
||||
if (!address) return '';
|
||||
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
|
||||
};
|
||||
|
||||
const formatDate = (timestamp) => {
|
||||
return new Date(timestamp).toLocaleString('ru-RU');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.treasury-container {
|
||||
padding: 20px;
|
||||
background-color: var(--color-white);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 40px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
color: var(--color-primary);
|
||||
font-size: 2.5rem;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: var(--color-grey-dark);
|
||||
font-size: 1.1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Секции */
|
||||
.treasury-overview,
|
||||
.assets-section,
|
||||
.operations-section,
|
||||
.history-section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.treasury-overview h2,
|
||||
.assets-section h2,
|
||||
.operations-section h2,
|
||||
.history-section h2 {
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
/* Обзор казны */
|
||||
.overview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
background: #f8f9fa;
|
||||
padding: 25px;
|
||||
border-radius: var(--radius-lg);
|
||||
border-left: 4px solid var(--color-primary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.overview-card h3 {
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 15px;
|
||||
font-size: 1rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.balance-amount {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 10px 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.balance-change {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.balance-change.positive {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.balance-change.negative {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.balance-description {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-grey-dark);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.risk-low {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
/* Активы */
|
||||
.assets-list {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.asset-card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 25px;
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.asset-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.asset-info {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.asset-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.asset-header h3 {
|
||||
margin: 0;
|
||||
color: var(--color-primary);
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.asset-symbol {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.asset-balance {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.asset-amount {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 5px 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.asset-value {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-grey-dark);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.asset-change {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.asset-change.positive {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.asset-change.negative {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.asset-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Операции */
|
||||
.operations-tabs {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
padding: 12px 24px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.tab-button:hover {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.operation-form {
|
||||
background: #f8f9fa;
|
||||
padding: 25px;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
color: var(--color-grey-dark);
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
/* История операций */
|
||||
.history-list {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 25px;
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.history-item:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.operation-info {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.operation-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.operation-header h3 {
|
||||
margin: 0;
|
||||
color: var(--color-primary);
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.operation-date {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-grey-dark);
|
||||
}
|
||||
|
||||
.operation-details p {
|
||||
margin: 5px 0;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.operation-status {
|
||||
padding: 6px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.operation-status.completed {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.operation-status.pending {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.operation-status.failed {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
/* Кнопки */
|
||||
.btn-primary {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--color-secondary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--color-secondary-dark);
|
||||
}
|
||||
|
||||
/* Состояния */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px;
|
||||
color: var(--color-grey-dark);
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 2px dashed #dee2e6;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.asset-card {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.asset-actions {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.operation-status {
|
||||
margin-left: 0;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.overview-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.operations-tabs {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user