ваше сообщение коммита
This commit is contained in:
@@ -12,7 +12,8 @@
|
|||||||
"lint:style": "stylelint \"**/*.{vue,css}\"",
|
"lint:style": "stylelint \"**/*.{vue,css}\"",
|
||||||
"lint:style:fix": "stylelint \"**/*.{vue,css}\" --fix",
|
"lint:style:fix": "stylelint \"**/*.{vue,css}\" --fix",
|
||||||
"format": "prettier --write \"**/*.{js,vue,json,md}\"",
|
"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": {
|
"dependencies": {
|
||||||
"axios": "^1.8.4",
|
"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 class="loading-spinner" />
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch, onMounted, computed } from 'vue';
|
||||||
import { RouterView } from 'vue-router';
|
import { RouterView } from 'vue-router';
|
||||||
import { useAuth } from './composables/useAuth';
|
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);
|
const isLoading = ref(false);
|
||||||
@@ -20,10 +36,111 @@
|
|||||||
// Использование composable для аутентификации
|
// Использование composable для аутентификации
|
||||||
const auth = useAuth();
|
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) => {
|
watch(auth.isAuthenticated, (isAuth) => {
|
||||||
if (newValue !== oldValue) {
|
console.log('[App] Состояние аутентификации изменилось:', isAuth);
|
||||||
console.log('[App] Состояние аутентификации изменилось:', newValue);
|
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>
|
</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 */
|
/* frontend/src/assets/styles/variables.css */
|
||||||
:root {
|
:root {
|
||||||
/* Цвета */
|
/*
|
||||||
--color-primary: #4CAF50;
|
* ЦВЕТОВАЯ СХЕМА
|
||||||
--color-primary-dark: #45a049;
|
* Основная палитра цветов приложения
|
||||||
--color-secondary: #2196F3;
|
*/
|
||||||
--color-danger: #F44336;
|
--color-primary: #4CAF50; /* Основной цвет (зеленый) */
|
||||||
--color-warning: #FF9800;
|
--color-primary-dark: #45a049; /* Темно-зеленый для наведения и акцентов */
|
||||||
--color-light: #f5f5f5;
|
--color-secondary: #2196F3; /* Второстепенный цвет (синий) */
|
||||||
--color-dark: #333333;
|
--color-accent: #5E35B1; /* Акцентный цвет (фиолетовый) */
|
||||||
--color-grey: #777777;
|
--color-accent-dark: #4527A0; /* Темно-фиолетовый для наведения */
|
||||||
--color-grey-light: #e0e0e0;
|
|
||||||
--color-white: #ffffff;
|
/* Статусные цвета */
|
||||||
--color-black: #000000;
|
--color-danger: #F44336; /* Ошибки, удаление, опасные действия */
|
||||||
--color-telegram: #0088cc;
|
--color-warning: #FF9800; /* Предупреждения */
|
||||||
--color-error: #e74c3c;
|
--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-user-message: #EFFAFF; /* Фон сообщений пользователя */
|
||||||
--color-ai-message: #F8F8F8;
|
--color-ai-message: #F8F8F8; /* Фон сообщений ИИ */
|
||||||
--color-system-message: #FFF3E0;
|
--color-system-message: #FFF3E0; /* Фон системных сообщений */
|
||||||
--color-system-text: #FF5722;
|
--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-xs: 5px; /* Очень маленькие отступы (между близкими элементами) */
|
||||||
--spacing-xl: 30px;
|
--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-xs: 12px; /* Маленькие надписи, подписи к полям */
|
||||||
--font-size-xl: 18px;
|
--font-size-sm: 13px; /* Второстепенный текст */
|
||||||
--font-size-xxl: 24px;
|
--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;
|
--nav-btn-size: 40px;
|
||||||
--chat-input-min-height: 100px;
|
--chat-input-min-height: 100px;
|
||||||
--chat-input-max-height: 200px;
|
--chat-input-max-height: 200px;
|
||||||
--chat-input-focus-min-height: 170px;
|
--chat-input-focus-min-height: 170px;
|
||||||
--chat-input-focus-max-height: 300px;
|
--chat-input-focus-max-height: 300px;
|
||||||
|
|
||||||
/* Унифицированные размеры для кнопок и форм */
|
/*
|
||||||
|
* УНИФИЦИРОВАННЫЕ РАЗМЕРЫ
|
||||||
|
* Для кнопок и форм
|
||||||
|
*/
|
||||||
--button-height: 48px;
|
--button-height: 48px;
|
||||||
--button-height-mobile: 42px;
|
--button-height-mobile: 42px;
|
||||||
--button-padding: 0 var(--spacing-lg);
|
--button-padding: 0 var(--spacing-lg);
|
||||||
--button-gap: var(--spacing-md);
|
--button-gap: var(--spacing-md);
|
||||||
|
|
||||||
--form-gap: var(--spacing-md);
|
--form-gap: var(--spacing-md);
|
||||||
|
|
||||||
--block-padding: 24px;
|
--block-padding: 24px;
|
||||||
--block-padding-mobile: 16px;
|
--block-padding-mobile: 16px;
|
||||||
--block-margin: 24px;
|
--block-margin: 24px;
|
||||||
--block-margin-mobile: 16px;
|
--block-margin-mobile: 16px;
|
||||||
|
|
||||||
--input-height: 48px;
|
--input-height: 48px;
|
||||||
--input-height-mobile: 42px;
|
--input-height-mobile: 42px;
|
||||||
--input-padding: 0 var(--spacing-lg);
|
--input-padding: 0 var(--spacing-lg);
|
||||||
|
|
||||||
/* Общие стили */
|
/*
|
||||||
|
* ОБЩИЕ СТИЛИ
|
||||||
|
* Производные параметры для единого стиля
|
||||||
|
*/
|
||||||
--button-radius: var(--radius-lg);
|
--button-radius: var(--radius-lg);
|
||||||
--input-radius: var(--radius-lg);
|
--input-radius: var(--radius-lg);
|
||||||
--block-radius: var(--radius-lg);
|
--block-radius: var(--radius-lg);
|
||||||
|
|||||||
@@ -15,11 +15,12 @@
|
|||||||
<!-- Правая панель с информацией о кошельке -->
|
<!-- Правая панель с информацией о кошельке -->
|
||||||
<Sidebar
|
<Sidebar
|
||||||
v-model="showWalletSidebar"
|
v-model="showWalletSidebar"
|
||||||
:is-authenticated="auth.isAuthenticated.value"
|
:is-authenticated="isAuthenticated"
|
||||||
:telegram-auth="telegramAuth"
|
:telegram-auth="telegramAuth"
|
||||||
:email-auth="emailAuth"
|
:email-auth="emailAuth"
|
||||||
:token-balances="tokenBalances.value"
|
:token-balances="tokenBalances"
|
||||||
:identities="auth.identities?.value"
|
:identities="identities"
|
||||||
|
:is-loading-tokens="isLoadingTokens"
|
||||||
@wallet-auth="handleWalletAuth"
|
@wallet-auth="handleWalletAuth"
|
||||||
@disconnect-wallet="disconnectWallet"
|
@disconnect-wallet="disconnectWallet"
|
||||||
@telegram-auth="handleTelegramAuth"
|
@telegram-auth="handleTelegramAuth"
|
||||||
@@ -36,9 +37,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch, onBeforeUnmount } from 'vue';
|
import { ref, onMounted, watch, onBeforeUnmount, defineProps, defineEmits } from 'vue';
|
||||||
import { useAuth } from '../composables/useAuth';
|
import { useAuth } from '../composables/useAuth';
|
||||||
import { useTokenBalances } from '../composables/useTokenBalances';
|
|
||||||
import { useAuthFlow } from '../composables/useAuthFlow';
|
import { useAuthFlow } from '../composables/useAuthFlow';
|
||||||
import { useNotifications } from '../composables/useNotifications';
|
import { useNotifications } from '../composables/useNotifications';
|
||||||
import { getFromStorage, setToStorage, removeFromStorage } from '../utils/storage';
|
import { getFromStorage, setToStorage, removeFromStorage } from '../utils/storage';
|
||||||
@@ -55,7 +55,17 @@ import NotificationDisplay from './NotificationDisplay.vue';
|
|||||||
|
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
const { notifications, showSuccessMessage, showErrorMessage } = useNotifications();
|
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
|
// Callback после успешной аутентификации/привязки через Email/Telegram
|
||||||
const handleAuthFlowSuccess = (authType) => {
|
const handleAuthFlowSuccess = (authType) => {
|
||||||
@@ -102,7 +112,7 @@ const handleWalletAuth = async () => {
|
|||||||
const linkResult = await auth.linkIdentity('wallet', result.address);
|
const linkResult = await auth.linkIdentity('wallet', result.address);
|
||||||
if (linkResult.success) {
|
if (linkResult.success) {
|
||||||
showSuccessMessage('Кошелек успешно подключен к вашему аккаунту!');
|
showSuccessMessage('Кошелек успешно подключен к вашему аккаунту!');
|
||||||
await auth.checkAuth(); // Обновить identities
|
emit('auth-action-completed');
|
||||||
} else {
|
} else {
|
||||||
showErrorMessage(linkResult.error || 'Не удалось подключить кошелек');
|
showErrorMessage(linkResult.error || 'Не удалось подключить кошелек');
|
||||||
}
|
}
|
||||||
@@ -112,12 +122,7 @@ const handleWalletAuth = async () => {
|
|||||||
if (authResponse.authenticated && authResponse.authType === 'wallet') {
|
if (authResponse.authenticated && authResponse.authType === 'wallet') {
|
||||||
console.log('[BaseLayout] Кошелёк успешно подключен и аутентифицирован');
|
console.log('[BaseLayout] Кошелёк успешно подключен и аутентифицирован');
|
||||||
showSuccessMessage('Кошелёк успешно подключен!');
|
showSuccessMessage('Кошелёк успешно подключен!');
|
||||||
// Оповещаем компоненты об успешной авторизации
|
emit('auth-action-completed');
|
||||||
eventBus.emit('auth-state-changed', {
|
|
||||||
isAuthenticated: true,
|
|
||||||
authType: 'wallet',
|
|
||||||
fromBaseLayout: true
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
showErrorMessage('Не удалось завершить аутентификацию через кошелек.');
|
showErrorMessage('Не удалось завершить аутентификацию через кошелек.');
|
||||||
}
|
}
|
||||||
@@ -141,16 +146,10 @@ const disconnectWallet = async () => {
|
|||||||
console.log('[BaseLayout] Выполняется выход из системы...');
|
console.log('[BaseLayout] Выполняется выход из системы...');
|
||||||
try {
|
try {
|
||||||
await api.post('/api/auth/logout');
|
await api.post('/api/auth/logout');
|
||||||
await auth.checkAuth();
|
|
||||||
showSuccessMessage('Вы успешно вышли из системы');
|
showSuccessMessage('Вы успешно вышли из системы');
|
||||||
removeFromStorage('guestMessages');
|
removeFromStorage('guestMessages');
|
||||||
removeFromStorage('hasUserSentMessage');
|
removeFromStorage('hasUserSentMessage');
|
||||||
|
emit('auth-action-completed');
|
||||||
// Оповещаем компоненты о выходе из системы
|
|
||||||
eventBus.emit('auth-state-changed', {
|
|
||||||
isAuthenticated: false,
|
|
||||||
fromBaseLayout: true
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[BaseLayout] Ошибка при выходе из системы:', error);
|
console.error('[BaseLayout] Ошибка при выходе из системы:', error);
|
||||||
showErrorMessage('Произошла ошибка при выходе из системы');
|
showErrorMessage('Произошла ошибка при выходе из системы');
|
||||||
@@ -206,9 +205,31 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Адаптивный дизайн */
|
/* Адаптивный дизайн */
|
||||||
|
@media (max-width: 1199px) {
|
||||||
|
.main-content {
|
||||||
|
max-width: calc(100% - 320px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.main-content {
|
.main-content {
|
||||||
max-width: 100%;
|
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>
|
</style>
|
||||||
@@ -437,16 +437,14 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* Стили здесь для инкапсуляции, можно вынести в home.css */
|
|
||||||
.chat-container {
|
.chat-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin: var(--spacing-lg) auto;
|
margin: var(--spacing-lg) 0;
|
||||||
padding: 0; /* Убираю padding, так как он теперь задается через .main-content */
|
padding: 0;
|
||||||
min-height: 500px; /* Или другая подходящая высота */
|
min-height: 500px;
|
||||||
max-width: 1150px; /* Ограничиваем ширину чата */
|
width: 100%;
|
||||||
width: 100%; /* Занимаем всю доступную ширину до максимума */
|
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,7 +460,7 @@ onUnmounted(() => {
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 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);
|
transition: bottom var(--transition-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -482,16 +480,32 @@ onUnmounted(() => {
|
|||||||
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.05);
|
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Стили для textarea, связанные с авто-ресайзом (дублируют home.css, но можно оставить для явности) */
|
|
||||||
.chat-input textarea {
|
.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;
|
overflow-y: hidden;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-input textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
.input-area {
|
.input-area {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
gap: var(--spacing-sm);
|
gap: var(--spacing-sm);
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-icons {
|
.chat-icons {
|
||||||
@@ -574,7 +588,7 @@ onUnmounted(() => {
|
|||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
border-top: 1px solid var(--color-grey-light);
|
border-top: 1px solid var(--color-grey-light);
|
||||||
max-height: 100px; /* Можно увеличить, если нужно больше места */
|
max-height: 100px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -622,4 +636,57 @@ onUnmounted(() => {
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
padding: 0;
|
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>
|
</style>
|
||||||
@@ -65,7 +65,7 @@ onBeforeUnmount(() => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.header {
|
.header {
|
||||||
background-color: var(--color-white);
|
background-color: var(--color-white);
|
||||||
padding: 15px 20px;
|
padding: 15px 20px; /* Возвращаем горизонтальный padding */
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 100; /* Ensure header stays on top */
|
z-index: 100; /* Ensure header stays on top */
|
||||||
@@ -75,8 +75,7 @@ onBeforeUnmount(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
max-width: 1200px; /* Optional: limit max width */
|
/* Убираем max-width, margin, padding */
|
||||||
margin: 0 auto; /* Optional: center content */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-text {
|
.header-text {
|
||||||
@@ -144,8 +143,10 @@ onBeforeUnmount(() => {
|
|||||||
top: 6px;
|
top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Удаляем стили для трансформации бургера в крестик */
|
||||||
|
/*
|
||||||
.header-wallet-btn.active .hamburger-line {
|
.header-wallet-btn.active .hamburger-line {
|
||||||
background-color: transparent; /* Hide middle line */
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-wallet-btn.active .hamburger-line::before {
|
.header-wallet-btn.active .hamburger-line::before {
|
||||||
@@ -157,6 +158,7 @@ onBeforeUnmount(() => {
|
|||||||
top: 0;
|
top: 0;
|
||||||
transform: rotate(-45deg);
|
transform: rotate(-45deg);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
.nav-btn-text {
|
.nav-btn-text {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ const formatFileSize = (bytes) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* Стили можно скопировать из home.css или оставить глобальными */
|
/* Стили сообщений, полностью перенесенные из home.css */
|
||||||
.message {
|
.message {
|
||||||
margin-bottom: var(--spacing-md);
|
margin-bottom: var(--spacing-md);
|
||||||
padding: var(--spacing-sm) var(--spacing-md);
|
padding: var(--spacing-sm) var(--spacing-md);
|
||||||
@@ -240,7 +240,7 @@ const formatFileSize = (bytes) => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: var(--spacing-xs); /* Добавлен отступ сверху */
|
margin-top: var(--spacing-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-time {
|
.message-time {
|
||||||
@@ -272,7 +272,7 @@ const formatFileSize = (bytes) => {
|
|||||||
border: 1px solid var(--color-danger);
|
border: 1px solid var(--color-danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- НОВЫЕ СТИЛИ --- */
|
/* Стили для вложений */
|
||||||
.message-attachments {
|
.message-attachments {
|
||||||
margin-top: var(--spacing-sm);
|
margin-top: var(--spacing-sm);
|
||||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
@@ -281,19 +281,19 @@ const formatFileSize = (bytes) => {
|
|||||||
|
|
||||||
.attachment-item {
|
.attachment-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column; /* Отображаем элементы в столбец */
|
flex-direction: column;
|
||||||
align-items: flex-start; /* Выравниваем по левому краю */
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-preview {
|
.attachment-preview {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 300px; /* Ограничение высоты для превью */
|
max-height: 300px;
|
||||||
margin-bottom: var(--spacing-xs);
|
margin-bottom: var(--spacing-xs);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-preview {
|
.image-preview {
|
||||||
object-fit: cover; /* Сохраняем пропорции */
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.audio-preview {
|
.audio-preview {
|
||||||
@@ -301,7 +301,7 @@ const formatFileSize = (bytes) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.video-preview {
|
.video-preview {
|
||||||
/* Стили для видео по умолчанию */
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-preview {
|
.file-preview {
|
||||||
@@ -317,16 +317,47 @@ const formatFileSize = (bytes) => {
|
|||||||
.attachment-name {
|
.attachment-name {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-right: var(--spacing-xs);
|
margin-right: var(--spacing-xs);
|
||||||
color: var(--color-primary); /* Делаем имя файла похожим на ссылку */
|
color: var(--color-primary);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-name:hover {
|
.attachment-name:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-size {
|
.attachment-size {
|
||||||
color: var(--color-grey);
|
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>
|
</style>
|
||||||
@@ -33,25 +33,69 @@
|
|||||||
<!-- Навигационные кнопки -->
|
<!-- Навигационные кнопки -->
|
||||||
<div class="navigation-buttons">
|
<div class="navigation-buttons">
|
||||||
<router-link to="/" class="nav-link-btn" active-class="active">
|
<router-link to="/" class="nav-link-btn" active-class="active">
|
||||||
<i class="nav-icon">💬</i>
|
|
||||||
<span>Чат</span>
|
<span>Чат</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link to="/crm" class="nav-link-btn" active-class="active">
|
<router-link to="/crm" class="nav-link-btn" active-class="active">
|
||||||
<i class="nav-icon">👥</i>
|
|
||||||
<span>CRM</span>
|
<span>CRM</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link to="/settings" class="nav-link-btn" active-class="active">
|
<router-link to="/settings" class="nav-link-btn" active-class="active">
|
||||||
<i class="nav-icon">⚙️</i>
|
|
||||||
<span>Настройки</span>
|
<span>Настройки</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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 { TOKEN_CONTRACTS } from '../services/tokens';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import eventBus from '../utils/eventBus';
|
import eventBus from '../utils/eventBus';
|
||||||
@@ -63,7 +107,8 @@ const props = defineProps({
|
|||||||
telegramAuth: Object,
|
telegramAuth: Object,
|
||||||
emailAuth: Object,
|
emailAuth: Object,
|
||||||
tokenBalances: Object,
|
tokenBalances: Object,
|
||||||
identities: Array
|
identities: Array,
|
||||||
|
isLoadingTokens: Boolean
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'wallet-auth', 'disconnect-wallet']);
|
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);
|
const identity = props.identities.find((identity) => identity.provider === type);
|
||||||
return identity ? identity.provider_id : null;
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<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 {
|
.button-with-close {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -167,15 +267,17 @@ const getIdentityValue = (type) => {
|
|||||||
.nav-link-btn {
|
.nav-link-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
height: 48px;
|
||||||
background-color: var(--color-light);
|
background-color: var(--color-light);
|
||||||
color: var(--color-dark);
|
color: var(--color-dark);
|
||||||
border: 1px solid var(--color-grey-light);
|
border: 1px solid var(--color-grey-light);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
padding: 12px 15px;
|
padding: 0 15px;
|
||||||
font-size: var(--font-size-md);
|
font-size: var(--font-size-md);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: all var(--transition-normal);
|
transition: all var(--transition-normal);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link-btn.active {
|
.nav-link-btn.active {
|
||||||
@@ -188,9 +290,109 @@ const getIdentityValue = (type) => {
|
|||||||
background-color: var(--color-grey-light);
|
background-color: var(--color-grey-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-icon {
|
/* Стили для общих кнопок аутентификации/действий в сайдбаре */
|
||||||
margin-right: 10px;
|
.auth-btn {
|
||||||
font-size: 1.2em;
|
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) {
|
@media screen and (max-width: 480px) {
|
||||||
@@ -201,8 +403,14 @@ const getIdentityValue = (type) => {
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-btn {
|
||||||
|
height: 42px;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
.nav-link-btn {
|
.nav-link-btn {
|
||||||
padding: 10px 12px;
|
height: 42px;
|
||||||
|
padding: 0 12px;
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -215,8 +423,13 @@ const getIdentityValue = (type) => {
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-btn {
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
.nav-link-btn {
|
.nav-link-btn {
|
||||||
padding: 8px 10px;
|
height: 36px;
|
||||||
|
padding: 0 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router';
|
import { createRouter, createWebHistory } from 'vue-router';
|
||||||
import HomeView from '../views/HomeView.vue';
|
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';
|
import axios from 'axios';
|
||||||
|
|
||||||
console.log('router/index.js: Script loaded');
|
console.log('router/index.js: Script loaded');
|
||||||
@@ -19,6 +24,35 @@ const routes = [
|
|||||||
path: '/settings',
|
path: '/settings',
|
||||||
name: 'settings',
|
name: 'settings',
|
||||||
component: () => import('../views/SettingsView.vue'),
|
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>
|
<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">
|
<div class="crm-view-container">
|
||||||
<h1>CRM Система</h1>
|
<h1>CRM Система</h1>
|
||||||
<div v-if="isLoading">Загрузка данных пользователя...</div>
|
<div v-if="isLoading">Загрузка данных пользователя...</div>
|
||||||
@@ -29,13 +35,24 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
import { ref, onMounted, onBeforeUnmount, defineProps, defineEmits } from 'vue';
|
||||||
import { useAuth } from '../composables/useAuth';
|
import { useAuth } from '../composables/useAuth';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { setToStorage } from '../utils/storage';
|
import { setToStorage } from '../utils/storage';
|
||||||
import BaseLayout from '../components/BaseLayout.vue';
|
import BaseLayout from '../components/BaseLayout.vue';
|
||||||
import eventBus from '../utils/eventBus';
|
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 auth = useAuth();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isLoading = ref(true);
|
const isLoading = ref(true);
|
||||||
@@ -74,12 +91,12 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.crm-view-container {
|
.crm-view-container {
|
||||||
max-width: 1150px;
|
|
||||||
margin: 20px auto;
|
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background-color: var(--color-white);
|
background-color: var(--color-white);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<BaseLayout>
|
<BaseLayout
|
||||||
|
:is-authenticated="isAuthenticated"
|
||||||
|
:identities="identities"
|
||||||
|
:token-balances="tokenBalances"
|
||||||
|
:is-loading-tokens="isLoadingTokens"
|
||||||
|
@auth-action-completed="$emit('auth-action-completed')"
|
||||||
|
>
|
||||||
<ChatInterface
|
<ChatInterface
|
||||||
:messages="messages"
|
:messages="messages"
|
||||||
:is-loading="isLoading || isConnectingWallet"
|
:is-loading="isLoading || isConnectingWallet"
|
||||||
@@ -13,17 +19,27 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch, onBeforeUnmount } from 'vue';
|
import { ref, onMounted, watch, onBeforeUnmount, defineProps, defineEmits } from 'vue';
|
||||||
import { useAuth } from '../composables/useAuth';
|
import { useAuth } from '../composables/useAuth';
|
||||||
import { useChat } from '../composables/useChat';
|
import { useChat } from '../composables/useChat';
|
||||||
import { connectWithWallet } from '../services/wallet';
|
import { connectWithWallet } from '../services/wallet';
|
||||||
import eventBus from '../utils/eventBus';
|
import eventBus from '../utils/eventBus';
|
||||||
import '../assets/styles/home.css';
|
|
||||||
import BaseLayout from '../components/BaseLayout.vue';
|
import BaseLayout from '../components/BaseLayout.vue';
|
||||||
import ChatInterface from '../components/ChatInterface.vue';
|
import ChatInterface from '../components/ChatInterface.vue';
|
||||||
|
|
||||||
console.log('HomeView.vue: Using BaseLayout');
|
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. ИСПОЛЬЗОВАНИЕ COMPOSABLES
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
|
|||||||
@@ -1,192 +1,64 @@
|
|||||||
<template>
|
<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">
|
<div class="settings-view-container">
|
||||||
<h1>Настройки</h1>
|
<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-if="isLoading">Загрузка данных пользователя...</div>
|
||||||
<div v-else-if="!auth.isAuthenticated.value">
|
<div v-else-if="!auth.isAuthenticated.value">
|
||||||
<p>Для доступа к настройкам необходимо <button @click="goToHomeAndShowSidebar">войти</button>.</p>
|
<p>Для доступа к настройкам необходимо <button @click="goToHomeAndShowSidebar">войти</button>.</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else class="settings-navigation-buttons">
|
||||||
<p>Добро пожаловать в Настройки!</p>
|
<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 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>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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 { useAuth } from '../composables/useAuth';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { getFromStorage, setToStorage } from '../utils/storage';
|
import { getFromStorage, setToStorage } from '../utils/storage';
|
||||||
import BaseLayout from '../components/BaseLayout.vue';
|
import BaseLayout from '../components/BaseLayout.vue';
|
||||||
import eventBus from '../utils/eventBus';
|
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 auth = useAuth();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isLoading = ref(true);
|
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) => {
|
const handleAuthEvent = (eventData) => {
|
||||||
console.log('[SettingsView] Получено событие изменения авторизации:', eventData);
|
console.log('[SettingsView] Получено событие изменения авторизации:', eventData);
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
|
|
||||||
// При изменении авторизации обновляем балансы
|
|
||||||
if (eventData.isAuthenticated) {
|
|
||||||
setTimeout(() => {
|
|
||||||
refreshTokenBalances(); // Небольшая задержка для обновления идентификаторов
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Сохраняем язык при изменении
|
|
||||||
watch(selectedLanguage, (newLang) => {
|
|
||||||
setToStorage('userLanguage', newLang);
|
|
||||||
// TODO: Добавить логику для реальной смены языка интерфейса (например, через i18n)
|
|
||||||
console.log(`[Settings] Язык изменен на: ${newLang}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
const goToHomeAndShowSidebar = () => {
|
const goToHomeAndShowSidebar = () => {
|
||||||
setToStorage('showWalletSidebar', true);
|
setToStorage('showWalletSidebar', true);
|
||||||
router.push({ name: 'home' });
|
router.push({ name: 'home' });
|
||||||
@@ -201,11 +73,6 @@ onMounted(() => {
|
|||||||
|
|
||||||
// Подписка на события авторизации
|
// Подписка на события авторизации
|
||||||
unsubscribe = eventBus.on('auth-state-changed', handleAuthEvent);
|
unsubscribe = eventBus.on('auth-state-changed', handleAuthEvent);
|
||||||
|
|
||||||
// Обновляем данные о балансе токенов при загрузке
|
|
||||||
if (auth.isAuthenticated.value && hasIdentityType('wallet')) {
|
|
||||||
refreshTokenBalances();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@@ -214,161 +81,90 @@ onBeforeUnmount(() => {
|
|||||||
unsubscribe();
|
unsubscribe();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Следим за состоянием авторизации для очистки данных при выходе
|
|
||||||
watch(() => auth.isAuthenticated.value, (isAuth) => {
|
|
||||||
if (!isAuth) {
|
|
||||||
// Очищаем данные при выходе из системы
|
|
||||||
tokenBalances.value = {};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* Основной контейнер */
|
||||||
.settings-view-container {
|
.settings-view-container {
|
||||||
max-width: 1150px;
|
padding: var(--block-padding);
|
||||||
margin: 20px auto;
|
|
||||||
padding: 20px;
|
|
||||||
background-color: var(--color-white);
|
background-color: var(--color-white);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--block-radius);
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
margin-top: 20px;
|
||||||
|
|
||||||
h1 {
|
|
||||||
color: var(--color-dark);
|
|
||||||
margin-bottom: 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 {
|
p {
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
margin-bottom: 10px;
|
margin-bottom: var(--spacing-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
strong {
|
strong {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.general-settings {
|
/* Стили для кнопки "войти" */
|
||||||
margin-top: 30px;
|
.settings-view-container > div > p > button {
|
||||||
padding-top: 20px;
|
background-color: var(--color-accent);
|
||||||
|
color: var(--color-white);
|
||||||
|
border: none;
|
||||||
|
padding: var(--spacing-xs) var(--spacing-sm);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color var(--transition-fast);
|
||||||
|
margin-left: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-view-container > div > p > button:hover {
|
||||||
|
background-color: var(--color-accent-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Новые стили для кнопок навигации */
|
||||||
|
.settings-navigation-buttons {
|
||||||
|
margin-top: var(--spacing-lg);
|
||||||
|
padding-top: var(--spacing-lg);
|
||||||
border-top: 1px solid var(--color-grey-light);
|
border-top: 1px solid var(--color-grey-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
.buttons-grid {
|
||||||
display: block;
|
display: grid;
|
||||||
margin-bottom: 10px;
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: var(--spacing-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
.buttons-grid .btn {
|
||||||
padding: 8px;
|
width: 100%;
|
||||||
border: 1px solid var(--color-grey);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
padding: 5px 10px;
|
|
||||||
background-color: var(--color-primary);
|
|
||||||
color: var(--color-white);
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
cursor: pointer;
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
button:disabled {
|
|
||||||
background-color: var(--color-grey);
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Стили для блока информации о пользователе */
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-info-item {
|
|
||||||
margin-top: 10px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-info-label {
|
|
||||||
min-width: 100px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--color-grey-dark);
|
justify-content: center;
|
||||||
font-style: italic;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Удаляем стили для кнопки обновления баланса, поскольку больше не нужны */
|
/* Анимации */
|
||||||
.token-action,
|
@keyframes fadeIn {
|
||||||
.refresh-btn {
|
from { opacity: 0; }
|
||||||
display: none;
|
to { opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-info-empty,
|
/* Адаптивный дизайн */
|
||||||
.token-no-data {
|
@media (max-width: 768px) {
|
||||||
margin: 15px 0;
|
.settings-navigation-buttons {
|
||||||
padding: 10px;
|
grid-template-columns: 1fr;
|
||||||
text-align: center;
|
}
|
||||||
color: var(--color-grey-dark);
|
|
||||||
font-style: italic;
|
.buttons-grid {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</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