rss_agent_hub/templates/feeds.html

451 lines
15 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>