Files
UrlNav/Manage.php

3500 lines
154 KiB
PHP
Raw Normal View History

2026-02-23 20:15:55 +08:00
<?php
if (!defined('__TYPECHO_ROOT_DIR__')) exit;
// 包含必要的文件
include 'header.php';
include 'menu.php';
include 'common-js.php';
include 'table-js.php';
// 获取插件选项
$options = Typecho_Widget::widget('Widget_Options');
$pluginOptions = $options->plugin('UrlNav');
$pageSize = isset($pluginOptions->pageSize) ? intval($pluginOptions->pageSize) : 20;
// 获取当前页码和分类
$currentPage = isset($_GET['page']) ? intval($_GET['page']) : 1;
$currentCategory = isset($_GET['category']) ? $_GET['category'] : '';
$currentStatus = isset($_GET['status']) ? $_GET['status'] : ''; // 新增:状态参数
$currentHasRss = isset($_GET['has_rss']) ? $_GET['has_rss'] : ''; // 新增RSS筛选参数
$searchKeyword = isset($_GET['search']) ? trim($_GET['search']) : '';
// 获取当前星级筛选参数
$currentStarRating = isset($_GET['star_rating']) ? $_GET['star_rating'] : '';
// 获取数据
$categories = UrlNav_Plugin::getAllCategories();
$urlsData = UrlNav_Plugin::getAllUrls($currentCategory, $currentPage, $pageSize, $searchKeyword, $currentStatus, $currentHasRss, $currentStarRating);
// 获取每个分类的统计信息
$categoryStats = [];
foreach ($categories as $category) {
$categoryStats[$category['id']] = UrlNav_Plugin::getCategoryStats($category['id']);
}
// 获取状态统计包含RSS统计
$statusStats = UrlNav_Plugin::getStatusStats();
?>
<style>
/* 星级筛选样式 */
.star-filter {
display: flex;
align-items: center;
gap: 5px;
flex-shrink: 0;
}
.star-filter select {
text-align: center;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 13px;
background: white;
height: 32px!important;
width: 80px;
}
/* 星级显示样式 */
.star-cell {
text-align: center;
width: 50px;
min-width: 50px;
}
.star-rating {
display: inline-block;
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
cursor: default;
}
.star-rating.has-star {
color: #ffc107;
font-weight: bold;
}
.star-rating.no-star {
color: #999;
font-style: italic;
}
/* 响应式调整 */
@media (max-width: 768px) {
.star-filter select {
width: 100%;
max-width: 100%;
}
.star-cell {
width: 40px;
min-width: 40px;
}
}
.search-container input::placeholder{color:#000!important;text-align:center;}
#category-list{text-align:center!important;}
#url-list td{text-align:center!important;}
th{text-align:center!important;}
.check-time-cell{color:#467b96!important;}
/* 进度窗口样式 */
#batch-progress-container {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
#batch-progress-container h4 {
margin-bottom: 15px;
font-size: 16px;
}
#progress-status {
font-size: 14px;
margin-bottom: 10px;
}
#progress-stats {
font-size: 13px;
padding: 8px;
background: #f8f9fa;
border-radius: 4px;
border: 1px solid #dee2e6;
}
#progress-bar {
transition: width 0.5s ease-in-out;
}
#stat-rss-no{color:#fff!important;}
#stat-unchecked{color:#28a745!important;}
.dropdown-toggle{background:#467b96!important;color:#fff!important;border-width:0px!important;}
/* 原有CSS样式保持不变 */
.btn-s{height:32px!important;}
.rss-filter {display: flex;align-items: center;gap: 5px;flex-shrink: 0;}
.rss-filter select {text-align:center;border: 1px solid #ddd;border-radius: 4px;font-size: 13px;background: white;width: 80px;height: 32px!important;}
@media (max-width: 768px) {.rss-filter select {width: 100%;max-width: 100%;}}
.urlnav-management {padding: 20px;}
#export-opml-btn{height:18px!important;}
.cz{height:14px!important;}
.search-container .btn {height:14px!important;}
.typecho-list-operate {display: flex;justify-content: space-between;align-items: center;flex-wrap: nowrap;gap: 10px;margin-bottom: 20px;overflow: visible !important;}
.operate-form-left {flex-shrink: 0;overflow: visible !important;}
.typecho-table-wrap{padding:0px 0!important;}
.operate {display: flex;align-items: center;gap: 10px;position: relative;overflow: visible !important;}
.btn-drop {position: relative;overflow: visible !important;}
.dropdown-menu {position: absolute;top: 100%;text-align: center;left: 0;z-index: 1000;min-width: 100px;background: white;border: 1px solid #ddd;border-radius: 4px;box-shadow: 0 2px 8px rgba(0,0,0,0.1);margin-top: 2px;display: none;overflow: visible !important;}
.dropdown-menu.show {display: block;}
.typecho-page-title{display:none;}
.filter-search-container {display: flex;align-items: center;gap: 10px;height: 32px!important;justify-content: center;flex-wrap: wrap;flex: 1;min-width: 0;}
.category-filter {display: flex;align-items: center;gap: 5px;flex-shrink: 0;}
.category-filter select {text-align:center;border: 1px solid #ddd;border-radius: 4px;font-size: 13px;background: white;height: 32px!important;width: 100px;}
.search-container {display: flex;gap: 10px;align-items: center;flex-shrink: 0;}
.search-input {padding: 6px 12px;border: 1px solid #ddd;border-radius: 4px!important;font-size: 13px;width: 100px;min-width: 100px;}
.search-button {padding: 6px 15px;background: #467b96;color: white;border: none;border-radius: 4px;cursor: pointer;font-size: 13px;}
.search-button:hover {background: #3a6a83;}
.btn-group {display: flex;gap: 10px;flex-shrink: 0;}
.btn {padding: 6px 16px;border: 1px solid #ddd;border-radius: 4px;background: white;color: #333;cursor: pointer;font-size: 13px;text-decoration: none;display: inline-flex;align-items: center;gap: 5px;white-space: nowrap;}
.btn:hover {background: #f5f5f5;text-decoration: none;}
.btn.primary {background: #467b96;color: white;border-color: #467b96;}
.btn.primary:hover {background: #3a6a83;border-color: #3a6a83;color: white;}
.btn.danger {background: #dc3545;color: white;border-color: #dc3545;}
.btn.danger:hover {background: #c82333;border-color: #c82333;color: white;}
.typecho-list-table th, .typecho-list-table td {vertical-align: middle;padding: 12px 8px;}
.url-cell {max-width: 300px;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;}
.url-link {color: #467b96;text-decoration: none;}
.url-link:hover {text-decoration: underline;}
.description-cell {max-width: 200px;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;}
.rss-cell {max-width: 200px;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;}
.rss-link {color: #28a745;text-decoration: none;font-size: 12px;display: inline-block;padding: 2px 6px;background: #e8f5e9;border-radius: 3px;}
.rss-link:hover {text-decoration: underline;background: #d4edda;}
.no-rss {color: #467b96;font-size: 12px;}
.category-tag {display: inline-block;padding: 2px 6px;background: #e8f4fd;color: #28a745;border-radius: 3px;font-size: 12px;}
.no-category {color: #999;font-style: italic;}
.action-cell {white-space: nowrap;}
.action-link {color: #467b96;text-decoration: none;margin-right: 10px;font-size: 13px;cursor: pointer;}
.action-link:hover {text-decoration: underline;}
.action-link.delete {color: #dc3545;}
.status-cell {text-align: center;width: 60px;min-width: 60px;}
.status-indicator {display: inline-block;width: 12px;height: 12px;border-radius: 50%;position: relative;cursor: pointer;transition: all 0.3s ease;}
.status-indicator:hover {transform: scale(1.2);}
.status-indicator:active {transform: scale(1.1);opacity: 0.8;}
.status-online {background-color: #28a745;box-shadow: 0 0 8px rgba(40, 167, 69, 0.6);}
.status-offline {background-color: #dc3545;box-shadow: 0 0 8px rgba(220, 53, 69, 0.4);}
.status-unknown {background-color: #6c757d;box-shadow: 0 0 8px rgba(108, 117, 125, 0.4);}
.status-checking {background-color: #ffc107;animation: pulse 1.5s infinite;box-shadow: 0 0 8px rgba(255, 193, 7, 0.6);}
@keyframes pulse {0% { opacity: 0.6; transform: scale(1); }50% { opacity: 1; transform: scale(1.1); }100% { opacity: 0.6; transform: scale(1); }}
.status-tooltip {position: relative;}
.status-tooltip:hover::after {content: attr(title);position: absolute;bottom: 100%;left: 50%;transform: translateX(-50%);background: rgba(0, 0, 0, 0.85);color: white;padding: 6px 10px;border-radius: 4px;font-size: 12px;white-space: nowrap;z-index: 1000;min-width: 200px;text-align: center;box-shadow: 0 2px 8px rgba(0,0,0,0.2);}
.status-stats {display: flex;gap: 20px;margin-bottom: 15px;padding: 12px 20px;background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);border-radius: 8px;border: 1px solid #dee2e6;flex-wrap: wrap;justify-content: space-around;}
.stat-item {display: flex;flex-direction: column;align-items: center;min-width: 80px;}
.stat-value {font-size: 20px;font-weight: bold;margin-bottom: 4px;}
.stat-label {font-size: 12px;color: #6c757d;font-weight: 500;}
#check-status-btn .spinner {display: inline-block;width: 14px;height: 14px;border: 2px solid rgba(255,255,255,.3);border-radius: 50%;border-top-color: #fff;animation: spin 1s ease-in-out infinite;margin-right: 5px;vertical-align: middle;}
@keyframes spin {to { transform: rotate(360deg); }}
.modal-overlay {position: fixed;top: 0;left: 0;right: 0;bottom: 0;background: rgba(0, 0, 0, 0.5);display: none;z-index: 10000;align-items: center;justify-content: center;overflow-y: auto;padding: 20px;}
.modal-container {background: white;border-radius: 8px;width: 90%;max-width: 500px;max-height: 85vh;overflow: hidden;display: none;animation: modalFadeIn 0.3s ease;margin: auto;}
@keyframes modalFadeIn {from {opacity: 0;transform: translateY(-20px);}to {opacity: 1;transform: translateY(0);}}
.modal-header {padding: 20px;border-bottom: 1px solid #eee;display: flex;justify-content: space-between;align-items: center;background: #f8f9fa;position: sticky;top: 0;z-index: 10;}
.modal-title {margin: 0;font-size: 18px;color: #333;}
.modal-close {background: none;border: none;font-size: 24px;cursor: pointer;color: #666;line-height: 1;}
.modal-close:hover {color: #333;}
.modal-body {padding: 20px;overflow-y: auto;max-height: calc(85vh - 140px);}
.modal-footer {padding: 15px 20px;border-top: 1px solid #eee;display: flex;justify-content: flex-end;gap: 10px;background: #f8f9fa;position: sticky;bottom: 0;z-index: 10;}
.form-group {margin-bottom: 20px;}
.form-label {display: block;margin-bottom: 8px;font-weight: 500;color: #333;font-size: 14px;}
.form-label.required::after {content: "*";color: #dc3545;margin-left: 4px;}
.form-input {width: 100%;padding: 8px 12px;border: 1px solid #ddd;border-radius: 4px;font-size: 14px;box-sizing: border-box;}
.form-input:focus {outline: none;border-color: #467b96;box-shadow: 0 0 0 3px rgba(70, 123, 150, 0.1);}
.form-textarea {width: 100%;padding: 8px 12px;border: 1px solid #ddd;border-radius: 4px;font-size: 14px;min-height: 80px;resize: vertical;box-sizing: border-box;}
.form-select {width: 100%;padding: 8px 12px;border: 1px solid #ddd;border-radius: 4px;font-size: 14px;background: white;max-height: 200px;overflow-y: auto;box-sizing: border-box;min-height: 38px;}
.form-select option {padding: 8px 12px;white-space: normal;word-wrap: break-word;}
.form-help {color: #666;font-size: 12px;margin-top: 4px;}
.rss-autofetch-section {margin: 15px 0;padding: 15px;background: #f8f9fa;border-radius: 6px;border-left: 4px solid #28a745;}
.rss-autofetch-section h4 {margin-top: 0;margin-bottom: 10px;color: #28a745;font-size: 14px;}
.rss-urls-list {max-height: 200px;overflow-y: auto;border: 1px solid #ddd;border-radius: 4px;background: white;margin-bottom: 10px;}
.rss-url-item {padding: 8px 12px;border-bottom: 1px solid #eee;cursor: pointer;transition: background-color 0.2s;}
.rss-url-item:hover {background-color: #e8f5e9;}
.rss-url-item:last-child {border-bottom: none;}
.rss-url-item.selected {background-color: #d4edda;border-left: 3px solid #28a745;}
.rss-url-item .url {font-size: 13px;color: #333;word-break: break-all;}
.rss-url-item .type {font-size: 11px;color: #666;background: #f1f1f1;padding: 1px 4px;border-radius: 2px;margin-left: 5px;}
.fetch-rss-btn {background: #28a745;color: white;border: none;padding: 8px 16px;border-radius: 4px;cursor: pointer;font-size: 13px;display: inline-flex;align-items: center;gap: 5px;}
.fetch-rss-btn:hover {background: #218838;}
.fetch-rss-btn:disabled {background: #6c757d;cursor: not-allowed;}
.fetching-spinner {display: inline-block;width: 16px;height: 16px;border: 2px solid rgba(255,255,255,.3);border-radius: 50%;border-top-color: #fff;animation: spin 1s ease-in-out infinite;}
.alert {padding: 12px 15px;border-radius: 4px;margin-bottom: 15px;display: none;position: fixed;top: 20px;left: 50%;transform: translateX(-50%);z-index: 99999;max-width: 90%;box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);}
.alert.success {background: #d4edda;border: 1px solid #c3e6cb;color: #155724;}
.alert.error {background: #f8d7da;border: 1px solid #f5c6cb;color: #721c24;}
.alert.warning {background: #fff3cd;border: 1px solid #ffeaa7;color: #856404;}
.alert.info {background: #d1ecf1;border: 1px solid #bee5eb;color: #0c5460;}
.loading-overlay {position: fixed;top: 0;left: 0;right: 0;bottom: 0;background: rgba(255, 255, 255, 0.8);display: none;align-items: center;justify-content: center;z-index: 99999;}
.loading-spinner {width: 40px;height: 40px;border: 3px solid #f3f3f3;border-top: 3px solid #467b96;border-radius: 50%;animation: spin 1s linear infinite;}
.tab-container {border-bottom: 1px solid #333;margin-bottom: 20px;padding-bottom: 10px;}
.tab-buttons {display: flex;gap: 10px;justify-content: center;}
.tab-button {padding: 10px 20px;background: none;border: 1px solid transparent;border-bottom: 2px solid transparent;cursor: pointer;color: #FFF;font-size: 14px;}
.tab-button:hover {color: #333;}
.tab-button.active {color: #467b96;border-bottom-color: #467b96;background: #f8f9fa;border-radius:8px;}
.tab-content {display: none;}
.tab-content.active {display: block;}
.category-actions {display: flex;gap: 10px;margin-bottom: 20px;}
/* 网址表格列宽 */
#urls-tab .typecho-list-table {
width: 100%;
min-width: 800px;
table-layout: fixed;
border-collapse: collapse;
}
#urls-tab .typecho-list-table colgroup col:nth-child(1) { width: 3%; }
#urls-tab .typecho-list-table colgroup col:nth-child(2) { width: 5%; }
#urls-tab .typecho-list-table colgroup col:nth-child(3) { width: 9%; }
#urls-tab .typecho-list-table colgroup col:nth-child(4) { width: 20%; }
#urls-tab .typecho-list-table colgroup col:nth-child(5) { width: 5%; }
#urls-tab .typecho-list-table colgroup col:nth-child(6) { width: 25%; }
#urls-tab .typecho-list-table colgroup col:nth-child(7) { width: 5%; }
#urls-tab .typecho-list-table colgroup col:nth-child(8) { width: 6%; } /* 星级列 */
#urls-tab .typecho-list-table colgroup col:nth-child(9) { width: 8%; }
#urls-tab .typecho-list-table colgroup col:nth-child(10) { width: 5%; }
#urls-tab .typecho-list-table colgroup col:nth-child(11) { width: 8%; }
/* 分类表格列宽 */
#categories-tab .typecho-list-table {
width: 100%;
min-width: 600px;
table-layout: fixed;
border-collapse: collapse;
}
#categories-tab .typecho-list-table colgroup col:nth-child(1) { width: 3%; }
#categories-tab .typecho-list-table colgroup col:nth-child(2) { width: 5%; }
#categories-tab .typecho-list-table colgroup col:nth-child(3) { width: 10%; }
#categories-tab .typecho-list-table colgroup col:nth-child(4) { width: 40%; }
#categories-tab .typecho-list-table colgroup col:nth-child(5) { width: 10%; }
#categories-tab .typecho-list-table colgroup col:nth-child(6) { width: 8%; }
#categories-tab .typecho-list-table colgroup col:nth-child(7) { width: 8%; }
#categories-tab .typecho-list-table colgroup col:nth-child(8) { width: 7%; }
.status-filter {display: flex;align-items: center;gap: 5px;flex-shrink: 0;}
.status-filter select {text-align:center;border: 1px solid #ddd;border-radius: 4px;font-size: 13px;background: white;height: 32px!important;width: 80px;}
.tabs {margin-bottom: 20px;}
.cron-info-section {background: #f8f9fa;padding: 20px;border-radius: 8px;margin-bottom: 20px;}
.cron-stats {display: grid;grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));gap: 15px;margin: 20px 0;}
.cron-stat-item {background: white;padding: 15px;border-radius: 6px;text-align: center;border: 1px solid #dee2e6;}
.cron-stat-value {font-size: 24px;font-weight: bold;margin-bottom: 5px;}
.cron-stat-label {font-size: 12px;color: #6c757d;}
.cron-urls {margin: 20px 0;}
.url-box {display: flex;gap: 10px;margin-bottom: 10px;}
.url-input {flex: 1;padding: 8px 12px;border: 1px solid #ddd;border-radius: 4px;background: white;font-size: 13px;font-family: monospace;}
.copy-btn {white-space: nowrap;height: 18px !important;padding: 4px 12px !important;font-size: 12px !important;}
.cron-actions {display: flex;gap: 10px;margin-top: 20px;}
.logs-container {border-radius: 6px;padding: 10px;background: #f8f9fa;}
.cron-log-item {padding: 10px;border-bottom: 1px solid #dee2e6;font-size: 12px;}
.cron-log-item:last-child {border-bottom: none;}
.cron-log-time {color: #6c757d;font-weight: bold;}
.cron-log-type {display: inline-block;padding: 2px 6px;border-radius: 3px;font-size: 11px;margin-right: 10px;}
.cron-log-type.status {background: #cce5ff;color: #004085;}
.cron-log-type.error {background: #f8d7da;color: #721c24;}
.cron-log-message {margin-top: 5px;color: #495057;word-break: break-word;}
.btn.small {padding: 4px 12px !important;font-size: 12px !important;height: auto !important;}
@media (max-width: 768px) {.status-filter select {width: 100%;max-width: 100%;}}
@media (max-width: 768px) {
.typecho-list-operate {flex-direction: column;align-items: stretch;flex-wrap: wrap;}
.filter-search-container {flex-direction: column;align-items: stretch;}
.search-input,.category-filter select {width: 100%;max-width: 100%;}
.btn-group {width: 100%;justify-content: center;}
.url-cell {max-width: 150px;}
.description-cell {max-width: 100px;}
.rss-cell {max-width: 100px;}
.status-cell {width: 40px;min-width: 40px;}
.modal-container {width: 95%;margin: 10px;}
.status-stats {flex-direction: column;gap: 10px;}
.stat-item {flex-direction: row;justify-content: space-between;width: 100%;}
}
/* 新增:统计数字样式 */
.stat-number {
display: inline-block;
min-width: 20px;
text-align: center;
font-weight: bold;
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
}
.url-count {
background-color: #e8f4fd;
color: #28a745;
}
.rss-count {
background-color: #e8f5e9;
color: #28a745;
}
.rss-yes-count {
background-color: #e8f5e9;
color: #28a745;
}
.rss-no-count {
background-color: #f8f9fa;
color: #6c757d;
}
/* 页码样式优化 */
.typecho-pager {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 5px;
margin: 20px 0;
list-style: none;
padding: 0;
}
.typecho-pager li {
margin: 0;
padding: 0;
}
.typecho-pager a,
.typecho-pager span {
display: inline-block;
padding: 5px 12px;
border: 1px solid #ddd;
border-radius: 4px;
text-decoration: none;
color: #467b96;
font-size: 13px;
min-width: 30px;
text-align: center;
transition: all 0.2s;
}
.typecho-pager a:hover {
background-color: #f5f5f5;
border-color: #467b96;
color: #333;
}
.typecho-pager .current {
background-color: #467b96;
border-color: #467b96;
color: white;
cursor: default;
}
.typecho-pager .ellipsis {
padding: 5px 8px;
border: none;
color: #999;
cursor: default;
}
.typecho-pager .disabled {
color: #999;
cursor: not-allowed;
}
.typecho-pager .disabled:hover {
background-color: transparent;
border-color: #ddd;
}
/* 网址管理页面的页码去除边框 */
#urls-tab .typecho-pager {
border: none !important;
}
#urls-tab .typecho-pager a,
#urls-tab .typecho-pager span {
border: none !important;
background: transparent !important;
}
#urls-tab .typecho-pager .current {
background-color: #467b96 !important;
color: white !important;
border-radius: 4px;
}
#urls-tab .typecho-pager a:hover {
background-color: #f5f5f5 !important;
border-radius: 4px;
}
/* 分类管理页面操作栏独立样式 */
#categories-tab .typecho-list-operate {
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
#categories-tab .operate-form-left {
flex: 1;
display: flex;
justify-content: flex-start;
}
#categories-tab .btn-group {
display: flex;
justify-content: flex-end;
}
/* 修改原有CSS只改变位置和z-index保持其他样式不变 */
#batch-progress-container {
position: fixed;
top: 100px; /* 改为页面顶部 */
left: 50%;
transform: translateX(-50%);
z-index: 99999; /* 增加z-index确保在最前面 */
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
min-width: 400px;
max-width: 500px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
border: 1px solid #dee2e6;
}
/* 保持你原有的其他样式完全不变 */
#batch-progress-container h4 {
margin-bottom: 15px;
font-size: 16px;
}
#progress-status {
font-size: 14px;
margin-bottom: 10px;
}
#progress-stats {
font-size: 13px;
padding: 8px;
background: #f8f9fa;
border-radius: 4px;
border: 1px solid #dee2e6;
}
#progress-bar {
transition: width 0.5s ease-in-out;
}
/* 检查时间列样式 */
.check-time-cell {
font-size: 12px;
color: #666;
text-align: center;
white-space: nowrap;
width: 70px;
min-width: 70px;
}
.no-check-time {
font-style: italic;
color: #999;
}
/* 响应式调整 */
@media (max-width: 768px) {
#batch-progress-container {
min-width: 90%;
max-width: 95%;
top: 80px;
padding: 15px;
}
.check-time-cell {
width: 50px;
min-width: 50px;
font-size: 11px;
}
}
</style>
<!-- 消息提示 -->
<div class="alert" id="message-alert" style="display: none;"></div>
<!-- 新增分批检查进度显示通过JS动态创建 -->
<div id="batch-progress-container" style="display: none;"></div>
<!-- 加载动画 -->
<div class="loading-overlay" id="loading-overlay">
<div class="loading-spinner"></div>
</div>
<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 typecho-list">
<!-- 选项卡 -->
<div class="tab-container">
<div class="tab-buttons">
<button type="button" class="tab-button active" data-tab="urls">网址管理</button>
<button type="button" class="tab-button" data-tab="categories">分类管理</button>
</div>
</div>
<!-- 网址管理 -->
<div class="tab-content active" id="urls-tab">
<!-- 状态统计 -->
<div class="status-stats" id="status-stats">
<div class="stat-item">
<span class="stat-value" id="stat-total"><?php echo $statusStats['total']; ?></span>
<span class="stat-label">总数</span>
</div>
<div class="stat-item">
<span class="stat-value" style="color:#28a745;" id="stat-online"><?php echo $statusStats['online']; ?></span>
<span class="stat-label">通连</span>
</div>
<div class="stat-item">
<span class="stat-value" style="color:#dc3545;" id="stat-offline"><?php echo $statusStats['offline']; ?></span>
<span class="stat-label">失连</span>
</div>
<div class="stat-item">
<span class="stat-value" style="color:#6c757d;" id="stat-unchecked"><?php echo $statusStats['unchecked']; ?></span>
<span class="stat-label">未检</span>
</div>
<div class="stat-item">
<span class="stat-value" id="stat-rate"><?php echo round($statusStats['online_rate']); ?></span>
<span class="stat-label">通连率</span>
</div>
<div class="stat-item">
<span class="stat-value" style="color:#28a745;" id="stat-rss-yes"><?php echo $statusStats['has_rss']; ?></span>
<span class="stat-label">有RSS</span>
</div>
<div class="stat-item">
<span class="stat-value" style="color:#6c757d;" id="stat-rss-no"><?php echo $statusStats['no_rss']; ?></span>
<span class="stat-label">无RSS</span>
</div>
</div>
<div class="typecho-list-operate clearfix">
<form method="get" class="operate-form-left">
<div class="operate" >
<div class="btn-group btn-drop">
<button class="btn dropdown-toggle btn-s" type="button"><i
class="sr-only"><?php _e('操作'); ?></i><?php _e('多选操作'); ?> <i
class="i-caret-down"></i></button>
<ul class="dropdown-menu">
<li>
<a href="javascript:void(0);" id="batch-delete-urls-btn"><?php _e('删除'); ?></a>
</li>
</ul>
</div>
<button type="button" class="btn" id="check-status-btn" title="检查网站状态">
<span class="spinner" style="display: none;"></span>
<span class="btn-text">检查</span>
</button>
<!-- 新增:定时任务状态按钮 -->
<button type="button" class="btn" id="cron-status-btn">
<span>统计</span>
</button>
</div>
</form>
<!-- 筛选和搜索 -->
<div class="filter-search-container">
<!-- 分类筛选 -->
<div class="category-filter">
<select id="category-filter">
<option value="">全部分类</option>
<?php foreach ($categories as $category): ?>
<option value="<?php echo $category['id']; ?>" <?php echo $currentCategory == $category['id'] ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($category['name']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<!-- 状态筛选 -->
<div class="status-filter">
<select id="status-filter">
<option value="">全部状态</option>
<option value="online" <?php echo $currentStatus == 'online' ? 'selected' : ''; ?>>通连</option>
<option value="offline" <?php echo $currentStatus == 'offline' ? 'selected' : ''; ?>>失连</option>
<option value="unchecked" <?php echo $currentStatus == 'unchecked' ? 'selected' : ''; ?>>未查</option>
</select>
</div>
<!-- RSS筛选 -->
<div class="rss-filter">
<select id="rss-filter">
<option value="">全部RSS</option>
<option value="yes" <?php echo $currentHasRss == 'yes' ? 'selected' : ''; ?>>有RSS</option>
<option value="no" <?php echo $currentHasRss == 'no' ? 'selected' : ''; ?>>无RSS</option>
</select>
</div>
<!-- 星级筛选 -->
<div class="star-filter">
<select id="star-filter">
<option value="">全部星级</option>
<option value="starred" <?php echo $currentStarRating == 'starred' ? 'selected' : ''; ?>>有星级</option>
<option value="0" <?php echo $currentStarRating === '0' ? 'selected' : ''; ?>>无星级</option>
<option value="1" <?php echo $currentStarRating == '1' ? 'selected' : ''; ?>>★</option>
<option value="2" <?php echo $currentStarRating == '2' ? 'selected' : ''; ?>>★★</option>
<option value="3" <?php echo $currentStarRating == '3' ? 'selected' : ''; ?>>★★★</option>
</select>
</div>
<!-- 搜索框 -->
<div class="search-container">
<form method="get" action="" style="display: flex; align-items: center; gap: 10px;">
<input type="text" name="search" class="search-input"
placeholder="<?php _e('搜索网址...'); ?>"
value="<?php echo htmlspecialchars($searchKeyword); ?>">
<input type="hidden" name="has_rss" value="<?php echo htmlspecialchars($currentHasRss); ?>">
<input type="hidden" name="star_rating" value="<?php echo htmlspecialchars($currentStarRating); ?>">
<input type="hidden" name="status" value="<?php echo htmlspecialchars($currentStatus); ?>">
<input type="hidden" name="category" value="<?php echo htmlspecialchars($currentCategory); ?>">
<input type="hidden" name="panel" value="UrlNav/Manage.php">
<?php if ($searchKeyword || $currentCategory || $currentStatus|| $currentHasRss || $currentStarRating): ?>
<a href="<?php echo \Typecho\Common::url('extending.php?panel=UrlNav/Manage.php', $options->adminUrl); ?>"
class="btn cz"><?php _e('重置'); ?></a>
<?php endif; ?>
</form>
</div>
</div>
<!-- 操作按钮 -->
<div class="btn-group">
<button type="button" class="btn" id="import-opml-btn" title="导入OPML文件">
<span>导入</span>
</button>
<a href="<?php echo Typecho_Common::url('/action/urlnav?do=exportOpml', $options->index); ?>"
class="btn" id="export-opml-btn" title="导出为OPML文件" target="_blank">
<span>导出</span>
</a>
<!-- 新增检查状态按钮 -->
<button type="button" class="btn primary" id="add-url-btn">
<span>新增网址</span>
</button>
</div>
</div><!-- end .typecho-list-operate -->
<?php if ($searchKeyword || $currentCategory || $currentStatus || $currentHasRss || $currentStarRating): ?>
<div class="alert info">
<p>
<?php
$statusNames = [
'online' => '通连',
'offline' => '失连',
'unchecked' => '未查'
];
// 新增星级名称映射
$starNames = [
'0' => '无星级',
'1' => '★',
'2' => '★★',
'3' => '★★★',
'starred' => '有星级'
];
$filters = [];
if ($searchKeyword) {
$filters[] = '搜索关键词:<strong>"' . htmlspecialchars($searchKeyword) . '"</strong>';
}
if ($currentCategory) {
$categoryName = '';
foreach ($categories as $cat) {
if ($cat['id'] == $currentCategory) {
$categoryName = $cat['name'];
break;
}
}
if ($categoryName) {
$filters[] = '分类:<strong>"' . htmlspecialchars($categoryName) . '"</strong>';
}
}
if ($currentStatus && isset($statusNames[$currentStatus])) {
$filters[] = '状态:<strong>"' . $statusNames[$currentStatus] . '"</strong>';
}
// 新增:星级筛选提示
if ($currentStarRating && isset($starNames[$currentStarRating])) {
$filters[] = '星级:<strong>"' . $starNames[$currentStarRating] . '"</strong>';
}
if ($currentHasRss) {
$filters[] = 'RSS<strong>"' . ($currentHasRss == 'yes' ? '有' : '无') . '"</strong>';
}
echo implode('', $filters);
?>
</p>
<a href="<?php echo \Typecho\Common::url('extending.php?panel=UrlNav/Manage.php', $options->adminUrl); ?>">
<?php _e('显示全部'); ?>
</a>
</div>
<?php endif; ?>
<form method="post" name="manage_urls" class="operate-form">
<div class="typecho-table-wrap">
<table class="typecho-list-table">
<colgroup>
<col width="3%" class="kit-hidden-mb"/>
<col width="5%"/>
<col width="10%"/>
<col width="4%"/>
<col width="6%"/>
<col width="15%"/>
<col width="5%"/>
<col width="3%"/> <!-- 新增:星级列 -->
<col width="30%"/>
<col width="5%"/>
<col width="6%"/>
</colgroup>
<thead>
<tr>
<th class="kit-hidden-mb">
<form method="get" class="operate-form-left">
<label><i class="sr-only"><?php _e('全选'); ?></i><input type="checkbox" class="typecho-table-select-all"/></label>
</form>
</th>
<th><?php _e('ID'); ?></th>
<th><?php _e('标题'); ?></th>
<th><?php _e('网址'); ?></th>
<th><?php _e('分类'); ?></th>
<th><?php _e('网站描述'); ?></th>
<th><?php _e('状态'); ?></th>
<th><?php _e('星级'); ?></th> <!-- 新增:星级表头 -->
<th><?php _e('检查时间'); ?></th>
<th><?php _e('RSS'); ?></th>
<th><?php _e('操作'); ?></th>
</tr>
</thead>
<tbody id="url-list">
<?php
$urls = $urlsData['data'];
if (empty($urls)) {
if ($searchKeyword || $currentCategory) {
echo '<tr><td colspan="11" style="text-align: center;"><h6 class="typecho-list-table-title">' . _t('没有找到符合条件的网址') . '</h6></td></tr>';
} else {
echo '<tr><td colspan="11" style="text-align: center;"><h6 class="typecho-list-table-title">' . _t('暂无网址,点击"新增网址"按钮添加') . '</h6></td></tr>';
}
} else {
foreach ($urls as $url) {
echo '<tr id="url-' . $url['id'] . '">';
echo '<td class="kit-hidden-mb"><input type="checkbox" value="' . $url['id'] . '" name="url[]"/></td>';
echo '<td>' . $url['id'] . '</td>';
echo '<td>' . htmlspecialchars($url['title']) . '</td>';
echo '<td class="url-cell"><a href="' . htmlspecialchars($url['url']) . '" target="_blank" class="url-link">' . htmlspecialchars($url['url']) . '</a></td>';
echo '<td>';
if ($url['category_name']) {
echo '<span class="category-tag">' . htmlspecialchars($url['category_name']) . '</span>';
} else {
echo '<span class="no-category">' . _t('未分类') . '</span>';
}
echo '</td>';
echo '<td class="description-cell">' . htmlspecialchars($url['description'] ?: '-') . '</td>';
// 状态列
echo '<td class="status-cell">';
$statusClass = 'status-unknown';
$statusTitle = '未查';
if (!empty($url['last_status_check'])) {
if ($url['is_online'] == 1) {
$statusClass = 'status-online';
$statusTitle = '通连';
if ($url['last_status_code']) {
$statusTitle .= ' (HTTP ' . $url['last_status_code'] . ')';
}
$statusTitle .= ' - 最后检查: ' . date('Y-m-d', strtotime($url['last_status_check']));
} else {
$statusClass = 'status-offline';
$statusTitle = '失连';
if ($url['last_status_code']) {
$statusTitle .= ' (HTTP ' . $url['last_status_code'] . ')';
}
$statusTitle .= ' - 最后检查: ' . date('Y-m-d', strtotime($url['last_status_check']));
}
} else if (!empty($url['created_at'])) {
$createdDays = floor((time() - strtotime($url['created_at'])) / (60 * 60 * 24));
if ($createdDays > 30) {
$statusTitle .= ' (添加超过' . $createdDays . '天)';
}
}
echo '<div class="status-indicator status-tooltip ' . $statusClass . '"
title="' . htmlspecialchars($statusTitle) . '"
data-id="' . $url['id'] . '"
data-url="' . htmlspecialchars($url['url']) . '">
</div>';
echo '</td>';
// 新增:星级列
echo '<td class="star-cell">';
$starRating = isset($url['star_rating']) ? intval($url['star_rating']) : 0;
$starText = UrlNav_Plugin::getStarRatingText($starRating);
$starClass = $starRating > 0 ? 'has-star' : 'no-star';
echo '<span class="star-rating ' . $starClass . '" title="' . $starText . '" data-rating="' . $starRating . '">' . $starText . '</span>';
echo '</td>';
// 新增:检查时间列
echo '<td class="check-time-cell">';
if (!empty($url['last_status_check'])) {
// 将UTC时间转换为北京时间
$beijingTime = date('Y-m-d', strtotime($url['last_status_check']) + (8 * 3600));
echo $beijingTime;
} else {
echo '<span class="no-check-time">未检查</span>';
}
echo '</td>';
echo '<td class="rss-cell">';
if (!empty($url['rss_url'])) {
echo '<a href="' . htmlspecialchars($url['rss_url']) . '" target="_blank" class="rss-link" title="' . htmlspecialchars($url['rss_url']) . '">RSS</a>';
} else {
echo '<span class="no-rss">无</span>';
}
echo '</td>';
// 操作列
echo '<td class="action-cell">';
echo '<a href="#" onclick="event.preventDefault(); event.stopPropagation(); window.urlnavEditUrl(' . $url['id'] . '); return false;" class="action-link edit-url" data-id="' . $url['id'] . '">' . _t('编辑') . '</a>';
echo '<a href="#" onclick="event.preventDefault(); event.stopPropagation(); window.urlnavDeleteUrl(' . $url['id'] . '); return false;" class="action-link delete delete-url" data-id="' . $url['id'] . '">' . _t('删除') . '</a>';
echo '</td>';
echo '</tr>';
}
}
?>
</tbody>
</table>
</div>
</form>
<?php if ($urlsData['totalPages'] > 1): ?>
<div class="typecho-list-operate clearfix">
<ul class="typecho-pager">
<li>
<?php if ($currentPage > 1): ?>
<a href="<?php
$url = \Typecho\Common::url('extending.php?panel=UrlNav/Manage.php&page=' . ($currentPage - 1), $options->adminUrl);
if ($currentCategory) $url .= '&category=' . urlencode($currentCategory);
if ($currentStatus) $url .= '&status=' . urlencode($currentStatus);
if ($currentHasRss) $url .= '&has_rss=' . urlencode($currentHasRss);
if ($searchKeyword) $url .= '&search=' . urlencode($searchKeyword);
echo $url;
?>">
<?php _e('上一页'); ?>
</a>
<?php else: ?>
<span class="current disabled"><?php _e('上一页'); ?></span>
<?php endif; ?>
</li>
<?php
$totalPages = $urlsData['totalPages'];
// 只显示开始两页、最后两页和当前页附近的页面
$showPages = [];
// 始终显示第1页
$showPages[] = 1;
// 显示第2页如果总页数>=2
if ($totalPages >= 2) {
$showPages[] = 2;
}
// 显示当前页前后各1页
for ($i = max(3, $currentPage - 1); $i <= min($totalPages - 2, $currentPage + 1); $i++) {
if ($i > 2 && $i < $totalPages - 1) {
$showPages[] = $i;
}
}
// 显示倒数第2页
if ($totalPages > 3 && $totalPages - 1 > $currentPage + 1) {
$showPages[] = $totalPages - 1;
}
// 显示最后一页
if ($totalPages > 2) {
$showPages[] = $totalPages;
}
// 去重并排序
$showPages = array_unique($showPages);
sort($showPages);
$prevPage = 0;
foreach ($showPages as $page) {
// 添加省略号
if ($page - $prevPage > 1) {
echo '<li><span class="ellipsis">...</span></li>';
}
if ($page === $currentPage) {
echo '<li><span class="current">' . $page . '</span></li>';
} else {
echo '<li><a href="';
$url = \Typecho\Common::url('extending.php?panel=UrlNav/Manage.php&page=' . $page, $options->adminUrl);
if ($currentCategory) $url .= '&category=' . urlencode($currentCategory);
if ($currentStatus) $url .= '&status=' . urlencode($currentStatus);
if ($currentHasRss) $url .= '&has_rss=' . urlencode($currentHasRss);
if ($searchKeyword) $url .= '&search=' . urlencode($searchKeyword);
if ($currentStarRating) $url .= '&star_rating=' . urlencode($currentStarRating);
echo $url . '">' . $page . '</a></li>';
}
$prevPage = $page;
}
?>
<li>
<?php if ($currentPage < $urlsData['totalPages']): ?>
<a href="<?php
$url = \Typecho\Common::url('extending.php?panel=UrlNav/Manage.php&page=' . ($currentPage + 1), $options->adminUrl);
if ($currentCategory) $url .= '&category=' . urlencode($currentCategory);
if ($currentStatus) $url .= '&status=' . urlencode($currentStatus);
if ($currentHasRss) $url .= '&has_rss=' . urlencode($currentHasRss);
if ($searchKeyword) $url .= '&search=' . urlencode($searchKeyword);
echo $url;
?>">
<?php _e('下一页'); ?></a>
<?php else: ?>
<span class="current disabled"><?php _e('下一页'); ?></span>
<?php endif; ?>
</li>
</ul>
</div>
<?php endif; ?>
</div><!-- end #urls-tab -->
<!-- 分类管理 -->
<div class="tab-content" id="categories-tab">
<div class="typecho-list-operate clearfix">
<form method="get" class="operate-form-left">
<div class="operate" style="justify-content:flex-start;">
<div class="btn-group btn-drop">
<button class="btn dropdown-toggle btn-s" type="button"><i
class="sr-only"><?php _e('操作'); ?></i><?php _e('多选操作'); ?> <i
class="i-caret-down"></i></button>
<ul class="dropdown-menu">
<li>
<a href="javascript:void(0);" id="batch-delete-categories-btn"><?php _e('删除'); ?></a>
</li>
</ul>
</div>
</div>
</form>
<!-- 操作按钮 -->
<div class="btn-group">
<button type="button" class="btn primary" id="add-category-btn">
<span>新增分类</span>
</button>
</div>
</div><!-- end .typecho-list-operate -->
<form method="post" name="manage_categories" class="operate-form">
<div class="typecho-table-wrap">
<table class="typecho-list-table">
<colgroup>
<col width="3%" class="kit-hidden-mb"/>
<col width="5%"/>
<col width="20%"/>
<col width="30%"/>
<col width="10%"/>
<col width="8%"/>
<col width="8%"/>
<col width="10%"/>
</colgroup>
<thead>
<tr>
<th class="kit-hidden-mb">
<form method="get" class="operate-form-left">
<label><i class="sr-only"><?php _e('全选'); ?></i><input type="checkbox" class="typecho-table-select-all"/></label>
</form>
</th>
<th><?php _e('ID'); ?></th>
<th><?php _e('名称'); ?></th>
<th><?php _e('描述'); ?></th>
<th><?php _e('排序'); ?></th>
<th><?php _e('网址'); ?></th>
<th><?php _e('RSS'); ?></th>
<th><?php _e('操作'); ?></th>
</tr>
</thead>
<tbody id="category-list">
<?php
if (empty($categories)) {
echo '<tr><td colspan="8" style="text-align: center;"><h6 class="typecho-list-table-title">' . _t('暂无分类,点击"新增分类"按钮添加') . '</h6></td></tr>';
} else {
foreach ($categories as $category) {
$stats = isset($categoryStats[$category['id']]) ? $categoryStats[$category['id']] : ['url_count' => 0, 'rss_count' => 0];
echo '<tr id="category-' . $category['id'] . '">';
echo '<td class="kit-hidden-mb"><input type="checkbox" value="' . $category['id'] . '" name="category[]"/></td>';
echo '<td>' . $category['id'] . '</td>';
echo '<td>' . htmlspecialchars($category['name']) . '</td>';
echo '<td>' . htmlspecialchars($category['description'] ?: '-') . '</td>';
echo '<td>' . $category['sort_order'] . '</td>';
echo '<td>';
echo '<span class="stat-number url-count" title="该分类下的网址数量">' . $stats['url_count'] . '</span>';
echo '</td>';
echo '<td>';
echo '<span class="stat-number rss-count" title="该分类下有RSS地址的网址数量">' . $stats['rss_count'] . '</span>';
echo '</td>';
echo '<td class="action-cell">';
echo '<a href="#" onclick="event.preventDefault(); event.stopPropagation(); window.urlnavEditCategory(' . $category['id'] . '); return false;" class="action-link edit-category" data-id="' . $category['id'] . '">' . _t('编辑') . '</a>';
echo '<a href="#" onclick="event.preventDefault(); event.stopPropagation(); window.urlnavDeleteCategory(' . $category['id'] . '); return false;" class="action-link delete delete-category" data-id="' . $category['id'] . '">' . _t('删除') . '</a>';
echo '</td>';
echo '</tr>';
}
}
?>
</tbody>
</table>
</div>
</form>
</div><!-- end #categories-tab -->
</div><!-- end .typecho-list -->
</div><!-- end .typecho-page-main -->
</div>
</div>
<!-- 网址模态框 -->
<div class="modal-overlay" id="url-modal-overlay">
<div class="modal-container" id="url-modal-container">
<div class="modal-header">
<h3 class="modal-title" id="url-modal-title">新增网址</h3>
<button type="button" class="modal-close" id="url-modal-close">&times;</button>
</div>
<div class="modal-body">
<div id="url-form-loading" style="display: none; text-align: center; padding: 40px; color: #666;">正在加载...</div>
<form id="url-form">
<div class="form-group">
<label class="form-label required" for="url-title">网站标题</label>
<input type="text" id="url-title" name="title" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label required" for="url-url">网站地址</label>
<div style="display: flex; gap: 10px;">
<input type="url" id="url-url" name="url" class="form-input"
placeholder="https://example.com" required style="flex: 1;">
<button type="button" id="fetch-rss-btn" class="fetch-rss-btn" style="white-space: nowrap;">
<span class="fetch-rss-text">获取网站信息</span>
<span class="fetching-spinner" style="display: none;"></span>
</button>
</div>
<div class="form-help">请输入完整的网址包含http://或https://</div>
</div>
<!-- RSS地址自动获取区域 -->
<div class="rss-autofetch-section" id="rss-autofetch-section" style="display: none;">
<h4>检测到的RSS地址</h4>
<div class="rss-urls-list" id="rss-urls-list">
<!-- RSS地址列表将动态加载到这里 -->
</div>
<div style="font-size: 12px; color: #666; margin-bottom: 10px;">
点击上面的RSS地址进行选择选中的地址将自动填入下面的RSS地址字段
</div>
</div>
<div class="form-group">
<label class="form-label" for="url-rss-url">RSS地址</label>
<input type="url" id="url-rss-url" name="rss_url" class="form-input"
placeholder="https://example.com/feed">
<div class="form-help">网站的RSS/Atom订阅地址可选可自动获取</div>
</div>
<div class="form-group">
<label class="form-label" for="url-description">网站描述</label>
<textarea id="url-description" name="description" class="form-textarea"></textarea>
</div>
<div class="form-group">
<label class="form-label" for="url-star-rating">星级评分</label>
<select id="url-star-rating" name="star_rating" class="form-select">
<?php
$starOptions = UrlNav_Plugin::getStarRatingOptions();
foreach ($starOptions as $value => $label) {
echo '<option value="' . $value . '">' . htmlspecialchars($label) . '</option>';
}
?>
</select>
<div class="form-help">0-3颗星,用于标识重要程度</div>
</div>
<div class="form-group">
<label class="form-label" for="url-category">所属分类</label>
<select id="url-category" name="category_id" class="form-select">
<option value="">未分类</option>
<?php foreach ($categories as $category): ?>
<option value="<?php echo $category['id']; ?>"><?php echo htmlspecialchars($category['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label" for="url-sort">排序</label>
<input type="number" id="url-sort" name="sort_order" class="form-input" value="0">
<div class="form-help">数字越大排序越靠前</div>
</div>
<input type="hidden" id="url-id" name="id" value="">
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn" id="url-modal-cancel">取消</button>
<button type="button" class="btn primary" id="url-modal-save">保存</button>
</div>
</div>
</div>
<!-- 分类模态框 -->
<div class="modal-overlay" id="category-modal-overlay">
<div class="modal-container" id="category-modal-container">
<div class="modal-header">
<h3 class="modal-title" id="category-modal-title">新增分类</h3>
<button type="button" class="modal-close" id="category-modal-close">&times;</button>
</div>
<div class="modal-body">
<div id="category-form-loading" style="display: none; text-align: center; padding: 40px; color: #666;">正在加载...</div>
<form id="category-form">
<div class="form-group">
<label class="form-label required" for="category-name">分类名称</label>
<input type="text" id="category-name" name="name" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label" for="category-description">分类描述</label>
<textarea id="category-description" name="description" class="form-textarea"></textarea>
</div>
<div class="form-group">
<label class="form-label" for="category-sort">排序</label>
<input type="number" id="category-sort" name="sort_order" class="form-input" value="0">
<div class="form-help">数字越小排序越靠前</div>
</div>
<input type="hidden" id="category-id" name="id" value="">
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn" id="category-modal-cancel">取消</button>
<button type="button" class="btn primary" id="category-modal-save">保存</button>
</div>
</div>
</div>
<!-- 导入OPML模态框 -->
<div class="modal-overlay" id="import-modal-overlay">
<div class="modal-container" id="import-modal-container">
<div class="modal-header">
<h3 class="modal-title">导入OPML文件</h3>
<button type="button" class="modal-close" id="import-modal-close">&times;</button>
</div>
<div class="modal-body">
<form id="import-form" enctype="multipart/form-data">
<div class="form-group">
<p style="margin-bottom: 15px; color: #666;">
支持标准的OPML格式文件导入将自动创建分类和RSS订阅。
</p>
<div style="background: #f8f9fa; padding: 15px; border-radius: 6px; margin-bottom: 15px; border: 1px solid #e8ebf0;">
<h4 style="margin-top: 0; margin-bottom: 10px; color: #2c3e50;">导入说明</h4>
<ul style="margin: 0; padding-left: 20px; color: #666; font-size: 13px;">
<li>支持标准的OPML 2.0格式</li>
<li>如果分类不存在会自动创建</li>
<li>自动跳过重复的网址和RSS地址</li>
<li>建议文件大小不超过2MB</li>
<li>导入过程可能需要几秒钟时间</li>
</ul>
</div>
</div>
<div class="form-group">
<label class="form-label required" for="opml-file">选择OPML文件</label>
<input type="file" id="opml-file" name="opml_file" class="form-input"
accept=".xml,.opml,.opml.xml" required>
<div class="form-help">支持 .xml, .opml, .opml.xml 格式文件</div>
</div>
<div class="form-group">
<label class="form-label">导入选项</label>
<div style="display: flex; flex-direction: column; gap: 10px;">
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
<input type="checkbox" name="skip_duplicates" checked>
<span style="font-size: 14px;color:#000;">跳过重复的网址</span>
</label>
<label style="display: flex; align-items: center;gap: 8px; cursor: pointer;">
<input type="checkbox" name="auto_create_categories" checked>
<span style="font-size: 14px;color:#000;">自动创建不存在的分类</span>
</label>
</div>
</div>
<div id="import-preview" style="display: none; margin-top: 15px;">
<h4 style="margin-top: 0; margin-bottom: 10px; color: #2c3e50;">文件预览</h4>
<div style="background: #f8f9fa; padding: 15px;border-radius: 6px; max-height: 200px; overflow-y: auto; font-family: monospace; font-size: 12px; color: #666; border: 1px solid #e8ebf0;">
<pre id="file-preview-content"></pre>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn" id="import-modal-cancel">取消</button>
<button type="button" class="btn primary" id="import-modal-start" disabled>开始导入</button>
</div>
</div>
</div>
<!-- 导入结果模态框 -->
<div class="modal-overlay" id="import-result-overlay">
<div class="modal-container" id="import-result-container">
<div class="modal-header">
<h3 class="modal-title">导入结果</h3>
<button type="button" class="modal-close" id="import-result-close">&times;</button>
</div>
<div class="modal-body">
<div id="import-result-content">
<!-- 导入结果将动态显示在这里 -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn" id="import-result-close-btn">关闭</button>
<button type="button" class="btn primary" id="import-result-refresh">刷新页面</button>
</div>
</div>
</div>
<!-- 状态检查定时任务状态监控 -->
<div class="modal-overlay" id="cron-status-overlay">
<div class="modal-container" id="cron-status-container" style="max-width: 800px;">
<div class="modal-header">
<h3 class="modal-title">定时任务</h3>
<button type="button" class="modal-close" id="cron-status-close">&times;</button>
</div>
<div class="modal-body">
<div class="cron-info-section">
<!--<h4 style="color:#000;">定时任务</h4>-->
<div class="cron-stats" id="status-cron-stats">
<div style="text-align: center; padding: 20px; color: #666;">加载中...</div>
</div>
<div class="cron-urls">
<h5 style="color:#000;">定时任务URL</h5>
<div class="url-box">
<input type="text" readonly value="<?php
echo $options->siteUrl . 'urlnav-status-cron?secret=' . $pluginOptions->statusCronSecret;
?>" id="status-cron-url" class="url-input">
<button class="btn small copy-btn" data-clipboard-target="#status-cron-url">复制</button>
</div>
<div class="form-help">将此URL添加到宝塔计划任务中用于自动检查网站状态</div>
</div>
<div class="cron-actions">
<button class="btn" id="refresh-status-cron-stats">刷新统计</button>
<button class="btn" id="run-status-cron-btn">立即运行</button>
</div>
</div>
<div class="cron-logs-section" style="margin-top: 20px;margin-bottom:30px;">
<!--<h4 style="color:#000;">近期日志</h4>-->
<div class="logs-container" id="status-cron-logs">
<div style="text-align: center; padding: 20px; color: #666;">加载中...</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn" id="cron-status-close-btn">关闭</button>
</div>
</div>
</div>
<script>
// 设置插件配置
window.UrlNavConfig = {
actionUrl: '<?php echo Typecho_Common::url('/action/urlnav', $options->index); ?>',
// 状态检查相关
statusCheckInterval: <?php echo isset($pluginOptions->statusCheckInterval) ? intval($pluginOptions->statusCheckInterval) : 30; ?>,
batchCheckSize: 30 // 每次批量检查数量
};
// ========== 全局函数定义 ==========
// 编辑网址 - 优化加载体验
window.urlnavEditUrl = function(id) {
if (!id) {
console.error('网址ID不能为空');
return false;
}
console.log('编辑网址ID:', id);
// 先显示模态框
document.getElementById('url-modal-title').textContent = '编辑网址';
document.getElementById('url-modal-overlay').style.display = 'flex';
document.getElementById('url-modal-container').style.display = 'block';
// 显示模态框中的加载状态
$('#url-form').hide();
$('#url-form-loading').show();
// 发送请求获取网址信息
$.ajax({
url: window.UrlNavConfig.actionUrl,
type: 'GET',
data: {do: 'getUrl', id: id},
dataType: 'json',
success: function(response) {
// 移除加载状态
$('#url-form-loading').hide();
$('#url-form').show();
if (response.success) {
// 填充表单
document.getElementById('url-id').value = response.data.id || '';
document.getElementById('url-title').value = response.data.title || '';
document.getElementById('url-url').value = response.data.url || '';
document.getElementById('url-rss-url').value = response.data.rss_url || '';
document.getElementById('url-description').value = response.data.description || '';
document.getElementById('url-category').value = response.data.category_id || '';
document.getElementById('url-sort').value = response.data.sort_order || 0;
document.getElementById('url-star-rating').value = response.data.star_rating || '0';
// 编辑模式下显示RSS自动获取区域
document.getElementById('rss-autofetch-section').style.display = 'block';
document.getElementById('rss-urls-list').innerHTML = '<div class="rss-url-item" style="text-align: center; padding: 20px; color: #666;">点击"获取网站信息"按钮获取RSS地址</div>';
console.log('网址数据加载完成');
// 聚焦到标题输入框
setTimeout(function() {
$('#url-title').focus();
}, 100);
} else {
alert(response.message || '获取网址信息失败');
hideUrlModal();
}
},
error: function(xhr, status, error) {
// 移除加载状态
$('#url-form-loading').hide();
$('#url-form').show();
alert('获取网址信息失败: ' + error);
hideUrlModal();
}
});
return false;
};
// 删除网址
window.urlnavDeleteUrl = function(id) {
if (!id) {
console.error('网址ID不能为空');
return false;
}
if (!confirm('你确认要删除这个网址吗?')) {
return false;
}
console.log('删除网址ID:', id);
// 显示加载动画
var loadingOverlay = document.getElementById('loading-overlay');
if (loadingOverlay) loadingOverlay.style.display = 'flex';
// 发送删除请求
$.ajax({
url: window.UrlNavConfig.actionUrl,
type: 'POST',
data: {do: 'deleteUrl', id: id},
dataType: 'json',
success: function(response) {
// 隐藏加载动画
if (loadingOverlay) loadingOverlay.style.display = 'none';
if (response.success) {
alert(response.message || '删除成功');
window.location.reload();
} else {
alert(response.message || '删除失败');
}
},
error: function(xhr, status, error) {
// 隐藏加载动画
if (loadingOverlay) loadingOverlay.style.display = 'none';
alert('删除失败: ' + error);
}
});
return false;
};
// 编辑分类
window.urlnavEditCategory = function(id) {
if (!id) {
console.error('分类ID不能为空');
return false;
}
console.log('编辑分类ID:', id);
// 先显示模态框
document.getElementById('category-modal-title').textContent = '编辑分类';
document.getElementById('category-modal-overlay').style.display = 'flex';
document.getElementById('category-modal-container').style.display = 'block';
// 显示模态框中的加载状态
$('#category-form').hide();
$('#category-form-loading').show();
// 发送请求获取分类信息
$.ajax({
url: window.UrlNavConfig.actionUrl,
type: 'GET',
data: {do: 'getCategory', id: id},
dataType: 'json',
success: function(response) {
// 移除加载状态
$('#category-form-loading').hide();
$('#category-form').show();
if (response.success) {
// 填充表单
document.getElementById('category-id').value = response.data.id || '';
document.getElementById('category-name').value = response.data.name || '';
document.getElementById('category-description').value = response.data.description || '';
document.getElementById('category-sort').value = response.data.sort_order || 0;
console.log('分类数据加载完成');
// 聚焦到名称输入框
setTimeout(function() {
$('#category-name').focus();
}, 100);
} else {
alert(response.message || '获取分类信息失败');
hideCategoryModal();
}
},
error: function(xhr, status, error) {
// 移除加载状态
$('#category-form-loading').hide();
$('#category-form').show();
alert('获取分类信息失败: ' + error);
hideCategoryModal();
}
});
return false;
};
// 删除分类
window.urlnavDeleteCategory = function(id) {
if (!id) {
console.error('分类ID不能为空');
return false;
}
if (!confirm('你确认要删除这个分类吗?')) {
return false;
}
console.log('删除分类ID:', id);
// 显示加载动画
var loadingOverlay = document.getElementById('loading-overlay');
if (loadingOverlay) loadingOverlay.style.display = 'flex';
// 发送删除请求
$.ajax({
url: window.UrlNavConfig.actionUrl,
type: 'POST',
data: {do: 'deleteCategory', id: id},
dataType: 'json',
success: function(response) {
// 隐藏加载动画
if (loadingOverlay) loadingOverlay.style.display = 'none';
if (response.success) {
alert(response.message || '删除成功');
window.location.reload();
} else {
alert(response.message || '删除失败');
}
},
error: function(xhr, status, error) {
// 隐藏加载动画
if (loadingOverlay) loadingOverlay.style.display = 'none';
alert('删除失败: ' + error);
}
});
return false;
};
// 选择RSS地址全局函数供HTML调用
function selectRssUrl(element) {
// 移除之前的选择
var items = document.querySelectorAll('.rss-url-item');
items.forEach(function(item) {
item.classList.remove('selected');
});
// 添加当前选择
element.classList.add('selected');
// 获取RSS地址
var rssUrl = element.getAttribute('data-url');
document.getElementById('url-rss-url').value = rssUrl;
// 显示成功消息
var alert = document.getElementById('message-alert');
alert.className = 'alert success';
alert.innerHTML = '已选择RSS地址' + rssUrl;
alert.style.display = 'block';
setTimeout(function() {
alert.style.display = 'none';
}, 2000);
}
// ========== 其他功能 ==========
// 使用立即执行函数确保代码安全执行
(function($) {
'use strict';
// 全局变量
var currentTab = 'urls';
var isEditMode = false;
var isFetchingRss = false;
var selectedRssUrl = null;
var autoCheckTimer = null;
// 初始化函数
function initUrlNav() {
console.log('UrlNav插件管理页面初始化');
// 记录页面加载时间
window.pageLoadStartTime = Date.now();
// 修复全选复选框功能 - 确保选中状态同步
function syncCheckAllState() {
var currentTab = $('.tab-content.active').attr('id');
var allCheckboxes, allChecked;
if (currentTab === 'urls-tab') {
allCheckboxes = $('#url-list input[name="url[]"]');
} else if (currentTab === 'categories-tab') {
allCheckboxes = $('#category-list input[name="category[]"]');
} else {
return;
}
// 如果没有复选框,全选框设为未选中
if (allCheckboxes.length === 0) {
$('.typecho-table-select-all').prop('checked', false);
return;
}
// 检查是否所有复选框都被选中
allChecked = allCheckboxes.length === allCheckboxes.filter(':checked').length;
// 设置全选复选框状态
$('.typecho-table-select-all').prop('checked', allChecked);
}
// 点击全选复选框
$('.typecho-table-select-all').off('change').on('change', function() {
var isChecked = $(this).is(':checked');
var currentTab = $('.tab-content.active').attr('id');
console.log('全选复选框变化: ' + isChecked + ', 当前标签页: ' + currentTab);
if (currentTab === 'urls-tab') {
$('#url-list input[name="url[]"]').prop('checked', isChecked);
} else if (currentTab === 'categories-tab') {
$('#category-list input[name="category[]"]').prop('checked', isChecked);
}
});
// 点击单个复选框时更新全选状态
$(document).off('change').on('change', 'input[name="url[]"], input[name="category[]"]', function() {
console.log('单个复选框变化');
syncCheckAllState();
});
// 切换标签页时更新
$('.tab-button').off('click').on('click', function() {
setTimeout(function() {
console.log('切换标签页,更新全选状态');
syncCheckAllState();
}, 100);
});
// 初始同步状态
setTimeout(function() {
console.log('初始同步全选状态');
syncCheckAllState();
}, 500);
// 选项卡切换
$('.tab-button').off('click').on('click', function() {
var tab = $(this).data('tab');
if (tab) {
switchTab(tab);
}
});
// 修复问题1下拉菜单显示
$('.dropdown-toggle').off('click').on('click', function(e) {
e.preventDefault();
e.stopPropagation();
$(this).next('.dropdown-menu').toggleClass('show');
});
// 点击其他地方关闭下拉菜单
$(document).off('click').on('click', function(e) {
if (!$(e.target).closest('.btn-drop').length) {
$('.dropdown-menu').removeClass('show');
}
});
// 新增网址按钮 - 确保只绑定一次
$('#add-url-btn').off('click').on('click', function(e) {
e.preventDefault();
console.log('新增网址按钮点击');
showUrlModal();
});
// 新增分类按钮 - 确保只绑定一次
$('#add-category-btn').off('click').on('click', function(e) {
e.preventDefault();
console.log('新增分类按钮点击');
showCategoryModal();
});
// 检查状态按钮 - 新增
$('#check-status-btn').off('click').on('click', function(e) {
e.preventDefault();
checkWebsiteStatus();
});
// 定时任务状态按钮
$('#cron-status-btn').off('click').on('click', function(e) {
e.preventDefault();
showCronStatus();
});
// 网址模态框关闭
$('#url-modal-close, #url-modal-cancel').off('click').on('click', function(e) {
e.preventDefault();
hideUrlModal();
});
// 分类模态框关闭
$('#category-modal-close, #category-modal-cancel').off('click').on('click', function(e) {
e.preventDefault();
hideCategoryModal();
});
// 点击模态框背景关闭
$('.modal-overlay').off('click').on('click', function(e) {
if (e.target === this) {
if ($(this).attr('id') === 'url-modal-overlay') {
hideUrlModal();
} else if ($(this).attr('id') === 'category-modal-overlay') {
hideCategoryModal();
} else if ($(this).attr('id') === 'cron-status-overlay') {
hideCronStatus();
}
}
});
// 保存按钮 - 确保只绑定一次
$('#url-modal-save').off('click').on('click', function(e) {
e.preventDefault();
console.log('网址保存按钮点击');
saveUrl();
});
$('#category-modal-save').off('click').on('click', function(e) {
e.preventDefault();
console.log('分类保存按钮点击');
saveCategory();
});
// 获取RSS按钮 - 确保只绑定一次
$('#fetch-rss-btn').off('click').on('click', function(e) {
e.preventDefault();
console.log('获取RSS按钮点击');
fetchRssFromUrl();
});
// 批量删除 - 确保只绑定一次
$('#batch-delete-urls-btn').off('click').on('click', function(e) {
e.preventDefault();
console.log('批量删除网址按钮点击');
$('.dropdown-menu').removeClass('show');
setTimeout(function() {
batchDeleteUrls();
}, 50);
});
$('#batch-delete-categories-btn').off('click').on('click', function(e) {
e.preventDefault();
console.log('批量删除分类按钮点击');
$('.dropdown-menu').removeClass('show');
setTimeout(function() {
batchDeleteCategories();
}, 50);
});
// 分类筛选
$('#category-filter').off('change').on('change', function() {
var categoryId = $(this).val();
var url = '<?php echo \Typecho\Common::url('extending.php?panel=UrlNav/Manage.php', $options->adminUrl); ?>';
if (categoryId) {
url += '&category=' + encodeURIComponent(categoryId);
}
window.location.href = url;
});
// 状态筛选
$('#status-filter').off('change').on('change', function() {
var status = $(this).val();
var url = '<?php echo \Typecho\Common::url('extending.php?panel=UrlNav/Manage.php', $options->adminUrl); ?>';
var category = $('#category-filter').val();
if (status) {
url += '&status=' + encodeURIComponent(status);
}
if (category) {
url += '&category=' + encodeURIComponent(category);
}
window.location.href = url;
});
// 星级筛选
$('#star-filter').off('change').on('change', function() {
var starRating = $(this).val();
var url = '<?php echo \Typecho\Common::url('extending.php?panel=UrlNav/Manage.php', $options->adminUrl); ?>';
var category = $('#category-filter').val();
if (starRating) {
url += '&star_rating=' + encodeURIComponent(starRating);
}
if (category) {
url += '&category=' + encodeURIComponent(category);
}
window.location.href = url;
});
// RSS筛选
$('#rss-filter').off('change').on('change', function() {
var hasRss = $(this).val();
var url = '<?php echo \Typecho\Common::url('extending.php?panel=UrlNav/Manage.php', $options->adminUrl); ?>';
var category = $('#category-filter').val();
if (hasRss) {
url += '&has_rss=' + encodeURIComponent(hasRss);
}
if (category) {
url += '&category=' + encodeURIComponent(category);
}
window.location.href = url;
});
// 导入OPML按钮
$('#import-opml-btn').off('click').on('click', function(e) {
e.preventDefault();
showImportModal();
});
// 导入模态框关闭
$('#import-modal-close, #import-modal-cancel').off('click').on('click', function(e) {
e.preventDefault();
hideImportModal();
});
// 导入结果模态框关闭
$('#import-result-close, #import-result-close-btn').off('click').on('click', function(e) {
e.preventDefault();
hideImportResultModal();
});
// 点击模态框背景关闭
$('#import-modal-overlay, #import-result-overlay').off('click').on('click', function(e) {
if (e.target === this) {
if ($(this).attr('id') === 'import-modal-overlay') {
hideImportModal();
} else {
hideImportResultModal();
}
}
});
// 开始导入按钮
$('#import-modal-start').off('click').on('click', function(e) {
e.preventDefault();
startImport();
});
// 刷新页面按钮
$('#import-result-refresh').off('click').on('click', function(e) {
e.preventDefault();
window.location.reload();
});
// 文件选择预览
$('#opml-file').off('change').on('change', function(e) {
var file = this.files[0];
if (file) {
previewFile(file);
}
});
// 定时任务状态模态框关闭
$('#cron-status-close, #cron-status-close-btn').off('click').on('click', function(e) {
e.preventDefault();
hideCronStatus();
});
// 刷新状态检查定时任务统计
$('#refresh-status-cron-stats').off('click').on('click', function() {
loadStatusCronStats();
});
// 立即运行定时任务按钮
$('#run-status-cron-btn').off('click').on('click', function(e) {
e.preventDefault();
runStatusCronNow();
});
// 阻止所有#链接的默认行为
$(document).off('click', 'a[href="#"]').on('click', 'a[href="#"]', function(e) {
e.preventDefault();
e.stopPropagation();
});
// 页面加载时不自动检查状态,改为手动或定时任务检查
// 只加载状态统计
loadStatusStats();
// 清空之前的定时器
if (autoCheckTimer) {
clearTimeout(autoCheckTimer);
}
// 修复:状态圆圈点击事件绑定 - 简化版本
$(document).on('click', '.status-indicator', function(e) {
e.preventDefault();
e.stopPropagation();
var $this = $(this);
var urlId = $this.data('id');
if (!urlId) {
console.error('缺少网址ID数据');
return;
}
console.log('点击检查单个网址ID:', urlId);
// 使用统一的单网址检查函数
checkSingleWebsiteStatus(urlId, $this);
});
}
// 修复检查单个网址的函数
function checkSingleWebsiteStatus(urlId, $indicator) {
if (!$indicator) {
$indicator = $('.status-indicator[data-id="' + urlId + '"]');
}
// 保存原始状态
var originalClass = $indicator.attr('class');
// 设置为检查中状态
$indicator.removeClass('status-online status-offline status-unknown')
.addClass('status-checking');
$.ajax({
url: window.UrlNavConfig.actionUrl,
type: 'POST',
data: {
do: 'checkSingleStatus',
id: urlId
},
dataType: 'json',
success: function(response) {
if (response.success && response.data) {
// 移除检查中状态
$indicator.removeClass('status-checking');
if (response.data.success) {
// 设置在线状态
$indicator.addClass('status-online').removeClass('status-offline status-unknown');
var title = '通连';
if (response.data.status_code && response.data.status_code > 0) {
title += ' (HTTP ' + response.data.status_code + ')';
}
if (response.data.response_time) {
title += ' - 响应: ' + response.data.response_time + 'ms';
}
title += ' - ' + new Date().toLocaleDateString();
$indicator.attr('title', title);
showMessage('网址检查完成:连接正常', 'success');
} else {
// 设置离线状态
$indicator.addClass('status-offline').removeClass('status-online status-unknown');
var title = '失连: ' + (response.data.message || '访问失败');
if (response.data.status_code && response.data.status_code > 0) {
title += ' (HTTP ' + response.data.status_code + ')';
}
title += ' - ' + new Date().toLocaleDateString();
$indicator.attr('title', title);
showMessage('网址检查完成:连接失败', 'error');
}
// 刷新状态统计
loadStatusStats();
} else {
// 恢复原始状态
$indicator.removeClass('status-checking').addClass(originalClass);
showMessage('检查失败: ' + (response.message || '未知错误'), 'error');
}
},
error: function(xhr, status, error) {
// 恢复原始状态
$indicator.removeClass('status-checking').addClass(originalClass);
showMessage('检查请求失败: ' + error, 'error');
}
});
}
// 显示网址模态框
function showUrlModal() {
console.log('显示网址模态框');
// 重置表单(重要:确保是新增模式)
resetUrlForm();
// 确保标题是"新增网址"
$('#url-modal-title').text('新增网址');
// 清除编辑模式下可能遗留的ID
$('#url-id').val('');
// 显示模态框
$('#url-modal-overlay').fadeIn(200);
$('#url-modal-container').fadeIn(200);
// 确保RSS自动获取区域可见
$('#rss-autofetch-section').show();
setTimeout(function() {
$('#url-title').focus();
}, 300);
}
// 隐藏网址模态框
function hideUrlModal() {
$('#url-modal-container').fadeOut(200);
$('#url-modal-overlay').fadeOut(200);
}
// 重置网址表单
function resetUrlForm() {
console.log('重置网址表单');
// 重置表单所有字段
$('#url-form')[0].reset();
// 明确设置默认值
$('#url-id').val('');
$('#url-title').val('');
$('#url-url').val('');
$('#url-rss-url').val('');
$('#url-description').val('');
$('#url-category').val(''); // 设置为空,不是'0'
$('#url-sort').val('0');
// 重置RSS区域
$('#rss-autofetch-section').show();
$('#rss-urls-list').html('<div class="rss-url-item" style="text-align: center; padding: 20px; color: #666;">填写网址后,点击"获取网站信息"按钮</div>');
// 重置状态变量
selectedRssUrl = null;
isEditMode = false;
isFetchingRss = false;
updateFetchRssButton();
}
// 显示分类模态框
function showCategoryModal() {
resetCategoryForm();
$('#category-modal-title').text('新增分类');
$('#category-modal-overlay').fadeIn(200);
$('#category-modal-container').fadeIn(200);
setTimeout(function() {
$('#category-name').focus();
}, 300);
}
// 隐藏分类模态框
function hideCategoryModal() {
$('#category-modal-container').fadeOut(200);
$('#category-modal-overlay').fadeOut(200);
}
// 重置分类表单
function resetCategoryForm() {
$('#category-form')[0].reset();
$('#category-id').val('');
$('#category-name').val('');
$('#category-description').val('');
$('#category-sort').val('0');
isEditMode = false;
}
// 显示定时任务状态
function showCronStatus() {
$('#cron-status-overlay').fadeIn(200);
$('#cron-status-container').fadeIn(200);
loadStatusCronStats();
}
// 隐藏定时任务状态
function hideCronStatus() {
$('#cron-status-container').fadeOut(200);
$('#cron-status-overlay').fadeOut(200);
}
// 加载状态检查定时任务统计
function loadStatusCronStats() {
showLoading();
$.ajax({
url: window.UrlNavConfig.actionUrl,
type: 'GET',
data: {do: 'getStatusCronStats'},
dataType: 'json',
success: function(response) {
hideLoading();
if (response.success) {
updateStatusCronStats(response.data);
loadStatusCronLogs();
showMessage('状态检查定时任务统计已刷新', 'success');
} else {
$('#status-cron-stats').html('<div class="alert error">加载失败: ' + response.message + '</div>');
showMessage('刷新失败: ' + response.message, 'error');
}
},
error: function(xhr, status, error) {
hideLoading();
$('#status-cron-stats').html('<div class="alert error">加载失败</div>');
showMessage('刷新失败: ' + error, 'error');
}
});
}
// 更新状态检查定时任务统计显示
function updateStatusCronStats(stats) {
var html = '';
html += '<div class="cron-stat-item">';
html += '<div class="cron-stat-value"style="color:#000;">' + stats.total + '</div>';
html += '<div class="cron-stat-label" >总执行次数</div>';
html += '</div>';
html += '<div class="cron-stat-item">';
html += '<div class="cron-stat-value" style="color:#28a745;">' + stats.success + '</div>';
html += '<div class="cron-stat-label">成功次数</div>';
html += '</div>';
html += '<div class="cron-stat-item">';
html += '<div class="cron-stat-value" style="color:#dc3545;">' + stats.failed + '</div>';
html += '<div class="cron-stat-label">失败次数</div>';
html += '</div>';
html += '<div class="cron-stat-item">';
html += '<div class="cron-stat-value" style="color:#000;">' + stats.success_rate + '%</div>';
html += '<div class="cron-stat-label">成功率</div>';
html += '</div>';
$('#status-cron-stats').html(html);
}
// 加载状态检查定时任务日志(修复版)
function loadStatusCronLogs() {
// 显示加载状态
$('#status-cron-logs').html('<div style="text-align: center; padding: 20px; color: #6c757d;">加载中...</div>');
$.ajax({
url: window.UrlNavConfig.actionUrl,
type: 'POST', // 改为POST
data: {do: 'getCronLogs', type: 'status', limit: 10},
dataType: 'json',
success: function(response) {
if (response.success && response.data) {
// 调用更新函数
updateStatusCronLogs(response.data);
} else {
$('#status-cron-logs').html('<div class="cron-log-item">加载失败: ' + (response.message || '未知错误') + '</div>');
}
},
error: function(xhr, status, error) {
console.error('加载状态检查日志失败:', error);
$('#status-cron-logs').html('<div class="cron-log-item">加载失败: ' + error + '</div>');
}
});
}
// HTML转义函数
function escapeHtml(text) {
if (text == null || text === '') return '';
return String(text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
// 优化版本:移除跳转功能,去掉高度限制和滚动条,显示所有内容
function updateStatusCronLogs(logs) {
try {
console.log('开始更新状态检查日志,日志数量:', logs.length);
var html = '';
if (logs.length === 0) {
html = '<div class="cron-log-item">暂无执行日志</div>';
} else {
logs.forEach(function(log, index) {
console.log('处理第', index, '条日志:', log);
var result = null;
try {
result = JSON.parse(log.result);
console.log('日志解析成功:', result);
} catch (e) {
console.error('解析JSON失败:', e, '原始内容:', log.result);
result = {message: log.result};
}
// 转换时间为北京时间
var beijingTime = convertToBeijingTime(log.executed_time);
var beijingTimeStr = formatDateTime(beijingTime);
var localTimeStr = formatDateTime(new Date(log.executed_time));
html += '<div class="cron-log-item">';
html += '<div>';
html += '<span class="cron-log-time" title="服务器时间: ' + localTimeStr + '">' + beijingTimeStr + '</span>';
html += '</div>';
// 安全显示日志消息
var logMessage = log.error_message || (result && result.message) || log.result || '执行成功';
html += '<div class="cron-log-message">' + escapeHtml(String(logMessage)) + '</div>';
// 🔴 只显示有具体失败网址的信息,不显示纯统计信息
if (result) {
// 尝试从日志结果中提取失败网址
var failedUrls = result.failedUrls || result.failed_urls || [];
console.log('第' + index + '条日志失败网址数量:', failedUrls.length);
// 只有有具体的失败网址时才显示
if (failedUrls.length > 0) {
console.log('显示失败网址,数量:', failedUrls.length);
html += '<div style="margin-top: 10px; padding: 10px; background: #fff5f5; border-radius: 6px; border-left: 4px solid #dc3545;">';
html += '<div style="font-size: 12px; color: #721c24; margin-bottom: 8px; font-weight: 500;">有问题网站 (' + failedUrls.length + '个)</div>';
html += '<div style="padding-right: 5px;">'; // 移除高度限制和overflow
// 显示所有失败的网址
failedUrls.forEach(function(site, i) {
var siteName = site.title || site.url || '未知网站';
// 截断过长的网站名称
if (siteName.length > 40) {
siteName = siteName.substring(0, 40) + '...';
}
// 错误信息处理
var errorMsg = site.error || '未知错误';
if (errorMsg.length > 60) {
errorMsg = errorMsg.substring(0, 60) + '...';
}
// 转义HTML字符
siteName = escapeHtml(siteName);
errorMsg = escapeHtml(errorMsg);
html += '<div style="padding: 6px 8px; margin-bottom: 6px; background: white; border: 1px solid #f8d7da; border-radius: 4px; transition: all 0.2s;">';
html += '<div>';
html += '<div style="display: flex; align-items: center; margin-bottom: 4px;">';
html += '<span style="font-weight: 500; color: #721c24; font-size: 11px;">' + siteName + '</span>';
html += '<span style="margin-left: 8px; color: #dc3545; font-size: 10px; background: #f8d7da; padding: 1px 4px; border-radius: 2px;">' + errorMsg + '</span>';
html += '</div>';
if (site.url) {
html += '<div style="color: #6c757d; font-size: 10px; word-break: break-all; line-height: 1.4;">' + escapeHtml(String(site.url)) + '</div>';
}
html += '</div>';
html += '</div>';
});
html += '</div>';
html += '</div>';
}
// 不再显示纯统计信息,避免重复难看
}
html += '</div>';
});
}
console.log('生成的HTML长度:', html.length);
$('#status-cron-logs').html(html);
console.log('更新完成');
} catch (error) {
console.error('更新状态检查日志时出错:', error);
$('#status-cron-logs').html('<div class="cron-log-item">显示日志时出错: ' + error.message + '</div>');
}
}
// 新增:将任意时间转换为北京时间
function convertToBeijingTime(timeString) {
var date = new Date(timeString);
// 如果已经是无效日期,返回原始值
if (isNaN(date.getTime())) {
return timeString;
}
// 将UTC时间转换为北京时间UTC+8
// 方法1如果服务器时间是UTC时间
var beijingTime = new Date(date.getTime() + (8 * 60 * 60 * 1000));
// 方法2更精确的转换处理夏令时等
// var beijingTime = new Date(date.toLocaleString('en-US', { timeZone: 'Asia/Shanghai' }));
return beijingTime;
}
// 新增:格式化日期时间
function formatDateTime(date) {
if (!(date instanceof Date) || isNaN(date.getTime())) {
return '无效时间';
}
var year = date.getFullYear();
var month = String(date.getMonth() + 1).padStart(2, '0');
var day = String(date.getDate()).padStart(2, '0');
var hours = String(date.getHours()).padStart(2, '0');
var minutes = String(date.getMinutes()).padStart(2, '0');
var seconds = String(date.getSeconds()).padStart(2, '0');
return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds;
}
// 显示加载动画
function showLoading() {
$('#loading-overlay').fadeIn(200);
}
// 隐藏加载动画
function hideLoading() {
$('#loading-overlay').fadeOut(200);
}
// 显示消息
function showMessage(message, type) {
var alert = $('#message-alert');
alert.removeClass('success error warning info').addClass(type);
alert.html(message).fadeIn(300);
setTimeout(function() {
alert.fadeOut(300);
}, 3000);
}
// 更新获取RSS按钮状态
function updateFetchRssButton() {
var btn = $('#fetch-rss-btn');
var text = $('.fetch-rss-text');
var spinner = $('.fetching-spinner');
if (isFetchingRss) {
btn.prop('disabled', true);
text.text('获取中...');
spinner.show();
} else {
btn.prop('disabled', false);
text.text('获取网站信息');
spinner.hide();
}
}
// 从URL获取网站信息和RSS地址
function fetchRssFromUrl() {
var url = $('#url-url').val().trim();
var isEditMode = !!$('#url-id').val(); // 检测是否是编辑模式
if (!url) {
showMessage('请先填写网站地址', 'warning');
$('#url-url').focus();
return;
}
// 验证URL格式
var urlPattern = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/;
if (!urlPattern.test(url)) {
showMessage('网站地址格式无效', 'warning');
return;
}
// 确保URL有协议前缀
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'https://' + url;
$('#url-url').val(url);
}
isFetchingRss = true;
updateFetchRssButton();
$('#rss-autofetch-section').show();
$('#rss-urls-list').html('<div class="rss-url-item" style="text-align: center; padding: 20px; color: #666;">正在获取网站信息...</div>');
$.ajax({
url: window.UrlNavConfig.actionUrl,
type: 'GET',
data: {do: 'fetchRss', url: url},
dataType: 'json',
success: function(response) {
isFetchingRss = false;
updateFetchRssButton();
if (response.success) {
// 1. 自动填充网站标题(如果是新增模式或者用户确认)
var currentTitle = $('#url-title').val();
var shouldUpdateTitle = false;
if (isEditMode) {
// 编辑模式下询问用户是否更新标题
if (currentTitle !== response.siteInfo.title && response.siteInfo.title) {
if (confirm('检测到网站标题与当前不同:\n当前: ' + currentTitle + '\n新标题: ' + response.siteInfo.title + '\n\n是否更新网站标题')) {
shouldUpdateTitle = true;
}
}
} else {
// 新增模式下直接更新
shouldUpdateTitle = true;
}
if (shouldUpdateTitle && response.siteInfo && response.siteInfo.title) {
$('#url-title').val(response.siteInfo.title);
}
// 2. 自动填充网站描述(如果为空或用户确认)
var currentDescription = $('#url-description').val();
if (response.siteInfo && response.siteInfo.description) {
if (!currentDescription || currentDescription === '-') {
// 描述为空,直接填充
$('#url-description').val(response.siteInfo.description);
} else if (isEditMode) {
// 编辑模式下,如果描述不同,询问用户
if (currentDescription !== response.siteInfo.description) {
if (confirm('检测到网站描述与当前不同,是否更新网站描述?')) {
$('#url-description').val(response.siteInfo.description);
}
}
}
}
// 3. 显示RSS地址如果有的话
if (response.hasRss && response.rssUrls && response.rssUrls.length > 0) {
var html = '';
response.rssUrls.forEach(function(rssUrl, index) {
var type = 'RSS';
if (rssUrl.includes('.atom')) type = 'ATOM';
if (rssUrl.includes('.rdf')) type = 'RDF';
html += '<div class="rss-url-item" data-url="' + rssUrl + '" onclick="selectRssUrl(this)">';
html += '<div class="url">' + rssUrl + '</div>';
html += '<div><span class="type">' + type + '</span></div>';
html += '</div>';
});
$('#rss-urls-list').html(html);
// 检查是否已经有RSS地址
var currentRssUrl = $('#url-rss-url').val();
if (!currentRssUrl && response.rssUrls.length === 1) {
// 如果没有RSS地址且只有一个选项自动选择
$('#url-rss-url').val(response.rssUrls[0]);
showMessage('已获取网站信息并自动选择一个RSS地址', 'success');
} else if (isEditMode && currentRssUrl && response.rssUrls.includes(currentRssUrl)) {
// 编辑模式下如果当前RSS地址在找到的列表中
showMessage('成功获取网站信息当前RSS地址有效', 'success');
} else {
showMessage('成功获取网站信息,找到 ' + response.rssUrls.length + ' 个RSS地址请点击选择', 'success');
}
} else {
$('#rss-urls-list').html('<div class="rss-url-item" style="text-align: center; padding: 20px; color: #666;">未检测到RSS地址请手动填写</div>');
if (isEditMode) {
showMessage('成功获取网站信息但未找到新的RSS地址', 'info');
} else {
showMessage('成功获取网站信息但未找到RSS地址', 'info');
}
}
} else {
$('#rss-urls-list').html('<div class="rss-url-item" style="text-align: center; padding: 20px; color: #666;">获取失败:' + (response.message || '未知错误') + '</div>');
showMessage('获取网站信息失败:' + response.message, 'error');
}
},
error: function(xhr, status, error) {
isFetchingRss = false;
updateFetchRssButton();
$('#rss-urls-list').html('<div class="rss-url-item" style="text-align: center; padding: 20px; color: #666;">获取失败:网络错误</div>');
showMessage('获取网站信息失败:网络错误', 'error');
}
});
}
// 保存网址 - 修复版本,不自动检查状态
function saveUrl() {
var id = $('#url-id').val();
isEditMode = !!id;
// 验证表单
var title = $('#url-title').val().trim();
var url = $('#url-url').val().trim();
var rssUrl = $('#url-rss-url').val().trim();
var starRating = $('#url-star-rating').val(); // 获取星级值
if (!title) {
showMessage('请填写网站标题', 'warning');
$('#url-title').focus();
return;
}
if (!url) {
showMessage('请填写网站地址', 'warning');
$('#url-url').focus();
return;
}
// 如果是新增模式且没有RSS地址显示提示
if (!isEditMode && !rssUrl) {
if (!confirm('未填写RSS地址确定要继续保存吗\n\n建议\n1. 点击"获取网站信息"按钮自动获取\n2. 或稍后在编辑时补充RSS地址')) {
return;
}
}
// 获取分类ID - 正确处理空值
var categorySelect = document.getElementById('url-category');
var categoryId = categorySelect ? categorySelect.value : '';
// 准备表单数据
var formData = {
do: isEditMode ? 'updateUrl' : 'addUrl',
title: title,
url: url,
description: $('#url-description').val().trim(),
sort_order: $('#url-sort').val() || 0,
star_rating: $('#url-star-rating').val() || '0' // 新增星级字段
};
// 添加RSS地址
if (rssUrl) {
formData.rss_url = rssUrl;
}
// 处理分类ID如果是空字符串不发送category_id参数
if (categoryId !== '') {
formData.category_id = categoryId;
}
if (isEditMode) {
formData.id = id;
}
console.log('保存网址数据:', formData);
console.log('操作类型:', isEditMode ? '编辑' : '新增');
// 新增和编辑保存后都不自动检查状态
console.log('保存操作,不触发状态检查');
showLoading();
$.ajax({
url: window.UrlNavConfig.actionUrl,
type: 'POST',
data: formData,
dataType: 'json',
success: function(response) {
hideLoading();
console.log('保存网址响应:', response);
if (response.success) {
showMessage(response.message, 'success');
hideUrlModal();
// 立即刷新页面,不等待
console.log('保存成功,立即刷新页面');
window.location.reload();
} else {
showMessage(response.message || '保存失败', 'error');
}
},
error: function(xhr, status, error) {
hideLoading();
console.error('保存网址错误:', status, error);
var errorMsg = '操作失败:';
try {
var responseText = xhr.responseText;
if (responseText.includes('FOREIGN KEY constraint failed')) {
errorMsg = '保存失败:所选分类不存在';
} else if (responseText.includes('标题已存在')) {
errorMsg = '保存失败:网站标题已存在';
} else if (responseText.includes('网站地址已存在')) {
errorMsg = '保存失败:网站地址已存在';
} else {
// 尝试解析JSON错误
var jsonResponse = JSON.parse(responseText);
errorMsg = jsonResponse.message || error;
}
} catch (e) {
errorMsg += error;
}
showMessage(errorMsg, 'error');
}
});
}
// 批量删除网址
function batchDeleteUrls() {
var selectedIds = [];
$('input[name="url[]"]:checked').each(function() {
selectedIds.push($(this).val());
});
if (selectedIds.length === 0) {
showMessage('请选择要删除的网址', 'warning');
return;
}
if (confirm('你确认要删除选中的 ' + selectedIds.length + ' 个网址吗?')) {
showLoading();
$.ajax({
url: window.UrlNavConfig.actionUrl,
type: 'POST',
data: {
do: 'batchDeleteUrls',
url: selectedIds
},
dataType: 'json',
success: function(response) {
hideLoading();
if (response.success) {
showMessage('成功删除 ' + selectedIds.length + ' 个网址', 'success');
setTimeout(function() {
window.location.reload();
}, 1500);
} else {
showMessage(response.message, 'error');
}
},
error: function(xhr, status, error) {
hideLoading();
showMessage('批量删除失败:' + (error || '网络错误'), 'error');
}
});
}
}
// 保存分类 - 修复版本
function saveCategory() {
var id = $('#category-id').val();
isEditMode = !!id;
// 验证表单
var name = $('#category-name').val().trim();
if (!name) {
showMessage('请填写分类名称', 'warning');
$('#category-name').focus();
return;
}
// 准备表单数据
var formData = {
do: isEditMode ? 'updateCategory' : 'addCategory',
name: name,
description: $('#category-description').val().trim(),
sort_order: $('#category-sort').val() || 0
};
if (isEditMode) {
formData.id = id;
}
console.log('保存分类数据:', formData);
console.log('操作类型:', isEditMode ? '编辑' : '新增');
showLoading();
$.ajax({
url: window.UrlNavConfig.actionUrl,
type: 'POST',
data: formData,
dataType: 'json',
success: function(response) {
hideLoading();
console.log('保存分类响应:', response);
if (response.success) {
showMessage(response.message, 'success');
hideCategoryModal();
// 立即刷新页面
console.log('分类保存成功,立即刷新页面');
window.location.reload();
} else {
showMessage(response.message || '保存失败', 'error');
}
},
error: function(xhr, status, error) {
hideLoading();
console.error('保存分类错误:', status, error);
var errorMsg = '操作失败:';
try {
var responseText = xhr.responseText;
if (responseText.includes('分类名称已存在')) {
errorMsg = '保存失败:分类名称已存在';
} else {
var jsonResponse = JSON.parse(responseText);
errorMsg = jsonResponse.message || error;
}
} catch (e) {
errorMsg += error;
}
showMessage(errorMsg, 'error');
}
});
}
// 批量删除分类
function batchDeleteCategories() {
var selectedIds = [];
$('input[name="category[]"]:checked').each(function() {
selectedIds.push($(this).val());
});
if (selectedIds.length === 0) {
showMessage('请选择要删除的分类', 'warning');
return;
}
if (confirm('你确认要删除选中的 ' + selectedIds.length + ' 个分类吗?')) {
showLoading();
$.ajax({
url: window.UrlNavConfig.actionUrl,
type: 'POST',
data: {
do: 'batchDeleteCategories',
category: selectedIds
},
dataType: 'json',
success: function(response) {
hideLoading();
if (response.success) {
showMessage('成功删除 ' + selectedIds.length + ' 个分类', 'success');
setTimeout(function() {
window.location.reload();
}, 1500);
} else {
showMessage(response.message, 'error');
}
},
error: function(xhr, status, error) {
hideLoading();
showMessage('批量删除失败:' + (error || '网络错误'), 'error');
}
});
}
}
// 切换选项卡
function switchTab(tab) {
if (tab === currentTab) return;
$('.tab-button').removeClass('active');
$('.tab-button[data-tab="' + tab + '"]').addClass('active');
$('.tab-content').removeClass('active');
$('#' + tab + '-tab').addClass('active');
currentTab = tab;
// 切换标签页时同步全选状态
setTimeout(syncCheckAllState, 100);
}
// 修复后的 checkWebsiteStatus 函数
function checkWebsiteStatus() {
var selectedIds = [];
$('input[name="url[]"]:checked').each(function() {
selectedIds.push($(this).val());
});
var $btn = $('#check-status-btn');
$btn.find('.spinner').show();
$btn.find('.btn-text').text('检查中...');
$btn.prop('disabled', true);
// 创建进度显示容器(如果不存在)
var $progressContainer = $('#batch-progress-container');
if ($progressContainer.length === 0) {
$progressContainer = $('<div id="batch-progress-container" style="position: fixed; top: 100px; left: 50%; transform: translateX(-50%); z-index: 9999; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.15); min-width: 400px; text-align: center;"></div>');
$('body').append($progressContainer);
}
// 初始化进度显示 - 增加累计数据显示
$progressContainer.html(`
<h4 style="margin-top: 0; color: #467b96;">正在检查</h4>
<div id="progress-status" style="margin-bottom: 15px; color: #666;">准备中...</div>
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 15px;">
<div style="flex: 1; background: #e9ecef; height: 10px; border-radius: 5px; overflow: hidden;">
<div id="progress-bar" style="width: 0%; height: 100%; background: #28a745; transition: width 0.3s;"></div>
</div>
<div id="progress-text" style="font-size: 12px; color: #666;">0%</div>
</div>
<div id="progress-stats" style="font-size: 12px; color: #666; margin-bottom: 15px;">
本批网址: 0,成功: 0,失败: 0
</div>
<button id="progress-stop-btn" class="btn danger" style="font-size: 12px; padding: 6px 16px;text-align:center;">停止检查</button>
`);
$progressContainer.show();
// 初始化累计数据变量
var cumulativeData = {
total: 0,
success: 0,
failed: 0
};
// 停止按钮事件
$('#progress-stop-btn').off('click').on('click', function() {
if (confirm('确定要停止检查吗?已完成的检查结果将保留。')) {
$progressContainer.remove();
resetCheckButton($btn);
showMessage('检查已停止', 'warning');
}
});
// 准备检查数据 - 保持原有格式
var data = {
do: 'checkStatus',
batch_size: 10, // 每批10个
total_batch: 1,
current_batch: 0
};
// 计算总网址数用于进度条
var totalUrlsForProgress = 0;
var totalBatches = 1;
if (selectedIds.length > 0) {
// 检查选中的网址 - 修复传递选中的ID
console.log('选中了' + selectedIds.length + '个网址进行检查:', selectedIds);
data.url_ids = selectedIds.join(',');
totalUrlsForProgress = selectedIds.length;
totalBatches = Math.ceil(selectedIds.length / 10);
data.total_urls = selectedIds.length;
data.total_batch = totalBatches;
data.check_type = 'selected';
} else {
// 检查所有网址
console.log('未选中任何网址,检查全部');
data.check_type = 'all';
totalUrlsForProgress = parseInt($('#stat-total').text()) || 0;
totalBatches = Math.ceil(totalUrlsForProgress / 10);
data.total_urls = totalUrlsForProgress;
data.total_batch = totalBatches;
// 重要不传递url_ids参数让后端检查全部
}
console.log('开始状态检查,参数:', data);
console.log('选中ID数:', selectedIds.length, '总网址数:', totalUrlsForProgress, '总批数:', totalBatches);
// 开始第一批检查
startBatchCheck(data, 1, $progressContainer, $btn, cumulativeData, totalUrlsForProgress, totalBatches, selectedIds);
}
// 新增:开始批次检查 - 修复批次停止逻辑
function startBatchCheck(data, batchNumber, $progressContainer, $btn, cumulativeData, totalUrlsForProgress, totalBatches, selectedIds) {
data.current_batch = batchNumber;
data.batch_number = batchNumber;
// 重要确保选中的ID持续传递
if (selectedIds && selectedIds.length > 0) {
data.url_ids = selectedIds.join(',');
data.total_urls = selectedIds.length;
data.total_batch = Math.ceil(selectedIds.length / 10);
}
// 更新进度显示
var processedBeforeThisBatch = (batchNumber - 1) * 10; // 每批10个
var progressPercent = Math.round(processedBeforeThisBatch / totalUrlsForProgress * 100);
// 确保进度不超过100%
progressPercent = Math.min(progressPercent, 100);
$('#progress-bar').css('width', progressPercent + '%');
$('#progress-text').text(progressPercent + '%');
$('#progress-status').html(`
<span>正在检查第 <strong>${batchNumber}</strong> / 总共 <strong>${totalBatches}</strong> </span>
<span id="progress-cumulative" style="font-size: 13px; color: #28a745; font-weight: 500;margin-left:15px;">
已查 <span id="cumulative-total">${cumulativeData.total}</span>个,成功 <span id="cumulative-success">${cumulativeData.success}</span>个,失败 <span id="cumulative-failed">${cumulativeData.failed}</span>
</span>
`);
// 更新累计数据显示
$('#cumulative-total').text(cumulativeData.total);
$('#cumulative-success').text(cumulativeData.success);
$('#cumulative-failed').text(cumulativeData.failed);
console.log('发送第' + batchNumber + '批请求选中的ID:', data.url_ids);
console.log('累计已查:', cumulativeData.total, '预期总数:', totalUrlsForProgress);
// 检查累计是否已超过或等于总数 - 新增停止条件
if (cumulativeData.total >= totalUrlsForProgress && batchNumber > 1) {
console.log('累计已查数已达到总数,停止检查');
completeCheck({success: true, message: '所有网址已检查完成'}, $progressContainer, $btn);
return;
}
$.ajax({
url: window.UrlNavConfig.actionUrl,
type: 'POST',
data: data,
dataType: 'json',
success: function(response) {
console.log('第' + batchNumber + '批检查响应:', response);
if (response.success) {
// 更新本批统计信息
updateProgressStats(response);
// 更新累计数据
cumulativeData.total += response.total || 0;
cumulativeData.success += response.success_count || 0;
cumulativeData.failed += response.failed_count || 0;
// 更新累计数据显示
$('#cumulative-total').text(cumulativeData.total);
$('#cumulative-success').text(cumulativeData.success);
$('#cumulative-failed').text(cumulativeData.failed);
// 更新表格中的状态指示器和检查时间
if (response.results) {
updateStatusInTable(response.results);
}
// 检查是否有更多批次 - 修复停止逻辑
var shouldContinue = false;
// 情况1后端返回了明确的has_more
if (response.has_more !== undefined) {
shouldContinue = response.has_more === true;
}
// 情况2累计数还没达到总数
else if (cumulativeData.total < totalUrlsForProgress) {
shouldContinue = true;
}
// 情况3后端明确返回completed标志
else if (response.completed === true) {
shouldContinue = false;
}
// 情况4批次号小于总批数但累计已达总数
else if (batchNumber < totalBatches && cumulativeData.total >= totalUrlsForProgress) {
shouldContinue = false;
}
// 情况5批次号小于总批数累计未达总数
else if (batchNumber < totalBatches) {
shouldContinue = true;
}
console.log('批次判断:', {
batchNumber: batchNumber,
totalBatches: totalBatches,
cumulativeTotal: cumulativeData.total,
totalUrlsForProgress: totalUrlsForProgress,
has_more: response.has_more,
completed: response.completed,
shouldContinue: shouldContinue
});
if (shouldContinue) {
// 批次间延迟1秒避免服务器压力过大
setTimeout(function() {
startBatchCheck(data, batchNumber + 1, $progressContainer, $btn, cumulativeData, totalUrlsForProgress, totalBatches, selectedIds);
}, 1000);
} else {
// 所有批次完成
completeCheck(response, $progressContainer, $btn);
}
// 每批完成后刷新统计
loadStatusStats();
} else {
// 当前批次失败
showMessage('第' + batchNumber + '批检查失败: ' + response.message, 'error');
// 是否继续下一批
if (confirm('当前批次检查失败,是否继续下一批?')) {
if (batchNumber < totalBatches) {
setTimeout(function() {
startBatchCheck(data, batchNumber + 1, $progressContainer, $btn, cumulativeData, totalUrlsForProgress, totalBatches, selectedIds);
}, 2000);
} else {
completeCheck(response, $progressContainer, $btn);
}
} else {
completeCheck(response, $progressContainer, $btn);
}
}
},
error: function(xhr, status, error) {
console.error('第' + batchNumber + '批请求错误:', error);
// 是否重试当前批次
if (confirm('第' + batchNumber + '批请求失败: ' + error + ',是否重试?')) {
setTimeout(function() {
startBatchCheck(data, batchNumber, $progressContainer, $btn, cumulativeData, totalUrlsForProgress, totalBatches, selectedIds);
}, 3000);
} else if (batchNumber < totalBatches) {
// 跳过当前批次,继续下一批
if (confirm('是否跳过当前批次,继续下一批?')) {
setTimeout(function() {
startBatchCheck(data, batchNumber + 1, $progressContainer, $btn, cumulativeData, totalUrlsForProgress, totalBatches, selectedIds);
}, 3000);
} else {
completeCheck(null, $progressContainer, $btn, '检查中断');
}
} else {
completeCheck(null, $progressContainer, $btn, '检查中断');
}
}
});
}
// 新增:更新进度统计(保持不变)
function updateProgressStats(response) {
var statsText = `本批网址: ${response.total || 0}个 | 成功: ${response.success_count || 0}个 | 失败: ${response.failed_count || 0}个`;
if (response.avg_response_time) {
statsText += ` | 响应时间: ${response.avg_response_time}MS`;
}
$('#progress-stats').html(statsText);
}
// 新增:完成检查(保持不变)
function completeCheck(response, $progressContainer, $btn, customMessage) {
// 更新最终进度
$('#progress-bar').css('width', '100%');
$('#progress-text').text('100%');
var message = customMessage || (response ? response.message : '检查完成');
$('#progress-status').html(`<strong>${message}</strong>`);
if (response && response.success) {
$('#progress-stats').css('color', '#28a745');
$('#progress-cumulative').css('color', '#28a745');
} else {
$('#progress-stats').css('color', '#dc3545');
$('#progress-cumulative').css('color', '#dc3545');
}
// 3秒后自动关闭进度窗口
setTimeout(function() {
$progressContainer.fadeOut(500, function() {
$(this).remove();
});
resetCheckButton($btn);
if (response && response.success) {
showMessage(message, 'success');
}
}, 3000);
}
// 新增:重置检查按钮状态
function resetCheckButton($btn) {
$btn.find('.spinner').hide();
$btn.find('.btn-text').text('检查网站状态');
$btn.prop('disabled', false);
}
// 更新表格中的状态显示和检查时间(不刷新页面)
function updateStatusInTable(results) {
var currentDate = new Date().toISOString().split('T')[0]; // 获取当前日期 YYYY-MM-DD
for (var urlId in results) {
var result = results[urlId];
var $row = $('#url-' + urlId);
if ($row.length) {
var $indicator = $row.find('.status-indicator');
if ($indicator.length) {
$indicator.removeClass('status-online status-offline status-unknown status-checking');
if (result.success) {
$indicator.addClass('status-online');
var title = '通连';
if (result.status_code && result.status_code > 0) {
title += ' (HTTP ' + result.status_code + ')';
}
if (result.response_time) {
title += ' - 响应: ' + result.response_time + 'ms';
}
title += ' - 最后检查: ' + currentDate;
$indicator.attr('title', title);
} else {
$indicator.addClass('status-offline');
var title = '失连: ' + (result.message || '访问失败');
if (result.status_code && result.status_code > 0) {
title += ' (HTTP ' + result.status_code + ')';
}
title += ' - 最后检查: ' + currentDate;
$indicator.attr('title', title);
}
}
// 更新检查时间列
var $checkTimeCell = $row.find('.check-time-cell');
if ($checkTimeCell.length) {
$checkTimeCell.html(currentDate);
}
}
}
}
// 加载状态统计包含RSS统计
function loadStatusStats() {
$.ajax({
url: window.UrlNavConfig.actionUrl,
type: 'GET',
data: {do: 'getStatusStats'},
dataType: 'json',
success: function(response) {
if (response.success) {
var stats = response.data;
$('#stat-total').text(stats.total);
$('#stat-online').text(stats.online);
$('#stat-offline').text(stats.offline);
$('#stat-unchecked').text(stats.unchecked);
$('#stat-rate').text(round(stats.online_rate)); // 改为整数
$('#stat-rss-yes').text(stats.has_rss);
$('#stat-rss-no').text(stats.no_rss);
// 更新统计面板样式
var $stats = $('#status-stats');
if (stats.online_rate >= 80) {
$stats.css({
'background': 'linear-gradient(135deg, #1f2937 0%, #111827 100%)',
'border-color': '#666'
});
} else if (stats.online_rate >= 60) {
$stats.css({
'background': 'linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%)',
'border-color': '#ffeaa7'
});
} else {
$stats.css({
'background': 'linear-gradient(135deg, #f8d7da 0%, #f5c6cb 100%)',
'border-color': '#f5c6cb'
});
}
}
}
});
}
// 四舍五入函数
function round(value) {
return Math.round(value);
}
// 显示导入模态框
function showImportModal() {
$('#import-form')[0].reset();
$('#import-preview').hide();
$('#import-modal-start').prop('disabled', true);
$('#import-modal-overlay').fadeIn(200);
$('#import-modal-container').fadeIn(200);
}
// 隐藏导入模态框
function hideImportModal() {
$('#import-modal-container').fadeOut(200);
$('#import-modal-overlay').fadeOut(200);
}
// 显示导入结果模态框
function showImportResultModal() {
$('#import-result-overlay').fadeIn(200);
$('#import-result-container').fadeIn(200);
}
// 隐藏导入结果模态框
function hideImportResultModal() {
$('#import-result-container').fadeOut(200);
$('#import-result-overlay').fadeOut(200);
}
// 预览文件内容
function previewFile(file) {
// 检查文件大小限制为100KB用于预览
if (file.size > 100 * 1024) {
$('#file-preview-content').text('文件过大,无法预览完整内容...');
$('#import-preview').show();
$('#import-modal-start').prop('disabled', false);
return;
}
var reader = new FileReader();
reader.onload = function(e) {
try {
var content = e.target.result;
// 只显示前1000个字符用于预览
var preview = content.substring(0, 1000);
if (content.length > 1000) {
preview += '\n...(文件内容过长,已截断)';
}
$('#file-preview-content').text(preview);
$('#import-preview').show();
$('#import-modal-start').prop('disabled', false);
} catch (error) {
$('#file-preview-content').text('无法预览文件内容');
$('#import-preview').show();
$('#import-modal-start').prop('disabled', true);
}
};
reader.onerror = function() {
$('#file-preview-content').text('读取文件失败');
$('#import-preview').show();
$('#import-modal-start').prop('disabled', true);
};
reader.readAsText(file);
}
// 开始导入
function startImport() {
var fileInput = $('#opml-file')[0];
if (!fileInput.files || !fileInput.files[0]) {
showMessage('请选择要导入的OPML文件', 'warning');
return;
}
var file = fileInput.files[0];
var formData = new FormData();
formData.append('opml_file', file);
// 添加导入选项
formData.append('skip_duplicates', $('#import-form input[name="skip_duplicates"]').is(':checked') ? '1' : '0');
formData.append('auto_create_categories', $('#import-form input[name="auto_create_categories"]').is(':checked') ? '1' : '0');
showLoading();
$.ajax({
url: window.UrlNavConfig.actionUrl + '?do=importOpml',
type: 'POST',
data: formData,
processData: false,
contentType: false,
dataType: 'json',
timeout: 60000, // 60秒超时
success: function(response) {
hideLoading();
hideImportModal();
console.log('导入响应:', response);
if (response.success) {
showImportResult(response.data);
} else {
// 显示更详细的错误信息
var errorMsg = response.message || '未知错误';
if (response.debug) {
errorMsg += ' (' + response.debug + ')';
}
showMessage('导入失败: ' + errorMsg, 'error');
}
},
error: function(xhr, status, error) {
hideLoading();
console.error('导入错误:', status, error);
console.error('响应文本:', xhr.responseText);
var errorMsg = '导入失败: ';
if (status === 'timeout') {
errorMsg += '请求超时,请稍后重试';
} else if (status === 'error') {
try {
var response = JSON.parse(xhr.responseText);
errorMsg += response.message || xhr.statusText;
} catch (e) {
errorMsg += xhr.statusText || '网络错误';
}
} else {
errorMsg += error || '未知错误';
}
showMessage(errorMsg, 'error');
}
});
}
// 显示导入结果
function showImportResult(data) {
var html = '';
html += '<div style="margin-bottom: 20px;">';
html += '<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; margin-bottom: 20px;">';
html += '<div style="background: #e8f5e9; padding: 15px; border-radius: 8px; text-align: center;">';
html += '<div style="font-size: 12px; color: #28a745; margin-bottom: 5px;">成功</div>';
html += '<div style="font-size: 24px; font-weight: bold; color: #28a745;">' + data.success + '</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: #467b96;">' + data.total + '</div>';
html += '</div>';
html += '<div style="background: #f8d7da; padding: 15px; border-radius: 8px; text-align: center;">';
html += '<div style="font-size: 12px; color: #dc3545; margin-bottom: 5px;">失败</div>';
html += '<div style="font-size: 24px; font-weight: bold; color: #dc3545;">' + data.failed + '</div>';
html += '</div>';
html += '</div>';
html += '<div style="display: flex; justify-content: space-between; margin-bottom: 15px; flex-wrap: wrap; gap: 10px;">';
html += '<span style="background: #d4edda; color: #155724; padding: 5px 10px; border-radius: 4px; font-size: 12px;">创建分类: ' + data.categories_created + '</span>';
html += '<span style="background: #d1ecf1; color: #0c5460; padding: 5px 10px; border-radius: 4px; font-size: 12px;">添加网址: ' + data.urls_added + '</span>';
html += '<span style="background: #fff3cd; color: #856404; padding: 5px 10px;border-radius: 4px; font-size: 12px;">跳过重复: ' + data.urls_skipped + '</span>';
html += '</div>';
html += '</div>';
if (data.details && data.details.length > 0) {
html += '<div style="max-height: 300px; 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: #000000;">类型</th>';
html += '<th style="padding: 10px; text-align: left;color: #000000;">名称</th>';
html += '<th style="padding: 10px; text-align: left;color: #000000;">状态</th>';
html += '<th style="padding: 10px; text-align: left;color: #000000;">说明</th>';
html += '</tr>';
html += '</thead>';
html += '<tbody>';
data.details.forEach(function(item) {
var statusColor = '';
if (item.status === 'added' || item.status === 'created') {
statusColor = 'color: #28a745;';
} else if (item.status === 'skipped') {
statusColor = 'color: #ffc107;';
} else {
statusColor = 'color: #dc3545;';
}
html += '<tr style="border-bottom: 1px solid #f0f2f5;">';
html += '<td style="padding: 10px;color: #000000;">' + (item.type === 'category' ? '分类' : '网址') + '</td>';
html += '<td style="padding: 10px; color: #000000;">' + (item.title || item.name || '') + '</td>';
html += '<td style="padding: 10px; ' + statusColor + ' font-weight: 500;">';
html += item.status === 'added' ? '已添加' :
item.status === 'created' ? '已创建' :
item.status === 'skipped' ? '已跳过' : '失败';
html += '</td>';
html += '<td style="padding: 10px; color: #666;">' + (item.message || '') + '</td>';
html += '</tr>';
});
html += '</tbody>';
html += '</table>';
html += '</div>';
}
$('#import-result-content').html(html);
showImportResultModal();
}
// 页面加载完成后初始化
$(document).ready(function() {
initUrlNav();
// 初始化剪贴板功能
initClipboard();
// 刷新状态检查定时任务统计
$('#refresh-status-cron-stats').on('click', function(e) {
e.preventDefault();
loadStatusCronStats();
showMessage('正在刷新状态检查定时任务统计...', 'info');
});
});
})(jQuery);
// 初始化剪贴板功能
function initClipboard() {
// 使用现代的 Clipboard API
$(document).on('click', '.copy-btn', function(e) {
e.preventDefault();
e.stopPropagation();
var targetId = $(this).data('clipboard-target');
if (!targetId) {
targetId = '#status-cron-url';
}
var inputElement = $(targetId)[0];
if (!inputElement) {
showMessage('找不到要复制的内容', 'error');
return;
}
// 现代剪贴板 API
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(inputElement.value)
.then(() => {
showMessage('已复制到剪贴板', 'success');
// 添加视觉反馈
var btn = $(this);
var originalText = btn.text();
btn.text('已复制');
btn.css('background', '#28a745').css('color', 'white');
setTimeout(function() {
btn.text(originalText);
btn.css('background', '').css('color', '');
}, 1500);
})
.catch(err => {
console.error('复制失败:', err);
// 降级到传统方法
fallbackCopyText(inputElement, this);
});
} else {
// 降级到传统方法
fallbackCopyText(inputElement, this);
}
});
}
// 传统复制方法
function fallbackCopyText(inputElement, buttonElement) {
inputElement.select();
inputElement.setSelectionRange(0, 99999); // For mobile devices
try {
var successful = document.execCommand('copy');
if (successful) {
showMessage('已复制到剪贴板', 'success');
// 添加视觉反馈
if (buttonElement) {
var originalText = buttonElement.textContent || $(buttonElement).text();
$(buttonElement).text('已复制');
$(buttonElement).css('background', '#28a745').css('color', 'white');
setTimeout(function() {
$(buttonElement).text(originalText);
$(buttonElement).css('background', '').css('color', '');
}, 1500);
}
} else {
showMessage('复制失败,请手动复制', 'warning');
}
} catch (err) {
console.error('复制失败:', err);
showMessage('复制失败: ' + err, 'error');
}
}
// 修复立即运行按钮的函数
function runStatusCronNow() {
if (!confirm('确定要立即运行状态检查定时任务吗?\n这将检查所有网站的状态。')) {
return;
}
showLoading();
// 使用正确的 action URL
var actionUrl = window.UrlNavConfig.actionUrl;
var secret = '<?php echo $pluginOptions->statusCronSecret; ?>';
// 确保 secret 不为空
if (!secret) {
showMessage('定时任务密钥未配置,请先设置密钥', 'error');
hideLoading();
return;
}
console.log('立即执行状态检查secret:', secret.substring(0, 10) + '...');
// 使用 POST 请求并发送正确的参数
$.ajax({
url: actionUrl,
type: 'POST',
data: {
do: 'statusCron',
secret: secret
},
dataType: 'json',
timeout: 30000, // 30秒超时
success: function(response) {
hideLoading();
console.log('定时任务执行响应:', response);
if (response && response.success) {
var message = '状态检查定时任务执行成功';
if (response.message) {
message += ': ' + response.message;
}
if (response.data) {
message += ' (' + response.data + ')';
}
showMessage(message, 'success');
// 重新加载统计和日志
setTimeout(function() {
loadStatusCronStats();
}, 1000);
} else {
var errorMsg = '执行失败: ' + (response ? response.message : '未知错误');
showMessage(errorMsg, 'error');
}
},
error: function(xhr, status, error) {
hideLoading();
console.error('定时任务执行错误:', status, error);
var errorMsg = '执行请求失败: ';
if (status === 'timeout') {
errorMsg += '请求超时,可能任务正在执行中';
} else if (xhr.status === 403) {
errorMsg += '密钥验证失败';
} else if (xhr.responseText) {
try {
var response = JSON.parse(xhr.responseText);
errorMsg = response.message || xhr.statusText;
} catch (e) {
errorMsg += xhr.statusText || '网络错误';
}
} else {
errorMsg += error;
}
showMessage(errorMsg, 'error');
}
});
}
</script>
<?php
include 'footer.php';
?>