From 13b613e191868f16b85d039f02ccafd24b98bb0d Mon Sep 17 00:00:00 2001 From: root Date: Mon, 15 Sep 2025 09:31:51 +0500 Subject: [PATCH] news --- ARCHITECTURE.md | 197 +++++ DEBUG_NEWS.md | 75 ++ FIX_PROCESS_ERROR.md | 48 ++ FRONTEND_API_GUIDE.md | 1044 +++++++++++++++++++++++++++ NEWS_README.md | 183 +++++ a.md | 351 +++++++++ docs/auth-api.md | 2 +- src/components/news/NewsFilters.vue | 220 ++++++ src/config/api.js | 36 + src/layout/AppMenu.vue | 12 +- src/router/index.js | 24 + src/service/AuthService.js | 13 +- src/service/NewsService.js | 325 +++++++++ src/views/news/NewsAdmin.vue | 498 +++++++++++++ src/views/news/NewsDashboard.vue | 366 ++++++++++ src/views/news/NewsDetail.vue | 327 +++++++++ src/views/news/NewsList.vue | 310 ++++++++ vite.config.mjs | 20 + 18 files changed, 4048 insertions(+), 3 deletions(-) create mode 100644 ARCHITECTURE.md create mode 100644 DEBUG_NEWS.md create mode 100644 FIX_PROCESS_ERROR.md create mode 100644 FRONTEND_API_GUIDE.md create mode 100644 NEWS_README.md create mode 100644 a.md create mode 100644 src/components/news/NewsFilters.vue create mode 100644 src/config/api.js create mode 100644 src/service/NewsService.js create mode 100644 src/views/news/NewsAdmin.vue create mode 100644 src/views/news/NewsDashboard.vue create mode 100644 src/views/news/NewsDetail.vue create mode 100644 src/views/news/NewsList.vue diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..3c5e64a --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,197 @@ +# Архитектура новостного интерфейса + +## Схема компонентов + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Vue.js Frontend │ +├─────────────────────────────────────────────────────────────────┤ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ NewsList │ │ NewsDashboard │ │ NewsAdmin │ │ +│ │ (Лента) │ │ (Статистика) │ │ (Управление) │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +│ │ │ │ │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ NewsFilters │ │ NewsDetail │ │ NewsService │ │ +│ │ (Фильтры) │ │ (Детали) │ │ (API слой) │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ News Parser API │ +├─────────────────────────────────────────────────────────────────┤ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ GET /items │ │ GET /stats │ │ GET /health │ │ +│ │ (Новости) │ │ (Статистика) │ │ (Статус) │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ POST /parse/all │ │POST /parse/{id} │ │GET /health/mongo│ │ +│ │ (Парсинг всех) │ │ (Парсинг по ID) │ │ (Статус БД) │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ MongoDB Database │ +├─────────────────────────────────────────────────────────────────┤ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ News Items │ │ Statistics │ │ Health Logs │ │ +│ │ (Новости) │ │ (Статистика) │ │ (Логи) │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## Поток данных + +### 1. Загрузка новостей + +``` +User → NewsList → NewsService → API → MongoDB + ← NewsFilters ← +``` + +### 2. Фильтрация + +``` +User → NewsFilters → NewsList → NewsService → API (с параметрами) +``` + +### 3. Статистика + +``` +User → NewsDashboard → NewsService → API → MongoDB +``` + +### 4. Управление парсером + +``` +User → NewsAdmin → NewsService → API → Parser Service → MongoDB +``` + +## Структура данных + +### NewsItem + +```javascript +{ + id: string, + sourceName: string, + title: string, + url: string, + publishedAt: string, + addedAt: string, + rawText: string, + hash: string, + category: string, + analytics: { + summary: string, + sentiment: string, + tags: string[], + entities: object + } +} +``` + +### API Response + +```javascript +{ + success: boolean, + message: string, + data: { + content: NewsItem[], + pageable: object, + totalElements: number, + totalPages: number, + last: boolean, + first: boolean, + numberOfElements: number + } +} +``` + +## Технологический стек + +### Frontend + +- **Vue 3** - основной фреймворк +- **PrimeVue** - UI компоненты +- **Tailwind CSS** - стилизация +- **Chart.js** - графики и диаграммы +- **Vue Router** - маршрутизация + +### Backend API + +- **REST API** - HTTP эндпоинты +- **JSON** - формат данных +- **MongoDB** - база данных + +### Инструменты разработки + +- **Vite** - сборщик +- **ESLint** - линтер +- **Prettier** - форматирование + +## Принципы архитектуры + +### 1. Разделение ответственности + +- **Компоненты** - только UI логика +- **Сервисы** - работа с API +- **Конфигурация** - настройки + +### 2. Переиспользование + +- **NewsService** - единая точка доступа к API +- **NewsFilters** - переиспользуемый компонент +- **Конфигурация** - централизованные настройки + +### 3. Производительность + +- **Ленивая загрузка** - компоненты загружаются по требованию +- **Кэширование** - данные статистики кэшируются +- **Оптимизация** - минимальные API запросы + +### 4. Масштабируемость + +- **Модульная структура** - легко добавлять новые функции +- **Конфигурируемость** - настройки через переменные окружения +- **Расширяемость** - простое добавление новых источников + +## Безопасность + +### 1. Валидация данных + +- **Проверка ответов API** - всегда проверяется поле `success` +- **Обработка ошибок** - graceful degradation +- **Санитизация** - очистка пользовательского ввода + +### 2. Аутентификация + +- **Требуется авторизация** - все маршруты защищены +- **Роли пользователей** - админ функции только для админов + +### 3. CORS + +- **Настройка CORS** - на уровне API сервера +- **Безопасные запросы** - только к настроенному API + +## Мониторинг и отладка + +### 1. Логирование + +- **Консольные логи** - для разработки +- **Обработка ошибок** - централизованная система + +### 2. Метрики + +- **Статистика загрузки** - количество запросов +- **Время отклика** - производительность API +- **Статус системы** - мониторинг здоровья + +### 3. Отладка + +- **DevTools** - инструменты разработчика +- **Network tab** - мониторинг запросов +- **Console** - логи и ошибки diff --git a/DEBUG_NEWS.md b/DEBUG_NEWS.md new file mode 100644 index 0000000..d7805fa --- /dev/null +++ b/DEBUG_NEWS.md @@ -0,0 +1,75 @@ +# Отладка проблем с новостным интерфейсом + +## Проблема: Не открываются страницы новостей + +### Шаги для диагностики: + +1. **Откройте консоль браузера** (F12 → Console) +2. **Попробуйте кликнуть на "Лента новостей"** в меню +3. **Проверьте ошибки в консоли** + +### Возможные причины и решения: + +#### 1. Ошибки импорта компонентов + +**Симптомы:** Ошибки типа "Failed to resolve component" или "Cannot resolve module" +**Решение:** Убедитесь, что все PrimeVue компоненты импортированы + +#### 2. Ошибки в API запросах + +**Симптомы:** Ошибки сети или CORS +**Решение:** + +- Проверьте, что бэкенд API запущен на https://api.konturai.kz +- Создайте файл `.env` с содержимым: `VUE_APP_API_URL=https://api.konturai.kz` + +#### 3. Ошибки в коде компонентов + +**Симптомы:** JavaScript ошибки в консоли +**Решение:** Проверьте синтаксис и логику компонентов + +### Быстрая проверка: + +1. **Проверьте, что сервер запущен:** + + ```bash + npm run dev + ``` + +2. **Проверьте консоль браузера** на наличие ошибок + +3. **Попробуйте перейти напрямую по URL:** + - http://localhost:3000/news + - http://localhost:3000/news/dashboard + - http://localhost:3000/admin/news + +### Если ничего не помогает: + +1. **Очистите кэш браузера** (Ctrl+Shift+R) +2. **Перезапустите сервер разработки** +3. **Проверьте, что все файлы сохранены** + +### Тестовый компонент: + +Если основные компоненты не работают, можно временно заменить их на простой тестовый компонент: + +```vue + + + +``` + +### Контакты: + +Если проблема не решается, проверьте: + +- Версию Node.js (должна быть 16+) +- Версию npm (должна быть 8+) +- Наличие всех зависимостей (`npm install`) diff --git a/FIX_PROCESS_ERROR.md b/FIX_PROCESS_ERROR.md new file mode 100644 index 0000000..6f01b64 --- /dev/null +++ b/FIX_PROCESS_ERROR.md @@ -0,0 +1,48 @@ +# Исправление ошибки "process is not defined" + +## Проблема + +В консоли браузера появляется ошибка: + +``` +[Vue Router warn]: Unexpected error when starting the router: ReferenceError: process is not defined at api.js:3:15 +``` + +## Причина + +В браузере нет объекта `process`, который используется для доступа к переменным окружения Node.js. В Vite нужно использовать `import.meta.env` вместо `process.env`. + +## Решение + +Ошибка уже исправлена в файле `src/config/api.js`: + +**Было:** + +```javascript +BASE_URL: process.env.VUE_APP_API_URL || 'https://api.konturai.kz', +``` + +**Стало:** + +```javascript +BASE_URL: 'https://api.konturai.kz', +``` + +## Дополнительные исправления + +1. **Добавлен Chart.js в main.js** для корректной работы графиков +2. **Зарегистрирован Chart как глобальный компонент** +3. **Исправлены все импорты PrimeVue компонентов** + +## Проверка + +1. Перезапустите сервер разработки: `npm run dev` +2. Обновите страницу в браузере (Ctrl+Shift+R) +3. Проверьте консоль - ошибка должна исчезнуть +4. Попробуйте перейти на страницы новостей + +## Если проблема остается + +1. Очистите кэш браузера +2. Удалите папку `node_modules` и выполните `npm install` +3. Перезапустите сервер разработки diff --git a/FRONTEND_API_GUIDE.md b/FRONTEND_API_GUIDE.md new file mode 100644 index 0000000..fc26e93 --- /dev/null +++ b/FRONTEND_API_GUIDE.md @@ -0,0 +1,1044 @@ +# Руководство по API для фронтенда + +## Обзор изменений + +После рефакторинга старый `ParserController` был разделен на три специализированных контроллера для лучшей организации и масштабируемости: + +- **`MarketItemController`** (`/api/items`) - для получения данных +- **`ParserAdminController`** (`/api/admin/parsers`) - для управления парсерами +- **`HealthCheckController`** (`/api/health`) - для мониторинга системы + +--- + +## 📊 MarketItemController - Получение данных + +**Базовый URL**: `/api/items` + +### 1. Получение списка новостей с фильтрацией и пагинацией + +```http +GET /api/items +``` + +**Параметры запроса:** + +- `sourceName` (optional) - фильтр по источнику (например: "Kursiv Media", "Kapital.kz", "LSM.kz", "РБК", "Ведомости") +- `startDate` (optional) - начальная дата в формате ISO (например: "2024-01-01T00:00:00") +- `endDate` (optional) - конечная дата в формате ISO (например: "2024-12-31T23:59:59") +- `page` (optional) - номер страницы (по умолчанию: 0) +- `size` (optional) - размер страницы (по умолчанию: 20) +- `sort` (optional) - сортировка (по умолчанию: "publishedAt,desc") + +**Примеры запросов:** + +```javascript +// Получить все новости +fetch('/api/items'); + +// Получить новости с пагинацией +fetch('/api/items?page=0&size=10'); + +// Получить новости от конкретного источника +fetch('/api/items?sourceName=Kursiv Media'); + +// Получить новости за последние 7 дней +const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(); +fetch(`/api/items?startDate=${weekAgo}`); + +// Получить новости с сортировкой по дате (старые сначала) +fetch('/api/items?sort=publishedAt,asc'); +``` + +**Ответ:** + +```json +{ + "success": true, + "message": "Записи получены успешно", + "data": { + "content": [ + { + "id": "64f1a2b3c4d5e6f7g8h9i0j1", + "sourceName": "Kursiv Media", + "url": "https://kursiv.media/news/example", + "title": "Заголовок новости", + "publishedAt": "2024-01-15T10:30:00", + "addedAt": "2024-01-15T10:35:00", + "rawText": "Текст новости без HTML тегов", + "hash": "abc123def456...", + "category": "Бизнес", + "analytics": { + "summary": null, + "sentiment": null, + "tags": [], + "entities": {} + } + } + ], + "pageable": { + "sort": { + "sorted": true, + "unsorted": false + }, + "pageNumber": 0, + "pageSize": 20, + "offset": 0, + "paged": true, + "unpaged": false + }, + "totalElements": 150, + "totalPages": 8, + "last": false, + "first": true, + "numberOfElements": 20, + "size": 20, + "number": 0, + "empty": false + } +} +``` + +### 2. Получение новости по ID + +```http +GET /api/items/{id} +``` + +**Пример:** + +```javascript +fetch('/api/items/64f1a2b3c4d5e6f7g8h9i0j1'); +``` + +**Ответ:** + +```json +{ + "success": false, + "message": "Функция поиска по ID пока не реализована" +} +``` + +### 3. Получение статистики + +```http +GET /api/items/stats +``` + +**Ответ:** + +```json +{ + "success": true, + "message": "Статистика получена успешно", + "data": { + "totalItems": 1250, + "sourceStatistics": { + "Kursiv Media": 300, + "Kapital.kz": 250, + "LSM.kz": 200, + "РБК": 300, + "Ведомости": 200 + }, + "lastUpdate": "2024-01-15T10:35:00" + } +} +``` + +### 4. Получение списка источников (sourceName) + +```http +GET /api/items/sources +``` + +**Описание:** +Возвращает список всех уникальных источников новостей, которые есть в базе данных. Этот эндпоинт полезен для: + +- Построения фильтров по источникам +- Отображения списка доступных источников в UI +- Динамического создания выпадающих списков + +**Ответ:** + +```json +{ + "success": true, + "message": "Источники получены успешно", + "data": ["Kursiv (Бизнес/экономика)", "Kapital.kz (Бизнес)", "LSM.kz", "РБК", "Ведомости"] +} +``` + +**Примеры использования:** + +```javascript +// Получить список всех источников +const response = await fetch('/api/items/sources'); +const result = await response.json(); + +if (result.success) { + const sources = result.data; + console.log('Доступные источники:', sources); + // sources = ["Kursiv (Бизнес/экономика)", "Kapital.kz (Бизнес)", "LSM.kz", "РБК", "Ведомости"] +} + +// Использование в React для создания фильтра +function SourceFilter({ onSourceChange }) { + const [sources, setSources] = useState([]); + + useEffect(() => { + fetch('/api/items/sources') + .then((res) => res.json()) + .then((data) => { + if (data.success) { + setSources(data.data); + } + }); + }, []); + + return ( + + ); +} + +// Использование в Vue.js +export default { + data() { + return { + sources: [], + selectedSource: '' + }; + }, + async mounted() { + const response = await fetch('/api/items/sources'); + const result = await response.json(); + if (result.success) { + this.sources = result.data; + } + } +}; +``` + +**Важные замечания:** + +- Список источников формируется на основе реальных данных в базе +- Если в базе нет данных от какого-то источника, он не будет включен в список +- Названия источников точно соответствуют тем, что используются в поле `sourceName` при фильтрации +- Порядок источников может изменяться в зависимости от того, как MongoDB возвращает уникальные значения + +### 5. Получение новостей по конкретному источнику + +```http +GET /api/items/source/{sourceName} +``` + +**Пример:** + +```javascript +fetch('/api/items/source/Kursiv Media?page=0&size=10'); +``` + +**Ответ:** Аналогичен ответу от `/api/items`, но только с новостями от указанного источника. + +--- + +## ⚙️ ParserAdminController - Управление парсерами + +**Базовый URL**: `/api/admin/parsers` + +> ⚠️ **Важно**: Эти эндпоинты предназначены для административных функций и могут потребовать аутентификации в будущем. + +### 1. Запуск конкретного парсера + +```http +POST /api/admin/parsers/parse/{sourceName} +``` + +**Доступные источники:** + +- `kursiv` +- `kapital` +- `lsm` +- `rbc` +- `vedomosti` + +**Пример:** + +```javascript +// Запустить парсер Kursiv +fetch('/api/admin/parsers/parse/kursiv', { method: 'POST' }); + +// Запустить парсер РБК +fetch('/api/admin/parsers/parse/rbc', { method: 'POST' }); +``` + +**Ответ:** + +```json +{ + "success": true, + "message": "Парсинг kursiv завершен", + "data": { + "source": "kursiv", + "processedItems": 15, + "totalItemsInDb": 1265, + "status": "completed" + } +} +``` + +### 2. Запуск всех парсеров + +```http +POST /api/admin/parsers/parse/all +``` + +**Пример:** + +```javascript +fetch('/api/admin/parsers/parse/all', { method: 'POST' }); +``` + +**Ответ:** + +```json +{ + "success": true, + "message": "Парсинг всех источников завершен", + "data": [ + { + "source": "kursiv", + "processedItems": 12, + "totalItemsInDb": 1277, + "status": "completed" + }, + { + "source": "kapital", + "processedItems": 8, + "totalItemsInDb": 1285, + "status": "completed" + }, + { + "source": "lsm", + "processedItems": 5, + "totalItemsInDb": 1290, + "status": "completed" + }, + { + "source": "rbc", + "processedItems": 20, + "totalItemsInDb": 1310, + "status": "completed" + }, + { + "source": "vedomosti", + "processedItems": 7, + "totalItemsInDb": 1317, + "status": "completed" + } + ] +} +``` + +### 3. Получение списка доступных парсеров + +```http +GET /api/admin/parsers +``` + +**Ответ:** + +```json +{ + "success": true, + "message": "Список парсеров получен", + "data": ["kapital", "kursiv", "lsm", "rbc", "vedomosti"] +} +``` + +### 4. Получение информации о парсерах + +```http +GET /api/admin/parsers/info +``` + +**Ответ:** + +```json +{ + "success": true, + "message": "Информация о парсерах получена", + "data": { + "totalParsers": 5, + "availableParsers": ["kapital", "kursiv", "lsm", "rbc", "vedomosti"], + "totalItems": 1317, + "sourceStatistics": { + "Kursiv Media": 300, + "Kapital.kz": 250, + "LSM.kz": 200, + "РБК": 300, + "Ведомости": 200 + } + } +} +``` + +### 5. Проверка существования парсера + +```http +GET /api/admin/parsers/exists/{sourceName} +``` + +**Пример:** + +```javascript +fetch('/api/admin/parsers/exists/kursiv'); +``` + +**Ответ:** + +```json +{ + "success": true, + "message": "Проверка завершена", + "data": true +} +``` + +--- + +## 🔍 HealthCheckController - Мониторинг системы + +**Базовый URL**: `/api/health` + +### 1. Базовая проверка состояния + +```http +GET /api/health +``` + +**Ответ:** + +```json +{ + "success": true, + "message": "Сервис работает", + "data": { + "status": "UP", + "service": "RSS Parser System", + "timestamp": 1705312500000, + "details": "Enabled - runs every 30 minutes" + } +} +``` + +### 2. Проверка подключения к MongoDB + +```http +GET /api/health/mongodb +``` + +**Ответ (успех):** + +```json +{ + "success": true, + "message": "MongoDB подключение успешно", + "data": { + "status": "UP", + "service": "MongoDB подключение успешно", + "timestamp": 1705312500000, + "details": "Всего записей: 1317" + } +} +``` + +**Ответ (ошибка):** + +```json +{ + "success": false, + "message": "Ошибка подключения к MongoDB", + "data": { + "status": "DOWN", + "service": "Ошибка подключения к MongoDB: Connection refused", + "timestamp": 1705312500000, + "suggestion": "Проверьте учетные данные в application.properties" + } +} +``` + +### 3. Информация о планировщике + +```http +GET /api/health/scheduler +``` + +**Ответ:** + +```json +{ + "success": true, + "message": "Информация о планировщике получена", + "data": { + "schedulerEnabled": true, + "cronExpression": "0 0/30 * * * ?", + "description": "Запуск каждые 30 минут (в 0 и 30 минут каждого часа)", + "nextRun": "Следующий запуск будет в ближайшие 0 или 30 минут часа", + "activeParsers": ["kapital", "kursiv", "lsm", "rbc", "vedomosti"], + "totalParsers": 5, + "sources": ["Kursiv Media", "Kapital.kz", "LSM.kz", "РБК", "Ведомости"] + } +} +``` + +### 4. Общая информация о системе + +```http +GET /api/health/system +``` + +**Ответ:** + +```json +{ + "success": true, + "message": "Информация о системе получена", + "data": { + "parsers": { + "total": 5, + "available": ["kapital", "kursiv", "lsm", "rbc", "vedomosti"] + }, + "data": { + "totalItems": 1317, + "sourceStatistics": { + "Kursiv Media": 300, + "Kapital.kz": 250, + "LSM.kz": 200, + "РБК": 300, + "Ведомости": 200 + } + }, + "system": { + "javaVersion": "17.0.2", + "osName": "Linux", + "osVersion": "5.4.0-74-generic", + "uptime": 1705312500000 + } + } +} +``` + +### 5. Проверка состояния парсеров + +```http +GET /api/health/parsers +``` + +**Ответ:** + +```json +{ + "success": true, + "message": "Проверка парсеров завершена", + "data": { + "totalParsers": 5, + "availableParsers": ["kapital", "kursiv", "lsm", "rbc", "vedomosti"], + "status": "UP", + "message": "Все парсеры доступны" + } +} +``` + +--- + +## 🚀 Примеры использования для фронтенда + +### React/JavaScript примеры + +```javascript +// Сервис для работы с API +class NewsApiService { + constructor(baseUrl = '') { + this.baseUrl = baseUrl; + } + + // Получить новости с фильтрацией + async getNews(filters = {}) { + const params = new URLSearchParams(); + + if (filters.sourceName) params.append('sourceName', filters.sourceName); + if (filters.startDate) params.append('startDate', filters.startDate); + if (filters.endDate) params.append('endDate', filters.endDate); + if (filters.page !== undefined) params.append('page', filters.page); + if (filters.size) params.append('size', filters.size); + if (filters.sort) params.append('sort', filters.sort); + + const response = await fetch(`${this.baseUrl}/api/items?${params}`); + return response.json(); + } + + // Получить статистику + async getStats() { + const response = await fetch(`${this.baseUrl}/api/items/stats`); + return response.json(); + } + + // Получить источники + async getSources() { + const response = await fetch(`${this.baseUrl}/api/items/sources`); + return response.json(); + } + + // Получить источники с обработкой ошибок + async getSourcesWithErrorHandling() { + try { + const response = await fetch(`${this.baseUrl}/api/items/sources`); + const result = await response.json(); + + if (!result.success) { + throw new Error(result.message || 'Ошибка при получении источников'); + } + + return result.data; // Возвращаем только массив источников + } catch (error) { + console.error('Ошибка при получении источников:', error); + return []; // Возвращаем пустой массив в случае ошибки + } + } + + // Запустить парсер (админ функция) + async runParser(sourceName) { + const response = await fetch(`${this.baseUrl}/api/admin/parsers/parse/${sourceName}`, { + method: 'POST' + }); + return response.json(); + } + + // Запустить все парсеры (админ функция) + async runAllParsers() { + const response = await fetch(`${this.baseUrl}/api/admin/parsers/parse/all`, { + method: 'POST' + }); + return response.json(); + } + + // Проверить состояние системы + async getHealthStatus() { + const response = await fetch(`${this.baseUrl}/api/health`); + return response.json(); + } +} + +// Использование +const apiService = new NewsApiService(); + +// Получить последние новости +const news = await apiService.getNews({ + page: 0, + size: 20, + sort: 'publishedAt,desc' +}); + +// Получить новости от конкретного источника +const kursivNews = await apiService.getNews({ + sourceName: 'Kursiv Media', + page: 0, + size: 10 +}); + +// Получить новости за последнюю неделю +const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(); +const recentNews = await apiService.getNews({ + startDate: weekAgo, + sort: 'publishedAt,desc' +}); + +// Получить список источников для фильтра +const sources = await apiService.getSourcesWithErrorHandling(); +console.log('Доступные источники:', sources); + +// Получить новости от конкретного источника +if (sources.length > 0) { + const newsFromFirstSource = await apiService.getNews({ + sourceName: sources[0], + page: 0, + size: 10 + }); +} +``` + +### Vue.js примеры + +```javascript +// composable для работы с API +export function useNewsApi() { + const baseUrl = ''; + + const getNews = async (filters = {}) => { + const params = new URLSearchParams(); + Object.entries(filters).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + params.append(key, value); + } + }); + + const response = await fetch(`${baseUrl}/api/items?${params}`); + return response.json(); + }; + + const getStats = async () => { + const response = await fetch(`${baseUrl}/api/items/stats`); + return response.json(); + }; + + const getSources = async () => { + const response = await fetch(`${baseUrl}/api/items/sources`); + return response.json(); + }; + + const getSourcesList = async () => { + try { + const response = await fetch(`${baseUrl}/api/items/sources`); + const result = await response.json(); + return result.success ? result.data : []; + } catch (error) { + console.error('Ошибка при получении источников:', error); + return []; + } + }; + + return { + getNews, + getStats, + getSources, + getSourcesList + }; +} +``` + +--- + +## 🎯 Практические примеры использования эндпоинта источников + +### 1. Создание динамического фильтра источников + +```javascript +// React компонент для фильтрации по источникам +import React, { useState, useEffect } from 'react'; + +function NewsFilter({ onFilterChange }) { + const [sources, setSources] = useState([]); + const [selectedSource, setSelectedSource] = useState(''); + + useEffect(() => { + // Загружаем список источников при монтировании компонента + fetch('/api/items/sources') + .then((res) => res.json()) + .then((data) => { + if (data.success) { + setSources(data.data); + } + }) + .catch((err) => console.error('Ошибка загрузки источников:', err)); + }, []); + + const handleSourceChange = (source) => { + setSelectedSource(source); + onFilterChange({ sourceName: source || undefined }); + }; + + return ( +
+ + +
+ ); +} + +export default NewsFilter; +``` + +### 2. Отображение статистики по источникам + +```javascript +// Vue.js компонент для отображения статистики + + + +``` + +### 3. Автодополнение для поиска по источникам + +```javascript +// Angular сервис для автодополнения +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable, BehaviorSubject } from 'rxjs'; +import { map, catchError } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root' +}) +export class SourceService { + private sourcesSubject = new BehaviorSubject([]); + public sources$ = this.sourcesSubject.asObservable(); + + constructor(private http: HttpClient) { + this.loadSources(); + } + + private loadSources(): void { + this.http.get('/api/items/sources') + .pipe( + map(response => response.success ? response.data : []), + catchError(error => { + console.error('Ошибка загрузки источников:', error); + return []; + }) + ) + .subscribe(sources => { + this.sourcesSubject.next(sources); + }); + } + + searchSources(query: string): Observable { + return this.sources$.pipe( + map(sources => + sources.filter(source => + source.toLowerCase().includes(query.toLowerCase()) + ) + ) + ); + } + + getSources(): Observable { + return this.sources$; + } +} + +// Компонент автодополнения +@Component({ + selector: 'app-source-autocomplete', + template: ` +
+ +
+
+ {{ source }} +
+
+
+ ` +}) +export class SourceAutocompleteComponent { + searchQuery = ''; + filteredSources: string[] = []; + + constructor(private sourceService: SourceService) {} + + onSearch(query: string): void { + if (query.length > 0) { + this.sourceService.searchSources(query) + .subscribe(sources => { + this.filteredSources = sources; + }); + } else { + this.filteredSources = []; + } + } + + selectSource(source: string): void { + this.searchQuery = source; + this.filteredSources = []; + // Эмитим событие выбора источника + } +} +``` + +### 4. Кэширование списка источников + +```javascript +// Сервис с кэшированием для React +class SourceCacheService { + constructor() { + this.cache = null; + this.cacheTime = null; + this.cacheTimeout = 5 * 60 * 1000; // 5 минут + } + + async getSources() { + // Проверяем, есть ли актуальный кэш + if (this.cache && this.cacheTime && Date.now() - this.cacheTime < this.cacheTimeout) { + return this.cache; + } + + try { + const response = await fetch('/api/items/sources'); + const result = await response.json(); + + if (result.success) { + this.cache = result.data; + this.cacheTime = Date.now(); + return result.data; + } else { + throw new Error(result.message); + } + } catch (error) { + console.error('Ошибка загрузки источников:', error); + // Возвращаем кэш, если есть, даже если он устарел + return this.cache || []; + } + } + + clearCache() { + this.cache = null; + this.cacheTime = null; + } +} + +// Использование в React хуке +function useSources() { + const [sources, setSources] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const sourceService = new SourceCacheService(); + + sourceService + .getSources() + .then((data) => { + setSources(data); + setLoading(false); + }) + .catch((err) => { + setError(err.message); + setLoading(false); + }); + }, []); + + return { sources, loading, error }; +} +``` + +--- + +## 📝 Важные замечания + +1. **Пагинация**: Все эндпоинты для получения данных поддерживают пагинацию через параметры `page` и `size`. + +2. **Сортировка**: По умолчанию новости сортируются по дате публикации (новые сначала). Можно изменить через параметр `sort`. + +3. **Фильтрация**: Поддерживается фильтрация по источнику и диапазону дат. + +4. **Формат дат**: Используйте ISO 8601 формат для дат (например: "2024-01-15T10:30:00"). + +5. **Обработка ошибок**: Все ответы содержат поле `success` для проверки успешности операции. + +6. **Административные функции**: Эндпоинты `/api/admin/parsers/*` предназначены для административных функций. + +7. **Мониторинг**: Используйте эндпоинты `/api/health/*` для проверки состояния системы. + +--- + +## 🔄 Миграция с старого API + +Если у вас был код, использующий старый `ParserController`, вот соответствие эндпоинтов: + +| Старый эндпоинт | Новый эндпоинт | +| ---------------------------------- | ----------------------------------------- | +| `POST /api/parser/parse/kursiv` | `POST /api/admin/parsers/parse/kursiv` | +| `POST /api/parser/parse/kapital` | `POST /api/admin/parsers/parse/kapital` | +| `POST /api/parser/parse/lsm` | `POST /api/admin/parsers/parse/lsm` | +| `POST /api/parser/parse/rbc` | `POST /api/admin/parsers/parse/rbc` | +| `POST /api/parser/parse/vedomosti` | `POST /api/admin/parsers/parse/vedomosti` | +| `POST /api/parser/parse/all` | `POST /api/admin/parsers/parse/all` | +| `GET /api/parser/items` | `GET /api/items` | +| `GET /api/parser/stats` | `GET /api/items/stats` | +| `GET /api/parser/health` | `GET /api/health` | +| `GET /api/parser/health/mongodb` | `GET /api/health/mongodb` | +| `GET /api/parser/scheduler/info` | `GET /api/health/scheduler` | diff --git a/NEWS_README.md b/NEWS_README.md new file mode 100644 index 0000000..9334421 --- /dev/null +++ b/NEWS_README.md @@ -0,0 +1,183 @@ +# Новостной интерфейс для KonturAI + +Этот модуль реализует полноценный интерфейс для работы с новостным парсером согласно API документации. + +## 🚀 Возможности + +### 📰 Лента новостей (`/news`) + +- **Отображение новостей** с заголовком, источником, датой и превью текста +- **Пагинация** с поддержкой "бесконечной прокрутки" и классической пагинации +- **Сортировка** по дате публикации, дате добавления, заголовку и источнику +- **Фильтрация** по источнику и датам +- **Быстрые фильтры** (сегодня, неделя, месяц) +- **Статистика** на странице с количеством новостей и активных фильтров + +### 📊 Дашборд статистики (`/news/dashboard`) + +- **Общая статистика** с количеством новостей, источников и средним количеством на источник +- **Статус системы** с проверкой состояния сервиса и MongoDB +- **Круговая диаграмма** распределения новостей по источникам +- **Детальная статистика** по каждому источнику + +### ⚙️ Админ-панель (`/admin/news`) + +- **Управление парсером** с кнопками запуска парсинга +- **Мониторинг состояния** сервиса и базы данных +- **Результаты парсинга** с отображением обработанных новостей +- **Статистика в реальном времени** + +### 📄 Детальный просмотр новости (`/news/:id`) + +- **Полный текст** новости с форматированием +- **Аналитика** (краткое содержание, тональность, теги, сущности) +- **Техническая информация** (ID, URL, хеш, даты) +- **Действия** (открыть оригинал, поделиться, вернуться) + +## 🛠 Технические детали + +### Структура файлов + +``` +src/ +├── service/ +│ └── NewsService.js # Сервис для работы с API +├── config/ +│ └── api.js # Конфигурация API +├── components/news/ +│ └── NewsFilters.vue # Компонент фильтров +└── views/news/ + ├── NewsList.vue # Лента новостей + ├── NewsDashboard.vue # Дашборд статистики + ├── NewsAdmin.vue # Админ-панель + └── NewsDetail.vue # Детальный просмотр +``` + +### API Endpoints + +- `GET /api/parser/items` - получение новостей с фильтрацией и пагинацией +- `GET /api/parser/stats` - статистика системы +- `GET /api/parser/health` - статус сервиса +- `GET /api/parser/health/mongodb` - статус MongoDB +- `POST /api/parser/parse/all` - запуск парсинга всех источников +- `POST /api/parser/parse/{source}` - запуск парсинга конкретного источника + +### Конфигурация + +Создайте файл `.env` в корне проекта: + +```env +VUE_APP_API_URL=https://api.konturai.kz +``` + +## 🎨 UI/UX особенности + +### Дизайн + +- **Современный интерфейс** с использованием PrimeVue компонентов +- **Адаптивная верстка** для всех устройств +- **Цветовая схема** с интуитивными индикаторами статуса +- **Анимации и переходы** для улучшения пользовательского опыта + +### Интерактивность + +- **Hover эффекты** на карточках новостей +- **Загрузочные индикаторы** для всех асинхронных операций +- **Уведомления** об успешных операциях и ошибках +- **Tooltips** для дополнительной информации + +### Производительность + +- **Ленивая загрузка** компонентов +- **Кэширование** данных статистики +- **Оптимизированные запросы** с минимальным количеством API вызовов + +## 🔧 Настройка и запуск + +1. **Установите зависимости:** + + ```bash + npm install + ``` + +2. **Настройте API URL:** + + ```bash + echo "VUE_APP_API_URL=http://your-api-server:8080" > .env + ``` + +3. **Запустите в режиме разработки:** + + ```bash + npm run dev + ``` + +4. **Соберите для продакшена:** + ```bash + npm run build + ``` + +## 📱 Навигация + +### Основное меню + +- **Home** → New Dashboard +- **Новости** → Лента новостей, Статистика +- **Admin** → Users, Управление парсером + +### Маршруты + +- `/news` - лента новостей +- `/news/dashboard` - статистика +- `/news/:id` - детальный просмотр новости +- `/admin/news` - управление парсером + +## 🎯 Особенности реализации + +### Фильтрация + +- **Источник**: выпадающий список с динамической загрузкой +- **Даты**: календарь с поддержкой диапазонов +- **Сортировка**: поле и направление +- **Размер страницы**: 10, 20, 50, 100 новостей + +### Пагинация + +- **Классическая**: кнопки страниц с информацией +- **Бесконечная**: кнопка "Загрузить еще" +- **Навигация**: первая, предыдущая, следующая, последняя + +### Обработка ошибок + +- **Graceful degradation** при недоступности API +- **Информативные сообщения** об ошибках +- **Retry механизмы** для критических операций + +### Аналитика + +- **Chart.js** для визуализации данных +- **Цветовое кодирование** источников +- **Процентное соотношение** в статистике + +## 🔮 Планы развития + +- [ ] **WebSocket** для обновлений в реальном времени +- [ ] **Поиск** по содержимому новостей +- [ ] **Избранное** и закладки +- [ ] **Экспорт** данных в различных форматах +- [ ] **Уведомления** о новых новостях +- [ ] **Темная тема** интерфейса +- [ ] **Мобильное приложение** на базе Capacitor + +## 📞 Поддержка + +При возникновении проблем: + +1. Проверьте конфигурацию API URL +2. Убедитесь, что бэкенд сервис запущен +3. Проверьте консоль браузера на наличие ошибок +4. Обратитесь к документации API + +--- + +**Создано с ❤️ для эффективной работы с новостными данными** diff --git a/a.md b/a.md new file mode 100644 index 0000000..edf931c --- /dev/null +++ b/a.md @@ -0,0 +1,351 @@ +# Руководство по API для фронтенда + +## Обзор + +Это руководство описывает API сервиса `Parser`, который собирает и предоставляет новостные данные. API спроектировано для эффективной работы с клиентскими приложениями и поддерживает пагинацию, сортировку и фильтрацию. + +## Реализация в UI: что мы должны показать пользователю + +Ниже описаны ключевые элементы интерфейса, которые необходимо создать, используя эндпоинты из этого руководства. + +### 1. Основной экран: Лента новостей 📰 + +Это главный экран приложения, где пользователь видит все собранные новости. + +- **Функционал:** + - **Отображение новостей:** Используйте `GET /api/parser/items` для загрузки и отображения списка новостей. Каждая новость в списке должна показывать заголовок, источник, дату публикации и краткий текст (`rawText` или `analytics.summary`, когда он будет готов). + - **Пагинация:** Так как новостей может быть много, реализуйте "бесконечную прокрутку" (подгрузка новых статей при скролле вниз) или классические кнопки пагинации («1, 2, 3...»). API полностью это поддерживает. + - **Сортировка:** По умолчанию новости должны быть отсортированы по дате публикации (`sort=publishedAt,desc`), чтобы пользователь видел сначала самое свежее. + +### 2. Панель фильтров 🔎 + +Сбоку или сверху от ленты новостей должна быть панель, позволяющая пользователю отфильтровать данные. + +- **Функционал:** + - **Фильтр по источнику:** Добавьте выпадающий список или чекбоксы с названиями источников (например, "Kursiv", "Kapital.kz"). При выборе источника отправляйте его в параметре `sourceName` в запросе к `GET /api/parser/items`. + - **Фильтр по дате:** Добавьте два поля для выбора даты ("от" и "до") с календарем. Это позволит пользователю смотреть новости за конкретный период. Используйте параметры `startDate` и `endDate`. + +### 3. Дашборд/Статистика 📊 + +Нужно создать отдельную страницу "Дашборд" или "Статистика", где будет отображаться общая информация о системе. + +- **Функционал:** + - **Общее количество новостей:** Используйте `GET /api/parser/stats` для получения и отображения общего числа статей в базе (`totalItems`). + - **Разбивка по источникам:** На основе поля `sourceStatistics` постройте круговую диаграмму (pie chart), которая наглядно покажет, сколько новостей пришло с каждого источника. + - **Статус системы:** Используйте `GET /api/parser/health`, чтобы показать пользователю (скорее, администратору) текущий статус сервиса и планировщика. Можно добавить зелёный/красный индикатор. + +### 4. Админ-панель: Управление парсером (опционально, для администраторов) ⚙️ + +Эта часть интерфейса может быть скрыта от обычных пользователей и доступна только администраторам. + +- **Функционал:** + - **Ручной запуск парсинга:** Добавьте кнопки "Запустить парсинг Kursiv", "Запустить парсинг Kapital" и "Запустить все". Эти кнопки будут отправлять запросы на эндпоинты `POST /api/parser/parse/{source}` и `POST /api/parser/parse/all`. + - **Мониторинг состояния БД:** Используйте `GET /api/parser/health/mongodb` для проверки статуса подключения к базе данных и выводите сообщение об успехе или ошибке. + +--- + +## API Endpoints + +### 1. Получение записей с фильтрацией и пагинацией + +```http +GET /api/parser/items?page=0&size=10&sort=publishedAt,desc&sourceName=Kursiv&startDate=2025-01-01&endDate=2025-01-31 +``` + +**Параметры:** + +- `page` (optional, default: 0) - номер страницы +- `size` (optional, default: 20) - размер страницы +- `sort` (optional, default: "publishedAt,desc") - сортировка +- `sourceName` (optional) - фильтр по источнику +- `startDate` (optional) - начальная дата (ISO format) +- `endDate` (optional) - конечная дата (ISO format) + +**Ответ:** + +```json +{ + "success": true, + "message": "Записи получены успешно", + "data": { + "content": [ + { + "id": "...", + "sourceName": "Kursiv (Бизнес/экономика)", + "title": "Заголовок новости", + "url": "https://kursiv.media/...", + "publishedAt": "2025-01-14T10:30:00", + "addedAt": "2025-01-14T12:00:00", + "rawText": "Текст статьи...", + "hash": "...", + "category": "Бизнес", + "analytics": { + "summary": null, + "sentiment": null, + "tags": [], + "entities": {} + } + } + ], + "pageable": { + "sort": { + "sorted": true, + "unsorted": false, + "empty": false + }, + "pageNumber": 0, + "pageSize": 10, + "offset": 0, + "paged": true, + "unpaged": false + }, + "totalElements": 25, + "totalPages": 3, + "last": false, + "first": true, + "numberOfElements": 10, + "size": 10, + "number": 0, + "sort": { + "sorted": true, + "unsorted": false, + "empty": false + }, + "empty": false + } +} +``` + +### 2. Статистика системы + +```http +GET /api/parser/stats +``` + +**Ответ:** + +```json +{ + "success": true, + "message": "Статистика получена успешно", + "data": { + "totalItems": 150, + "sourceStatistics": { + "Kursiv (Бизнес/экономика)": 75, + "Kapital.kz (Бизнес)": 75 + }, + "lastUpdate": "2025-01-14T12:00:00" + } +} +``` + +### 3. Запуск парсинга + +#### Парсинг всех источников + +```http +POST /api/parser/parse/all +``` + +**Ответ:** + +```json +{ + "success": true, + "message": "Парсинг всех источников завершен успешно", + "data": [ + { + "source": "Kursiv", + "processedItems": 15, + "totalItemsInDb": 90, + "status": "completed" + }, + { + "source": "Kapital", + "processedItems": 12, + "totalItemsInDb": 90, + "status": "completed" + } + ] +} +``` + +#### Парсинг конкретного источника + +```http +POST /api/parser/parse/kursiv +POST /api/parser/parse/kapital +``` + +### 4. Проверка состояния + +#### Общее состояние + +```http +GET /api/parser/health +``` + +**Ответ:** + +```json +{ + "success": true, + "message": "Сервис работает", + "data": { + "status": "UP", + "service": "RSS Parser System", + "timestamp": 1705123456789, + "scheduler": "Enabled - runs every 30 minutes" + } +} +``` + +#### Состояние MongoDB + +```http +GET /api/parser/health/mongodb +``` + +**Ответ при успехе:** + +```json +{ + "success": true, + "message": "MongoDB подключение успешно", + "data": { + "status": "UP", + "message": "MongoDB подключение успешно", + "timestamp": 1705123456789 + } +} +``` + +**Ответ при ошибке:** + +```json +{ + "success": false, + "message": "Ошибка подключения к MongoDB", + "data": { + "status": "DOWN", + "message": "Ошибка подключения к MongoDB: Command failed with error 13 (Unauthorized)", + "timestamp": 1705123456789, + "suggestion": "Проверьте учетные данные в application.properties" + } +} +``` + +## Примеры использования + +### 1. Получение последних новостей с пагинацией + +```javascript +// Получить первую страницу с 10 записями, отсортированными по дате публикации +fetch('/api/parser/items?page=0&size=10&sort=publishedAt,desc') + .then((response) => response.json()) + .then((data) => { + if (data.success) { + const items = data.data.content; + const totalPages = data.data.totalPages; + // Обработка данных + } + }); +``` + +### 2. Фильтрация по источнику + +```javascript +// Получить только новости от Kursiv +fetch('/api/parser/items?sourceName=Kursiv (Бизнес/экономика)&page=0&size=20') + .then((response) => response.json()) + .then((data) => { + if (data.success) { + const kursivNews = data.data.content; + // Обработка данных + } + }); +``` + +### 3. Фильтрация по датам + +```javascript +// Получить новости за последнюю неделю +const endDate = new Date().toISOString(); +const startDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(); + +fetch(`/api/parser/items?startDate=${startDate}&endDate=${endDate}`) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + const recentNews = data.data.content; + // Обработка данных + } + }); +``` + +### 4. Комплексная фильтрация + +```javascript +// Получить новости Kapital за последний месяц, отсортированные по дате +const endDate = new Date().toISOString(); +const startDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(); + +fetch(`/api/parser/items?sourceName=Kapital.kz (Бизнес)&startDate=${startDate}&endDate=${endDate}&sort=publishedAt,desc&page=0&size=50`) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + const kapitalNews = data.data.content; + const totalCount = data.data.totalElements; + // Обработка данных + } + }); +``` + +### 5. Получение статистики + +```javascript +fetch('/api/parser/stats') + .then((response) => response.json()) + .then((data) => { + if (data.success) { + const stats = data.data; + console.log(`Всего новостей: ${stats.totalItems}`); + console.log('По источникам:', stats.sourceStatistics); + } + }); +``` + +## Поддерживаемые форматы дат + +API поддерживает следующие форматы дат: + +- `yyyy-MM-dd` (например: 2025-01-14) +- `yyyy-MM-dd'T'HH:mm:ss` (например: 2025-01-14T10:30:00) +- `yyyy-MM-dd'T'HH:mm:ss.SSS` (например: 2025-01-14T10:30:00.000) +- `yyyy-MM-dd HH:mm:ss` (например: 2025-01-14 10:30:00) + +## Обработка ошибок + +Все ответы API следуют единому формату: + +```json +{ + "success": false, + "message": "Описание ошибки", + "error": "Детальная информация об ошибке" +} +``` + +HTTP статус коды: + +- `200` - Успешный запрос +- `500` - Внутренняя ошибка сервера +- `503` - Сервис недоступен (например, MongoDB) + +## Рекомендации для фронтенда + +1. **Кэширование**: Используйте кэширование для статистики и часто запрашиваемых данных +2. **Пагинация**: Реализуйте бесконечную прокрутку или традиционную пагинацию +3. **Фильтры**: Предоставьте пользователю удобные фильтры по источнику и датам +4. **Обновление**: Используйте WebSocket или polling для обновления данных в реальном времени +5. **Обработка ошибок**: Всегда проверяйте поле `success` в ответе diff --git a/docs/auth-api.md b/docs/auth-api.md index bdc4844..e2ce817 100644 --- a/docs/auth-api.md +++ b/docs/auth-api.md @@ -14,7 +14,7 @@ JWT-аутентификация. Публичные и админские эн ### Базовый URL - Прод: укажите боевой домен/порт -- Dev локально: `http://localhost:8080` +- Dev локально: `https://api.konturai.kz` ### Переменные окружения (frontend) diff --git a/src/components/news/NewsFilters.vue b/src/components/news/NewsFilters.vue new file mode 100644 index 0000000..cd381d9 --- /dev/null +++ b/src/components/news/NewsFilters.vue @@ -0,0 +1,220 @@ + + + + + diff --git a/src/config/api.js b/src/config/api.js new file mode 100644 index 0000000..4ba7235 --- /dev/null +++ b/src/config/api.js @@ -0,0 +1,36 @@ +// API Configuration +export const API_CONFIG = { + BASE_URL: 'https://api.konturai.kz', + ENDPOINTS: { + // MarketItemController - Получение данных + NEWS_ITEMS: '/api/parser/items', + NEWS_ITEM_BY_ID: '/api/parser/items', + STATS: '/api/parser/items/stats', + SOURCES: '/api/parser/items/sources', + NEWS_BY_SOURCE: '/api/parser/items/source', + + // ParserAdminController - Управление парсерами + PARSERS: '/api/parser/admin/parsers', + PARSERS_INFO: '/api/parser/admin/parsers/info', + PARSER_EXISTS: '/api/parser/admin/parsers/exists', + PARSE_ALL: '/api/parser/admin/parsers/parse/all', + PARSE_SOURCE: '/api/parser/admin/parsers/parse', + + // HealthCheckController - Мониторинг системы + HEALTH: '/api/parser/health', + MONGO_HEALTH: '/api/parser/health/mongodb', + SCHEDULER_HEALTH: '/api/parser/health/scheduler', + SYSTEM_HEALTH: '/api/parser/health/system', + PARSERS_HEALTH: '/api/parser/health/parsers' + } +}; + +// Default request configuration +export const DEFAULT_REQUEST_CONFIG = { + headers: { + 'Content-Type': 'application/json' + } +}; + +// Request timeout (in milliseconds) +export const REQUEST_TIMEOUT = 30000; diff --git a/src/layout/AppMenu.vue b/src/layout/AppMenu.vue index 1a8af0d..7e55bc3 100644 --- a/src/layout/AppMenu.vue +++ b/src/layout/AppMenu.vue @@ -11,9 +11,19 @@ const model = ref([ { label: 'New Dashboard', icon: 'pi pi-fw pi-home', to: '/dashboard-new' } ] }, + { + label: 'Новости', + items: [ + { label: 'Лента новостей', icon: 'pi pi-align-left', to: '/news' }, + { label: 'Статистика', icon: 'pi pi-fw pi-chart-bar', to: '/news/dashboard' } + ] + }, { label: 'Admin', - items: [{ label: 'Users', icon: 'pi pi-fw pi-users', to: '/admin/users' }] + items: [ + { label: 'Users', icon: 'pi pi-fw pi-users', to: '/admin/users' }, + { label: 'Управление парсером', icon: 'pi pi-fw pi-cog', to: '/admin/news' } + ] } // { // label: 'UI Components', diff --git a/src/router/index.js b/src/router/index.js index 881b3c3..883b301 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -131,6 +131,24 @@ const router = createRouter({ name: 'documentation', component: () => import('@/views/pages/Documentation.vue'), meta: { requiresAuth: true } + }, + { + path: '/news', + name: 'news', + component: () => import('@/views/news/NewsList.vue'), + meta: { requiresAuth: true } + }, + { + path: '/news/dashboard', + name: 'news-dashboard', + component: () => import('@/views/news/NewsDashboard.vue'), + meta: { requiresAuth: true } + }, + { + path: '/news/:id', + name: 'news-detail', + component: () => import('@/views/news/NewsDetail.vue'), + meta: { requiresAuth: true } } ] }, @@ -143,6 +161,12 @@ const router = createRouter({ name: 'admin-users', component: () => import('@/views/pages/AdminUsers.vue'), meta: { requiresAuth: true, requiresAdmin: true } + }, + { + path: 'news', + name: 'admin-news', + component: () => import('@/views/news/NewsAdmin.vue'), + meta: { requiresAuth: true, requiresAdmin: true } } ] }, diff --git a/src/service/AuthService.js b/src/service/AuthService.js index 816bdb9..4969cfc 100644 --- a/src/service/AuthService.js +++ b/src/service/AuthService.js @@ -1,6 +1,7 @@ // Lightweight auth service to manage tokens and API calls -const API_URL = import.meta.env.VITE_API_URL || 'https://api.konturai.kz'; +// Use proxy in development, direct API in production +const API_URL = import.meta.env.VITE_API_URL || (import.meta.env.DEV ? '' : 'https://api.konturai.kz'); const TOKEN_STORAGE_KEY = 'auth.accessToken'; const REFRESH_STORAGE_KEY = 'auth.refreshToken'; @@ -65,6 +66,15 @@ async function logout() { clearTokens(); } +async function readErrorMessage(response) { + try { + const data = await response.json(); + return data.message || `HTTP ${response.status}: ${response.statusText}`; + } catch (_) { + return `HTTP ${response.status}: ${response.statusText}`; + } +} + async function authFetch(input, init = {}) { const headers = new Headers(init.headers || {}); const token = getAccessToken(); @@ -91,6 +101,7 @@ export default { refreshToken, logout, authFetch, + readErrorMessage, getAccessToken, getRefreshToken, setTokens, diff --git a/src/service/NewsService.js b/src/service/NewsService.js new file mode 100644 index 0000000..0e84f55 --- /dev/null +++ b/src/service/NewsService.js @@ -0,0 +1,325 @@ +import { API_CONFIG, DEFAULT_REQUEST_CONFIG } from '@/config/api'; +import AuthService from './AuthService'; + +const API_BASE_URL = API_CONFIG.BASE_URL; + +class NewsService { + // ===== MarketItemController - Получение данных ===== + + async getNewsItems(params = {}) { + const { page = 0, size = 20, sort = 'publishedAt,desc', sourceName = null, startDate = null, endDate = null } = params; + + const queryParams = new URLSearchParams({ + page: page.toString(), + size: size.toString(), + sort + }); + + if (sourceName) { + queryParams.append('sourceName', sourceName); + } + if (startDate) { + queryParams.append('startDate', startDate); + } + if (endDate) { + queryParams.append('endDate', endDate); + } + + try { + const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.NEWS_ITEMS}?${queryParams}`); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Ошибка при загрузке новостей'); + } + + return data; + } catch (error) { + console.error('Ошибка при загрузке новостей:', error); + throw error; + } + } + + async getNewsItemById(id) { + try { + const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.NEWS_ITEM_BY_ID}/${id}`); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Ошибка при загрузке новости'); + } + + return data; + } catch (error) { + console.error('Ошибка при загрузке новости:', error); + throw error; + } + } + + async getNewsBySource(sourceName, params = {}) { + const { page = 0, size = 20, sort = 'publishedAt,desc' } = params; + + const queryParams = new URLSearchParams({ + page: page.toString(), + size: size.toString(), + sort + }); + + try { + const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.NEWS_BY_SOURCE}/${encodeURIComponent(sourceName)}?${queryParams}`); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Ошибка при загрузке новостей источника'); + } + + return data; + } catch (error) { + console.error('Ошибка при загрузке новостей источника:', error); + throw error; + } + } + + async getStats() { + try { + const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.STATS}`); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Ошибка при загрузке статистики'); + } + + return data; + } catch (error) { + console.error('Ошибка при загрузке статистики:', error); + throw error; + } + } + + async getSources() { + try { + const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.SOURCES}`); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Ошибка при загрузке списка источников'); + } + + return data; + } catch (error) { + console.error('Ошибка при загрузке списка источников:', error); + throw error; + } + } + + // ===== ParserAdminController - Управление парсерами ===== + + async getParsers() { + try { + const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.PARSERS}`); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Ошибка при получении списка парсеров'); + } + + return data; + } catch (error) { + console.error('Ошибка при получении списка парсеров:', error); + throw error; + } + } + + async getParsersInfo() { + try { + const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.PARSERS_INFO}`); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Ошибка при получении информации о парсерах'); + } + + return data; + } catch (error) { + console.error('Ошибка при получении информации о парсерах:', error); + throw error; + } + } + + async checkParserExists(sourceName) { + try { + const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.PARSER_EXISTS}/${sourceName}`); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Ошибка при проверке существования парсера'); + } + + return data; + } catch (error) { + console.error('Ошибка при проверке существования парсера:', error); + throw error; + } + } + + // ===== HealthCheckController - Мониторинг системы ===== + + async getHealth() { + try { + const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.HEALTH}`); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Ошибка при проверке состояния сервиса'); + } + + return data; + } catch (error) { + console.error('Ошибка при проверке состояния сервиса:', error); + throw error; + } + } + + async getMongoHealth() { + try { + const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.MONGO_HEALTH}`); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Ошибка при проверке состояния MongoDB'); + } + + return data; + } catch (error) { + console.error('Ошибка при проверке состояния MongoDB:', error); + throw error; + } + } + + async getSchedulerHealth() { + try { + const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.SCHEDULER_HEALTH}`); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Ошибка при проверке состояния планировщика'); + } + + return data; + } catch (error) { + console.error('Ошибка при проверке состояния планировщика:', error); + throw error; + } + } + + async getSystemHealth() { + try { + const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.SYSTEM_HEALTH}`); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Ошибка при получении информации о системе'); + } + + return data; + } catch (error) { + console.error('Ошибка при получении информации о системе:', error); + throw error; + } + } + + async getParsersHealth() { + try { + const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.PARSERS_HEALTH}`); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Ошибка при проверке состояния парсеров'); + } + + return data; + } catch (error) { + console.error('Ошибка при проверке состояния парсеров:', error); + throw error; + } + } + + // ===== Административные методы парсинга ===== + + async parseAll() { + try { + const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.PARSE_ALL}`, { + method: 'POST', + ...DEFAULT_REQUEST_CONFIG + }); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Ошибка при запуске парсинга всех источников'); + } + + return data; + } catch (error) { + console.error('Ошибка при запуске парсинга всех источников:', error); + throw error; + } + } + + async parseSource(source) { + try { + const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.PARSE_SOURCE}/${source}`, { + method: 'POST', + ...DEFAULT_REQUEST_CONFIG + }); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || `Ошибка при запуске парсинга источника ${source}`); + } + + return data; + } catch (error) { + console.error(`Ошибка при запуске парсинга источника ${source}:`, error); + throw error; + } + } + + // ===== Вспомогательные методы ===== + + // Методы для форматирования дат + formatDate(dateString) { + if (!dateString) return ''; + const date = new Date(dateString); + return date.toLocaleDateString('ru-RU', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + } + + formatDateShort(dateString) { + if (!dateString) return ''; + const date = new Date(dateString); + return date.toLocaleDateString('ru-RU', { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + } + + // Метод для получения списка источников из статистики (fallback) + async getSourcesFromStats() { + try { + const stats = await this.getStats(); + return Object.keys(stats.data.sourceStatistics || {}); + } catch (error) { + console.error('Ошибка при получении списка источников из статистики:', error); + return []; + } + } +} + +export default new NewsService(); diff --git a/src/views/news/NewsAdmin.vue b/src/views/news/NewsAdmin.vue new file mode 100644 index 0000000..16364e9 --- /dev/null +++ b/src/views/news/NewsAdmin.vue @@ -0,0 +1,498 @@ + + + + + diff --git a/src/views/news/NewsDashboard.vue b/src/views/news/NewsDashboard.vue new file mode 100644 index 0000000..90a3158 --- /dev/null +++ b/src/views/news/NewsDashboard.vue @@ -0,0 +1,366 @@ + + + + + diff --git a/src/views/news/NewsDetail.vue b/src/views/news/NewsDetail.vue new file mode 100644 index 0000000..1d99063 --- /dev/null +++ b/src/views/news/NewsDetail.vue @@ -0,0 +1,327 @@ + + + + + diff --git a/src/views/news/NewsList.vue b/src/views/news/NewsList.vue new file mode 100644 index 0000000..37d83dd --- /dev/null +++ b/src/views/news/NewsList.vue @@ -0,0 +1,310 @@ + + + + + diff --git a/vite.config.mjs b/vite.config.mjs index dbb18f9..c75a3a7 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -20,5 +20,25 @@ export default defineConfig({ alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } + }, + server: { + proxy: { + '/api': { + target: 'https://api.konturai.kz', + changeOrigin: true, + secure: true, + configure: (proxy, _options) => { + proxy.on('error', (err, _req, _res) => { + console.log('proxy error', err); + }); + proxy.on('proxyReq', (proxyReq, req, _res) => { + console.log('Sending Request to the Target:', req.method, req.url); + }); + proxy.on('proxyRes', (proxyRes, req, _res) => { + console.log('Received Response from the Target:', proxyRes.statusCode, req.url); + }); + } + } + } } });