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

This commit is contained in:
2025-09-24 14:46:54 +03:00
parent 76cde4b53d
commit 792282cd75
17 changed files with 631 additions and 330 deletions

View File

@@ -113,39 +113,28 @@
<div class="detail-item">
<strong>Юрисдикция:</strong> {{ dle.jurisdiction }}
</div>
<div class="detail-item">
<strong>Коды ОКВЭД:</strong> {{ dle.okvedCodes?.join(', ') || 'Не указаны' }}
<div class="detail-item" v-if="dle.quorumPercentage">
<strong>Кворум:</strong>
<span class="quorum-info">{{ dle.quorumPercentage }}%</span>
</div>
<div class="detail-item">
<strong>Партнеры:</strong>
<span v-if="dle.partnerBalances && dle.partnerBalances.length > 0">
{{ dle.participantCount || dle.partnerBalances.length }} участников
<div class="partners-details">
<div v-for="(partner, index) in dle.partnerBalances.slice(0, 3)" :key="index" class="partner-info">
<span class="partner-address">{{ shortenAddress(partner.address) }}</span>
<span class="partner-balance">{{ partner.balance }} токенов ({{ partner.percentage.toFixed(1) }}%)</span>
</div>
<div v-if="dle.partnerBalances.length > 3" class="more-partners">
+{{ dle.partnerBalances.length - 3 }} еще
</div>
</div>
</span>
<span v-else>
{{ dle.participantCount || 0 }} участников
</span>
<strong>Коды ОКВЭД:</strong> {{ dle.okvedCodes?.join(', ') || 'Не указаны' }}
</div>
<div class="detail-item">
<strong>Статус:</strong>
<span class="status active">Активен</span>
</div>
<div class="detail-item" v-if="verificationStatuses[dle.dleAddress]">
<strong>Верификация:</strong>
<ul class="verify-list">
<li v-for="(info, chainId) in verificationStatuses[dle.dleAddress].chains" :key="chainId">
Chain {{ chainId }}: {{ info.status || '' }}<span v-if="info.guid"> (guid: {{ info.guid.slice(0,8) }})</span>
</li>
</ul>
<button class="details-btn btn-sm" @click.stop="refreshVerification(dle.dleAddress)">Обновить статус</button>
<div class="detail-item" v-if="dle.totalSupply">
<strong>Общий объем токенов:</strong>
<span class="token-supply">{{ parseFloat(dle.totalSupply).toLocaleString() }} {{ dle.symbol }}</span>
</div>
<div class="detail-item" v-if="dle.logoURI">
<strong>Логотип:</strong>
<span class="logo-info">Установлен</span>
</div>
<div class="detail-item" v-if="dle.creationTimestamp">
<strong>Дата создания:</strong>
<span class="creation-date">{{ formatTimestamp(dle.creationTimestamp) }}</span>
</div>
</div>
@@ -187,8 +176,6 @@ const router = useRouter();
// Состояние для DLE
const deployedDles = ref([]);
const isLoadingDles = ref(false);
const verificationStatuses = ref({}); // { [address]: { address, chains: { [chainId]: { guid, status } } } }
let verifyPollTimer = null;
@@ -308,18 +295,6 @@ async function loadDeployedDles() {
deployedDles.value = dlesWithBlockchainData;
console.log('[ManagementView] Итоговый список DLE:', deployedDles.value);
// Подгружаем статусы верификации для всех адресов
for (const dle of deployedDles.value) {
try {
const st = await api.get(`/dle-v2/verify/status/${dle.dleAddress}`);
if (st.data?.success && st.data.data) {
verificationStatuses.value[dle.dleAddress] = st.data.data;
}
} catch (e) {
// no-op
}
}
} else {
console.error('[ManagementView] Ошибка при загрузке DLE:', response.data.message);
deployedDles.value = [];
@@ -367,6 +342,19 @@ function getExplorerUrl(chainId, address) {
return `${baseUrl}/address/${address}`;
}
function formatTimestamp(timestamp) {
if (!timestamp) return '';
const date = new Date(timestamp * 1000); // Конвертируем из Unix timestamp
return date.toLocaleDateString('ru-RU', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
function openDleOnEtherscan(address) {
window.open(`https://sepolia.etherscan.io/address/${address}`, '_blank');
}
@@ -377,35 +365,6 @@ function openDleManagement(dleAddress) {
}
async function refreshVerification(address) {
try {
const resp = await api.post(`/dle-v2/verify/refresh/${address}`, {});
if (resp.data?.success && resp.data.data) {
verificationStatuses.value[address] = resp.data.data;
}
} catch (e) {
// no-op
}
}
function isTerminalStatus(status) {
if (!status) return false;
const s = String(status).toLowerCase();
return s.includes('pass') || s.includes('verified') || s.startsWith('error');
}
async function pollVerifications() {
try {
const addresses = Object.keys(verificationStatuses.value || {});
for (const addr of addresses) {
const chains = verificationStatuses.value[addr]?.chains || {};
const hasPending = Object.values(chains).some((c) => !isTerminalStatus(c.status));
if (hasPending) {
await refreshVerification(addr);
}
}
} catch {}
}
// function openMultisig() {
// router.push('/management/multisig');
@@ -416,14 +375,6 @@ async function pollVerifications() {
onMounted(() => {
loadDeployedDles();
verifyPollTimer = setInterval(pollVerifications, 15000);
});
onBeforeUnmount(() => {
if (verifyPollTimer) {
clearInterval(verifyPollTimer);
verifyPollTimer = null;
}
});
</script>
@@ -813,55 +764,31 @@ onBeforeUnmount(() => {
align-self: flex-start;
}
/* Стили для отображения партнеров */
.partners-details {
margin-top: 0.5rem;
padding: 0.5rem;
background: #f8f9fa;
border-radius: 6px;
border-left: 3px solid var(--color-primary);
}
.partner-info {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.25rem 0;
font-size: 0.875rem;
}
.partner-info:not(:last-child) {
border-bottom: 1px solid #e9ecef;
}
.partner-address {
font-family: 'Courier New', monospace;
color: #495057;
font-weight: 600;
}
.partner-balance {
/* Стили для новых элементов */
.token-supply {
color: var(--color-primary);
font-weight: 600;
}
.more-partners {
text-align: center;
color: #6c757d;
font-style: italic;
font-size: 0.8rem;
padding: 0.25rem 0;
.logo-info {
color: #28a745;
font-weight: 600;
}
.quorum-info {
color: #fd7e14;
font-weight: 600;
}
.creation-date {
color: #6c757d;
font-weight: 500;
}
/* Адаптивность */
@media (max-width: 768px) {
.partner-info {
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
}
.dle-title-section {
flex-direction: column;
align-items: flex-start;

View File

@@ -693,9 +693,17 @@
</div>
<!-- Основная информация DLE -->
<div v-if="dleSettings.name || dleSettings.tokenSymbol" class="preview-section">
<div v-if="dleSettings.name || dleSettings.tokenSymbol || logoPreviewUrl" class="preview-section">
<h4>Основная информация DLE</h4>
<div v-if="logoPreviewUrl" class="preview-item">
<strong>🎨 Логотип:</strong>
<div style="display: flex; align-items: center; gap: 10px; margin-top: 5px;">
<img :src="logoPreviewUrl" alt="Logo preview" style="width: 48px; height: 48px; border-radius: 6px; object-fit: contain; border: 1px solid #e9ecef;" />
<span style="color: #666; font-size: 0.9em;">{{ logoFile?.name || 'ENS аватар' || 'Дефолтный логотип' }}</span>
</div>
</div>
<div v-if="dleSettings.name" class="preview-item">
<strong>📋 Название:</strong> {{ dleSettings.name }}
</div>
@@ -929,6 +937,7 @@
:private-key="unifiedPrivateKey"
:selected-networks="selectedNetworks"
:dle-data="dleSettings"
:logo-uri="getLogoURI()"
:etherscan-api-key="etherscanApiKey"
@deployment-completed="handleDeploymentCompleted"
/>
@@ -2751,6 +2760,19 @@ async function resolveEnsAvatar() {
}
}
// Функция для получения URI логотипа
function getLogoURI() {
if (logoFile.value) {
// Если выбран файл, возвращаем временный URL для предпросмотра
// В реальности файл будет загружен на сервер и получен настоящий URL
return logoPreviewUrl.value || '/uploads/logos/default-token.svg';
} else if (ensResolvedUrl.value) {
return ensResolvedUrl.value;
} else {
return '/uploads/logos/default-token.svg';
}
}
async function submitDeploy() {
try {
// Подготовка данных формы

View File

@@ -289,7 +289,10 @@
<span>{{ chain.name }} ({{ chain.chainId }})</span>
</label>
</div>
<small class="text-muted">Для offchainдействий целевые сети не требуются.</small>
<small class="text-muted">Выберите хотя бы одну целевую сеть для исполнения операции.</small>
<div v-if="showTargetChains && newProposal.targetChains.length === 0" class="form-error">
<small class="text-danger"> Необходимо выбрать хотя бы одну целевую сеть</small>
</div>
</div>
@@ -958,7 +961,8 @@ const isFormValid = computed(() => {
newProposal.value.governanceChainId &&
newProposal.value.operationType &&
newProposal.value.timelockHours >= 0 &&
validateOperationParams()
validateOperationParams() &&
validateTargetChains()
);
});
@@ -1069,6 +1073,14 @@ function validateOperationParams() {
}
}
function validateTargetChains() {
// Если показываем целевые сети, то должна быть выбрана хотя бы одна
if (showTargetChains.value) {
return newProposal.value.targetChains.length > 0;
}
return true;
}
function validateAddress(address) {
if (!address) return false;
// Проверяем формат Ethereum адреса
@@ -1582,22 +1594,22 @@ function encodeBurnOperation(from, amount) {
}
function encodeUpdateDLEInfoOperation(name, symbol, location, coordinates, jurisdiction, oktmo, kpp) {
// Селектор для updateDLEInfo(string,string,string,string,uint256,uint256,string[],uint256)
const selector = '0x' + ethers.keccak256(ethers.toUtf8Bytes('updateDLEInfo(string,string,string,string,uint256,uint256,string[],uint256)')).slice(0, 10);
// Селектор для _updateDLEInfo(string,string,string,string,uint256,string[],uint256)
const selector = '0x' + ethers.keccak256(ethers.toUtf8Bytes('_updateDLEInfo(string,string,string,string,uint256,string[],uint256)')).slice(0, 10);
// Кодируем параметры
const abiCoder = new ethers.AbiCoder();
const encodedData = abiCoder.encode(
['string', 'string', 'string', 'string', 'uint256', 'uint256', 'string[]', 'uint256'],
[name, symbol, location, coordinates, jurisdiction, oktmo, [], kpp] // okvedCodes пока пустой массив
['string', 'string', 'string', 'string', 'uint256', 'string[]', 'uint256'],
[name, symbol, location, coordinates, jurisdiction, [], kpp] // okvedCodes пока пустой массив
);
return selector + encodedData.slice(2);
}
function encodeUpdateQuorumOperation(quorumPercentage) {
// Селектор для updateQuorumPercentage(uint256)
const selector = '0x' + ethers.keccak256(ethers.toUtf8Bytes('updateQuorumPercentage(uint256)')).slice(0, 10);
// Селектор для _updateQuorumPercentage(uint256)
const selector = '0x' + ethers.keccak256(ethers.toUtf8Bytes('_updateQuorumPercentage(uint256)')).slice(0, 10);
// Кодируем параметр
const abiCoder = new ethers.AbiCoder();
@@ -1607,8 +1619,8 @@ function encodeUpdateQuorumOperation(quorumPercentage) {
}
function encodeUpdateChainOperation(chainId) {
// Селектор для updateCurrentChainId(uint256)
const selector = '0x' + ethers.keccak256(ethers.toUtf8Bytes('updateCurrentChainId(uint256)')).slice(0, 10);
// Селектор для _updateCurrentChainId(uint256)
const selector = '0x' + ethers.keccak256(ethers.toUtf8Bytes('_updateCurrentChainId(uint256)')).slice(0, 10);
// Кодируем параметр
const abiCoder = new ethers.AbiCoder();
@@ -2656,4 +2668,43 @@ onUnmounted(() => {
color: #28a745;
font-weight: 500;
}
/* Стили для ошибок валидации */
.form-error {
margin-top: 0.5rem;
padding: 0.5rem;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 4px;
}
.text-danger {
color: #dc3545 !important;
}
.targets-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.5rem;
margin-bottom: 1rem;
}
.target-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
border: 1px solid #e9ecef;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.target-item:hover {
background-color: #f8f9fa;
}
.target-item input[type="checkbox"] {
margin: 0;
}
</style>

View File

@@ -840,14 +840,14 @@ async function loadModules() {
active: m.isActive,
id: m.moduleId
})),
modulesInitialized: modulesResponse.data.modulesInitialized,
requiresGovernance: modulesResponse.data.requiresGovernance,
totalModules: modulesResponse.data.totalModules,
activeModules: modulesResponse.data.activeModules
});
// Обновляем счетчики
if (modulesResponse.data.modulesInitialized === false) {
console.log('[ModulesView] Модули для DLE не инициализированы');
if (modulesResponse.data.requiresGovernance === true) {
console.log('[ModulesView] Модули требуют инициализации через governance');
}
// Обновляем время последнего обновления