Files
UrlNav/Rss.php
2026-02-23 20:15:55 +08:00

5121 lines
179 KiB
PHP
Raw Permalink 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.
<?php
if (!defined('__TYPECHO_ROOT_DIR__')) exit;
// 包含必要的文件
include 'header.php';
include 'menu.php';
// 获取插件选项
$options = Typecho_Widget::widget('Widget_Options');
$pluginConfig = UrlNav_Plugin::getConfig();
// 获取参数
$currentPage = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
$categoryId = isset($_GET['category']) ? $_GET['category'] : 'all';
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
$showFavorites = isset($_GET['favorites']) && $_GET['favorites'] == '1';
// 修复这里:转换旧值到新值
$starRating = isset($_GET['star']) ? $_GET['star'] : 'all';
// 转换旧值到新值
if ($starRating === 'none') {
$starRating = '0';
} elseif ($starRating === 'has') {
$starRating = 'starred';
}
// 获取每页显示数量
$pageSize = isset($pluginConfig->rssPageSize) ? max(1, intval($pluginConfig->rssPageSize)) : 20;
// 获取RSS信息支持分页和筛选
if ($showFavorites) {
// 获取收藏列表
$feedsData = UrlNav_Plugin::getFavorites(0, $currentPage, $pageSize, $search);
if ($feedsData['success']) {
$feeds = $feedsData['data'];
$totalFeeds = $feedsData['total'];
$totalPages = $feedsData['totalPages'];
} else {
$feeds = [];
$totalFeeds = 0;
$totalPages = 0;
}
} else {
// 获取正常RSS列表
$feedsData = UrlNav_Plugin::getAllRssFeeds($categoryId, $currentPage, $pageSize, $search, $starRating);
$feeds = $feedsData['data'];
$totalFeeds = $feedsData['total'];
$totalPages = $feedsData['totalPages'];
}
// 获取有RSS的网址数量
$rssUrlCount = UrlNav_Plugin::getTotalUrlsWithRss();
// 获取所有分类
$categories = UrlNav_Plugin::getAllCategories();
// 获取所有星级假设有0-3星级0表示无星级
$allStarRatings = [
['value' => 'all', 'label' => '全部星级'],
['value' => 'starred', 'label' => '有星级'], // 删除 'has',改为 'starred'
['value' => '0', 'label' => '无星级'], // 删除 'none',改为 '0'
['value' => '1', 'label' => '⭐'],
['value' => '2', 'label' => '⭐⭐ '],
['value' => '3', 'label' => '⭐⭐⭐']
];
// 获取缓存统计信息
$cacheStats = UrlNav_Plugin::getCacheStats();
// 获取RSS定时任务统计
$rssCronStats = UrlNav_Plugin::getRssCronStats();
// 获取收藏统计
$favoriteStats = UrlNav_Plugin::getFavoriteStats(0);
// 检查RSS定时任务锁
$rssLockFile = __DIR__ . '/db/rss_cron.lock';
$isRssLocked = false;
$rssLockTime = '';
$rssLockDuration = 0;
if (file_exists($rssLockFile)) {
$rssLockTime = @filemtime($rssLockFile);
if ($rssLockTime) {
$isRssLocked = true;
$rssLockDuration = time() - $rssLockTime;
}
}
// 转换为北京时间函数(修复版)
function convertToBeijingTime($datetime) {
if (empty($datetime) || $datetime == '0000-00-00 00:00:00' || $datetime == '0000-00-00 00:00') {
return '';
}
try {
// 创建DateTime对象
$date = new DateTime($datetime);
// 如果已经是北京时间,直接返回
if ($date->getTimezone()->getName() == 'Asia/Shanghai') {
return $date->format('Y-m-d H:i:s');
}
// 转换为北京时间
$date->setTimezone(new DateTimeZone('Asia/Shanghai'));
return $date->format('Y-m-d H:i:s');
} catch (Exception $e) {
// 如果解析失败,尝试简单的时间转换
$timestamp = strtotime($datetime);
if ($timestamp !== false) {
// 服务器时间已经是北京时间,直接返回
return date('Y-m-d H:i:s', $timestamp);
}
return $datetime;
}
}
// 格式化时间为易读格式(北京时间)
function formatTimeAgo($datetime) {
if (empty($datetime)) {
return '';
}
// 转换为北京时间的时间戳
$date = new DateTime($datetime);
$date->setTimezone(new DateTimeZone('Asia/Shanghai'));
$beijingTimestamp = $date->getTimestamp();
$currentTimestamp = time();
$diff = $currentTimestamp - $beijingTimestamp;
if ($diff < 60) {
return '刚刚';
} elseif ($diff < 3600) {
$minutes = floor($diff / 60);
return $minutes . '分钟前';
} elseif ($diff < 86400) {
$hours = floor($diff / 3600);
return $hours . '小时前';
} elseif ($diff < 2592000) {
$days = floor($diff / 86400);
return $days . '天前';
} elseif ($diff < 31536000) {
$months = floor($diff / 2592000);
return $months . '个月前';
} else {
$years = floor($diff / 31536000);
return $years . '年前';
}
}
// 获取北京时间显示
function formatBeijingDateTime($datetime, $format = 'Y-m-d H:i:s') {
if (empty($datetime)) {
return '';
}
$date = new DateTime($datetime);
$date->setTimezone(new DateTimeZone('Asia/Shanghai'));
return $date->format($format);
}
// 简化时间显示
function formatSimpleTime($datetime) {
return formatBeijingDateTime($datetime, 'm-d H:i');
}
// 格式化统计时间(北京时间)
function formatStatsTime($datetime) {
if (empty($datetime)) {
return '从未更新';
}
$date = new DateTime($datetime);
$date->setTimezone(new DateTimeZone('Asia/Shanghai'));
return $date->format('Y-m-d H:i');
}
// 获取RSS定时任务URL
function getRssCronUrl() {
global $pluginConfig, $options;
$baseUrl = $options->siteUrl;
if (substr($baseUrl, -1) == '/') {
$baseUrl = substr($baseUrl, 0, -1);
}
return $baseUrl . '/urlnav-rss-cron?secret=' . urlencode($pluginConfig->rssCronSecret ?? '');
}
// 文章内容摘要处理
function getArticleExcerpt($content, $length = 180) {
if (empty($content)) {
return '';
}
// 移除HTML标签
$text = strip_tags($content);
// 移除多余的空格和换行
$text = preg_replace('/\s+/', ' ', $text);
$text = trim($text);
// 截取指定长度
if (mb_strlen($text, 'UTF-8') > $length) {
$text = mb_substr($text, 0, $length, 'UTF-8') . '...';
}
return $text;
}
// 检查文章是否有完整内容
function hasFullContent($feed) {
return !empty($feed['full_content']) && trim($feed['full_content']) !== '';
}
?>
<style>
.filter-group a:hover{text-decoration:none!important;}
.typecho-page-main{padding:10px!important;}
.container{padding:0px!important;width:1200!important;}
/* 星级样式 */
.star-link {
text-decoration: none !important;
transition: all 0.2s ease;
}
.star-link:hover {
color: #ff9800 !important;
transform: scale(1.1);
}
.no-star-link {
text-decoration: none !important;
transition: all 0.2s ease;
}
.no-star-link:hover {
color: #495057 !important;
text-decoration: underline !important;
}
.star-rating-section {
display: flex;
align-items: center;
gap: 3px;
}
.star-filter-option {
display: flex;
align-items: center;
gap: 5px;
}
/* 状态相关样式 - 优化版 */
.feed-status-cell {
text-align: center;
padding: 10px 5px !important;
width: 30px;
cursor: pointer;
padding-left:20px!important;
}
.status-selector {
position: relative;
display: inline-block;
cursor: pointer;
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.status-selector:hover {
background-color: #f8f9fa;
transform: scale(1.1);
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
transition: all 0.2s ease;
position: relative;
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.8);
border: 1px solid rgba(0, 0, 0, 0.1);
}
.status-dot::after {
content: '';
position: absolute;
top: -3px;
left: -3px;
right: -3px;
bottom: -3px;
border-radius: 50%;
opacity: 0;
transition: opacity 0.2s ease;
}
.status-selector:hover .status-dot {
transform: scale(1.3);
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.9);
}
.status-selector:hover .status-dot::after {
opacity: 0.1;
}
.status-unread {
background-color: #000;
}
.status-unread::after {
background-color: #dc3545;
}
.status-read {
background-color: #28a745;
}
.status-read::after {
background-color: #28a745;
}
.status-ignore {
background-color: #c82333;
}
.status-ignore::after {
background-color: #c82333;
}
/* 状态下拉菜单 */
.status-dropdown {
display: none;
position: absolute;
top: 100%;
left: 55%;
transform: translateX(-50%);
background: white;
border: 1px solid #e8ebf0;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.12);
width: 50px;
z-index: 1000;
padding: 8px 0;
margin-top: 8px;
}
.status-selector.active .status-dropdown {
display: block;
}
.status-dropdown::before {
content: '';
position: absolute;
top: -8px;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-bottom: 8px solid white;
z-index: 1;
}
.status-dropdown::after {
content: '';
position: absolute;
top: -9px;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 9px solid transparent;
border-right: 9px solid transparent;
border-bottom: 9px solid #e8ebf0;
z-index: 0;
}
.status-option {
padding: 8px 16px;
display: flex;
align-items: center;
gap: 10px;
justify-content: center;
font-size: 13px;
color: #495057;
transition: all 0.2s;
cursor: pointer;
position: relative;
}
.status-option:hover {
background-color: #f8f9fa;
color: #467b96;
}
.status-option::before {
content: '';
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
}
.status-option[data-status="unread"]::before {
background-color: #000;
}
.status-option[data-status="read"]::before {
background-color: #28a745;
}
.status-option[data-status="ignore"]::before {
background-color: #c82333;
}
/* 行状态样式 - 只影响状态列,不影响整行 */
.rss-feeds-table tbody tr .feed-status-cell.status-read {
opacity: 0.8;
}
.rss-feeds-table tbody tr .feed-status-cell.status-read .status-dot {
background-color: #28a745;
}
.rss-feeds-table tbody tr .feed-status-cell.status-ignore {
opacity: 0.6;
}
.rss-feeds-table tbody tr .feed-status-cell.status-ignore .status-dot {
background-color: #6c757d;
}
/* 行选中状态 */
.rss-feeds-table tbody tr.selected {
background-color: #e3f2fd !important;
}
.rss-feeds-table tbody tr.selected td {
background-color: #e3f2fd !important;
}
/* 状态筛选器 - 改为点击展开的下拉菜单 */
.status-filter-dropdown {
position: relative;
display: inline-block;
}
.status-filter-btn {
padding: 8px 15px;
border: 1px solid #ced4da;
background: white;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
color: #000;
transition: all 0.2s;
display: flex;
height: 40px;
align-items: center;
gap: 8px;
min-width: 120px;
justify-content: space-between;
}
.status-filter-btn:hover {
background: #f8f9fa;
border-color: #467b96;
}
.status-filter-btn::after {
content: '▼';
font-size: 10px;
margin-left: 5px;
opacity: 0.7;
}
.status-filter-btn.active {
background: #467b96;
border-color: #467b96;
color: white;
}
.status-filter-btn.active::after {
color: white;
}
/* 状态筛选下拉菜单 - 点击展开 */
.status-filter-dropdown-content {
display: none;
position: absolute;
top: 100%;
left: 0;
background: white;
border: 1px solid #e8ebf0;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.12);
min-width: 120px;
z-index: 1000;
padding: 8px 0;
margin-top: 5px;
}
.status-filter-dropdown.active .status-filter-dropdown-content {
display: block;
}
.status-filter-option {
padding: 8px 16px;
display: flex;
align-items: center;
gap: 10px;
font-size: 13px;
color: #495057;
transition: all 0.2s;
cursor: pointer;
}
.status-filter-option:hover {
background-color: #f8f9fa;
color: #467b96;
}
.status-filter-option::before {
content: '';
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
}
.status-filter-option[data-status="all"]::before {
background-color: #467b96;
}
.status-filter-option[data-status="unread"]::before {
background-color: #dc3545;
}
.status-filter-option[data-status="read"]::before {
background-color: #28a745;
}
.status-filter-option[data-status="ignore"]::before {
background-color: #6c757d;
}
.status-filter-count {
margin-left: auto;
font-size: 11px;
color: #6c757d;
background: rgba(0,0,0,0.05);
padding: 1px 6px;
border-radius: 10px;
}
/* 批量操作下拉菜单 - 点击展开 */
.batch-dropdown {
position: relative;
display: inline-block;
}
.batch-dropdown-btn {
padding: 8px 15px;
border: 1px solid #ced4da;
background: white;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
color: #000;
height: 40px;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 8px;
min-width: 120px;
justify-content: space-between;
}
.batch-dropdown-btn:hover {
background: #f8f9fa;
border-color: #467b96;
}
.batch-dropdown-btn::after {
content: '▼';
font-size: 10px;
margin-left: 5px;
opacity: 0.7;
}
.batch-dropdown-btn.active {
background: #467b96;
border-color: #467b96;
color: white;
}
.batch-dropdown-btn.active::after {
color: white;
}
.batch-dropdown-content {
display: none;
position: absolute;
top: 100%;
left: 0;
background: white;
border: 1px solid #e8ebf0;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.12);
min-width: 120px;
z-index: 1000;
padding: 8px 0;
margin-top: 5px;
}
.batch-dropdown.active .batch-dropdown-content {
display: block;
}
.batch-option {
padding: 8px 16px;
font-size: 13px;
color: #495057;
transition: all 0.2s;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
}
.batch-option:hover {
background-color: #f8f9fa;
color: #467b96;
}
.batch-option.select-all::before {
content: '✓';
color: #467b96;
}
.batch-option.clear-selection::before {
content: '○';
color: #6c757d;
}
.batch-option.mark-read::before {
content: '●';
color: #28a745;
}
.batch-option.mark-unread::before {
content: '●';
color: #dc3545;
}
.batch-option.mark-ignore::before {
content: '●';
color: #6c757d;
}
.batch-option.export-status::before {
content: '↓';
color: #467b96;
font-size: 12px;
width: 12px;
text-align: center;
line-height: 1;
font-weight: bold;
}
.batch-option.import-status::before {
content: '↑';
color: #28a745;
font-size: 12px;
width: 12px;
text-align: center;
line-height: 1;
font-weight: bold;
}
.batch-option.clear-status::before {
content: '×';
color: #dc3545;
font-size: 14px;
width: 12px;
text-align: center;
line-height: 1;
font-weight: bold;
}
#full-content-original-title{display:none;}
#full-content-site-title{display:none;}
/* 添加保存按钮样式 */
.action-btn.save-btn {
background: #17a2b8;
border-color: #17a2b8;
color: #fff;
}
.action-btn.save-btn:hover {
background: #138496;
border-color: #138496;
color: #fff;
}
#modal-title{color:#000!important;}
/**顶部标题栏干掉***/
.typecho-page-title{display:none;}
/* RSS信息阅读样式 - 优化版 */
.rss-feeds-container {
padding: 10px;
min-height: calc(100vh - 120px);
}
.rss-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 22px 25px;
background: white;
border-radius: 10px;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
border: 1px solid #e8ebf0;
}
.rss-title {
margin: 0;
font-size: 22px;
color: #2c3e50;
font-weight: 600;
line-height: 1.3;
}
.rss-stats {
color: #6c757d;
font-size: 14px;
margin-top: 8px;
line-height: 1.5;
}
.rss-stats strong {
color: #467b96;
font-weight: 600;
}
.rss-refresh-btn {
background: #28a745;
color: #FFF;
border: none;
padding: 10px 24px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.25s ease;
box-shadow: 0 2px 4px rgba(40, 167, 69, 0.15);
}
.rss-refresh-btn:hover {
background: #218838;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(40, 167, 69, 0.2);
color: #000000;
}
.rss-refresh-btn:active {
transform: translateY(0);
box-shadow: 0 1px 3px rgba(40, 167, 69, 0.2);
}
.rss-refresh-btn:disabled {
background: #adb5bd;
cursor: not-allowed;
opacity: 0.7;
transform: none;
box-shadow: none;
color: #000000;
}
.rss-refresh-btn.manage-btn {
background: #467b96;
color: #fff;
padding: 8px 24px!important;
}
.rss-refresh-btn.manage-btn:hover {
background: #3a6a83;
box-shadow: 0 4px 8px rgba(70, 123, 150, 0.2);
color: #000000;
}
.rss-refresh-btn.clean-btn {
background: #dc3545;
color: #fff;
}
.rss-refresh-btn.clean-btn:hover {
background: #c82333;
box-shadow: 0 4px 8px rgba(220, 53, 69, 0.2);
color: #fff;
}
.rss-refresh-btn.stats-btn {
background: #6f42c1;
color: #fff;
}
.rss-refresh-btn.stats-btn:hover {
background: #5a379f;
box-shadow: 0 4px 8px rgba(111, 66, 193, 0.2);
color: #000000;
}
.refreshing-spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid rgba(0,0,0,.3);
border-radius: 50%;
border-top-color: #000000;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 筛选栏 */
.rss-filter-bar {
display: flex;
flex-wrap: wrap;
gap: 12px;
justify-content: center;
align-items: center;
margin-bottom: 20px;
padding: 20px;
background: white;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
border: 1px solid #e8ebf0;
}
.filter-group {
display: flex;
align-items: center;
gap: 10px;
}
.filter-label {
font-size: 14px;
color: #495057;
font-weight: 500;
white-space: nowrap;
}
.filter-select option{
text-align: center;
justify-content: center;
align-items: center;
}
.filter-select {
border: 1px solid #ced4da;
border-radius: 6px;
background: white;
color: #000000;
font-size: 14px;
min-width: 140px;
height: 40px;
cursor: pointer;
}
.filter-select:hover {
border-color: #adb5bd;
}
.filter-select:focus {
border-color: #467b96;
outline: none;
box-shadow: 0 0 0 3px rgba(70, 123, 150, 0.1);
}
.search-input {
padding: 8px 12px;
border: 1px solid #ced4da;
border-radius: 6px!important;
font-size: 14px;
min-width: 180px;
transition: border-color 0.2s;
height: 40px;
box-sizing: border-box;
color: #000000;
}
.search-input:focus {
border-color: #467b96;
outline: none;
box-shadow: 0 0 0 3px rgba(70, 123, 150, 0.1);
}
.search-btn {
padding: 8px 20px;
background: #467b96;
color: #fff;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
height: 40px;
box-sizing: border-box;
}
.search-btn:hover {
background: #3a6a83;
transform: translateY(-1px);
color: #000000;
}
.search-btn.active {
background: #ffc107;
color: #000;
border-color: #ffc107;
}
.search-btn.active:hover {
background: #e0a800;
color: #000;
border-color: #e0a800;
}
.clear-btn {
padding: 8px 20px;
background: #6c757d;
color: #000000;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
text-decoration: none;
height: 40px;
box-sizing: border-box;
display: inline-flex;
align-items: center;
justify-content: center;
}
.clear-btn:hover {
background: #5a6268;
transform: translateY(-1px);
color: #000000;
text-decoration: none;
}
/* 分页样式 */
.rss-pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
margin-top: 20px;
padding: 20px;
background: white;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
border: 1px solid #e8ebf0;
}
.pagination-btn {
padding: 8px 16px;
border: 1px solid #dee2e6;
background: white;
color: #467b96;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
text-decoration: none;
color: #000000;
}
.pagination-btn:hover:not(:disabled) {
background: #f8f9fa;
border-color: #467b96;
color: #000000;
text-decoration: none;
}
.pagination-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pagination-current {
padding: 8px 16px;
background: #467b96;
color: #fff;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
border: 1px solid #467b96;
}
.pagination-ellipsis {
padding: 8px 12px;
color: #6c757d;
font-size: 14px;
}
.pagination-info {
color: #6c757d;
font-size: 14px;
margin-left: auto;
}
/* 表格容器 - 确保不超出边框 */
.table-container {
width: 100%;
overflow-x: auto;
border-radius: 10px;
background: white;
box-shadow: 0 3px 12px rgba(0,0,0,0.06);
margin-top: 20px;
border: 1px solid #e8ebf0;
}
.rss-feeds-table {
width: 100%;
border-collapse: collapse;
min-width: 900px; /* 确保表格有最小宽度 */
}
.rss-feeds-table th {
background: #f8fafc;
padding: 16px 20px;
text-align: left;
font-weight: 600;
color: #2c3e50;
border-bottom: 2px solid #e8ebf0;
font-size: 14px;
white-space: nowrap;
}
.rss-feeds-table td {
padding: 18px 20px;
border-bottom: 1px solid #f0f2f5;
vertical-align: top;
font-size: 14px;
transition: background-color 0.2s;
line-height: 1.5;
}
.rss-feeds-table tr:hover td {
background-color: #f8fafc;
}
.rss-feeds-table tr:last-child td {
border-bottom: none;
}
/* 序号 */
.feed-index {
text-align: center;
color: #6c757d;
font-weight: 500;
width: 20px;
font-size: 14px;
}
/* 分类标签 */
.category-tag {
display: inline-block;
padding: 4px 10px;
background: #e9f7fe;
color: #467b96;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
max-width: 26px;
margin-top: 4px;
border: 1px solid #b8d4ff;
}
/* 网站信息 */
.site-info {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 150px;
max-width: 150px;
}
.site-title {
font-weight: 600;
color: #2c3e50;
font-size: 15px;
line-height: 1.4;
word-break: break-word;
}
.site-url {
font-size: 12px;
color: #6c757d;
word-break: break-all;
line-height: 1.4;
}
.site-url a {
color: #467b96;
text-decoration: none;
transition: color 0.2s;
}
.site-url a:hover {
color: #3a6a83;
text-decoration: underline;
}
/* 文章信息 */
.article-info {
display: flex;
flex-direction: column;
gap: 10px;
min-width: 300px;
max-width: 400px;
}
.article-title {
font-size: 15px;
line-height: 1.5;
margin: 0;
}
.article-title a {
color: #2c3e50;
text-decoration: none;
font-weight: 600;
transition: color 0.2s;
display: block;
word-break: break-word;
}
.article-title a:hover {
color: #467b96;
text-decoration: underline;
}
.article-description {
font-size: 13px;
color: #6c757d;
line-height: 1.6;
max-height: 40px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
word-break: break-word;
}
/* 时间信息 */
.time-info {
font-size: 13px;
color: #6c757d;
min-width: 140px;
max-width: 160px;
}
.pub-date {
color: #2c3e50;
font-weight: 500;
margin-bottom: 6px;
font-size: 14px;
word-break: break-word;
}
.time-ago {
color: #28a745;
font-size: 12px;
margin-bottom: 5px;
border-radius: 12px;
display: inline-block;
font-weight: 500;
}
.cache-time {
font-size: 11px;
color: #adb5bd;
margin-top: 6px;
}
.favorited-time {
font-size: 11px;
color: #ffc107;
margin-top: 6px;
font-weight: 500;
}
/* 操作列 */
.feed-actions {
display: flex;
gap: 8px;
min-width: 120px;
max-width: 150px;
flex-wrap: wrap;
}
.action-btn {
padding: 7px 14px;
border: none;
border-radius: 5px;
background: white;
color: #000000;
cursor: pointer;
font-size: 13px;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 5px;
transition: all 0.2s;
min-width: 60px;
font-weight: 500;
border: 1px solid #dee2e6;
box-sizing: border-box;
}
.action-btn:hover {
background: #f8f9fa;
text-decoration: none;
border-color: #ced4da;
transform: translateY(-1px);
color: #000000;
}
.action-btn.visit {
background: #467b96;
border-color: #467b96;
color: #fff;
}
.action-btn.visit:hover {
background: #3a6a83;
border-color: #3a6a83;
color: #fff;
}
.action-btn.copy {
background: #28a745;
border-color: #28a745;
color: #fff;
}
.action-btn.copy:hover {
background: #218838;
border-color: #218838;
color: #fff;
}
.action-btn.favorite-btn {
background: #ffc107;
border-color: #ffc107;
color: #FFF;
}
.action-btn.favorite-btn:hover {
background: #e0a800;
border-color: #e0a800;
color: #000;
}
.action-btn.favorite-btn.active {
background: #28a745;
border-color: #28a745;
color: #fff;
}
.action-btn.favorite-btn.active:hover {
background: #218838;
border-color: #218838;
color: #fff;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 70px 30px;
color: #6c757d;
background: white;
border-radius: 10px;
box-shadow: 0 3px 12px rgba(0,0,0,0.06);
margin-top: 30px;
border: 1px solid #e8ebf0;
}
.empty-state-icon {
font-size: 56px;
color: #dee2e6;
margin-bottom: 20px;
opacity: 0.7;
}
.empty-state-title {
font-size: 22px;
color: #2c3e50;
margin-bottom: 15px;
font-weight: 600;
}
.empty-state-message {
font-size: 15px;
color: #6c757d;
margin-bottom: 30px;
max-width: 500px;
margin-left: auto;
margin-right: auto;
line-height: 1.6;
}
.empty-state-actions {
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
.empty-state-btn {
padding: 11px 26px;
border: none;
border-radius: 6px;
background: #467b96;
color: #000000;
text-decoration: none;
font-size: 15px;
font-weight: 500;
cursor: pointer;
transition: all 0.25s ease;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
box-shadow: 0 2px 6px rgba(70, 123, 150, 0.2);
}
.empty-state-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(70, 123, 150, 0.25);
text-decoration: none;
color: #000000;
background: #3a6a83;
}
.empty-state-btn.secondary {
background: white;
color: #467b96;
border: 1px solid #467b96;
box-shadow: 0 2px 4px rgba(70, 123, 150, 0.1);
}
.empty-state-btn.secondary:hover {
background: #f8f9fa;
color: #3a6a83;
border-color: #3a6a83;
}
/* 消息提示 */
.rss-alert {
padding: 16px 24px;
border-radius: 8px;
margin-bottom: 20px;
display: none;
position: fixed;
top: 30px;
left: 50%;
transform: translateX(-50%);
z-index: 99999;
max-width: 500px;
width: 90%;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.12);
animation: slideDown 0.3s ease;
border: 1px solid rgba(0,0,0,0.05);
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateX(-50%) translateY(-20px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
.rss-alert.success {
background: #d4edda;
border-color: #c3e6cb;
color: #000000;
}
.rss-alert.error {
background: #f8d7da;
border-color: #f5c6cb;
color: #000000;
}
.rss-alert.info {
background: #d1ecf1;
border-color: #bee5eb;
color: #000000;
}
.rss-alert.warning {
background: #fff3cd;
border-color: #ffeaa7;
color: #000000;
}
/* 加载动画 */
#loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.92);
display: none;
align-items: center;
justify-content: center;
z-index: 99999;
backdrop-filter: blur(3px);
}
.loading-content {
text-align: center;
background: white;
padding: 40px 50px;
border-radius: 10px;
box-shadow: 0 8px 30px rgba(0,0,0,0.15);
min-width: 320px;
border-top: 3px solid #467b96;
}
.loading-spinner {
width: 50px;
height: 50px;
border: 3px solid #f3f3f3;
border-top: 3px solid #467b96;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
.loading-text {
color: #2c3e50;
font-size: 16px;
margin: 0;
font-weight: 500;
}
.loading-subtext {
color: #6c757d;
font-size: 13px;
margin-top: 8px;
}
/* 定时任务管理 */
.cron-management {
margin-top: 20px;
padding: 20px;
background: white;
border-radius: 10px;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
border: 1px solid #e8ebf0;
}
.cron-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.cron-header h3 {
margin: 0;
font-size: 18px;
color: #2c3e50;
}
.cron-actions {
display: flex;
gap: 10px;
}
.cron-info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.cron-info-box {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
border-left: 4px solid #467b96;
}
.cron-info-title {
font-size: 14px;
color: #495057;
margin-bottom: 8px;
font-weight: 500;
}
.cron-info-content {
font-size: 13px;
color: #6c757d;
}
.cron-url-box {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
font-family: monospace;
font-size: 12px;
word-break: break-all;
margin-bottom: 10px;
}
.cron-note {
font-size: 12px;
color: #6c757d;
margin-top: 10px;
}
/* 失败网址相关样式 */
.status-active {
color: #28a745;
font-weight: 500;
}
.status-inactive {
color: #6c757d;
text-decoration: line-through;
}
.failed-url-item {
background: #fff5f5;
border: 1px solid #f5c6cb;
border-radius: 6px;
padding: 12px;
margin-bottom: 10px;
}
.failed-url-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.failed-url-name {
font-weight: 500;
color: #2c3e50;
}
.failed-url-time {
font-size: 11px;
color: #6c757d;
}
.failed-url-error {
color: #dc3545;
font-size: 12px;
margin-bottom: 8px;
word-break: break-word;
}
.failed-url-actions {
display: flex;
gap: 8px;
}
/* 成功网址相关样式 */
.success-url-item {
background: #d4edda;
border: 1px solid #c3e6cb;
border-radius: 6px;
padding: 12px;
margin-bottom: 10px;
}
.success-url-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.success-url-name {
font-weight: 500;
color: #155724;
}
.success-url-time {
font-size: 11px;
color: #6c757d;
}
.success-url-stats {
color: #28a745;
font-size: 12px;
margin-bottom: 8px;
word-break: break-word;
}
.success-url-actions {
display: flex;
gap: 8px;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.rss-header {
flex-direction: column;
align-items: flex-start;
gap: 18px;
}
.rss-stats {
font-size: 13px;
}
.empty-state {
padding: 50px 25px;
}
.empty-state-title {
font-size: 20px;
}
}
@media (max-width: 768px) {
.rss-feeds-container {
padding: 15px;
}
.rss-header {
padding: 18px 20px;
margin-bottom: 20px;
}
.rss-title {
font-size: 20px;
}
.rss-refresh-btn {
padding: 9px 20px;
font-size: 13px;
height: 38px;
}
.table-container {
margin-top: 15px;
border-radius: 8px;
}
.rss-feeds-table th,
.rss-feeds-table td {
padding: 14px 16px;
font-size: 13px;
}
.article-description {
-webkit-line-clamp: 2;
max-height: 45px;
}
.feed-actions {
flex-direction: column;
gap: 6px;
}
.action-btn {
width: 100%;
justify-content: center;
padding: 6px 12px;
font-size: 12px;
}
.empty-state {
padding: 40px 20px;
margin-top: 20px;
}
.empty-state-title {
font-size: 18px;
}
.empty-state-message {
font-size: 14px;
}
.empty-state-actions {
flex-direction: column;
align-items: center;
gap: 12px;
}
.empty-state-btn {
width: 220px;
justify-content: center;
padding: 10px 20px;
font-size: 14px;
}
.loading-content {
padding: 30px 40px;
min-width: 280px;
}
.loading-spinner {
width: 40px;
height: 40px;
}
.loading-text {
font-size: 15px;
}
/* 移动端下拉菜单优化 */
.status-filter-dropdown,
.batch-dropdown {
width: 100%;
}
.status-filter-btn,
.batch-dropdown-btn {
width: 100%;
justify-content: space-between;
}
.status-filter-dropdown-content,
.batch-dropdown-content {
width: 100%;
left: 0;
right: 0;
}
}
/* 美化滚动条 */
.table-container::-webkit-scrollbar {
height: 6px;
width: 6px;
}
.table-container::-webkit-scrollbar-track {
background: #f1f3f5;
border-radius: 3px;
}
.table-container::-webkit-scrollbar-thumb {
background: #adb5bd;
border-radius: 3px;
}
.table-container::-webkit-scrollbar-thumb:hover {
background: #868e96;
}
/* 工具类 */
.text-truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.text-muted {
color: #6c757d;
}
.text-primary {
color: #467b96;
}
.text-success {
color: #28a745;
}
/* 阅读全文相关样式 */
.read-full-btn {
padding: 4px 10px;
border: 1px solid #467b96;
background: white;
color: #467b96;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
max-width: 48px;
font-weight: 500;
margin-top2px;
transition: all 0.2s;
display: inline-block;
text-decoration: none;
}
.read-full-btn:hover {
background: #467b96;
color: white;
text-decoration: none;
}
/* 阅读全文弹窗样式 */
.full-content-body {
padding: 30px;
line-height: 1.8;
color: #333;
font-size: 15px;
overflow-y: auto;
flex: 1;
}
.full-content-body img {
max-width: 100%;
height: auto;
margin: 10px 0;
border-radius: 6px;
}
.full-content-body p {
margin-bottom: 15px;
}
.full-content-body h1,
.full-content-body h2,
.full-content-body h3,
.full-content-body h4 {
margin: 25px 0 15px 0;
color: #2c3e50;
font-weight: 600;
}
.full-content-body ul,
.full-content-body ol {
margin: 15px 0;
padding-left: 25px;
}
.full-content-body li {
margin-bottom: 8px;
}
.full-content-body code {
background: #f5f7fa;
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
font-size: 13px;
color: #467b96;
}
.full-content-body pre {
background: #2c3e50;
color: #ecf0f1;
padding: 15px;
border-radius: 6px;
overflow-x: auto;
margin: 15px 0;
}
.full-content-body blockquote {
border-left: 4px solid #467b96;
padding-left: 15px;
margin: 15px 0;
color: #6c757d;
font-style: italic;
}
.full-content-body a {
color: #467b96;
text-decoration: none;
}
.full-content-body a:hover {
text-decoration: underline;
}
/* 文章元信息 */
.full-content-meta {
display: flex;
flex-wrap: wrap;
gap: 15px;
justify-content: center;
margin-top: -20px;
background: #f8f9fa;
border-radius: 8px;
}
.meta-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #6c757d;
}
.meta-item i {
font-style: normal;
color: #467b96;
font-weight: 500;
}
/* 弹窗工具栏 */
.modal-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 25px;
border-top: 1px solid #e8ebf0;
background: #f8fafc;
}
.toolbar-actions {
display: flex;
gap: 10px;
}
/* 文章操作按钮 */
.article-action-btn {
padding: 8px 15px;
border: 1px solid #dee2e6;
background: white;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
display: inline-flex;
align-items: center;
gap: 6px;
transition: all 0.2s;
color: #495057;
}
.article-action-btn:hover {
background: #f8f9fa;
border-color: #467b96;
color: #467b96;
}
.article-action-btn.primary {
background: #467b96;
border-color: #467b96;
color: white;
}
.article-action-btn.primary:hover {
background: #3a6a83;
border-color: #3a6a83;
color: white;
}
.article-action-btn.danger {
background: #dc3545;
border-color: #dc3545;
color: white;
}
.article-action-btn.danger:hover {
background: #c82333;
border-color: #c82333;
color: white;
}
/* 响应式调整 */
@media (max-width: 768px) {
.full-content-body {
padding: 20px;
font-size: 14px;
}
.full-content-meta {
flex-direction: column;
gap: 10px;
}
.modal-toolbar {
flex-direction: column;
gap: 15px;
align-items: flex-start;
}
.toolbar-actions {
width: 100%;
justify-content: space-between;
}
.article-action-btn {
padding: 6px 12px;
font-size: 12px;
}
}
</style>
<div class="main">
<div class="body container">
<?php include 'page-title.php'; ?>
<div class="row typecho-page-main" role="main">
<div class="col-mb-12 rss-feeds-container">
<!-- 页面头部 -->
<div class="rss-header">
<div>
<h2 class="rss-title">
<?php echo $showFavorites ? '收藏文章' : 'RSS信息订阅'; ?>
</h2>
<div class="rss-stats">
<?php if ($showFavorites): ?>
已藏 <strong><?php echo $favoriteStats['total']; ?></strong> 篇文章 |
显示 <?php echo count($feeds); ?> 条,第 <?php echo $currentPage; ?>/<?php echo $totalPages; ?> 页
<?php elseif (!empty($feeds)): ?>
已订阅 <strong><?php echo $totalFeeds; ?></strong> 条信息 |
显示 <?php echo count($feeds); ?> 条,第 <?php echo $currentPage; ?>/<?php echo $totalPages; ?> 页
<?php elseif ($rssUrlCount > 0): ?>
检测到 <strong><?php echo $rssUrlCount; ?></strong> 个RSS源
<?php if ($cacheStats['total'] > 0): ?>
| 缓存 <strong><?php echo $cacheStats['total']; ?></strong> 条记录
<?php endif; ?>
<?php else: ?>
暂无RSS源
<?php endif; ?>
</div>
</div>
<div style="display: flex; gap: 12px; align-items: center; flex-wrap: wrap;">
<?php if ($rssUrlCount > 0): ?>
<button type="button" class="rss-refresh-btn" id="refresh-rss-btn">
<span class="refresh-text">手动刷新</span>
<span class="refreshing-spinner" style="display: none;"></span>
</button>
<button type="button" class="rss-refresh-btn clean-btn" id="clean-cache-btn" title="清理过期缓存">
清理缓存
</button>
<?php endif; ?>
<a href="<?php echo \Typecho\Common::url('extending.php?panel=UrlNav/Manage.php', $options->adminUrl); ?>"
class="rss-refresh-btn manage-btn">
源头管理
</a>
<button type="button" class="rss-refresh-btn stats-btn" id="show-stats-btn" title="查看统计信息">
数据统计
</button>
</div>
</div>
<!-- 筛选栏 -->
<div class="rss-filter-bar">
<form method="get" action="" class="filter-form" style="display: contents;">
<input type="hidden" name="panel" value="UrlNav/Rss.php">
<input type="hidden" name="page" value="1">
<!-- 收藏按钮 -->
<div class="filter-group">
<a href="?panel=UrlNav/Rss.php&favorites=1&page=1<?php echo $search ? '&search=' . urlencode($search) : ''; ?>"
class="search-btn <?php echo $showFavorites ? 'active' : ''; ?>"
style="display: inline-flex; align-items: center; gap: 5px;">
<span></span>
已藏文章
</a>
</div>
<div class="filter-group">
<select name="category" class="filter-select" onchange="this.form.submit()" style="position: relative;">
<option value="all" <?php echo $categoryId === 'all' ? 'selected' : ''; ?>>全部分类</option>
<?php foreach ($categories as $cat): ?>
<option value="<?php echo $cat['id']; ?>" <?php echo $categoryId == $cat['id'] ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($cat['name']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<!-- 星级筛选下拉菜单 -->
<div class="filter-group">
<select name="star" class="filter-select" onchange="this.form.submit()" style="position: relative;">
<?php foreach ($allStarRatings as $star): ?>
<option value="<?php echo $star['value']; ?>" <?php echo $starRating === $star['value'] ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($star['label']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<!-- 状态筛选下拉菜单 -->
<div class="status-filter-dropdown" id="status-filter-dropdown">
<button type="button" class="status-filter-btn" onclick="toggleStatusFilter()">
全部状态
</button>
<div class="status-filter-dropdown-content">
<div class="status-filter-option" data-status="all" onclick="filterByStatus('all')">
全部 <span class="status-filter-count" id="count-all">0</span>
</div>
<div class="status-filter-option" data-status="unread" onclick="filterByStatus('unread')">
未读 <span class="status-filter-count" id="count-unread">0</span>
</div>
<div class="status-filter-option" data-status="read" onclick="filterByStatus('read')">
已读 <span class="status-filter-count" id="count-read">0</span>
</div>
<div class="status-filter-option" data-status="ignore" onclick="filterByStatus('ignore')">
无感 <span class="status-filter-count" id="count-ignore">0</span>
</div>
</div>
</div>
<!-- 批量操作下拉菜单 -->
<div class="batch-dropdown" id="batch-dropdown">
<button type="button" class="batch-dropdown-btn" onclick="toggleBatchDropdown()">
批量操作
</button>
<div class="batch-dropdown-content">
<div class="batch-option select-all" onclick="selectAllFeeds()">全部选择</div>
<div class="batch-option clear-selection" onclick="clearSelection()">取消选择</div>
<div class="batch-option mark-read" onclick="batchMarkAsRead()">标记已读</div>
<div class="batch-option mark-unread" onclick="batchMarkAsUnread()">标记未读</div>
<div class="batch-option mark-ignore" onclick="batchMarkAsIgnore()">标记无感</div>
<div class="batch-option export-status" onclick="exportStatusData()">导出状态</div>
<div class="batch-option import-status" onclick="importStatusData()">导入状态</div>
<div class="batch-option clear-status" onclick="clearAllStatuses()">清除状态</div>
</div>
</div>
<div class="filter-group">
<input type="text" name="search" class="search-input" placeholder="搜索文章标题或内容..." value="<?php echo htmlspecialchars($search); ?>">
</div>
<div class="filter-group">
<button type="submit" class="search-btn">立即搜索</button>
</div>
<?php if ($categoryId !== 'all' || $starRating !== 'all'|| $search || $showFavorites): ?>
<div class="filter-group">
<a href="?panel=UrlNav/Rss.php" class="clear-btn">清除筛选</a>
</div>
<?php endif; ?>
</form>
</div>
<!-- 内容区域 -->
<?php
if ($rssUrlCount == 0): ?>
<!-- 情况1没有任何RSS源 -->
<div class="empty-state">
<div class="empty-state-icon">📭</div>
<h3 class="empty-state-title">尚未添加RSS地址</h3>
<p class="empty-state-message">
您还没有为任何网址添加RSS地址。<br>
请在网址管理中为网站添加RSS地址然后返回此页面刷新RSS信息。
</p>
<div class="empty-state-actions">
<a href="<?php echo \Typecho\Common::url('extending.php?panel=UrlNav/Manage.php', $options->adminUrl); ?>"
class="empty-state-btn">
去添加RSS地址
</a>
</div>
</div>
<?php elseif (empty($feeds)): ?>
<!-- 情况2有RSS源但没有文章 -->
<?php if ($showFavorites): ?>
<!-- 情况2.1:收藏模式下没有收藏文章 -->
<div class="empty-state">
<div class="empty-state-icon"></div>
<h3 class="empty-state-title">暂无收藏文章</h3>
<p class="empty-state-message">
您还没有收藏任何文章。<br>
在文章列表中找到喜欢的文章,点击"收藏"按钮即可添加到收藏夹。
</p>
<div class="empty-state-actions">
<a href="?panel=UrlNav/Rss.php" class="empty-state-btn">
查看所有文章
</a>
<a href="<?php echo \Typecho\Common::url('extending.php?panel=UrlNav/Manage.php', $options->adminUrl); ?>"
class="empty-state-btn secondary">
管理RSS地址
</a>
</div>
</div>
<?php else: ?>
<!-- 情况2.2:普通模式下没有文章 -->
<div class="empty-state">
<div class="empty-state-icon">🔄</div>
<h3 class="empty-state-title">RSS缓存为空</h3>
<p class="empty-state-message">
<?php if ($categoryId !== 'all'): ?>
在选定的分类中未找到RSS缓存记录。<br>
请点击"手动刷新RSS"按钮获取最新文章,或选择其他分类。
<?php elseif ($starRating !== 'all'): ?>
在"<?php
// 使用一致的显示名称
$starDisplayNames = [
'0' => '无星级',
'starred' => '有星级',
'1' => '一星',
'2' => '二星',
'3' => '三星'
];
// 确保 $starRating 是后端能处理的值
$filterValue = $starRating;
if ($starRating === 'none') {
$filterValue = '0'; // 将 'none' 转换为 '0'
} elseif ($starRating === 'has') {
$filterValue = 'starred'; // 将 'has' 转换为 'starred'
}
$starLabel = isset($starDisplayNames[$filterValue]) ? $starDisplayNames[$filterValue] : '所有';
echo htmlspecialchars($starLabel);
?>"中未找到RSS缓存记录。<br>
请点击"手动刷新RSS"按钮获取最新文章,或选择其他星级。
<?php elseif ($search): ?>
未找到包含"<?php echo htmlspecialchars($search); ?>"的文章。<br>
请尝试其他关键词,或点击"清除筛选"按钮查看所有文章。
<?php else: ?>
检测到 <strong><?php echo $rssUrlCount; ?></strong> 个RSS源但缓存为空。<br>
请点击"手动刷新RSS"按钮获取最新的RSS文章。
<?php endif; ?>
</p>
<div class="empty-state-actions">
<button type="button" class="empty-state-btn" id="prompt-refresh-btn">
手动刷新RSS
</button>
<a href="<?php echo \Typecho\Common::url('extending.php?panel=UrlNav/Manage.php', $options->adminUrl); ?>"
class="empty-state-btn secondary">
管理RSS地址
</a>
<?php if ($categoryId !== 'all' || $search): ?>
<a href="?panel=UrlNav/Rss.php" class="empty-state-btn secondary">
清除筛选
</a>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<?php else: ?>
<!-- 情况3正常显示表格 -->
<div class="table-container">
<table class="rss-feeds-table">
<colgroup>
<col/>
<col/>
<col/>
<col/>
<col/>
</colgroup>
<thead>
<tr>
<th style="width: 30px;">状态</th>
<!--<th style="width: 20px;">序号</th>-->
<th>网站</th>
<th>文章</th>
<th>时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<?php
$index = ($currentPage - 1) * $pageSize + 1;
foreach ($feeds as $feed):
// 转换为北京时间
if ($showFavorites) {
$beijingPubDate = formatBeijingDateTime($feed['pub_date'], 'Y-m-d H:i');
$beijingCachedAt = '';
$timeAgo = '';
} else {
$beijingPubDate = formatBeijingDateTime($feed['pub_date'], 'Y-m-d H:i');
$beijingCachedAt = !empty($feed['cached_at']) ? formatSimpleTime($feed['cached_at']) : '';
$timeAgo = formatTimeAgo($feed['pub_date']);
}
$feedId = md5(($feed['feed_link'] ?? '') . ($feed['feed_title'] ?? '') . ($feed['pub_date'] ?? ''));
// 检查是否有完整内容
$hasFullContent = hasFullContent($feed);
$articleExcerpt = getArticleExcerpt($feed['feed_description'] ?? '', 120);
// 获取收藏状态
$isFavorite = UrlNav_Plugin::isFavorite($feed['id'], 0);
?>
<tr data-feed-id="<?php echo $feedId; ?>">
<!-- 状态列 -->
<td class="feed-status-cell">
<div class="status-selector" data-feed-id="<?php echo $feedId; ?>" onclick="toggleStatusDropdown(this)">
<div class="status-dot status-unread" title="未读"></div>
<div class="status-dropdown">
<div class="status-option" title="未读" data-status="unread" data-color="#dc3545" onclick="setFeedStatus('<?php echo $feedId; ?>', 'unread')"></div>
<div class="status-option" title="已读" data-status="read" data-color="#28a745" onclick="setFeedStatus('<?php echo $feedId; ?>', 'read')"></div>
<div class="status-option" title="无感" data-status="ignore" data-color="#6c757d" onclick="setFeedStatus('<?php echo $feedId; ?>', 'ignore')"></div>
</div>
</div>
</td>
<!-- <td class="feed-index"><?php echo $index++; ?></td>-->
<td>
<div class="site-info">
<div class="site-title"> <a href="?panel=UrlNav/Rss.php&search=<?php echo urlencode($feed['site_title'] ?? ''); ?>"
title="查看此网站的所有文章"
style="color: #2c3e50; text-decoration: none; font-weight: 600; cursor: pointer;">
<?php echo htmlspecialchars($feed['site_title'] ?? '未知网站'); ?>
</a>
</div>
<div class="star-rating-section" style="display: flex; align-items: center; gap: 5px; margin-top: 4px;">
<?php
// 确保安全获取并转换为整数
$feedStarRating = isset($feed['star_rating']) ? intval($feed['star_rating']) : 0;
if ($feedStarRating > 0):
$stars = '';
for ($i = 1; $i <= 3; $i++) {
$stars .= ($i <= $feedStarRating) ? '★' : '☆';
}
?>
<a href="?panel=UrlNav/Rss.php&star=<?php echo $feedStarRating; ?>&page=1"
class="star-link"
title="查看此星级的所有文章"
style="text-decoration: none; color: #ffc107; font-size: 14px; cursor: pointer;">
<?php echo $stars; ?>
</a>
<!--<span style="font-size: 11px; color: #ff9800; margin-left: 2px;">
(<?php echo $feedStarRating; ?>星)
</span>-->
<?php else: ?>
<a href="?panel=UrlNav/Rss.php&star=0&page=1"
class="no-star-link"
title="查看无星级的文章"
style="text-decoration: none; color: #6c757d; font-size: 12px; cursor: pointer;">
☆☆☆
</a>
<?php endif; ?>
</div>
<div class="site-url">
<a href="<?php echo htmlspecialchars($feed['site_url'] ?? '#'); ?>" target="_blank" title="访问网站">
<?php echo htmlspecialchars($feed['site_url'] ?? '未知地址'); ?>
</a>
</div>
<?php if (!empty($feed['category_name'])): ?>
<span class="category-tag"><?php echo htmlspecialchars($feed['category_name']); ?></span>
<?php endif; ?>
</div>
</td>
<td>
<div class="article-info">
<div class="article-title">
<a href="<?php echo htmlspecialchars($feed['feed_link'] ?? '#'); ?>" target="_blank" title="查看文章">
<?php echo htmlspecialchars($feed['feed_title'] ?? '无标题'); ?>
</a>
</div>
<?php if (!empty($articleExcerpt)): ?>
<div class="article-description">
<?php echo htmlspecialchars($articleExcerpt); ?>
</div>
<?php endif; ?>
<!-- 阅读全文按钮 -->
<?php if ($hasFullContent): ?>
<a href="javascript:void(0);"
class="read-full-btn"
onclick="showFullContent(<?php echo htmlspecialchars(json_encode([
'id' => $feed['id'],
'title' => $feed['feed_title'] ?? '无标题',
'site_title' => $feed['site_title'] ?? '未知网站',
'site_url' => $feed['site_url'] ?? '',
'link' => $feed['feed_link'] ?? '',
'pub_date' => $beijingPubDate,
'category' => $feed['category_name'] ?? '',
'full_content' => $feed['full_content'],
'description' => $feed['feed_description'] ?? ''
])); ?>)">
阅读全文
</a>
<?php endif; ?>
</div>
</td>
<td>
<div class="time-info">
<div class="pub-date" title="北京时间:<?php echo $beijingPubDate; ?>">
<?php echo $beijingPubDate ?: '未知时间'; ?>
</div>
<?php if (!$showFavorites && $timeAgo): ?>
<div class="time-ago"><?php echo $timeAgo; ?></div>
<?php endif; ?>
<?php if (!$showFavorites && $beijingCachedAt): ?>
<div class="cache-time" title="缓存时间(北京时间)">
<?php echo $beijingCachedAt; ?>
</div>
<?php endif; ?>
<?php if ($showFavorites && !empty($feed['favorited_at'])): ?>
<div class="favorited-time" title="收藏时间(北京时间)">
<?php echo formatSimpleTime($feed['favorited_at']); ?>
</div>
<?php endif; ?>
</div>
</td>
<td>
<div class="feed-actions">
<a href="<?php echo htmlspecialchars($feed['feed_link'] ?? '#'); ?>" target="_blank"
class="action-btn visit" title="查看文章">
原文
</a>
<button type="button" class="action-btn copy"
onclick="copyToClipboard('<?php echo htmlspecialchars(addslashes($feed['feed_link'] ?? '')); ?>')"
title="复制链接">
复制
</button>
<!-- 收藏按钮 -->
<?php if (!$showFavorites): ?>
<button type="button" class="action-btn favorite-btn <?php echo $isFavorite ? 'active' : ''; ?>"
data-feed-id="<?php echo $feed['id']; ?>"
data-is-favorite="<?php echo $isFavorite ? '1' : '0'; ?>"
onclick="toggleFavorite(this, <?php echo $feed['id']; ?>, <?php echo $isFavorite ? 'true' : 'false'; ?>)"
title="<?php echo $isFavorite ? '已藏,点击取消' : '收藏文章'; ?>">
<?php echo $isFavorite ? '已藏' : '收藏'; ?>
</button>
<?php else: ?>
<!-- 在收藏页面显示取消收藏按钮 -->
<button type="button" class="action-btn favorite-btn active"
data-feed-id="<?php echo $feed['original_feed_id'] ?? $feed['id']; ?>"
data-is-favorite="1"
onclick="toggleFavorite(this, <?php echo $feed['original_feed_id'] ?? $feed['id']; ?>, true)"
title="已藏,点击取消">
取消收藏
</button>
<?php endif; ?>
<!-- 找到操作按钮部分,在收藏按钮后面添加保存按钮 -->
<?php if (hasFullContent($feed)): ?>
<button type="button" class="action-btn save-btn"
onclick="saveAsMarkdown(<?php echo htmlspecialchars(json_encode([
'id' => $feed['id'],
'title' => $feed['feed_title'] ?? '无标题',
'site_title' => $feed['site_title'] ?? '未知网站',
'site_url' => $feed['site_url'] ?? '',
'link' => $feed['feed_link'] ?? '',
'pub_date' => $beijingPubDate,
'category' => $feed['category_name'] ?? '',
'full_content' => $feed['full_content'],
'description' => $feed['feed_description'] ?? ''
])); ?>)"
title="保存全文为Markdown格式">
保存
</button>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- 分页 -->
<?php if ($totalPages > 1): ?>
<div class="rss-pagination">
<?php
// 构建基础URL
$baseUrl = '?panel=UrlNav/Rss.php';
if ($showFavorites) {
$baseUrl .= '&favorites=1';
}
if ($categoryId !== 'all') {
$baseUrl .= '&category=' . urlencode($categoryId);
}
if ($starRating !== 'all') {
$baseUrl .= '&star=' . urlencode($starRating);
} if ($search) {
$baseUrl .= '&search=' . urlencode($search);
}
?>
<?php if ($currentPage > 1): ?>
<a href="<?php echo $baseUrl; ?>&page=1" class="pagination-btn">首页</a>
<a href="<?php echo $baseUrl; ?>&page=<?php echo $currentPage - 1; ?>" class="pagination-btn">上一页</a>
<?php else: ?>
<span class="pagination-btn" disabled>首页</span>
<span class="pagination-btn" disabled>上一页</span>
<?php endif; ?>
<?php
// 显示分页数字
$startPage = max(1, $currentPage - 2);
$endPage = min($totalPages, $currentPage + 2);
if ($startPage > 1): ?>
<span class="pagination-ellipsis">...</span>
<?php endif;
for ($i = $startPage; $i <= $endPage; $i++): ?>
<?php if ($i == $currentPage): ?>
<span class="pagination-current"><?php echo $i; ?></span>
<?php else: ?>
<a href="<?php echo $baseUrl; ?>&page=<?php echo $i; ?>" class="pagination-btn"><?php echo $i; ?></a>
<?php endif; ?>
<?php endfor;
if ($endPage < $totalPages): ?>
<span class="pagination-ellipsis">...</span>
<?php endif; ?>
<?php if ($currentPage < $totalPages): ?>
<a href="<?php echo $baseUrl; ?>&page=<?php echo $currentPage + 1; ?>" class="pagination-btn">下一页</a>
<a href="<?php echo $baseUrl; ?>&page=<?php echo $totalPages; ?>" class="pagination-btn">末页</a>
<?php else: ?>
<span class="pagination-btn" disabled>下一页</span>
<span class="pagination-btn" disabled>末页</span>
<?php endif; ?>
<div class="pagination-info">
第 <?php echo $currentPage; ?> 页 / 共 <?php echo $totalPages; ?> 页,每页 <?php echo $pageSize; ?> 条
</div>
</div>
<?php endif; ?>
<?php endif; ?>
<!-- RSS定时任务管理 -->
<div class="cron-management">
<div style="display: flex; justify-content: center; align-items: center; margin-bottom: 20px;">
<h3 style="margin: 0;color:#000;text-align:center;">宝塔任务配置、统计</h3>
</div>
<div style="">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px;">
<div>
<h4 style="margin-top: 0; margin-bottom: 10px; color: #2c3e50;">RSS任务地址</h4>
<div style="background: #f8f9fa; color:#000;padding: 15px; border-radius: 6px; font-family: monospace; font-size: 12px; word-break: break-all; margin-bottom: 10px; position: relative;">
<?php
$rssCronUrl = getRssCronUrl();
echo htmlspecialchars($rssCronUrl);
?><br>
<button type="button" class="action-btn copy"
style="padding: 3px 8px; font-size: 11px;margin-top:10px;"
onclick="copyToClipboard('<?php echo htmlspecialchars($rssCronUrl); ?>')"
title="复制RSS任务地址">
复制
</button>
</div>
<div style="font-size: 12px; color: #6c757d;">
请妥善保管此URL不要泄露给他人
</div>
</div>
</div>
<!--<div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #e8ebf0;">
<h4 style="margin-top: 0; margin-bottom: 15px; color: #2c3e50;">宝塔计划任务设置步骤</h4>
<div style="background: #f8f9fa; padding: 15px; border-radius: 6px; margin-bottom: 15px;">
<div style="display: flex; align-items: flex-start; gap: 10px; margin-bottom: 10px;">
<span style="background: #467b96; color: #000000; width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; flex-shrink: 0;">1</span>
<div>
<strong style="color:#000;">登录宝塔面板</strong>
<div style="font-size: 12px; color: #6c757d;">进入"计划任务"功能</div>
</div>
</div>
<div style="display: flex; align-items: flex-start; gap: 10px; margin-bottom: 10px;">
<span style="background: #467b96; color: #000000; width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; flex-shrink: 0;">2</span>
<div>
<strong style="color:#000;">添加Shell脚本任务</strong>
<div style="font-size: 12px; color: #6c757d;">任务类型选择"Shell脚本"</div>
</div>
</div>
<div style="display: flex; align-items: flex-start; gap: 10px; margin-bottom: 10px;">
<span style="background: #467b96; color: #000000; width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; flex-shrink: 0;">3</span>
<div>
<strong style="color:#000;">设置执行周期</strong>
<div style="font-size: 12px; color: #6c757d;">建议每小时或每30分钟执行一次</div>
</div>
</div>
<div style="display: flex; align-items: flex-start; gap: 10px;">
<span style="background: #467b96; color: #000000; width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; flex-shrink: 0;">4</span>
<div>
<strong style="color:#000;">填写脚本内容</strong>
<div style="font-size: 12px; color: #6c757d;">复制下面的Shell命令到脚本框中</div>
</div>
</div>
</div>-->
<!-- <div style="background: #f8f9fa; padding: 15px; border-radius: 6px; margin-bottom: 15px;">
<h5 style="margin-top: 0; margin-bottom: 10px; color: #2c3e50;">Shell脚本命令</h5>
<div style="background: #2c3e50; color: #e9ecef; padding: 10px; border-radius: 4px; font-family: monospace; font-size: 12px; word-break: break-all;">
curl -s "<?php echo htmlspecialchars($rssCronUrl); ?>"
</div>
<div style="font-size: 11px; color: #6c757d; margin-top: 5px;">
如果服务器没有安装curl可以使用wget -O /dev/null -q "<?php echo htmlspecialchars($rssCronUrl); ?>"
</div>
</div>-->
<!--<div style="background: #e7f3ff; padding: 12px; border-radius: 6px; border-left: 4px solid #467b96;">
<div style="display: flex; align-items: flex-start; gap: 8px;">
<div>
<strong style="color:#000;">提示</strong>
<div style="font-size: 12px; color: #2c3e50; margin-top: 2px;">
配置后系统将按宝塔设置的时间周期自动刷新RSS信息无需人工干预。
建议设置为每1-2小时执行一次避免对目标网站造成过大压力。
</div>
</div>
</div>
</div>-->
<!-- RSS定时任务监控部分 -->
<div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #e8ebf0;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<h4 style="margin: 0; color: #2c3e50;">定时任务监控</h4>
<div style="display: flex; gap: 10px;">
<button type="button" class="action-btn" onclick="showRssCronStats()">
统计
</button>
<button type="button" class="action-btn" onclick="showRssCronLogs()">
日志
</button>
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px;">
<div style="background: #f8f9fa; padding: 12px; border-radius: 6px; text-align: center;">
<div style="font-size: 11px; color: #6c757d; margin-bottom: 4px;">总执行次数</div>
<div id="rss-cron-total" style="font-size: 18px; font-weight: bold; color: #467b96;"><?php echo $rssCronStats['total'] ?? 0; ?></div>
</div>
<div style="background: #f8f9fa; padding: 12px; border-radius: 6px; text-align: center;">
<div style="font-size: 11px; color: #6c757d; margin-bottom: 4px;">成功率</div>
<div id="rss-cron-success-rate" style="font-size: 18px; font-weight: bold; color: #28a745;"><?php echo $rssCronStats['success_rate'] ?? 0; ?>%</div>
</div>
<div style="background: #f8f9fa; padding: 12px; border-radius: 6px; text-align: center;">
<div style="font-size: 11px; color: #6c757d; margin-bottom: 4px;">最后执行</div>
<div id="rss-cron-last-executed" style="font-size: 14px; color: #6c757d;">
<?php
if (!empty($rssCronStats['last_executed'])) {
echo formatStatsTime($rssCronStats['last_executed']);
} else {
echo '从未执行';
}
?>
</div>
</div>
<!--<div style="background: #f8f9fa; padding: 12px; border-radius: 6px; text-align: center;">
<div style="font-size: 11px; color: #6c757d; margin-bottom: 4px;">当前状态</div>
<div id="rss-cron-status" style="font-size: 18px; font-weight: bold; <?php echo $isRssLocked ? 'color: #dc3545;' : 'color: #28a745;'; ?>">
<?php echo $isRssLocked ? '运行中' : '空闲'; ?>
</div>
</div>
</div>-->
<?php if ($isRssLocked): ?>
<div style="margin-top: 15px; padding: 12px; background: <?php echo $rssLockDuration > 600 ? '#f8d7da' : '#fff3cd'; ?>; border-radius: 6px; border: 1px solid <?php echo $rssLockDuration > 600 ? '#f5c6cb' : '#ffeaa7'; ?>;">
<div style="display: flex; align-items: center; gap: 10px;">
<div style="flex: 1; font-size: 13px; color: <?php echo $rssLockDuration > 600 ? '#721c24' : '#856404'; ?>;">
<strong>RSS定时任务状态</strong>
已运行 <?php echo floor($rssLockDuration / 60); ?> 分 <?php echo $rssLockDuration % 60; ?> 秒
<?php if ($rssLockDuration > 600): ?>
<br><small>如果确定没有任务在运行,请等待或重启相关服务。</small>
<?php endif; ?>
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 消息提示 -->
<div class="rss-alert" id="rss-message-alert" style="display: none;"></div>
<!-- 统计信息弹窗 -->
<div id="stats-modal" class="modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 10000; align-items: center; justify-content: center;">
<div class="modal-content" style="background: white; padding: 30px; border-radius: 10px; max-width: 600px; width: 90%; max-height: 80vh; overflow-y: auto;">
<h3 style="margin-top: 0;" id="modal-title">统计信息</h3>
<div id="modal-content">
<p>正在加载统计信息...</p>
</div>
<!--<div style="display: flex; justify-content: flex-end; margin-top: 20px; gap: 10px;">
<button type="button" class="action-btn" onclick="closeModal()">关闭</button>
</div>-->
</div>
</div>
<!-- 阅读全文弹窗 -->
<div id="full-content-modal" class="modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 100000; align-items: center; justify-content: center; padding: 20px; backdrop-filter: blur(5px);">
<div class="modal-content" style="background: white; border-radius: 12px; max-width: 900px; width: 100%; max-height: 85vh; display: flex; flex-direction: column; box-shadow: 0 25px 100px rgba(0,0,0,0.4); overflow: hidden; ">
<div style="display: flex; justify-content: space-between; align-items: center; padding: 20px 25px; background: linear-gradient(135deg, #467b96 0%, #3a6a83 100%); border-bottom: 1px solid rgba(0,0,0,0.1);">
<h3 style="margin: 0 auto; font-size: 18px; font-weight: 600; color: white; display: flex; align-items: center; gap: 10px;justify-content:center;">
<span id="full-content-title" style="flex: 1;"><!--文章详情--></span>
</h3>
<button type="button" onclick="closeFullContentModal()" style="background: rgba(255,255,255,0.2); border: none; color: white; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 18px; transition: all 0.2s; font-weight: bold;">&times;</button>
</div>
<div style="flex: 1; overflow-y: auto; padding: 0;">
<!-- 文章头部信息 -->
<div style="padding: 25px; border-bottom: 1px solid #e8ebf0; background: #f8fafc;">
<div style="display: flex; align-items: flex-start; gap: 15px; margin-bottom: 15px;">
<div style="flex: 1;">
<div id="full-content-site-title" style="font-size: 15px; color: #467b96; font-weight: 500; margin-bottom: 5px;"></div>
<div id="full-content-original-title" style="font-size: 20px; font-weight: 600; color: #2c3e50; line-height: 1.4; margin-bottom: 10px;"></div>
</div>
</div>
<!-- 文章元信息 -->
<div id="full-content-meta" class="full-content-meta">
<!-- 元信息将通过JavaScript动态填充 -->
</div>
</div>
<!-- 文章内容区域 -->
<div id="full-content-body" class="full-content-body">
<!-- 内容将通过JavaScript动态填充 -->
</div>
</div>
<!-- 工具栏 -->
<div class="modal-toolbar">
<div id="full-content-status" style="font-size: 13px; color: #6c757d;">
加载中...
</div>
<div class="toolbar-actions">
<button type="button" class="article-action-btn" onclick="copyFullContentLink()">
复制
</button>
<button type="button" class="article-action-btn primary" onclick="openOriginalLink()">
原文
</button>
<!-- 新增保存为Markdown按钮 -->
<button type="button" class="article-action-btn" onclick="saveCurrentArticle()">
保存
</button>
<button type="button" class="article-action-btn" onclick="closeFullContentModal()">
关闭
</button>
</div>
</div>
</div>
</div>
<!-- 加载动画 -->
<div id="loading-overlay">
<div class="loading-content">
<div class="loading-spinner"></div>
<p class="loading-text">正在刷新RSS信息请稍候...</p>
<p class="loading-subtext">这可能需要一些时间,请耐心等待</p>
</div>
</div>
<script>
// 保存当前阅读的文章
function saveCurrentArticle() {
if (window.currentArticleData) {
saveAsMarkdown(window.currentArticleData);
}
}
// HTML转Markdown的简单函数
function htmlToMarkdown(html) {
if (!html) return '';
let markdown = html;
// 基本转换
markdown = markdown.replace(/<h1[^>]*>(.*?)<\/h1>/gi, '# $1\n\n');
markdown = markdown.replace(/<h2[^>]*>(.*?)<\/h2>/gi, '## $1\n\n');
markdown = markdown.replace(/<h3[^>]*>(.*?)<\/h3>/gi, '### $1\n\n');
markdown = markdown.replace(/<p[^>]*>(.*?)<\/p>/gi, '$1\n\n');
markdown = markdown.replace(/<br\s*\/?>/gi, ' \n');
markdown = markdown.replace(/<strong[^>]*>(.*?)<\/strong>/gi, '**$1**');
markdown = markdown.replace(/<b[^>]*>(.*?)<\/b>/gi, '**$1**');
markdown = markdown.replace(/<em[^>]*>(.*?)<\/em>/gi, '*$1*');
markdown = markdown.replace(/<i[^>]*>(.*?)<\/i>/gi, '*$1*');
markdown = markdown.replace(/<a[^>]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/gi, '[$2]($1)');
markdown = markdown.replace(/<img[^>]*src=["']([^"']+)["'][^>]*alt=["']([^"']*)["'][^>]*>/gi, '![$2]($1)');
markdown = markdown.replace(/<li[^>]*>(.*?)<\/li>/gi, '- $1\n');
markdown = markdown.replace(/<blockquote[^>]*>(.*?)<\/blockquote>/gi, '> $1\n\n');
markdown = markdown.replace(/<code[^>]*>(.*?)<\/code>/gi, '`$1`');
// 移除所有HTML标签
markdown = markdown.replace(/<[^>]+>/g, '');
// 处理特殊字符
markdown = markdown.replace(/&nbsp;/g, ' ');
markdown = markdown.replace(/&amp;/g, '&');
markdown = markdown.replace(/&lt;/g, '<');
markdown = markdown.replace(/&gt;/g, '>');
markdown = markdown.replace(/&quot;/g, '"');
return markdown.trim();
}
// 保存为Markdown文件
function saveAsMarkdown(articleData) {
if (!articleData || !articleData.full_content) {
showRssMessage('没有完整的文章内容可供保存', 'warning');
return;
}
try {
// 生成Markdown内容
const title = articleData.title || '无标题';
const site = articleData.site_title || '未知网站';
const date = articleData.pub_date || new Date().toISOString().slice(0, 10);
const link = articleData.link || '';
let markdown = `# ${title}\n\n`;
markdown += `**来源:** ${site}\n`;
markdown += `**时间:** ${date}\n`;
if (link) markdown += `**链接:** ${link}\n`;
markdown += '\n---\n\n';
// 转换正文内容
const content = htmlToMarkdown(articleData.full_content);
markdown += content;
// 生成文件名
const cleanTitle = title.replace(/[\\/*?:"<>|]/g, '_').substring(0, 50);
const cleanSite = site.replace(/[\\/*?:"<>|]/g, '_').substring(0, 20);
const fileName = `${date.replace(/[^0-9]/g, '')}_${cleanSite}_${cleanTitle}.md`;
// 创建并下载文件
const blob = new Blob([markdown], { type: 'text/markdown;charset=utf-8' });
const url = URL.createObjectURL(blob);
const linkElem = document.createElement('a');
linkElem.href = url;
linkElem.download = fileName;
linkElem.style.display = 'none';
document.body.appendChild(linkElem);
linkElem.click();
document.body.removeChild(linkElem);
URL.revokeObjectURL(url);
showRssMessage('已保存', 'success');
} catch (error) {
console.error('保存失败:', error);
showRssMessage('保存失败: ' + error.message, 'error');
}
}
// 页面加载后自动加载定时任务统计
document.addEventListener('DOMContentLoaded', function() {
// 加载RSS定时任务统计信息
loadRssCronStats();
// 绑定所有刷新按钮
bindRefreshButton('refresh-rss-btn');
bindRefreshButton('prompt-refresh-btn');
// 绑定清理缓存按钮
var cleanBtn = document.getElementById('clean-cache-btn');
if (cleanBtn) {
cleanBtn.addEventListener('click', function(e) {
e.preventDefault();
cleanRssCache();
});
}
// 绑定统计按钮
var statsBtn = document.getElementById('show-stats-btn');
if (statsBtn) {
statsBtn.addEventListener('click', function(e) {
e.preventDefault();
showStats();
});
}
// 绑定RSS定时任务监控按钮
var cronStatsBtn = document.querySelector('.cron-management .action-btn[onclick*="showRssCronStats"]');
if (cronStatsBtn) {
cronStatsBtn.onclick = function(e) {
e.preventDefault();
showRssCronStats();
};
}
var cronLogsBtn = document.querySelector('.cron-management .action-btn[onclick*="showRssCronLogs"]');
if (cronLogsBtn) {
cronLogsBtn.onclick = function(e) {
e.preventDefault();
showRssCronLogs();
};
}
// 复制功能
window.copyToClipboard = function(text) {
if (!text || text === '#') {
showRssMessage('链接无效,无法复制', 'warning');
return;
}
navigator.clipboard.writeText(text).then(function() {
showRssMessage('链接已复制到剪贴板', 'success');
}, function() {
// 降级方案
var textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
try {
var successful = document.execCommand('copy');
if (successful) {
showRssMessage('链接已复制到剪贴板', 'success');
}
} catch (err) {
showRssMessage('复制失败:' + err, 'error');
}
document.body.removeChild(textarea);
});
};
// 自动隐藏消息
setTimeout(function() {
var alert = document.getElementById('rss-message-alert');
if (alert && alert.style.display === 'block') {
alert.style.display = 'none';
}
}, 5000);
// 添加回车键刷新支持
document.addEventListener('keydown', function(e) {
if (e.key === 'F5' || (e.ctrlKey && e.key === 'r')) {
e.preventDefault();
var refreshBtn = document.getElementById('refresh-rss-btn') || document.getElementById('prompt-refresh-btn');
if (refreshBtn && !refreshBtn.disabled) {
refreshBtn.click();
}
}
});
// 关闭模态框
document.addEventListener('click', function(e) {
var modal = document.getElementById('stats-modal');
if (e.target === modal) {
closeModal();
}
var fullContentModal = document.getElementById('full-content-modal');
if (e.target === fullContentModal) {
closeFullContentModal();
}
// 点击页面其他地方关闭下拉菜单
if (!e.target.closest('.status-filter-dropdown')) {
document.querySelectorAll('.status-filter-dropdown').forEach(dropdown => {
dropdown.classList.remove('active');
});
}
if (!e.target.closest('.batch-dropdown')) {
document.querySelectorAll('.batch-dropdown').forEach(dropdown => {
dropdown.classList.remove('active');
});
}
if (!e.target.closest('.status-selector')) {
document.querySelectorAll('.status-selector').forEach(selector => {
selector.classList.remove('active');
});
}
});
// ESC键关闭弹窗
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeModal();
closeFullContentModal();
// 关闭所有下拉菜单
document.querySelectorAll('.status-filter-dropdown, .batch-dropdown, .status-selector').forEach(el => {
el.classList.remove('active');
});
}
});
// 初始化状态管理
if (document.querySelector('.rss-feeds-table')) {
initStatusManagement();
}
});
// 设置插件配置
window.UrlNavConfig = {
actionUrl: '<?php echo Typecho_Common::url('/action/urlnav', $options->index); ?>',
adminUrl: '<?php echo $options->adminUrl; ?>'
};
// 显示加载动画
function showLoading() {
document.getElementById('loading-overlay').style.display = 'flex';
}
// 隐藏加载动画
function hideLoading() {
document.getElementById('loading-overlay').style.display = 'none';
}
// 显示消息
function showRssMessage(message, type) {
var alert = document.getElementById('rss-message-alert');
if (!alert) {
alert = document.createElement('div');
alert.id = 'rss-message-alert';
alert.className = 'rss-alert';
document.body.appendChild(alert);
}
alert.className = 'rss-alert ' + type;
alert.innerHTML = message;
alert.style.display = 'block';
setTimeout(function() {
alert.style.display = 'none';
}, 5000);
}
// 加载RSS定时任务统计
function loadRssCronStats() {
fetch(window.UrlNavConfig.actionUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'do=getRssCronStats',
credentials: 'include'
})
.then(response => response.json())
.then(data => {
if (data.success) {
var stats = data.data;
var totalElement = document.getElementById('rss-cron-total');
var successRateElement = document.getElementById('rss-cron-success-rate');
var lastExecutedElement = document.getElementById('rss-cron-last-executed');
var statusElement = document.getElementById('rss-cron-status');
if (totalElement) totalElement.textContent = stats.total || 0;
if (successRateElement) successRateElement.textContent = stats.success_rate || '0%';
if (lastExecutedElement) {
if (stats.last_executed) {
lastExecutedElement.textContent = formatBeijingDateJS(stats.last_executed);
} else {
lastExecutedElement.textContent = '从未执行';
}
}
}
})
.catch(error => {
console.error('加载RSS定时任务统计失败:', error);
});
}
// 手动刷新RSS
function refreshRss() {
showLoading();
// 更新按钮状态
var refreshBtn = document.getElementById('refresh-rss-btn');
var promptBtn = document.getElementById('prompt-refresh-btn');
if (refreshBtn) {
refreshBtn.disabled = true;
var spinner = refreshBtn.querySelector('.refreshing-spinner');
var text = refreshBtn.querySelector('.refresh-text');
if (spinner) spinner.style.display = 'inline-block';
if (text) text.textContent = '刷新中...';
}
if (promptBtn) {
promptBtn.disabled = true;
promptBtn.textContent = '刷新中...';
}
// 发送请求
fetch(window.UrlNavConfig.actionUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'do=getRssFeeds',
credentials: 'include'
})
.then(response => {
if (!response.ok) {
if (response.status === 403) {
throw new Error('无权限访问,请重新登录');
}
throw new Error('HTTP错误: ' + response.status);
}
return response.json();
})
.then(data => {
hideLoading();
// 恢复按钮状态
if (refreshBtn) {
refreshBtn.disabled = false;
var spinner = refreshBtn.querySelector('.refreshing-spinner');
var text = refreshBtn.querySelector('.refresh-text');
if (spinner) spinner.style.display = 'none';
if (text) text.textContent = '手动刷新RSS';
}
if (promptBtn) {
promptBtn.disabled = false;
promptBtn.textContent = '手动刷新RSS';
}
if (data.success) {
var message = data.message;
if (data.newArticles !== undefined) {
message += ',新增 ' + data.newArticles + ' 篇文章';
}
showRssMessage(message, 'success');
// 3秒后刷新页面
setTimeout(() => {
window.location.reload();
}, 3000);
} else {
showRssMessage(data.message, 'error');
}
})
.catch(error => {
hideLoading();
// 恢复按钮状态
if (refreshBtn) {
refreshBtn.disabled = false;
var spinner = refreshBtn.querySelector('.refreshing-spinner');
var text = refreshBtn.querySelector('.refresh-text');
if (spinner) spinner.style.display = 'none';
if (text) text.textContent = '手动刷新RSS';
}
if (promptBtn) {
promptBtn.disabled = false;
promptBtn.textContent = '手动刷新RSS';
}
console.error('刷新错误:', error);
if (error.message.includes('无权限') || error.message.includes('403') || error.message.includes('401')) {
showRssMessage('权限错误:请确保您已登录管理员账号', 'warning');
setTimeout(() => {
window.location.href = window.UrlNavConfig.adminUrl + 'login.php';
}, 2000);
} else {
showRssMessage('刷新失败:' + error.message, 'error');
}
});
}
// 清理缓存函数
function cleanRssCache() {
if (!confirm('确定要清理RSS缓存吗\n\n这将删除所有陈旧的缓存记录但不会影响RSS源本身。')) {
return;
}
showLoading();
fetch(window.UrlNavConfig.actionUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'do=cleanRssCache',
credentials: 'include'
})
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success) {
var message = '缓存清理完成';
if (data.cleaned_count > 0) {
message += ',删除了 ' + data.cleaned_count + ' 条记录';
} else {
message += ',没有需要清理的记录';
}
showRssMessage(message, 'success');
// 2秒后刷新页面
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
showRssMessage('清理失败:' + data.message, 'error');
}
})
.catch(error => {
hideLoading();
showRssMessage('清理失败:' + error.message, 'error');
});
}
// 切换收藏状态函数 - 修复版
function toggleFavorite(button, feedId, isFavorite) {
// 修复直接从按钮的data属性获取当前状态
if (typeof isFavorite === 'undefined') {
isFavorite = button.getAttribute('data-is-favorite') === '1';
}
var action = isFavorite ? 'removeFavorite' : 'addFavorite';
// 显示加载状态
var originalText = button.textContent;
var originalTitle = button.title;
button.textContent = '处理中...';
button.disabled = true;
fetch(window.UrlNavConfig.actionUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'do=' + action + '&feed_id=' + feedId,
credentials: 'include'
})
.then(response => response.json())
.then(data => {
// 恢复按钮状态
button.disabled = false;
if (data.success) {
if (isFavorite) {
// 取消收藏
button.textContent = '收藏';
button.title = '收藏文章';
button.classList.remove('active');
button.setAttribute('data-is-favorite', '0');
showRssMessage(data.message, 'success');
// 如果当前在收藏页面,刷新页面
if (window.location.href.includes('favorites=1')) {
setTimeout(() => {
window.location.reload();
}, 1000);
}
} else {
// 添加收藏
button.textContent = '已藏';
button.title = '已藏,点击取消';
button.classList.add('active');
button.setAttribute('data-is-favorite', '1');
showRssMessage(data.message, 'success');
}
} else {
// 操作失败,恢复原始状态
button.textContent = originalText;
button.title = originalTitle;
showRssMessage(data.message, 'error');
}
})
.catch(error => {
console.error('收藏操作失败:', error);
// 恢复原始状态
button.textContent = originalText;
button.title = originalTitle;
button.disabled = false;
showRssMessage('操作失败: ' + error.message, 'error');
});
}
// 显示统计信息
function showStats() {
showLoading();
// 同时获取缓存统计和RSS定时任务统计
Promise.all([
fetch(window.UrlNavConfig.actionUrl, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'do=getCacheStats',
credentials: 'include'
}).then(r => r.json()),
fetch(window.UrlNavConfig.actionUrl, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'do=getRssCronStats',
credentials: 'include'
}).then(r => r.json())
]).then(([cacheData, cronData]) => {
hideLoading();
if (cacheData.success && cronData.success) {
var cacheStats = cacheData.data;
var rssCronStats = cronData.data;
var html = '';
html += '<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin-bottom: 20px;">';
html += '<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; text-align: center;">';
html += '<div style="font-size: 12px; color: #6c757d; margin-bottom: 5px;">总缓存数</div>';
html += '<div style="font-size: 24px; font-weight: bold; color: #467b96;">' + (cacheStats.total || 0) + '</div>';
html += '</div>';
html += '<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; text-align: center;">';
html += '<div style="font-size: 12px; color: #6c757d; margin-bottom: 5px;">新鲜缓存</div>';
html += '<div style="font-size: 24px; font-weight: bold; color: #28a745;">' + (cacheStats.fresh || 0) + '</div>';
html += '</div>';
html += '<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; text-align: center;">';
html += '<div style="font-size: 12px; color: #6c757d; margin-bottom: 5px;">陈旧缓存</div>';
html += '<div style="font-size: 24px; font-weight: bold; color: #dc3545;">' + (cacheStats.stale || 0) + '</div>';
html += '</div>';
html += '<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; text-align: center;">';
html += '<div style="font-size: 12px; color: #6c757d; margin-bottom: 5px;">活跃RSS源</div>';
html += '<div style="font-size: 24px; font-weight: bold; color: #6f42c1;">' + (cacheStats.url_stats.url_count || 0) + '</div>';
html += '</div>';
html += '<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; text-align: center;">';
html += '<div style="font-size: 12px; color: #6c757d; margin-bottom: 5px;">最后缓存时间</div>';
html += '<div style="font-size: 14px; font-weight: 500;color:#000;">' + (cacheStats.last_cached ? formatBeijingDateJS(cacheStats.last_cached) : '无记录') + '</div>';
html += '</div>';
// 最后自动刷新时间
html += '<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; text-align: center;">';
html += '<div style="font-size: 12px; color: #6c757d; margin-bottom: 5px;">最后自动刷新</div>';
html += '<div style="font-size: 14px; font-weight: 500;color:#000;">' + (rssCronStats.last_executed ? formatBeijingDateJS(rssCronStats.last_executed) : '从未执行') + '</div>';
html += '</div>';
html += '</div>';
showModal('缓存统计信息', html);
} else {
var errorMsg = '';
if (!cacheData.success) errorMsg += '缓存统计:' + cacheData.message + ' ';
if (!cronData.success) errorMsg += '定时任务统计:' + cronData.message;
showRssMessage('获取统计信息失败:' + errorMsg, 'error');
}
}).catch(error => {
hideLoading();
showRssMessage('获取失败:' + error.message, 'error');
});
}
// 显示RSS定时任务统计修复版
function showRssCronStats() {
showLoading();
// 同时获取所有需要的数据
Promise.all([
fetch(window.UrlNavConfig.actionUrl, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'do=getRssCronStats',
credentials: 'include'
}).then(r => r.json()),
fetch(window.UrlNavConfig.actionUrl, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'do=getRefreshLogs&type=failed&pageSize=5',
credentials: 'include'
}).then(r => r.json()),
fetch(window.UrlNavConfig.actionUrl, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'do=getRefreshLogs&type=success&pageSize=5',
credentials: 'include'
}).then(r => r.json())
]).then(([statsData, failedData, successData]) => {
hideLoading();
if (!statsData.success) {
showRssMessage('获取统计信息失败:' + statsData.message, 'error');
return;
}
var stats = statsData.data;
var failures = failedData.success ? failedData.data.logs : [];
var successes = successData.success ? successData.data.logs : [];
var html = '';
// 1. 统计概览
html += '<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; margin-bottom: 20px;">';
html += '<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; text-align: center;">';
html += '<div style="font-size: 12px; color: #6c757d; margin-bottom: 5px;">总执行次数</div>';
html += '<div style="font-size: 24px; font-weight: bold; color: #467b96;">' + (stats.total || 0) + '</div>';
html += '</div>';
html += '<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; text-align: center;">';
html += '<div style="font-size: 12px; color: #6c757d; margin-bottom: 5px;">成功次数</div>';
html += '<div style="font-size: 24px; font-weight: bold; color: #28a745;">' + (stats.success || 0) + '</div>';
html += '</div>';
html += '<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; text-align: center;">';
html += '<div style="font-size: 12px; color: #6c757d; margin-bottom: 5px;">失败次数</div>';
html += '<div style="font-size: 24px; font-weight: bold; color: #dc3545;">' + (stats.failed || 0) + '</div>';
html += '</div>';
html += '<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; text-align: center;">';
html += '<div style="font-size: 12px; color: #6c757d; margin-bottom: 5px;">成功率</div>';
html += '<div style="font-size: 24px; font-weight: bold; color: ' + ((stats.success_rate || 0) > 80 ? '#28a745' : '#ffc107') + ';">' + (stats.success_rate || 0) + '%</div>';
html += '</div>';
html += '</div>';
// 2. 成功率进度条
html += '<div style="margin-bottom: 20px;">';
html += '<div style="display: flex; justify-content: space-between; margin-bottom: 5px;">';
html += '<span style="font-size: 14px;">成功率进度</span>';
html += '<span style="font-size: 14px; font-weight: 500;">' + (stats.success_rate || 0) + '%</span>';
html += '</div>';
html += '<div style="background: #e9ecef; height: 10px; border-radius: 5px; overflow: hidden;">';
html += '<div style="background: ' + ((stats.success_rate || 0) > 80 ? '#28a745' : ((stats.success_rate || 0) > 50 ? '#ffc107' : '#dc3545')) + '; height: 100%; width: ' + (stats.success_rate || 0) + '%;"></div>';
html += '</div>';
html += '</div>';
// 3. 最后执行时间
if (stats.last_executed) {
html += '<div style="margin-bottom: 20px;">';
html += '<div style="font-size: 14px; color: #6c757d; margin-bottom: 5px;">最后执行时间</div>';
html += '<div style="font-size: 16px; font-weight: 500; color: #2c3e50;">' + formatBeijingDateJS(stats.last_executed) + '</div>';
html += '</div>';
}
// 4. 最近失败的网址
if (failures.length > 0) {
html += '<div style="margin-bottom: 25px;">';
html += '<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">';
html += '<h4 style="margin: 0; color: #2c3e50;">最近失败的网址</h4>';
html += '<span style="font-size: 12px; color: #dc3545;">最近24小时内共 ' + failures.length + ' 个</span>';
html += '</div>';
html += '<div style="background: #fff5f5; border: 1px solid #f5c6cb; border-radius: 8px; overflow: hidden;">';
html += '<div style="max-height: 200px; overflow-y: auto;">';
html += '<table style="width: 100%; border-collapse: collapse; font-size: 12px;">';
html += '<thead>';
html += '<tr style="background: #f8d7da; color: #721c24; border-bottom: 1px solid #f5c6cb;">';
html += '<th style="padding: 10px; text-align: left; font-weight: 500;width:100px;">网站名称</th>';
html += '<th style="padding: 10px; text-align: left; font-weight: 500;">错误信息</th>';
// html += '<th style="padding: 10px; text-align: left; font-weight: 500; width: 100px;">操作</th>';
html += '</tr>';
html += '</thead>';
html += '<tbody>';
failures.forEach(function(failure, index) {
var errorMsg = failure.last_error || '未知错误';
if (errorMsg.length > 80) {
errorMsg = errorMsg.substring(0, 80) + '...';
}
html += '<tr style="border-bottom: 1px solid #f8d7da;' + (index % 2 === 0 ? ' background: #fff;' : ' background: #f8f9fa;') + '">';
html += '<td style="padding: 10px; vertical-align: top;">';
html += '<div style="font-weight: 500; color: #2c3e50; margin-bottom: 4px;">' + escapeHtml(failure.site_name || failure.url || '未命名') + '</div>';
if (failure.category_name) {
html += '<span style="background: #e9f7fe; color: #467b96; padding: 2px 6px; border-radius: 3px; font-size: 11px; display: inline-block;">' + escapeHtml(failure.category_name) + '</span>';
}
html += '</td>';
html += '<td style="padding: 10px; vertical-align: top;">';
html += '<div style="color: #dc3545; font-size: 11px;" title="' + escapeHtml(failure.last_error || '') + '">' + escapeHtml(errorMsg) + '</div>';
html += '</td>';
//html += '<td style="padding: 10px; vertical-align: top;">';
//html += '<button type="button" class="action-btn" onclick="retryFailedUrl(\'' + failure.id + '\')" style="padding: 4px 8px; font-size: 11px;">重试</button>';
// html += '</td>';
html += '</tr>';
});
html += '</tbody>';
html += '</table>';
html += '</div>';
html += '</div>';
html += '<div style="display: flex; gap: 10px; margin-top: 10px; justify-content: flex-end;">';
html += '<button type="button" class="action-btn" onclick="showAllFailedUrls()" style="padding: 5px 12px; font-size: 12px;">查看全部失败网址</button>';
// html += '<button type="button" class="action-btn" onclick="retryAllFailedUrls()" style="padding: 5px 12px; font-size: 12px; background: #28a745; color: white;">重试全部</button>';
html += '</div>';
html += '</div>';
}
// 5. 最近成功的网址
if (successes.length > 0) {
html += '<div style="margin-bottom: 25px;">';
html += '<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">';
html += '<h4 style="margin: 0; color: #2c3e50;">最近成功的网址</h4>';
html += '<span style="font-size: 12px; color: #28a745;">最近24小时内共 ' + successes.length + ' 个</span>';
html += '</div>';
html += '<div style="background: #d4edda; border: 1px solid #c3e6cb; border-radius: 8px; overflow: hidden;">';
html += '<div style="max-height: 200px; overflow-y: auto;">';
html += '<table style="width: 100%; border-collapse: collapse; font-size: 12px;">';
html += '<thead>';
html += '<tr style="background: #c3e6cb; color: #155724; border-bottom: 1px solid #b1dfbb;">';
html += '<th style="padding: 10px; text-align: left; font-weight: 500;">网站名称</th>';
html += '<th style="padding: 10px; text-align: left; font-weight: 500;">RSS地址</th>';
html += '<th style="padding: 10px; text-align: left; font-weight: 500; width: 120px;">最后刷新</th>';
html += '</tr>';
html += '</thead>';
html += '<tbody>';
successes.forEach(function(success, index) {
var rssUrl = success.rss_url || '';
if (rssUrl.length > 60) {
rssUrl = rssUrl.substring(0, 60) + '...';
}
html += '<tr style="border-bottom: 1px solid #c3e6cb;' + (index % 2 === 0 ? ' background: #fff;' : ' background: #f8f9fa;') + '">';
html += '<td style="padding: 10px; vertical-align: top;">';
html += '<div style="font-weight: 500; color: #155724; margin-bottom: 4px;">' + escapeHtml(success.site_name || success.url || '未命名') + '</div>';
if (success.category_name) {
html += '<span style="background: #d1ecf1; color: #0c5460; padding: 2px 6px; border-radius: 3px; font-size: 11px; display: inline-block;">' + escapeHtml(success.category_name) + '</span>';
}
html += '</td>';
html += '<td style="padding: 10px; vertical-align: top;">';
html += '<div style="color: #155724; font-family: monospace; font-size: 11px;" title="' + escapeHtml(success.rss_url) + '">' + escapeHtml(rssUrl) + '</div>';
html += '</td>';
html += '<td style="padding: 10px; vertical-align: top; color: #6c757d; font-size: 11px;">';
html += success.last_refresh_beijing || formatBeijingDateJS(success.last_refresh);
html += '</td>';
html += '</tr>';
});
html += '</tbody>';
html += '</table>';
html += '</div>';
html += '</div>';
html += '<div style="display: flex; gap: 10px; margin-top: 10px; justify-content: flex-end;">';
html += '<button type="button" class="action-btn" onclick="showAllSuccessUrls()" style="padding: 5px 12px; font-size: 12px; background: #28a745; color: white;">查看全部成功网址</button>';
html += '</div>';
html += '</div>';
}
// 6. 如果没有数据
if (failures.length === 0 && successes.length === 0) {
html += '<div style="text-align: center; padding: 40px; color: #6c757d; background: #f8f9fa; border-radius: 8px; margin-bottom: 20px;">';
html += '<div style="font-size: 48px; margin-bottom: 15px;">📊</div>';
html += '<div style="font-weight: 500; margin-bottom: 10px;">暂无最近刷新记录</div>';
html += '<div style="font-size: 13px;">最近24小时内没有成功或失败的刷新记录</div>';
html += '</div>';
}
// 7. 底部操作按钮
html += '<div style="display: flex; gap: 10px; margin-top: 20px; border-top: 1px solid #e8ebf0; padding-top: 20px;">';
html += '<button type="button" class="action-btn" onclick="showRssCronLogs()">查看详细日志</button>';
//html += '<button type="button" class="action-btn" onclick="loadRssCronStats()" style="background: #467b96; color: white;">刷新统计</button>';
html += '</div>';
showModal('RSS定时任务统计', html);
}).catch(error => {
hideLoading();
showRssMessage('获取统计信息失败:' + error.message, 'error');
});
}
// 查看网址详情
function viewUrlDetails(urlId) {
showLoading();
fetch(window.UrlNavConfig.actionUrl, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'do=getUrlDetails&url_id=' + urlId,
credentials: 'include'
}).then(response => response.json()).then(data => {
hideLoading();
if (data.success) {
var url = data.data;
var html = '';
html += '<div style="max-width: 500px;">';
html += '<h4 style="margin-top: 0; color: #2c3e50;">网址详情</h4>';
html += '<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 15px;">';
html += '<div style="margin-bottom: 10px;">';
html += '<div style="font-size: 12px; color: #6c757d;">网站名称</div>';
html += '<div style="font-weight: 500; color: #2c3e50;">' + escapeHtml(url.title || url.url || '未命名') + '</div>';
html += '</div>';
html += '<div style="margin-bottom: 10px;">';
html += '<div style="font-size: 12px; color: #6c757d;">网站地址</div>';
html += '<div><a href="' + escapeHtml(url.url || '#') + '" target="_blank" style="color: #467b96; text-decoration: none;">' + escapeHtml(url.url || '') + '</a></div>';
html += '</div>';
html += '<div style="margin-bottom: 10px;">';
html += '<div style="font-size: 12px; color: #6c757d;">RSS地址</div>';
html += '<div><a href="' + escapeHtml(url.rss_url || '#') + '" target="_blank" style="color: #467b96; text-decoration: none;">' + escapeHtml(url.rss_url || '') + '</a></div>';
html += '</div>';
if (url.category_name) {
html += '<div style="margin-bottom: 10px;">';
html += '<div style="font-size: 12px; color: #6c757d;">分类</div>';
html += '<div><span style="background: #e9f7fe; color: #467b96; padding: 4px 8px; border-radius: 4px; font-size: 12px;">' + escapeHtml(url.category_name) + '</span></div>';
html += '</div>';
}
if (url.star_rating) {
html += '<div style="margin-bottom: 10px;">';
html += '<div style="font-size: 12px; color: #6c757d;">星级</div>';
html += '<div style="color: #ffc107;">';
for (var i = 1; i <= 3; i++) {
html += (i <= url.star_rating) ? '★' : '☆';
}
html += '</div>';
html += '</div>';
}
if (url.last_refresh) {
html += '<div style="margin-bottom: 10px;">';
html += '<div style="font-size: 12px; color: #6c757d;">最后刷新时间</div>';
html += '<div>' + formatBeijingDateJS(url.last_refresh) + '</div>';
html += '</div>';
}
if (url.last_error) {
html += '<div style="margin-bottom: 10px;">';
html += '<div style="font-size: 12px; color: #6c757d;">最后错误</div>';
html += '<div style="color: #dc3545; font-size: 12px;">' + escapeHtml(url.last_error) + '</div>';
html += '</div>';
}
html += '</div>';
html += '<div style="display: flex; gap: 10px; justify-content: flex-end;">';
html += '<button type="button" class="action-btn" onclick="showRssCronStats()">返回统计</button>';
html += '<button type="button" class="action-btn" onclick="testRssUrl(\'' + url.id + '\')" style="background: #28a745; color: white;">测试RSS</button>';
html += '</div>';
html += '</div>';
showModal('网址详情', html);
} else {
showRssMessage('获取网址详情失败: ' + data.message, 'error');
}
}).catch(error => {
hideLoading();
showRssMessage('获取失败: ' + error.message, 'error');
});
}
// 测试RSS网址
function testRssUrl(urlId) {
showLoading();
fetch(window.UrlNavConfig.actionUrl, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'do=testRssUrl&url_id=' + urlId,
credentials: 'include'
}).then(response => response.json()).then(data => {
hideLoading();
if (data.success) {
var result = data.data;
var html = '';
html += '<div style="max-width: 500px;">';
html += '<h4 style="margin-top: 0; color: #2c3e50;">RSS测试结果</h4>';
if (result.success) {
html += '<div style="background: #d4edda; padding: 15px; border-radius: 8px; margin-bottom: 15px;">';
html += '<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;">';
html += '<div style="font-size: 24px; color: #28a745;">✓</div>';
html += '<div style="font-weight: 500; color: #155724;">测试成功</div>';
html += '</div>';
if (result.feed_title) {
html += '<div style="margin-bottom: 8px;">';
html += '<div style="font-size: 12px; color: #6c757d;">RSS标题</div>';
html += '<div style="font-weight: 500;">' + escapeHtml(result.feed_title) + '</div>';
html += '</div>';
}
if (result.feed_description) {
html += '<div style="margin-bottom: 8px;">';
html += '<div style="font-size: 12px; color: #6c757d;">RSS描述</div>';
html += '<div style="font-size: 13px;">' + escapeHtml(result.feed_description.substring(0, 100)) + (result.feed_description.length > 100 ? '...' : '') + '</div>';
html += '</div>';
}
if (result.item_count > 0) {
html += '<div style="margin-bottom: 8px;">';
html += '<div style="font-size: 12px; color: #6c757d;">文章数量</div>';
html += '<div>' + result.item_count + ' 篇文章</div>';
html += '</div>';
}
if (result.latest_article) {
html += '<div>';
html += '<div style="font-size: 12px; color: #6c757d;">最新文章</div>';
html += '<div style="font-size: 13px;">' + escapeHtml(result.latest_article.substring(0, 80)) + (result.latest_article.length > 80 ? '...' : '') + '</div>';
html += '</div>';
}
html += '</div>';
} else {
html += '<div style="background: #f8d7da; padding: 15px; border-radius: 8px; margin-bottom: 15px;">';
html += '<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;">';
html += '<div style="font-size: 24px; color: #dc3545;">✗</div>';
html += '<div style="font-weight: 500; color: #721c24;">测试失败</div>';
html += '</div>';
if (result.error) {
html += '<div>';
html += '<div style="font-size: 12px; color: #721c24;">错误信息</div>';
html += '<div style="font-size: 13px; color: #dc3545;">' + escapeHtml(result.error) + '</div>';
html += '</div>';
}
html += '</div>';
}
html += '<div style="display: flex; gap: 10px; justify-content: flex-end;">';
html += '<button type="button" class="action-btn" onclick="showRssCronStats()">返回统计</button>';
html += '</div>';
html += '</div>';
showModal('RSS测试结果', html);
} else {
showRssMessage('测试失败: ' + data.message, 'error');
}
}).catch(error => {
hideLoading();
showRssMessage('测试失败: ' + error.message, 'error');
});
}
// 查看所有成功网址
function showAllSuccessUrls() {
showLoading();
fetch(window.UrlNavConfig.actionUrl, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'do=getRefreshLogs&type=success&pageSize=50',
credentials: 'include'
}).then(response => response.json()).then(data => {
hideLoading();
if (data.success) {
var successes = data.data.logs;
var html = '';
if (successes.length === 0) {
html += '<div style="text-align: center; padding: 30px; color: #6c757d;">暂无成功网址记录</div>';
} else {
html += '<div style="margin-bottom: 15px; color: #6c757d; font-size: 13px;">';
html += '共找到 ' + successes.length + ' 个成功网址';
html += '</div>';
html += '<div style="max-height: 400px; overflow-y: auto;">';
html += '<table style="width: 100%; border-collapse: collapse; font-size: 12px;">';
html += '<thead>';
html += '<tr style="background: #d4edda; border-bottom: 2px solid #c3e6cb;">';
html += '<th style="padding: 10px; text-align: left;">网站名称</th>';
html += '<th style="padding: 10px; text-align: left;">RSS地址</th>';
html += '<th style="padding: 10px; text-align: left;">最后刷新</th>';
html += '<th style="padding: 10px; text-align: left;">状态</th>';
html += '</tr>';
html += '</thead>';
html += '<tbody>';
successes.forEach(function(success, index) {
var lastRefresh = success.last_refresh_beijing || formatBeijingDateJS(success.last_refresh);
html += '<tr style="border-bottom: 1px solid #c3e6cb;' + (index % 2 === 0 ? ' background: #fff;' : ' background: #f8f9fa;') + '">';
html += '<td style="padding: 10px; vertical-align: top;">';
html += '<div style="font-weight: 500; color: #155724; margin-bottom: 4px;">' + escapeHtml(success.site_name || success.url || '未命名') + '</div>';
if (success.category_name) {
html += '<span style="background: #d1ecf1; color: #0c5460; padding: 2px 6px; border-radius: 3px; font-size: 11px; display: inline-block;">' + escapeHtml(success.category_name) + '</span>';
}
html += '</td>';
html += '<td style="padding: 10px; vertical-align: top;">';
html += '<div style="color: #155724; font-family: monospace; font-size: 11px; word-break: break-all;" title="' + escapeHtml(success.rss_url) + '">' + escapeHtml(success.rss_url) + '</div>';
html += '</td>';
html += '<td style="padding: 10px; vertical-align: top; color: #6c757d; font-size: 11px;">';
html += lastRefresh;
html += '</td>';
html += '<td style="padding: 10px; vertical-align: top;">';
html += '<span style="color: #28a745; font-size: 11px; font-weight: 500;">成功</span>';
html += '</td>';
html += '</tr>';
});
html += '</tbody>';
html += '</table>';
html += '</div>';
}
html += '<div style="display: flex; justify-content: space-between; margin-top: 20px;">';
//html += '<button type="button" class="action-btn" onclick="exportSuccessUrls()" style="padding: 8px 16px; background: #28a745; color: white;">导出列表</button>';
html += '<button type="button" class="action-btn" onclick="showRssCronStats()" style="padding: 8px 16px;">返回统计</button>';
html += '</div>';
showModal('所有成功网址', html);
} else {
showRssMessage('获取成功网址列表失败:' + data.message, 'error');
}
}).catch(error => {
hideLoading();
showRssMessage('获取失败:' + error.message, 'error');
});
}
// 导出成功网址
function exportSuccessUrls() {
showLoading();
fetch(window.UrlNavConfig.actionUrl, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'do=exportSuccessUrls',
credentials: 'include'
}).then(response => response.json()).then(data => {
hideLoading();
if (data.success && data.csv_data) {
// 创建并下载CSV文件
const blob = new Blob([data.csv_data], { type: 'text/csv;charset=utf-8' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `success-urls-${new Date().toISOString().slice(0,10)}.csv`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
showRssMessage('成功网址列表已导出', 'success');
} else {
showRssMessage('导出失败:' + data.message, 'error');
}
}).catch(error => {
hideLoading();
showRssMessage('导出失败:' + error.message, 'error');
});
}
// 重试单个失败网址
function retryFailedUrl(urlId) {
if (!confirm('确定要重试这个网址的RSS刷新吗')) {
return;
}
showLoading();
fetch(window.UrlNavConfig.actionUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'do=retryRssUrl&url_id=' + urlId,
credentials: 'include'
})
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success) {
showRssMessage('重试成功:' + data.message, 'success');
// 刷新统计信息
setTimeout(() => {
loadRssCronStats();
if (typeof showRssCronStats === 'function') {
showRssCronStats();
}
}, 1000);
} else {
showRssMessage('重试失败:' + data.message, 'error');
}
})
.catch(error => {
hideLoading();
showRssMessage('重试失败:' + error.message, 'error');
});
}
// 忽略失败网址(标记为不活跃)
function ignoreFailedUrl(urlId) {
if (!confirm('确定要忽略这个网址吗?\n\n忽略后该网址将不再参与自动刷新。您可以在网址管理中重新启用它。')) {
return;
}
showLoading();
fetch(window.UrlNavConfig.actionUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'do=ignoreFailedUrl&url_id=' + urlId,
credentials: 'include'
})
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success) {
showRssMessage('已忽略该网址', 'success');
// 刷新统计信息
setTimeout(() => {
loadRssCronStats();
if (typeof showRssCronStats === 'function') {
showRssCronStats();
}
}, 1000);
} else {
showRssMessage('操作失败:' + data.message, 'error');
}
})
.catch(error => {
hideLoading();
showRssMessage('操作失败:' + error.message, 'error');
});
}
// 重试所有失败网址
function retryAllFailedUrls() {
if (!confirm('确定要重试所有失败的RSS网址吗\n\n这可能需要一些时间。')) {
return;
}
showLoading();
fetch(window.UrlNavConfig.actionUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'do=retryAllFailedUrls',
credentials: 'include'
})
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success) {
showRssMessage('批量重试完成:' + data.message, 'success');
// 刷新统计信息
setTimeout(() => {
loadRssCronStats();
if (typeof showRssCronStats === 'function') {
showRssCronStats();
}
}, 2000);
} else {
showRssMessage('批量重试失败:' + data.message, 'error');
}
})
.catch(error => {
hideLoading();
showRssMessage('批量重试失败:' + error.message, 'error');
});
}
// 查看所有失败网址
function showAllFailedUrls() {
showLoading();
fetch(window.UrlNavConfig.actionUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'do=getRefreshLogs&type=failed&pageSize=50',
credentials: 'include'
})
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success) {
var failures = data.data.logs;
var html = '';
if (failures.length === 0) {
html += '<div style="text-align: center; padding: 30px; color: #6c757d;">暂无失败网址记录</div>';
} else {
html += '<div style="margin-bottom: 15px; color: #6c757d; font-size: 13px;">';
html += '共找到 ' + failures.length + ' 个失败网址';
html += '</div>';
html += '<div style="max-height: 400px; overflow-y: auto;">';
html += '<table style="width: 100%; border-collapse: collapse; font-size: 12px;">';
html += '<thead>';
html += '<tr style="background: #f8f9fa; border-bottom: 2px solid #dee2e6;">';
html += '<th style="padding: 10px; text-align: left;color:#000;">网站名称</th>';
html += '<th style="padding: 10px; text-align: left;color:#000;width:100px;">RSS地址</th>';
html += '<th style="padding: 10px; text-align: left;color:#000;width:80px;">最后刷新</th>';
html += '<th style="padding: 10px; text-align: left;color:#000;">错误原因</th>';
//html += '<th style="padding: 10px; text-align: left;color:#000;">操作</th>';
html += '</tr>';
html += '</thead>';
html += '<tbody>';
failures.forEach(function(failure, index) {
var lastRefresh = failure.last_refresh_beijing || formatBeijingDateJS(failure.last_refresh);
var errorMsg = failure.last_error || '未知错误';
var statusClass = failure.is_active ? 'status-active' : 'status-inactive';
var statusText = failure.is_active ? '活跃' : '已停用';
html += '<tr style="border-bottom: 1px solid #f0f2f5;' + (!failure.is_active ? ' background: #f8f9fa; opacity: 0.7;' : '') + '">';
html += '<td style="padding: 10px; vertical-align: top;">';
html += '<div style="font-weight: 500; margin-bottom: 4px;color:#000;">' + escapeHtml(failure.site_name || failure.url || '未命名') + '</div>';
if (failure.category_name) {
html += '<span style="background: #e9f7fe; color: #467b96; padding: 2px 6px; border-radius: 3px; font-size: 11px; display: inline-block;">' + escapeHtml(failure.category_name) + '</span>';
}
html += '</td>';
html += '<td style="padding: 10px; vertical-align: top;">';
html += '<div style="color: #467b96; font-family: monospace; font-size: 11px; word-break: break-all;" title="' + escapeHtml(failure.rss_url) + '">' + escapeHtml(failure.rss_url) + '</div>';
html += '</td>';
html += '<td style="padding: 10px; vertical-align: top;">';
html += '<div style="color: #6c757d; font-size: 11px;">' + lastRefresh + '</div>';
if (!failure.is_active) {
html += '<span style="background: #6c757d; color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; display: inline-block; margin-top: 3px;">已停用</span>';
}
html += '</td>';
html += '<td style="padding: 10px; vertical-align: top;">';
html += '<div style="color: #dc3545; font-size: 11px;" title="' + escapeHtml(errorMsg) + '">' + escapeHtml(errorMsg.length > 100 ? errorMsg.substring(0, 100) + '...' : errorMsg) + '</div>';
html += '</td>';
// html += '<td style="padding: 10px; vertical-align: top;">';
// html += '<div style="display: flex; flex-direction: column; gap: 5px;">';
// if (failure.is_active) {
// html += '<button type="button" class="action-btn" onclick="retryFailedUrl(\'' + failure.id + '\')" style="padding: 4px 8px; font-size: 11px;">重试</button>';
//html += '<button type="button" class="action-btn" onclick="ignoreFailedUrl(\'' + failure.id + '\')" style="padding: 4px 8px; font-size: 11px; background: #6c757d; color: white;">停用</button>';
// } else {
// html += '<button type="button" class="action-btn" onclick="enableFailedUrl(\'' + failure.id + '\')" style="padding: 4px 8px; font-size: 11px; background: #28a745; color: white;">启用</button>';
// }
// html += '</div>';
//html += '</td>';
html += '</tr>';
});
html += '</tbody>';
html += '</table>';
html += '</div>';
}
html += '<div style="display: flex; justify-content: space-between; margin-top: 20px;">';
//html += '<button type="button" class="action-btn" onclick="exportFailedUrls()" style="padding: 8px 16px;">导出列表</button>';
html += '<button type="button" class="action-btn" onclick="showRssCronStats()" style="padding: 8px 16px;">返回统计</button>';
html += '</div>';
showModal('所有失败网址', html);
} else {
showRssMessage('获取失败网址列表失败:' + data.message, 'error');
}
})
.catch(error => {
hideLoading();
showRssMessage('获取失败:' + error.message, 'error');
});
}
// 启用已停用的失败网址
function enableFailedUrl(urlId) {
if (!confirm('确定要重新启用这个网址吗?')) {
return;
}
showLoading();
fetch(window.UrlNavConfig.actionUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'do=enableFailedUrl&url_id=' + urlId,
credentials: 'include'
})
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success) {
showRssMessage('网址已重新启用', 'success');
// 刷新列表
setTimeout(() => {
showAllFailedUrls();
}, 1000);
} else {
showRssMessage('启用失败:' + data.message, 'error');
}
})
.catch(error => {
hideLoading();
showRssMessage('启用失败:' + error.message, 'error');
});
}
// 导出失败网址列表
function exportFailedUrls() {
showLoading();
fetch(window.UrlNavConfig.actionUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'do=getAllFailedUrls',
credentials: 'include'
})
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success && data.csv_data) {
// 创建并下载CSV文件
const blob = new Blob([data.csv_data], { type: 'text/csv;charset=utf-8' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `failed-urls-${new Date().toISOString().slice(0,10)}.csv`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
showRssMessage('失败网址列表已导出', 'success');
} else {
showRssMessage('导出失败:' + data.message, 'error');
}
})
.catch(error => {
hideLoading();
showRssMessage('导出失败:' + error.message, 'error');
});
}
// 显示RSS定时任务日志两行显示版
function showRssCronLogs() {
showLoading();
fetch(window.UrlNavConfig.actionUrl, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'do=getCronLogs&type=rss&limit=50',
credentials: 'include'
})
.then(res => res.json())
.then(data => {
hideLoading();
if (!data.success) {
showRssMessage('获取日志失败:' + data.message, 'error');
return;
}
var logs = data.data || [];
var html = '';
if (logs.length === 0) {
html += '<div style="text-align: center; padding: 30px; color: #6c757d;">暂无RSS定时任务日志</div>';
} else {
html += '<div style="background: #f8f9fa;padding: 15px;border-radius: 8px;">';
html += '<table style="width: 100%; border-collapse: collapse; font-size: 13px;">';
html += '<thead>';
html += '<tr style="background: #f8f9fa; border-bottom: 2px solid #dee2e6;">';
html += '<th style="padding: 12px; text-align: left; color: #6c757d; width: 150px;">时间</th>';
html += '<th style="padding: 12px; text-align: left; color: #6c757d; width: 100px;">类型</th>';
html += '<th style="padding: 12px; text-align: left; color: #6c757d;width:200px;">结果详情</th>';
html += '<th style="padding: 12px; text-align: left; color: #6c757d; width: 80px;">状态</th>';
html += '</tr>';
html += '</thead>';
html += '<tbody>';
logs.forEach(function(log, index) {
var result = {};
try {
if (log.result) {
result = JSON.parse(log.result);
}
} catch (e) {
console.error('解析result失败:', e);
}
var hasError = log.error_message && log.error_message.trim() !== '';
// 第一行:基本信息
html += '<tr style="border-bottom: 1px solid #f0f2f5;' + (hasError ? ' background: #fff3cd;' : '') + '">';
html += '<td style="padding: 12px;color:#000; vertical-align: center;;">' + formatBeijingDateJS(log.executed_time) + '</td>';
html += '<td style="padding: 12px; vertical-align: center;">';
html += '<span style="background: #e9f7fe; color: #467b96; padding: 4px 10px; border-radius: 4px; display: inline-block; font-size: 12px;">' + log.cron_type + '</span>';
html += '</td>';
// 第一行:结果文本
var resultText = '';
if (result.message) {
// 移除 "刷新完成:" 前缀
resultText = result.message.replace('刷新完成:', '');
} else {
var successCount = result.successCount || result.successcount || 0;
var failureCount = result.failureCount || result.failurecount || 0;
resultText = '成功 ' + successCount + ' 个,失败 ' + failureCount + ' 个';
}
html += '<td style="padding: 12px;color:#000; vertical-align: center;">';
html += '<div style="font-weight: 500; color: #343a40;">' + resultText + '</div>';
html += '</td>';
// 状态列
if (hasError) {
html += '<td style="padding: 12px; vertical-align: center;">';
html += '<span style="color: #dc3545; font-weight: 500; background: #f8d7da; padding: 4px 10px; border-radius: 4px; display: inline-block; font-size: 12px;">失败</span>';
html += '</td>';
} else {
html += '<td style="padding: 12px; vertical-align: center;">';
html += '<span style="color: #28a745; font-weight: 500; background: #d4edda; padding: 4px 10px; border-radius: 4px; display: inline-block; font-size: 12px;">成功</span>';
html += '</td>';
}
html += '</tr>';
// 第二行RSS地址详情跨列显示
var successRssUrls = result.successRssUrls || [];
var failedRssUrls = result.failedRssUrls || [];
if (successRssUrls.length > 0 || failedRssUrls.length > 0) {
html += '<tr style="border-bottom: 1px solid #f0f2f5;">';
html += '<td colspan="4" style="padding: 10px;">';
// 成功地址
if (successRssUrls.length > 0) {
html += '<div style="margin-bottom: 12px;">';
html += '<div style="color: #28a745; font-weight: 500; margin-bottom: 6px; font-size: 12px; display: flex; align-items: center;">';
html += '<span style="background: #28a745; color: white; width: 20px; height: 20px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-right: 8px; font-size: 10px;">✓</span>';
html += '成功地址 (' + successRssUrls.length + '个)';
html += '</div>';
html += '<div style="display: flex; flex-wrap: wrap; gap: 6px;">';
successRssUrls.forEach(function(url, idx) {
var displayUrl = url;
if (displayUrl.length > 80) {
displayUrl = displayUrl.substring(0, 80) + '...';
}
html += '<span style="background: #f8f9fa; color: #155724; padding: 4px 10px; border-radius: 4px; font-size: 11px; border: 1px solid #d4edda;" title="' + escapeHtml(url) + '">';
html += escapeHtml(displayUrl);
html += '</span>';
});
html += '</div>';
html += '</div>';
}
// 失败地址
if (failedRssUrls.length > 0) {
html += '<div>';
html += '<div style="color: #dc3545; font-weight: 500; margin-bottom: 6px; font-size: 12px; display: flex; align-items: center;">';
html += '<span style="background: #dc3545; color: white; width: 20px; height: 20px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-right: 8px; font-size: 10px;">✗</span>';
html += '失败地址 (' + failedRssUrls.length + '个)';
html += '</div>';
html += '<div style="display: flex; flex-wrap: wrap; gap: 6px;">';
failedRssUrls.forEach(function(url, idx) {
var displayUrl = url;
// 处理异常标记
if (url.includes('[异常]')) {
displayUrl = url.replace(' [异常]', '');
}
if (displayUrl.length > 80) {
displayUrl = displayUrl.substring(0, 80) + '...';
}
if (url.includes('[异常]')) {
displayUrl += ' [异常]';
}
html += '<span style="background: #f8f9fa; color: #721c24; padding: 4px 10px; border-radius: 4px; font-size: 11px; border: 1px solid #f8d7da; " title="' + escapeHtml(url) + '">';
html += escapeHtml(displayUrl);
html += '</span>';
});
html += '</div>';
html += '</div>';
}
html += '</td>';
html += '</tr>';
}
});
html += '</tbody>';
html += '</table>';
html += '</div>';
}
html += '<div style="display: flex; gap: 10px; margin-top: 20px;">';
html += '<button type="button" class="action-btn" onclick="showRssCronStats()">查看统计</button>';
html += '<span style="color: #6c757d; font-size: 12px; margin-left: auto; padding: 5px 10px; background: #f8f9fa; border-radius: 3px;">';
html += '📋 网址列表独立显示鼠标悬停查看完整URL';
html += '</span>';
html += '</div>';
showModal('RSS定时任务日志', html);
})
.catch(error => {
hideLoading();
showRssMessage('获取失败:' + error.message, 'error');
});
}
// JavaScript中格式化北京时间
function formatBeijingDateJS(datetime) {
if (!datetime) return '';
var date = new Date(datetime);
// 转换为北京时间(+8小时
var beijingTime = new Date(date.getTime() + (8 * 60 * 60 * 1000));
return beijingTime.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).replace(/\//g, '-');
}
// 显示模态框
function showModal(title, content) {
var modal = document.getElementById('stats-modal');
var modalTitle = document.getElementById('modal-title');
var modalContent = document.getElementById('modal-content');
modalTitle.textContent = title;
modalContent.innerHTML = content;
modal.style.display = 'flex';
}
// 关闭模态框
function closeModal() {
var modal = document.getElementById('stats-modal');
modal.style.display = 'none';
}
// 绑定刷新按钮事件
function bindRefreshButton(buttonId) {
var btn = document.getElementById(buttonId);
if (btn) {
btn.addEventListener('click', function(e) {
e.preventDefault();
if (confirm('确定要手动刷新RSS信息吗\n\n这将获取所有RSS源的最新文章可能需要一些时间。')) {
refreshRss();
}
});
}
}
// ==================== 新增:阅读全文功能相关函数 ====================
// 显示阅读全文弹窗
function showFullContent(articleData) {
var modal = document.getElementById('full-content-modal');
var titleElement = document.getElementById('full-content-title');
var siteTitleElement = document.getElementById('full-content-site-title');
var originalTitleElement = document.getElementById('full-content-original-title');
var metaElement = document.getElementById('full-content-meta');
var bodyElement = document.getElementById('full-content-body');
var statusElement = document.getElementById('full-content-status');
// 设置标题
var articleTitle = articleData.title || '无标题';
var titleText = (articleTitle.length > 50 ? articleTitle.substring(0, 50) + '...' : articleTitle);
titleElement.textContent = titleText;
siteTitleElement.textContent = '来源:' + (articleData.site_title || '未知网站');
originalTitleElement.textContent = articleTitle;
// 设置元信息
var metaHtml = '';
if (articleData.category) {
metaHtml += '<div class="meta-item"><span>分类:</span><i>' + escapeHtml(articleData.category) + '</i></div>';
}
if (articleData.pub_date) {
metaHtml += '<div class="meta-item"><span>发布时间:</span><i>' + escapeHtml(articleData.pub_date) + '</i></div>';
}
if (articleData.site_url) {
var siteUrl = articleData.site_url || '#';
var siteTitle = articleData.site_title || articleData.site_url || '访问网站';
metaHtml += '<div class="meta-item"><span>网站:</span><i><a href="' + escapeHtml(siteUrl) + '" target="_blank" style="color: #467b96; text-decoration: none;">' + escapeHtml(siteTitle) + '</a></i></div>';
}
metaElement.innerHTML = metaHtml;
// 处理文章内容
var content = articleData.full_content || articleData.description || '暂无内容';
// 清理和格式化内容
var formattedContent = formatArticleContent(content);
bodyElement.innerHTML = formattedContent;
// 更新状态
var contentLength = content.length;
var wordCount = contentLength > 0 ? Math.ceil(contentLength / 3) : 0;
statusElement.innerHTML = '字数:' + wordCount;
// 存储当前文章数据供其他函数使用
window.currentArticleData = articleData;
// 显示弹窗
modal.style.display = 'flex';
// 滚动到顶部
var contentArea = modal.querySelector('.full-content-body');
if (contentArea) {
contentArea.scrollTop = 0;
}
// 添加键盘快捷键
document.addEventListener('keydown', handleFullContentKeyboard);
}
// 关闭阅读全文弹窗
function closeFullContentModal() {
var modal = document.getElementById('full-content-modal');
modal.style.display = 'none';
window.currentArticleData = null;
// 移除键盘快捷键
document.removeEventListener('keydown', handleFullContentKeyboard);
}
// 处理阅读全文弹窗的键盘快捷键
function handleFullContentKeyboard(e) {
if (e.key === 'Escape') {
closeFullContentModal();
} else if (e.key === 'c' && (e.ctrlKey || e.metaKey)) {
copyFullContentLink();
e.preventDefault();
} else if (e.key === 'o' && (e.ctrlKey || e.metaKey)) {
openOriginalLink();
e.preventDefault();
}
}
// 复制文章链接
function copyFullContentLink() {
if (!window.currentArticleData || !window.currentArticleData.link) {
showRssMessage('无法获取文章链接', 'warning');
return;
}
copyToClipboard(window.currentArticleData.link);
}
// 打开原文链接
function openOriginalLink() {
if (!window.currentArticleData || !window.currentArticleData.link) {
showRssMessage('无法获取文章链接', 'warning');
return;
}
window.open(window.currentArticleData.link, '_blank');
}
// 格式化文章内容 - 安全渲染HTML版本
function formatArticleContent(content) {
if (!content || content.trim() === '') {
return '<div style="text-align: center; padding: 40px; color: #6c757d;"><p>暂无内容</p></div>';
}
try {
// 先清理内容但保留安全的HTML标签
content = content.trim();
// 移除可能有害的标签和属性
content = sanitizeHtml(content);
// 处理段落 - 确保<p>标签正确闭合
content = content.replace(/<p>/g, '<p style="margin-bottom: 15px; line-height: 1.6;">');
// 处理图片 - 添加懒加载和响应式
content = content.replace(/<img([^>]+)>/gi, function(match, attrs) {
// 提取src
var srcMatch = attrs.match(/src=["']([^"']+)["']/);
var altMatch = attrs.match(/alt=["']([^"']+)["']/);
var src = srcMatch ? srcMatch[1] : '';
var alt = altMatch ? altMatch[1] : '图片';
if (src) {
return '<img src="' + src + '" alt="' + alt + '" ' +
'style="max-width: 100%; height: auto; border-radius: 6px; margin: 10px 0; display: block;" ' +
'loading="lazy" onerror="this.style.display=\'none\'">';
}
return match;
});
// 处理链接 - 添加安全属性
content = content.replace(/<a([^>]+)>/gi, function(match, attrs) {
var hrefMatch = attrs.match(/href=["']([^"']+)["']/);
if (hrefMatch) {
var href = hrefMatch[1];
// 确保外部链接在新窗口打开
if (href.startsWith('http')) {
return '<a href="' + href + '" target="_blank" rel="noopener noreferrer"' +
' style="color: #467b96; text-decoration: none;">';
}
}
return '<a href="#" style="color: #467b96; text-decoration: none;">';
});
// 处理引用块
content = content.replace(/<blockquote>/g,
'<blockquote style="border-left: 4px solid #467b96; padding-left: 15px; margin: 15px 0; color: #6c757d; font-style: italic; background: #f8f9fa; padding: 12px 15px;">');
// 处理代码
content = content.replace(/<code>/g,
'<code style="background: #f5f7fa; padding: 2px 6px; border-radius: 4px; font-family: monospace; font-size: 0.9em; color: #467b96;">');
// 处理预格式化文本
content = content.replace(/<pre>/g,
'<pre style="background: #2c3e50; color: #ecf0f1; padding: 15px; border-radius: 6px; overflow-x: auto; margin: 15px 0; font-family: monospace; font-size: 13px; line-height: 1.4;">');
// 添加响应式容器
content = '<div style="max-width: 100%; overflow-wrap: break-word; word-wrap: break-word;">' +
content +
'</div>';
return content;
} catch (error) {
console.error('格式化文章内容时出错:', error);
// 出错时显示原始内容(安全转义后)
return '<div style="background: #f8f9fa; padding: 15px; border-radius: 6px; white-space: pre-wrap; font-family: monospace; font-size: 13px;">' +
escapeHtml(content) +
'</div>';
}
}
// 安全的HTML清理函数
function sanitizeHtml(html) {
// 只允许安全的标签和属性
var allowedTags = {
'p': ['style', 'class'],
'br': [],
'b': [], 'strong': [],
'i': [], 'em': [],
'u': [],
'a': ['href', 'target', 'rel', 'style', 'title'],
'img': ['src', 'alt', 'title', 'style', 'loading'],
'ul': ['style'],
'ol': ['style'],
'li': ['style'],
'blockquote': ['style'],
'code': ['style'],
'pre': ['style'],
'h1': ['style'], 'h2': ['style'], 'h3': ['style'], 'h4': ['style'], 'h5': ['style'], 'h6': ['style'],
'div': ['style'],
'span': ['style']
};
// 简单的标签清理实际项目应该使用更严格的库如DOMPurify
// 这里做一个基本的安全处理
html = html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '');
html = html.replace(/<style\b[^>]*>[\s\ S]*?<\/style>/gi, '');
html = html.replace(/on\w+="[^"]*"/gi, '');
html = html.replace(/on\w+='[^']*'/gi, '');
html = html.replace(/javascript:/gi, '');
html = html.replace(/data:/gi, '');
return html;
}
// 更新escapeHtml函数不再转义所有HTML
function escapeHtml(text) {
if (!text) return '';
// 只转义真正危险的字符保留HTML标签
var map = {
'&': '&amp;',
'<': '&lt;', // 这行会导致HTML标签被转义但我们需要它
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
// 临时解决方案:先不转义<和>让HTML标签正常工作
// return String(text).replace(/[&<>"']/g, function(m) { return map[m]; });
// 简单转义但保留HTML标签
text = String(text);
text = text.replace(/&(?!(amp|lt|gt|quot|#039);)/g, '&amp;');
text = text.replace(/"/g, '&quot;');
text = text.replace(/'/g, '&#039;');
return text;
}
// ==================== 新增:文章状态管理功能 ====================
// 初始化状态管理
function initStatusManagement() {
// 从LocalStorage加载所有状态
loadFeedStatuses();
// 绑定状态选择器点击事件
bindStatusSelectors();
// 初始化状态筛选
updateStatusFilter();
// 添加行选择功能
addRowSelection();
}
// 加载文章状态
function loadFeedStatuses() {
const rows = document.querySelectorAll('.rss-feeds-table tbody tr');
rows.forEach(row => {
const feedId = row.getAttribute('data-feed-id');
const status = localStorage.getItem(`feed_status_${feedId}`) || 'unread';
// 更新状态点
const statusDot = row.querySelector('.status-dot');
if (statusDot) {
statusDot.className = `status-dot status-${status}`;
}
// 只更新状态列的样式,不更新整行
// const statusCell = row.querySelector('.feed-status-cell');
//if (statusCell) {
// statusCell.classList.remove('status-unread', 'status-read', 'status-ignore');
// statusCell.classList.add(`status-${status}`);
// }
});
}
// 保存文章状态
function saveFeedStatus(feedId, status) {
localStorage.setItem(`feed_status_${feedId}`, status);
// 更新界面
const row = document.querySelector(`tr[data-feed-id="${feedId}"]`);
if (row) {
const statusDot = row.querySelector('.status-dot');
if (statusDot) {
statusDot.className = `status-dot status-${status}`;
}
// 只更新状态列的样式,不更新整行
//const statusCell = row.querySelector('.feed-status-cell');
//if (statusCell) {
// statusCell.classList.remove('status-unread', 'status-read', 'status-ignore');
// statusCell.classList.add(`status-${status}`);
// }
}
// 关闭状态下拉菜单
const statusSelector = row.querySelector('.status-selector');
if (statusSelector) {
statusSelector.classList.remove('active');
}
// 更新筛选器计数
updateStatusFilter();
}
// 设置文章状态(从下拉菜单调用)
function setFeedStatus(feedId, status) {
saveFeedStatus(feedId, status);
// 显示操作反馈
const statusNames = {
'unread': '未读',
'read': '已读',
'ignore': '无感'
};
showRssMessage(`文章已标记为"${statusNames[status]}"`, 'success');
}
// 切换状态下拉菜单
function toggleStatusDropdown(selector) {
event.stopPropagation();
// 关闭所有其他状态下拉菜单
document.querySelectorAll('.status-selector').forEach(s => {
if (s !== selector) {
s.classList.remove('active');
}
});
// 切换当前状态下拉菜单
selector.classList.toggle('active');
}
// 绑定状态选择器事件
function bindStatusSelectors() {
const selectors = document.querySelectorAll('.status-selector');
selectors.forEach(selector => {
const options = selector.querySelectorAll('.status-option');
options.forEach(option => {
option.addEventListener('click', function(e) {
e.stopPropagation();
const feedId = this.closest('.status-selector').getAttribute('data-feed-id');
const status = this.getAttribute('data-status');
setFeedStatus(feedId, status);
});
});
});
}
// 更新状态筛选器
function updateStatusFilter() {
const rows = document.querySelectorAll('.rss-feeds-table tbody tr');
let allCount = rows.length;
let unreadCount = 0;
let readCount = 0;
let ignoreCount = 0;
rows.forEach(row => {
const feedId = row.getAttribute('data-feed-id');
const status = localStorage.getItem(`feed_status_${feedId}`) || 'unread';
switch(status) {
case 'unread':
unreadCount++;
break;
case 'read':
readCount++;
break;
case 'ignore':
ignoreCount++;
break;
}
});
// 更新筛选器计数
document.getElementById('count-all').textContent = allCount;
document.getElementById('count-unread').textContent = unreadCount;
document.getElementById('count-read').textContent = readCount;
document.getElementById('count-ignore').textContent = ignoreCount;
// 更新状态筛选按钮文本
const statusFilterBtn = document.getElementById('status-filter-btn');
if (statusFilterBtn) {
let statusText = '状态筛选';
const currentFilter = localStorage.getItem('currentStatusFilter') || 'all';
if (currentFilter !== 'all') {
const statusNames = {
'unread': '未读',
'read': '已读',
'ignore': '无感'
};
statusText = `${statusNames[currentFilter]} (${eval(currentFilter + 'Count')})`;
}
statusFilterBtn.innerHTML = statusText;
}
}
// 切换状态筛选下拉菜单
function toggleStatusFilter() {
event.stopPropagation();
const dropdown = document.getElementById('status-filter-dropdown');
dropdown.classList.toggle('active');
// 关闭批量操作下拉菜单
document.getElementById('batch-dropdown').classList.remove('active');
}
// 切换批量操作下拉菜单
function toggleBatchDropdown() {
event.stopPropagation();
const dropdown = document.getElementById('batch-dropdown');
dropdown.classList.toggle('active');
// 关闭状态筛选下拉菜单
document.getElementById('status-filter-dropdown').classList.remove('active');
}
// 按状态筛选
function filterByStatus(status) {
event.stopPropagation();
// 保存当前筛选状态
localStorage.setItem('currentStatusFilter', status);
// 更新按钮文本
const statusFilterBtn = document.getElementById('status-filter-btn');
if (statusFilterBtn) {
if (status === 'all') {
statusFilterBtn.textContent = '状态筛选';
} else {
const statusNames = {
'unread': '未读',
'read': '已读',
'ignore': '无感'
};
const rows = document.querySelectorAll('.rss-feeds-table tbody tr');
let count = 0;
rows.forEach(row => {
const feedId = row.getAttribute('data-feed-id');
const rowStatus = localStorage.getItem(`feed_status_${feedId}`) || 'unread';
if (rowStatus === status) {
count++;
}
});
statusFilterBtn.textContent = `${statusNames[status]} (${count})`;
}
}
// 显示/隐藏行
const rows = document.querySelectorAll('.rss-feeds-table tbody tr');
let visibleCount = 0;
rows.forEach(row => {
const feedId = row.getAttribute('data-feed-id');
const rowStatus = localStorage.getItem(`feed_status_${feedId}`) || 'unread';
if (status === 'all' || rowStatus === status) {
row.style.display = '';
visibleCount++;
} else {
row.style.display = 'none';
}
});
// 显示筛选结果
const statusNames = {
'all': '全部',
'unread': '未读',
'read': '已读',
'ignore': '无感'
};
showRssMessage(`已筛选${statusNames[status]}文章,显示${visibleCount}条`, 'info');
// 关闭下拉菜单
document.getElementById('status-filter-dropdown').classList.remove('active');
}
// 添加行选择功能
function addRowSelection() {
const rows = document.querySelectorAll('.rss-feeds-table tbody tr');
rows.forEach(row => {
// 点击行可以选择(除了状态选择器区域)
row.addEventListener('click', function(e) {
if (e.target.closest('.status-selector') ||
e.target.closest('.status-dropdown') ||
e.target.tagName === 'A' ||
e.target.tagName === 'BUTTON') {
return;
}
this.classList.toggle('selected');
updateBatchActions();
});
// 双击行标记为已读
row.addEventListener('dblclick', function(e) {
if (e.target.closest('.status-selector') ||
e.target.closest('.status-dropdown')) {
return;
}
const feedId = this.getAttribute('data-feed-id');
setFeedStatus(feedId, 'read');
});
});
}
// 全选
function selectAllFeeds() {
const rows = document.querySelectorAll('.rss-feeds-table tbody tr');
const visibleRows = Array.from(rows).filter(row => row.style.display !== 'none');
visibleRows.forEach(row => {
row.classList.add('selected');
});
updateBatchActions();
showRssMessage(`已选择 ${visibleRows.length} 篇文章`, 'info');
// 关闭下拉菜单
document.getElementById('batch-dropdown').classList.remove('active');
}
// 清除选择
function clearSelection() {
const rows = document.querySelectorAll('.rss-feeds-table tbody tr');
rows.forEach(row => {
row.classList.remove('selected');
});
updateBatchActions();
showRssMessage('已清除所有选择', 'info');
// 关闭下拉菜单
document.getElementById('batch-dropdown').classList.remove('active');
}
// 更新批量操作按钮状态
function updateBatchActions() {
const selectedCount = document.querySelectorAll('.rss-feeds-table tbody tr.selected').length;
// 可以在这里更新批量操作按钮的文本或状态
if (selectedCount > 0) {
// 自动显示消息已在 selectAllFeeds 和 clearSelection 中处理
}
}
// 批量标记为已读
function batchMarkAsRead() {
const selectedRows = document.querySelectorAll('.rss-feeds-table tbody tr.selected');
if (selectedRows.length === 0) {
showRssMessage('请先选择文章', 'warning');
return;
}
selectedRows.forEach(row => {
const feedId = row.getAttribute('data-feed-id');
saveFeedStatus(feedId, 'read');
row.classList.remove('selected');
});
showRssMessage(`已将 ${selectedRows.length} 篇文章标记为已读`, 'success');
updateStatusFilter();
// 关闭下拉菜单
document.getElementById('batch-dropdown').classList.remove('active');
}
// 批量标记为未读
function batchMarkAsUnread() {
const selectedRows = document.querySelectorAll('.rss-feeds-table tbody tr.selected');
if (selectedRows.length === 0) {
showRssMessage('请先选择文章', 'warning');
return;
}
selectedRows.forEach(row => {
const feedId = row.getAttribute('data-feed-id');
saveFeedStatus(feedId, 'unread');
row.classList.remove('selected');
});
showRssMessage(`已将 ${selectedRows.length} 篇文章标记为未读`, 'success');
updateStatusFilter();
// 关闭下拉菜单
document.getElementById('batch-dropdown').classList.remove('active');
}
// 批量标记为无感
function batchMarkAsIgnore() {
const selectedRows = document.querySelectorAll('.rss-feeds-table tbody tr.selected');
if (selectedRows.length === 0) {
showRssMessage('请先选择文章', 'warning');
return;
}
if (!confirm(`确定要将 ${selectedRows.length} 篇文章标记为"无感"吗?\n这些文章将被隐藏除非选择"全部"筛选器。`)) {
return;
}
selectedRows.forEach(row => {
const feedId = row.getAttribute('data-feed-id');
saveFeedStatus(feedId, 'ignore');
row.classList.remove('selected');
});
showRssMessage(`已将 ${selectedRows.length} 篇文章标记为无感`, 'success');
updateStatusFilter();
// 关闭下拉菜单
document.getElementById('batch-dropdown').classList.remove('active');
}
// 清除所有状态(重置功能)
function clearAllStatuses() {
if (!confirm('确定要清除所有文章的状态标记吗?\n这将重置所有文章的未读/已读/无感状态。')) {
return;
}
// 找到所有有状态存储的文章ID
const rows = document.querySelectorAll('.rss-feeds-table tbody tr');
rows.forEach(row => {
const feedId = row.getAttribute('data-feed-id');
localStorage.removeItem(`feed_status_${feedId}`);
});
// 重新加载状态
loadFeedStatuses();
updateStatusFilter();
showRssMessage('已清除所有文章状态', 'success');
// 关闭下拉菜单
document.getElementById('batch-dropdown').classList.remove('active');
}
// 导出状态数据(备份)
function exportStatusData() {
const statusData = {};
const rows = document.querySelectorAll('.rss-feeds-table tbody tr');
rows.forEach(row => {
const feedId = row.getAttribute('data-feed-id');
const status = localStorage.getItem(`feed_status_${feedId}`) || 'unread';
statusData[feedId] = status;
});
const dataStr = JSON.stringify(statusData, null, 2);
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
const exportFileDefaultName = `rss-status-backup-${new Date().toISOString().slice(0,10)}.json`;
const linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', exportFileDefaultName);
linkElement.click();
showRssMessage('状态数据已导出', 'success');
// 关闭下拉菜单
document.getElementById('batch-dropdown').classList.remove('active');
}
// 导入状态数据(恢复)
function importStatusData() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
try {
const statusData = JSON.parse(e.target.result);
let importedCount = 0;
// 应用导入的状态
for (const feedId in statusData) {
if (statusData.hasOwnProperty(feedId)) {
localStorage.setItem(`feed_status_${feedId}`, statusData[feedId]);
importedCount++;
}
}
// 重新加载状态
loadFeedStatuses();
updateStatusFilter();
showRssMessage(`已导入 ${importedCount} 条状态记录`, 'success');
} catch (error) {
showRssMessage('导入失败:文件格式不正确', 'error');
}
};
reader.readAsText(file);
};
input.click();
// 关闭下拉菜单
document.getElementById('batch-dropdown').classList.remove('active');
}
// ==================== 新增:刷新日志相关函数 ====================
// 获取刷新日志
function getRefreshLogs(type, pageSize = 10) {
return fetch(window.UrlNavConfig.actionUrl, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'do=getRefreshLogs&type=' + type + '&pageSize=' + pageSize,
credentials: 'include'
}).then(response => response.json());
}
// 获取最近成功的网址24小时内
function getRecentSuccessUrls(pageSize = 10) {
return getRefreshLogs('success', pageSize);
}
// 获取最近失败的网址24小时内
function getRecentFailedUrls(pageSize = 10) {
return getRefreshLogs('failed', pageSize);
}
</script>
<?php
include 'footer.php';
?>