5121 lines
179 KiB
PHP
5121 lines
179 KiB
PHP
|
|
<?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-top:2px;
|
|||
|
|
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;">×</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, '');
|
|||
|
|
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(/ /g, ' ');
|
|||
|
|
markdown = markdown.replace(/&/g, '&');
|
|||
|
|
markdown = markdown.replace(/</g, '<');
|
|||
|
|
markdown = markdown.replace(/>/g, '>');
|
|||
|
|
markdown = markdown.replace(/"/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 = {
|
|||
|
|
'&': '&',
|
|||
|
|
'<': '<', // 这行会导致HTML标签被转义,但我们需要它
|
|||
|
|
'>': '>',
|
|||
|
|
'"': '"',
|
|||
|
|
"'": '''
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 临时解决方案:先不转义<和>,让HTML标签正常工作
|
|||
|
|
// return String(text).replace(/[&<>"']/g, function(m) { return map[m]; });
|
|||
|
|
|
|||
|
|
// 简单转义,但保留HTML标签
|
|||
|
|
text = String(text);
|
|||
|
|
text = text.replace(/&(?!(amp|lt|gt|quot|#039);)/g, '&');
|
|||
|
|
text = text.replace(/"/g, '"');
|
|||
|
|
text = text.replace(/'/g, ''');
|
|||
|
|
|
|||
|
|
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';
|
|||
|
|
?>
|