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

This commit is contained in:
2025-08-15 22:36:50 +03:00
parent 5238e1ee55
commit 3765c65a18
29 changed files with 904 additions and 651 deletions

View File

@@ -22,10 +22,19 @@
:isAuthenticated="auth.isAuthenticated.value"
:identities="auth.identities.value"
:tokenBalances="tokenBalances"
:isLoadingTokens="isLoadingTokens"
:isLoadingTokens="isLoadingTokens"
:formattedLastUpdate="formattedLastUpdate"
@auth-action-completed="handleAuthActionCompleted"
/>
</RouterView>
<!-- Отладочная информация -->
<div v-if="false" style="position: fixed; top: 10px; right: 10px; background: white; padding: 10px; border: 1px solid black; z-index: 9999;">
<h4>Debug Info:</h4>
<p>isAuthenticated: {{ auth.isAuthenticated.value }}</p>
<p>tokenBalances: {{ JSON.stringify(tokenBalances) }}</p>
<p>isLoadingTokens: {{ isLoadingTokens }}</p>
</div>
</div>
</template>
@@ -33,7 +42,7 @@
import { ref, watch, onMounted, computed, onUnmounted } from 'vue';
import { RouterView } from 'vue-router';
import { useAuth, provideAuth } from './composables/useAuth';
import { fetchTokenBalances } from './services/tokens';
import { useTokenBalancesWebSocket } from './composables/useTokenBalancesWebSocket';
import eventBus from './utils/eventBus';
import wsClient from './utils/websocket';
@@ -46,12 +55,26 @@
// Состояние загрузки
const isLoading = ref(false);
// Проверка наличия MetaMask
const isMetaMaskAvailable = ref(false);
// Использование composable для аутентификации
const auth = useAuth();
// --- Логика загрузки баланса токенов ---
const tokenBalances = ref({});
const isLoadingTokens = ref(false);
// --- Логика загрузки баланса токенов через WebSocket ---
// Предоставляем auth контекст
provideAuth();
// Инициализируем WebSocket composable
const {
tokenBalances,
isLoadingTokens,
lastUpdateTime,
formattedLastUpdate,
requestTokenBalances,
startAutoUpdate,
stopAutoUpdate
} = useTokenBalancesWebSocket();
const identities = computed(() => auth.identities.value);
@@ -66,60 +89,57 @@
return identity ? identity.provider_id : null;
};
const refreshTokenBalances = async () => {
const refreshTokenBalances = () => {
if (!hasIdentityType('wallet') || !auth.isAuthenticated.value) {
tokenBalances.value = {}; // Очищаем, если нет кошелька или не авторизован
console.log('[App] Нет кошелька или не авторизован');
return;
}
isLoadingTokens.value = true;
try {
const walletAddress = getIdentityValue('wallet');
// console.log('[App] Обновление балансов для адреса:', walletAddress);
const balances = await fetchTokenBalances(walletAddress);
// console.log('[App] Полученные балансы:', balances);
tokenBalances.value = balances || {};
} catch (error) {
// console.error('[App] Ошибка при получении балансов:', error);
tokenBalances.value = {};
} finally {
isLoadingTokens.value = false;
}
const walletAddress = getIdentityValue('wallet');
console.log('[App] Запрашиваем обновление балансов через WebSocket для:', walletAddress, 'userId:', auth.userId.value);
requestTokenBalances(walletAddress, auth.userId.value);
};
// Следим за изменениями в идентификаторах
watch(identities, (newIdentities, oldIdentities) => {
console.log('[App] identities changed:', { newIdentities, oldIdentities });
if (auth.isAuthenticated.value) {
const newWalletId = getIdentityValue('wallet');
const oldWalletIdentity = oldIdentities ? oldIdentities.find(id => id.provider === 'wallet') : null;
const oldWalletId = oldWalletIdentity ? oldWalletIdentity.provider_id : null;
console.log('[App] wallet IDs comparison:', { newWalletId, oldWalletId });
if (newWalletId !== oldWalletId) {
// console.log('[App] Обнаружено изменение идентификатора кошелька, обновляем балансы');
refreshTokenBalances();
} else if (hasIdentityType('wallet') && Object.keys(tokenBalances.value).length === 0 && !isLoadingTokens.value) {
// Если кошелек есть, но баланс пустой и не грузится - пробуем загрузить
// console.log('[App] Кошелек есть, но баланс пуст, пытаемся загрузить.');
refreshTokenBalances();
console.log('[App] Обнаружено изменение идентификатора кошелька, обновляем балансы');
if (newWalletId) {
startAutoUpdate(newWalletId, auth.userId.value);
} else {
stopAutoUpdate();
}
}
}
}, { deep: true });
// Мониторинг изменений состояния аутентификации
watch(auth.isAuthenticated, (isAuth) => {
// console.log('[App] Состояние аутентификации изменилось:', isAuth);
console.log('[App] Состояние аутентификации изменилось:', isAuth);
if (isAuth) {
// Убираем задержку, полагаемся на watch(identities) или прямо вызываем
// setTimeout(refreshTokenBalances, 500);
refreshTokenBalances(); // Вызываем сразу, если нужно обновить при смене auth
} else {
// Очищаем баланс при выходе
tokenBalances.value = {};
}
});
// Проверка наличия MetaMask при загрузке
const checkMetaMaskAvailability = () => {
try {
isMetaMaskAvailable.value = !!window.ethereum && window.ethereum.isMetaMask;
console.log('[App] MetaMask доступен:', isMetaMaskAvailable.value);
} catch (error) {
console.error('[App] Ошибка проверки MetaMask:', error);
isMetaMaskAvailable.value = false;
}
};
// --- Возвращаем и улучшаем функцию-обработчик ---
const handleAuthActionCompleted = async () => {
// console.log('[App] Auth action completed, triggering updates...');
@@ -129,8 +149,8 @@
await auth.checkAuth();
// console.log('[App] auth.checkAuth() completed. isAuthenticated:', auth.isAuthenticated.value);
// 2. Обновляем баланс (использует обновленные identities)
await refreshTokenBalances();
// 2. Обновляем баланс через WebSocket
refreshTokenBalances();
// console.log('[App] refreshTokenBalances() completed.');
// 3. Явно оповещаем компоненты об изменении состояния авторизации
@@ -152,7 +172,13 @@
// Первичная загрузка баланса при монтировании, если пользователь уже авторизован
onMounted(() => {
console.log('[App] onMounted - auth.isAuthenticated:', auth.isAuthenticated.value);
console.log('[App] onMounted - identities:', auth.identities.value);
// Проверяем наличие MetaMask
checkMetaMaskAvailability();
if (auth.isAuthenticated.value) {
console.log('[App] onMounted - вызываем refreshTokenBalances');
refreshTokenBalances();
}
@@ -164,27 +190,7 @@
}
});
// Подписываемся на WebSocket события для токенов
wsClient.onAuthTokenAdded(() => {
console.log('[App] WebSocket: токен добавлен, обновляем балансы');
if (auth.isAuthenticated.value) {
refreshTokenBalances();
}
});
wsClient.onAuthTokenDeleted(() => {
console.log('[App] WebSocket: токен удален, обновляем балансы');
if (auth.isAuthenticated.value) {
refreshTokenBalances();
}
});
wsClient.onAuthTokenUpdated(() => {
console.log('[App] WebSocket: токен обновлен, обновляем балансы');
if (auth.isAuthenticated.value) {
refreshTokenBalances();
}
});
// WebSocket события для токенов обрабатываются в useTokenBalancesWebSocket
// Отписываемся при размонтировании компонента
onUnmounted(() => {
@@ -193,8 +199,6 @@
}
});
});
provideAuth();
</script>
<style>

View File

@@ -85,6 +85,13 @@ provide('identities', computed(() => props.identities));
provide('tokenBalances', computed(() => props.tokenBalances));
provide('isLoadingTokens', computed(() => props.isLoadingTokens));
// Отладочная информация
console.log('[BaseLayout] Props received:', {
isAuthenticated: props.isAuthenticated,
tokenBalances: props.tokenBalances,
isLoadingTokens: props.isLoadingTokens
});
// Callback после успешной аутентификации/привязки через Email/Telegram
const handleAuthFlowSuccess = (authType) => {
// console.log(`[BaseLayout] Auth flow success: ${authType}`);
@@ -151,7 +158,21 @@ const handleWalletAuth = async () => {
}
} catch (error) {
// console.error('[BaseLayout] Ошибка при подключении кошелька:', error);
showErrorMessage('Произошла ошибка при подключении кошелька');
// Улучшенная обработка ошибок MetaMask
let errorMessage = 'Произошла ошибка при подключении кошелька';
if (error.message && error.message.includes('MetaMask extension not found')) {
errorMessage = 'Расширение MetaMask не найдено. Пожалуйста, установите MetaMask и обновите страницу.';
} else if (error.message && error.message.includes('Failed to connect to MetaMask')) {
errorMessage = 'Не удалось подключиться к MetaMask. Проверьте, что расширение установлено и активно.';
} else if (error.message && error.message.includes('Браузерный кошелек не установлен')) {
errorMessage = 'Браузерный кошелек не установлен. Пожалуйста, установите MetaMask.';
} else if (error.message) {
errorMessage = error.message;
}
showErrorMessage(errorMessage);
} finally {
isConnectingWallet.value = false;
}

View File

@@ -117,12 +117,14 @@
Загрузка балансов...
</div>
<div v-else-if="!tokenBalances || tokenBalances.length === 0" class="token-no-data">
Баланс не доступен
Баланс не доступен (tokenBalances: {{ tokenBalances }}, length: {{ tokenBalances?.length }})
</div>
<div v-else>
<div class="token-balance-header">
<small class="last-update">Обновлено: {{ formattedLastUpdate }}</small>
<small class="debug-info">Debug: {{ tokenBalances.length }} токенов</small>
</div>
<div v-for="(token, index) in tokenBalances.data || []" :key="token.tokenAddress ? token.tokenAddress : 'token-' + index" class="token-balance-row">
<div v-for="(token, index) in tokenBalances" :key="token.tokenAddress ? token.tokenAddress : 'token-' + index" class="token-balance-row">
<span class="token-name">{{ token.tokenName }}</span>
<span class="token-network">{{ token.network }}</span>
<span class="token-amount">{{ isNaN(Number(token.balance)) ? '—' : Number(token.balance).toLocaleString() }}</span>
@@ -172,9 +174,10 @@ const props = defineProps({
isAuthenticated: Boolean,
telegramAuth: Object,
emailAuth: Object,
tokenBalances: Object,
tokenBalances: Array,
identities: Array,
isLoadingTokens: Boolean
isLoadingTokens: Boolean,
formattedLastUpdate: String
});
const emit = defineEmits(['update:modelValue', 'wallet-auth', 'disconnect-wallet', 'telegram-auth', 'email-auth', 'cancel-email-auth']);
@@ -239,11 +242,20 @@ const handleDeleteIdentity = async (provider, providerId) => {
// Добавляем watch для отслеживания props
watch(() => props.tokenBalances, (newVal, oldVal) => {
// console.log('[Sidebar] tokenBalances prop changed:', JSON.stringify(newVal));
console.log('[Sidebar] tokenBalances prop changed:', JSON.stringify(newVal));
}, { deep: true });
watch(() => props.isLoadingTokens, (newVal, oldVal) => {
// console.log(`[Sidebar] isLoadingTokens prop changed: ${newVal}`);
console.log(`[Sidebar] isLoadingTokens prop changed: ${newVal}`);
});
// Добавляем отладочную информацию при монтировании
onMounted(() => {
console.log('[Sidebar] Mounted with props:', {
isAuthenticated: props.isAuthenticated,
tokenBalances: props.tokenBalances,
isLoadingTokens: props.isLoadingTokens
});
});
</script>

View File

@@ -67,7 +67,21 @@
}
} catch (err) {
// console.error('Error connecting wallet:', err);
error.value = err.message || 'Произошла ошибка при подключении кошелька';
// Улучшенная обработка ошибок MetaMask
let errorMessage = 'Произошла ошибка при подключении кошелька';
if (err.message && err.message.includes('MetaMask extension not found')) {
errorMessage = 'Расширение MetaMask не найдено. Пожалуйста, установите MetaMask и обновите страницу.';
} else if (err.message && err.message.includes('Failed to connect to MetaMask')) {
errorMessage = 'Не удалось подключиться к MetaMask. Проверьте, что расширение установлено и активно.';
} else if (err.message && err.message.includes('Браузерный кошелек не установлен')) {
errorMessage = 'Браузерный кошелек не установлен. Пожалуйста, установите MetaMask.';
} else if (err.message) {
errorMessage = err.message;
}
error.value = errorMessage;
} finally {
isLoading.value = false;
}

View File

@@ -0,0 +1,146 @@
/**
* 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 { ref, computed, onMounted, onUnmounted } from 'vue';
import wsClient from '../utils/websocket';
export function useTokenBalancesWebSocket() {
// Состояние балансов
const tokenBalances = ref([]);
const isLoadingTokens = ref(false);
const lastUpdateTime = ref(null);
// Запрос балансов через WebSocket
const requestTokenBalances = (address, userId) => {
if (!address) {
console.log('[useTokenBalancesWebSocket] Нет адреса для запроса');
return;
}
console.log('[useTokenBalancesWebSocket] Запрашиваем балансы для:', address, 'userId:', userId);
isLoadingTokens.value = true;
const message = {
type: 'request_token_balances',
address: address,
userId: userId
};
console.log('[useTokenBalancesWebSocket] Отправляем WebSocket сообщение:', message);
wsClient.ws.send(JSON.stringify(message));
};
// Обработчик ответа с балансами
const handleTokenBalancesResponse = (data) => {
console.log('[useTokenBalancesWebSocket] Получены балансы:', data);
console.log('[useTokenBalancesWebSocket] data.balances:', data.balances);
tokenBalances.value = data.balances || [];
isLoadingTokens.value = false;
lastUpdateTime.value = new Date();
console.log('[useTokenBalancesWebSocket] Обновлен tokenBalances.value:', tokenBalances.value);
};
// Обработчик ошибки
const handleTokenBalancesError = (data) => {
console.error('[useTokenBalancesWebSocket] Ошибка получения балансов:', data.error);
isLoadingTokens.value = false;
tokenBalances.value = [];
};
// Обработчик обновления балансов
const handleTokenBalancesUpdated = (data) => {
console.log('[useTokenBalancesWebSocket] Обновление балансов:', data);
tokenBalances.value = data.balances || [];
lastUpdateTime.value = new Date();
};
// Обработчик изменения конкретного баланса
const handleTokenBalanceChanged = (data) => {
console.log('[useTokenBalancesWebSocket] Изменение баланса токена:', data);
// Обновляем конкретный токен в списке
const tokenIndex = tokenBalances.value.findIndex(
token => token.tokenAddress === data.tokenAddress && token.network === data.network
);
if (tokenIndex !== -1) {
tokenBalances.value[tokenIndex].balance = data.balance;
lastUpdateTime.value = new Date();
}
};
// Вычисляемое свойство для форматированного времени обновления
const formattedLastUpdate = computed(() => {
if (!lastUpdateTime.value) return 'Не обновлялось';
return lastUpdateTime.value.toLocaleTimeString();
});
// Автоматическое обновление каждые 5 минут
let autoUpdateInterval = null;
const startAutoUpdate = (address, userId) => {
stopAutoUpdate();
// Первоначальный запрос
if (address) {
requestTokenBalances(address, userId);
}
// Автообновление каждые 5 минут
autoUpdateInterval = setInterval(() => {
if (address) {
console.log('[useTokenBalancesWebSocket] Автообновление балансов');
requestTokenBalances(address, userId);
}
}, 5 * 60 * 1000); // 5 минут
};
const stopAutoUpdate = () => {
if (autoUpdateInterval) {
clearInterval(autoUpdateInterval);
autoUpdateInterval = null;
}
};
// Подписка на WebSocket события
onMounted(() => {
// Подписываемся на события WebSocket
wsClient.on('token_balances_response', handleTokenBalancesResponse);
wsClient.on('token_balances_error', handleTokenBalancesError);
wsClient.on('token_balances_updated', handleTokenBalancesUpdated);
wsClient.on('token_balance_changed', handleTokenBalanceChanged);
});
onUnmounted(() => {
// Отписываемся от событий
wsClient.off('token_balances_response');
wsClient.off('token_balances_error');
wsClient.off('token_balances_updated');
wsClient.off('token_balance_changed');
// Останавливаем автообновление
stopAutoUpdate();
});
return {
// Состояние
tokenBalances: computed(() => tokenBalances.value),
isLoadingTokens: computed(() => isLoadingTokens.value),
lastUpdateTime: computed(() => lastUpdateTime.value),
formattedLastUpdate,
// Методы
requestTokenBalances,
startAutoUpdate,
stopAutoUpdate
};
}

View File

@@ -43,5 +43,27 @@ app.use(ElementPlus);
// console.log('API URL:', axios.defaults.baseURL);
// console.log('main.js: Starting application with router');
// Глобальная обработка ошибок MetaMask
window.addEventListener('error', (event) => {
if (event.error && event.error.message &&
(event.error.message.includes('MetaMask extension not found') ||
event.error.message.includes('Failed to connect to MetaMask'))) {
console.warn('[MetaMask] Ошибка MetaMask перехвачена:', event.error.message);
// Предотвращаем показ ошибки в консоли
event.preventDefault();
}
});
// Обработка необработанных промисов
window.addEventListener('unhandledrejection', (event) => {
if (event.reason && event.reason.message &&
(event.reason.message.includes('MetaMask extension not found') ||
event.reason.message.includes('Failed to connect to MetaMask'))) {
console.warn('[MetaMask] Необработанная ошибка MetaMask перехвачена:', event.reason.message);
// Предотвращаем показ ошибки в консоли
event.preventDefault();
}
});
app.mount('#app');
// console.log('main.js: Application with router mounted');

View File

@@ -108,6 +108,30 @@ export async function connectWithWallet() {
return verificationResponse.data;
} catch (error) {
// console.error('Error connecting wallet:', error);
throw error;
// Улучшенная обработка ошибок MetaMask
let errorMessage = 'Произошла ошибка при подключении кошелька.';
if (error.message && error.message.includes('MetaMask extension not found')) {
errorMessage = 'Расширение MetaMask не найдено. Пожалуйста, установите MetaMask и обновите страницу.';
} else if (error.message && error.message.includes('Failed to connect to MetaMask')) {
errorMessage = 'Не удалось подключиться к MetaMask. Проверьте, что расширение установлено и активно.';
} else if (error.code === 4001) {
errorMessage = 'Вы отклонили запрос на подключение в MetaMask.';
} else if (error.message && error.message.includes('No accounts found')) {
errorMessage = 'Аккаунты не найдены. Пожалуйста, разблокируйте MetaMask и попробуйте снова.';
} else if (error.message && error.message.includes('MetaMask not detected')) {
errorMessage = 'MetaMask не обнаружен. Пожалуйста, установите расширение MetaMask.';
} else if (error.response && error.response.data && error.response.data.error) {
errorMessage = error.response.data.error;
} else if (error.message) {
errorMessage = error.message;
}
// Возвращаем объект с ошибкой вместо выброса исключения
return {
success: false,
error: errorMessage
};
}
}

View File

@@ -25,7 +25,21 @@ export async function checkWalletConnection() {
};
} catch (error) {
console.error('Ошибка подключения к кошельку:', error);
throw error;
// Улучшенная обработка ошибок MetaMask
let errorMessage = 'Ошибка подключения к кошельку.';
if (error.message && error.message.includes('MetaMask extension not found')) {
errorMessage = 'Расширение MetaMask не найдено. Пожалуйста, установите MetaMask и обновите страницу.';
} else if (error.message && error.message.includes('Failed to connect to MetaMask')) {
errorMessage = 'Не удалось подключиться к MetaMask. Проверьте, что расширение установлено и активно.';
} else if (error.message && error.message.includes('Браузерный кошелек не установлен')) {
errorMessage = 'Браузерный кошелек не установлен. Пожалуйста, установите MetaMask.';
} else if (error.message) {
errorMessage = error.message;
}
throw new Error(errorMessage);
}
}

View File

@@ -151,8 +151,16 @@ export const connectWallet = async () => {
// Формируем понятное сообщение об ошибке
let errorMessage = 'Произошла ошибка при подключении кошелька.';
if (error.code === 4001) {
if (error.message && error.message.includes('MetaMask extension not found')) {
errorMessage = 'Расширение MetaMask не найдено. Пожалуйста, установите MetaMask и обновите страницу.';
} else if (error.message && error.message.includes('Failed to connect to MetaMask')) {
errorMessage = 'Не удалось подключиться к MetaMask. Проверьте, что расширение установлено и активно.';
} else if (error.code === 4001) {
errorMessage = 'Вы отклонили запрос на подпись в MetaMask.';
} else if (error.message && error.message.includes('No accounts found')) {
errorMessage = 'Аккаунты не найдены. Пожалуйста, разблокируйте MetaMask и попробуйте снова.';
} else if (error.message && error.message.includes('MetaMask not detected')) {
errorMessage = 'MetaMask не обнаружен. Пожалуйста, установите расширение MetaMask.';
} else if (error.response && error.response.data && error.response.data.error) {
errorMessage = error.response.data.error;
} else if (error.message) {

View File

@@ -260,7 +260,7 @@
<h5>Добавленные коды ОКВЭД:</h5>
<ul class="codes-list">
<li v-for="(code, index) in dleSettings.selectedOkved" :key="index" class="code-item">
<span>{{ getOkvedTitle(code) }}</span>
<span>{{ code }}</span>
<button
type="button"
class="btn btn-danger btn-sm"
@@ -325,37 +325,7 @@
<small class="form-help">3-10 символов для токена управления (Governance Token)</small>
</div>
<!-- Картинка токена -->
<div class="form-group">
<label class="form-label" for="tokenImage">Картинка токена:</label>
<div class="token-image-upload">
<input
type="file"
id="tokenImage"
ref="tokenImageInput"
class="form-control"
accept="image/*"
@change="handleTokenImageUpload"
style="display: none;"
>
<div class="upload-area" @click="triggerImageUpload">
<div v-if="!dleSettings.tokenImage" class="upload-placeholder">
<i class="fas fa-image"></i>
<p>Нажмите для выбора картинки токена</p>
<small>Поддерживаются форматы: JPG, PNG, GIF (макс. 1MB, 200x200px)</small>
</div>
<div v-else class="image-preview">
<img :src="dleSettings.tokenImage" alt="Картинка токена" class="token-image">
<div class="image-overlay">
<button type="button" @click.stop="removeTokenImage" class="btn btn-danger btn-sm">
<i class="fas fa-trash"></i> Удалить
</button>
</div>
</div>
</div>
</div>
<small class="form-help">Загрузите картинку для вашего токена (макс. 350 байт в base64, автоматически сжимается до 200x200px)</small>
</div>
@@ -379,13 +349,24 @@
<div class="form-row">
<div class="form-group flex-grow">
<label class="form-label">Адрес кошелька:</label>
<input
type="text"
v-model="partner.address"
class="form-control"
placeholder="0x..."
@input="validateEthereumAddress(partner, index)"
>
<div class="address-input-group">
<input
type="text"
v-model="partner.address"
class="form-control"
placeholder="0x..."
@input="validateEthereumAddress(partner, index)"
>
<button
v-if="index === 0 && address"
@click="useMyWalletAddress"
type="button"
class="btn btn-outline-primary btn-sm"
title="Использовать мой адрес кошелька"
>
<i class="fas fa-wallet"></i> Мой кошелек
</button>
</div>
</div>
<div class="form-group">
<label class="form-label">Количество токенов:</label>
@@ -688,12 +669,7 @@
<strong>🪙 Токен:</strong> {{ dleSettings.tokenSymbol }}
</div>
<div v-if="dleSettings.tokenImage" class="preview-item">
<strong>🖼 Картинка токена:</strong>
<div class="token-image-preview">
<img :src="dleSettings.tokenImage" alt="Картинка токена" class="preview-token-image">
</div>
</div>
</div>
@@ -928,7 +904,6 @@ const dleSettings = reactive({
selectedOkved: [], // ОКВЭД - дополнительные коды деятельности
name: '', // Имя DLE
tokenSymbol: '', // Символ токена
tokenImage: '', // Картинка токена (base64 или URL)
partners: [{ address: '', amount: 1 }], // Партнеры и их доли токенов
governanceQuorum: 51, // Кворум для принятия решений (%)
@@ -1460,7 +1435,7 @@ const loadFormData = () => {
selectedOkved: parsedData.selectedOkved || [],
name: parsedData.name || '',
tokenSymbol: parsedData.tokenSymbol || '',
tokenImage: parsedData.tokenImage || '',
partners: parsedData.partners || [{ address: '', amount: 1 }],
governanceQuorum: parsedData.governanceQuorum || 51,
// Координаты
@@ -1541,7 +1516,7 @@ const clearAllData = () => {
dleSettings.selectedOkved = [];
dleSettings.name = '';
dleSettings.tokenSymbol = '';
dleSettings.tokenImage = ''; // Очищаем картинку токена
dleSettings.partners = [{ address: '', amount: 1 }]; // Сброс к одному пустому партнеру
dleSettings.governanceQuorum = 51; // Сброс кворума к значению по умолчанию
@@ -1815,88 +1790,7 @@ const formatTokenSymbol = () => {
}
};
// Функции для работы с картинкой токена
const tokenImageInput = ref(null);
// Запуск выбора файла
const triggerImageUpload = () => {
tokenImageInput.value?.click();
};
// Обработка загрузки изображения
const handleTokenImageUpload = (event) => {
const file = event.target.files[0];
if (!file) return;
// Проверка типа файла
if (!file.type.startsWith('image/')) {
alert('Пожалуйста, выберите файл изображения (JPG, PNG, GIF)');
return;
}
// Проверка размера файла (максимум 1MB)
const maxSize = 1 * 1024 * 1024; // 1MB
if (file.size > maxSize) {
alert('Размер файла не должен превышать 1MB');
return;
}
// Создаем canvas для сжатия изображения
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = () => {
// Ограничиваем размер изображения
const maxWidth = 200;
const maxHeight = 200;
let { width, height } = img;
if (width > maxWidth) {
height = (height * maxWidth) / width;
width = maxWidth;
}
if (height > maxHeight) {
width = (width * maxHeight) / height;
height = maxHeight;
}
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
// Конвертируем в base64 с сжатием
const compressedDataUrl = canvas.toDataURL('image/jpeg', 0.7); // 70% качество
// Проверяем размер base64 (максимум 350 байт)
const base64Size = compressedDataUrl.length;
if (base64Size > 350) {
alert(`Изображение слишком большое (${base64Size} байт). Максимальный размер: 350 байт. Попробуйте уменьшить размер или качество изображения.`);
return;
}
dleSettings.tokenImage = compressedDataUrl;
// Сохраняем в localStorage
saveFormData();
};
img.onerror = () => {
alert('Ошибка при загрузке изображения');
};
img.src = URL.createObjectURL(file);
};
// Удаление картинки токена
const removeTokenImage = () => {
dleSettings.tokenImage = '';
// Очищаем input
if (tokenImageInput.value) {
tokenImageInput.value.value = '';
}
// Сохраняем в localStorage
saveFormData();
};
// Функция загрузки стран
@@ -2398,9 +2292,13 @@ onMounted(() => {
loadRussianClassifiers();
}
// Автозаполнение первого партнера подключенным кошельком (если данные не были загружены)
if (!dataLoaded && address.value && dleSettings.partners[0] && !dleSettings.partners[0].address) {
dleSettings.partners[0].address = address.value;
// Автозаполнение первого партнера подключенным кошельком
if (address.value && dleSettings.partners[0]) {
// Если адрес пустой или это новый пользователь, подставляем адрес кошелька
if (!dleSettings.partners[0].address || !dataLoaded) {
dleSettings.partners[0].address = address.value;
console.log('Автоматически подставлен адрес кошелька:', address.value);
}
}
// Добавляем слушатель события видимости страницы для обновления списка сетей
@@ -2417,8 +2315,12 @@ onUnmounted(() => {
// Watcher для автоматического обновления адреса первого партнера при подключении кошелька
watch(address, (newAddress) => {
if (newAddress && dleSettings.partners[0] && !dleSettings.partners[0].address) {
dleSettings.partners[0].address = newAddress;
if (newAddress && dleSettings.partners[0]) {
// Подставляем адрес, если поле пустое или пользователь только что подключил кошелек
if (!dleSettings.partners[0].address) {
dleSettings.partners[0].address = newAddress;
console.log('Кошелек подключен, подставлен адрес:', newAddress);
}
}
});
@@ -2469,6 +2371,16 @@ const validateEthereumAddress = (partner, index) => {
}
};
// Функция для подставления адреса кошелька в первого партнера
const useMyWalletAddress = () => {
if (address.value && dleSettings.partners[0]) {
dleSettings.partners[0].address = address.value;
console.log('Подставлен адрес кошелька:', address.value);
} else {
alert('Кошелек не подключен. Пожалуйста, подключите кошелек сначала.');
}
};
// Маскированный приватный ключ для превью (устаревшее)
const maskedPrivateKey = computed(() => {
if (!dleSettings.privateKey) return '';
@@ -2498,7 +2410,7 @@ const deploySmartContracts = async () => {
// Основная информация DLE
name: dleSettings.name,
symbol: dleSettings.tokenSymbol,
tokenImage: dleSettings.tokenImage, // Картинка токена
location: dleSettings.addressData.fullAddress || 'Не указан',
coordinates: dleSettings.coordinates || '0,0',
jurisdiction: parseInt(dleSettings.jurisdiction) || 0,
@@ -2706,6 +2618,21 @@ const validateCoordinates = (coordinates) => {
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2);
}
.address-input-group {
display: flex;
gap: 8px;
align-items: flex-end;
}
.address-input-group .form-control {
flex: 1;
}
.address-input-group .btn {
white-space: nowrap;
flex-shrink: 0;
}
.input-icon-wrapper {
position: relative;
}

View File

@@ -44,6 +44,7 @@
<option value="succeeded">Принятые</option>
<option value="defeated">Отклоненные</option>
<option value="executed">Выполненные</option>
<option value="canceled">Отмененные</option>
</select>
<button
class="btn btn-sm btn-outline-secondary"
@@ -657,11 +658,11 @@ async function loadDleData() {
// Преобразуем данные из API в формат для frontend
proposals.value = proposalsData.map(proposal => {
const transformedProposal = {
...proposal,
status: getProposalStatus(proposal),
deadline: proposal.deadline || (proposal.startTime + proposal.duration)
};
const transformedProposal = {
...proposal,
status: getProposalStatus(proposal),
deadline: proposal.deadline || 0
};
console.log('[Frontend] Преобразованное предложение:', transformedProposal);
return transformedProposal;
});
@@ -776,11 +777,28 @@ function getProposalStatus(proposal) {
const now = Math.floor(Date.now() / 1000);
const deadline = proposal.deadline || 0;
// Проверяем отменено ли предложение
if (proposal.canceled) {
return 'canceled';
}
// Проверяем выполнено ли предложение
if (proposal.executed) {
return 'executed';
}
// Проверяем, достигнут ли кворум
// Используем isPassed из API, если доступно
if (proposal.isPassed !== undefined) {
if (proposal.isPassed) {
return 'succeeded';
} else if (deadline > 0 && now >= deadline) {
return 'defeated';
} else {
return 'active';
}
}
// Fallback логика для старых данных
const quorumPercentage = getQuorumPercentage(proposal);
const requiredQuorum = getRequiredQuorum();
const hasReachedQuorum = quorumPercentage >= requiredQuorum;
@@ -793,7 +811,8 @@ function getProposalStatus(proposal) {
deadlinePassed: deadline > 0 && now >= deadline,
quorumPercentage,
requiredQuorum,
hasReachedQuorum
hasReachedQuorum,
isPassed: proposal.isPassed
});
// Если кворум достигнут, предложение можно выполнить
@@ -815,7 +834,8 @@ function getProposalStatusText(status) {
'active': 'Активно',
'succeeded': 'Принято',
'defeated': 'Отклонено',
'executed': 'Выполнено'
'executed': 'Выполнено',
'canceled': 'Отменено'
};
return statusMap[status] || status;
}
@@ -1719,11 +1739,21 @@ onUnmounted(() => {
color: #0c5460;
}
.proposal-status.executed {
background: #d4edda;
color: #155724;
}
.proposal-status.defeated {
background: #f8d7da;
color: #721c24;
}
.proposal-status.canceled {
background: #fff3cd;
color: #856404;
}
.proposal-details {
margin-bottom: 1rem;
}