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

3500 lines
154 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
if (!defined('__TYPECHO_ROOT_DIR__')) exit;
// 包含必要的文件
include 'header.php';
include 'menu.php';
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';
?>