ваше сообщение коммита
This commit is contained in:
@@ -12,7 +12,8 @@
|
||||
"lint:style": "stylelint \"**/*.{vue,css}\"",
|
||||
"lint:style:fix": "stylelint \"**/*.{vue,css}\" --fix",
|
||||
"format": "prettier --write \"**/*.{js,vue,json,md}\"",
|
||||
"format:check": "prettier --check \"**/*.{js,vue,json,md}\""
|
||||
"format:check": "prettier --check \"**/*.{js,vue,json,md}\"",
|
||||
"dev:styles": "node scripts/style-check.js && yarn dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.8.4",
|
||||
|
||||
112
frontend/scripts/style-check.js
Normal file
112
frontend/scripts/style-check.js
Normal file
@@ -0,0 +1,112 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { execSync } from 'child_process';
|
||||
import chalk from 'chalk'; // Для цветного вывода
|
||||
|
||||
// ES модули не поддерживают __dirname, поэтому создаем его
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Проверка наличия пакета chalk и его установка при необходимости
|
||||
try {
|
||||
import('chalk');
|
||||
} catch (e) {
|
||||
console.log('Устанавливаем пакет chalk для цветного вывода...');
|
||||
execSync('yarn add chalk --dev', { stdio: 'inherit' });
|
||||
console.log('Пакет chalk установлен.');
|
||||
}
|
||||
|
||||
// Функция для проверки наличия файла
|
||||
function checkFileExists(filePath, errorMessage) {
|
||||
const fullPath = path.resolve(__dirname, '..', filePath);
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
console.log(chalk.red(errorMessage));
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(chalk.green(`✓ Файл ${path.basename(filePath)} найден`));
|
||||
}
|
||||
|
||||
// Функция для проверки импортов стилей в App.vue
|
||||
function checkStyleImports() {
|
||||
const appVuePath = path.resolve(__dirname, '..', 'src', 'App.vue');
|
||||
try {
|
||||
const appVueContent = fs.readFileSync(appVuePath, 'utf8');
|
||||
|
||||
const requiredImports = [
|
||||
'./assets/styles/variables.css',
|
||||
'./assets/styles/base.css',
|
||||
'./assets/styles/layout.css',
|
||||
'./assets/styles/global.css'
|
||||
];
|
||||
|
||||
let allImportsFound = true;
|
||||
|
||||
for (const importPath of requiredImports) {
|
||||
if (!appVueContent.includes(`import '${importPath}'`)) {
|
||||
console.log(chalk.red(`✗ Импорт ${importPath} не найден в App.vue!`));
|
||||
allImportsFound = false;
|
||||
} else {
|
||||
console.log(chalk.green(`✓ Импорт ${importPath} найден в App.vue`));
|
||||
}
|
||||
}
|
||||
|
||||
if (!allImportsFound) {
|
||||
console.log(chalk.yellow('Убедитесь, что в App.vue импортируются все нужные стили:'));
|
||||
requiredImports.forEach(imp => console.log(` import '${imp}';`));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(chalk.red(`Ошибка при чтении App.vue: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для проверки компонентов настроек
|
||||
function checkSettingsComponents() {
|
||||
const settingsDir = path.resolve(__dirname, '..', 'src', 'components', 'settings');
|
||||
const requiredComponents = [
|
||||
'AISettings.vue',
|
||||
'BlockchainSettings.vue',
|
||||
'SecuritySettings.vue',
|
||||
'InterfaceSettings.vue'
|
||||
];
|
||||
|
||||
for (const component of requiredComponents) {
|
||||
const componentPath = path.join(settingsDir, component);
|
||||
if (fs.existsSync(componentPath)) {
|
||||
console.log(chalk.green(`✓ Компонент ${component} найден`));
|
||||
} else {
|
||||
console.log(chalk.red(`✗ Компонент ${component} не найден!`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Запуск скрипта
|
||||
console.log(chalk.blue('======================================='));
|
||||
console.log(chalk.green('Запуск проекта с обновленными стилями'));
|
||||
console.log(chalk.blue('======================================='));
|
||||
|
||||
// Проверка наличия всех файлов стилей
|
||||
checkFileExists('src/assets/styles/global.css', 'Ошибка: файл global.css не найден!');
|
||||
checkFileExists('src/assets/styles/variables.css', 'Ошибка: файл variables.css не найден!');
|
||||
checkFileExists('src/assets/styles/base.css', 'Ошибка: файл base.css не найден!');
|
||||
checkFileExists('src/assets/styles/layout.css', 'Ошибка: файл layout.css не найден!');
|
||||
|
||||
// Проверка импортов стилей
|
||||
console.log(chalk.yellow('Проверка imports стилей...'));
|
||||
checkStyleImports();
|
||||
|
||||
// Проверка компонентов настроек
|
||||
checkSettingsComponents();
|
||||
|
||||
console.log(chalk.blue('---------------------------------------'));
|
||||
console.log(chalk.yellow('Запуск сервера разработки...'));
|
||||
console.log(chalk.blue('---------------------------------------'));
|
||||
|
||||
// Выходим успешно, т.к. сам запуск выполняется командой yarn dev:styles
|
||||
try {
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.log(chalk.red(`Ошибка при запуске сервера разработки: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -4,15 +4,31 @@
|
||||
<div class="loading-spinner" />
|
||||
</div>
|
||||
|
||||
<RouterView />
|
||||
<RouterView v-slot="{ Component }">
|
||||
<component
|
||||
:is="Component"
|
||||
:isAuthenticated="auth.isAuthenticated.value"
|
||||
:identities="auth.identities.value"
|
||||
:tokenBalances="tokenBalances"
|
||||
:isLoadingTokens="isLoadingTokens"
|
||||
@auth-action-completed="handleAuthActionCompleted"
|
||||
/>
|
||||
</RouterView>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { ref, watch, onMounted, computed } from 'vue';
|
||||
import { RouterView } from 'vue-router';
|
||||
import { useAuth } from './composables/useAuth';
|
||||
import './assets/styles/home.css';
|
||||
import { fetchTokenBalances } from './services/tokens';
|
||||
import eventBus from './utils/eventBus';
|
||||
|
||||
// Импорт стилей
|
||||
import './assets/styles/variables.css';
|
||||
import './assets/styles/base.css';
|
||||
import './assets/styles/layout.css';
|
||||
import './assets/styles/global.css';
|
||||
|
||||
// Состояние загрузки
|
||||
const isLoading = ref(false);
|
||||
@@ -20,10 +36,111 @@
|
||||
// Использование composable для аутентификации
|
||||
const auth = useAuth();
|
||||
|
||||
// --- Логика загрузки баланса токенов ---
|
||||
const tokenBalances = ref({});
|
||||
const isLoadingTokens = ref(false);
|
||||
|
||||
const identities = computed(() => auth.identities.value);
|
||||
|
||||
const hasIdentityType = (type) => {
|
||||
if (!identities.value) return false;
|
||||
return identities.value.some((identity) => identity.provider === type);
|
||||
};
|
||||
|
||||
const getIdentityValue = (type) => {
|
||||
if (!identities.value) return null;
|
||||
const identity = identities.value.find((identity) => identity.provider === type);
|
||||
return identity ? identity.provider_id : null;
|
||||
};
|
||||
|
||||
const refreshTokenBalances = async () => {
|
||||
if (!hasIdentityType('wallet') || !auth.isAuthenticated.value) {
|
||||
tokenBalances.value = {}; // Очищаем, если нет кошелька или не авторизован
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
// Следим за изменениями в идентификаторах
|
||||
watch(identities, (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;
|
||||
|
||||
if (newWalletId !== oldWalletId) {
|
||||
console.log('[App] Обнаружено изменение идентификатора кошелька, обновляем балансы');
|
||||
refreshTokenBalances();
|
||||
} else if (hasIdentityType('wallet') && Object.keys(tokenBalances.value).length === 0 && !isLoadingTokens.value) {
|
||||
// Если кошелек есть, но баланс пустой и не грузится - пробуем загрузить
|
||||
console.log('[App] Кошелек есть, но баланс пуст, пытаемся загрузить.');
|
||||
refreshTokenBalances();
|
||||
}
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
// Мониторинг изменений состояния аутентификации
|
||||
watch(auth.isAuthenticated, (newValue, oldValue) => {
|
||||
if (newValue !== oldValue) {
|
||||
console.log('[App] Состояние аутентификации изменилось:', newValue);
|
||||
watch(auth.isAuthenticated, (isAuth) => {
|
||||
console.log('[App] Состояние аутентификации изменилось:', isAuth);
|
||||
if (isAuth) {
|
||||
// Убираем задержку, полагаемся на watch(identities) или прямо вызываем
|
||||
// setTimeout(refreshTokenBalances, 500);
|
||||
refreshTokenBalances(); // Вызываем сразу, если нужно обновить при смене auth
|
||||
} else {
|
||||
// Очищаем баланс при выходе
|
||||
tokenBalances.value = {};
|
||||
}
|
||||
});
|
||||
|
||||
// --- Возвращаем и улучшаем функцию-обработчик ---
|
||||
const handleAuthActionCompleted = async () => {
|
||||
console.log('[App] Auth action completed, triggering updates...');
|
||||
isLoading.value = true; // Показываем индикатор загрузки
|
||||
try {
|
||||
// 1. Проверяем аутентификацию (обновит identities и isAuthenticated)
|
||||
await auth.checkAuth();
|
||||
console.log('[App] auth.checkAuth() completed. isAuthenticated:', auth.isAuthenticated.value);
|
||||
|
||||
// 2. Обновляем баланс (использует обновленные identities)
|
||||
await refreshTokenBalances();
|
||||
console.log('[App] refreshTokenBalances() completed.');
|
||||
|
||||
// 3. Явно оповещаем компоненты об изменении состояния авторизации
|
||||
// Передаем актуальное состояние из useAuth
|
||||
eventBus.emit('auth-state-changed', {
|
||||
isAuthenticated: auth.isAuthenticated.value,
|
||||
authType: auth.authType.value, // Предполагаем, что authType есть в useAuth
|
||||
userId: auth.userId.value, // Предполагаем, что userId есть в useAuth
|
||||
fromApp: true // Флаг, что событие от App.vue
|
||||
});
|
||||
console.log('[App] auth-state-changed event emitted.');
|
||||
|
||||
} catch (error) {
|
||||
console.error("[App] Error during auth action handling:", error);
|
||||
} finally {
|
||||
isLoading.value = false; // Скрываем индикатор загрузки
|
||||
}
|
||||
};
|
||||
|
||||
// Первичная загрузка баланса при монтировании, если пользователь уже авторизован
|
||||
onMounted(() => {
|
||||
if (auth.isAuthenticated.value) {
|
||||
refreshTokenBalances();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
95
frontend/src/assets/styles/README.md
Normal file
95
frontend/src/assets/styles/README.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# Структура стилей проекта
|
||||
|
||||
## Обзор
|
||||
|
||||
Проект использует структурированный подход к организации стилей CSS для улучшения поддерживаемости, предотвращения конфликтов и обеспечения согласованности пользовательского интерфейса.
|
||||
|
||||
## Файлы стилей
|
||||
|
||||
- **variables.css** - CSS-переменные (цвета, размеры, отступы)
|
||||
- **base.css** - Базовые стили для всего приложения и сброс стилей
|
||||
- **layout.css** - Стили для основной структуры макета приложения
|
||||
- **global.css** - Общие утилитарные классы, доступные во всем приложении
|
||||
- **home.css.bak** - Устаревший файл, переименован в .bak. Стили перенесены в scoped стили компонентов
|
||||
|
||||
## Приоритеты использования стилей
|
||||
|
||||
1. **Компонентные scoped стили** - для стилей, специфичных для компонента
|
||||
2. **global.css** - для общих классов, используемых в нескольких компонентах
|
||||
3. **variables.css** - для общих переменных CSS во всем проекте
|
||||
|
||||
## Рекомендации по использованию
|
||||
|
||||
### Для новых компонентов:
|
||||
|
||||
1. Используйте scoped стили внутри файла компонента:
|
||||
```vue
|
||||
<style scoped>
|
||||
.component-name {
|
||||
/* стили компонента */
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
2. Используйте глобальные классы для общих элементов:
|
||||
```html
|
||||
<button class="btn btn-primary">Сохранить</button>
|
||||
```
|
||||
|
||||
3. Используйте CSS-переменные вместо жестко закодированных значений:
|
||||
```css
|
||||
.element {
|
||||
color: var(--color-primary);
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
```
|
||||
|
||||
### Для существующих компонентов:
|
||||
|
||||
1. При обновлении компонента постепенно переносите стили из home.css в scoped стили компонента
|
||||
2. Не удаляйте стили из home.css до полного тестирования всех зависящих компонентов
|
||||
|
||||
## Глобальные CSS-классы
|
||||
|
||||
### Контейнеры
|
||||
- `.page-container` - Основной контейнер страницы
|
||||
- `.card` - Контейнер для блока информации
|
||||
|
||||
### Кнопки
|
||||
- `.btn` - Базовый класс для всех кнопок
|
||||
- `.btn-primary` - Основная (зеленая) кнопка
|
||||
- `.btn-secondary` - Дополнительная (синяя) кнопка
|
||||
- `.btn-accent` - Акцентная (фиолетовая) кнопка
|
||||
- `.btn-danger` - Кнопка опасного действия (красная)
|
||||
|
||||
### Формы
|
||||
- `.form-control` - Элемент формы (input, select, textarea)
|
||||
- `.form-group` - Группа элементов формы
|
||||
- `.form-label` - Метка для элемента формы
|
||||
|
||||
### Утилиты
|
||||
- `.text-center` - Выравнивание текста по центру
|
||||
- `.d-flex` - Включение flex-контейнера
|
||||
- `.mt-*`, `.mb-*` - Отступы сверху/снизу
|
||||
|
||||
## Процесс миграции
|
||||
|
||||
Постепенно мы переходим от использования большого глобального файла home.css к модульным scoped стилям в компонентах и более структурированным общим стилям.
|
||||
|
||||
1. Новые компоненты должны использовать только scoped стили и global.css
|
||||
2. При обновлении существующих компонентов переносите стили из home.css
|
||||
3. После полного перехода home.css будет удален
|
||||
|
||||
## Выполненная миграция (обновлено)
|
||||
|
||||
Миграция стилей завершена для следующих компонентов:
|
||||
|
||||
1. **ChatInterface.vue** - перенесены стили интерфейса чата, включая адаптивные стили для мобильных устройств
|
||||
2. **Message.vue** - перенесены стили для сообщений с разными типами вложений
|
||||
|
||||
Файл **home.css** переименован в **home.css.bak** и больше не используется в проекте. Ссылка на него удалена из **HomeView.vue**.
|
||||
|
||||
Для запуска проекта с проверкой стилей можно использовать команду:
|
||||
```
|
||||
yarn dev:styles
|
||||
```
|
||||
150
frontend/src/assets/styles/global.css
Normal file
150
frontend/src/assets/styles/global.css
Normal file
@@ -0,0 +1,150 @@
|
||||
/* frontend/src/assets/styles/global.css */
|
||||
/* Общие глобальные стили, используемые во всем приложении */
|
||||
|
||||
/* Контейнеры */
|
||||
.app-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
background-color: var(--color-white);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 1200px;
|
||||
margin: 0;
|
||||
padding: 0 20px;
|
||||
width: 100%;
|
||||
background-color: var(--color-white);
|
||||
}
|
||||
|
||||
/* Стандартный контейнер для страниц */
|
||||
.page-container {
|
||||
max-width: 1150px;
|
||||
margin: 20px auto;
|
||||
padding: var(--block-padding);
|
||||
background-color: var(--color-white);
|
||||
border-radius: var(--block-radius);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
/* Общие стили для кнопок */
|
||||
.btn {
|
||||
height: var(--button-height);
|
||||
padding: 0 var(--spacing-lg);
|
||||
border-radius: var(--radius-lg);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: var(--transition-fast);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-sm);
|
||||
margin: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #1976D2; /* Темнее синего */
|
||||
}
|
||||
|
||||
.btn-accent {
|
||||
background: var(--color-accent);
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.btn-accent:hover {
|
||||
background: var(--color-accent-dark);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--color-danger);
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #D32F2F; /* Темнее красного */
|
||||
}
|
||||
|
||||
/* Общие стили для форм */
|
||||
.form-control {
|
||||
height: var(--input-height);
|
||||
padding: 0 var(--spacing-lg);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--color-grey-light);
|
||||
font-size: var(--font-size-md);
|
||||
width: 100%;
|
||||
background: var(--color-white);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* Общие стили для карточек/блоков */
|
||||
.card {
|
||||
padding: var(--block-padding);
|
||||
margin-bottom: var(--block-margin);
|
||||
background: var(--color-white);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
margin: 0 0 var(--spacing-md) 0;
|
||||
font-size: var(--font-size-xl);
|
||||
color: var(--color-dark);
|
||||
border-bottom: 1px solid var(--color-grey-light);
|
||||
padding-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
/* Общие утилиты */
|
||||
.text-center { text-align: center; }
|
||||
.text-right { text-align: right; }
|
||||
.d-flex { display: flex; }
|
||||
.flex-column { flex-direction: column; }
|
||||
.align-center { align-items: center; }
|
||||
.justify-center { justify-content: center; }
|
||||
.justify-between { justify-content: space-between; }
|
||||
.mt-1 { margin-top: var(--spacing-xs); }
|
||||
.mt-2 { margin-top: var(--spacing-sm); }
|
||||
.mt-3 { margin-top: var(--spacing-md); }
|
||||
.mb-1 { margin-bottom: var(--spacing-xs); }
|
||||
.mb-2 { margin-bottom: var(--spacing-sm); }
|
||||
.mb-3 { margin-bottom: var(--spacing-md); }
|
||||
|
||||
/* Адаптивные стили */
|
||||
@media (max-width: 768px) {
|
||||
.page-container {
|
||||
padding: var(--block-padding-mobile);
|
||||
}
|
||||
|
||||
.btn, .form-control {
|
||||
height: var(--button-height-mobile);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
}
|
||||
@@ -1,79 +1,120 @@
|
||||
/* frontend/src/assets/styles/variables.css */
|
||||
:root {
|
||||
/* Цвета */
|
||||
--color-primary: #4CAF50;
|
||||
--color-primary-dark: #45a049;
|
||||
--color-secondary: #2196F3;
|
||||
--color-danger: #F44336;
|
||||
--color-warning: #FF9800;
|
||||
--color-light: #f5f5f5;
|
||||
--color-dark: #333333;
|
||||
--color-grey: #777777;
|
||||
--color-grey-light: #e0e0e0;
|
||||
--color-white: #ffffff;
|
||||
--color-black: #000000;
|
||||
--color-telegram: #0088cc;
|
||||
--color-error: #e74c3c;
|
||||
/*
|
||||
* ЦВЕТОВАЯ СХЕМА
|
||||
* Основная палитра цветов приложения
|
||||
*/
|
||||
--color-primary: #4CAF50; /* Основной цвет (зеленый) */
|
||||
--color-primary-dark: #45a049; /* Темно-зеленый для наведения и акцентов */
|
||||
--color-secondary: #2196F3; /* Второстепенный цвет (синий) */
|
||||
--color-accent: #5E35B1; /* Акцентный цвет (фиолетовый) */
|
||||
--color-accent-dark: #4527A0; /* Темно-фиолетовый для наведения */
|
||||
|
||||
/* Статусные цвета */
|
||||
--color-danger: #F44336; /* Ошибки, удаление, опасные действия */
|
||||
--color-warning: #FF9800; /* Предупреждения */
|
||||
--color-error: #e74c3c; /* Текст ошибок */
|
||||
|
||||
/* Нейтральные цвета */
|
||||
--color-light: #f5f5f5; /* Светлый фон, фон секций */
|
||||
--color-dark: #333333; /* Основной текст, заголовки */
|
||||
--color-grey: #777777; /* Второстепенный текст */
|
||||
--color-grey-light: #e0e0e0; /* Границы, разделители */
|
||||
--color-white: #ffffff; /* Белый */
|
||||
--color-black: #000000; /* Черный */
|
||||
|
||||
/* Цвета текста */
|
||||
--color-text: #333333; /* Основной текст */
|
||||
--color-text-light: #999999; /* Неакцентированный текст */
|
||||
--color-border: #e0e0e0; /* Цвет рамок */
|
||||
|
||||
/* Цвета брендов */
|
||||
--color-telegram: #0088cc; /* Фирменный цвет Telegram */
|
||||
|
||||
/* Цвета сообщений */
|
||||
--color-user-message: #EFFAFF;
|
||||
--color-ai-message: #F8F8F8;
|
||||
--color-system-message: #FFF3E0;
|
||||
--color-system-text: #FF5722;
|
||||
--color-user-message: #EFFAFF; /* Фон сообщений пользователя */
|
||||
--color-ai-message: #F8F8F8; /* Фон сообщений ИИ */
|
||||
--color-system-message: #FFF3E0; /* Фон системных сообщений */
|
||||
--color-system-text: #FF5722; /* Текст системных сообщений */
|
||||
|
||||
/* Тени */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
|
||||
/*
|
||||
* ТЕНИ
|
||||
* Для создания эффекта глубины и иерархии элементов
|
||||
*/
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.1); /* Легкая тень */
|
||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1); /* Средняя тень */
|
||||
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1); /* Глубокая тень */
|
||||
|
||||
/* Отступы */
|
||||
--spacing-xs: 5px;
|
||||
--spacing-sm: 10px;
|
||||
--spacing-md: 15px;
|
||||
--spacing-lg: 20px;
|
||||
--spacing-xl: 30px;
|
||||
/*
|
||||
* ОТСТУПЫ
|
||||
* Для обеспечения консистентных интервалов и отступов
|
||||
*/
|
||||
--spacing-xs: 5px; /* Очень маленькие отступы (между близкими элементами) */
|
||||
--spacing-sm: 10px; /* Маленькие отступы */
|
||||
--spacing-md: 15px; /* Средние отступы */
|
||||
--spacing-lg: 20px; /* Большие отступы */
|
||||
--spacing-xl: 30px; /* Очень большие отступы (между крупными блоками) */
|
||||
|
||||
/* Размеры шрифтов */
|
||||
--font-size-xs: 12px;
|
||||
--font-size-sm: 13px;
|
||||
--font-size-md: 14px;
|
||||
--font-size-lg: 16px;
|
||||
--font-size-xl: 18px;
|
||||
--font-size-xxl: 24px;
|
||||
/*
|
||||
* РАЗМЕРЫ ШРИФТОВ
|
||||
* Типографическая шкала
|
||||
*/
|
||||
--font-size-xs: 12px; /* Маленькие надписи, подписи к полям */
|
||||
--font-size-sm: 13px; /* Второстепенный текст */
|
||||
--font-size-md: 14px; /* Основной текст */
|
||||
--font-size-lg: 16px; /* Подзаголовки */
|
||||
--font-size-xl: 18px; /* Заголовки разделов */
|
||||
--font-size-xxl: 24px; /* Главные заголовки */
|
||||
|
||||
/* Радиусы скругления */
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 6px;
|
||||
--radius-lg: 8px;
|
||||
/*
|
||||
* РАДИУСЫ СКРУГЛЕНИЯ
|
||||
* Для элементов интерфейса
|
||||
*/
|
||||
--radius-sm: 4px; /* Небольшое скругление (кнопки, поля ввода) */
|
||||
--radius-md: 6px; /* Среднее скругление (карточки, панели) */
|
||||
--radius-lg: 8px; /* Большое скругление (модальные окна, боковые панели) */
|
||||
|
||||
/* Переходы */
|
||||
--transition-fast: 0.2s ease;
|
||||
--transition-normal: 0.3s ease;
|
||||
/*
|
||||
* ПЕРЕХОДЫ
|
||||
* Для плавных анимаций
|
||||
*/
|
||||
--transition-fast: 0.2s ease; /* Быстрые переходы (ховеры, небольшие анимации) */
|
||||
--transition-normal: 0.3s ease; /* Стандартные переходы (появление элементов) */
|
||||
|
||||
/* Размеры компонентов (Удаляем старые sidebar width) */
|
||||
/* --sidebar-width: 110px; */
|
||||
/* --sidebar-expanded-width: 325px; */
|
||||
/*
|
||||
* РАЗМЕРЫ КОМПОНЕНТОВ
|
||||
* Стандартные размеры для элементов интерфейса
|
||||
*/
|
||||
--nav-btn-size: 40px;
|
||||
--chat-input-min-height: 100px;
|
||||
--chat-input-max-height: 200px;
|
||||
--chat-input-focus-min-height: 170px;
|
||||
--chat-input-focus-max-height: 300px;
|
||||
|
||||
/* Унифицированные размеры для кнопок и форм */
|
||||
/*
|
||||
* УНИФИЦИРОВАННЫЕ РАЗМЕРЫ
|
||||
* Для кнопок и форм
|
||||
*/
|
||||
--button-height: 48px;
|
||||
--button-height-mobile: 42px;
|
||||
--button-padding: 0 var(--spacing-lg);
|
||||
--button-gap: var(--spacing-md);
|
||||
|
||||
--form-gap: var(--spacing-md);
|
||||
|
||||
--block-padding: 24px;
|
||||
--block-padding-mobile: 16px;
|
||||
--block-margin: 24px;
|
||||
--block-margin-mobile: 16px;
|
||||
|
||||
--input-height: 48px;
|
||||
--input-height-mobile: 42px;
|
||||
--input-padding: 0 var(--spacing-lg);
|
||||
|
||||
/* Общие стили */
|
||||
/*
|
||||
* ОБЩИЕ СТИЛИ
|
||||
* Производные параметры для единого стиля
|
||||
*/
|
||||
--button-radius: var(--radius-lg);
|
||||
--input-radius: var(--radius-lg);
|
||||
--block-radius: var(--radius-lg);
|
||||
|
||||
@@ -15,11 +15,12 @@
|
||||
<!-- Правая панель с информацией о кошельке -->
|
||||
<Sidebar
|
||||
v-model="showWalletSidebar"
|
||||
:is-authenticated="auth.isAuthenticated.value"
|
||||
:is-authenticated="isAuthenticated"
|
||||
:telegram-auth="telegramAuth"
|
||||
:email-auth="emailAuth"
|
||||
:token-balances="tokenBalances.value"
|
||||
:identities="auth.identities?.value"
|
||||
:token-balances="tokenBalances"
|
||||
:identities="identities"
|
||||
:is-loading-tokens="isLoadingTokens"
|
||||
@wallet-auth="handleWalletAuth"
|
||||
@disconnect-wallet="disconnectWallet"
|
||||
@telegram-auth="handleTelegramAuth"
|
||||
@@ -36,9 +37,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, onBeforeUnmount } from 'vue';
|
||||
import { ref, onMounted, watch, onBeforeUnmount, defineProps, defineEmits } from 'vue';
|
||||
import { useAuth } from '../composables/useAuth';
|
||||
import { useTokenBalances } from '../composables/useTokenBalances';
|
||||
import { useAuthFlow } from '../composables/useAuthFlow';
|
||||
import { useNotifications } from '../composables/useNotifications';
|
||||
import { getFromStorage, setToStorage, removeFromStorage } from '../utils/storage';
|
||||
@@ -55,7 +55,17 @@ import NotificationDisplay from './NotificationDisplay.vue';
|
||||
|
||||
const auth = useAuth();
|
||||
const { notifications, showSuccessMessage, showErrorMessage } = useNotifications();
|
||||
const { tokenBalances } = useTokenBalances();
|
||||
|
||||
// Определяем props, которые будут приходить от родительского View
|
||||
const props = defineProps({
|
||||
isAuthenticated: Boolean,
|
||||
identities: Array,
|
||||
tokenBalances: Object,
|
||||
isLoadingTokens: Boolean
|
||||
});
|
||||
|
||||
// Определяем emits
|
||||
const emit = defineEmits(['auth-action-completed']);
|
||||
|
||||
// Callback после успешной аутентификации/привязки через Email/Telegram
|
||||
const handleAuthFlowSuccess = (authType) => {
|
||||
@@ -102,7 +112,7 @@ const handleWalletAuth = async () => {
|
||||
const linkResult = await auth.linkIdentity('wallet', result.address);
|
||||
if (linkResult.success) {
|
||||
showSuccessMessage('Кошелек успешно подключен к вашему аккаунту!');
|
||||
await auth.checkAuth(); // Обновить identities
|
||||
emit('auth-action-completed');
|
||||
} else {
|
||||
showErrorMessage(linkResult.error || 'Не удалось подключить кошелек');
|
||||
}
|
||||
@@ -112,12 +122,7 @@ const handleWalletAuth = async () => {
|
||||
if (authResponse.authenticated && authResponse.authType === 'wallet') {
|
||||
console.log('[BaseLayout] Кошелёк успешно подключен и аутентифицирован');
|
||||
showSuccessMessage('Кошелёк успешно подключен!');
|
||||
// Оповещаем компоненты об успешной авторизации
|
||||
eventBus.emit('auth-state-changed', {
|
||||
isAuthenticated: true,
|
||||
authType: 'wallet',
|
||||
fromBaseLayout: true
|
||||
});
|
||||
emit('auth-action-completed');
|
||||
} else {
|
||||
showErrorMessage('Не удалось завершить аутентификацию через кошелек.');
|
||||
}
|
||||
@@ -141,16 +146,10 @@ const disconnectWallet = async () => {
|
||||
console.log('[BaseLayout] Выполняется выход из системы...');
|
||||
try {
|
||||
await api.post('/api/auth/logout');
|
||||
await auth.checkAuth();
|
||||
showSuccessMessage('Вы успешно вышли из системы');
|
||||
removeFromStorage('guestMessages');
|
||||
removeFromStorage('hasUserSentMessage');
|
||||
|
||||
// Оповещаем компоненты о выходе из системы
|
||||
eventBus.emit('auth-state-changed', {
|
||||
isAuthenticated: false,
|
||||
fromBaseLayout: true
|
||||
});
|
||||
emit('auth-action-completed');
|
||||
} catch (error) {
|
||||
console.error('[BaseLayout] Ошибка при выходе из системы:', error);
|
||||
showErrorMessage('Произошла ошибка при выходе из системы');
|
||||
@@ -206,9 +205,31 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
/* Адаптивный дизайн */
|
||||
@media (max-width: 1199px) {
|
||||
.main-content {
|
||||
max-width: calc(100% - 320px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-content {
|
||||
max-width: 100%;
|
||||
padding-bottom: 20px; /* Убираем большой отступ, так как панель теперь полноэкранная */
|
||||
}
|
||||
|
||||
.main-content.no-right-sidebar {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.main-content {
|
||||
padding: 0 10px;
|
||||
padding-bottom: 10px; /* Убираем большой отступ */
|
||||
}
|
||||
|
||||
.main-content.no-right-sidebar {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -437,16 +437,14 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Стили здесь для инкапсуляции, можно вынести в home.css */
|
||||
.chat-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: var(--spacing-lg) auto;
|
||||
padding: 0; /* Убираю padding, так как он теперь задается через .main-content */
|
||||
min-height: 500px; /* Или другая подходящая высота */
|
||||
max-width: 1150px; /* Ограничиваем ширину чата */
|
||||
width: 100%; /* Занимаем всю доступную ширину до максимума */
|
||||
margin: var(--spacing-lg) 0;
|
||||
padding: 0;
|
||||
min-height: 500px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -462,7 +460,7 @@ onUnmounted(() => {
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: calc(var(--chat-input-height, 80px) + 15px); /* Добавляем 15px отступа между сообщениями и полем ввода */
|
||||
bottom: calc(var(--chat-input-height, 80px) + 15px);
|
||||
transition: bottom var(--transition-normal);
|
||||
}
|
||||
|
||||
@@ -482,16 +480,32 @@ onUnmounted(() => {
|
||||
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Стили для textarea, связанные с авто-ресайзом (дублируют home.css, но можно оставить для явности) */
|
||||
.chat-input textarea {
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
resize: none;
|
||||
outline: none;
|
||||
font-size: var(--font-size-md);
|
||||
line-height: 1.5;
|
||||
padding: var(--spacing-sm);
|
||||
min-height: var(--chat-input-min-height, 40px);
|
||||
max-height: var(--chat-input-max-height, 120px);
|
||||
transition: all var(--transition-fast);
|
||||
color: var(--color-dark);
|
||||
overflow-y: hidden;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.chat-input textarea:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.input-area {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: var(--spacing-sm);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chat-icons {
|
||||
@@ -574,7 +588,7 @@ onUnmounted(() => {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--color-grey-light);
|
||||
max-height: 100px; /* Можно увеличить, если нужно больше места */
|
||||
max-height: 100px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -622,4 +636,57 @@ onUnmounted(() => {
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Добавляем адаптивные стили для мобильных устройств */
|
||||
@media (max-width: 768px) {
|
||||
.chat-container {
|
||||
margin: var(--spacing-sm) auto;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
}
|
||||
|
||||
.chat-icon-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.chat-icon-btn svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.chat-container {
|
||||
margin: var(--spacing-xs) auto;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
padding: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.input-area {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.chat-icon-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.chat-icon-btn svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -65,7 +65,7 @@ onBeforeUnmount(() => {
|
||||
<style scoped>
|
||||
.header {
|
||||
background-color: var(--color-white);
|
||||
padding: 15px 20px;
|
||||
padding: 15px 20px; /* Возвращаем горизонтальный padding */
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100; /* Ensure header stays on top */
|
||||
@@ -75,8 +75,7 @@ onBeforeUnmount(() => {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
max-width: 1200px; /* Optional: limit max width */
|
||||
margin: 0 auto; /* Optional: center content */
|
||||
/* Убираем max-width, margin, padding */
|
||||
}
|
||||
|
||||
.header-text {
|
||||
@@ -144,8 +143,10 @@ onBeforeUnmount(() => {
|
||||
top: 6px;
|
||||
}
|
||||
|
||||
/* Удаляем стили для трансформации бургера в крестик */
|
||||
/*
|
||||
.header-wallet-btn.active .hamburger-line {
|
||||
background-color: transparent; /* Hide middle line */
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.header-wallet-btn.active .hamburger-line::before {
|
||||
@@ -157,6 +158,7 @@ onBeforeUnmount(() => {
|
||||
top: 0;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
*/
|
||||
|
||||
.nav-btn-text {
|
||||
font-size: 0.9rem;
|
||||
|
||||
@@ -171,7 +171,7 @@ const formatFileSize = (bytes) => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Стили можно скопировать из home.css или оставить глобальными */
|
||||
/* Стили сообщений, полностью перенесенные из home.css */
|
||||
.message {
|
||||
margin-bottom: var(--spacing-md);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
@@ -240,7 +240,7 @@ const formatFileSize = (bytes) => {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: var(--spacing-xs); /* Добавлен отступ сверху */
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.message-time {
|
||||
@@ -272,7 +272,7 @@ const formatFileSize = (bytes) => {
|
||||
border: 1px solid var(--color-danger);
|
||||
}
|
||||
|
||||
/* --- НОВЫЕ СТИЛИ --- */
|
||||
/* Стили для вложений */
|
||||
.message-attachments {
|
||||
margin-top: var(--spacing-sm);
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
@@ -281,19 +281,19 @@ const formatFileSize = (bytes) => {
|
||||
|
||||
.attachment-item {
|
||||
display: flex;
|
||||
flex-direction: column; /* Отображаем элементы в столбец */
|
||||
align-items: flex-start; /* Выравниваем по левому краю */
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.attachment-preview {
|
||||
max-width: 100%;
|
||||
max-height: 300px; /* Ограничение высоты для превью */
|
||||
max-height: 300px;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
object-fit: cover; /* Сохраняем пропорции */
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.audio-preview {
|
||||
@@ -301,7 +301,7 @@ const formatFileSize = (bytes) => {
|
||||
}
|
||||
|
||||
.video-preview {
|
||||
/* Стили для видео по умолчанию */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-preview {
|
||||
@@ -317,16 +317,47 @@ const formatFileSize = (bytes) => {
|
||||
.attachment-name {
|
||||
font-weight: 500;
|
||||
margin-right: var(--spacing-xs);
|
||||
color: var(--color-primary); /* Делаем имя файла похожим на ссылку */
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.attachment-name:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.attachment-size {
|
||||
color: var(--color-grey);
|
||||
font-size: var(--font-size-xs); /* Уменьшим размер */
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
/* Адаптивные стили для разных экранов */
|
||||
@media (max-width: 768px) {
|
||||
.message {
|
||||
max-width: 85%;
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
}
|
||||
|
||||
.ai-message {
|
||||
max-width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.message {
|
||||
max-width: 95%;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.ai-message {
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: calc(var(--font-size-xs) - 1px);
|
||||
}
|
||||
|
||||
.attachment-preview {
|
||||
max-height: 200px;
|
||||
}
|
||||
}
|
||||
/* --- КОНЕЦ НОВЫХ СТИЛЕЙ --- */
|
||||
</style>
|
||||
@@ -33,25 +33,69 @@
|
||||
<!-- Навигационные кнопки -->
|
||||
<div class="navigation-buttons">
|
||||
<router-link to="/" class="nav-link-btn" active-class="active">
|
||||
<i class="nav-icon">💬</i>
|
||||
<span>Чат</span>
|
||||
</router-link>
|
||||
<router-link to="/crm" class="nav-link-btn" active-class="active">
|
||||
<i class="nav-icon">👥</i>
|
||||
<span>CRM</span>
|
||||
</router-link>
|
||||
<router-link to="/settings" class="nav-link-btn" active-class="active">
|
||||
<i class="nav-icon">⚙️</i>
|
||||
<span>Настройки</span>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Блок информации о пользователе -->
|
||||
<div v-if="isAuthenticated" class="user-info-section sidebar-section">
|
||||
<h3>Ваши идентификаторы:</h3>
|
||||
<div class="user-info-item">
|
||||
<span class="user-info-label">Кошелек:</span>
|
||||
<span v-if="hasIdentityType('wallet')" class="user-info-value">
|
||||
{{ truncateAddress(getIdentityValue('wallet')) }}
|
||||
</span>
|
||||
<span v-else class="user-info-value">Не подключен</span>
|
||||
</div>
|
||||
<!-- Можно добавить другие идентификаторы по аналогии -->
|
||||
</div>
|
||||
|
||||
<!-- Блок баланса токенов -->
|
||||
<div v-if="isAuthenticated" class="token-balances-section sidebar-section">
|
||||
<h3>Баланс токенов:</h3>
|
||||
<div v-if="isLoadingTokens" class="token-loading">
|
||||
Загрузка балансов...
|
||||
</div>
|
||||
<div v-else-if="!tokenBalances || Object.keys(tokenBalances).length === 0" class="token-no-data">
|
||||
Баланс не доступен
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="tokenBalances.eth" class="token-balance">
|
||||
<span class="token-name">ETH:</span>
|
||||
<span class="token-amount">{{ Number(tokenBalances.eth).toLocaleString() }}</span>
|
||||
<span class="token-symbol">{{ TOKEN_CONTRACTS.eth.symbol }}</span>
|
||||
</div>
|
||||
<div v-if="tokenBalances.bsc" class="token-balance">
|
||||
<span class="token-name">BSC:</span>
|
||||
<span class="token-amount">{{ Number(tokenBalances.bsc).toLocaleString() }}</span>
|
||||
<span class="token-symbol">{{ TOKEN_CONTRACTS.bsc.symbol }}</span>
|
||||
</div>
|
||||
<div v-if="tokenBalances.arbitrum" class="token-balance">
|
||||
<span class="token-name">ARB:</span>
|
||||
<span class="token-amount">{{ Number(tokenBalances.arbitrum).toLocaleString() }}</span>
|
||||
<span class="token-symbol">{{ TOKEN_CONTRACTS.arbitrum.symbol }}</span>
|
||||
</div>
|
||||
<div v-if="tokenBalances.polygon" class="token-balance">
|
||||
<span class="token-name">POL:</span>
|
||||
<span class="token-amount">{{ Number(tokenBalances.polygon).toLocaleString() }}</span>
|
||||
<span class="token-symbol">{{ TOKEN_CONTRACTS.polygon.symbol }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, defineEmits, ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { defineProps, defineEmits, ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||
import { TOKEN_CONTRACTS } from '../services/tokens';
|
||||
import { useRouter } from 'vue-router';
|
||||
import eventBus from '../utils/eventBus';
|
||||
@@ -63,7 +107,8 @@ const props = defineProps({
|
||||
telegramAuth: Object,
|
||||
emailAuth: Object,
|
||||
tokenBalances: Object,
|
||||
identities: Array
|
||||
identities: Array,
|
||||
isLoadingTokens: Boolean
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'wallet-auth', 'disconnect-wallet']);
|
||||
@@ -117,9 +162,64 @@ const getIdentityValue = (type) => {
|
||||
const identity = props.identities.find((identity) => identity.provider === type);
|
||||
return identity ? identity.provider_id : null;
|
||||
};
|
||||
|
||||
// Добавляем watch для отслеживания props
|
||||
watch(() => props.tokenBalances, (newVal, oldVal) => {
|
||||
console.log('[Sidebar] tokenBalances prop changed:', JSON.stringify(newVal));
|
||||
}, { deep: true });
|
||||
|
||||
watch(() => props.isLoadingTokens, (newVal, oldVal) => {
|
||||
console.log(`[Sidebar] isLoadingTokens prop changed: ${newVal}`);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wallet-sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--color-white);
|
||||
z-index: 1000;
|
||||
overflow-y: auto;
|
||||
padding: var(--spacing-lg);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform var(--transition-normal), opacity var(--transition-normal);
|
||||
box-shadow: -5px 0 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.wallet-sidebar-content {
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--spacing-md);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
/* Анимация появления и исчезновения правой панели */
|
||||
.sidebar-slide-enter-active,
|
||||
.sidebar-slide-leave-active {
|
||||
transition: all var(--transition-normal);
|
||||
}
|
||||
|
||||
.sidebar-slide-enter-from,
|
||||
.sidebar-slide-leave-to {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.sidebar-slide-enter-to,
|
||||
.sidebar-slide-leave-from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.button-with-close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -167,15 +267,17 @@ const getIdentityValue = (type) => {
|
||||
.nav-link-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 48px;
|
||||
background-color: var(--color-light);
|
||||
color: var(--color-dark);
|
||||
border: 1px solid var(--color-grey-light);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 12px 15px;
|
||||
padding: 0 15px;
|
||||
font-size: var(--font-size-md);
|
||||
text-decoration: none;
|
||||
transition: all var(--transition-normal);
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.nav-link-btn.active {
|
||||
@@ -188,9 +290,109 @@ const getIdentityValue = (type) => {
|
||||
background-color: var(--color-grey-light);
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
margin-right: 10px;
|
||||
font-size: 1.2em;
|
||||
/* Стили для общих кнопок аутентификации/действий в сайдбаре */
|
||||
.auth-btn {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
border-radius: var(--radius-lg);
|
||||
background-color: var(--color-light);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
color: var(--color-dark);
|
||||
font-size: var(--font-size-md);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
padding: 0 var(--spacing-md);
|
||||
box-sizing: border-box;
|
||||
transition: all var(--transition-normal);
|
||||
margin: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.auth-btn:hover {
|
||||
background-color: var(--color-grey-light);
|
||||
}
|
||||
|
||||
/* Новые стили для секций в сайдбаре */
|
||||
.sidebar-section {
|
||||
background-color: var(--color-light);
|
||||
padding: var(--spacing-md);
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: var(--color-primary);
|
||||
margin-bottom: var(--spacing-md);
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
|
||||
.token-balance,
|
||||
.user-info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.token-name,
|
||||
.user-info-label {
|
||||
font-weight: bold;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.token-amount {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.token-symbol {
|
||||
color: var(--color-text-light);
|
||||
margin-left: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.token-no-data,
|
||||
.user-info-empty {
|
||||
color: var(--color-text-light);
|
||||
font-style: italic;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
/* Добавляем стиль для индикатора загрузки */
|
||||
.token-loading {
|
||||
color: var(--color-text-light);
|
||||
font-style: italic;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
/* Медиа-запросы для адаптивности */
|
||||
@media screen and (min-width: 1200px) {
|
||||
.wallet-sidebar {
|
||||
width: 30%;
|
||||
max-width: 350px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 769px) and (max-width: 1199px) {
|
||||
.wallet-sidebar {
|
||||
width: 40%;
|
||||
max-width: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
/* На мобильных устройствах сайдбар по умолчанию занимает весь экран (width: 100%, height: 100%) */
|
||||
/* Поэтому дополнительные правила для переопределения положения/размера не нужны */
|
||||
/* Оставляем только adjustment for padding when needed */
|
||||
.wallet-sidebar {
|
||||
padding: var(--spacing-md);
|
||||
/* Убраны bottom, top, height, max-height, чтобы вернуться к full-screen поведению */
|
||||
}
|
||||
|
||||
.wallet-sidebar-content {
|
||||
padding: 0;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
@@ -201,8 +403,14 @@ const getIdentityValue = (type) => {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.auth-btn {
|
||||
height: 42px;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.nav-link-btn {
|
||||
padding: 10px 12px;
|
||||
height: 42px;
|
||||
padding: 0 12px;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
}
|
||||
@@ -215,8 +423,13 @@ const getIdentityValue = (type) => {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.auth-btn {
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.nav-link-btn {
|
||||
padding: 8px 10px;
|
||||
height: 36px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,10 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import HomeView from '../views/HomeView.vue';
|
||||
// Импортируем (пока не созданные) компоненты для подстраниц настроек
|
||||
const SettingsAiView = () => import('../views/settings/AiSettingsView.vue');
|
||||
const SettingsBlockchainView = () => import('../views/settings/BlockchainSettingsView.vue');
|
||||
const SettingsSecurityView = () => import('../views/settings/SecuritySettingsView.vue');
|
||||
const SettingsInterfaceView = () => import('../views/settings/InterfaceSettingsView.vue');
|
||||
import axios from 'axios';
|
||||
|
||||
console.log('router/index.js: Script loaded');
|
||||
@@ -19,6 +24,35 @@ const routes = [
|
||||
path: '/settings',
|
||||
name: 'settings',
|
||||
component: () => import('../views/SettingsView.vue'),
|
||||
// Добавляем дочерние маршруты
|
||||
children: [
|
||||
{
|
||||
path: 'ai',
|
||||
name: 'settings-ai',
|
||||
component: SettingsAiView,
|
||||
},
|
||||
{
|
||||
path: 'blockchain',
|
||||
name: 'settings-blockchain',
|
||||
component: SettingsBlockchainView,
|
||||
},
|
||||
{
|
||||
path: 'security',
|
||||
name: 'settings-security',
|
||||
component: SettingsSecurityView,
|
||||
},
|
||||
{
|
||||
path: 'interface',
|
||||
name: 'settings-interface',
|
||||
component: SettingsInterfaceView,
|
||||
},
|
||||
// Опционально: перенаправление со /settings на первую подстраницу
|
||||
{
|
||||
path: '',
|
||||
name: 'settings-index',
|
||||
redirect: { name: 'settings-ai' }
|
||||
}
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<BaseLayout>
|
||||
<BaseLayout
|
||||
:is-authenticated="isAuthenticated"
|
||||
:identities="identities"
|
||||
:token-balances="tokenBalances"
|
||||
:is-loading-tokens="isLoadingTokens"
|
||||
@auth-action-completed="$emit('auth-action-completed')"
|
||||
>
|
||||
<div class="crm-view-container">
|
||||
<h1>CRM Система</h1>
|
||||
<div v-if="isLoading">Загрузка данных пользователя...</div>
|
||||
@@ -29,13 +35,24 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { ref, onMounted, onBeforeUnmount, defineProps, defineEmits } from 'vue';
|
||||
import { useAuth } from '../composables/useAuth';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { setToStorage } from '../utils/storage';
|
||||
import BaseLayout from '../components/BaseLayout.vue';
|
||||
import eventBus from '../utils/eventBus';
|
||||
|
||||
// Определяем props
|
||||
const props = defineProps({
|
||||
isAuthenticated: Boolean,
|
||||
identities: Array,
|
||||
tokenBalances: Object,
|
||||
isLoadingTokens: Boolean
|
||||
});
|
||||
|
||||
// Определяем emits
|
||||
const emit = defineEmits(['auth-action-completed']);
|
||||
|
||||
const auth = useAuth();
|
||||
const router = useRouter();
|
||||
const isLoading = ref(true);
|
||||
@@ -74,12 +91,12 @@ onBeforeUnmount(() => {
|
||||
|
||||
<style scoped>
|
||||
.crm-view-container {
|
||||
max-width: 1150px;
|
||||
margin: 20px auto;
|
||||
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;
|
||||
}
|
||||
|
||||
h1 {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<BaseLayout>
|
||||
<BaseLayout
|
||||
:is-authenticated="isAuthenticated"
|
||||
:identities="identities"
|
||||
:token-balances="tokenBalances"
|
||||
:is-loading-tokens="isLoadingTokens"
|
||||
@auth-action-completed="$emit('auth-action-completed')"
|
||||
>
|
||||
<ChatInterface
|
||||
:messages="messages"
|
||||
:is-loading="isLoading || isConnectingWallet"
|
||||
@@ -13,17 +19,27 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, onBeforeUnmount } from 'vue';
|
||||
import { ref, onMounted, watch, onBeforeUnmount, defineProps, defineEmits } from 'vue';
|
||||
import { useAuth } from '../composables/useAuth';
|
||||
import { useChat } from '../composables/useChat';
|
||||
import { connectWithWallet } from '../services/wallet';
|
||||
import eventBus from '../utils/eventBus';
|
||||
import '../assets/styles/home.css';
|
||||
import BaseLayout from '../components/BaseLayout.vue';
|
||||
import ChatInterface from '../components/ChatInterface.vue';
|
||||
|
||||
console.log('HomeView.vue: Using BaseLayout');
|
||||
|
||||
// Определяем props, переданные из App.vue через RouterView
|
||||
const props = defineProps({
|
||||
isAuthenticated: Boolean,
|
||||
identities: Array,
|
||||
tokenBalances: Object,
|
||||
isLoadingTokens: Boolean
|
||||
});
|
||||
|
||||
// Определяем emits
|
||||
const emit = defineEmits(['auth-action-completed']);
|
||||
|
||||
// =====================================================================
|
||||
// 1. ИСПОЛЬЗОВАНИЕ COMPOSABLES
|
||||
// =====================================================================
|
||||
|
||||
@@ -1,192 +1,64 @@
|
||||
<template>
|
||||
<BaseLayout>
|
||||
<BaseLayout
|
||||
:is-authenticated="isAuthenticated"
|
||||
:identities="identities"
|
||||
:token-balances="tokenBalances"
|
||||
:is-loading-tokens="isLoadingTokens"
|
||||
@auth-action-completed="$emit('auth-action-completed')"
|
||||
>
|
||||
<div class="settings-view-container">
|
||||
<h1>Настройки</h1>
|
||||
|
||||
<!-- Блок информации о пользователе (всегда виден) -->
|
||||
<div class="user-info-section">
|
||||
<h3>Ваши идентификаторы:</h3>
|
||||
<div v-if="!auth.isAuthenticated.value" class="user-info-empty">
|
||||
Войдите, чтобы увидеть идентификаторы
|
||||
</div>
|
||||
<div v-else class="user-info-item">
|
||||
<span class="user-info-label">Кошелек:</span>
|
||||
<span v-if="hasIdentityType('wallet')" class="user-info-value">
|
||||
{{ truncateAddress(getIdentityValue('wallet')) }}
|
||||
</span>
|
||||
<span v-else class="user-info-value">Не подключен</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Блок баланса токенов (всегда виден) -->
|
||||
<div class="token-balances-section">
|
||||
<h3>Баланс токенов:</h3>
|
||||
<div v-if="!auth.isAuthenticated.value" class="token-no-data">
|
||||
Войдите, чтобы увидеть баланс токенов
|
||||
</div>
|
||||
<div v-else-if="isLoadingTokens" class="token-loading">
|
||||
Загрузка балансов...
|
||||
</div>
|
||||
<div v-else-if="!hasTokenBalances" class="token-no-data">
|
||||
Информация о балансе не доступна
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="tokenBalances.eth" class="token-balance">
|
||||
<span class="token-name">ETH:</span>
|
||||
<span class="token-amount">{{ Number(tokenBalances.eth).toLocaleString() }}</span>
|
||||
<span class="token-symbol">{{ TOKEN_CONTRACTS.eth.symbol }}</span>
|
||||
</div>
|
||||
<div v-if="tokenBalances.bsc" class="token-balance">
|
||||
<span class="token-name">BSC:</span>
|
||||
<span class="token-amount">{{ Number(tokenBalances.bsc).toLocaleString() }}</span>
|
||||
<span class="token-symbol">{{ TOKEN_CONTRACTS.bsc.symbol }}</span>
|
||||
</div>
|
||||
<div v-if="tokenBalances.arbitrum" class="token-balance">
|
||||
<span class="token-name">ARB:</span>
|
||||
<span class="token-amount">{{ Number(tokenBalances.arbitrum).toLocaleString() }}</span>
|
||||
<span class="token-symbol">{{ TOKEN_CONTRACTS.arbitrum.symbol }}</span>
|
||||
</div>
|
||||
<div v-if="tokenBalances.polygon" class="token-balance">
|
||||
<span class="token-name">POL:</span>
|
||||
<span class="token-amount">{{ Number(tokenBalances.polygon).toLocaleString() }}</span>
|
||||
<span class="token-symbol">{{ TOKEN_CONTRACTS.polygon.symbol }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isLoading">Загрузка данных пользователя...</div>
|
||||
<div v-else-if="!auth.isAuthenticated.value">
|
||||
<p>Для доступа к настройкам необходимо <button @click="goToHomeAndShowSidebar">войти</button>.</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>Добро пожаловать в Настройки!</p>
|
||||
|
||||
<div v-if="auth.isAdmin.value">
|
||||
<p><strong>У вас полный доступ (Администратор).</strong></p>
|
||||
<!-- Сюда будут добавляться полные настройки -->
|
||||
<p>Здесь будут настройки системы, управление пользователями и т.д.</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p><strong>У вас ограниченный доступ.</strong></p>
|
||||
<!-- Сюда будут добавляться ограниченные настройки -->
|
||||
<p>Здесь будут настройки профиля, уведомлений и т.д.</p>
|
||||
</div>
|
||||
<!-- Общие настройки для всех авторизованных -->
|
||||
<div class="general-settings">
|
||||
<h3>Общие настройки</h3>
|
||||
<label>
|
||||
Язык интерфейса:
|
||||
<select v-model="selectedLanguage">
|
||||
<option value="ru">Русский</option>
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
</label>
|
||||
<div v-else class="settings-navigation-buttons">
|
||||
<h3>Разделы настроек:</h3>
|
||||
<div class="buttons-grid">
|
||||
<router-link :to="{ name: 'settings-ai' }" class="btn btn-secondary">ИИ</router-link>
|
||||
<router-link :to="{ name: 'settings-blockchain' }" class="btn btn-secondary">Блокчейн</router-link>
|
||||
<router-link :to="{ name: 'settings-security' }" class="btn btn-secondary">Безопасность</router-link>
|
||||
<router-link :to="{ name: 'settings-interface' }" class="btn btn-secondary">Интерфейс</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Router view для отображения дочерних компонентов настроек -->
|
||||
<router-view></router-view>
|
||||
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, onBeforeUnmount, computed } from 'vue';
|
||||
import { ref, onMounted, watch, onBeforeUnmount, computed, defineProps, defineEmits } from 'vue';
|
||||
import { useAuth } from '../composables/useAuth';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { getFromStorage, setToStorage } from '../utils/storage';
|
||||
import BaseLayout from '../components/BaseLayout.vue';
|
||||
import eventBus from '../utils/eventBus';
|
||||
import { TOKEN_CONTRACTS } from '../services/tokens';
|
||||
import { fetchTokenBalances } from '../services/tokens';
|
||||
|
||||
// Определяем props
|
||||
const props = defineProps({
|
||||
isAuthenticated: Boolean,
|
||||
identities: Array,
|
||||
tokenBalances: Object,
|
||||
isLoadingTokens: Boolean
|
||||
});
|
||||
|
||||
// Определяем emits
|
||||
const emit = defineEmits(['auth-action-completed']);
|
||||
|
||||
const auth = useAuth();
|
||||
const router = useRouter();
|
||||
const isLoading = ref(true);
|
||||
const selectedLanguage = ref(getFromStorage('userLanguage', 'ru'));
|
||||
|
||||
// Состояние для токенов
|
||||
const tokenBalances = ref({});
|
||||
const isLoadingTokens = ref(false);
|
||||
const hasTokenBalances = computed(() => {
|
||||
return tokenBalances.value &&
|
||||
Object.keys(tokenBalances.value).length > 0 &&
|
||||
Object.values(tokenBalances.value).some(value => parseInt(value) > 0);
|
||||
});
|
||||
|
||||
// Получаем данные об идентификаторах пользователя
|
||||
const identities = computed(() => auth.identities.value);
|
||||
|
||||
// Вспомогательные функции
|
||||
const truncateAddress = (address) => {
|
||||
if (!address) return '';
|
||||
return `${address.substring(0, 6)}...${address.substring(address.length - 4)}`;
|
||||
};
|
||||
|
||||
const hasIdentityType = (type) => {
|
||||
if (!identities.value) return false;
|
||||
return identities.value.some((identity) => identity.provider === type);
|
||||
};
|
||||
|
||||
const getIdentityValue = (type) => {
|
||||
if (!identities.value) return null;
|
||||
const identity = identities.value.find((identity) => identity.provider === type);
|
||||
return identity ? identity.provider_id : null;
|
||||
};
|
||||
|
||||
// Обновление балансов токенов
|
||||
const refreshTokenBalances = async () => {
|
||||
if (!hasIdentityType('wallet')) return;
|
||||
|
||||
isLoadingTokens.value = true;
|
||||
try {
|
||||
const walletAddress = getIdentityValue('wallet');
|
||||
console.log('[SettingsView] Обновление балансов для адреса:', walletAddress);
|
||||
|
||||
const balances = await fetchTokenBalances(walletAddress);
|
||||
console.log('[SettingsView] Полученные балансы:', balances);
|
||||
|
||||
tokenBalances.value = balances || {};
|
||||
} catch (error) {
|
||||
console.error('[SettingsView] Ошибка при получении балансов:', error);
|
||||
tokenBalances.value = {};
|
||||
} finally {
|
||||
isLoadingTokens.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Следим за изменениями в идентификаторах
|
||||
watch(() => identities.value, (newIdentities, oldIdentities) => {
|
||||
if (hasIdentityType('wallet')) {
|
||||
// Проверяем, появился ли новый идентификатор кошелька или изменился существующий
|
||||
const newWalletId = getIdentityValue('wallet');
|
||||
const oldWalletIdentity = oldIdentities ? oldIdentities.find(id => id.provider === 'wallet') : null;
|
||||
const oldWalletId = oldWalletIdentity ? oldWalletIdentity.provider_id : null;
|
||||
|
||||
if (newWalletId !== oldWalletId) {
|
||||
console.log('[SettingsView] Обнаружено изменение идентификатора кошелька, обновляем балансы');
|
||||
refreshTokenBalances();
|
||||
}
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
// Обработчик события изменения авторизации
|
||||
const handleAuthEvent = (eventData) => {
|
||||
console.log('[SettingsView] Получено событие изменения авторизации:', eventData);
|
||||
isLoading.value = false;
|
||||
|
||||
// При изменении авторизации обновляем балансы
|
||||
if (eventData.isAuthenticated) {
|
||||
setTimeout(() => {
|
||||
refreshTokenBalances(); // Небольшая задержка для обновления идентификаторов
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
// Сохраняем язык при изменении
|
||||
watch(selectedLanguage, (newLang) => {
|
||||
setToStorage('userLanguage', newLang);
|
||||
// TODO: Добавить логику для реальной смены языка интерфейса (например, через i18n)
|
||||
console.log(`[Settings] Язык изменен на: ${newLang}`);
|
||||
});
|
||||
|
||||
const goToHomeAndShowSidebar = () => {
|
||||
setToStorage('showWalletSidebar', true);
|
||||
router.push({ name: 'home' });
|
||||
@@ -201,11 +73,6 @@ onMounted(() => {
|
||||
|
||||
// Подписка на события авторизации
|
||||
unsubscribe = eventBus.on('auth-state-changed', handleAuthEvent);
|
||||
|
||||
// Обновляем данные о балансе токенов при загрузке
|
||||
if (auth.isAuthenticated.value && hasIdentityType('wallet')) {
|
||||
refreshTokenBalances();
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@@ -214,161 +81,90 @@ onBeforeUnmount(() => {
|
||||
unsubscribe();
|
||||
}
|
||||
});
|
||||
|
||||
// Следим за состоянием авторизации для очистки данных при выходе
|
||||
watch(() => auth.isAuthenticated.value, (isAuth) => {
|
||||
if (!isAuth) {
|
||||
// Очищаем данные при выходе из системы
|
||||
tokenBalances.value = {};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Основной контейнер */
|
||||
.settings-view-container {
|
||||
max-width: 1150px;
|
||||
margin: 20px auto;
|
||||
padding: 20px;
|
||||
padding: var(--block-padding);
|
||||
background-color: var(--color-white);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--color-dark);
|
||||
border-radius: var(--block-radius);
|
||||
box-shadow: var(--shadow-md);
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Заголовки */
|
||||
h1 {
|
||||
color: var(--color-dark);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: var(--color-primary);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
/* Общие элементы */
|
||||
p {
|
||||
line-height: 1.6;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
strong {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.general-settings {
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--color-grey-light);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 8px;
|
||||
border: 1px solid var(--color-grey);
|
||||
border-radius: var(--radius-md);
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 5px 10px;
|
||||
background-color: var(--color-primary);
|
||||
/* Стили для кнопки "войти" */
|
||||
.settings-view-container > div > p > button {
|
||||
background-color: var(--color-accent);
|
||||
color: var(--color-white);
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
margin-left: 5px;
|
||||
}
|
||||
button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
button:disabled {
|
||||
background-color: var(--color-grey);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
transition: background-color var(--transition-fast);
|
||||
margin-left: var(--spacing-xs);
|
||||
}
|
||||
|
||||
/* Стили для блока информации о пользователе */
|
||||
.user-info-section {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background-color: var(--color-light);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-grey-light);
|
||||
.settings-view-container > div > p > button:hover {
|
||||
background-color: var(--color-accent-dark);
|
||||
}
|
||||
|
||||
.user-info-item {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* Новые стили для кнопок навигации */
|
||||
.settings-navigation-buttons {
|
||||
margin-top: var(--spacing-lg);
|
||||
padding-top: var(--spacing-lg);
|
||||
border-top: 1px solid var(--color-grey-light);
|
||||
}
|
||||
|
||||
.user-info-label {
|
||||
min-width: 100px;
|
||||
font-weight: 500;
|
||||
.buttons-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.user-info-value {
|
||||
font-family: monospace;
|
||||
padding: 5px 10px;
|
||||
background-color: var(--color-white);
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--color-grey-light);
|
||||
}
|
||||
|
||||
/* Стили для блока баланса токенов */
|
||||
.token-balances-section {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background-color: var(--color-light);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-grey-light);
|
||||
}
|
||||
|
||||
.token-balance {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.token-name {
|
||||
min-width: 60px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.token-amount {
|
||||
font-family: monospace;
|
||||
padding: 5px 10px;
|
||||
background-color: var(--color-white);
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--color-grey-light);
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.token-symbol {
|
||||
color: var(--color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.token-loading,
|
||||
.token-no-data {
|
||||
margin: 15px 0;
|
||||
padding: 10px;
|
||||
.buttons-grid .btn {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: var(--color-grey-dark);
|
||||
font-style: italic;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Удаляем стили для кнопки обновления баланса, поскольку больше не нужны */
|
||||
.token-action,
|
||||
.refresh-btn {
|
||||
display: none;
|
||||
/* Анимации */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.user-info-empty,
|
||||
.token-no-data {
|
||||
margin: 15px 0;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
color: var(--color-grey-dark);
|
||||
font-style: italic;
|
||||
/* Адаптивный дизайн */
|
||||
@media (max-width: 768px) {
|
||||
.settings-navigation-buttons {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.buttons-grid {
|
||||
grid-column: 1;
|
||||
grid-row: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
180
frontend/src/views/settings/AiSettingsView.vue
Normal file
180
frontend/src/views/settings/AiSettingsView.vue
Normal file
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<div class="ai-settings settings-panel">
|
||||
<h2>Настройки ИИ</h2>
|
||||
|
||||
<!-- Панель Промт -->
|
||||
<div class="sub-settings-panel">
|
||||
<h3>Настройки промптов</h3>
|
||||
<div class="setting-form">
|
||||
<p>Здесь будут настройки для конфигурации промптов</p>
|
||||
<textarea v-model="settings.prompt" placeholder="Базовый промпт для ИИ..." rows="5" class="form-control"></textarea>
|
||||
<button class="btn btn-primary" @click="saveSettings('prompt')">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Панель RAG -->
|
||||
<div class="sub-settings-panel">
|
||||
<h3>Настройки RAG (Retrieval Augmented Generation)</h3>
|
||||
<div class="setting-form">
|
||||
<p>Конфигурация системы поиска и генерации ответов</p>
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
<input type="checkbox" v-model="settings.ragEnabled">
|
||||
Включить RAG
|
||||
</label>
|
||||
</div>
|
||||
<button class="btn btn-primary" @click="saveSettings('rag')">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Панель Каналы -->
|
||||
<div class="sub-settings-panel">
|
||||
<h3>Настройки каналов для ИИ</h3>
|
||||
<div class="setting-form">
|
||||
<p>Управление каналами связи для ИИ</p>
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
<input type="checkbox" v-model="settings.channels.telegram">
|
||||
Telegram
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
<input type="checkbox" v-model="settings.channels.email">
|
||||
Email
|
||||
</label>
|
||||
</div>
|
||||
<button class="btn btn-primary" @click="saveSettings('channels')">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Панель Модели -->
|
||||
<div class="sub-settings-panel">
|
||||
<h3>Настройки моделей ИИ</h3>
|
||||
<div class="setting-form">
|
||||
<p>Выбор и конфигурация моделей ИИ</p>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Модель по умолчанию:</label>
|
||||
<select v-model="settings.defaultModel" class="form-control">
|
||||
<option value="claude-3-haiku">Claude 3 Haiku</option>
|
||||
<option value="claude-3-sonnet">Claude 3 Sonnet</option>
|
||||
<option value="claude-3-opus">Claude 3 Opus</option>
|
||||
<option value="gpt-4o">GPT-4o</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-primary" @click="saveSettings('models')">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// Логика из AISettings.vue
|
||||
import { reactive, onMounted } from 'vue';
|
||||
// TODO: Импортировать API для загрузки/сохранения
|
||||
|
||||
// Локальное состояние настроек
|
||||
const settings = reactive({
|
||||
prompt: '',
|
||||
ragEnabled: false,
|
||||
channels: {
|
||||
telegram: false,
|
||||
email: false
|
||||
},
|
||||
defaultModel: 'claude-3-sonnet'
|
||||
});
|
||||
|
||||
// Загрузка настроек при монтировании
|
||||
onMounted(() => {
|
||||
loadAiSettings();
|
||||
});
|
||||
|
||||
const loadAiSettings = async () => {
|
||||
console.log('[AiSettingsView] Загрузка настроек ИИ...');
|
||||
// TODO: Заменить на реальный вызов API
|
||||
// Пример:
|
||||
// try {
|
||||
// const response = await api.get('/api/settings/ai');
|
||||
// Object.assign(settings, response.data);
|
||||
// } catch (error) {
|
||||
// console.error('Ошибка загрузки настроек ИИ:', error);
|
||||
// }
|
||||
};
|
||||
|
||||
// Сохранение настроек
|
||||
const saveSettings = async (section) => {
|
||||
console.log(`[AiSettingsView] Сохранение настроек раздела: ${section}`);
|
||||
// TODO: Заменить на реальный вызов API
|
||||
// Пример:
|
||||
// try {
|
||||
// const dataToSave = { [section]: settings[section] }; // Или вся группа настроек
|
||||
// await api.post('/api/settings/ai', dataToSave);
|
||||
// // Показать сообщение об успехе
|
||||
// } catch (error) {
|
||||
// console.error('Ошибка сохранения настроек ИИ:', error);
|
||||
// // Показать сообщение об ошибке
|
||||
// }
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.settings-panel {
|
||||
padding: var(--block-padding);
|
||||
background-color: var(--color-light);
|
||||
border-radius: var(--radius-md);
|
||||
margin-top: var(--spacing-lg);
|
||||
animation: fadeIn var(--transition-normal);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
border-bottom: 1px solid var(--color-grey-light);
|
||||
padding-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: var(--spacing-md);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.sub-settings-panel {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
padding-bottom: var(--spacing-lg);
|
||||
border-bottom: 1px dashed var(--color-grey-light);
|
||||
}
|
||||
|
||||
.sub-settings-panel:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.setting-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 0; /* Убираем лишний отступ, т.к. есть gap */
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.form-control {
|
||||
max-width: 500px; /* Ограничим ширину для select/textarea */
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
123
frontend/src/views/settings/BlockchainSettingsView.vue
Normal file
123
frontend/src/views/settings/BlockchainSettingsView.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div class="blockchain-settings settings-panel">
|
||||
<h2>Настройки Блокчейна</h2>
|
||||
|
||||
<!-- Панель Смарт-контракты -->
|
||||
<div class="sub-settings-panel">
|
||||
<h3>Настройки смарт-контрактов</h3>
|
||||
<div class="setting-form">
|
||||
<p>Управление смарт-контрактами</p>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Адрес основного контракта:</label>
|
||||
<input type="text" v-model="settings.contractAddress" class="form-control">
|
||||
</div>
|
||||
<button class="btn btn-primary" @click="saveSettings('smartContract')">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Панель Кворум -->
|
||||
<div class="sub-settings-panel">
|
||||
<h3>Настройки кворума</h3>
|
||||
<div class="setting-form">
|
||||
<p>Настройки кворума для блокчейн-операций</p>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Минимальный кворум (%):</label>
|
||||
<input type="number" v-model="settings.quorumPercent" min="0" max="100" class="form-control">
|
||||
</div>
|
||||
<button class="btn btn-primary" @click="saveSettings('quorum')">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Панель RWA -->
|
||||
<div class="sub-settings-panel">
|
||||
<h3>Настройки Real World Assets (RWA)</h3>
|
||||
<div class="setting-form">
|
||||
<p>Конфигурация для работы с реальными активами</p>
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
<input type="checkbox" v-model="settings.rwaEnabled">
|
||||
Включить поддержку RWA
|
||||
</label>
|
||||
</div>
|
||||
<button class="btn btn-primary" @click="saveSettings('rwa')">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, onMounted } from 'vue';
|
||||
// TODO: Импортировать API
|
||||
|
||||
const settings = reactive({
|
||||
contractAddress: '',
|
||||
quorumPercent: 51,
|
||||
rwaEnabled: false
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
loadBlockchainSettings();
|
||||
});
|
||||
|
||||
const loadBlockchainSettings = async () => {
|
||||
console.log('[BlockchainSettingsView] Загрузка настроек блокчейна...');
|
||||
// TODO: API call
|
||||
};
|
||||
|
||||
const saveSettings = async (section) => {
|
||||
console.log(`[BlockchainSettingsView] Сохранение настроек раздела: ${section}`);
|
||||
// TODO: API call
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.settings-panel {
|
||||
padding: var(--block-padding);
|
||||
background-color: var(--color-light);
|
||||
border-radius: var(--radius-md);
|
||||
margin-top: var(--spacing-lg);
|
||||
animation: fadeIn var(--transition-normal);
|
||||
}
|
||||
h2 {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
border-bottom: 1px solid var(--color-grey-light);
|
||||
padding-bottom: var(--spacing-md);
|
||||
}
|
||||
h3 {
|
||||
margin-bottom: var(--spacing-md);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.sub-settings-panel {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
padding-bottom: var(--spacing-lg);
|
||||
border-bottom: 1px dashed var(--color-grey-light);
|
||||
}
|
||||
.sub-settings-panel:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.setting-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.form-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
.form-control {
|
||||
max-width: 500px;
|
||||
}
|
||||
.btn-primary {
|
||||
align-self: flex-start;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
94
frontend/src/views/settings/InterfaceSettingsView.vue
Normal file
94
frontend/src/views/settings/InterfaceSettingsView.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div class="interface-settings settings-panel">
|
||||
<h2>Настройки Интерфейса</h2>
|
||||
|
||||
<!-- Панель Язык -->
|
||||
<div class="sub-settings-panel">
|
||||
<h3>Настройки языка</h3>
|
||||
<div class="setting-form">
|
||||
<p>Выбор языка интерфейса</p>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Язык интерфейса:</label>
|
||||
<select v-model="selectedLanguage" class="form-control">
|
||||
<option value="ru">Русский</option>
|
||||
<option value="en">English</option>
|
||||
<!-- Добавить другие языки по необходимости -->
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-primary" @click="saveLanguageSetting">Сохранить язык</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Другие настройки интерфейса можно добавить сюда -->
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { getFromStorage, setToStorage } from '../../utils/storage'; // Путь к utils может отличаться
|
||||
// TODO: Импортировать API для сохранения, если нужно
|
||||
|
||||
const selectedLanguage = ref(getFromStorage('userLanguage', 'ru'));
|
||||
|
||||
// Функция сохранения
|
||||
const saveLanguageSetting = () => {
|
||||
setToStorage('userLanguage', selectedLanguage.value);
|
||||
console.log(`[InterfaceSettingsView] Язык сохранен как: ${selectedLanguage.value}`);
|
||||
// TODO: Добавить реальную смену языка (i18n)
|
||||
// TODO: Возможно, отправить на сервер, если язык влияет на бэкенд
|
||||
// alert('Язык сохранен!'); // Пример уведомления
|
||||
};
|
||||
|
||||
// Можно убрать watch, если сохранение происходит только по кнопке
|
||||
// watch(selectedLanguage, (newLang) => {
|
||||
// setToStorage('userLanguage', newLang);
|
||||
// console.log(`[InterfaceSettingsView] Язык изменен на: ${newLang}`);
|
||||
// });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.settings-panel {
|
||||
padding: var(--block-padding);
|
||||
background-color: var(--color-light);
|
||||
border-radius: var(--radius-md);
|
||||
margin-top: var(--spacing-lg);
|
||||
animation: fadeIn var(--transition-normal);
|
||||
}
|
||||
h2 {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
border-bottom: 1px solid var(--color-grey-light);
|
||||
padding-bottom: var(--spacing-md);
|
||||
}
|
||||
h3 {
|
||||
margin-bottom: var(--spacing-md);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.sub-settings-panel {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
padding-bottom: var(--spacing-lg);
|
||||
/* Убираем нижнюю границу, если это последняя панель */
|
||||
}
|
||||
.setting-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.form-label {
|
||||
display: block; /* Можно оставить блочным */
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
.form-control {
|
||||
max-width: 300px; /* Ограничим ширину select */
|
||||
}
|
||||
.btn-primary {
|
||||
align-self: flex-start;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
171
frontend/src/views/settings/SecuritySettingsView.vue
Normal file
171
frontend/src/views/settings/SecuritySettingsView.vue
Normal file
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<div class="security-settings settings-panel">
|
||||
<h2>Настройки Безопасности</h2>
|
||||
|
||||
<!-- Панель Авторизация -->
|
||||
<div class="sub-settings-panel">
|
||||
<h3>Настройки авторизации</h3>
|
||||
<div class="setting-form">
|
||||
<p>Управление параметрами авторизации</p>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Метод авторизации по умолчанию:</label>
|
||||
<select v-model="settings.defaultAuthMethod" class="form-control">
|
||||
<option value="wallet">Кошелек</option>
|
||||
<option value="email">Email</option>
|
||||
<option value="telegram">Telegram</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-primary" @click="saveSettings('authorization')">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Панель RPC -->
|
||||
<div class="sub-settings-panel">
|
||||
<h3>Настройки RPC</h3>
|
||||
<div class="setting-form">
|
||||
<p>Настройка RPC-эндпоинтов</p>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Ethereum RPC:</label>
|
||||
<input type="text" v-model="settings.rpcEndpoints.ethereum" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Polygon RPC:</label>
|
||||
<input type="text" v-model="settings.rpcEndpoints.polygon" class="form-control">
|
||||
</div>
|
||||
<button class="btn btn-primary" @click="saveSettings('rpc')">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Панель Noda -->
|
||||
<div class="sub-settings-panel">
|
||||
<h3>Настройки Noda</h3>
|
||||
<div class="setting-form">
|
||||
<p>Конфигурация нод</p>
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
<input type="checkbox" v-model="settings.useCustomNode">
|
||||
Использовать собственную ноду
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group" v-if="settings.useCustomNode">
|
||||
<label class="form-label">URL собственной ноды:</label>
|
||||
<input type="text" v-model="settings.customNodeUrl" class="form-control">
|
||||
</div>
|
||||
<button class="btn btn-primary" @click="saveSettings('noda')">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Панель Хранилище -->
|
||||
<div class="sub-settings-panel">
|
||||
<h3>Настройки хранилища</h3>
|
||||
<div class="setting-form">
|
||||
<p>Настройки для хранения данных</p>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Тип хранилища:</label>
|
||||
<select v-model="settings.storageType" class="form-control">
|
||||
<option value="local">Локальное</option>
|
||||
<option value="ipfs">IPFS</option>
|
||||
<option value="arweave">Arweave</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-primary" @click="saveSettings('storage')">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Панель CPU -->
|
||||
<div class="sub-settings-panel">
|
||||
<h3>Настройки CPU</h3>
|
||||
<div class="setting-form">
|
||||
<p>Оптимизация использования CPU</p>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Максимальное использование CPU (%):</label>
|
||||
<input type="number" v-model="settings.maxCpuUsage" min="0" max="100" class="form-control">
|
||||
</div>
|
||||
<button class="btn btn-primary" @click="saveSettings('cpu')">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, onMounted } from 'vue';
|
||||
// TODO: Импортировать API
|
||||
|
||||
const settings = reactive({
|
||||
defaultAuthMethod: 'wallet',
|
||||
rpcEndpoints: {
|
||||
ethereum: '',
|
||||
polygon: ''
|
||||
},
|
||||
useCustomNode: false,
|
||||
customNodeUrl: '',
|
||||
storageType: 'local',
|
||||
maxCpuUsage: 80
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
loadSecuritySettings();
|
||||
});
|
||||
|
||||
const loadSecuritySettings = async () => {
|
||||
console.log('[SecuritySettingsView] Загрузка настроек безопасности...');
|
||||
// TODO: API call
|
||||
};
|
||||
|
||||
const saveSettings = async (section) => {
|
||||
console.log(`[SecuritySettingsView] Сохранение настроек раздела: ${section}`);
|
||||
// TODO: API call
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.settings-panel {
|
||||
padding: var(--block-padding);
|
||||
background-color: var(--color-light);
|
||||
border-radius: var(--radius-md);
|
||||
margin-top: var(--spacing-lg);
|
||||
animation: fadeIn var(--transition-normal);
|
||||
}
|
||||
h2 {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
border-bottom: 1px solid var(--color-grey-light);
|
||||
padding-bottom: var(--spacing-md);
|
||||
}
|
||||
h3 {
|
||||
margin-bottom: var(--spacing-md);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.sub-settings-panel {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
padding-bottom: var(--spacing-lg);
|
||||
border-bottom: 1px dashed var(--color-grey-light);
|
||||
}
|
||||
.sub-settings-panel:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.setting-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.form-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
.form-control {
|
||||
max-width: 500px;
|
||||
}
|
||||
.btn-primary {
|
||||
align-self: flex-start;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user