Files
DLE/frontend/src/views/smartcontracts/AnalyticsView.vue

1001 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!--
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>Аналитика DLE</h1>
<p v-if="selectedDle">{{ selectedDle.name }} ({{ selectedDle.symbol }}) - {{ formatAddress(dleAddress) }}</p>
<p v-else-if="isLoadingDle">Загрузка...</p>
<p v-else>Подробная аналитика и статистика DLE</p>
</div>
<button class="close-btn" @click="goBackToBlocks">×</button>
</div>
<!-- Основная информация -->
<div class="info-section">
<h2>Основная информация</h2>
<div class="info-grid">
<div class="info-card">
<h3>Название</h3>
<p class="info-value">{{ selectedDle?.name || 'Загрузка...' }}</p>
</div>
<div class="info-card">
<h3>Символ</h3>
<p class="info-value">{{ selectedDle?.symbol || 'Загрузка...' }}</p>
</div>
<div class="info-card">
<h3>Статус</h3>
<p class="info-value" :class="selectedDle?.isActive ? 'status-active' : 'status-inactive'">
{{ selectedDle?.isActive ? 'Активен' : 'Неактивен' }}
</p>
</div>
<div class="info-card">
<h3>Дата создания</h3>
<p class="info-value">{{ formatDate(selectedDle?.creationTimestamp) }}</p>
</div>
<div class="info-card">
<h3>Местоположение</h3>
<p class="info-value">{{ selectedDle?.location || 'Не указано' }}</p>
</div>
<div class="info-card">
<h3>Юрисдикция</h3>
<p class="info-value">{{ selectedDle?.jurisdiction || 'Не указано' }}</p>
</div>
</div>
</div>
<!-- Токеномика -->
<div class="tokenomics-section">
<h2>Токеномика</h2>
<div class="tokenomics-grid">
<div class="tokenomics-card">
<h3>Общий объем токенов</h3>
<p class="tokenomics-value">{{ formatNumber(tokenomics.totalSupply) }}</p>
<p class="tokenomics-label">Токенов в обращении</p>
</div>
<div class="tokenomics-card">
<h3>Держатели токенов</h3>
<p class="tokenomics-value">{{ tokenomics.holdersCount }}</p>
<p class="tokenomics-label">Активных держателей</p>
</div>
<div class="tokenomics-card">
<h3>Крупнейший держатель</h3>
<p class="tokenomics-value">{{ tokenomics.topHolderPercentage }}%</p>
<p class="tokenomics-label">{{ formatAddress(tokenomics.topHolderAddress) }}</p>
</div>
</div>
</div>
<!-- Управление -->
<div class="governance-section">
<h2>Управление</h2>
<div class="governance-grid">
<div class="governance-card">
<h3>Всего предложений</h3>
<p class="governance-value">{{ governance.totalProposals }}</p>
</div>
<div class="governance-card">
<h3>Исполнено</h3>
<p class="governance-value">{{ governance.executedProposals }}</p>
</div>
<div class="governance-card">
<h3>Отклонено</h3>
<p class="governance-value">{{ governance.defeatedProposals }}</p>
</div>
<div class="governance-card">
<h3>Кворум</h3>
<p class="governance-value">{{ governance.quorumPercentage }}%</p>
</div>
<div class="governance-card">
<h3>Поддерживаемые сети</h3>
<p class="governance-value">{{ governance.supportedChainsCount }}</p>
</div>
<div class="governance-card">
<h3>Текущая сеть</h3>
<p class="governance-value">{{ getChainName(governance.currentChainId) }}</p>
</div>
</div>
</div>
<!-- Статистика предложений -->
<div class="proposals-section">
<h2>Статистика предложений</h2>
<div class="proposals-grid">
<div class="proposals-card">
<h3>Статусы предложений</h3>
<div class="proposals-stats">
<div class="stat-item">
<span class="stat-label">Ожидают голосования</span>
<span class="stat-value">{{ proposalsStats.pending }}</span>
</div>
<div class="stat-item">
<span class="stat-label">Успешные</span>
<span class="stat-value">{{ proposalsStats.succeeded }}</span>
</div>
<div class="stat-item">
<span class="stat-label">Отклоненные</span>
<span class="stat-value">{{ proposalsStats.defeated }}</span>
</div>
<div class="stat-item">
<span class="stat-label">Исполненные</span>
<span class="stat-value">{{ proposalsStats.executed }}</span>
</div>
<div class="stat-item">
<span class="stat-label">Отмененные</span>
<span class="stat-value">{{ proposalsStats.canceled }}</span>
</div>
<div class="stat-item">
<span class="stat-label">Готовы к исполнению</span>
<span class="stat-value">{{ proposalsStats.readyForExecution }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- Модули -->
<div class="modules-section">
<h2>Модули</h2>
<div class="modules-grid">
<div class="modules-card">
<h3>Активные модули</h3>
<div class="modules-list">
<div v-if="modules.length === 0" class="no-modules">
<p>Нет активных модулей</p>
</div>
<div
v-for="module in modules"
:key="module.id"
class="module-item"
>
<div class="module-info">
<span class="module-id">{{ module.id }}</span>
<span class="module-address">{{ formatAddress(module.address) }}</span>
</div>
<div class="module-status">
<span class="status-badge active">Активен</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Мульти-чейн -->
<div class="multichain-section">
<h2>Мульти-чейн</h2>
<div class="multichain-grid">
<div class="multichain-card">
<h3>Поддерживаемые сети</h3>
<div class="chains-list">
<div
v-for="chainId in multichain.supportedChains"
:key="chainId"
class="chain-item"
>
<span class="chain-name">{{ getChainName(chainId) }}</span>
<span class="chain-id">ID: {{ chainId }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- Топ держатели -->
<div class="holders-section">
<h2>Топ держатели токенов</h2>
<div class="holders-grid">
<div class="holders-card">
<div class="holders-list">
<div
v-for="(holder, index) in topHolders"
:key="holder.address"
class="holder-item"
>
<div class="holder-rank">#{{ index + 1 }}</div>
<div class="holder-info">
<div class="holder-address">{{ formatAddress(holder.address) }}</div>
<div class="holder-balance">{{ formatNumber(holder.balance) }} токенов</div>
</div>
<div class="holder-percentage">{{ holder.percentage.toFixed(2) }}%</div>
</div>
<div v-if="topHolders.length === 0" class="no-holders">
<p>Нет данных о держателях токенов</p>
</div>
</div>
</div>
</div>
</div>
</div>
</BaseLayout>
</template>
<script setup>
import { ref, defineProps, defineEmits, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import BaseLayout from '../../components/BaseLayout.vue';
import api from '../../api/axios';
// Определяем props
const props = defineProps({
isAuthenticated: Boolean,
identities: Array,
tokenBalances: Object,
isLoadingTokens: Boolean
});
// Определяем emits
const emit = defineEmits(['auth-action-completed']);
const router = useRouter();
const route = useRoute();
// Получаем адрес DLE из URL параметров
const dleAddress = ref(route.query.address || '');
// Функция возврата к блокам управления
const goBackToBlocks = () => {
if (dleAddress.value) {
router.push(`/management/dle-blocks?address=${dleAddress.value}`);
} else {
router.push('/management');
}
};
// Состояние
const selectedDle = ref(null);
const isLoadingDle = ref(false);
// Данные аналитики
const tokenomics = ref({
totalSupply: 0,
holdersCount: 0,
topHolderAddress: '',
topHolderPercentage: 0
});
const governance = ref({
totalProposals: 0,
executedProposals: 0,
defeatedProposals: 0,
quorumPercentage: 0,
supportedChainsCount: 0,
currentChainId: 0
});
const proposalsStats = ref({
pending: 0,
succeeded: 0,
defeated: 0,
executed: 0,
canceled: 0,
readyForExecution: 0
});
const modules = ref([]);
const multichain = ref({
supportedChains: []
});
const topHolders = ref([]);
// Загрузка данных DLE
async function loadDleData() {
try {
isLoadingDle.value = true;
if (!dleAddress.value) {
console.error('Адрес DLE не указан');
return;
}
console.log('[AnalyticsView] Загрузка данных DLE:', dleAddress.value);
// Читаем данные из блокчейна
const response = await api.post('/dle-core/read-dle-info', {
dleAddress: dleAddress.value
});
if (response.data.success) {
selectedDle.value = response.data.data;
console.log('[AnalyticsView] Данные DLE загружены:', selectedDle.value);
// Загружаем все аналитические данные
await Promise.all([
loadTokenomics(),
loadGovernance(),
loadProposalsStats(),
loadModules(),
loadMultichain(),
loadTopHolders()
]);
} else {
console.error('[AnalyticsView] Ошибка загрузки DLE:', response.data.error);
}
} catch (error) {
console.error('[AnalyticsView] Ошибка загрузки DLE:', error);
} finally {
isLoadingDle.value = false;
}
}
// Загрузка токеномики
async function loadTokenomics() {
try {
const response = await api.post('/dle-tokens/get-total-supply', {
dleAddress: dleAddress.value
});
if (response.data.success) {
tokenomics.value.totalSupply = response.data.data.totalSupply;
// Получаем держателей токенов
const holdersResponse = await api.post('/dle-tokens/get-token-holders', {
dleAddress: dleAddress.value
});
if (holdersResponse.data.success) {
const holders = holdersResponse.data.data.holders;
tokenomics.value.holdersCount = holders.length;
if (holders.length > 0) {
const topHolder = holders[0];
tokenomics.value.topHolderAddress = topHolder.address;
tokenomics.value.topHolderPercentage = topHolder.percentage;
}
}
}
} catch (error) {
console.error('[AnalyticsView] Ошибка загрузки токеномики:', error);
}
}
// Загрузка данных управления
async function loadGovernance() {
try {
const response = await api.post('/dle-core/get-governance-params', {
dleAddress: dleAddress.value
});
if (response.data.success) {
const data = response.data.data;
governance.value.quorumPercentage = data.quorumPct;
governance.value.currentChainId = data.chainId;
governance.value.supportedChainsCount = data.supportedCount;
}
// Получаем количество предложений
const proposalsResponse = await api.post('/dle-proposals/get-proposals-count', {
dleAddress: dleAddress.value
});
if (proposalsResponse.data.success) {
governance.value.totalProposals = proposalsResponse.data.data.count;
}
// Получаем статистику предложений
const listResponse = await api.post('/dle-proposals/get-proposals', {
dleAddress: dleAddress.value
});
if (listResponse.data.success) {
const proposals = listResponse.data.data.proposals || [];
let executed = 0;
let defeated = 0;
for (const proposal of proposals) {
if (proposal.executed) executed++;
else if (proposal.state === 2) defeated++; // Defeated
}
governance.value.executedProposals = executed;
governance.value.defeatedProposals = defeated;
}
} catch (error) {
console.error('[AnalyticsView] Ошибка загрузки управления:', error);
}
}
// Загрузка статистики предложений
async function loadProposalsStats() {
try {
const response = await api.post('/dle-proposals/get-proposals', {
dleAddress: dleAddress.value
});
if (response.data.success) {
const proposals = response.data.data.proposals || [];
const stats = {
pending: 0,
succeeded: 0,
defeated: 0,
executed: 0,
canceled: 0,
readyForExecution: 0
};
for (const proposal of proposals) {
// Определяем статус предложения по той же логике что и в DleProposalsView
let status = 'active';
const now = Math.floor(Date.now() / 1000);
const deadline = proposal.deadline || 0;
if (proposal.canceled) {
status = 'canceled';
} else if (proposal.executed) {
status = 'executed';
} else if (deadline > 0 && now >= deadline) {
// Если дедлайн истек, определяем результат по голосам
const forVotes = Number(proposal.forVotes) || 0;
const againstVotes = Number(proposal.againstVotes) || 0;
if (forVotes > againstVotes) {
status = 'succeeded';
} else {
status = 'defeated';
}
} else {
// Если дедлайн не истек, но есть голоса, определяем текущий статус
const forVotes = Number(proposal.forVotes) || 0;
const againstVotes = Number(proposal.againstVotes) || 0;
if (forVotes > 0 || againstVotes > 0) {
if (forVotes > againstVotes) {
status = 'succeeded';
} else if (againstVotes > forVotes) {
status = 'defeated';
}
}
}
switch (status) {
case 'active': stats.pending++; break;
case 'succeeded': stats.succeeded++; break;
case 'defeated': stats.defeated++; break;
case 'executed': stats.executed++; break;
case 'canceled': stats.canceled++; break;
default: stats.pending++; break;
}
}
proposalsStats.value = stats;
}
} catch (error) {
console.error('[AnalyticsView] Ошибка загрузки статистики предложений:', error);
}
}
// Загрузка модулей
async function loadModules() {
try {
const response = await api.post('/dle-modules/get-all-modules', {
dleAddress: dleAddress.value
});
if (response.data.success) {
modules.value = response.data.data.modules || [];
}
} catch (error) {
console.error('[AnalyticsView] Ошибка загрузки модулей:', error);
}
}
// Загрузка мульти-чейн данных
async function loadMultichain() {
try {
const response = await api.post('/dle-multichain/get-supported-chains', {
dleAddress: dleAddress.value
});
if (response.data.success) {
multichain.value.supportedChains = response.data.data.chains || [];
}
} catch (error) {
console.error('[AnalyticsView] Ошибка загрузки мульти-чейн данных:', error);
}
}
// Загрузка топ держателей
async function loadTopHolders() {
try {
const response = await api.post('/dle-tokens/get-token-holders', {
dleAddress: dleAddress.value
});
if (response.data.success) {
topHolders.value = response.data.data.holders || [];
}
} catch (error) {
console.error('[AnalyticsView] Ошибка загрузки топ держателей:', error);
}
}
// Методы
const formatAddress = (address) => {
if (!address) return '';
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
};
const formatNumber = (number) => {
if (!number) return '0';
return Number(number).toLocaleString();
};
const formatDate = (timestamp) => {
if (!timestamp) return 'Не указано';
return new Date(Number(timestamp) * 1000).toLocaleDateString('ru-RU');
};
const getChainName = (chainId) => {
const chains = {
1: 'Ethereum Mainnet',
11155111: 'Sepolia Testnet',
17000: 'Holesky Testnet',
84532: 'Base Sepolia Testnet',
80002: 'Polygon Amoy Testnet',
421614: 'Arbitrum Sepolia Testnet',
137: 'Polygon',
56: 'BSC',
42161: 'Arbitrum One'
};
return chains[chainId] || `Chain ID: ${chainId}`;
};
// Загружаем данные при монтировании компонента
onMounted(() => {
if (dleAddress.value) {
loadDleData();
}
});
</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;
}
/* Секции */
.info-section,
.tokenomics-section,
.governance-section,
.proposals-section,
.modules-section,
.multichain-section,
.holders-section {
margin-bottom: 40px;
}
.info-section h2,
.tokenomics-section h2,
.governance-section h2,
.proposals-section h2,
.modules-section h2,
.multichain-section h2,
.holders-section h2 {
color: var(--color-primary);
margin-bottom: 20px;
font-size: 1.8rem;
}
/* Основная информация */
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.info-card {
background: #f8f9fa;
padding: 20px;
border-radius: var(--radius-lg);
border-left: 4px solid var(--color-primary);
}
.info-card h3 {
color: var(--color-primary);
margin-bottom: 10px;
font-size: 1rem;
text-transform: uppercase;
font-weight: 600;
}
.info-value {
font-size: 1.2rem;
font-weight: 600;
margin: 0;
color: var(--color-grey-dark);
}
.status-active {
color: #28a745 !important;
}
.status-inactive {
color: #dc3545 !important;
}
/* Токеномика */
.tokenomics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.tokenomics-card {
background: white;
padding: 25px;
border-radius: var(--radius-lg);
border: 1px solid #e9ecef;
text-align: center;
}
.tokenomics-card h3 {
color: var(--color-primary);
margin-bottom: 15px;
font-size: 1.2rem;
}
.tokenomics-value {
font-size: 2.5rem;
font-weight: 700;
color: var(--color-primary);
margin: 10px 0;
}
.tokenomics-label {
color: var(--color-grey-dark);
font-size: 0.9rem;
margin: 0;
}
/* Управление */
.governance-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.governance-card {
background: white;
padding: 20px;
border-radius: var(--radius-lg);
border: 1px solid #e9ecef;
text-align: center;
}
.governance-card h3 {
color: var(--color-primary);
margin-bottom: 10px;
font-size: 1rem;
}
.governance-value {
font-size: 2rem;
font-weight: 700;
color: var(--color-secondary);
margin: 0;
}
/* Предложения */
.proposals-grid {
display: grid;
grid-template-columns: 1fr;
gap: 20px;
}
.proposals-card {
background: white;
padding: 25px;
border-radius: var(--radius-lg);
border: 1px solid #e9ecef;
}
.proposals-card h3 {
color: var(--color-primary);
margin-bottom: 20px;
font-size: 1.3rem;
}
.proposals-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.stat-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
background: #f8f9fa;
border-radius: var(--radius-sm);
}
.stat-label {
color: var(--color-grey-dark);
font-size: 0.9rem;
}
.stat-value {
font-weight: 600;
color: var(--color-primary);
font-size: 1.1rem;
}
/* Модули */
.modules-grid {
display: grid;
grid-template-columns: 1fr;
gap: 20px;
}
.modules-card {
background: white;
padding: 25px;
border-radius: var(--radius-lg);
border: 1px solid #e9ecef;
}
.modules-card h3 {
color: var(--color-primary);
margin-bottom: 20px;
font-size: 1.3rem;
}
.modules-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.module-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background: #f8f9fa;
border-radius: var(--radius-sm);
}
.module-info {
display: flex;
flex-direction: column;
gap: 5px;
}
.module-id {
font-weight: 600;
color: var(--color-primary);
font-family: monospace;
}
.module-address {
font-family: monospace;
font-size: 0.9rem;
color: var(--color-grey-dark);
}
.status-badge {
padding: 4px 8px;
border-radius: var(--radius-sm);
font-size: 0.8rem;
font-weight: 600;
}
.status-badge.active {
background: #d4edda;
color: #155724;
}
.no-modules {
text-align: center;
padding: 20px;
color: var(--color-grey-dark);
}
/* Мульти-чейн */
.multichain-grid {
display: grid;
grid-template-columns: 1fr;
gap: 20px;
}
.multichain-card {
background: white;
padding: 25px;
border-radius: var(--radius-lg);
border: 1px solid #e9ecef;
}
.multichain-card h3 {
color: var(--color-primary);
margin-bottom: 20px;
font-size: 1.3rem;
}
.chains-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.chain-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background: #f8f9fa;
border-radius: var(--radius-sm);
}
.chain-name {
font-weight: 600;
color: var(--color-primary);
}
.chain-id {
font-family: monospace;
color: var(--color-grey-dark);
font-size: 0.9rem;
}
/* Держатели */
.holders-grid {
display: grid;
grid-template-columns: 1fr;
gap: 20px;
}
.holders-card {
background: white;
padding: 25px;
border-radius: var(--radius-lg);
border: 1px solid #e9ecef;
}
.holders-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.holder-item {
display: flex;
align-items: center;
gap: 15px;
padding: 15px;
background: #f8f9fa;
border-radius: var(--radius-sm);
}
.holder-rank {
font-weight: 700;
color: var(--color-primary);
min-width: 30px;
}
.holder-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 5px;
}
.holder-address {
font-family: monospace;
font-weight: 600;
color: var(--color-grey-dark);
}
.holder-balance {
font-size: 0.9rem;
color: var(--color-grey-dark);
}
.holder-percentage {
font-weight: 600;
color: var(--color-secondary);
min-width: 60px;
text-align: right;
}
.no-holders {
text-align: center;
padding: 20px;
color: var(--color-grey-dark);
}
/* Адаптивность */
@media (max-width: 768px) {
.info-grid,
.tokenomics-grid,
.governance-grid {
grid-template-columns: 1fr;
}
.proposals-stats {
grid-template-columns: 1fr;
}
.holder-item {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.holder-percentage {
align-self: flex-end;
}
}
</style>