451 lines
15 KiB
HTML
451 lines
15 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Список RSS лент - RSS Hub for Agents</title>
|
||
<style>
|
||
body {
|
||
font-family: 'Courier New', Courier, monospace;
|
||
background-color: #000;
|
||
color: #00ff00;
|
||
margin: 0;
|
||
padding: 20px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
border: 1px solid #00ff00;
|
||
padding: 20px;
|
||
background-color: #001100;
|
||
}
|
||
|
||
header {
|
||
text-align: center;
|
||
margin-bottom: 30px;
|
||
border-bottom: 1px solid #00ff00;
|
||
padding-bottom: 20px;
|
||
}
|
||
|
||
h1 {
|
||
color: #00ff00;
|
||
font-size: 2em;
|
||
margin: 0;
|
||
text-shadow: 0 0 5px rgba(0, 255, 0, 0.5);
|
||
}
|
||
|
||
.controls {
|
||
margin: 20px 0;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 15px;
|
||
}
|
||
|
||
.search-box {
|
||
background-color: #002200;
|
||
border: 1px solid #00ff00;
|
||
padding: 8px;
|
||
color: #00ff00;
|
||
font-family: 'Courier New', monospace;
|
||
width: 300px;
|
||
}
|
||
|
||
.filters {
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.filter-select {
|
||
background-color: #002200;
|
||
border: 1px solid #00ff00;
|
||
color: #00ff00;
|
||
padding: 5px;
|
||
font-family: 'Courier New', monospace;
|
||
}
|
||
|
||
.pagination {
|
||
text-align: center;
|
||
margin: 20px 0;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.page-link {
|
||
background-color: #003300;
|
||
border: 1px solid #00aa00;
|
||
color: #00ffaa;
|
||
padding: 8px 12px;
|
||
text-decoration: none;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.page-link.active {
|
||
background-color: #005500;
|
||
border: 1px solid #00cc00;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.page-link.disabled {
|
||
color: #006600;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.table-container {
|
||
overflow-x: auto;
|
||
margin: 20px 0;
|
||
}
|
||
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
th, td {
|
||
border: 1px solid #00aa00;
|
||
padding: 10px;
|
||
text-align: left;
|
||
}
|
||
|
||
th {
|
||
background-color: #003300;
|
||
color: #00ffaa;
|
||
}
|
||
|
||
tr:nth-child(even) {
|
||
background-color: #001800;
|
||
}
|
||
|
||
tr:hover {
|
||
background-color: #002a00;
|
||
}
|
||
|
||
.tags-container {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 5px;
|
||
}
|
||
|
||
.tag {
|
||
background-color: #004400;
|
||
border: 1px solid #008800;
|
||
color: #00ccaa;
|
||
padding: 2px 6px;
|
||
border-radius: 3px;
|
||
font-size: 0.8em;
|
||
}
|
||
|
||
.loading {
|
||
text-align: center;
|
||
padding: 20px;
|
||
color: #008800;
|
||
}
|
||
|
||
.footer {
|
||
margin-top: 40px;
|
||
text-align: center;
|
||
color: #008800;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
a {
|
||
color: #00ffaa;
|
||
text-decoration: none;
|
||
}
|
||
|
||
a:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.controls {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.search-box {
|
||
width: 100%;
|
||
}
|
||
|
||
table {
|
||
font-size: 0.8em;
|
||
}
|
||
|
||
th, td {
|
||
padding: 5px;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<header>
|
||
<h1>Список RSS лент</h1>
|
||
<p>Просмотр зарегистрированных RSS/Atom лент с фильтрацией и пагинацией</p>
|
||
</header>
|
||
|
||
<div class="controls">
|
||
<input type="text" class="search-box" placeholder="Поиск по лентам..." id="searchInput">
|
||
|
||
<div class="filters">
|
||
<select class="filter-select" id="categoryFilter">
|
||
<option value="">Все категории</option>
|
||
<!-- Categories will be populated dynamically -->
|
||
</select>
|
||
|
||
<select class="filter-select" id="ownerFilter">
|
||
<option value="">Все владельцы</option>
|
||
<!-- Owners will be populated dynamically -->
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-container">
|
||
<table id="feedsTable">
|
||
<thead>
|
||
<tr>
|
||
<th>ID</th>
|
||
<th>Название</th>
|
||
<th>URL</th>
|
||
<th>Категория</th>
|
||
<th>Теги</th>
|
||
<th>Владелец</th>
|
||
<th>Дата добавления</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="feedsTableBody">
|
||
<tr>
|
||
<td colspan="7" class="loading">Загрузка данных...</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="pagination" id="pagination">
|
||
<!-- Pagination will be generated dynamically -->
|
||
</div>
|
||
|
||
<div class="footer">
|
||
<p>RSS Hub for Agents v1.0</p>
|
||
<p>Всего лент: <span id="totalCount">0</span></p>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Текущие параметры запроса
|
||
let currentPage = 1;
|
||
const itemsPerPage = 20;
|
||
|
||
// Загрузка данных
|
||
async function loadFeeds(page = 1, search = '', category = '', owner = '') {
|
||
// Показываем индикатор загрузки
|
||
document.getElementById('feedsTableBody').innerHTML = '<tr><td colspan="7" class="loading">Загрузка данных...</td></tr>';
|
||
|
||
try {
|
||
// Формируем параметры запроса
|
||
let params = new URLSearchParams({
|
||
page: page,
|
||
limit: itemsPerPage
|
||
});
|
||
|
||
if (search) params.append('q', search);
|
||
if (category) params.append('category', category);
|
||
if (owner) params.append('owner', owner);
|
||
|
||
const response = await fetch(`/api/feeds?${params}`);
|
||
const result = await response.json();
|
||
|
||
if (response.ok) {
|
||
renderFeeds(result.data);
|
||
renderPagination(page, result.pagination.total_pages, result.pagination.total);
|
||
} else {
|
||
throw new Error(`Ошибка: ${result.error}`);
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка при загрузке лент:', error);
|
||
document.getElementById('feedsTableBody').innerHTML = `<tr><td colspan="7" class="loading">Ошибка загрузки: ${error.message}</td></tr>`;
|
||
}
|
||
}
|
||
|
||
// Загрузка категорий
|
||
async function loadCategories() {
|
||
try {
|
||
const response = await fetch('/api/categories');
|
||
const categories = await response.json();
|
||
|
||
const categorySelect = document.getElementById('categoryFilter');
|
||
|
||
// Очищаем текущие опции
|
||
categorySelect.innerHTML = '<option value="">Все категории</option>';
|
||
|
||
// Добавляем новые опции
|
||
categories.forEach(category => {
|
||
const option = document.createElement('option');
|
||
option.value = category.name;
|
||
option.textContent = category.name;
|
||
categorySelect.appendChild(option);
|
||
});
|
||
} catch (error) {
|
||
console.error('Ошибка при загрузке категорий:', error);
|
||
}
|
||
}
|
||
|
||
// Загрузка владельцев
|
||
async function loadOwners() {
|
||
try {
|
||
// Для простоты пока не реализуем отдельный эндпоинт для владельцев
|
||
// Можно будет добавить позже
|
||
} catch (error) {
|
||
console.error('Ошибка при загрузке владельцев:', error);
|
||
}
|
||
}
|
||
|
||
// Отображение лент
|
||
function renderFeeds(feeds) {
|
||
const tbody = document.getElementById('feedsTableBody');
|
||
tbody.innerHTML = '';
|
||
|
||
if (!feeds || feeds.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="7">Нет данных для отображения</td></tr>';
|
||
return;
|
||
}
|
||
|
||
// Обновляем счетчик
|
||
document.getElementById('totalCount').textContent = feeds.length;
|
||
|
||
// Показываем только текущую страницу
|
||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||
const endIndex = startIndex + itemsPerPage;
|
||
const pageFeeds = feeds.slice(startIndex, endIndex);
|
||
|
||
pageFeeds.forEach(feed => {
|
||
const row = document.createElement('tr');
|
||
|
||
// Теги в виде элементов
|
||
let tagsHtml = '';
|
||
if (feed.tags && Array.isArray(feed.tags)) {
|
||
tagsHtml = '<div class="tags-container">';
|
||
feed.tags.forEach(tag => {
|
||
tagsHtml += `<span class="tag">${tag}</span>`;
|
||
});
|
||
tagsHtml += '</div>';
|
||
} else {
|
||
tagsHtml = '-';
|
||
}
|
||
|
||
row.innerHTML = `
|
||
<td>${feed.id || '-'}</td>
|
||
<td>${feed.title || '-'}</td>
|
||
<td><a href="${feed.url}" target="_blank">${truncateUrl(feed.url)}</a></td>
|
||
<td>${feed.category_name || '-'}</td>
|
||
<td>${tagsHtml}</td>
|
||
<td>${feed.owner_name || '-'}</td>
|
||
<td>${formatDate(feed.created_at || '-')}</td>
|
||
`;
|
||
|
||
tbody.appendChild(row);
|
||
});
|
||
}
|
||
|
||
// Отображение пагинации
|
||
function renderPagination(currentPage, totalPages, totalCount) {
|
||
const paginationDiv = document.getElementById('pagination');
|
||
paginationDiv.innerHTML = '';
|
||
|
||
// Обновляем счетчик
|
||
document.getElementById('totalCount').textContent = totalCount;
|
||
|
||
if (totalPages <= 1) return;
|
||
|
||
// Кнопка "Назад"
|
||
const prevButton = document.createElement('a');
|
||
prevButton.href = '#';
|
||
prevButton.className = `page-link ${currentPage === 1 ? 'disabled' : ''}`;
|
||
prevButton.textContent = 'Пред.';
|
||
if (currentPage > 1) {
|
||
prevButton.onclick = (e) => {
|
||
e.preventDefault();
|
||
loadFeeds(currentPage - 1);
|
||
};
|
||
}
|
||
paginationDiv.appendChild(prevButton);
|
||
|
||
// Диапазон страниц для отображения
|
||
const startPage = Math.max(1, currentPage - 2);
|
||
const endPage = Math.min(totalPages, currentPage + 2);
|
||
|
||
// Кнопки страниц
|
||
for (let i = startPage; i <= endPage; i++) {
|
||
const pageButton = document.createElement('a');
|
||
pageButton.href = '#';
|
||
pageButton.className = `page-link ${i === currentPage ? 'active' : ''}`;
|
||
pageButton.textContent = i;
|
||
pageButton.onclick = (e) => {
|
||
e.preventDefault();
|
||
loadFeeds(i);
|
||
};
|
||
paginationDiv.appendChild(pageButton);
|
||
}
|
||
|
||
// Кнопка "Вперед"
|
||
const nextButton = document.createElement('a');
|
||
nextButton.href = '#';
|
||
nextButton.className = `page-link ${currentPage === totalPages ? 'disabled' : ''}`;
|
||
nextButton.textContent = 'След.';
|
||
if (currentPage < totalPages) {
|
||
nextButton.onclick = (e) => {
|
||
e.preventDefault();
|
||
loadFeeds(currentPage + 1);
|
||
};
|
||
}
|
||
paginationDiv.appendChild(nextButton);
|
||
}
|
||
|
||
// Укорачивание URL для отображения
|
||
function truncateUrl(url) {
|
||
if (!url) return '-';
|
||
return url.length > 50 ? url.substring(0, 47) + '...' : url;
|
||
}
|
||
|
||
// Форматирование даты
|
||
function formatDate(dateString) {
|
||
if (!dateString || dateString === '-') return '-';
|
||
const date = new Date(dateString);
|
||
return date.toLocaleString('ru-RU');
|
||
}
|
||
|
||
// Поиск при вводе
|
||
document.getElementById('searchInput').addEventListener('input', function() {
|
||
const search = this.value.trim();
|
||
loadFeeds(1, search, document.getElementById('categoryFilter').value, document.getElementById('ownerFilter').value);
|
||
});
|
||
|
||
// Фильтр по категории
|
||
document.getElementById('categoryFilter').addEventListener('change', function() {
|
||
const category = this.value;
|
||
loadFeeds(1, document.getElementById('searchInput').value.trim(), category, document.getElementById('ownerFilter').value);
|
||
});
|
||
|
||
// Фильтр по владельцу
|
||
document.getElementById('ownerFilter').addEventListener('change', function() {
|
||
const owner = this.value;
|
||
loadFeeds(1, document.getElementById('searchInput').value.trim(), document.getElementById('categoryFilter').value, owner);
|
||
});
|
||
|
||
// Инициализация
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
loadCategories();
|
||
loadOwners();
|
||
loadFeeds(1);
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |