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

This commit is contained in:
2025-12-23 12:25:12 +03:00
parent 8fe63beb8f
commit c02d0a38ac
37 changed files with 883 additions and 1131 deletions

View File

@@ -1553,4 +1553,109 @@ router.get('/public/:id', async (req, res) => {
} }
}); });
// Endpoint для robots.txt
router.get('/public/robots.txt', async (req, res) => {
try {
const domain = req.get('host') || req.headers.host || 'localhost';
const protocol = req.protocol || 'https';
const baseUrl = `${protocol}://${domain}`;
const robotsContent = `User-agent: *
Allow: /
Allow: /content/published
Disallow: /api/
Disallow: /ws
Disallow: /admin/
Disallow: /content/create
Disallow: /content/edit
Sitemap: ${baseUrl}/sitemap.xml
`;
res.setHeader('Content-Type', 'text/plain');
res.send(robotsContent);
} catch (error) {
console.error('Ошибка генерации robots.txt:', error);
res.status(500).send('Error generating robots.txt');
}
});
// Endpoint для sitemap.xml
router.get('/public/sitemap.xml', async (req, res) => {
try {
const tableName = `admin_pages_simple`;
const domain = req.get('host') || req.headers.host || 'localhost';
const protocol = req.protocol || 'https';
const baseUrl = `${protocol}://${domain}`;
// Проверяем, есть ли таблица
const existsRes = await db.getQuery()(
`SELECT to_regclass($1) as exists`, [tableName]
);
let pages = [];
if (existsRes.rows[0].exists) {
// Получаем все опубликованные публичные страницы
const { rows } = await db.getQuery()(`
SELECT id, title, updated_at, created_at
FROM ${tableName}
WHERE status = 'published' AND visibility = 'public'
ORDER BY created_at DESC
`);
pages = rows;
}
// Генерируем XML sitemap
let sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>${baseUrl}/</loc>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>${baseUrl}/content/published</loc>
<changefreq>daily</changefreq>
<priority>0.8</priority>
</url>
`;
// Добавляем страницы документов
for (const page of pages) {
const lastmod = page.updated_at || page.created_at || new Date().toISOString();
const pageUrl = `${baseUrl}/content/published?page=${page.id}`;
sitemap += ` <url>
<loc>${escapeXml(pageUrl)}</loc>
<lastmod>${lastmod.split('T')[0]}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
</url>
`;
}
sitemap += `</urlset>`;
res.setHeader('Content-Type', 'application/xml');
res.send(sitemap);
} catch (error) {
console.error('Ошибка генерации sitemap.xml:', error);
res.status(500).send('Error generating sitemap.xml');
}
});
// Вспомогательная функция для экранирования XML
function escapeXml(unsafe) {
return unsafe.replace(/[<>&'"]/g, (c) => {
switch (c) {
case '<': return '&lt;';
case '>': return '&gt;';
case '&': return '&amp;';
case '\'': return '&apos;';
case '"': return '&quot;';
default: return c;
}
});
}
module.exports = router; module.exports = router;

View File

@@ -1,884 +0,0 @@
<!--
Copyright (c) 2024-2025 Тарабанов Александр Викторович
All rights reserved.
This software is proprietary and confidential.
Unauthorized copying, modification, or distribution is prohibited.
For licensing inquiries: info@hb3-accelerator.com
Website: https://hb3-accelerator.com
GitHub: https://github.com/VC-HB3-Accelerator
-->
# Обзор приложения Digital Legal Entity (DLE)
## 📋 Содержание
1. [Установка приложения](#установка-приложения)
2. [Настройка безопасности](#настройка-безопасности)
3. [Модули и микросервисная архитектура](#модули-и-микросервисная-архитектура)
4. [Корпоративный мессенджер с ИИ ассистентом](#корпоративный-мессенджер-с-ии-ассистентом)
5. [Настройки приложения для работы с интернет пользователями](#настройки-приложения-для-работы-с-интернет-пользователями)
6. [Требования регулятора](#требования-регулятора)
7. [Обновления софта](#обновления-софта)
---
## 🚀 Установка приложения
### Быстрый старт
Digital Legal Entity (DLE) разворачивается с помощью Docker Compose, что обеспечивает простую и надежную установку на любой платформе.
### Системные требования
**Минимальные требования**:
- **CPU**: 4 ядра
- **RAM**: 12 GB (4 GB приложение + 6 GB AI + 2 GB Vector Search)
- **Хранилище**: 100 GB SSD
- **ОС**: Ubuntu 20.04+, Debian 11+, CentOS 8+, любая Linux с Docker
- **Docker**: версия 20.10+
- **Docker Compose**: версия 2.0+
### Процесс установки
Для Linux/macOS/WSL:
```bash
curl -fsSL https://raw.githubusercontent.com/VC-HB3-Accelerator/DLE/main/setup.sh | bash
```
Скрипт автоматически:
- Скачивает последние артефакты из релиза
- Разворачивает `docker-data`
- Настраивает необходимые конфигурации
### Компоненты системы
После установки запускаются следующие сервисы:
| Сервис | Описание | Порт |
|--------|----------|------|
| **PostgreSQL** | База данных с расширением pgvector | Внутренний |
| **Ollama** | Локальный AI сервер | Внутренний (11434) |
| **Vector Search** | Сервис векторного поиска (RAG) | Внутренний (8001) |
| **Backend** | Node.js API сервер | Внутренний (8000) |
| **Frontend (Nginx)** | Веб-интерфейс | 9000 (HTTP)
### Доступ к приложению
После успешного запуска приложение доступно по адресу:
- **Production**: `http://localhost:9000` (HTTP)
### Первоначальная настройка
1. Откройте приложение в браузере
2. Подключите крипто-кошелек (MetaMask, WalletConnect)
3. Настройте RPC провайдеры в разделе **Настройки → Безопасность**
4. Настройте смарт-контракты в разделе **Настройки → Блокчейн**
> 💡 **Подробная инструкция**: См. [Инструкция по установке](./setup-instruction.md) для пошаговой настройки всех компонентов.
---
## 🔒 Настройка безопасности
### Многоуровневая модель безопасности
DLE использует комплексный подход к безопасности на всех уровнях архитектуры:
```
┌─────────────────────────────────────────────────────────────┐
│ Уровни защиты DLE │
├─────────────────────────────────────────────────────────────┤
│ │
│ Уровень 1: Блокчейн (Неизменяемая база) │
│ • Смарт-контракт DLE (проверен, иммутабельный) │
│ • Токены управления (ERC20Votes) │
│ • История всех операций на блокчейне │
│ • Невозможность изменения правил без голосования │
│ │
│ Уровень 2: Веб-приложение (Backend) │
│ • Проверка токенов в реальном времени │
│ • Аутентификация через кошелек (SIWE) │
│ • Шифрование данных (AES-256) │
│ • Rate limiting и защита от DDoS │
│ │
│ Уровень 3: Frontend (Vue.js) │
│ • Подключение к кошельку │
│ • Подпись транзакций │
│ • XSS защита (DOMPurify) │
│ • CSRF токены │
│ │
│ Уровень 4: Пользователь │
│ • Приватный ключ кошелька (MetaMask, WalletConnect) │
│ • Подтверждение каждой операции │
│ │
└─────────────────────────────────────────────────────────────┘
```
### Настройка безопасности
#### 1. Настройка RPC провайдеров
**Путь**: Настройки → Безопасность → RPC провайдеры
Для каждой блокчейн-сети необходимо добавить RPC провайдера:
- **Network Name**: Название сети (Ethereum, Polygon, BSC и т.д.)
- **RPC URL**: URL подключения (например: `https://eth-mainnet.g.alchemy.com/v2/YOUR-API-KEY`)
- **Network ID**: Chain ID сети
> ⚠️ **Важно**: Получите API ключи от провайдеров (Alchemy, Infura, Quicknode и т.д.) перед добавлением.
#### 2. Настройка смарт-контрактов
**Путь**: Настройки → Блокчейн
Настройте адреса смарт-контрактов для каждой сети:
- **Factory Address**: Адрес фабрики контрактов
- **Core Contract Address**: Адрес основного контракта DLE
#### 3. Настройка шифрования
Все чувствительные данные шифруются с использованием AES-256:
- Персональные данные пользователей
- Идентификаторы гостей
- Приватные сообщения
- Ключи шифрования хранятся в защищенном хранилище
#### 4. Управление доступом
Система ролей и разрешений:
- **Guest**: Базовый доступ, чат с AI
- **User**: Полный доступ к коммуникациям
- **ReadOnly**: Просмотр данных без редактирования
- **Editor**: Полный доступ ко всем функциям
#### 5. Аудит и мониторинг
- Все действия логируются
- Audit trail для критичных операций
- Мониторинг подозрительной активности
- Уведомления о важных событиях
> 💡 **Подробная информация**: См. [Безопасность DLE](./security.md) для детального описания всех уровней защиты, сценариев атак и рекомендаций.
---
## 🏗️ Модули и микросервисная архитектура
### Архитектура системы
DLE построен на принципах микросервисной архитектуры, что обеспечивает:
- ✅ Легкое масштабирование отдельных компонентов
- ✅ Независимое развертывание сервисов
- ✅ Высокую отказоустойчивость
- ✅ Гибкость в разработке и поддержке
### Основные сервисы
#### 1. PostgreSQL (База данных)
- **Назначение**: Хранение всех данных приложения
- **Особенности**: Расширение pgvector для векторного поиска
- **Масштабирование**: Репликация, шардирование
#### 2. Ollama (AI сервер)
- **Назначение**: Локальный AI для генерации ответов
- **Модель**: qwen2.5:7b (настраивается)
- **Особенности**: 100% конфиденциальность, работа offline
- **Ресурсы**: 6 GB RAM, 2 CPU cores
#### 3. Vector Search (RAG сервис)
- **Назначение**: Векторный поиск для контекстных ответов AI
- **Технология**: FAISS, pgvector
- **Модель эмбеддингов**: mxbai-embed-large:latest
- **Особенности**: Семантический поиск по документам
#### 4. Backend (Node.js API)
- **Назначение**: Основной API сервер
- **Технологии**: Express.js, WebSocket
- **Функции**:
- Обработка запросов
- Управление сообщениями
- Интеграция с блокчейном
- Управление пользователями
#### 5. Frontend (Vue.js)
- **Назначение**: Пользовательский интерфейс
- **Технологии**: Vue 3, Vite, Element Plus
- **Особенности**: SPA, адаптивный дизайн
### Модульная система
DLE поддерживает расширение функциональности через модули:
#### Доступные модули
1. **HierarchicalVotingModule**
- Иерархическое голосование
- Делегирование голосов
- Сложные сценарии управления
2. **Custom Modules**
- Возможность добавления собственных модулей
- Интеграция через смарт-контракты
- Управление через голосование токен-холдеров
#### Добавление модулей
Модули добавляются через:
1. Голосование токен-холдеров
2. Деплой смарт-контракта модуля
3. Регистрация в основном контракте DLE
### Коммуникация между сервисами
```
┌─────────────┐
│ Frontend │
│ (Vue.js) │
└──────┬──────┘
│ HTTP/WebSocket
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Backend │──────│ PostgreSQL │ │ Ollama │
│ (Node.js) │ │ (pgvector) │ │ (AI Server)│
└──────┬──────┘ └──────────────┘ └─────────────┘
│ HTTP
┌─────────────┐
│Vector Search│
│ (RAG) │
└─────────────┘
```
### Масштабирование
Каждый сервис может масштабироваться независимо:
- **Горизонтальное масштабирование**: Добавление инстансов
- **Вертикальное масштабирование**: Увеличение ресурсов
- **Балансировка нагрузки**: Nginx, HAProxy
> 💡 **Подробная информация**: См. [Техническая документация по блокчейну](./blockchain-integration-technical.md) для информации о модульной системе смарт-контрактов.
---
## 💬 Корпоративный мессенджер с ИИ ассистентом
### Обзор функциональности
DLE включает полнофункциональный корпоративный мессенджер с интегрированным AI ассистентом, который обеспечивает:
- Единый интерфейс для всех каналов коммуникации
- Автоматические ответы с помощью AI
- Контекстное понимание истории общения
- Многоканальную поддержку клиентов
### Каналы коммуникации
#### 1. Веб-чат
- Публичный чат на сайте
- Приватные сообщения между пользователями
- Административный чат для связи с клиентами
#### 2. Telegram бот
- Интеграция с Telegram
- Автоматические ответы через AI
- Синхронизация с веб-интерфейсом
#### 3. Email
- Обработка входящих писем
- Автоматические ответы
- Интеграция с CRM
### ИИ ассистент
#### Возможности AI ассистента
**Локальный AI на вашем сервере**:
- Модель: qwen2.5:7b (настраивается)
- Технология: Ollama + Vector Search (RAG)
- Конфиденциальность: 100% (данные не покидают сервер)
- Стоимость: $0 (без лимитов на запросы)
#### Функции AI ассистента
1. **Автоматические ответы**
- Генерация ответов на основе контекста
- Обучение на ваших документах
- Персонализация под ваш бизнес
2. **Анализ настроения**
- Определение эмоционального тона сообщений
- Приоритизация запросов
- Эскалация критичных ситуаций
3. **Контекстный поиск**
- Поиск информации в базе знаний
- Использование истории общения
- Ссылки на релевантные документы
4. **Многозадачность**
- Обработка нескольких запросов одновременно
- Очередь запросов
- Приоритизация важных сообщений
#### Настройка AI ассистента
**Путь**: Настройки → AI Ассистент
1. **Базовая настройка**
- Выбор модели AI
- Настройка температуры генерации
- Лимиты токенов
2. **Правила и контекст**
- Добавление правил поведения
- Загрузка документов для обучения
- Настройка тона общения
3. **Векторный поиск**
- Индексация документов
- Настройка релевантности
- Обновление базы знаний
### Типы чатов
#### 1. Публичный чат
- Доступен всем пользователям
- AI может отвечать автоматически
- Модерация сообщений
#### 2. Приватный чат
- Личные сообщения между пользователями
- Шифрование контента
- История переписки
#### 3. Административный чат
- Связь администраторов с клиентами
- Управление через веб-интерфейс
- Интеграция с CRM
#### 4. Гостевой чат
- Для неавторизованных пользователей
- Ограниченный функционал
- Требуется согласие на обработку данных
### Управление сообщениями
- **История**: Полная история всех сообщений
- **Поиск**: Поиск по содержимому, отправителю, дате
- **Фильтры**: По каналам, типам, статусам
- **Экспорт**: Выгрузка переписки в различных форматах
> 💡 **Подробная информация**: См. [AI Ассистент](./ai-assistant.md) для полного описания возможностей и [Настройка AI ассистента](./setup-ai-assistant.md) для инструкций по настройке.
---
## 🌐 Настройки приложения для работы с интернет пользователями
### Работа с гостями (неавторизованными пользователями)
DLE поддерживает работу с интернет-пользователями без обязательной регистрации, обеспечивая при этом соответствие требованиям регуляторов.
#### Роли и права доступа
**Guest (Гость)**:
- ✅ Просмотр главной страницы
- ✅ Чат с AI ассистентом
- ❌ Отправка сообщений пользователям
- ❌ Доступ к CRM и данным
**User (Пользователь)**:
-Все права гостя
- ✅ Получение сообщений
- ✅ Отправка сообщений другим пользователям
- ✅ Просмотр контактов
- ✅ Приватный чат с администраторами
- ✅ Просмотр базовых документов
#### Настройка гостевого доступа
**Путь**: Настройки → Пользователи → Гостевой доступ
1. **Включение гостевого режима**
- Разрешить доступ без регистрации
- Настройка ограничений
- Лимиты на использование
2. **Сбор согласий**
- Автоматический запрос согласия на обработку ПД
- Политика конфиденциальности
- Пользовательское соглашение
- Политика использования cookies
3. **Идентификация гостей**
- Уникальные идентификаторы
- Шифрование данных
- Анонимизация при необходимости
### Обработка персональных данных
#### Согласие на обработку ПД
При первом обращении гостя система:
1. Показывает политику конфиденциальности
2. Запрашивает согласие на обработку ПД
3. Сохраняет факт согласия с временной меткой
4. Предоставляет доступ к функциям
#### Управление согласиями
**Путь**: Настройки → Контент → Юридические документы
- Просмотр всех согласий
- Экспорт данных по запросу
- Удаление данных при отзыве согласия
- История изменений согласий
### Публичные документы
#### Обязательные документы для публичного доступа
1. **Политика конфиденциальности**
- Описание обработки ПД
- Права пользователей
- Контакты для обращений
2. **Пользовательское соглашение**
- Условия использования сервиса
- Права и обязанности
- Ограничения ответственности
3. **Согласие на обработку персональных данных**
- Форма согласия
- Информация о целях обработки
- Возможность отзыва
4. **Политика использования cookies**
- Типы используемых cookies
- Цели использования
- Управление настройками
#### Публикация документов
**Путь**: Контент → Шаблоны
1. Выберите необходимые шаблоны
2. Предварительный просмотр с автозаполнением
3. Редактирование специфичных параметров
4. Публикация:
- **Публичное использование**: Доступно на сайте
- **Внутреннее использование**: Только в CRM
- **Печать**: Экспорт в PDF
### Настройки веб-интерфейса
#### Публичные страницы
- **Главная страница**: Информация о сервисе
- **Чат**: Публичный чат с AI
- **Контакты**: Форма обратной связи
- **Документы**: Публичные юридические документы
#### Интеграция с сайтом
DLE может быть интегрирован на ваш сайт:
- Виджет чата
- Формы обратной связи
- API для кастомных решений
### Управление контактами
#### Автоматическое создание контактов
При обращении гостя система:
1. Создает контакт в CRM
2. Присваивает уникальный идентификатор
3. Сохраняет канал коммуникации
4. Записывает историю взаимодействий
#### Объединение контактов
Если один пользователь обращается через разные каналы:
- Автоматическое определение дубликатов
- Объединение в единый контакт
- Единая история коммуникаций
> 💡 **Важно**: Все настройки для работы с интернет-пользователями должны соответствовать требованиям регуляторов (см. раздел "Требования регулятора").
---
## ⚖️ Требования регулятора
### Соответствие законодательству
DLE разработан с учетом требований основных регуляторов по защите персональных данных и обеспечению прозрачности работы с пользователями.
### GDPR (General Data Protection Regulation)
**Европейское законодательство о защите данных**
#### Основные требования и их реализация:
1. **Право на информацию**
- Политика конфиденциальности доступна на сайте
- Прозрачная информация о целях обработки данных
- Контакты контролера данных
2. **Право на доступ**
- Пользователи могут запросить копию своих данных
- Экспорт данных в структурированном формате
- История обработки данных
3. **Право на исправление**
- Редактирование персональных данных
- Обновление информации в профиле
- Корректировка неточных данных
4. **Право на удаление ("право быть забытым")**
- Удаление данных по запросу
- Отзыв согласия на обработку
- Полное удаление из системы
5. **Право на ограничение обработки**
- Временная блокировка обработки
- Сохранение данных без использования
- Уведомление о снятии ограничений
6. **Право на переносимость данных**
- Экспорт данных в машиночитаемом формате
- Передача данных другому контролеру
- Структурированные форматы (JSON, CSV)
7. **Право на возражение**
- Отказ от обработки данных
- Отзыв согласия
- Остановка маркетинговых рассылок
8. **Автоматизированное принятие решений**
- Прозрачность использования AI
- Возможность человеческого вмешательства
- Объяснение логики решений
### CCPA (California Consumer Privacy Act)
**Калифорнийский закон о защите конфиденциальности**
#### Основные требования:
1. **Право знать**
- Информация о собираемых данных
- Цели использования данных
- Категории третьих сторон
2. **Право на удаление**
- Удаление персональных данных
- Исключения для бизнес-нужд
- Подтверждение удаления
3. **Право на отказ от продажи**
- DLE не продает данные пользователей
- Прозрачная политика
- Механизм отказа
4. **Недискриминация**
- Равный доступ к сервису
- Отсутствие штрафов за использование прав
- Справедливое ценообразование
### 152-ФЗ (Российское законодательство)
**Федеральный закон "О персональных данных"**
#### Основные требования:
1. **Согласие на обработку**
- Явное согласие субъекта ПД
- Информированное согласие
- Возможность отзыва
2. **Уведомление Роскомнадзора**
- Документация для уведомления
- Описание целей обработки
- Меры безопасности
3. **Локализация данных**
- Хранение на серверах в РФ (опционально)
- Контроль местоположения данных
- Соответствие требованиям
4. **Права субъектов ПД**
- Доступ к данным
- Исправление данных
- Удаление данных
- Отзыв согласия
5. **Меры безопасности**
- Шифрование данных
- Контроль доступа
- Аудит действий
- Резервное копирование
### Готовые документы для регулятора
DLE включает готовый пакет документов, необходимых для соответствия требованиям:
#### 1. Политика конфиденциальности
- Описание обработки ПД
- Права пользователей
- Контакты контролера
- Механизмы реализации прав
#### 2. Пользовательское соглашение
- Условия использования
- Права и обязанности сторон
- Ограничения ответственности
- Разрешение споров
#### 3. Согласие на обработку персональных данных
- Форма согласия
- Цели обработки
- Сроки хранения
- Право отзыва
#### 4. Политика использования cookies
- Типы cookies
- Цели использования
- Управление настройками
- Отключение cookies
#### 5. Документация для Роскомнадзора
- Описание системы обработки ПД
- Меры безопасности
- Технические характеристики
- Процедуры обработки запросов
### Аудит и соответствие
#### Регулярные проверки
- Проверка актуальности документов
- Обновление политик при изменении законодательства
- Аудит обработки данных
- Тестирование механизмов реализации прав
#### Рекомендации
1. **Регулярное обновление**
- Следите за изменениями в законодательстве
- Обновляйте документы при необходимости
- Проводите аудит соответствия
2. **Консультации**
- Рекомендуется консультация с юристом
- Проверка соответствия локальному законодательству
- Адаптация под специфику вашего региона
3. **Документирование**
- Ведите журнал обработки данных
- Сохраняйте историю согласий
- Документируйте запросы пользователей
> 💡 **Подробная информация**: См. [Безопасность DLE](./security.md) для детального описания мер безопасности и соответствия требованиям регуляторов.
---
## 🔄 Обновления софта
### Политика обновлений
DLE предоставляет бесплатные обновления в течение 5 лет для держателей лицензионных токенов. Это включает:
- ✅ Исправления ошибок (bug fixes)
- ✅ Новые функции (features)
- ✅ Улучшения безопасности (security updates)
- ✅ Обновления зависимостей (dependencies)
- ✅ Оптимизация производительности
### Типы обновлений
#### 1. Патч-обновления (Patch)
- **Формат версии**: X.Y.**Z** (например, 1.0.1 → 1.0.2)
- **Содержание**: Исправления ошибок, мелкие улучшения
- **Частота**: По мере необходимости
- **Критичность**: Обычно низкая-средняя
#### 2. Минорные обновления (Minor)
- **Формат версии**: X.**Y**.Z (например, 1.0.2 → 1.1.0)
- **Содержание**: Новые функции, улучшения
- **Частота**: Ежеквартально или по мере готовности
- **Критичность**: Средняя
#### 3. Мажорные обновления (Major)
- **Формат версии**: **X**.Y.Z (например, 1.1.0 → 2.0.0)
- **Содержание**: Крупные изменения, breaking changes
- **Частота**: Раз в год или реже
- **Критичность**: Высокая (требуется миграция)
### Процесс обновления
#### Автоматическое обновление (рекомендуется)
```bash
# 1. Остановка текущей версии
docker-compose down
# 2. Резервное копирование данных
docker run --rm -v dle_postgres_data:/data -v $(pwd):/backup \
alpine tar czf /backup/postgres_backup.tar.gz -C /data .
# 3. Обновление кода
git pull origin main
# 4. Обновление образов
docker-compose pull
# 5. Пересборка (если необходимо)
docker-compose build
# 6. Запуск обновленной версии
docker-compose up -d
# 7. Проверка статуса
docker-compose ps
docker-compose logs -f
```
#### Ручное обновление
1. **Подготовка**
- Проверьте текущую версию
- Изучите changelog обновления
- Создайте резервную копию
2. **Резервное копирование**
```bash
# База данных
docker-compose exec postgres pg_dump -U dapp_user dapp_db > backup.sql
# Файлы загрузок
tar -czf uploads_backup.tar.gz backend/uploads/
# Конфигурации
cp .env .env.backup
```
3. **Обновление**
- Скачайте новую версию
- Обновите зависимости
- Примените миграции БД (если есть)
4. **Проверка**
- Проверьте работоспособность
- Протестируйте ключевые функции
- Проверьте логи на ошибки
### Миграции базы данных
При обновлении могут потребоваться миграции БД:
```bash
# Автоматическое применение миграций
docker-compose exec backend npm run migrate
# Или вручную через SQL
docker-compose exec postgres psql -U dapp_user -d dapp_db -f migrations/version_X.X.X.sql
```
### Откат обновления
В случае проблем можно откатиться к предыдущей версии:
```bash
# 1. Остановка текущей версии
docker-compose down
# 2. Восстановление из резервной копии
docker run --rm -v dle_postgres_data:/data -v $(pwd):/backup \
alpine tar xzf /backup/postgres_backup.tar.gz -C /data
# 3. Переключение на предыдущую версию
git checkout <previous-version-tag>
# 4. Запуск предыдущей версии
docker-compose up -d
```
### Уведомления об обновлениях
#### Каналы уведомлений
1. **GitHub Releases**
- Официальные релизы
- Changelog изменений
- Инструкции по обновлению
2. **Email рассылка**
- Уведомления о важных обновлениях
- Информация о breaking changes
- Рекомендации по обновлению
3. **В приложении**
- Уведомления о доступных обновлениях
- Информация о новых функциях
- Ссылки на документацию
### Рекомендации по обновлениям
1. **Регулярность**
- Обновляйтесь регулярно для получения исправлений безопасности
- Не откладывайте критичные обновления
- Следите за changelog
2. **Тестирование**
- Тестируйте обновления на тестовой среде
- Проверяйте совместимость с вашими данными
- Убедитесь в работоспособности интеграций
3. **Резервное копирование**
- Всегда создавайте резервные копии перед обновлением
- Храните несколько версий бэкапов
- Тестируйте восстановление из бэкапа
4. **Документирование**
- Ведите журнал обновлений
- Записывайте возникшие проблемы
- Документируйте кастомные изменения
### Долгосрочная поддержка (LTS)
Для стабильных версий может быть доступна долгосрочная поддержка:
- Расширенный период поддержки
- Приоритетные исправления безопасности
- Гарантированная совместимость
> 💡 **Важно**: Обновления предоставляются бесплатно в течение 5 лет для держателей лицензионных токенов. После этого периода обновления могут быть платными или доступны через сообщество.
---
## 📚 Дополнительные ресурсы
### Документация
- [Описание приложения](./application-description.md) - Полный обзор возможностей и преимуществ
- [Инструкция по установке](./setup-instruction.md) - Пошаговая настройка
- [Настройка AI ассистента](./setup-ai-assistant.md) - Конфигурация AI
- [Безопасность DLE](./security.md) - Детальная информация о безопасности
- [FAQ](./FAQ.md) - Ответы на частые вопросы
### Поддержка
- **Email**: info@hb3-accelerator.com
- **Сайт**: https://hb3-accelerator.com
- **GitHub**: https://github.com/VC-HB3-Accelerator
---
**© 2024-2025 Тарабанов Александр Викторович. Все права защищены.**
**Digital Legal Entity (DLE)** - комплексное решение для управления бизнесом с блокчейн и AI.
**Версия документа**: 1.0.0
**Последнее обновление**: January 2025

View File

@@ -81,12 +81,31 @@ http {
return 404; return 404;
} }
# Защита от доступа к чувствительным файлам # Защита от доступа к чувствительным файлам (исключаем robots.txt и sitemap.xml для SEO)
location ~* /(\.htaccess|\.htpasswd|\.env|\.git|\.svn|\.DS_Store|Thumbs\.db|web\.config|robots\.txt|sitemap\.xml)$ { location ~* /(\.htaccess|\.htpasswd|\.env|\.git|\.svn|\.DS_Store|Thumbs\.db|web\.config)$ {
deny all; deny all;
return 404; return 404;
} }
# Разрешаем доступ к robots.txt и sitemap.xml для поисковых систем
location = /robots.txt {
proxy_pass http://${BACKEND_CONTAINER}:8000/api/pages/public/robots.txt;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
add_header Content-Type text/plain;
}
location = /sitemap.xml {
proxy_pass http://${BACKEND_CONTAINER}:8000/api/pages/public/sitemap.xml;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
add_header Content-Type application/xml;
}
# Защита от доступа к конфигурационным файлам # Защита от доступа к конфигурационным файлам
location ~* /\.(env|config|ini|conf|cfg|yml|yaml|json|xml|sql|db|bak|backup|old|tmp|temp|log)$ { location ~* /\.(env|config|ini|conf|cfg|yml|yaml|json|xml|sql|db|bak|backup|old|tmp|temp|log)$ {
deny all; deny all;

View File

@@ -420,6 +420,35 @@ const isResizing = ref(false);
const resizeStartX = ref(0); const resizeStartX = ref(0);
const resizeStartWidth = ref(0); const resizeStartWidth = ref(0);
// Определяем, является ли устройство мобильным
const isMobile = ref(false);
// Функция для проверки мобильного устройства
const checkMobile = () => {
isMobile.value = window.innerWidth <= 1024;
if (isMobile.value) {
// На мобильных устройствах устанавливаем ширину в 100%
messagesWidth.value = 100;
inputWidth.value = 100;
} else {
// На десктопе используем стандартные значения
if (messagesWidth.value === 100) {
messagesWidth.value = 70;
inputWidth.value = 30;
}
}
};
// Отслеживаем изменение размера окна
onMounted(() => {
checkMobile();
window.addEventListener('resize', checkMobile);
});
onUnmounted(() => {
window.removeEventListener('resize', checkMobile);
});
const startResize = (e) => { const startResize = (e) => {
isResizing.value = true; isResizing.value = true;
@@ -535,6 +564,10 @@ const updateChatInputHeight = () => {
}; };
onMounted(() => { onMounted(() => {
// Проверяем мобильное устройство и устанавливаем ширину
checkMobile();
window.addEventListener('resize', checkMobile);
// Начальная установка высоты textarea и блока ввода // Начальная установка высоты textarea и блока ввода
adjustTextareaHeight(); adjustTextareaHeight();
updateChatInputHeight(); updateChatInputHeight();
@@ -548,6 +581,8 @@ onMounted(() => {
}); });
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener('resize', checkMobile);
if (resizeObserver && chatInputRef.value) { if (resizeObserver && chatInputRef.value) {
resizeObserver.unobserve(chatInputRef.value); resizeObserver.unobserve(chatInputRef.value);
} }
@@ -709,8 +744,8 @@ async function handleAiReply() {
/* На мобильных устройствах блок ввода занимает всё пространство внизу */ /* На мобильных устройствах блок ввода занимает всё пространство внизу */
@media (max-width: 1024px) { @media (max-width: 1024px) {
.chat-input { .chat-input {
width: 100%; width: 100% !important;
max-width: 100%; max-width: 100% !important;
height: auto; height: auto;
border-top: none; border-top: none;
border-right: none; border-right: none;
@@ -737,6 +772,7 @@ async function handleAiReply() {
color: var(--color-dark); color: var(--color-dark);
overflow-y: auto; overflow-y: auto;
box-sizing: border-box; box-sizing: border-box;
max-width: 100%;
} }
.chat-input textarea:focus { .chat-input textarea:focus {
@@ -748,12 +784,15 @@ async function handleAiReply() {
/* На мобильных устройствах поле ввода меньше */ /* На мобильных устройствах поле ввода меньше */
@media (max-width: 1024px) { @media (max-width: 1024px) {
.chat-input textarea { .chat-input textarea {
width: 100% !important;
max-width: 100% !important;
border-radius: 20px; border-radius: 20px;
padding: 12px 16px; padding: 12px 16px;
min-height: var(--chat-input-min-height, 40px); min-height: var(--chat-input-min-height, 40px);
max-height: var(--chat-input-max-height, 120px); max-height: var(--chat-input-max-height, 120px);
overflow-y: hidden; overflow-y: hidden;
resize: none; resize: none;
box-sizing: border-box;
} }
} }
@@ -942,11 +981,25 @@ async function handleAiReply() {
.chat-messages { .chat-messages {
padding: var(--spacing-md) var(--spacing-md) 8px; padding: var(--spacing-md) var(--spacing-md) 8px;
width: 100% !important;
} }
.chat-input { .chat-input {
width: 100% !important;
max-width: 100% !important;
padding: var(--spacing-xs) var(--spacing-sm); padding: var(--spacing-xs) var(--spacing-sm);
height: auto; height: auto;
box-sizing: border-box;
}
.input-area {
width: 100% !important;
box-sizing: border-box;
}
.input-area textarea {
width: 100% !important;
box-sizing: border-box;
} }
.chat-icon-btn { .chat-icon-btn {
@@ -958,10 +1011,16 @@ async function handleAiReply() {
width: 20px; width: 20px;
height: 20px; height: 20px;
} }
.resizer {
display: none;
}
} }
@media (max-width: 480px) { @media (max-width: 480px) {
.chat-input { .chat-input {
width: 100% !important;
max-width: 100% !important;
position: sticky !important; position: sticky !important;
bottom: 0 !important; bottom: 0 !important;
border-radius: 0 !important; border-radius: 0 !important;
@@ -969,14 +1028,27 @@ async function handleAiReply() {
background: #f8f8f8 !important; background: #f8f8f8 !important;
border-top: 1px solid #eee !important; border-top: 1px solid #eee !important;
} }
.chat-messages { .chat-messages {
width: 100% !important;
padding: var(--spacing-md) var(--spacing-md) 8px !important; padding: var(--spacing-md) var(--spacing-md) 8px !important;
overflow-y: auto !important; overflow-y: auto !important;
} }
.input-area {
width: 100% !important;
}
.input-area textarea {
width: 100% !important;
box-sizing: border-box;
}
} }
@media (max-width: 600px) { @media (max-width: 600px) {
.chat-input { .chat-input {
width: 100% !important;
max-width: 100% !important;
position: sticky !important; position: sticky !important;
bottom: 0 !important; bottom: 0 !important;
border-radius: 0 !important; border-radius: 0 !important;
@@ -984,7 +1056,9 @@ async function handleAiReply() {
background: #f8f8f8 !important; background: #f8f8f8 !important;
border-top: 1px solid #eee !important; border-top: 1px solid #eee !important;
} }
.chat-messages { .chat-messages {
width: 100% !important;
padding: var(--spacing-md) var(--spacing-md) 8px !important; padding: var(--spacing-md) var(--spacing-md) 8px !important;
overflow-y: auto !important; overflow-y: auto !important;
} }

View File

@@ -47,6 +47,9 @@
<router-link to="/" class="nav-link-btn" active-class="active"> <router-link to="/" class="nav-link-btn" active-class="active">
<span>Чат</span> <span>Чат</span>
</router-link> </router-link>
<router-link to="/content/published" class="nav-link-btn" active-class="active">
<span>Блог</span>
</router-link>
<router-link to="/crm" class="nav-link-btn" active-class="active"> <router-link to="/crm" class="nav-link-btn" active-class="active">
<span>CRM</span> <span>CRM</span>
</router-link> </router-link>

View File

@@ -30,7 +30,7 @@
<!-- Заголовок страницы --> <!-- Заголовок страницы -->
<header v-if="page" class="page-header"> <header v-if="page" class="page-header">
<div class="page-header-top"> <div class="page-header-top">
<button v-if="breadcrumbs.length > 0" class="back-btn" @click="$emit('back')" title="Вернуться к списку"> <button class="back-btn" @click="$emit('back')" title="Вернуться к списку">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
<span>Назад</span> <span>Назад</span>
</button> </button>
@@ -186,6 +186,68 @@ const navigation = ref(null);
const breadcrumbs = ref([]); const breadcrumbs = ref([]);
const isLoading = ref(false); const isLoading = ref(false);
// Установка мета-тегов для SEO
function updateMetaTags(pageData) {
if (!pageData) return;
// Парсим seo, если это строка (может прийти из базы как JSON строка)
let seoData = pageData.seo;
if (typeof seoData === 'string') {
try {
seoData = JSON.parse(seoData);
} catch (e) {
console.warn('Ошибка парсинга SEO данных:', e);
seoData = null;
}
}
const title = seoData?.title || pageData.title || 'Документ';
const description = seoData?.description || pageData.summary || '';
const keywords = seoData?.keywords || '';
const canonicalUrl = `${window.location.origin}/content/published?page=${pageData.id}`;
// Обновляем title
document.title = title;
// Обновляем или создаем meta теги
const updateOrCreateMeta = (name, content, attribute = 'name') => {
if (!content) return;
let meta = document.querySelector(`meta[${attribute}="${name}"]`);
if (!meta) {
meta = document.createElement('meta');
meta.setAttribute(attribute, name);
document.head.appendChild(meta);
}
meta.setAttribute('content', content);
};
// Meta description
updateOrCreateMeta('description', description);
// Meta keywords
if (keywords) {
updateOrCreateMeta('keywords', keywords);
}
// Canonical URL
let canonical = document.querySelector('link[rel="canonical"]');
if (!canonical) {
canonical = document.createElement('link');
canonical.setAttribute('rel', 'canonical');
document.head.appendChild(canonical);
}
canonical.setAttribute('href', canonicalUrl);
// Open Graph теги для социальных сетей
updateOrCreateMeta('og:title', title, 'property');
updateOrCreateMeta('og:description', description, 'property');
updateOrCreateMeta('og:type', 'article', 'property');
updateOrCreateMeta('og:url', canonicalUrl, 'property');
// Robots meta
updateOrCreateMeta('robots', 'index, follow');
}
// Загрузка страницы // Загрузка страницы
async function loadPage() { async function loadPage() {
if (!props.pageId) return; if (!props.pageId) return;
@@ -194,6 +256,11 @@ async function loadPage() {
isLoading.value = true; isLoading.value = true;
page.value = await pagesService.getPublicPage(props.pageId); page.value = await pagesService.getPublicPage(props.pageId);
// Устанавливаем мета-теги для SEO
if (page.value) {
updateMetaTags(page.value);
}
// Загружаем навигацию // Загружаем навигацию
try { try {
navigation.value = await pagesService.getPublicPageNavigation(props.pageId); navigation.value = await pagesService.getPublicPageNavigation(props.pageId);
@@ -959,12 +1026,44 @@ onMounted(() => {
@media (max-width: 768px) { @media (max-width: 768px) {
.docs-content { .docs-content {
padding: 20px; padding: 20px;
min-height: auto;
width: 100%;
max-width: 100%;
display: block;
visibility: visible;
opacity: 1;
} }
.page-header h1 { .page-header h1 {
font-size: 2rem; font-size: 2rem;
} }
.page-article {
width: 100%;
overflow-x: auto;
}
.content-text {
width: 100%;
overflow-x: auto;
word-wrap: break-word;
}
.file-preview {
width: 100%;
}
.pdf-embed {
width: 100%;
height: 60vh;
min-height: 400px;
}
.image-preview {
width: 100%;
max-width: 100%;
}
.page-navigation { .page-navigation {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }

View File

@@ -199,4 +199,55 @@ onMounted(() => {
padding: 1rem; padding: 1rem;
background: #f9f9f9; background: #f9f9f9;
} }
@media (max-width: 768px) {
.admin-chat-header {
padding: 0.75rem;
font-size: 1rem;
}
.close-btn {
font-size: 1.25rem;
padding: 0.2rem 0.4rem;
}
.chat-container {
height: calc(100vh - 100px);
}
:deep(.chat-messages) {
padding: 0.75rem;
}
:deep(.chat-input) {
padding: 0.75rem;
}
.loading-container {
padding: 1.5rem;
}
.loading {
font-size: 1rem;
}
}
@media (max-width: 480px) {
.admin-chat-header {
padding: 0.5rem;
font-size: 0.9rem;
}
.chat-container {
height: calc(100vh - 80px);
}
:deep(.chat-messages) {
padding: 0.5rem;
}
:deep(.chat-input) {
padding: 0.5rem;
}
}
</style> </style>

View File

@@ -377,5 +377,98 @@ h1 {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
} }
@media (max-width: 768px) {
.connect-wallet-container {
padding: 16px;
}
.connect-wallet-card {
padding: 30px 24px;
max-width: 100%;
}
.icon {
font-size: 48px;
margin-bottom: 16px;
}
h1 {
font-size: 24px;
margin-bottom: 12px;
}
.info-block {
margin: 20px 0;
}
.provider-info {
font-size: 14px;
}
.description {
font-size: 13px;
}
.connect-button,
.go-chat-button {
padding: 12px 24px;
font-size: 15px;
}
}
@media (max-width: 480px) {
.connect-wallet-container {
padding: 12px;
}
.connect-wallet-card {
padding: 24px 16px;
border-radius: 12px;
}
.icon {
font-size: 40px;
margin-bottom: 12px;
}
h1 {
font-size: 20px;
margin-bottom: 10px;
}
.provider-info {
font-size: 13px;
}
.description {
font-size: 12px;
}
.connect-button,
.go-chat-button {
padding: 10px 20px;
font-size: 14px;
}
.error-message {
font-size: 13px;
padding: 10px;
}
.expires-info {
font-size: 12px;
}
.hint {
font-size: 13px;
padding: 10px;
}
.stats {
font-size: 13px;
padding: 10px;
}
}
</style> </style>

View File

@@ -94,4 +94,24 @@ function goBack() {
margin-left: 7px; margin-left: 7px;
} }
@media (max-width: 768px) {
.contacts-header {
font-size: 1rem;
margin-bottom: 16px;
padding: 0 10px;
}
.badge {
font-size: 0.85em;
padding: 2px 6px;
}
}
@media (max-width: 480px) {
.contacts-header {
font-size: 0.9rem;
margin-bottom: 12px;
}
}
</style> </style>

View File

@@ -19,26 +19,6 @@
@auth-action-completed="$emit('auth-action-completed')" @auth-action-completed="$emit('auth-action-completed')"
> >
<div class="content-create-page"> <div class="content-create-page">
<!-- Заголовок страницы -->
<div class="page-header">
<div class="header-content">
<h1>{{ isEditMode ? 'Редактирование страницы' : 'Создание страницы' }}</h1>
<p>{{ isEditMode ? 'Редактируйте существующую страницу' : 'Создайте новую страницу для вашего DLE' }}</p>
</div>
<div class="header-actions">
<button
v-if="isEditMode && canManageLegalDocs && address"
class="btn btn-danger"
@click="deletePage"
type="button"
>
<i class="fas fa-trash"></i>
Удалить
</button>
<button class="close-btn" @click="goBack">×</button>
</div>
</div>
<!-- Основной контент с тенью --> <!-- Основной контент с тенью -->
<div class="content-block"> <div class="content-block">
<form class="content-form" @submit.prevent="handleSubmit"> <form class="content-form" @submit.prevent="handleSubmit">

View File

@@ -19,13 +19,6 @@
@auth-action-completed="$emit('auth-action-completed')" @auth-action-completed="$emit('auth-action-completed')"
> >
<div class="crm-management"> <div class="crm-management">
<!-- Заголовок -->
<div class="management-header">
<div class="header-content">
<h1>CRM Система</h1>
</div>
</div>
<!-- Блоки CRM --> <!-- Блоки CRM -->
<div class="management-blocks"> <div class="management-blocks">
<!-- Столбец 1 --> <!-- Столбец 1 -->

View File

@@ -19,13 +19,6 @@
@auth-action-completed="$emit('auth-action-completed')" @auth-action-completed="$emit('auth-action-completed')"
> >
<div class="home-management"> <div class="home-management">
<!-- Заголовок -->
<div class="management-header">
<div class="header-content">
<h1>Сообщения</h1>
</div>
</div>
<!-- Чат интерфейс --> <!-- Чат интерфейс -->
<div class="chat-wrapper"> <div class="chat-wrapper">
<template v-if="auth.userAccessLevel.value && auth.userAccessLevel.value.hasAccess"> <template v-if="auth.userAccessLevel.value && auth.userAccessLevel.value.hasAccess">

View File

@@ -19,12 +19,6 @@
@auth-action-completed="$emit('auth-action-completed')" @auth-action-completed="$emit('auth-action-completed')"
> >
<div class="management-container"> <div class="management-container">
<!-- Заголовок -->
<div class="management-header">
<h1>Управление DLE</h1>
<button class="close-btn" @click="router.push('/')">×</button>
</div>
<!-- Деплоированные DLE --> <!-- Деплоированные DLE -->
<div class="deployed-dles-section"> <div class="deployed-dles-section">
<div class="section-header"> <div class="section-header">

View File

@@ -308,4 +308,65 @@ onUnmounted(() => {
font-size: 1rem; font-size: 1rem;
text-align: center; text-align: center;
} }
@media (max-width: 768px) {
.personal-messages-header {
padding: 0.75rem;
font-size: 1rem;
flex-wrap: wrap;
gap: 0.5rem;
}
.close-btn {
font-size: 1.25rem;
padding: 0.2rem 0.4rem;
}
.personal-messages-list {
padding: 0.5rem;
}
.message-item {
flex-direction: column;
align-items: stretch;
gap: 0.75rem;
padding: 0.75rem;
}
.message-info {
width: 100%;
}
.admin-name {
font-size: 1rem;
}
.message-preview {
font-size: 0.85rem;
}
.message-date {
font-size: 0.75rem;
}
}
@media (max-width: 480px) {
.personal-messages-header {
padding: 0.5rem;
font-size: 0.9rem;
}
.message-item {
padding: 0.5rem;
}
.admin-name {
font-size: 0.95rem;
}
.empty-state {
padding: 1.5rem 1rem;
font-size: 0.9rem;
}
}
</style> </style>

View File

@@ -19,10 +19,8 @@
@auth-action-completed="$emit('auth-action-completed')" @auth-action-completed="$emit('auth-action-completed')"
> >
<div class="settings-view-container"> <div class="settings-view-container">
<div v-if="route.name !== 'settings-index'" class="page-header"> <div v-if="route.name === 'settings-blockchain-dle-deploy' || route.name === 'settings-dle-v2-deploy'" class="page-header">
<h1>{{ pageTitle }}</h1>
<button <button
v-if="route.name === 'settings-blockchain-dle-deploy' || route.name === 'settings-dle-v2-deploy'"
class="close-btn" class="close-btn"
@click="router.push('/settings')" @click="router.push('/settings')"
>×</button> >×</button>

View File

@@ -19,9 +19,7 @@
@auth-action-completed="$emit('auth-action-completed')" @auth-action-completed="$emit('auth-action-completed')"
> >
<div class="vds-management-container"> <div class="vds-management-container">
<!-- Заголовок -->
<div class="vds-header"> <div class="vds-header">
<h1>Управление VDS</h1>
<div class="status-badge" :class="{ online: isOnline }"> <div class="status-badge" :class="{ online: isOnline }">
<div class="status-indicator" :class="{ online: isOnline }"></div> <div class="status-indicator" :class="{ online: isOnline }"></div>
<span>{{ isOnline ? 'Онлайн' : 'Офлайн' }}</span> <span>{{ isOnline ? 'Онлайн' : 'Офлайн' }}</span>

View File

@@ -19,18 +19,6 @@
@auth-action-completed="$emit('auth-action-completed')" @auth-action-completed="$emit('auth-action-completed')"
> >
<div class="content-management-page"> <div class="content-management-page">
<!-- Заголовок страницы -->
<div class="page-header">
<div class="header-content">
<h1>Управление контентом</h1>
<p v-if="canEditData && address">Создавайте и управляйте страницами вашего DLE</p>
<p v-else>Просмотр опубликованных страниц DLE</p>
</div>
<div class="header-actions">
<button class="close-btn" @click="goBack">×</button>
</div>
</div>
<!-- Основной контент с тенью --> <!-- Основной контент с тенью -->
<div class="content-block"> <div class="content-block">
<!-- Быстрые разделы --> <!-- Быстрые разделы -->
@@ -248,8 +236,33 @@ async function deletePage(id) {
.details-btn { background: var(--color-primary); color: #fff; border: none; border-radius: 8px; padding: 0.75rem 1.5rem; cursor: pointer; font-size: 1rem; font-weight: 600; transition: all 0.2s; min-width: 120px; margin-top: auto; } .details-btn { background: var(--color-primary); color: #fff; border: none; border-radius: 8px; padding: 0.75rem 1.5rem; cursor: pointer; font-size: 1rem; font-weight: 600; transition: all 0.2s; min-width: 120px; margin-top: auto; }
.details-btn:hover { background: var(--color-primary-dark); transform: translateY(-1px); } .details-btn:hover { background: var(--color-primary-dark); transform: translateY(-1px); }
@media (max-width: 1024px) { .management-blocks { grid-template-columns: repeat(2, 1fr); } } @media (max-width: 1024px) {
@media (max-width: 768px) { .management-blocks { grid-template-columns: 1fr; } } .management-blocks {
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
}
}
@media (max-width: 768px) {
.management-blocks {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.blocks-column {
gap: 1.2rem;
}
}
@media (max-width: 480px) {
.management-blocks {
gap: 1rem;
}
.blocks-column {
gap: 1rem;
}
}
.content-section { .content-section {
background: #f8f9fa; background: #f8f9fa;
@@ -496,6 +509,12 @@ async function deletePage(id) {
color: white; color: white;
} }
.header-actions {
display: flex;
align-items: center;
gap: 10px;
}
.close-btn { .close-btn {
background: none; background: none;
border: none; border: none;
@@ -505,6 +524,11 @@ async function deletePage(id) {
padding: 5px; padding: 5px;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
transition: background 0.2s; transition: background 0.2s;
min-width: 32px;
min-height: 32px;
display: flex;
align-items: center;
justify-content: center;
} }
.close-btn:hover { .close-btn:hover {
@@ -513,9 +537,37 @@ async function deletePage(id) {
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.content-management-page {
padding: 16px;
}
.page-header { .page-header {
flex-direction: column; flex-direction: column;
gap: 15px; gap: 15px;
margin-bottom: 20px;
padding-bottom: 15px;
}
.header-content h1 {
font-size: 1.8rem;
}
.header-content p {
font-size: 1rem;
}
.header-actions {
align-self: flex-start;
}
.close-btn {
font-size: 1.3rem;
min-width: 28px;
min-height: 28px;
}
.content-block {
padding: 20px;
} }
.nav-tabs { .nav-tabs {
@@ -536,9 +588,59 @@ async function deletePage(id) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.settings-grid { .settings-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.management-block {
padding: 1.5rem;
height: auto;
min-height: 200px;
}
.management-block h3 {
font-size: 1.3rem;
}
.management-block p {
font-size: 0.9rem;
}
.details-btn {
padding: 0.6rem 1.2rem;
font-size: 0.9rem;
}
}
@media (max-width: 480px) {
.content-management-page {
padding: 12px;
}
.page-header {
margin-bottom: 15px;
padding-bottom: 12px;
}
.header-content h1 {
font-size: 1.5rem;
}
.content-block {
padding: 16px;
}
.management-block {
padding: 1.2rem;
min-height: 180px;
}
.management-block h3 {
font-size: 1.2rem;
}
.management-block p {
font-size: 0.85rem;
}
} }
</style> </style>

View File

@@ -13,12 +13,6 @@
<template> <template>
<BaseLayout> <BaseLayout>
<div class="list-page"> <div class="list-page">
<div class="page-header">
<div class="header-content">
<h1>Настройки контента</h1>
<p>Юр. реквизиты и параметры подстановки переменных</p>
</div>
</div>
<div class="content-block"> <div class="content-block">
<div class="section-header"> <div class="section-header">
<h2>Юр. реквизиты (переменные)</h2> <h2>Юр. реквизиты (переменные)</h2>
@@ -38,13 +32,136 @@ import BaseLayout from '../../components/BaseLayout.vue';
</script> </script>
<style scoped> <style scoped>
.list-page { padding: 20px; width: 100%; } .list-page {
.page-header { display:flex; justify-content: space-between; align-items: flex-start; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 2px solid #f0f0f0; } padding: 20px;
.header-content h1 { color: var(--color-primary); font-size: 2.2rem; margin: 0 0 8px 0; } width: 100%;
.header-content p { color: var(--color-grey-dark); margin: 0; } }
.content-block { background: #f8f9fa; border-radius: var(--radius-lg); padding: 25px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
.section-header { display:flex; justify-content: space-between; align-items:center; margin-bottom: 20px; } .page-header {
.section-header h2 { color: var(--color-primary); margin: 0; } display: flex;
.empty-state { text-align:center; padding: 60px 20px; } justify-content: space-between;
.empty-icon { font-size: 3rem; color: var(--color-grey-dark); margin-bottom: 10px; } align-items: flex-start;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #f0f0f0;
}
.header-content h1 {
color: var(--color-primary);
font-size: 2.2rem;
margin: 0 0 8px 0;
}
.header-content p {
color: var(--color-grey-dark);
margin: 0;
}
.content-block {
background: #f8f9fa;
border-radius: var(--radius-lg);
padding: 25px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.section-header h2 {
color: var(--color-primary);
margin: 0;
}
.empty-state {
text-align: center;
padding: 60px 20px;
}
.empty-icon {
font-size: 3rem;
color: var(--color-grey-dark);
margin-bottom: 10px;
}
@media (max-width: 768px) {
.list-page {
padding: 16px;
}
.page-header {
flex-direction: column;
gap: 15px;
margin-bottom: 20px;
padding-bottom: 15px;
}
.header-content h1 {
font-size: 1.8rem;
}
.header-content p {
font-size: 1rem;
}
.content-block {
padding: 20px;
}
.section-header {
flex-direction: column;
gap: 10px;
align-items: flex-start;
}
.section-header h2 {
font-size: 1.5rem;
}
.empty-state {
padding: 40px 16px;
}
.empty-icon {
font-size: 2.5rem;
}
.empty-state h3 {
font-size: 1.2rem;
}
.empty-state p {
font-size: 0.9rem;
}
}
@media (max-width: 480px) {
.list-page {
padding: 12px;
}
.page-header {
margin-bottom: 15px;
padding-bottom: 12px;
}
.header-content h1 {
font-size: 1.5rem;
}
.content-block {
padding: 16px;
}
.empty-state {
padding: 30px 12px;
}
.empty-icon {
font-size: 2rem;
}
}
</style> </style>

View File

@@ -16,13 +16,7 @@
<template> <template>
<BaseLayout :is-authenticated="isAuthenticated" :identities="identities" :token-balances="tokenBalances" :is-loading-tokens="isLoadingTokens" @auth-action-completed="$emit('auth-action-completed')"> <BaseLayout :is-authenticated="isAuthenticated" :identities="identities" :token-balances="tokenBalances" :is-loading-tokens="isLoadingTokens" @auth-action-completed="$emit('auth-action-completed')">
<div class="list-page"> <div class="list-page">
<div class="page-header"> <button class="close-btn" @click="goBack" style="margin-bottom: 20px;">×</button>
<div class="header-content">
<h1>Внутренние документы</h1>
<p>Документы, доступные только пользователям с ролями</p>
</div>
<button class="close-btn" @click="goBack">×</button>
</div>
<div class="content-block"> <div class="content-block">
<div class="section-header"> <div class="section-header">
<h2>Список документов</h2> <h2>Список документов</h2>

View File

@@ -19,38 +19,6 @@
@auth-action-completed="$emit('auth-action-completed')" @auth-action-completed="$emit('auth-action-completed')"
> >
<div class="page-view-container"> <div class="page-view-container">
<!-- Заголовок страницы -->
<div v-if="page" class="page-header">
<div class="header-content">
<h1>📄 {{ page.title }}</h1>
<div class="page-meta">
<span class="page-status" :class="page.status">
<i class="fas fa-circle"></i>
{{ getStatusText(page.status) }}
</span>
<span class="page-date">
<i class="fas fa-calendar"></i>
Создано: {{ formatDate(page.createdAt) }}
</span>
<span v-if="page.updatedAt" class="page-date">
<i class="fas fa-edit"></i>
Обновлено: {{ formatDate(page.updatedAt) }}
</span>
</div>
</div>
<div class="header-actions">
<button v-if="canEditPage" class="btn btn-outline" @click="goToEdit">
<i class="fas fa-edit"></i>
Редактировать
</button>
<button v-if="canManageLegalDocs && address" class="btn btn-danger" @click="deletePage" type="button">
<i class="fas fa-trash"></i>
Удалить
</button>
<button class="close-btn" @click="goBack">×</button>
</div>
</div>
<!-- Контент страницы --> <!-- Контент страницы -->
<div v-if="page" class="page-content-block"> <div v-if="page" class="page-content-block">
<div class="page-content"> <div class="page-content">

View File

@@ -19,13 +19,6 @@
@auth-action-completed="$emit('auth-action-completed')" @auth-action-completed="$emit('auth-action-completed')"
> >
<div class="public-pages-page"> <div class="public-pages-page">
<!-- Заголовок страницы -->
<div class="page-header">
<div class="header-content">
<h1>📄 Публичные страницы</h1>
<p>Просмотр опубликованных страниц DLE</p>
</div>
</div>
<!-- Основной контент --> <!-- Основной контент -->
<div class="content-block"> <div class="content-block">

View File

@@ -16,16 +16,12 @@
<template> <template>
<BaseLayout :is-authenticated="isAuthenticated" :identities="identities" :token-balances="tokenBalances" :is-loading-tokens="isLoadingTokens" @auth-action-completed="$emit('auth-action-completed')"> <BaseLayout :is-authenticated="isAuthenticated" :identities="identities" :token-balances="tokenBalances" :is-loading-tokens="isLoadingTokens" @auth-action-completed="$emit('auth-action-completed')">
<div class="docs-page"> <div class="docs-page">
<!-- Заголовок страницы -->
<div class="docs-header"> <div class="docs-header">
<div class="header-content">
<h1>Публичные документы</h1>
</div>
<button class="close-btn" @click="goBack" title="Закрыть">×</button> <button class="close-btn" @click="goBack" title="Закрыть">×</button>
</div> </div>
<!-- Основной контент: сайдбар + контент --> <!-- Основной контент: сайдбар + контент -->
<div class="docs-layout"> <div class="docs-layout" :class="{ 'has-content': currentPageId }">
<!-- Сайдбар навигации --> <!-- Сайдбар навигации -->
<DocsSidebar :current-page-id="currentPageId" /> <DocsSidebar :current-page-id="currentPageId" />
@@ -734,7 +730,8 @@ onBeforeUnmount(() => {
} }
.docs-main { .docs-main {
overflow-y: visible; overflow-y: auto;
min-height: 0;
} }
} }
@@ -954,6 +951,32 @@ onBeforeUnmount(() => {
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.docs-page {
height: auto;
min-height: calc(100vh - 40px);
overflow: visible;
}
.docs-layout {
flex: 1;
min-height: 0;
overflow: visible;
flex-direction: column;
}
/* В мобильной версии, когда выбран документ, скрываем сайдбар */
.docs-layout.has-content .docs-sidebar {
display: none;
}
.docs-main {
overflow-y: visible;
min-height: auto;
width: 100%;
display: block;
flex: 1;
}
.docs-header { .docs-header {
padding: 16px; padding: 16px;
} }

View File

@@ -5,14 +5,8 @@
<template> <template>
<BaseLayout :is-authenticated="isAuthenticated" :identities="identities" :token-balances="tokenBalances" :is-loading-tokens="isLoadingTokens" @auth-action-completed="$emit('auth-action-completed')"> <BaseLayout :is-authenticated="isAuthenticated" :identities="identities" :token-balances="tokenBalances" :is-loading-tokens="isLoadingTokens" @auth-action-completed="$emit('auth-action-completed')">
<div class="list-page"> <div class="list-page">
<div class="page-header"> <div class="header-actions" style="margin-bottom: 20px;">
<div class="header-content"> <button class="close-btn" @click="goBack">×</button>
<h1>Шаблоны документов</h1>
<p>Системные шаблоны для персонализации и публикации.</p>
</div>
<div class="header-actions">
<button class="close-btn" @click="goBack">×</button>
</div>
</div> </div>
<div class="content-block"> <div class="content-block">

View File

@@ -11,9 +11,8 @@
@auth-action-completed="$emit('auth-action-completed')" @auth-action-completed="$emit('auth-action-completed')"
> >
<div class="system-messages-page"> <div class="system-messages-page">
<div class="page-header"> <div class="page-header" v-if="canManageSystemMessages">
<div class="header-content"> <div class="header-content">
<h1>Системные сообщения</h1>
<p v-if="canManageSystemMessages"> <p v-if="canManageSystemMessages">
Создавайте и управляйте уведомлениями, которые видят пользователи в чате и интерфейсе DLE Создавайте и управляйте уведомлениями, которые видят пользователи в чате и интерфейсе DLE
</p> </p>

View File

@@ -19,14 +19,6 @@
@auth-action-completed="$emit('auth-action-completed')" @auth-action-completed="$emit('auth-action-completed')"
> >
<div class="groups-management"> <div class="groups-management">
<div class="management-header">
<div class="header-content">
<h1>Группы</h1>
<p class="groups-description">
Создание и управление группами
</p>
</div>
</div>
<!-- Блоки управления группами --> <!-- Блоки управления группами -->
<div class="management-blocks"> <div class="management-blocks">

View File

@@ -14,13 +14,6 @@
<div class="security-settings"> <div class="security-settings">
<button class="close-btn" @click="goBack">×</button> <button class="close-btn" @click="goBack">×</button>
<!-- Заголовок в стиле основной страницы настроек -->
<div class="management-header">
<div class="header-content">
<h1>Настройки безопасности</h1>
</div>
</div>
<!-- Блоки настроек в едином стиле --> <!-- Блоки настроек в едином стиле -->
<div class="management-blocks"> <div class="management-blocks">
<!-- Столбец 1 --> <!-- Столбец 1 -->

View File

@@ -12,13 +12,6 @@
<template> <template>
<div class="settings-management"> <div class="settings-management">
<!-- Заголовок -->
<div class="management-header">
<div class="header-content">
<h1>Настройки системы</h1>
</div>
</div>
<!-- Блоки настроек --> <!-- Блоки настроек -->
<div class="management-blocks"> <div class="management-blocks">
<!-- Столбец 1 --> <!-- Столбец 1 -->

View File

@@ -19,18 +19,19 @@
@auth-action-completed="$emit('auth-action-completed')" @auth-action-completed="$emit('auth-action-completed')"
> >
<div class="add-module-page"> <div class="add-module-page">
<!-- Заголовок --> <!-- Информация для неавторизованных пользователей -->
<div class="page-header"> <div style="margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center;">
<div class="header-content"> <div v-if="selectedDle?.dleAddress" style="color: var(--color-grey-dark); font-size: 0.9rem;">
<h1>Добавление модуля в DLE</h1> {{ selectedDle.dleAddress }}
<p v-if="selectedDle">{{ selectedDle.name }} ({{ selectedDle.symbol }}) - {{ selectedDle.dleAddress }}</p> </div>
<p v-else-if="isLoadingDle">Загрузка...</p> <div v-else-if="dleAddress" style="color: var(--color-grey-dark); font-size: 0.9rem;">
<p v-else>DLE не выбран (Адрес: {{ dleAddress }})</p> {{ dleAddress }}
</div>
<div v-else-if="isLoadingDle" style="color: var(--color-grey-dark); font-size: 0.9rem;">
Загрузка...
</div> </div>
<button class="close-btn" @click="goBackToProposals">×</button> <button class="close-btn" @click="goBackToProposals">×</button>
</div> </div>
<!-- Информация для неавторизованных пользователей -->
<div v-if="!props.isAuthenticated" class="auth-notice"> <div v-if="!props.isAuthenticated" class="auth-notice">
<div class="alert alert-info"> <div class="alert alert-info">
<i class="fas fa-info-circle"></i> <i class="fas fa-info-circle"></i>

View File

@@ -19,18 +19,16 @@
@auth-action-completed="$emit('auth-action-completed')" @auth-action-completed="$emit('auth-action-completed')"
> >
<div class="analytics-container"> <div class="analytics-container">
<!-- Заголовок --> <!-- Основная информация -->
<div class="page-header"> <div style="margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center;">
<div class="header-content"> <div v-if="dleAddress" style="color: var(--color-grey-dark); font-size: 0.9rem;">
<h1>Аналитика DLE</h1> {{ dleAddress }}
<p v-if="selectedDle">{{ selectedDle.name }} ({{ selectedDle.symbol }}) - {{ formatAddress(dleAddress) }}</p> </div>
<p v-else-if="isLoadingDle">Загрузка...</p> <div v-else-if="isLoadingDle" style="color: var(--color-grey-dark); font-size: 0.9rem;">
<p v-else>Подробная аналитика и статистика DLE</p> Загрузка...
</div> </div>
<button class="close-btn" @click="goBackToBlocks">×</button> <button class="close-btn" @click="goBackToBlocks">×</button>
</div> </div>
<!-- Основная информация -->
<div class="info-section"> <div class="info-section">
<h2>Основная информация</h2> <h2>Основная информация</h2>
<div class="info-grid"> <div class="info-grid">

View File

@@ -19,18 +19,19 @@
@auth-action-completed="$emit('auth-action-completed')" @auth-action-completed="$emit('auth-action-completed')"
> >
<div class="create-proposal-page"> <div class="create-proposal-page">
<!-- Заголовок --> <!-- Информация для неавторизованных пользователей -->
<div class="page-header"> <div style="margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center;">
<div class="header-content"> <div v-if="selectedDle?.dleAddress" style="color: var(--color-grey-dark); font-size: 0.9rem;">
<h1>Создание предложения</h1> {{ selectedDle.dleAddress }}
<p v-if="selectedDle">{{ selectedDle.name }} ({{ selectedDle.symbol }}) - {{ selectedDle.dleAddress }}</p> </div>
<p v-else-if="isLoadingDle">Загрузка...</p> <div v-else-if="dleAddress" style="color: var(--color-grey-dark); font-size: 0.9rem;">
<p v-else>DLE не выбран</p> {{ dleAddress }}
</div>
<div v-else-if="isLoadingDle" style="color: var(--color-grey-dark); font-size: 0.9rem;">
Загрузка...
</div> </div>
<button class="close-btn" @click="goBackToBlocks">×</button> <button class="close-btn" @click="goBackToBlocks">×</button>
</div> </div>
<!-- Информация для неавторизованных пользователей -->
<div v-if="!props.isAuthenticated" class="auth-notice"> <div v-if="!props.isAuthenticated" class="auth-notice">
<div class="alert alert-info"> <div class="alert alert-info">
<i class="fas fa-info-circle"></i> <i class="fas fa-info-circle"></i>

View File

@@ -19,18 +19,13 @@
@auth-action-completed="$emit('auth-action-completed')" @auth-action-completed="$emit('auth-action-completed')"
> >
<div class="dle-blocks-management"> <div class="dle-blocks-management">
<!-- Заголовок --> <!-- Блоки управления -->
<div class="management-header"> <div style="margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center;">
<div class="header-content"> <div v-if="dleAddress" style="color: var(--color-grey-dark); font-size: 0.9rem;">
<h1>Управление DLE</h1> {{ dleAddress }}
<p v-if="dleAddress" class="dle-address">
<strong>DLE:</strong> {{ dleAddress }}
</p>
</div> </div>
<button class="close-btn" @click="router.push('/management')">×</button> <button class="close-btn" @click="router.push('/management')">×</button>
</div> </div>
<!-- Блоки управления -->
<div class="management-blocks"> <div class="management-blocks">
<!-- Столбец 1 --> <!-- Столбец 1 -->
<div class="blocks-column"> <div class="blocks-column">

View File

@@ -19,11 +19,6 @@
@auth-action-completed="$emit('auth-action-completed')" @auth-action-completed="$emit('auth-action-completed')"
> >
<div class="dle-management"> <div class="dle-management">
<div class="management-header">
<h3>🏢 Управление DLE</h3>
<p>Добавление DLE контрактов администраторами</p>
</div>
<!-- Форма добавления DLE --> <!-- Форма добавления DLE -->
<div class="add-dle-form"> <div class="add-dle-form">
<div class="form-header"> <div class="form-header">

View File

@@ -29,11 +29,9 @@
@auth-action-completed="$emit('auth-action-completed')" @auth-action-completed="$emit('auth-action-completed')"
> >
<div class="proposals-page"> <div class="proposals-page">
<!-- Заголовок --> <div style="margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center;">
<div class="page-header"> <div v-if="dleAddress" style="color: var(--color-grey-dark); font-size: 0.9rem;">
<div class="header-content"> {{ dleAddress }}
<h1>Предложения DLE</h1>
<p v-if="dleAddress">Адрес DLE: {{ dleAddress }}</p>
</div> </div>
<button @click="goBack" class="close-btn">×</button> <button @click="goBack" class="close-btn">×</button>
</div> </div>

View File

@@ -19,18 +19,16 @@
@auth-action-completed="$emit('auth-action-completed')" @auth-action-completed="$emit('auth-action-completed')"
> >
<div class="history-container"> <div class="history-container">
<!-- Заголовок --> <!-- Фильтры -->
<div class="page-header"> <div style="margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center;">
<div class="header-content"> <div v-if="selectedDle?.dleAddress" style="color: var(--color-grey-dark); font-size: 0.9rem;">
<h1>История DLE</h1> {{ selectedDle.dleAddress }}
<p v-if="selectedDle">{{ selectedDle.name }} ({{ selectedDle.symbol }}) - {{ selectedDle.dleAddress }}</p> </div>
<p v-else-if="isLoadingDle">Загрузка...</p> <div v-else-if="isLoadingDle" style="color: var(--color-grey-dark); font-size: 0.9rem;">
<p v-else>Лог операций, события и транзакции DLE</p> Загрузка...
</div> </div>
<button class="close-btn" @click="goBackToBlocks">×</button> <button class="close-btn" @click="goBackToBlocks">×</button>
</div> </div>
<!-- Фильтры -->
<div class="filters-section"> <div class="filters-section">
<h2>Фильтры</h2> <h2>Фильтры</h2>
<div class="filters-form"> <div class="filters-form">

View File

@@ -19,24 +19,22 @@
@auth-action-completed="$emit('auth-action-completed')" @auth-action-completed="$emit('auth-action-completed')"
> >
<div class="modules-management"> <div class="modules-management">
<!-- Заголовок --> <!-- Модальное окно деплоя -->
<div class="page-header"> <div style="margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center;">
<div class="header-content"> <div style="display: flex; align-items: center; gap: 20px;">
<div class="title-section"> <div v-if="selectedDle?.dleAddress" style="color: var(--color-grey-dark); font-size: 0.9rem;">
<h1>Модули DLE</h1> {{ selectedDle.dleAddress }}
<div class="websocket-status" :class="{ connected: isWSConnected }" title="WebSocket соединение для обновления модулей"> </div>
<i class="fas fa-circle" :class="isWSConnected ? 'fa-solid' : 'fa-light'"></i> <div v-else-if="isLoadingDle" style="color: var(--color-grey-dark); font-size: 0.9rem;">
<span>{{ isWSConnected ? 'Подключено' : 'Отключено' }}</span> Загрузка...
</div> </div>
<div class="websocket-status" :class="{ connected: isWSConnected }" title="WebSocket соединение для обновления модулей">
<i class="fas fa-circle" :class="isWSConnected ? 'fa-solid' : 'fa-light'"></i>
<span>{{ isWSConnected ? 'Подключено' : 'Отключено' }}</span>
</div> </div>
<p v-if="selectedDle">{{ selectedDle.name }} ({{ selectedDle.symbol }}) - {{ selectedDle.dleAddress }}</p>
<p v-else-if="isLoadingDle">Загрузка...</p>
<p v-else>DLE не выбран</p>
</div> </div>
<button class="close-btn" @click="goBackToBlocks">×</button> <button class="close-btn" @click="goBackToBlocks">×</button>
</div> </div>
<!-- Модальное окно деплоя -->
<div v-if="showDeploymentModal" class="modal-overlay" @click="moduleDeploymentStatus === 'error' || !isDeploying ? closeDeploymentModal() : null"> <div v-if="showDeploymentModal" class="modal-overlay" @click="moduleDeploymentStatus === 'error' || !isDeploying ? closeDeploymentModal() : null">
<div class="modal-content" @click.stop> <div class="modal-content" @click.stop>
<div class="modal-header"> <div class="modal-header">
@@ -171,11 +169,6 @@
<!-- Блоки для деплоя стандартных модулей --> <!-- Блоки для деплоя стандартных модулей -->
<div class="standard-modules"> <div class="standard-modules">
<div class="modules-header">
<h3>🚀 Деплой стандартных модулей</h3>
<p>Быстрый деплой предустановленных модулей DLE</p>
</div>
<div class="modules-grid"> <div class="modules-grid">
<!-- TreasuryModule --> <!-- TreasuryModule -->
<div class="module-deploy-card"> <div class="module-deploy-card">

View File

@@ -19,18 +19,19 @@
@auth-action-completed="$emit('auth-action-completed')" @auth-action-completed="$emit('auth-action-completed')"
> >
<div class="settings-container"> <div class="settings-container">
<!-- Заголовок --> <!-- Основной контент -->
<div class="page-header"> <div style="margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center;">
<div class="header-content"> <div v-if="dleInfo?.address" style="color: var(--color-grey-dark); font-size: 0.9rem;">
<h1>Настройки DLE</h1> {{ dleInfo.address }}
<p v-if="dleInfo">{{ dleInfo.name }} ({{ dleInfo.symbol }}) - {{ dleInfo.address }}</p> </div>
<p v-else-if="address">Загрузка...</p> <div v-else-if="address" style="color: var(--color-grey-dark); font-size: 0.9rem;">
<p v-else>DLE не выбран</p> {{ address }}
</div>
<div v-else-if="isLoading" style="color: var(--color-grey-dark); font-size: 0.9rem;">
Загрузка...
</div> </div>
<button class="close-btn" @click="goBackToBlocks">×</button> <button class="close-btn" @click="goBackToBlocks">×</button>
</div> </div>
<!-- Основной контент -->
<div v-if="dleInfo" class="main-content"> <div v-if="dleInfo" class="main-content">
<!-- Отображение в футере --> <!-- Отображение в футере -->
<div v-if="canSetFooterDle" class="footer-card"> <div v-if="canSetFooterDle" class="footer-card">

View File

@@ -27,7 +27,14 @@ export default defineConfig({
build: { build: {
rollupOptions: { rollupOptions: {
plugins: [polyfillNode()], plugins: [polyfillNode()],
output: {
manualChunks: undefined,
chunkFileNames: 'assets/[name]-[hash].js',
entryFileNames: 'assets/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash].[ext]',
},
}, },
chunkSizeWarningLimit: 1000,
}, },
optimizeDeps: { optimizeDeps: {
esbuildOptions: { esbuildOptions: {