Files

5950 lines
301 KiB
HTML
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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HYC下载站 v2.2 - 管理控制台</title>
<!-- Font Awesome 图标库 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
<!-- Chart.js 图表库 -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg: #0f0f23;
--bg-dark: #0f0f23;
--bg-card: #1a1a2e;
--bg-input: #16213e;
--primary: #e94560;
--primary-hover: #ff6b6b;
--text: #eee;
--text-muted: #888;
--border: #2a2a4a;
--success: #4ade80;
--warning: #fbbf24;
--danger: #ef4444;
--info: #60a5fa;
}
html, body {
height: 100%;
overflow: hidden;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-dark);
color: var(--text);
}
/* 主滚动条样式 */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; margin-right: 2px; }
::-webkit-scrollbar-thumb { background: var(--primary); border-radius: 4px; border: 2px solid var(--bg-dark); }
::-webkit-scrollbar-thumb:hover { background: var(--primary-hover); }
/* 登录页 */
.login-container { display: flex; justify-content: center; align-items: center; min-height: 100vh; background: var(--bg-dark); }
.login-box { background: var(--bg-card); padding: 40px; border-radius: 12px; width: 400px; box-shadow: 0 10px 40px rgba(0,0,0,0.4); border: 1px solid var(--border); }
.login-box h1 { text-align: center; margin-bottom: 10px; color: var(--primary); font-size: 28px; }
.login-box .subtitle { text-align: center; color: var(--text-muted); margin-bottom: 30px; }
.login-box input { width: 100%; padding: 12px 16px; margin: 10px 0; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-input); color: var(--text); font-size: 14px; }
.login-box input:focus { outline: none; border-color: var(--primary); }
.login-box button { width: 100%; padding: 14px; border: none; border-radius: 8px; background: var(--primary); color: #fff; cursor: pointer; font-size: 16px; font-weight: 600; margin-top: 10px; transition: all 0.2s; }
.login-box button:hover { background: var(--primary-hover); transform: translateY(-1px); }
.login-box .tips { text-align: center; margin-top: 20px; color: var(--text-muted); font-size: 12px; line-height: 1.8; }
/* 主界面 */
.app-container { display: none; height: 100vh; overflow: hidden; }
.app-container.active { display: flex; }
/* 侧边栏 */
.sidebar {
width: 260px;
background: var(--bg-card);
border-right: 1px solid var(--border);
position: fixed;
top: 0;
left: 0;
height: 100vh;
overflow-y: auto;
display: flex;
flex-direction: column;
z-index: 50;
}
.sidebar::-webkit-scrollbar { width: 6px; }
.sidebar::-webkit-scrollbar-track { background: var(--bg-input); }
.sidebar::-webkit-scrollbar-thumb { background: var(--primary); border-radius: 3px; }
.sidebar::-webkit-scrollbar-thumb:hover { background: #c93550; }
.sidebar-header { padding: 20px; border-bottom: 1px solid var(--border); }
.sidebar-header h2 { font-size: 18px; color: var(--primary); display: flex; align-items: center; gap: 10px; }
.sidebar-nav { flex: 1; padding: 10px 0; }
.nav-item {
padding: 14px 20px;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 12px;
border-left: 3px solid transparent;
color: var(--text-muted);
font-size: 14px;
}
.nav-item:hover { background: rgba(233, 69, 96, 0.1); color: var(--text); }
.nav-item.active { background: rgba(233, 69, 96, 0.15); border-left-color: var(--primary); color: var(--text); }
.nav-item .icon { font-size: 18px; width: 24px; text-align: center; }
.sidebar-footer { padding: 20px; border-top: 1px solid var(--border); }
.logout-btn { width: 100%; padding: 12px; border: 1px solid var(--border); border-radius: 8px; background: transparent; color: var(--text-muted); cursor: pointer; font-size: 14px; transition: all 0.2s; display: flex; align-items: center; justify-content: center; gap: 8px; }
.logout-btn:hover { border-color: var(--danger); color: var(--danger); background: rgba(239, 68, 68, 0.1); }
/* 主内容 */
.main-content { margin-left: 260px; flex: 1; padding: 24px; height: 100vh; display: flex; flex-direction: column; overflow: hidden; box-sizing: border-box; }
.page { display: none; flex-direction: column; flex: 1; min-height: 0; overflow: hidden; }
.page.active { display: flex; animation: fadeIn 0.3s ease; overflow-y: auto; margin-right: -8px; padding-right: 16px; }
.page.active::-webkit-scrollbar { width: 6px; }
.page.active::-webkit-scrollbar-track { background: var(--bg-input); border-radius: 3px; }
.page.active::-webkit-scrollbar-thumb { background: var(--primary); border-radius: 3px; }
.page.active::-webkit-scrollbar-thumb:hover { background: #c93550; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
/* 页面头部 - 固定不滚动 */
.page-header { padding: 0 0 12px 0; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; position: sticky; top: 0px; background: var(--bg); z-index: 100; margin-bottom: 16px; }
.page-header h1 { font-size: 24px; color: var(--text); display: flex; align-items: center; gap: 12px; }
.page-header .desc { color: var(--text-muted); font-size: 14px; margin-top: 4px; }
/* 统计卡片 */
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; margin-bottom: 24px; }
.stat-card {
background: var(--bg-card);
border-radius: 12px;
padding: 20px;
border: 1px solid var(--border);
transition: all 0.2s;
}
.stat-card:hover { transform: translateY(-2px); border-color: var(--primary); }
.stat-card .icon { font-size: 28px; margin-bottom: 10px; }
.stat-card .value { font-size: 28px; font-weight: 700; color: var(--text); margin-bottom: 4px; }
.stat-card .label { color: var(--text-muted); font-size: 13px; }
.stat-card .trend { font-size: 12px; margin-top: 8px; display: flex; align-items: center; gap: 4px; }
.trend.up { color: var(--success); }
.trend.down { color: var(--danger); }
/* 卡片 */
.card { background: var(--bg-card); border-radius: 12px; padding: 20px; margin-bottom: 20px; border: 1px solid var(--border); }
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--border); }
.card-title { font-size: 16px; font-weight: 600; display: flex; align-items: center; gap: 8px; }
.card-title .icon { font-size: 18px; }
.card-body { padding: 0; }
/* 增强统计卡片样式 */
.stat-card { display: flex; align-items: center; gap: 16px; }
.stat-card-icon { font-size: 36px; line-height: 1; }
.stat-card-content { flex: 1; }
.stat-card-value { font-size: 26px; font-weight: 700; color: var(--text); line-height: 1.2; }
.stat-card-label { font-size: 13px; color: var(--text-muted); margin-top: 2px; }
/* 统计卡片颜色变体 */
.stat-card-primary { background: linear-gradient(135deg, var(--primary) 0%, #ff6b6b 100%); border: none; }
.stat-card-primary .stat-card-icon, .stat-card-primary .stat-card-value, .stat-card-primary .stat-card-label { color: #fff; }
.stat-card-primary .stat-card-label { opacity: 0.9; }
.stat-card-info { background: linear-gradient(135deg, #4dabf7 0%, #339af0 100%); border: none; }
.stat-card-info .stat-card-icon, .stat-card-info .stat-card-value, .stat-card-info .stat-card-label { color: #fff; }
.stat-card-info .stat-card-label { opacity: 0.9; }
.stat-card-success { background: linear-gradient(135deg, #51cf66 0%, #40c057 100%); border: none; }
.stat-card-success .stat-card-icon, .stat-card-success .stat-card-value, .stat-card-success .stat-card-label { color: #fff; }
.stat-card-success .stat-card-label { opacity: 0.9; }
.stat-card-warning { background: linear-gradient(135deg, #ffd43b 0%, #fab005 100%); border: none; }
.stat-card-warning .stat-card-icon, .stat-card-warning .stat-card-value, .stat-card-warning .stat-card-label { color: #333; }
.stat-card-warning .stat-card-label { opacity: 0.8; }
/* 小型统计项 */
.stat-item-mini { display: flex; align-items: center; gap: 12px; padding: 12px; background: var(--bg-input); border-radius: 8px; }
.stat-item-icon { font-size: 24px; }
.stat-item-value { font-size: 18px; font-weight: 700; color: var(--text); }
.stat-item-label { font-size: 12px; color: var(--text-muted); }
/* 表格 */
.table-container { overflow-x: auto; }
.table-container table thead { position: sticky; top: 0; z-index: 10; }
/* 最近活动列表滚动容器 */
#activityContainer {
max-height: 400px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--primary) var(--bg-input);
}
#activityContainer::-webkit-scrollbar { width: 6px; }
#activityContainer::-webkit-scrollbar-track { background: var(--bg-input); }
#activityContainer::-webkit-scrollbar-thumb { background: var(--primary); border-radius: 3px; }
#activityContainer::-webkit-scrollbar-thumb:hover { background: #c93550; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 14px 16px; text-align: left; border-bottom: 1px solid var(--border); }
th { color: var(--text-muted); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; background: var(--bg-input); }
tr:hover { background: rgba(233, 69, 96, 0.05); }
tr:last-child td { border-bottom: none; }
/* 按钮 */
.btn { padding: 10px 18px; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s; display: inline-flex; align-items: center; gap: 6px; }
.btn-primary { background: var(--primary); color: #fff; }
.btn-primary:hover { background: var(--primary-hover); }
.btn-secondary { background: var(--bg-input); color: var(--text); border: 1px solid var(--border); }
.btn-secondary:hover { border-color: var(--primary); }
.btn-danger { background: var(--danger); color: #fff; }
.btn-danger:hover { opacity: 0.9; }
.btn-success { background: var(--success); color: #000; }
.btn-warning { background: var(--warning); color: #000; }
.btn-sm { padding: 6px 12px; font-size: 12px; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
/* 文本框滚动条 */
textarea::-webkit-scrollbar { width: 6px; }
textarea::-webkit-scrollbar-track { background: var(--bg-input); }
textarea::-webkit-scrollbar-thumb { background: var(--primary); border-radius: 3px; }
textarea::-webkit-scrollbar-thumb:hover { background: #c93550; }
/* 图表切换按钮 */
.chart-type-toggle { display: flex; gap: 4px; }
.chart-type-toggle .btn {
background: var(--bg-input);
color: var(--text-muted);
border: 1px solid var(--border);
padding: 5px 10px;
}
.chart-type-toggle .btn.active {
background: var(--primary);
color: #fff;
border-color: var(--primary);
}
.chart-type-toggle .btn:hover {
border-color: var(--primary);
color: var(--primary);
}
.chart-type-toggle .btn.active:hover {
color: #fff;
}
/* 输入框 */
input, select, textarea {
padding: 10px 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg-input);
color: var(--text);
font-size: 14px;
}
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--primary); }
textarea { resize: vertical; min-height: 100px; }
/* 标签 */
.badge { padding: 4px 10px; border-radius: 20px; font-size: 12px; font-weight: 500; }
.badge-success { background: rgba(74, 222, 128, 0.15); color: var(--success); }
.badge-warning { background: rgba(251, 191, 36, 0.15); color: var(--warning); }
.badge-danger { background: rgba(239, 68, 68, 0.15); color: var(--danger); }
.badge-info { background: rgba(96, 165, 250, 0.15); color: var(--info); }
.badge-primary { background: rgba(233, 69, 96, 0.15); color: var(--primary); }
/* 进度条 */
.progress { background: var(--bg-input); border-radius: 10px; height: 8px; overflow: hidden; }
.progress-bar { background: linear-gradient(90deg, var(--primary), var(--primary-hover)); height: 100%; transition: width 0.3s; border-radius: 10px; }
.progress-sm { height: 4px; }
/* 开关 - 极简细长样式 */
.switch {
position: relative;
display: inline-block;
width: 36px;
height: 20px;
vertical-align: middle;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
position: absolute;
}
.switch-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #555;
border-radius: 20px;
transition: 0.3s;
opacity: 0.6;
}
.switch-slider:before {
position: absolute;
content: "";
height: 14px;
width: 14px;
left: 3px;
bottom: 3px;
background-color: white;
border-radius: 50%;
transition: 0.3s;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.switch input:checked + .switch-slider {
background-color: var(--primary, #6366f1);
opacity: 1;
}
.switch input:checked + .switch-slider:before {
transform: translateX(16px);
}
/* 对话框 */
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); justify-content: center; align-items: center; z-index: 1000; backdrop-filter: blur(4px); }
.modal.active { display: flex; }
.modal-content {
background: var(--bg-card);
border-radius: 16px;
width: 500px;
max-height: 80vh;
display: flex;
flex-direction: column;
border: 1px solid var(--border);
animation: modalIn 0.3s ease;
overflow: hidden;
}
@keyframes modalIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
.modal-header {
padding: 20px 24px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
background: var(--bg-card);
position: sticky;
top: 0;
z-index: 1;
}
.modal-header h3 { font-size: 18px; display: flex; align-items: center; gap: 10px; margin: 0; }
.modal-close { cursor: pointer; font-size: 24px; color: var(--text-muted); transition: color 0.2s; background: none; border: none; padding: 0; line-height: 1; }
.modal-close:hover { color: var(--text); }
.modal-body { padding: 24px; flex: 1; overflow-y: auto; }
.modal-body::-webkit-scrollbar { width: 6px; }
.modal-body::-webkit-scrollbar-track { background: var(--bg-input); }
.modal-body::-webkit-scrollbar-thumb { background: var(--primary); border-radius: 3px; }
.modal-body::-webkit-scrollbar-thumb:hover { background: #c93550; }
.modal-footer { padding: 16px 24px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 12px; flex-shrink: 0; background: var(--bg-card); }
.form-group { margin-bottom: 16px; }
.form-group label { display: block; margin-bottom: 8px; color: var(--text-muted); font-size: 13px; }
.form-group input, .form-group select, .form-group textarea { width: 100%; }
/* 输入框组样式 */
.input-group { margin-bottom: 0; }
.input-group label { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; color: var(--text-muted); font-size: 13px; font-weight: 500; }
.input-group label i { color: var(--primary); width: 16px; text-align: center; }
.input-group input, .input-group select {
width: 100%;
padding: 12px 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg-card);
color: var(--text);
font-size: 14px;
transition: all 0.2s;
}
.input-group input:focus, .input-group select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
.input-group input::placeholder { color: var(--text-muted); opacity: 0.7; }
/* 美化选择框 */
.select-wrapper { position: relative; }
.select-wrapper select {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
padding-right: 40px !important;
cursor: pointer;
}
.select-arrow {
position: absolute;
right: 14px;
top: 50%;
transform: translateY(-50%);
color: var(--text-muted);
pointer-events: none;
font-size: 12px;
}
select option {
padding: 12px;
background: var(--bg-card);
color: var(--text);
}
/* 配置卡片样式 */
.config-card {
background: var(--bg-input);
border-radius: 12px;
overflow: hidden;
border: 1px solid var(--border);
}
.config-card-header {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 18px;
background: linear-gradient(135deg, var(--primary) 0%, #2563eb 100%);
color: white;
font-weight: 600;
font-size: 14px;
}
.config-card-body {
padding: 18px;
}
/* 状态徽章 */
.status-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
color: white;
}
.status-badge.enabled { background: var(--success); }
.status-badge.disabled { background: var(--danger); }
/* 开关卡片 */
.toggle-card {
background: var(--bg-input);
border-radius: 12px;
padding: 16px 20px;
border: 1px solid var(--border);
}
/* 搜索框 */
.search-box { position: relative; width: 300px; }
.search-box input { width: 100%; padding-left: 44px; }
.search-icon { position: absolute; left: 16px; top: 50%; transform: translateY(-50%); color: var(--text-muted); }
/* 通知 */
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 14px 24px;
border-radius: 10px;
z-index: 2000;
animation: slideIn 0.3s ease;
font-weight: 500;
display: flex;
align-items: center;
gap: 10px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
}
.notification-success { background: var(--success); color: #000; }
.notification-error { background: var(--danger); color: #fff; }
.notification-warning { background: var(--warning); color: #000; }
.notification-info { background: var(--info); color: #fff; }
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
/* 标签页 */
.tabs { display: flex; gap: 4px; margin-bottom: 20px; border-bottom: 1px solid var(--border); padding-bottom: 0; }
.tab { padding: 12px 20px; cursor: pointer; border: none; background: none; color: var(--text-muted); font-size: 14px; border-bottom: 2px solid transparent; margin-bottom: -1px; transition: all 0.2s; }
.tab:hover { color: var(--text); }
.tab.active { color: var(--primary); border-bottom-color: var(--primary); }
/* 空状态 */
.empty { text-align: center; padding: 60px 20px; color: var(--text-muted); }
.empty .icon { font-size: 48px; margin-bottom: 16px; opacity: 0.5; }
.empty p { font-size: 14px; }
/* 文件浏览器 */
.file-browser {
background: var(--bg-card);
border-radius: 12px;
overflow: hidden;
border: 1px solid var(--border);
margin-top: 16px;
}
/* 面包屑导航 - 整合式设计 */
.breadcrumb {
padding: 6px 16px;
background: linear-gradient(180deg, var(--bg-input) 0%, var(--bg-card) 100%);
color: var(--text-muted);
font-size: 13px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 4px;
flex-wrap: wrap;
margin-bottom: 8px;
border-radius: 8px 8px 0 0;
}
.breadcrumb a {
color: var(--primary);
cursor: pointer;
text-decoration: none;
padding: 4px 8px;
border-radius: 4px;
transition: all 0.2s;
}
.breadcrumb a:hover {
background: rgba(233, 69, 96, 0.1);
text-decoration: none;
}
.breadcrumb .separator {
color: var(--text-muted);
padding: 0 2px;
}
.breadcrumb .current {
color: var(--text);
font-weight: 500;
padding: 4px 8px;
background: rgba(233, 69, 96, 0.08);
border-radius: 4px;
}
.file-list {
max-height: 500px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--primary) var(--bg-input);
}
.file-list::-webkit-scrollbar { width: 6px; }
.file-list::-webkit-scrollbar-track { background: var(--bg-input); }
.file-list::-webkit-scrollbar-thumb { background: var(--primary); border-radius: 3px; }
.file-list::-webkit-scrollbar-thumb:hover { background: #c93550; }
/* 文件项 - 大卡片式设计 */
.file-item {
padding: 12px 16px;
display: flex;
align-items: center;
gap: 14px;
cursor: pointer;
border-bottom: 1px solid var(--border);
transition: all 0.2s;
}
.file-item:last-child { border-bottom: none; }
.file-item:hover {
background: rgba(233, 69, 96, 0.05);
padding-left: 20px;
}
.file-item.selected {
background: rgba(233, 69, 96, 0.12);
padding-left: 20px;
border-left: 3px solid var(--primary);
}
.file-icon {
font-size: 32px;
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
flex-shrink: 0;
}
.file-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.file-name {
font-weight: 500;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--text);
}
.file-meta {
color: var(--text-muted);
font-size: 12px;
display: flex;
gap: 12px;
align-items: center;
}
.file-meta .meta-item {
display: flex;
align-items: center;
gap: 4px;
}
.file-actions {
display: flex;
gap: 6px;
opacity: 0;
transition: opacity 0.2s;
}
.file-item:hover .file-actions,
.file-item.selected .file-actions {
opacity: 1;
}
/* 工具栏 */
.toolbar { display: flex; gap: 12px; margin-bottom: 16px; align-items: center; flex-wrap: wrap; }
.toolbar-left { display: flex; gap: 12px; align-items: center; flex: 1; }
.toolbar-right { display: flex; gap: 12px; align-items: center; }
/* 系统资源 */
.resource-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
.resource-item { background: var(--bg-input); border-radius: 12px; padding: 16px; }
.resource-header { display: flex; justify-content: space-between; margin-bottom: 12px; }
.resource-label { color: var(--text-muted); font-size: 13px; }
.resource-value { font-weight: 600; }
.resource-bar { margin-top: 8px; }
/* 网格布局 */
.grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; }
@media (max-width: 1200px) { .grid-2 { grid-template-columns: 1fr; } }
/* 仪表盘底部行 - 下载趋势与服务器信息 */
.dashboard-bottom-row {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px;
}
.trend-card { min-height: 260px; }
.server-info-card { min-height: 260px; }
.server-details .detail-item:last-child { border-bottom: none !important; }
@media (max-width: 1200px) {
.dashboard-bottom-row { grid-template-columns: 1fr; }
.trend-card, .server-info-card { min-height: auto; }
}
/* 移动端标题栏内的菜单按钮 */
.mobile-menu-btn {
display: none;
width: 40px;
height: 40px;
border: none;
border-radius: 8px;
background: var(--bg-input);
color: var(--text);
font-size: 18px;
cursor: pointer;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
/* 移动端遮罩层 */
.sidebar-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 999;
}
.sidebar-overlay.active { display: block; }
/* 响应式 - 平板 */
@media (max-width: 1024px) {
.sidebar {
transform: translateX(-100%);
z-index: 1000;
transition: transform 0.3s ease;
width: 260px;
}
.sidebar.active { transform: translateX(0); }
.main-content { margin-left: 0; padding: 24px 16px; box-sizing: border-box; }
.mobile-menu-btn { display: flex; }
.stats-grid { grid-template-columns: repeat(2, 1fr); }
.resource-grid { grid-template-columns: 1fr; }
.page-header { gap: 12px; }
.page-header > div { flex: 1; min-width: 0; display: flex; align-items: baseline; gap: 8px; }
.page-header h1 { display: flex; align-items: center; gap: 8px; margin: 0; }
.page-header .desc { font-size: 13px; margin-top: 0; }
}
/* 响应式 - 移动端 */
@media (max-width: 600px) {
.main-content { padding: 16px 12px; box-sizing: border-box; width: 100%; max-width: 100vw; overflow-x: hidden; }
.page-header { flex-direction: row; align-items: center; justify-content: flex-start; gap: 8px; width: 100%; box-sizing: border-box; }
.page-header > div { flex: 1; min-width: 0; display: flex; align-items: baseline; gap: 8px; }
.page-header h1 { font-size: 18px; flex: 1; min-width: 0; margin: 0; white-space: nowrap; }
.page-header .desc { font-size: 12px; margin-top: 0; white-space: nowrap; }
.page-header > *:not(h1):not(div) { flex-shrink: 0; }
.stats-grid { grid-template-columns: 1fr 1fr; gap: 12px; }
.stat-card { padding: 16px; }
.stat-card .icon { font-size: 24px; }
.stat-card .value { font-size: 22px; }
.card { padding: 16px; }
.card-header { padding-bottom: 12px; margin-bottom: 12px; }
.card-title { font-size: 15px; }
.toolbar { gap: 8px; }
.toolbar-left, .toolbar-right { gap: 8px; flex-wrap: wrap; }
.search-box { width: 100%; order: -1; }
.search-box input { padding-left: 40px; }
.table-container { margin: 0 -16px; }
th, td { padding: 10px 12px; font-size: 13px; }
.modal-content { width: 95%; max-height: 90vh; margin: 20px; }
.modal-body { padding: 16px; }
.modal-header { padding: 16px; }
.modal-footer { padding: 12px 16px; }
.dashboard-bottom-row { gap: 16px; }
.btn { padding: 8px 14px; font-size: 13px; white-space: nowrap; }
.btn-sm { padding: 5px 10px; font-size: 12px; }
.input-group input, .input-group select, .input-group textarea { width: 100%; max-width: 100%; box-sizing: border-box; }
.grid-2, .grid-3 { grid-template-columns: 1fr; }
.config-card, .toggle-card { width: 100%; box-sizing: border-box; max-width: 100vw; }
.badge { font-size: 11px; padding: 3px 6px; }
* { max-width: 100vw; }
}
/* 小屏幕优化 */
@media (max-width: 480px) {
.stats-grid { grid-template-columns: 1fr; }
.stat-card { padding: 14px; }
.stat-card .value { font-size: 20px; }
.stat-card .label { font-size: 12px; }
}
</style>
</head>
<body>
<!-- 登录页 -->
<div class="login-container" id="loginPage">
<div class="login-box">
<h1>📦 HYC</h1>
<p class="subtitle">下载站管理控制台 v2.2</p>
<input type="text" id="loginUser" placeholder="用户名">
<input type="password" id="loginPass" placeholder="密码">
<input type="text" id="loginKey" placeholder="API Key (可选)">
<button onclick="login()">登录</button>
<div class="tips">
支持 Authorization Bearer / X-API-Key / Cookie 认证<br>
API Key 格式: hyc_xxx 或直接输入密钥
</div>
</div>
</div>
<!-- 主界面 -->
<div class="app-container" id="appPage">
<!-- 移动端侧边栏遮罩 -->
<div class="sidebar-overlay" id="sidebarOverlay" onclick="toggleSidebar()"></div>
<!-- 侧边栏 -->
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<h2>📦 HYC 下载站</h2>
</div>
<nav class="sidebar-nav">
<div class="nav-item active" data-page="dashboard">
<span class="icon">📊</span>
<span>仪表盘</span>
</div>
<div class="nav-item" data-page="files">
<span class="icon">📁</span>
<span>文件管理</span>
</div>
<div class="nav-item" data-page="sync">
<span class="icon">🔄</span>
<span>镜像同步</span>
</div>
<div class="nav-item" data-page="mirrors">
<span class="icon">🚀</span>
<span>加速源</span>
</div>
<div class="nav-item" data-page="downloads">
<span class="icon">📥</span>
<span>下载统计</span>
</div>
<div class="nav-item" data-page="config">
<span class="icon">📄</span>
<span>配置文件</span>
</div>
<div class="nav-item" data-page="settings">
<span class="icon">⚙️</span>
<span>系统设置</span>
</div>
<div class="nav-item" data-page="api-docs">
<span class="icon">📚</span>
<span>API 文档</span>
</div>
</nav>
<div class="sidebar-footer">
<button class="logout-btn" onclick="logout()">
<span>🚪</span>
<span>退出登录</span>
</button>
</div>
</aside>
<!-- 主内容 -->
<main class="main-content">
<!-- 仪表盘 -->
<div class="page active" id="page-dashboard">
<div class="page-header">
<div>
<h1>📊 系统仪表盘</h1>
<p class="desc">实时监控系统状态</p>
</div>
<span class="badge badge-success" id="serverStatus">● 运行中</span>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="icon">📁</div>
<div class="value" id="statFiles">-</div>
<div class="label">文件总数</div>
</div>
<div class="stat-card">
<div class="icon">💾</div>
<div class="value" id="statStorage">-</div>
<div class="label">存储空间</div>
</div>
<div class="stat-card">
<div class="icon">📥</div>
<div class="value" id="statDownloads">-</div>
<div class="label">今日下载</div>
</div>
</div>
<!-- 系统资源监控 -->
<div class="card">
<div class="card-header">
<span class="card-title"><span class="icon">🖥️</span>系统资源</span>
</div>
<div class="resource-grid" style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px;">
<div class="resource-item">
<div class="resource-header">
<span class="resource-label">CPU 使用率</span>
<span class="resource-value" id="cpuValue">-</span>
</div>
<div class="progress" style="height: 8px; background: var(--bg-input); border-radius: 4px; overflow: hidden;">
<div class="progress-bar" id="cpuBar" style="width: 0%; height: 100%; transition: width 0.3s ease; background: linear-gradient(90deg, var(--primary), var(--primary-hover));"></div>
</div>
</div>
<div class="resource-item">
<div class="resource-header">
<span class="resource-label">内存使用率</span>
<span class="resource-value" id="memValue">-</span>
</div>
<div class="progress" style="height: 8px; background: var(--bg-input); border-radius: 4px; overflow: hidden;">
<div class="progress-bar" id="memBar" style="width: 0%; height: 100%; transition: width 0.3s ease; background: linear-gradient(90deg, var(--success), #4ade80);"></div>
</div>
</div>
<div class="resource-item">
<div class="resource-header">
<span class="resource-label">磁盘使用率</span>
<span class="resource-value" id="diskValue">-</span>
</div>
<div class="progress" style="height: 8px; background: var(--bg-input); border-radius: 4px; overflow: hidden;">
<div class="progress-bar" id="diskBar" style="width: 0%; height: 100%; transition: width 0.3s ease; background: linear-gradient(90deg, var(--warning), #fbbf24);"></div>
</div>
</div>
</div>
</div>
<!-- 下载趋势与服务器信息 -->
<div class="dashboard-bottom-row">
<div class="card trend-card">
<div class="card-header">
<span class="card-title"><span class="icon">📈</span>下载趋势 (近7天)</span>
</div>
<div id="downloadChart" style="height: 180px; display: flex; align-items: flex-end; gap: 8px; padding: 20px 10px;">
<!-- 图表由 JS 生成 -->
</div>
</div>
<div class="card server-info-card">
<div class="card-header">
<span class="card-title"><span class="icon">ℹ️</span>服务器信息</span>
</div>
<div class="server-details" style="padding: 16px 20px;">
<div class="detail-item" style="display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid var(--border-color);">
<span style="color: var(--text-muted);">运行时间</span>
<span style="font-weight: 500;" id="serverUptimeDetail">-</span>
</div>
<div class="detail-item" style="display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid var(--border-color);">
<span style="color: var(--text-muted);">加速源数量</span>
<span style="font-weight: 500;" id="mirrorCount">-</span>
</div>
<div class="detail-item" style="display: flex; justify-content: space-between; padding: 10px 0;">
<span style="color: var(--text-muted);">同步状态</span>
<span style="font-weight: 500; color: var(--success);" id="syncStatus">正常</span>
</div>
</div>
</div>
</div>
<!-- 最近活动 -->
<div class="card">
<div class="card-header">
<span class="card-title"><span class="icon">📋</span>最近活动</span>
</div>
<div class="table-container" id="activityContainer">
<table>
<thead>
<tr>
<th style="width: 160px;">时间</th>
<th style="width: 100px;">类型</th>
<th>内容</th>
<th style="width: 100px;">状态</th>
</tr>
</thead>
<tbody id="activityLog">
<tr><td colspan="4" class="empty">加载中...</td></tr>
</tbody>
</table>
</div>
<div id="activityLoadMore" style="display: none; text-align: center; padding: 15px;">
<button class="btn btn-primary" onclick="loadMoreActivity()">加载更多</button>
</div>
<div id="activityLoading" style="display: none; text-align: center; padding: 15px; color: var(--text-muted);">
<span>加载中...</span>
</div>
<div id="activityEnd" style="display: none; text-align: center; padding: 15px; color: var(--text-muted); font-size: 12px;">
已显示全部记录
</div>
</div>
</div>
<!-- 文件管理 -->
<div class="page" id="page-files">
<div class="page-header">
<div>
<h1>📁 文件管理</h1>
<p class="desc">浏览和管理服务器上的文件</p>
</div>
</div>
<div class="card">
<div class="toolbar">
<div class="toolbar-left">
<button class="btn btn-secondary btn-sm" onclick="goUp()" id="goUpBtn" style="display: none;">⬆️ 返回上级</button>
<div class="search-box">
<span class="search-icon">🔍</span>
<input type="text" id="fileSearch" placeholder="搜索文件..." onkeyup="searchFiles()">
</div>
<button class="btn btn-secondary btn-sm" onclick="refreshFiles()">🔄 刷新</button>
</div>
<div class="toolbar-right">
<button class="btn btn-secondary" onclick="showModal('mkdirModal')">📁 新建目录</button>
<button class="btn btn-primary" onclick="showModal('uploadModal')">📤 上传文件</button>
</div>
</div>
<div class="breadcrumb" id="fileBreadcrumb">
<a onclick="navigateTo('/')">根目录</a>
</div>
<div class="file-browser">
<div class="file-list" id="fileList">
<div class="empty"><div class="icon"><i class="fa-solid fa-spinner fa-spin"></i></div><p>加载中...</p></div>
</div>
</div>
</div>
</div>
<!-- 镜像同步 -->
<div class="page" id="page-sync">
<div class="page-header">
<div>
<h1>🔄 镜像同步</h1>
<p class="desc">管理镜像同步源和同步任务</p>
</div>
<button class="btn btn-primary" onclick="showModal('addSyncModal')">+ 添加同步源</button>
</div>
<div class="tabs">
<button class="tab active" onclick="switchSyncTab('sources')">同步源</button>
<button class="tab" onclick="switchSyncTab('history')">同步历史</button>
</div>
<div id="sync-sources-tab">
<div class="card">
<div class="card-header">
<span class="card-title">同步源列表</span>
<span id="syncStatus" class="badge badge-info">空闲</span>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>名称</th>
<th>类型</th>
<th>目标路径</th>
<th>状态</th>
<th>进度</th>
<th>同步方式</th>
<th>操作</th>
</tr>
</thead>
<tbody id="syncList">
<tr><td colspan="7" class="empty">加载中...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<div id="sync-history-tab" style="display: none;">
<div class="card">
<div class="card-header">
<span class="card-title">同步历史记录</span>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>同步ID</th>
<th>类型</th>
<th>状态</th>
<th>文件数</th>
<th>大小</th>
<th>开始时间</th>
<th>耗时</th>
</tr>
</thead>
<tbody id="syncHistory">
<tr><td colspan="7" class="empty">暂无同步记录</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 加速源 -->
<div class="page" id="page-mirrors">
<div class="page-header">
<div>
<h1>🚀 加速源</h1>
<p class="desc">配置和管理下载加速源</p>
</div>
<button onclick="showAddMirrorModal()" class="btn btn-primary">
<i class="fa-solid fa-plus"></i> 添加加速源
</button>
</div>
<div class="card">
<div class="card-header">
<span class="card-title"><i class="fa-solid fa-list"></i> 加速源列表</span>
</div>
<div id="mirrorList">
<div style="text-align: center; padding: 40px; color: var(--text-muted);">
<i class="fa-solid fa-spinner fa-spin" style="font-size: 24px; margin-bottom: 10px;"></i>
<div>加载中...</div>
</div>
</div>
</div>
<div class="card" style="margin-top: 20px;">
<div class="card-header">
<span class="card-title">📝 使用说明</span>
</div>
<div id="mirrorUsageGuide" style="background: var(--bg-input); padding: 16px; border-radius: 8px; font-size: 13px; line-height: 1.8; overflow-wrap: break-word; word-break: break-all;">
<div style="color: var(--text-muted); text-align: center; padding: 20px;">
<i class="fa-solid fa-info-circle" style="font-size: 24px; margin-bottom: 10px;"></i>
<div>添加加速源后,此处将显示对应的使用命令</div>
</div>
</div>
</div>
</div>
<!-- 临时同步弹窗 -->
<div id="tempSyncModal" class="modal" style="display: none;">
<div class="modal-content" style="max-width: 500px;">
<div class="modal-header">
<h3><i class="fa-solid fa-bolt"></i> 临时单次同步</h3>
<button class="modal-close" onclick="document.getElementById('tempSyncModal').style.display='none'">&times;</button>
</div>
<div class="modal-body">
<div class="input-group">
<label><i class="fa-solid fa-server"></i> 同步源</label>
<input type="text" id="tempSyncSource" readonly />
</div>
<div class="input-group">
<label><i class="fa-solid fa-box"></i> 要同步的包名</label>
<textarea id="tempSyncPackages" rows="5" style="width: 100%; box-sizing: border-box;" placeholder="输入要同步的包名,多个包名用逗号或换行分隔"></textarea>
</div>
<div style="margin-top: 15px; color: var(--text-secondary); font-size: 12px;">
<i class="fa-solid fa-info-circle"></i> 此功能只同步指定的包,不会同步整个源
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="document.getElementById('tempSyncModal').style.display='none'">取消</button>
<button class="btn btn-primary" onclick="doTempSync()">
<i class="fa-solid fa-bolt"></i> 开始同步
</button>
</div>
</div>
</div>
<!-- 添加加速源弹窗 -->
<div id="addMirrorModal" class="modal" style="display: none;">
<div class="modal-content" style="max-width: 500px;">
<div class="modal-header">
<h3 id="mirrorModalTitle"><i class="fa-solid fa-plus-circle"></i> 添加加速源</h3>
<button class="modal-close" onclick="closeAddMirrorModal()">&times;</button>
</div>
<div class="modal-body">
<div class="input-group">
<label><i class="fa-solid fa-tag"></i> 加速源名称</label>
<input type="text" id="newMirrorName" placeholder="例如: docker-mirror" />
</div>
<div class="input-group">
<label><i class="fa-solid fa-layer-group"></i> 类型</label>
<select id="newMirrorType">
<option value="http">🌐 HTTP 文件服务</option>
<option value="docker">🐳 Docker Registry</option>
<option value="apt">📦 APT (Debian/Ubuntu)</option>
<option value="yum">🎯 YUM (CentOS/RHEL)</option>
<!-- Python -->
<option value="pypi">🐍 PyPI (Python)</option>
<option value="pip">📦 pip</option>
<option value="pipenv">🥃 Pipenv</option>
<option value="poetry">📝 Poetry</option>
<!-- Node.js -->
<option value="npm">📦 npm</option>
<option value="yarn">🧶 Yarn</option>
<option value="pnpm">📦 pnpm</option>
<option value="bower">🎀 Bower</option>
<!-- Go -->
<option value="go">🔷 Go 模块</option>
<!-- Java -->
<option value="maven">🫙 Maven</option>
<option value="gradle">🔨 Gradle</option>
<!-- .NET -->
<option value="nuget">📚 NuGet</option>
<!-- Ruby -->
<option value="gem">💎 RubyGems</option>
<!-- Rust -->
<option value="cargo">🦀 Cargo</option>
<!-- PHP -->
<option value="composer">🎼 Composer</option>
<!-- Swift/Apple -->
<option value="cocoapods">🍎 CocoaPods</option>
<!-- C/C++ -->
<option value="conan">⚔️ Conan</option>
<!-- Dart/Flutter -->
<option value="pub">🎯 Pub.dev</option>
<option value="dart">🐦 Dart SDK</option>
<option value="flutter">🪶 Flutter</option>
<!-- Haskell -->
<option value="hackage">🦄 Hackage</option>
<option value="stackage">📚 Stackage</option>
<!-- OCaml -->
<option value="opam">🔶 OPAM</option>
<!-- D语言 -->
<option value="dub">💠 Dub</option>
<!-- Nim -->
<option value="nimble">⚡ Nimble</option>
<!-- Julia -->
<option value="julia">🔬 Julia</option>
<!-- Lua -->
<option value="lua">🌙 Lua</option>
<option value="luarocks">🌙 LuaRocks</option>
<!-- Elm -->
<option value="elm">🌳 Elm</option>
<!-- V -->
<option value="v">V V语言</option>
<!-- Helm -->
<option value="helm">⚓ Helm Charts</option>
<!-- R -->
<option value="cran">📊 CRAN</option>
<!-- LaTeX -->
<option value="ctan">📝 CTAN</option>
<!-- CUDA -->
<option value="cuda">🎮 CUDA</option>
<!-- Linux 发行版 -->
<option value="alpine">🏔️ Alpine</option>
<option value="arch">🔵 Arch</option>
<option value="aur">📦 AUR</option>
<option value="centos">🎯 CentOS</option>
<option value="debian">🐱 Debian</option>
<option value="fedora">🎩 Fedora</option>
<option value="gentoo">🧬 Gentoo</option>
<option value="opensuse">🦎 openSUSE</option>
<option value="void">➰ Void</option>
<option value="freebsd">🦅 FreeBSD</option>
<option value="netbsd">🌐 NetBSD</option>
<option value="openbsd">🐡 OpenBSD</option>
<option value="rocky">🪨 Rocky Linux</option>
<option value="alma">🔔 AlmaLinux</option>
<option value="kali">🔪 Kali Linux</option>
<option value="ubuntu">🟠 Ubuntu</option>
<option value="mint">🌿 Linux Mint</option>
<!-- 开发工具 -->
<option value="jetbrains">💜 JetBrains</option>
<option value="vscode">💙 VS Code</option>
<!-- 语言运行时 -->
<option value="rustup">🦀 Rustup</option>
<option value="dotnet">🔷 .NET SDK</option>
<option value="node">🟢 Node.js</option>
<option value="python">🐍 Python</option>
<option value="ruby">💎 Ruby</option>
<option value="php">🐘 PHP</option>
<!-- 系统包管理器 -->
<option value="homebrew">🍺 Homebrew</option>
<option value="brew">🍺 Brew</option>
<option value="chocolatey">🍫 Chocolatey</option>
<option value="snap">📦 Snap</option>
<option value="flatpak">📦 Flatpak</option>
<option value="winget">🪟 winget</option>
<option value="scoop">🥄 Scoop</option>
<!-- 其他 -->
<option value="termux">📱 Termux</option>
<option value="msys2">💻 MSYS2</option>
<option value="nix">❄️ Nix</option>
<option value="guix">🌿 Guix</option>
<option value="custom">✨ 自定义</option>
</select>
</div>
<div class="input-group">
<label><i class="fa-solid fa-link"></i> 上游地址</label>
<input type="text" id="newMirrorUrl" placeholder="例如: https://mirror.example.com" />
</div>
<div class="input-group">
<label><i class="fa-solid fa-bullseye"></i> 目标路径 (可选)</label>
<input type="text" id="newMirrorTarget" placeholder="例如: docker, apt/ubuntu" />
</div>
<div class="input-group">
<label><i class="fa-solid fa-folder"></i> 存储目录 (可选)</label>
<input type="text" id="newMirrorStorageDir" placeholder="例如: docker-cn, pypi-cn (默认: 镜像名称)" />
</div>
<div class="input-group">
<label><i class="fa-solid fa-pen-to-square"></i> 描述 (可选)</label>
<input type="text" id="newMirrorDesc" placeholder="描述这个加速源的用途" />
</div>
<div style="display: flex; align-items: center; gap: 10px; padding: 12px; background: var(--bg-input); border-radius: 8px;">
<input type="checkbox" id="newMirrorEnabled" checked />
<span>启用此加速源</span>
</div>
</div>
<div class="modal-footer">
<button onclick="closeAddMirrorModal()" class="btn btn-secondary">取消</button>
<button onclick="addMirror()" id="mirrorModalBtn" class="btn btn-primary">添加</button>
</div>
</div>
</div>
<!-- 配置文件 -->
<div class="page" id="page-config">
<div class="page-header">
<div>
<h1>📄 配置文件</h1>
<p class="desc">查看和编辑服务器配置文件</p>
</div>
<div style="display: flex; gap: 10px;">
<button onclick="reloadConfig()" class="btn btn-secondary">🔄 重新加载</button>
<button onclick="saveConfig()" class="btn btn-primary">💾 保存配置</button>
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">settings.json</span>
<span id="configPath" style="color: var(--text-muted); font-size: 12px;"></span>
</div>
<div style="position: relative;">
<textarea id="configEditor" style="width: 100%; min-height: 500px; max-height: 70vh; background: var(--bg-input); border: 1px solid var(--border); border-radius: 8px; padding: 16px; color: var(--text); font-family: 'Monaco', 'Menlo', 'Consolas', monospace; font-size: 13px; line-height: 1.6; resize: vertical; overflow-y: auto;" placeholder="加载配置中..."></textarea>
<div id="configJsonError" style="position: absolute; bottom: 10px; right: 10px; background: var(--danger); color: white; padding: 8px 12px; border-radius: 6px; font-size: 12px; display: none;"></div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 12px;">
<span id="configStatus" style="color: var(--text-muted); font-size: 12px;"></span>
<div style="display: flex; gap: 10px;">
<button onclick="formatConfig()" class="btn btn-secondary" style="padding: 8px 16px;">📐 格式化</button>
<button onclick="resetConfig()" class="btn btn-warning" style="padding: 8px 16px;">🔄 重置</button>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">📋 配置说明</span>
</div>
<div style="background: var(--bg-input); padding: 16px; border-radius: 8px; font-size: 13px; line-height: 1.8;">
<div style="margin-bottom: 12px;">
<strong style="color: var(--info);">mirrors</strong> - 加速源配置<br>
<code style="color: var(--text-muted);">"enabled": true/false</code> - 启用或禁用该加速源
</div>
<div style="margin-bottom: 12px;">
<strong style="color: var(--info);">自定义加速源</strong><br>
添加自定义镜像源,支持 HTTP、Git 等类型
</div>
<div>
<strong style="color: var(--warning);">注意:</strong>保存后需要点击"重新加载"或重启服务器使配置生效
</div>
</div>
</div>
</div>
<!-- 下载统计 -->
<div class="page" id="page-downloads">
<div class="page-header">
<div>
<h1>📥 下载统计</h1>
<p class="desc">下载量和流量统计</p>
</div>
</div>
<!-- 核心指标区 - 2行4列 -->
<div class="stats-grid" style="grid-template-columns: repeat(4, 1fr);">
<div class="stat-card stat-card-primary">
<div class="stat-card-icon">📥</div>
<div class="stat-card-content">
<div class="stat-card-value" id="todayDl">-</div>
<div class="stat-card-label">今日下载</div>
</div>
</div>
<div class="stat-card stat-card-info">
<div class="stat-card-icon">📅</div>
<div class="stat-card-content">
<div class="stat-card-value" id="weekDl">-</div>
<div class="stat-card-label">本周下载</div>
</div>
</div>
<div class="stat-card stat-card-warning">
<div class="stat-card-icon">📆</div>
<div class="stat-card-content">
<div class="stat-card-value" id="yearDl">-</div>
<div class="stat-card-label">今年下载</div>
</div>
</div>
<div class="stat-card stat-card-warning">
<div class="stat-card-icon">📊</div>
<div class="stat-card-content">
<div class="stat-card-value" id="totalDl">-</div>
<div class="stat-card-label">累计下载</div>
</div>
</div>
</div>
<!-- 图表区域 -->
<div class="card" style="margin-top: 20px;">
<div class="card-header">
<span class="card-title">📊 下载趋势分析</span>
<div style="display: flex; gap: 10px; align-items: center;">
<!-- 时间粒度选择 -->
<select id="periodSelect" onchange="changePeriod()" style="padding: 6px 12px; border-radius: 6px; background: var(--bg-input); color: var(--text); border: 1px solid var(--border);">
<option value="year" selected>按年统计</option>
<option value="month">按月统计</option>
</select>
<!-- 年份选择 -->
<select id="yearSelect" onchange="changePeriod()" style="padding: 6px 12px; border-radius: 6px; background: var(--bg-input); color: var(--text); border: 1px solid var(--border);">
</select>
<!-- 月份选择 -->
<select id="monthSelect" onchange="changePeriod()" style="padding: 6px 12px; border-radius: 6px; background: var(--bg-input); color: var(--text); border: 1px solid var(--border);">
</select>
<!-- 图表类型切换 -->
<div class="chart-type-toggle">
<button class="btn btn-sm" id="lineChartBtn" onclick="switchChartType('line')" title="折线图">
<i class="fas fa-chart-line"></i>
</button>
<button class="btn btn-sm" id="barChartBtn" onclick="switchChartType('bar')" title="柱状图">
<i class="fas fa-chart-bar"></i>
</button>
</div>
</div>
</div>
<div style="padding: 20px;">
<canvas id="downloadChartCanvas" height="100"></canvas>
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">下载排行 TOP 20</span>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th style="width: 60px; text-align: center;">排名</th>
<th>文件</th>
<th style="width: 100px; text-align: center;">下载次数</th>
<th style="width: 100px; text-align: center;">大小</th>
<th style="width: 120px; text-align: center;">占比</th>
</tr>
</thead>
<tbody id="downloadRank">
<tr><td colspan="5" class="empty">加载中...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 系统设置 -->
<div class="page" id="page-settings">
<div class="page-header">
<div>
<h1>⚙️ 系统设置</h1>
<p class="desc">系统配置和安全管理</p>
</div>
<button class="btn btn-primary" onclick="saveSettings()">💾 保存设置</button>
</div>
<div class="tabs">
<button class="tab active" onclick="switchSettingsTab('security')">安全</button>
<button class="tab" onclick="switchSettingsTab('storage')">存储</button>
<button class="tab" onclick="switchSettingsTab('sync')">同步</button>
<button class="tab" onclick="switchSettingsTab('features')">功能</button>
<button class="tab" onclick="switchSettingsTab('performance')">性能</button>
<button class="tab" onclick="switchSettingsTab('log')">日志</button>
<button class="tab" onclick="switchSettingsTab('alerts')">告警</button>
</div>
<div id="settings-security">
<div class="card">
<div class="card-header">
<span class="card-title">ℹ️ 服务器信息</span>
</div>
<div class="stats-grid" style="margin-bottom: 0;">
<div class="stat-card">
<div class="icon">⏱️</div>
<div class="value" id="serverUptime">-</div>
<div class="label">运行时间</div>
</div>
<div class="stat-card">
<div class="icon">📊</div>
<div class="value" id="serverVersion">-</div>
<div class="label">版本</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">🔒 IP 访问控制</span>
</div>
<div class="form-group">
<label>IP 白名单 (每行一个,支持 CIDR)</label>
<textarea id="ipWhitelist" rows="5" placeholder="127.0.0.1&#10;192.168.1.0/24&#10;10.0.0.0/8"></textarea>
</div>
<div class="form-group">
<label>IP 黑名单 (每行一个)</label>
<textarea id="ipBlacklist" rows="5" placeholder="1.2.3.4&#10;5.6.7.8"></textarea>
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">⚡ 速率限制</span>
</div>
<div class="form-group" style="max-width: 300px;">
<label>请求速率限制 (请求/分钟)</label>
<input type="number" id="rateLimit" value="100">
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">🔐 认证设置</span>
</div>
<div class="form-group" style="max-width: 300px;">
<label>认证方式</label>
<select id="authType">
<option value="none">无认证</option>
<option value="basic">Basic Auth</option>
<option value="token">Token</option>
</select>
</div>
<div class="form-group">
<label>会话超时时间 (分钟)</label>
<input type="number" id="sessionTimeout" value="60" style="max-width: 300px;">
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">🔑 修改密码</span>
</div>
<div class="form-group">
<label>用户名</label>
<input type="text" id="changePasswordUsername" placeholder="admin" style="max-width: 300px;">
</div>
<div class="form-group">
<label>原密码 (可选,不填则强制修改)</label>
<input type="password" id="changePasswordOld" placeholder="输入原密码" style="max-width: 300px;">
</div>
<div class="form-group">
<label>新密码</label>
<input type="password" id="changePasswordNew" placeholder="输入新密码" style="max-width: 300px;">
</div>
<div class="form-group">
<label>确认新密码</label>
<input type="password" id="changePasswordConfirm" placeholder="再次输入新密码" style="max-width: 300px;">
</div>
<button class="btn btn-primary" onclick="changePassword()">
<i class="fa-solid fa-key"></i> 修改密码
</button>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">📋 登录日志</span>
<button class="btn btn-sm" onclick="loadLoginLogs()">
<i class="fa-solid fa-refresh"></i> 刷新
</button>
</div>
<div class="table-container" style="max-height: 300px; overflow-y: auto;">
<table>
<thead>
<tr>
<th>时间</th>
<th>用户名</th>
<th>状态</th>
<th>IP</th>
<th>原因</th>
</tr>
</thead>
<tbody id="loginLogsTable">
<tr><td colspan="5" class="empty">点击刷新加载日志</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<div id="settings-storage" style="display: none;">
<div class="card">
<div class="card-header">
<span class="card-title">🖥️ 服务器配置</span>
</div>
<div class="form-group" style="max-width: 300px;">
<label>服务器名称</label>
<input type="text" id="serverName" placeholder="HYC下载站">
</div>
<div class="form-group" style="max-width: 300px;">
<label>监听地址</label>
<input type="text" id="serverHost" placeholder="0.0.0.0">
</div>
<div class="form-group" style="max-width: 300px;">
<label>监听端口</label>
<input type="number" id="serverPort" value="8080">
</div>
<div class="form-group" style="max-width: 300px;">
<label>API 版本</label>
<select id="apiVersion">
<option value="v1">v1 (基础)</option>
<option value="v2">v2 (增强)</option>
</select>
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">📁 存储配置</span>
</div>
<div class="form-group">
<label>文件存储目录</label>
<input type="text" id="storagePath" placeholder="./downloads">
</div>
<div class="form-group" style="max-width: 300px;">
<label>排序方式</label>
<select id="sortBy">
<option value="name">名称</option>
<option value="size">大小</option>
<option value="time">时间</option>
</select>
</div>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; margin-top: 12px;">
<input type="checkbox" id="sortReverse">
<span>反向排序</span>
</label>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">🗄️ 数据库配置</span>
</div>
<div class="form-group" style="max-width: 300px;">
<label>数据库类型</label>
<select id="dbType" onchange="switchDbType()">
<option value="sqlite">SQLite</option>
<option value="postgresql">PostgreSQL</option>
<option value="mysql">MySQL</option>
</select>
</div>
<!-- SQLite 配置 -->
<div id="db-sqlite-config">
<div class="form-group">
<label>数据库文件路径</label>
<input type="text" id="dbPath" placeholder="./data/hyc.db">
</div>
</div>
<!-- PostgreSQL 配置 -->
<div id="db-postgresql-config" style="display: none;">
<div class="form-group" style="max-width: 300px;">
<label>主机</label>
<input type="text" id="dbHost" placeholder="localhost">
</div>
<div class="form-group" style="max-width: 300px;">
<label>端口</label>
<input type="number" id="dbPort" value="5432">
</div>
<div class="form-group" style="max-width: 300px;">
<label>用户</label>
<input type="text" id="dbUser" placeholder="postgres">
</div>
<div class="form-group" style="max-width: 300px;">
<label>密码</label>
<input type="password" id="dbPassword" placeholder="数据库密码">
</div>
<div class="form-group" style="max-width: 300px;">
<label>数据库名称</label>
<input type="text" id="dbName" placeholder="hyc">
</div>
</div>
<!-- MySQL 配置 -->
<div id="db-mysql-config" style="display: none;">
<div class="form-group" style="max-width: 300px;">
<label>主机</label>
<input type="text" id="mysqlHost" placeholder="localhost">
</div>
<div class="form-group" style="max-width: 300px;">
<label>端口</label>
<input type="number" id="mysqlPort" value="3306">
</div>
<div class="form-group" style="max-width: 300px;">
<label>用户</label>
<input type="text" id="mysqlUser" placeholder="root">
</div>
<div class="form-group" style="max-width: 300px;">
<label>密码</label>
<input type="password" id="mysqlPassword" placeholder="数据库密码">
</div>
<div class="form-group" style="max-width: 300px;">
<label>数据库名称</label>
<input type="text" id="mysqlName" placeholder="hyc">
</div>
</div>
</div>
</div>
<div id="settings-sync" style="display: none;">
<div class="card">
<div class="card-header">
<span class="card-title">🔄 同步配置</span>
</div>
<div class="form-group" style="max-width: 300px;">
<label>自动同步间隔 (分钟)</label>
<input type="number" id="syncInterval" value="60">
</div>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" id="autoSync">
<span>启用自动同步</span>
</label>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">🧹 自动清理</span>
</div>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" id="autoClean">
<span>启用自动清理</span>
</label>
<div class="form-group" style="max-width: 300px; margin-top: 12px;">
<label>清理间隔 (天)</label>
<input type="number" id="cleanInterval" value="7">
</div>
</div>
</div>
<div id="settings-features" style="display: none;">
<div class="card">
<div class="card-header">
<span class="card-title">📂 文件浏览</span>
</div>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" id="enableBrowse" checked>
<span>启用目录浏览</span>
</label>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; margin-top: 12px;">
<input type="checkbox" id="showHidden">
<span>显示隐藏文件</span>
</label>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; margin-top: 12px;">
<input type="checkbox" id="showHash" checked>
<span>显示文件哈希</span>
</label>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">🔌 实时通信</span>
</div>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" id="enableWebSocket" checked>
<span>启用 WebSocket</span>
</label>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; margin-top: 12px;">
<input type="checkbox" id="enableSSE" checked>
<span>启用 SSE</span>
</label>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">📊 监控</span>
</div>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" id="enableMonitor" checked>
<span>启用监控</span>
</label>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">🔍 搜索设置</span>
</div>
<div class="form-group" style="max-width: 300px;">
<label>最大搜索结果数</label>
<input type="number" id="maxSearchResults" value="100">
</div>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; margin-top: 12px;">
<input type="checkbox" id="calculateHash">
<span>计算文件哈希</span>
</label>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">🔐 SSL/HTTPS 设置</span>
</div>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" id="enableSSL">
<span>启用 SSL/HTTPS</span>
</label>
<div class="form-group" style="max-width: 500px; margin-top: 12px;">
<label>SSL 证书路径</label>
<input type="text" id="sslCert" placeholder="/path/to/cert.pem">
</div>
<div class="form-group" style="max-width: 500px;">
<label>SSL 密钥路径</label>
<input type="text" id="sslKey" placeholder="/path/to/key.pem">
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">❤️ 健康检查</span>
</div>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" id="enableHealthCheck">
<span>启用健康检查</span>
</label>
<div class="form-group" style="max-width: 300px; margin-top: 12px;">
<label>健康检查间隔 (秒)</label>
<input type="number" id="healthCheckInterval" value="60">
</div>
<div class="form-group" style="max-width: 300px;">
<label>超时时间 (秒)</label>
<input type="number" id="healthCheckTimeout" value="10">
</div>
<div class="form-group" style="max-width: 300px;">
<label>最大重试次数</label>
<input type="number" id="healthCheckRetries" value="3">
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">⚡ 故障转移</span>
</div>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" id="enableFailover">
<span>启用故障转移</span>
</label>
<div class="form-group" style="max-width: 300px; margin-top: 12px;">
<label>故障阈值 (次)</label>
<input type="number" id="failoverThreshold" value="3">
</div>
</div>
</div>
<div id="settings-performance" style="display: none;">
<div class="card">
<div class="card-header">
<span class="card-title">⚙️ 线程与监控</span>
</div>
<div class="form-group" style="max-width: 300px;">
<label>最大线程数</label>
<input type="number" id="maxThreads" value="4">
</div>
<div class="form-group" style="max-width: 300px;">
<label>监控间隔 (秒)</label>
<input type="number" id="monitorInterval" value="3">
</div>
<div class="form-group" style="max-width: 300px;">
<label>健康检查间隔 (秒)</label>
<input type="number" id="healthInterval" value="30">
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">📁 文件上传</span>
</div>
<div class="form-group" style="max-width: 300px;">
<label>最大上传大小 (MB)</label>
<input type="number" id="maxUploadSize" value="1024">
</div>
<div class="form-group" style="max-width: 300px;">
<label>请求超时 (秒)</label>
<input type="number" id="requestTimeout" value="300">
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">🧵 连接池配置</span>
</div>
<div class="form-group" style="max-width: 300px;">
<label>连接池大小</label>
<input type="number" id="poolSize" value="5">
</div>
<div class="form-group" style="max-width: 300px;">
<label>最大溢出连接</label>
<input type="number" id="maxOverflow" value="10">
</div>
</div>
</div>
<div id="settings-log" style="display: none;">
<div class="card">
<div class="card-header">
<span class="card-title">🔍 调试模式</span>
</div>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" id="debugMode">
<span>启用调试模式</span>
</label>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; margin-top: 12px;">
<input type="checkbox" id="debugHttp">
<span>调试 HTTP 请求</span>
</label>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; margin-top: 12px;">
<input type="checkbox" id="debugAuth">
<span>调试认证</span>
</label>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">📝 日志配置</span>
</div>
<div class="form-group">
<label>日志文件路径</label>
<input type="text" id="logFile" placeholder="./logs/hyc.log">
</div>
<div class="form-group" style="max-width: 300px;">
<label>日志级别</label>
<select id="logLevel">
<option value="DEBUG">DEBUG</option>
<option value="INFO">INFO</option>
<option value="WARNING">WARNING</option>
<option value="ERROR">ERROR</option>
</select>
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">🛡️ 安全选项</span>
</div>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" id="enableRange" checked>
<span>启用 HTTP Range 请求 (断点续传)</span>
</label>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; margin-top: 12px;">
<input type="checkbox" id="enableStats" checked>
<span>启用统计</span>
</label>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">📦 缓存配置</span>
</div>
<div class="form-group" style="max-width: 300px;">
<label>缓存大小 (MB)</label>
<input type="number" id="cacheSize" value="1024">
</div>
<div class="form-group" style="max-width: 300px;">
<label>缓存 TTL (秒)</label>
<input type="number" id="cacheTTL" value="3600">
</div>
</div>
</div>
<div id="settings-alerts" style="display: none;">
<div class="card">
<div class="card-header">
<span class="card-title">🔔 告警设置</span>
</div>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" id="enableAlerts" checked>
<span>启用告警</span>
</label>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">💾 磁盘告警</span>
</div>
<div class="form-group" style="max-width: 300px;">
<label>磁盘使用率警告阈值 (%)</label>
<input type="number" id="diskWarning" value="80">
</div>
<div class="form-group" style="max-width: 300px;">
<label>磁盘使用率严重阈值 (%)</label>
<input type="number" id="diskCritical" value="90">
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">📧 邮件通知</span>
</div>
<div class="form-group">
<label>SMTP 服务器</label>
<input type="text" id="smtpServer" placeholder="smtp.example.com">
</div>
<div class="form-group" style="max-width: 300px;">
<label>SMTP 端口</label>
<input type="number" id="smtpPort" value="587">
</div>
<div class="form-group">
<label>发送邮箱</label>
<input type="email" id="smtpFrom" placeholder="[email protected]">
</div>
<div class="form-group">
<label>收件人邮箱</label>
<input type="email" id="alertEmail" placeholder="[email protected]">
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">🔗 Webhook 通知</span>
</div>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" id="enableWebhook">
<span>启用 Webhook</span>
</label>
<div class="form-group" style="max-width: 500px; margin-top: 12px;">
<label>Webhook URL</label>
<input type="url" id="webhookUrl" placeholder="https://example.com/webhook">
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">📋 定时任务</span>
</div>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" id="enableCleanupTask">
<span>启用数据库清理任务</span>
</label>
<div class="form-group" style="max-width: 300px; margin-top: 12px;">
<label>清理任务 Cron 表达式</label>
<input type="text" id="cleanupCron" placeholder="0 2 * * *" value="0 2 * * *">
</div>
</div>
</div>
</div>
<!-- API 文档 -->
<div class="page" id="page-api-docs">
<div class="page-header">
<div>
<h1>📚 API 文档</h1>
<p class="desc">接口调用说明</p>
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">🔑 认证方式</span>
</div>
<pre style="background: var(--bg-input); padding: 16px; border-radius: 8px; overflow-x: auto; font-size: 13px; line-height: 1.8;">
# 1. Bearer Token
Authorization: Bearer YOUR_API_KEY
# 2. API Key Header (推荐)
X-API-Key: YOUR_API_KEY
# 3. Cookie
Cookie: hyc_auth=SESSION_ID
# 4. Query Parameter
?key=YOUR_API_KEY</pre>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">📁 文件 API</span>
</div>
<div class="table-container">
<table>
<thead>
<tr><th>方法</th><th>路径</th><th>说明</th><th>认证</th></tr>
</thead>
<tbody>
<tr><td><span class="badge badge-info">GET</span></td><td>/api/v1/files</td><td>列出文件</td><td>-</td></tr>
<tr><td><span class="badge badge-info">GET</span></td><td>/api/v1/file/*</td><td>获取文件信息</td><td>-</td></tr>
<tr><td><span class="badge badge-info">GET</span></td><td>/api/v1/file/*/preview</td><td>预览文件</td><td>-</td></tr>
<tr><td><span class="badge badge-danger">DELETE</span></td><td>/api/v1/file/*</td><td>删除文件</td><td>admin</td></tr>
<tr><td><span class="badge badge-warning">PUT</span></td><td>/api/v1/mkdir</td><td>创建目录</td><td>admin</td></tr>
<tr><td><span class="badge badge-warning">PUT</span></td><td>/api/v1/file/rename</td><td>重命名文件</td><td>admin</td></tr>
<tr><td><span class="badge badge-primary">POST</span></td><td>/api/v1/upload</td><td>上传文件</td><td>admin</td></tr>
<tr><td><span class="badge badge-primary">POST</span></td><td>/api/v1/batch</td><td>批量操作</td><td>admin</td></tr>
<tr><td><span class="badge badge-info">GET</span></td><td>/api/v1/search</td><td>搜索文件</td><td>-</td></tr>
</tbody>
</table>
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">🔄 同步 API</span>
</div>
<div class="table-container">
<table>
<thead>
<tr><th>方法</th><th>路径</th><th>说明</th></tr>
</thead>
<tbody>
<tr><td><span class="badge badge-info">GET</span></td><td>/api/v1/sync/sources</td><td>获取同步源</td></tr>
<tr><td><span class="badge badge-primary">POST</span></td><td>/api/v1/sync/sources</td><td>添加同步源</td></tr>
<tr><td><span class="badge badge-danger">DELETE</span></td><td>/api/v1/sync/sources/{name}</td><td>删除同步源</td></tr>
<tr><td><span class="badge badge-primary">POST</span></td><td>/api/v1/sync/start</td><td>开始同步</td></tr>
<tr><td><span class="badge badge-warning">POST</span></td><td>/api/v1/sync/stop</td><td>停止同步</td></tr>
<tr><td><span class="badge badge-info">GET</span></td><td>/api/v1/sync/status</td><td>获取状态</td></tr>
</tbody>
</table>
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">🚀 加速源 API</span>
</div>
<div class="table-container">
<table>
<thead>
<tr><th>方法</th><th>路径</th><th>说明</th></tr>
</thead>
<tbody>
<tr><td><span class="badge badge-info">GET</span></td><td>/api/v2/mirrors</td><td>获取镜像列表</td></tr>
<tr><td><span class="badge badge-primary">POST</span></td><td>/api/v2/mirrors</td><td>添加镜像</td></tr>
<tr><td><span class="badge badge-warning">PUT</span></td><td>/api/v2/mirrors/{name}</td><td>更新镜像</td></tr>
<tr><td><span class="badge badge-danger">DELETE</span></td><td>/api/v2/mirrors/{name}</td><td>删除镜像</td></tr>
<tr><td><span class="badge badge-warning">PUT</span></td><td>/api/v2/mirrors/{name}/enable</td><td>启用/禁用</td></tr>
<tr><td><span class="badge badge-primary">POST</span></td><td>/api/v2/mirrors/{name}/refresh</td><td>刷新镜像</td></tr>
</tbody>
</table>
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">📊 监控 API</span>
</div>
<div class="table-container">
<table>
<thead>
<tr><th>方法</th><th>路径</th><th>说明</th></tr>
</thead>
<tbody>
<tr><td><span class="badge badge-info">GET</span></td><td>/api/v1/stats</td><td>基础统计</td></tr>
<tr><td><span class="badge badge-info">GET</span></td><td>/api/v2/monitor/realtime</td><td>实时监控</td></tr>
<tr><td><span class="badge badge-info">GET</span></td><td>/api/v1/health</td><td>健康检查</td></tr>
<tr><td><span class="badge badge-info">GET</span></td><td>/api/v2/server/info</td><td>服务器信息</td></tr>
</tbody>
</table>
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">🔔 告警 API</span>
</div>
<div class="table-container">
<table>
<thead>
<tr><th>方法</th><th>路径</th><th>说明</th></tr>
</thead>
<tbody>
<tr><td><span class="badge badge-info">GET</span></td><td>/api/v2/alerts</td><td>获取告警</td></tr>
<tr><td><span class="badge badge-primary">POST</span></td><td>/api/v2/alerts/{id}/acknowledge</td><td>确认告警</td></tr>
<tr><td><span class="badge badge-info">GET</span></td><td>/api/v2/alerts/config</td><td>告警配置</td></tr>
<tr><td><span class="badge badge-warning">PUT</span></td><td>/api/v2/alerts/config</td><td>更新告警配置</td></tr>
</tbody>
</table>
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">🪝 Webhooks API</span>
</div>
<div class="table-container">
<table>
<thead>
<tr><th>方法</th><th>路径</th><th>说明</th></tr>
</thead>
<tbody>
<tr><td><span class="badge badge-info">GET</span></td><td>/api/v2/webhooks</td><td>列出 Webhooks</td></tr>
<tr><td><span class="badge badge-primary">POST</span></td><td>/api/v2/webhooks</td><td>创建 Webhook</td></tr>
<tr><td><span class="badge badge-warning">PUT</span></td><td>/api/v2/webhooks/{id}</td><td>更新 Webhook</td></tr>
<tr><td><span class="badge badge-danger">DELETE</span></td><td>/api/v2/webhooks/{id}</td><td>删除 Webhook</td></tr>
<tr><td><span class="badge badge-primary">POST</span></td><td>/api/v2/webhooks/{id}/test</td><td>测试 Webhook</td></tr>
</tbody>
</table>
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">👥 管理 API</span>
</div>
<div class="table-container">
<table>
<thead>
<tr><th>方法</th><th>路径</th><th>说明</th></tr>
</thead>
<tbody>
<tr><td><span class="badge badge-info">GET</span></td><td>/api/v2/admin/stats</td><td>获取统计</td></tr>
<tr><td><span class="badge badge-primary">POST</span></td><td>/api/v2/admin/auth/verify</td><td>验证认证</td></tr>
<tr><td><span class="badge badge-warning">POST</span></td><td>/api/v2/server/restart</td><td>重启服务器</td></tr>
</tbody>
</table>
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">⚙️ 配置 API</span>
</div>
<div class="table-container">
<table>
<thead>
<tr><th>方法</th><th>路径</th><th>说明</th></tr>
</thead>
<tbody>
<tr><td><span class="badge badge-info">GET</span></td><td>/api/v1/config</td><td>获取配置</td></tr>
<tr><td><span class="badge badge-warning">PUT</span></td><td>/api/v2/config</td><td>保存配置</td></tr>
<tr><td><span class="badge badge-primary">POST</span></td><td>/api/v2/config/reload</td><td>重新加载</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</main>
</div>
<!-- 上传对话框 -->
<div class="modal" id="uploadModal">
<div class="modal-content">
<div class="modal-header">
<h3>📤 上传文件</h3>
<button class="modal-close" onclick="hideModal('uploadModal')">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>选择文件</label>
<input type="file" id="uploadFile" multiple>
</div>
<div class="form-group">
<label>目标目录</label>
<input type="text" id="uploadPath" placeholder="留空为根目录">
</div>
<div id="uploadProgress" style="display: none;">
<div class="progress" style="height: 10px;"><div class="progress-bar" id="uploadBar" style="width: 0%"></div></div>
<p style="text-align: center; margin-top: 8px; font-size: 13px; color: var(--text-muted);" id="uploadStatus">上传中...</p>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="hideModal('uploadModal')">取消</button>
<button class="btn btn-primary" onclick="uploadFiles()">上传</button>
</div>
</div>
</div>
<!-- 新建目录对话框 -->
<div class="modal" id="mkdirModal">
<div class="modal-content">
<div class="modal-header">
<h3>📁 新建目录</h3>
<button class="modal-close" onclick="hideModal('mkdirModal')">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>目录路径</label>
<input type="text" id="mkdirPath" placeholder="例如: downloads/newfolder">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="hideModal('mkdirModal')">取消</button>
<button class="btn btn-primary" onclick="createDirectory()">创建</button>
</div>
</div>
</div>
<!-- 重命名文件对话框 -->
<div class="modal" id="renameModal">
<div class="modal-content">
<div class="modal-header">
<h3>✏️ 重命名文件</h3>
<button class="modal-close" onclick="hideModal('renameModal')">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>新文件名</label>
<input type="text" id="renameFileName" placeholder="输入新文件名">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="hideModal('renameModal')">取消</button>
<button class="btn btn-primary" onclick="renameFile()">重命名</button>
</div>
</div>
</div>
<!-- 文件预览对话框 -->
<div class="modal" id="previewModal">
<div class="modal-content" style="max-width: 900px; width: 95vw;">
<div class="modal-header">
<h3 id="previewFileName">📄 文件预览</h3>
<button class="modal-close" onclick="hideModal('previewModal')">×</button>
</div>
<div class="modal-body" id="previewContent" style="max-height: 70vh; overflow: auto;">
<!-- 预览内容将由 JS 填充 -->
</div>
</div>
</div>
<!-- 添加同步源对话框 -->
<div class="modal" id="addSyncModal">
<div class="modal-content">
<div class="modal-header">
<h3>+ 添加同步源</h3>
<button class="modal-close" onclick="hideModal('addSyncModal')">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>名称</label>
<input type="text" id="syncName" placeholder="例如: ubuntu-releases">
</div>
<div class="form-group">
<label>同步类型</label>
<select id="syncType" onchange="updateSyncTypeFields()">
<option value="http">HTTP/HTTPS</option>
<option value="ftp">FTP</option>
<option value="sftp">SFTP</option>
<option value="rsync">Rsync</option>
<option value="git">Git</option>
<option value="s3">AWS S3 / MinIO</option>
<option value="oss">阿里云 OSS</option>
<option value="cos">腾讯云 COS</option>
<option value="local">本地目录</option>
<option value="webdav">WebDAV</option>
</select>
</div>
<!-- 通用字段 -->
<div id="syncUrlGroup" class="form-group">
<label id="syncUrlLabel">源 URL</label>
<input type="text" id="syncUrl" placeholder="例如: https://releases.ubuntu.com/">
</div>
<!-- S3/OSS/COS 额外字段 -->
<div id="syncCloudFields" style="display: none;">
<div class="form-group">
<label>Bucket 名称</label>
<input type="text" id="syncBucket" placeholder="例如: my-bucket">
</div>
<div class="form-group">
<label>Access Key</label>
<input type="text" id="syncAccessKey" placeholder="Access Key ID">
</div>
<div class="form-group">
<label>Secret Key</label>
<input type="password" id="syncSecretKey" placeholder="Secret Access Key">
</div>
<div class="form-group">
<label>区域/Endpoint</label>
<input type="text" id="syncRegion" placeholder="例如: cn-hangzhou 或 https://s3.amazonaws.com">
</div>
<div class="form-group">
<label>前缀 (可选)</label>
<input type="text" id="syncPrefix" placeholder="例如: mirrors/">
</div>
</div>
<!-- FTP/SFTP 额外字段 -->
<div id="syncFtpFields" style="display: none;">
<div class="form-group">
<label>主机地址</label>
<input type="text" id="syncHost" placeholder="例如: ftp.example.com">
</div>
<div class="form-group">
<label>端口</label>
<input type="number" id="syncPort" placeholder="21" value="21">
</div>
<div class="form-group">
<label>用户名</label>
<input type="text" id="syncUsername" placeholder="anonymous">
</div>
<div class="form-group">
<label>密码</label>
<input type="password" id="syncPassword" placeholder="密码">
</div>
<div class="form-group">
<label>远程路径</label>
<input type="text" id="syncRemotePath" placeholder="/pub">
</div>
</div>
<!-- 本地目录额外字段 -->
<div id="syncLocalFields" style="display: none;">
<div class="form-group">
<label>本地源路径</label>
<input type="text" id="syncLocalPath" placeholder="例如: /mnt/external/mirrors">
</div>
</div>
<!-- 通用目标路径 -->
<div id="syncTargetGroup" class="form-group">
<label>存储路径</label>
<input type="text" id="syncTarget" placeholder="例如: pypi/simple, npm/packages">
<small style="color: var(--text-muted);">相对于 downloads 目录的存储路径,如填写 pypi/simple 则文件将存放到 downloads/pypi/simple/</small>
</div>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" id="syncAuto" checked>
<span>启用自动同步</span>
</label>
<!-- 定时同步配置 -->
<div style="margin-top: 15px; padding: 15px; background: var(--bg-input); border-radius: 8px;">
<label style="font-weight: 600; margin-bottom: 10px; display: block;">⏰ 定时同步 (Cron 表达式)</label>
<div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">
<input type="checkbox" id="syncScheduleEnabled">
<span>启用定时</span>
<input type="text" id="syncScheduleCron" placeholder="0 2 * * *" style="flex: 1; min-width: 150px;" value="0 2 * * *">
</div>
<div style="font-size: 12px; color: var(--text-muted); margin-top: 8px;">
常用: 每天凌晨 2 点 <code>0 2 * * *</code> | 每周日凌晨 <code>0 2 * * 0</code> | 每月1号 <code>0 2 1 * *</code>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="hideModal('addSyncModal')">取消</button>
<button class="btn btn-primary" id="btnAddSyncSource" onclick="addSyncSource()">添加</button>
</div>
</div>
</div>
<!-- 编辑同步源对话框 -->
<div class="modal" id="editSyncModal">
<div class="modal-content">
<div class="modal-header">
<h3><i class="fa-solid fa-edit"></i> 编辑同步源</h3>
<button class="modal-close" onclick="hideModal('editSyncModal')">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>名称</label>
<input type="text" id="editSyncName" disabled style="background: var(--bg-input); opacity: 0.7; cursor: not-allowed;">
<small style="color: var(--text-muted);">名称不可更改</small>
</div>
<div class="form-group">
<label>同步类型</label>
<select id="editSyncType" onchange="updateEditSyncTypeFields()">
<option value="http">HTTP/HTTPS</option>
<option value="ftp">FTP</option>
<option value="sftp">SFTP</option>
<option value="rsync">Rsync</option>
<option value="git">Git</option>
<option value="s3">AWS S3 / MinIO</option>
<option value="oss">阿里云 OSS</option>
<option value="cos">腾讯云 COS</option>
<option value="local">本地目录</option>
<option value="webdav">WebDAV</option>
</select>
</div>
<!-- 通用字段 -->
<div id="editSyncUrlGroup" class="form-group">
<label id="editSyncUrlLabel">源 URL</label>
<input type="text" id="editSyncUrl" placeholder="例如: https://releases.ubuntu.com/">
</div>
<!-- S3/OSS/COS 额外字段 -->
<div id="editSyncCloudFields" style="display: none;">
<div class="form-group">
<label>Bucket 名称</label>
<input type="text" id="editSyncBucket" placeholder="例如: my-bucket">
</div>
<div class="form-group">
<label>Access Key</label>
<input type="text" id="editSyncAccessKey" placeholder="Access Key ID">
</div>
<div class="form-group">
<label>Secret Key</label>
<input type="password" id="editSyncSecretKey" placeholder="Secret Access Key">
</div>
<div class="form-group">
<label>区域/Endpoint</label>
<input type="text" id="editSyncRegion" placeholder="例如: cn-hangzhou 或 https://s3.amazonaws.com">
</div>
<div class="form-group">
<label>前缀 (可选)</label>
<input type="text" id="editSyncPrefix" placeholder="例如: mirrors/">
</div>
</div>
<!-- FTP/SFTP 额外字段 -->
<div id="editSyncFtpFields" style="display: none;">
<div class="form-group">
<label>主机地址</label>
<input type="text" id="editSyncHost" placeholder="例如: ftp.example.com">
</div>
<div class="form-group">
<label>端口</label>
<input type="number" id="editSyncPort" placeholder="21" value="21">
</div>
<div class="form-group">
<label>用户名</label>
<input type="text" id="editSyncUsername" placeholder="anonymous">
</div>
<div class="form-group">
<label>密码</label>
<input type="password" id="editSyncPassword" placeholder="密码">
</div>
<div class="form-group">
<label>远程路径</label>
<input type="text" id="editSyncRemotePath" placeholder="/pub">
</div>
</div>
<!-- 本地目录额外字段 -->
<div id="editSyncLocalFields" style="display: none;">
<div class="form-group">
<label>本地源路径</label>
<input type="text" id="editSyncLocalPath" placeholder="例如: /mnt/external/mirrors">
</div>
</div>
<!-- 存储路径 -->
<div id="editSyncTargetGroup" class="form-group">
<label>存储路径</label>
<input type="text" id="editSyncTarget" placeholder="例如: pypi/simple, npm/packages">
<small style="color: var(--text-muted);">相对于 downloads 目录的存储路径</small>
</div>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" id="editSyncAuto">
<span>启用自动同步</span>
</label>
<!-- 定时同步配置 -->
<div style="margin-top: 15px; padding: 15px; background: var(--bg-input); border-radius: 8px;">
<label style="font-weight: 600; margin-bottom: 10px; display: block;">⏰ 定时同步 (Cron 表达式)</label>
<div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">
<input type="checkbox" id="editSyncScheduleEnabled">
<span>启用定时</span>
<input type="text" id="editSyncScheduleCron" placeholder="0 2 * * *" style="flex: 1; min-width: 150px;" value="0 2 * * *">
</div>
<div style="font-size: 12px; color: var(--text-muted); margin-top: 8px;">
常用: 每天凌晨 2 点 <code>0 2 * * *</code> | 每周日凌晨 <code>0 2 * * 0</code> | 每月1号 <code>0 2 1 * *</code>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="hideModal('editSyncModal')">取消</button>
<button class="btn btn-primary" id="btnEditSyncSource" onclick="saveEditSyncSource()">保存</button>
</div>
</div>
</div>
<!-- 编辑加速源对话框 -->
<div class="modal" id="editMirrorModal">
<div class="modal-content" style="max-width: 560px;">
<div class="modal-header" style="background: linear-gradient(135deg, var(--primary) 0%, #2563eb 100%); border-radius: 16px 16px 0 0;">
<h3 style="color: white;"><i class="fa-solid fa-sliders"></i> 编辑加速源</h3>
<button class="modal-close" onclick="hideModal('editMirrorModal')" style="color: white; opacity: 0.8;">×</button>
</div>
<div class="modal-body" style="padding: 24px;">
<!-- 加速源名称 -->
<div class="input-group" style="margin-bottom: 20px;">
<label><i class="fa-solid fa-tag"></i> 加速源名称</label>
<input type="text" id="editMirrorName" disabled style="background: var(--bg-input); opacity: 0.7; cursor: not-allowed;">
<small style="color: var(--text-muted); display: flex; align-items: center; gap: 6px; margin-top: 6px;"><i class="fa-solid fa-info-circle"></i> 名称不可更改</small>
</div>
<!-- 协议类型和优先级 -->
<div style="display: grid; grid-template-columns: 1fr 120px; gap: 16px; margin-bottom: 20px;">
<div class="input-group">
<label><i class="fa-solid fa-layer-group"></i> 协议类型</label>
<div class="select-wrapper">
<select id="editMirrorType">
<option value="custom" data-icon="fa-gear">⚙️ 自定义 (Custom)</option>
<option value="http" data-icon="fa-globe">🌐 HTTP 文件服务</option>
<option value="docker" data-icon="fa-docker">🐳 Docker Registry V2</option>
<option value="apt" data-icon="fa-debian">📦 APT (Debian/Ubuntu)</option>
<option value="yum" data-icon="fa-redhat">🎯 YUM (CentOS/RHEL)</option>
<option value="pacman" data-icon="fa-archway">🔵 Pacman (Arch Linux)</option>
<option value="pypi" data-icon="fa-python">🐍 PyPI (Python)</option>
<option value="npm" data-icon="fa-npm">📦 NPM (Node.js)</option>
<option value="go" data-icon="fa-golang">🔷 Go 模块</option>
<option value="maven" data-icon="fa-leaf">🫙 Maven (Java)</option>
<option value="gradle" data-icon="fa-hammer">🔨 Gradle</option>
<option value="gem" data-icon="fa-gem">💎 RubyGems</option>
<option value="cargo" data-icon="fa-ship">🦀 Cargo (Rust)</option>
<option value="nuget" data-icon="fa-book">📚 NuGet (.NET)</option>
<option value="cocoapods" data-icon="fa-apple">🍎 CocoaPods</option>
<option value="cran" data-icon="fa-chart-bar">📊 CRAN (R)</option>
<option value="ctan" data-icon="fa-file-alt">📝 CTAN (LaTeX)</option>
<option value="cuda" data-icon="fa-gamepad">🎮 NVIDIA CUDA</option>
<option value="goproxy" data-icon="fa-golang">🔷 GoProxy (Go 模块)</option>
<option value="git" data-icon="fa-git-alt">🔀 Git 仓库</option>
</select>
<i class="fa-solid fa-chevron-down select-arrow"></i>
</div>
</div>
<div class="input-group">
<label><i class="fa-solid fa-sort-numeric-up"></i> 优先级</label>
<input type="number" id="editMirrorPriority" value="0" min="0" max="100" style="text-align: center;">
</div>
</div>
<!-- 上游地址 -->
<div class="input-group" style="margin-bottom: 20px;">
<label><i class="fa-solid fa-link"></i> 上游地址</label>
<input type="text" id="editMirrorUrl" placeholder="例如: https://mirror.example.com" />
</div>
<!-- 存储目录 -->
<div class="input-group" style="margin-bottom: 20px;">
<label><i class="fa-solid fa-folder"></i> 存储目录</label>
<input type="text" id="editMirrorStorageDir" placeholder="例如: docker-cn, pypi-cn (默认: 镜像名称)" />
</div>
<!-- 描述 -->
<div class="input-group" style="margin-bottom: 20px;">
<label><i class="fa-solid fa-pen-to-square"></i> 描述</label>
<input type="text" id="editMirrorDesc" placeholder="描述这个加速源的用途" />
</div>
<!-- 启用开关 -->
<div class="toggle-card">
<label style="display: flex; align-items: center; gap: 12px; cursor: pointer;">
<label class="switch">
<input type="checkbox" id="editMirrorEnabled" checked>
<span class="switch-slider"></span>
</label>
<span style="flex: 1;"><i class="fa-solid fa-power-off"></i> 启用此加速源</span>
<span class="status-badge enabled" id="editMirrorStatusBadge"><i class="fa-solid fa-check"></i> 已启用</span>
</label>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="hideModal('editMirrorModal')">取消</button>
<button class="btn btn-primary" onclick="updateMirror()">保存</button>
</div>
</div>
</div>
<script>
// 全局变量
let apiToken = localStorage.getItem('hyc_api_key') || '';
let currentPath = '/';
let currentFiles = [];
let refreshTimer = null;
// API 基础地址
const API_BASE = window.location.origin;
// ============ 认证相关 ============
async function login() {
const user = document.getElementById('loginUser').value;
const pass = document.getElementById('loginPass').value;
const key = document.getElementById('loginKey').value.trim();
if (!key && (!user || !pass)) {
showNotification('请输入用户名密码或 API Key', 'error');
return;
}
if (key) {
// 直接使用 API Key
apiToken = key;
localStorage.setItem('hyc_api_key', apiToken);
// 保存用户名
if (user) localStorage.setItem('hyc_username', user);
enterApp();
showNotification('登录成功', 'success');
} else {
// 调用登录 API 获取 token
try {
const response = await fetch(API_BASE + '/api/v2/user/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: user, password: pass })
});
const data = await response.json();
if (data.success && data.token) {
apiToken = data.token;
localStorage.setItem('hyc_api_key', apiToken);
// 保存用户名
if (user) localStorage.setItem('hyc_username', user);
enterApp();
showNotification('登录成功', 'success');
} else {
showNotification(data.error || '登录失败', 'error');
}
} catch (e) {
// 登录 API 失败,回退到 Basic Auth 方式
apiToken = btoa(user + ':' + pass);
localStorage.setItem('hyc_api_key', apiToken);
// 保存用户名
if (user) localStorage.setItem('hyc_username', user);
enterApp();
showNotification('登录成功', 'success');
}
}
}
function enterApp() {
document.getElementById('loginPage').style.display = 'none';
document.getElementById('appPage').style.display = 'flex';
document.getElementById('appPage').classList.add('active');
// 自动填充用户名到修改密码输入框
const savedUsername = localStorage.getItem('hyc_username') || '';
if (savedUsername) {
const usernameInput = document.getElementById('changePasswordUsername');
if (usernameInput) usernameInput.value = savedUsername;
}
initMobileMenuButtons();
loadDashboard();
}
function logout() {
localStorage.removeItem('hyc_api_key');
apiToken = '';
location.reload();
}
// ============ 初始化 ============
async function initApp() {
// 有保存的token,直接进入(只对非空token生效)
if (apiToken && apiToken.trim() !== '') {
enterApp();
return;
}
// 清除可能残留的空token
localStorage.removeItem('hyc_api_key');
apiToken = '';
// 无token,保持登录页,不调用需要认证的API
}
// ============ 页面导航 ============
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', function() {
const page = this.dataset.page;
if (page) showPage(page);
});
});
function showPage(pageId) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
document.getElementById('page-' + pageId).classList.add('active');
document.querySelector('.nav-item[data-page="' + pageId + '"]').classList.add('active');
// 确保移动端菜单按钮存在
initMobileMenuButtons();
// 加载页面数据
const loaders = {
dashboard: loadDashboard,
files: loadFiles,
sync: loadSync,
mirrors: loadMirrors,
downloads: loadDownloads,
config: loadConfig,
settings: loadSettings
};
if (loaders[pageId]) loaders[pageId]();
}
function switchSyncTab(tab) {
document.querySelectorAll('#page-sync .tab').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
document.getElementById('sync-sources-tab').style.display = tab === 'sources' ? 'block' : 'none';
document.getElementById('sync-history-tab').style.display = tab === 'history' ? 'block' : 'none';
// 切换到历史标签时加载历史记录
if (tab === 'history') {
loadSyncHistory();
}
}
// 切换数据库类型时显示/隐藏对应的配置输入框
function switchDbType() {
const dbType = document.getElementById('dbType').value;
document.getElementById('db-sqlite-config').style.display = dbType === 'sqlite' ? 'block' : 'none';
document.getElementById('db-postgresql-config').style.display = dbType === 'postgresql' ? 'block' : 'none';
document.getElementById('db-mysql-config').style.display = dbType === 'mysql' ? 'block' : 'none';
}
function switchSettingsTab(tab) {
document.querySelectorAll('#page-settings .tab').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
document.getElementById('settings-security').style.display = tab === 'security' ? 'block' : 'none';
document.getElementById('settings-storage').style.display = tab === 'storage' ? 'block' : 'none';
document.getElementById('settings-sync').style.display = tab === 'sync' ? 'block' : 'none';
document.getElementById('settings-features').style.display = tab === 'features' ? 'block' : 'none';
document.getElementById('settings-performance').style.display = tab === 'performance' ? 'block' : 'none';
document.getElementById('settings-log').style.display = tab === 'log' ? 'block' : 'none';
document.getElementById('settings-alerts').style.display = tab === 'alerts' ? 'block' : 'none';
}
// ============ API 请求 ============
async function apiRequest(url, options = {}) {
const headers = { 'Accept': 'application/json' };
if (apiToken) {
if (apiToken.includes(':')) {
headers['Authorization'] = 'Basic ' + apiToken;
} else if (apiToken.startsWith('eyJ')) {
headers['Authorization'] = 'Bearer ' + apiToken;
} else {
headers['X-API-Key'] = apiToken;
}
}
try {
const r = await fetch(API_BASE + url, { ...options, headers: { ...headers, ...options.headers } });
return await r.json().catch(() => ({}));
} catch (e) {
console.error('API Error:', url, e);
return { error: e.message };
}
}
// ============ 通知 ============
function showNotification(msg, type = 'success') {
const div = document.createElement('div');
div.className = 'notification notification-' + type;
div.innerHTML = '<span>' + (type === 'success' ? '✓' : type === 'error' ? '✕' : 'ℹ') + '</span> ' + msg;
document.body.appendChild(div);
setTimeout(() => div.remove(), 3000);
}
// 弹窗打开/关闭管理
function showModal(id) {
const modal = document.getElementById(id);
if (!modal) return;
// 打开弹窗时清空表单输入框
modal.querySelectorAll('input[type="text"], input[type="password"], input[type="number"], input[type="email"], textarea').forEach(input => {
// 保留有 disabled 属性的输入框(显示只读信息的)
if (!input.disabled) {
input.value = '';
}
});
modal.querySelectorAll('input[type="checkbox"], input[type="radio"]').forEach(input => {
input.checked = input.getAttribute('data-default-checked') === 'true' || input.hasAttribute('checked');
});
// 防止背景滚动
document.body.style.overflow = 'hidden';
modal.classList.add('active');
}
function hideModal(id) {
document.getElementById(id).classList.remove('active');
// 恢复背景滚动
document.body.style.overflow = '';
}
// ============ 移动端侧边栏 ============
// 初始化页面标题栏菜单按钮
function initMobileMenuButtons() {
const pageHeaders = document.querySelectorAll('.page-header');
pageHeaders.forEach(header => {
if (!header.querySelector('.mobile-menu-btn')) {
const btn = document.createElement('button');
btn.className = 'mobile-menu-btn';
btn.innerHTML = '☰';
btn.onclick = toggleSidebar;
btn.title = '切换菜单';
header.insertBefore(btn, header.firstChild);
}
});
}
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('sidebarOverlay');
if (sidebar && overlay) {
const isActive = sidebar.classList.contains('active');
if (isActive) {
sidebar.classList.remove('active');
overlay.classList.remove('active');
// 更新所有菜单按钮图标
document.querySelectorAll('.mobile-menu-btn').forEach(btn => btn.innerHTML = '☰');
} else {
sidebar.classList.add('active');
overlay.classList.add('active');
document.querySelectorAll('.mobile-menu-btn').forEach(btn => btn.innerHTML = '✕');
}
}
}
// 点击导航项后关闭侧边栏(移动端)
document.addEventListener('DOMContentLoaded', function() {
const navItems = document.querySelectorAll('.nav-item[data-page]');
navItems.forEach(item => {
item.addEventListener('click', function() {
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('sidebarOverlay');
// 检查是否为移动端
if (window.innerWidth <= 1024 && sidebar && sidebar.classList.contains('active')) {
sidebar.classList.remove('active');
overlay.classList.remove('active');
document.querySelectorAll('.mobile-menu-btn').forEach(btn => btn.innerHTML = '☰');
}
});
});
});
// ============ 仪表盘 ============
let dashboardTimer = null;
let activityOffset = 0; // 已加载的活动记录偏移量
let activityHasMore = true; // 是否还有更多数据
let activityLoading = false; // 是否正在加载
let activityDataCache = []; // 缓存已加载的活动数据
// 加载更多活动记录
window.loadMoreActivity = async function() {
if (activityLoading || !activityHasMore) return;
activityLoading = true;
document.getElementById('activityLoading').style.display = 'block';
document.getElementById('activityLoadMore').style.display = 'none';
try {
const newActivity = await apiRequest(`/api/v2/activity?limit=20&offset=${activityOffset}`).catch(e => ({ error: e.message }));
if (newActivity && !newActivity.error) {
const newItems = newActivity.activities || [];
const total = newActivity.total || 0;
if (newItems.length === 0) {
activityHasMore = false;
} else {
// 追加新数据
activityDataCache = [...activityDataCache, ...newItems];
activityOffset += newItems.length;
// 根据总数判断是否还有更多
activityHasMore = activityOffset < total;
}
renderActivityTable(newItems, false);
}
} catch (e) {
console.error('加载更多活动失败:', e);
}
activityLoading = false;
// 更新按钮状态
document.getElementById('activityLoading').style.display = 'none';
if (activityHasMore) {
document.getElementById('activityLoadMore').style.display = 'block';
} else {
document.getElementById('activityEnd').style.display = 'block';
}
};
async function loadDashboard() {
// 取消之前的定时器
if (dashboardTimer) {
clearInterval(dashboardTimer);
dashboardTimer = null;
}
const updateData = async () => {
try {
// 分别请求,每个请求独立处理错误
const stats = await apiRequest('/api/v1/stats').catch(e => ({ error: e.message }));
const monitor = await apiRequest('/api/v2/monitor/realtime').catch(e => ({ error: e.message }));
const health = await apiRequest('/api/v1/health').catch(e => ({ error: e.message }));
const mirrors = await apiRequest('/api/v2/mirrors').catch(e => ({ error: e.message }));
// 首次加载活动时重置偏移量和缓存
if (activityOffset === 0) {
const activity = await apiRequest('/api/v2/activity?limit=20').catch(e => ({ error: e.message }));
if (activity && !activity.error) {
activityDataCache = activity.activities || [];
activityOffset = activityDataCache.length;
activityHasMore = activity.count > activityDataCache.length;
renderActivityTable(activityDataCache, true);
}
}
const downloadTrend = await apiRequest('/api/v2/stats/download-trend?days=7').catch(e => ({ error: e.message }));
// 更新统计卡片
if (stats && !stats.error) {
const el = document.getElementById('statFiles');
if (el) el.textContent = formatNum(stats.total_files || 0);
const el2 = document.getElementById('statStorage');
if (el2) el2.textContent = stats.total_size_formatted || formatSize(stats.total_size || 0);
const el3 = document.getElementById('statDownloads');
if (el3) el3.textContent = formatNum(stats.downloads_today || 0);
}
// 更新监控数据 - 逐个更新不怕失败
try {
const cpuPercent = (monitor?.cpu?.percent || 0);
const memPercent = (monitor?.memory?.percent || 0);
const diskPercent = (monitor?.disk?.percent || 0);
const cpuValEl = document.getElementById('cpuValue');
const cpuBarEl = document.getElementById('cpuBar');
const memValEl = document.getElementById('memValue');
const memBarEl = document.getElementById('memBar');
const diskValEl = document.getElementById('diskValue');
const diskBarEl = document.getElementById('diskBar');
if (cpuValEl) cpuValEl.textContent = cpuPercent.toFixed(1) + '%';
if (cpuBarEl) cpuBarEl.style.width = Math.min(cpuPercent, 100) + '%';
if (memValEl) memValEl.textContent = memPercent.toFixed(1) + '%';
if (memBarEl) memBarEl.style.width = Math.min(memPercent, 100) + '%';
if (diskValEl) diskValEl.textContent = diskPercent.toFixed(1) + '%';
if (diskBarEl) diskBarEl.style.width = Math.min(diskPercent, 100) + '%';
} catch (e) {
console.error('Monitor update error:', e);
}
// 更新服务器信息
if (health?.server) {
const uptimeText = formatUptime(health.server.uptime);
// 更新统计卡片的运行时间
const cardEl = document.getElementById('serverUptime');
if (cardEl) cardEl.textContent = uptimeText;
// 更新详情页的运行时间
const detailEl = document.getElementById('serverUptimeDetail');
if (detailEl) detailEl.textContent = uptimeText;
}
// 更新加速源数量
if (mirrors && !mirrors.error) {
const mirrorCount = mirrors.count || (mirrors.mirrors ? Object.keys(mirrors.mirrors).length : 0);
const el = document.getElementById('mirrorCount');
if (el) el.textContent = mirrorCount;
// 更新同步状态
const syncStatusEl = document.getElementById('syncStatus');
if (syncStatusEl && mirrors.mirrors) {
const syncing = Object.values(mirrors.mirrors).some(m => m?.syncing) || false;
syncStatusEl.textContent = syncing ? '同步中...' : '正常';
syncStatusEl.style.color = syncing ? 'var(--warning)' : 'var(--success)';
}
}
// 更新首页下载趋势图表(使用Chart.js)
if (downloadTrend?.trend) {
renderDashboardChart(downloadTrend.trend);
}
} catch (e) {
console.error('Failed to load dashboard:', e);
}
};
// 立即更新一次
await updateData();
// 每3秒自动刷新监控数据
dashboardTimer = setInterval(updateData, 3000);
}
// ============ 下载趋势图表(折线图)============
// 首页仪表板下载趋势图表(Chart.js)
let dashboardChart = null;
let dashboardChartInitialized = false;
function renderDashboardChart(trend) {
const container = document.getElementById('downloadChart');
if (!container) return;
try {
if (!trend || trend.length === 0) {
if (!dashboardChartInitialized) {
container.innerHTML = '<div class="empty" style="width: 100%; text-align: center; padding: 40px;">暂无下载数据</div>';
}
return;
}
const labels = trend.map(d => {
let label = d.date || '';
if (label.includes('-')) {
const parts = label.split('-');
if (parts.length >= 3) label = `${parts[1]}-${parts[2]}`;
}
return label;
});
const values = trend.map(d => d.downloads || 0);
// 首次创建 canvas
if (!dashboardChartInitialized) {
container.innerHTML = '';
const canvas = document.createElement('canvas');
container.appendChild(canvas);
dashboardChartInitialized = true;
}
const canvas = container.querySelector('canvas');
const ctx = canvas.getContext('2d');
const primaryColor = '#e94560';
// 如果已有图表实例,更新数据而不是重建
if (dashboardChart) {
dashboardChart.data.labels = labels;
dashboardChart.data.datasets[0].data = values;
dashboardChart.update('none');
return;
}
// 首次创建图表
dashboardChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: '下载量',
data: values,
backgroundColor: 'rgba(233, 69, 96, 0.1)',
borderColor: primaryColor,
borderWidth: 2,
fill: true,
tension: 0.4,
pointBackgroundColor: primaryColor,
pointBorderColor: '#fff',
pointBorderWidth: 2,
pointRadius: 3
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: 'rgba(26, 26, 46, 0.95)',
titleColor: '#fff',
bodyColor: '#aaa',
borderColor: primaryColor,
borderWidth: 1
}
},
scales: {
x: { grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#888' } },
y: { beginAtZero: true, grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#888', precision: 0 } }
}
}
});
} catch (e) {
console.error('Failed to render dashboard chart:', e);
container.innerHTML = '<div class="empty" style="width: 100%; text-align: center; padding: 40px;">加载失败</div>';
}
}
function renderDownloadTrend(trendData) {
const container = document.getElementById('downloadChart');
if (!container) return;
try {
const trend = trendData?.trend || [];
if (trend.length === 0) {
container.innerHTML = '<div class="empty" style="width: 100%; text-align: center; padding: 40px;">暂无下载数据</div>';
return;
}
// 计算数据范围
const maxDownloads = Math.max(...trend.map(d => d.downloads), 1);
const minDownloads = Math.min(...trend.map(d => d.downloads), 0);
const range = maxDownloads - minDownloads || 1;
// 图表尺寸
const padding = { top: 20, right: 15, bottom: 30, left: 10 };
const width = container.clientWidth || 400;
const height = 180;
const chartWidth = width - padding.left - padding.right;
const chartHeight = height - padding.top - padding.bottom;
// 计算坐标点
const points = trend.map((item, index) => {
const x = padding.left + 20 + (index / (trend.length - 1 || 1)) * (chartWidth - 20);
const y = padding.top + chartHeight - ((item.downloads - minDownloads) / range) * chartHeight;
return { x, y, value: item.downloads, date: item.date, index };
});
// 生成 SVG 折线图
const linePath = points.length > 1
? points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`).join(' ')
: `M ${points[0].x + 15} ${points[0].y}`;
// 生成填充区域路径
const fillPath = points.length > 1
? `${linePath} L ${points[points.length - 1].x} ${padding.top + chartHeight} L ${points[0].x} ${padding.top + chartHeight} Z`
: '';
// 生成数据点
const dots = points.map(p => `
<circle cx="${p.x}" cy="${p.y}" r="4" fill="var(--primary)" stroke="#fff" stroke-width="2">
<title>${p.date}: ${p.value} 次下载</title>
</circle>
`).join('');
// 生成 X 轴标签
const labels = trend.map((item, index) => {
const x = padding.left + 20 + (index / (trend.length - 1 || 1)) * (chartWidth - 20);
// 解析日期格式 (YYYY-MM-DD -> MM-DD)
let label = item.date;
if (item.date && item.date.includes('-')) {
const parts = item.date.split('-');
if (parts.length >= 3) {
label = `${parts[1]}-${parts[2]}`;
} else if (parts.length === 2) {
label = parts[1];
}
}
return `<text x="${x}" y="${height - 8}" text-anchor="middle" font-size="11" fill="var(--text-muted)">${label}</text>`;
}).join('');
// 生成 Y 轴网格线和数值
const yTicks = 4;
const gridLines = [];
const yLabels = [];
for (let i = 0; i <= yTicks; i++) {
const value = maxDownloads - (i / yTicks) * range;
const y = padding.top + (i / yTicks) * chartHeight;
gridLines.push(`<line x1="${padding.left}" y1="${y}" x2="${width - padding.right}" y2="${y}" stroke="var(--border-color)" stroke-dasharray="3,3"/>`);
yLabels.push(`<text x="${padding.left - 5}" y="${y + 4}" text-anchor="end" font-size="10" fill="var(--text-muted)">${Math.round(value)}</text>`);
}
container.innerHTML = `
<svg width="100%" height="${height}" viewBox="0 0 ${width} ${height}" style="display: block;">
<!-- 网格线和 Y 轴标签 -->
${gridLines.join('')}
${yLabels.join('')}
<!-- 填充区域 -->
${fillPath ? `<path d="${fillPath}" fill="url(#gradient)" opacity="0.3"/>` : ''}
<!-- 折线 -->
<path d="${linePath}" fill="none" stroke="var(--primary)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<!-- 数据点 -->
${dots}
<!-- X 轴标签 -->
${labels}
</svg>
<svg width="100%" height="0" style="position: absolute;">
<defs>
<linearGradient id="gradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="var(--primary)" stop-opacity="0.6"/>
<stop offset="100%" stop-color="var(--primary)" stop-opacity="0.05"/>
</linearGradient>
</defs>
</svg>
`;
// 添加 hover 效果
container.querySelectorAll('circle').forEach((circle, i) => {
circle.style.transition = 'r 0.2s ease';
circle.style.cursor = 'pointer';
circle.addEventListener('mouseenter', () => {
circle.setAttribute('r', '6');
});
circle.addEventListener('mouseleave', () => {
circle.setAttribute('r', '4');
});
});
} catch (e) {
console.error('Failed to render download trend:', e);
container.innerHTML = '<div class="empty" style="width: 100%; text-align: center; padding: 40px;">加载失败</div>';
}
}
// 渲染最近活动表格
function renderActivityTable(activities, isFirstLoad) {
const tbody = document.getElementById('activityLog');
if (!tbody) return;
try {
if (activities.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" class="empty">暂无活动记录</td></tr>';
document.getElementById('activityLoadMore').style.display = 'none';
document.getElementById('activityEnd').style.display = 'none';
return;
}
const html = activities.map(a => {
const statusClass = a.status_type === 'success' ? 'var(--success)' :
a.status_type === 'error' ? 'var(--danger)' :
a.status_type === 'warning' ? 'var(--warning)' : 'var(--info)';
const typeIcon = a.type === '同步' ? 'fa-sync' :
a.type === '下载' ? 'fa-download' :
a.type === '告警' ? 'fa-exclamation-triangle' : 'fa-info';
return `<tr>
<td>${a.time || '-'}</td>
<td><i class="fa-solid ${typeIcon}"></i> ${a.type}</td>
<td>${a.content || '-'}</td>
<td><span style="color: ${statusClass}; font-weight: 500;">${a.status || '-'}</span></td>
</tr>`;
}).join('');
if (isFirstLoad) {
tbody.innerHTML = html;
} else {
tbody.insertAdjacentHTML('beforeend', html);
}
// 更新按钮状态
if (activityHasMore) {
document.getElementById('activityLoadMore').style.display = 'block';
document.getElementById('activityEnd').style.display = 'none';
} else {
document.getElementById('activityLoadMore').style.display = 'none';
document.getElementById('activityEnd').style.display = 'block';
}
} catch (e) {
console.error('Failed to render activity:', e);
tbody.innerHTML = '<tr><td colspan="4" class="empty">加载失败</td></tr>';
}
}
// ============ 文件管理 ============
async function loadFiles() {
const list = document.getElementById('fileList');
list.innerHTML = '<div class="empty"><div class="icon"><i class="fa-solid fa-folder-open fa-spin"></i></div><p>加载中...</p></div>';
try {
const url = '/api/v1/files?path=' + encodeURIComponent(currentPath);
const data = await apiRequest(url);
// 检查是否有数据 (即使有error,如果有files也要显示)
if (!data || (!data.files && data.error)) {
list.innerHTML = '<div class="empty"><div class="icon"><i class="fa-solid fa-triangle-exclamation"></i></div><p>' + (data?.error || '服务器错误') + '</p><p style="font-size: 12px; color: var(--text-muted);">当前路径: ' + (currentPath || '/') + '</p></div>';
return;
}
// 如果有files,即使有error也要显示
if (!data.files) {
list.innerHTML = '<div class="empty"><div class="icon"><i class="fa-regular fa-folder-open"></i></div><p>目录为空</p></div>';
currentFiles = [];
return;
}
const files = data.files;
if (files.length === 0) {
list.innerHTML = '<div class="empty"><div class="icon"><i class="fa-regular fa-folder-open"></i></div><p>目录为空</p><p style="font-size: 12px; color: var(--text-muted);">当前路径: ' + (currentPath || '/') + '</p></div>';
currentFiles = [];
return;
}
// Windows 文件管理器风格排序:文件夹在前,文件在后,按名称递增排序
files.sort((a, b) => {
// 文件夹优先
if (a.is_dir && !b.is_dir) return -1;
if (!a.is_dir && b.is_dir) return 1;
// 名称递增排序(不区分大小写)
return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
});
currentFiles = files;
list.innerHTML = currentFiles.map(f => {
const pathStr = (f.path || '').replace(/'/g, "\\'");
const fileName = f.name.replace(/'/g, "\\'");
const isDir = f.is_dir;
// 单击目录进入,单击文件预览
const clickAction = isDir
? 'navigateTo(\'' + pathStr + '\')'
: 'showFilePreview(\'' + pathStr + '\')';
// 格式化修改时间
const modifiedTime = f.modified ? f.modified.split('T')[0] + ' ' + f.modified.split('T')[1]?.substring(0, 8) : '';
// 获取文件类型图标(仅图标)
const fileIcon = getFileIcon(f.name);
return '<div class="file-item" onclick="' + clickAction + '; selectFile(this, \'' + fileName + '\')">' +
'<div class="file-icon">' + (isDir ? '<i class="fa-solid fa-folder-open"></i>' : fileIcon) + '</div>' +
'<div class="file-info">' +
'<div class="file-name">' + f.name + (isDir ? '/' : '') + '</div>' +
'<div class="file-meta">' +
(isDir
? '<span class="meta-item"><i class="fa-solid fa-folder"></i> 文件夹</span>'
: '<span class="meta-item"><i class="fa-regular fa-file-lines"></i> ' + (f.size_formatted || '0 B') + '</span>') +
(modifiedTime ? '<span class="meta-item"><i class="fa-regular fa-clock"></i> ' + modifiedTime + '</span>' : '') +
'</div></div>' +
'<div class="file-actions">' +
(!isDir ? '<button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); downloadFile(\'' + pathStr + '\')" title="下载"><i class="fa-solid fa-download"></i></button>' : '') +
'<button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); showRenameModal(\'' + pathStr + '\')" title="重命名"><i class="fa-solid fa-pen-to-square"></i></button>' +
'<button class="btn btn-danger btn-sm" onclick="event.stopPropagation(); deleteFile(\'' + pathStr + '\')" title="删除"><i class="fa-solid fa-trash"></i></button>' +
'</div></div>';
}).join('');
} catch (e) {
console.error('Failed to load files:', e);
list.innerHTML = '<div class="empty"><div class="icon"><i class="fa-solid fa-circle-exclamation"></i></div><p>加载失败</p><p style="font-size: 12px; color: var(--text-muted);">' + e.message + '</p></div>';
}
}
function navigateTo(path) {
// 移除路径末尾的 /
if (path.endsWith('/') && path !== '/') {
path = path.slice(0, -1);
}
currentPath = path;
updateBreadcrumb();
loadFiles();
}
function goUp() {
if (!currentPath || currentPath === '/') {
return;
}
const parts = currentPath.split('/');
parts.pop();
navigateTo(parts.join('/') || '/');
}
function updateBreadcrumb() {
const parts = currentPath.split('/').filter(p => p);
let html = '<a onclick="navigateTo(\'/\')"><i class="fa-solid fa-house"></i> 根目录</a>';
let acc = '';
parts.forEach((p, index) => {
acc += '/' + p;
const navPath = acc.replace(/'/g, "\\'");
if (index === parts.length - 1) {
// 当前目录
html += '<span class="separator">/</span><span class="current">' + p + '</span>';
} else {
html += '<span class="separator">/</span><a onclick="navigateTo(\'' + navPath + '\')">' + p + '</a>';
}
});
document.getElementById('fileBreadcrumb').innerHTML = html;
// 显示/隐藏返回上级按钮
const goUpBtn = document.getElementById('goUpBtn');
if (goUpBtn) {
goUpBtn.style.display = currentPath && currentPath !== '/' ? 'inline-flex' : 'none';
}
}
function selectFile(el, name) {
document.querySelectorAll('.file-item').forEach(f => f.classList.remove('selected'));
el.classList.add('selected');
}
async function searchFiles() {
const q = document.getElementById('fileSearch').value;
if (!q) {
loadFiles();
return;
}
const data = await apiRequest('/api/v1/search?q=' + encodeURIComponent(q));
const list = document.getElementById('fileList');
if (data?.results) {
list.innerHTML = data.results.map(f => {
const navPath = f.path.substring(0, f.path.lastIndexOf('/'));
const safePath = navPath.replace(/'/g, "\\'");
const safeName = f.name.replace(/'/g, "\\'");
const fileIcon = f.is_dir ? '<i class="fa-solid fa-folder-open"></i>' : getFileIcon(f.name);
return '<div class="file-item" onclick="navigateTo(\'' + safePath + '\')">' +
'<div class="file-icon">' + fileIcon + '</div>' +
'<div class="file-info">' +
'<div class="file-name">' + f.name + '</div>' +
'<div class="file-meta">' +
'<span><i class="fa-solid fa-link"></i> ' + f.path + '</span>' +
'<span><i class="fa-solid fa-weight-hanging"></i> ' + (f.size_formatted || '') + '</span>' +
'</div></div></div>';
}).join('');
}
}
function refreshFiles() { loadFiles(); }
async function uploadFiles() {
const input = document.getElementById('uploadFile');
const path = document.getElementById('uploadPath').value || currentPath;
const progress = document.getElementById('uploadProgress');
const bar = document.getElementById('uploadBar');
const status = document.getElementById('uploadStatus');
if (!input.files.length) {
showNotification('请选择文件', 'warning');
return;
}
progress.style.display = 'block';
let completed = 0;
for (const file of input.files) {
const formData = new FormData();
formData.append('file', file);
formData.append('path', path);
await apiRequest('/api/v1/upload', { method: 'POST', body: formData });
completed++;
bar.style.width = (completed / input.files.length * 100) + '%';
status.textContent = `上传中... ${completed}/${input.files.length}`;
}
status.textContent = '上传完成!';
setTimeout(() => {
hideModal('uploadModal');
progress.style.display = 'none';
input.value = '';
loadFiles();
showNotification('上传完成', 'success');
}, 1000);
}
async function createDirectory() {
const path = document.getElementById('mkdirPath').value;
if (!path) {
showNotification('请输入目录路径', 'warning');
return;
}
await apiRequest('/api/v1/mkdir', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path })
});
hideModal('mkdirModal');
loadFiles();
showNotification('目录创建成功', 'success');
}
async function deleteFile(path) {
if (!confirm('确定要删除 ' + path + ' 吗?')) return;
await apiRequest('/api/v1/file/' + encodeURIComponent(path), { method: 'DELETE' });
loadFiles();
showNotification('文件已删除', 'success');
}
function downloadFile(path) {
window.open(API_BASE + '/file/' + path + '?key=' + apiToken, '_blank');
}
function previewFile(path) {
window.open(API_BASE + '/api/v1/file/' + encodeURIComponent(path) + '/preview?key=' + apiToken, '_blank');
}
// ============ 文件重命名 ============
let renameFilePath = '';
function showRenameModal(path) {
renameFilePath = path;
const fileName = path.split('/').pop();
showModal('renameModal');
document.getElementById('renameFileName').value = fileName;
}
async function renameFile() {
const newName = document.getElementById('renameFileName').value.trim();
if (!newName) {
showNotification('请输入新文件名', 'warning');
return;
}
if (newName.includes('/') || newName.includes('\\')) {
showNotification('文件名不能包含路径分隔符', 'warning');
return;
}
// 获取目录路径和新完整路径
const dirPath = renameFilePath.substring(0, renameFilePath.lastIndexOf('/'));
const newPath = dirPath + '/' + newName;
await apiRequest('/api/v1/file/rename', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
old_path: renameFilePath,
new_path: newPath
})
});
hideModal('renameModal');
loadFiles();
showNotification('文件已重命名', 'success');
}
// ============ 文件预览 ============
async function showFilePreview(path) {
const fileName = path.split('/').pop();
document.getElementById('previewFileName').innerHTML = '<i class="fa-regular fa-file"></i> ' + fileName;
document.getElementById('previewContent').innerHTML = '<div style="text-align: center; padding: 40px; color: var(--text-muted);">加载中...</div>';
try {
const response = await fetch(API_BASE + '/api/v1/file/' + encodeURIComponent(path) + '/preview?key=' + apiToken);
const contentType = response.headers.get('content-type') || '';
// 检查是否是 JSON 响应(后端返回的预览格式)
if (contentType.includes('application/json')) {
const jsonData = await response.json();
// 检查是否是错误响应
if (jsonData.error) {
document.getElementById('previewContent').innerHTML = '<div style="text-align: center; padding: 40px; color: var(--danger);">' + jsonData.error + '</div>';
showModal('previewModal');
return;
}
// 图片预览(使用 data_url)
if (jsonData.preview_available && jsonData.data_url) {
let imagePreview = '<img src="' + jsonData.data_url + '" style="max-width: 100%; max-height: 55vh; display: block; margin: 0 auto; border-radius: 8px;" />';
// 如果是 SVG,额外提供查看源代码的功能
const isSvg = jsonData.svg_content || jsonData.name?.endsWith('.svg') || jsonData.name?.endsWith('.SVG');
if (isSvg && jsonData.svg_content) {
const svgText = typeof jsonData.svg_content === 'string' ? jsonData.svg_content : JSON.stringify(jsonData.svg_content, null, 2);
const lines = svgText.split('\n');
const lineNumbers = lines.map((_, i) => i + 1).join('\n');
imagePreview = '<div id="svgPreviewMode">' +
'<div style="display: flex; justify-content: center; gap: 12px; margin-bottom: 16px;">' +
'<button onclick="switchSvgMode(\'image\')" class="btn btn-primary" style="padding: 8px 20px;"><i class="fa-regular fa-image"></i> 图片预览</button>' +
'<button onclick="switchSvgMode(\'source\')" class="btn btn-secondary" style="padding: 8px 20px;"><i class="fa-solid fa-code"></i> 查看源码</button>' +
'</div>' +
'<div id="svgImageContainer" style="display: block;">' + imagePreview + '</div>' +
'<div id="svgSourceContainer" style="display: none;">' +
'<div style="display: flex; background: var(--bg-input); border-radius: 8px; overflow: auto;">' +
'<div style="padding: 16px 8px; color: var(--text-muted); font-family: monospace; font-size: 13px; text-align: right; user-select: none; border-right: 1px solid var(--border); white-space: pre;">' +
lineNumbers + '</div>' +
'<pre style="flex: 1; margin: 0; padding: 16px; overflow: auto; white-space: pre-wrap; word-wrap: break-word; font-family: monospace; font-size: 13px;">' +
escapeHtml(svgText) + '</pre></div>' +
'</div></div>';
}
document.getElementById('previewContent').innerHTML = imagePreview;
}
// 检查是否有文本内容预览
else if (jsonData.preview_available && jsonData.content !== undefined) {
if (jsonData.preview_available === false && jsonData.error) {
// 错误响应
document.getElementById('previewContent').innerHTML = '<div style="text-align: center; padding: 40px; color: var(--danger);"><i class="fa-solid fa-circle-exclamation"></i> ' + jsonData.error + '</div>';
} else if (jsonData.preview_type === 'text' || jsonData.type?.startsWith('text/')) {
// 文本内容预览(带行号)
let text = typeof jsonData.content === 'string' ? jsonData.content : JSON.stringify(jsonData.content, null, 2);
if (jsonData.truncated) {
text += '\n\n... (内容过长,已截断)';
}
const lines = text.split('\n');
const lineNumbers = lines.map((_, i) => i + 1).join('\n');
document.getElementById('previewContent').innerHTML =
'<div style="display: flex; background: var(--bg-input); border-radius: 8px; overflow: auto;">' +
'<div style="padding: 16px 8px; color: var(--text-muted); font-family: monospace; font-size: 13px; text-align: right; user-select: none; border-right: 1px solid var(--border); white-space: pre;">' +
lineNumbers + '</div>' +
'<pre style="flex: 1; margin: 0; padding: 16px; overflow: auto; white-space: pre-wrap; word-wrap: break-word; font-family: monospace; font-size: 13px;">' +
escapeHtml(text) + '</pre></div>';
} else if (jsonData.preview_type === 'json') {
// JSON 文件预览(带行号+语法高亮)
let text = typeof jsonData.content === 'string' ? jsonData.content : JSON.stringify(jsonData.content, null, 2);
if (jsonData.truncated) {
text += '\n\n... (内容过长,已截断)';
}
const lines = text.split('\n');
const lineNumbers = lines.map((_, i) => i + 1).join('\n');
document.getElementById('previewContent').innerHTML =
'<div style="display: flex; background: var(--bg-input); border-radius: 8px; overflow: auto;">' +
'<div style="padding: 16px 8px; color: var(--text-muted); font-family: monospace; font-size: 13px; text-align: right; user-select: none; border-right: 1px solid var(--border); white-space: pre;">' +
lineNumbers + '</div>' +
'<pre style="flex: 1; margin: 0; padding: 16px; overflow: auto; white-space: pre-wrap; word-wrap: break-word; font-family: monospace; font-size: 13px;">' +
escapeHtml(text) + '</pre></div>';
} else if (jsonData.preview_type === 'markdown') {
// Markdown 预览
let text = jsonData.content || '';
if (jsonData.truncated) {
text += '\n\n... (内容过长,已截断)';
}
document.getElementById('previewContent').innerHTML =
'<div style="padding: 20px; background: var(--bg-card); border-radius: 8px;">' +
'<div class="markdown-body">' + escapeHtml(text).replace(/\n/g, '<br>').replace(/`([^`]+)`/g, '<code style="background: var(--bg-input); padding: 2px 6px; border-radius: 4px; font-family: monospace;">$1</code>').replace(/\*\*([^\*]+)\*\*/g, '<strong>$1</strong>').replace(/\*([^\*]+)\*/g, '<em>$1</em>') + '</div></div>';
} else if (jsonData.preview_type === 'xml' || jsonData.type?.includes('xml') || jsonData.name?.endsWith('.xml') || jsonData.name?.endsWith('.svg')) {
// XML/SVG 预览(格式化+语法高亮)
let text = typeof jsonData.content === 'string' ? jsonData.content : JSON.stringify(jsonData.content, null, 2);
if (jsonData.truncated) {
text += '\n\n... (内容过长,已截断)';
}
const lines = text.split('\n');
const lineNumbers = lines.map((_, i) => i + 1).join('\n');
document.getElementById('previewContent').innerHTML =
'<div style="display: flex; background: var(--bg-input); border-radius: 8px; overflow: auto;">' +
'<div style="padding: 16px 8px; color: var(--text-muted); font-family: monospace; font-size: 13px; text-align: right; user-select: none; border-right: 1px solid var(--border); white-space: pre;">' +
lineNumbers + '</div>' +
'<pre style="flex: 1; margin: 0; padding: 16px; overflow: auto; white-space: pre-wrap; word-wrap: break-word; font-family: monospace; font-size: 13px;">' +
escapeHtml(text) + '</pre></div>';
} else if (jsonData.preview_type === 'csv') {
// CSV 预览(表格形式)
let text = jsonData.content || '';
const rows = text.split('\n').filter(r => r.trim());
let tableHtml = '<table style="width: 100%; border-collapse: collapse; font-size: 13px;">';
rows.forEach((row, idx) => {
const cells = row.split(',');
const cellTag = idx === 0 ? 'th' : 'td';
tableHtml += '<tr>';
cells.forEach(cell => {
tableHtml += '<' + cellTag + ' style="border: 1px solid var(--border); padding: 8px 12px; text-align: left;">' + escapeHtml(cell.trim()) + '</' + cellTag + '>';
});
tableHtml += '</tr>';
});
tableHtml += '</table>';
document.getElementById('previewContent').innerHTML =
'<div style="padding: 16px; background: var(--bg-input); border-radius: 8px; overflow: auto;">' + tableHtml + '</div>';
} else if (jsonData.preview_type === 'yaml') {
// YAML 预览(带行号)
let text = typeof jsonData.content === 'string' ? jsonData.content : JSON.stringify(jsonData.content, null, 2);
if (jsonData.truncated) {
text += '\n\n... (内容过长,已截断)';
}
const lines = text.split('\n');
const lineNumbers = lines.map((_, i) => i + 1).join('\n');
document.getElementById('previewContent').innerHTML =
'<div style="display: flex; background: var(--bg-input); border-radius: 8px; overflow: auto;">' +
'<div style="padding: 16px 8px; color: var(--text-muted); font-family: monospace; font-size: 13px; text-align: right; user-select: none; border-right: 1px solid var(--border); white-space: pre;">' +
lineNumbers + '</div>' +
'<pre style="flex: 1; margin: 0; padding: 16px; overflow: auto; white-space: pre-wrap; word-wrap: break-word; font-family: monospace; font-size: 13px;">' +
escapeHtml(text) + '</pre></div>';
} else if (jsonData.preview_type === 'audio') {
// 音频预览
if (jsonData.data_url) {
document.getElementById('previewContent').innerHTML =
'<div style="text-align: center; padding: 40px;">' +
'<i class="fa-solid fa-music fa-3x"></i><br><br>' +
'<audio controls style="width: 100%; max-width: 400px;">' +
'<source src="' + jsonData.data_url + '" type="' + jsonData.type + '">' +
'您的浏览器不支持音频播放' +
'</audio>' +
'<p style="margin-top: 16px; color: var(--text-muted);">点击下载完整音频</p>' +
'</div>';
} else {
document.getElementById('previewContent').innerHTML = '<div style="text-align: center; padding: 40px;"><i class="fa-solid fa-music"></i> 音频预览不可用</div>';
}
} else if (jsonData.preview_type === 'pdf') {
// PDF 预览
if (jsonData.data_url) {
document.getElementById('previewContent').innerHTML =
'<div style="text-align: center; padding: 20px;"><i class="fa-regular fa-file-pdf fa-2x"></i><br><br>' +
'<embed src="' + jsonData.data_url + '" type="application/pdf" width="100%" height="70vh" />' +
'<p style="margin-top: 10px; color: var(--text-muted);">如果PDF无法显示,请点击下载</p>' +
'</div>';
} else {
document.getElementById('previewContent').innerHTML = '<div style="text-align: center; padding: 40px;"><i class="fa-regular fa-file-pdf"></i> PDF预览不可用</div>';
}
} else {
document.getElementById('previewContent').innerHTML = '<div style="text-align: center; padding: 40px;"><i class="fa-regular fa-file"></i> 此文件类型不支持在线预览</div>';
}
} else {
document.getElementById('previewContent').innerHTML = '<div style="text-align: center; padding: 40px;"><i class="fa-regular fa-file"></i> 此文件类型不支持在线预览</div>';
}
} else if (contentType.includes('image/')) {
// 图片预览
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
document.getElementById('previewContent').innerHTML = '<img src="' + blobUrl + '" style="max-width: 100%; max-height: 60vh; display: block; margin: 0 auto;" />';
} else if (contentType.includes('text/') || contentType.includes('application/javascript')) {
// 文本预览(带行号)
const text = await response.text();
let displayText = text.length > 50000 ? text.substring(0, 50000) + '\n\n... (内容过长,已截断)' : text;
const lines = displayText.split('\n');
const lineNumbers = lines.map((_, i) => i + 1).join('\n');
document.getElementById('previewContent').innerHTML =
'<div style="display: flex; background: var(--bg-input); border-radius: 8px; overflow: auto;">' +
'<div style="padding: 16px 8px; color: var(--text-muted); font-family: monospace; font-size: 13px; text-align: right; user-select: none; border-right: 1px solid var(--border); white-space: pre;">' +
lineNumbers + '</div>' +
'<pre style="flex: 1; margin: 0; padding: 16px; overflow: auto; white-space: pre-wrap; word-wrap: break-word; font-family: monospace; font-size: 13px;">' +
escapeHtml(displayText) + '</pre></div>';
} else {
// 其他类型显示信息
const size = response.headers.get('x-file-size') || '未知';
document.getElementById('previewContent').innerHTML = '<div style="text-align: center; padding: 40px;"><i class="fa-regular fa-file fa-3x"></i><br><br><p>此文件类型不支持在线预览</p><p style="color: var(--text-muted); margin-top: 10px;">文件大小: ' + size + '</p><button class="btn btn-primary" style="margin-top: 20px;" onclick="downloadFile(\'' + path.replace(/'/g, "\\'") + '\')"><i class="fa-solid fa-download"></i> 下载文件</button></div>';
}
} catch (e) {
document.getElementById('previewContent').innerHTML = '<div style="text-align: center; padding: 40px; color: var(--danger);"><i class="fa-solid fa-circle-exclamation"></i> 预览失败: ' + e.message + '</div>';
}
showModal('previewModal');
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// SVG 预览模式切换
function switchSvgMode(mode) {
const imgContainer = document.getElementById('svgImageContainer');
const srcContainer = document.getElementById('svgSourceContainer');
const imgBtn = imgContainer?.parentElement?.querySelector('button:nth-child(1)');
const srcBtn = srcContainer?.parentElement?.querySelector('button:nth-child(2)');
if (mode === 'image') {
if (imgContainer) imgContainer.style.display = 'block';
if (srcContainer) srcContainer.style.display = 'none';
if (imgBtn) {
imgBtn.className = 'btn btn-primary';
imgBtn.style.padding = '8px 20px';
}
if (srcBtn) {
srcBtn.className = 'btn btn-secondary';
srcBtn.style.padding = '8px 20px';
}
} else {
if (imgContainer) imgContainer.style.display = 'none';
if (srcContainer) srcContainer.style.display = 'block';
if (imgBtn) {
imgBtn.className = 'btn btn-secondary';
imgBtn.style.padding = '8px 20px';
}
if (srcBtn) {
srcBtn.className = 'btn btn-primary';
srcBtn.style.padding = '8px 20px';
}
}
}
// ============ 镜像同步 ============
async function loadSync() {
const [sources, status] = await Promise.all([
apiRequest('/api/v1/sync/sources'),
apiRequest('/api/v1/sync/status')
]);
const syncList = document.getElementById('syncList');
const statusBadge = document.getElementById('syncStatus');
// 检查任意源是否在运行
const running = Object.values(status || {}).some(s => s?.status === 'syncing');
if (sources?.sources?.length) {
syncList.innerHTML = sources.sources.map(s => {
const safeName = s.name.replace(/'/g, "\\'");
// 定时同步信息
const schedule = s.schedule || {};
const scheduleEnabled = schedule.enabled;
const scheduleCron = schedule.cron || '-';
const nextSync = s.next_sync ? new Date(s.next_sync).toLocaleString() : '-';
// 获取当前源的同步状态 (status直接是字典格式)
const sourceStatusData = status?.[s.name] || {};
const sourceStatus = sourceStatusData?.status || 'stopped';
// 只有正在同步时才能停止,其他状态都是开始/重新同步
const isRunning = sourceStatus === 'syncing';
const filesSynced = sourceStatusData?.files_synced || 0;
const totalFiles = sourceStatusData?.total_files || 0;
const progress = totalFiles > 0 ? Math.round((filesSynced / totalFiles) * 100) : 0;
// 状态显示
let statusBadge = '';
if (sourceStatus === 'syncing') {
statusBadge = '<span class="badge badge-success">同步中 ' + progress + '%</span>';
} else if (sourceStatus === 'completed') {
statusBadge = '<span class="badge badge-info">已完成</span>';
} else if (sourceStatus === 'error') {
statusBadge = '<span class="badge badge-danger">错误</span>';
} else {
statusBadge = '<span class="badge badge-secondary">空闲</span>';
}
// 进度条
const progressBar = sourceStatus === 'syncing'
? '<div class="progress" style="width: 60px; height: 6px;"><div class="progress-bar" style="width: ' + progress + '%"></div></div>'
: '-';
// 检查是否是PyPI类型的源(支持临时单次同步)
const isPyPI = (s.type === 'http' && (s.url || '').includes('pypi'));
return '<tr>' +
'<td><strong>' + s.name + '</strong></td>' +
'<td><span class="badge badge-info">' + (s.type || 'http') + '</span></td>' +
'<td><code>' + (s.target || '-') + '</code></td>' +
'<td>' + statusBadge + '</td>' +
'<td>' + progressBar + '</td>' +
'<td><span class="badge ' + (scheduleEnabled ? 'badge-warning' : 'badge-secondary') + '">' + (scheduleEnabled ? '⏰ 定时' : '手动') + '</span></td>' +
'<td>' +
'<button class="btn btn-secondary btn-sm" onclick="editSyncSource(\'' + safeName + '\')" title="编辑"><i class="fa-solid fa-edit"></i></button> ' +
(isPyPI ? '<button class="btn btn-primary btn-sm" onclick="showTempSyncModal(\'' + safeName + '\')" title="临时单次同步"><i class="fa-solid fa-bolt"></i></button> ' : '') +
'<button class="btn btn-' + (isRunning ? 'warning' : 'success') + ' btn-sm" onclick="toggleSync(\'' + safeName + '\')" title="' + (isRunning ? '停止同步' : '开始同步') + '"><i class="fa-solid fa-' + (isRunning ? 'stop' : 'play') + '"></i></button> ' +
'<button class="btn btn-danger btn-sm" onclick="deleteSync(\'' + safeName + '\')" title="删除"><i class="fa-solid fa-trash"></i></button>' +
'</td></tr>';
}).join('');
} else {
syncList.innerHTML = '<tr><td colspan="7" class="empty">暂无同步源</td></tr>';
}
statusBadge.textContent = running ? '运行中' : '空闲';
statusBadge.className = 'badge badge-' + (running ? 'success' : 'info');
}
// 根据同步类型显示/隐藏配置字段
function updateSyncTypeFields() {
const type = document.getElementById('syncType').value;
const urlGroup = document.getElementById('syncUrlGroup');
const urlLabel = document.getElementById('syncUrlLabel');
const cloudFields = document.getElementById('syncCloudFields');
const ftpFields = document.getElementById('syncFtpFields');
const localFields = document.getElementById('syncLocalFields');
const targetGroup = document.getElementById('syncTargetGroup');
// 隐藏所有额外字段
cloudFields.style.display = 'none';
ftpFields.style.display = 'none';
localFields.style.display = 'none';
targetGroup.style.display = 'none';
// 根据类型显示字段
switch(type) {
case 'http':
urlLabel.textContent = '源 URL';
urlGroup.style.display = 'block';
targetGroup.style.display = 'block';
break;
case 'ftp':
case 'sftp':
urlGroup.style.display = 'none';
ftpFields.style.display = 'block';
targetGroup.style.display = 'block';
break;
case 'rsync':
urlLabel.textContent = 'Rsync 源';
urlGroup.style.display = 'block';
targetGroup.style.display = 'block';
break;
case 'git':
urlLabel.textContent = 'Git 仓库 URL';
urlGroup.style.display = 'block';
targetGroup.style.display = 'block';
break;
case 's3':
case 'oss':
case 'cos':
urlGroup.style.display = 'none';
cloudFields.style.display = 'block';
targetGroup.style.display = 'block';
break;
case 'local':
urlGroup.style.display = 'none';
localFields.style.display = 'block';
targetGroup.style.display = 'block';
break;
case 'webdav':
urlLabel.textContent = 'WebDAV 服务器';
urlGroup.style.display = 'block';
ftpFields.style.display = 'block';
targetGroup.style.display = 'block';
break;
}
}
async function addSyncSource() {
try {
const name = document.getElementById('syncName').value;
const type = document.getElementById('syncType').value;
const auto = document.getElementById('syncAuto').checked;
if (!name) {
showNotification('请填写名称', 'warning');
return;
}
// 定时同步配置
const scheduleEnabledEl = document.getElementById('syncScheduleEnabled');
const scheduleCronEl = document.getElementById('syncScheduleCron');
const scheduleEnabled = scheduleEnabledEl ? scheduleEnabledEl.checked : false;
const scheduleCron = scheduleCronEl ? scheduleCronEl.value.trim() : '';
// 构建配置对象
let config = {
type: type,
enabled: true,
auto_sync: auto
};
// 添加定时同步配置
if (scheduleEnabled && scheduleCron) {
config.schedule = {
enabled: true,
cron: scheduleCron
};
}
// 根据类型收集配置
switch(type) {
case 'http':
case 'rsync':
case 'git':
case 'webdav': {
const url = document.getElementById('syncUrl').value;
if (!url) { showNotification('请填写URL', 'warning'); return; }
config.url = url;
config.target = document.getElementById('syncTarget').value || name;
break;
}
case 'ftp':
case 'sftp': {
const host = document.getElementById('syncHost').value;
const port = document.getElementById('syncPort').value;
const username = document.getElementById('syncUsername').value;
const password = document.getElementById('syncPassword').value;
const remotePath = document.getElementById('syncRemotePath').value;
if (!host) { showNotification('请填写主机地址', 'warning'); return; }
config.host = host;
config.port = parseInt(port) || 21;
config.username = username || 'anonymous';
config.password = password;
config.remote_path = remotePath || '/';
config.target = document.getElementById('syncTarget').value || name;
break;
}
case 's3':
case 'oss':
case 'cos': {
const bucket = document.getElementById('syncBucket').value;
const accessKey = document.getElementById('syncAccessKey').value;
const secretKey = document.getElementById('syncSecretKey').value;
const region = document.getElementById('syncRegion').value;
const prefix = document.getElementById('syncPrefix').value;
if (!bucket || !accessKey || !secretKey) { showNotification('请填写Bucket和密钥信息', 'warning'); return; }
config.bucket = bucket;
config.access_key = accessKey;
config.secret_key = secretKey;
config.region = region;
if (prefix) config.prefix = prefix;
config.target = document.getElementById('syncTarget').value || name;
break;
}
case 'local': {
const localPath = document.getElementById('syncLocalPath').value;
if (!localPath) { showNotification('请填写本地路径', 'warning'); return; }
config.path = localPath;
config.target = document.getElementById('syncTarget').value || name;
break;
}
}
console.log('准备添加同步源:', name, config);
const result = await apiRequest('/api/v1/sync/sources', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, config })
});
console.log('添加结果:', result);
hideModal('addSyncModal');
loadSync();
if (result.success) {
showNotification('同步源添加成功', 'success');
} else {
showNotification('添加失败: ' + (result.error || '未知错误'), 'error');
}
} catch (e) {
console.error('添加同步源失败:', e);
showNotification('添加同步源失败: ' + e.message, 'error');
}
}
// 存储当前同步源数据
let currentSyncSources = [];
// 编辑同步源 - 打开编辑对话框
async function editSyncSource(name) {
try {
// 获取同步源列表
const sources = await apiRequest('/api/v1/sync/sources');
const source = sources.sources.find(s => s.name === name);
if (!source) {
showNotification('同步源不存在', 'error');
return;
}
// 存储当前同步源列表
currentSyncSources = sources.sources;
// 先显示对话框(避免 showModal 清空数据)
showModal('editSyncModal');
// 填充基本字段
document.getElementById('editSyncName').value = name;
document.getElementById('editSyncType').value = source.type || 'http';
document.getElementById('editSyncTarget').value = source.target || '';
document.getElementById('editSyncAuto').checked = source.auto_sync !== false;
// 定时同步配置
const schedule = source.schedule || {};
document.getElementById('editSyncScheduleEnabled').checked = schedule.enabled || false;
document.getElementById('editSyncScheduleCron').value = schedule.cron || '0 2 * * *';
// 根据类型填充字段
const type = source.type || 'http';
if (type === 'http' || type === 'rsync' || type === 'git' || type === 'webdav') {
document.getElementById('editSyncUrl').value = source.url || '';
} else if (type === 'ftp' || type === 'sftp') {
document.getElementById('editSyncHost').value = source.host || '';
document.getElementById('editSyncPort').value = source.port || 21;
document.getElementById('editSyncUsername').value = source.username || '';
document.getElementById('editSyncPassword').value = source.password || '';
document.getElementById('editSyncRemotePath').value = source.remote_path || '/';
} else if (type === 's3' || type === 'oss' || type === 'cos') {
document.getElementById('editSyncBucket').value = source.bucket || '';
document.getElementById('editSyncAccessKey').value = source.access_key || '';
document.getElementById('editSyncSecretKey').value = source.secret_key || '';
document.getElementById('editSyncRegion').value = source.region || '';
document.getElementById('editSyncPrefix').value = source.prefix || '';
} else if (type === 'local') {
document.getElementById('editSyncLocalPath').value = source.path || '';
}
// 更新字段显示
updateEditSyncTypeFields();
} catch (e) {
console.error('获取同步源信息失败:', e);
showNotification('获取同步源信息失败: ' + e.message, 'error');
}
}
// 根据同步类型显示/隐藏编辑配置字段
function updateEditSyncTypeFields() {
const type = document.getElementById('editSyncType').value;
const urlGroup = document.getElementById('editSyncUrlGroup');
const urlLabel = document.getElementById('editSyncUrlLabel');
const cloudFields = document.getElementById('editSyncCloudFields');
const ftpFields = document.getElementById('editSyncFtpFields');
const localFields = document.getElementById('editSyncLocalFields');
const targetGroup = document.getElementById('editSyncTargetGroup');
// 隐藏所有额外字段
cloudFields.style.display = 'none';
ftpFields.style.display = 'none';
localFields.style.display = 'none';
targetGroup.style.display = 'none';
// 根据类型显示对应字段
if (type === 'http' || type === 'rsync' || type === 'git' || type === 'webdav') {
urlGroup.style.display = 'block';
targetGroup.style.display = 'block';
if (type === 'http') urlLabel.textContent = '源 URL';
else if (type === 'rsync') urlLabel.textContent = 'Rsync 源';
else if (type === 'git') urlLabel.textContent = 'Git 仓库 URL';
else urlLabel.textContent = 'WebDAV URL';
} else if (type === 'ftp' || type === 'sftp') {
ftpFields.style.display = 'block';
targetGroup.style.display = 'block';
} else if (type === 's3' || type === 'oss' || type === 'cos') {
cloudFields.style.display = 'block';
targetGroup.style.display = 'block';
} else if (type === 'local') {
localFields.style.display = 'block';
targetGroup.style.display = 'block';
}
}
// 保存编辑后的同步源
async function saveEditSyncSource() {
try {
const name = document.getElementById('editSyncName').value;
const type = document.getElementById('editSyncType').value;
const auto = document.getElementById('editSyncAuto').checked;
if (!name) {
showNotification('同步源名称不能为空', 'warning');
return;
}
// 定时同步配置
const scheduleEnabledEl = document.getElementById('editSyncScheduleEnabled');
const scheduleCronEl = document.getElementById('editSyncScheduleCron');
const scheduleEnabled = scheduleEnabledEl ? scheduleEnabledEl.checked : false;
const scheduleCron = scheduleCronEl ? scheduleCronEl.value.trim() : '';
// 构建配置对象
let config = {
type: type,
enabled: true,
auto_sync: auto
};
// 添加定时同步配置
if (scheduleEnabled && scheduleCron) {
config.schedule = {
enabled: true,
cron: scheduleCron
};
}
// 根据类型收集配置
switch(type) {
case 'http':
case 'rsync':
case 'git':
case 'webdav': {
const url = document.getElementById('editSyncUrl').value;
if (!url) { showNotification('请填写URL', 'warning'); return; }
config.url = url;
config.target = document.getElementById('editSyncTarget').value || name;
break;
}
case 'ftp':
case 'sftp': {
const host = document.getElementById('editSyncHost').value;
const port = document.getElementById('editSyncPort').value;
const username = document.getElementById('editSyncUsername').value;
const password = document.getElementById('editSyncPassword').value;
const remotePath = document.getElementById('editSyncRemotePath').value;
if (!host) { showNotification('请填写主机地址', 'warning'); return; }
config.host = host;
config.port = parseInt(port) || 21;
config.username = username || 'anonymous';
config.password = password;
config.remote_path = remotePath || '/';
config.target = document.getElementById('editSyncTarget').value || name;
break;
}
case 's3':
case 'oss':
case 'cos': {
const bucket = document.getElementById('editSyncBucket').value;
const accessKey = document.getElementById('editSyncAccessKey').value;
const secretKey = document.getElementById('editSyncSecretKey').value;
const region = document.getElementById('editSyncRegion').value;
const prefix = document.getElementById('editSyncPrefix').value;
if (!bucket || !accessKey || !secretKey) { showNotification('请填写Bucket和密钥信息', 'warning'); return; }
config.bucket = bucket;
config.access_key = accessKey;
config.secret_key = secretKey;
config.region = region;
if (prefix) config.prefix = prefix;
config.target = document.getElementById('editSyncTarget').value || name;
break;
}
case 'local': {
const localPath = document.getElementById('editSyncLocalPath').value;
if (!localPath) { showNotification('请填写本地路径', 'warning'); return; }
config.path = localPath;
config.target = document.getElementById('editSyncTarget').value || name;
break;
}
}
console.log('准备编辑同步源:', name, config);
// 使用 PUT 方法更新同步源
const result = await apiRequest('/api/v1/sync/sources/' + name, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, config })
});
console.log('编辑结果:', result);
hideModal('editSyncModal');
loadSync();
if (result.success) {
showNotification('同步源更新成功', 'success');
} else {
showNotification('更新失败: ' + (result.error || '未知错误'), 'error');
}
} catch (e) {
console.error('编辑同步源失败:', e);
showNotification('编辑同步源失败: ' + e.message, 'error');
}
}
async function startSync(name) {
await apiRequest('/api/v1/sync/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sources: [name] })
});
loadSync();
showNotification('同步任务已启动', 'success');
}
async function stopSync() {
await apiRequest('/api/v1/sync/stop', { method: 'POST' });
loadSync();
showNotification('同步已停止', 'info');
}
// 切换同步状态(开始/停止)
async function toggleSync(name) {
const status = await apiRequest('/api/v1/sync/status');
console.log('同步状态:', status);
// status直接是字典格式: {source_name: {...}}
const sourceStatusData = status?.[name];
const sourceStatus = sourceStatusData?.status || 'stopped';
// 只有 'syncing' 才表示正在运行,'completed' 和 'stopped' 都应该可以启动
const isRunning = sourceStatus === 'syncing';
console.log('源状态:', sourceStatus, '是否运行:', isRunning);
if (isRunning) {
console.log('执行停止操作');
await apiRequest('/api/v1/sync/stop', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name })
});
showNotification('同步已停止', 'info');
} else {
console.log('执行启动操作');
await apiRequest('/api/v1/sync/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name })
});
showNotification('同步任务已启动', 'success');
}
loadSync();
}
async function deleteSync(name) {
if (!confirm('确定删除同步源 ' + name + '?')) return;
await apiRequest('/api/v1/sync/sources/' + name, { method: 'DELETE' });
loadSync();
showNotification('同步源已删除', 'success');
}
// 加载同步历史记录
async function loadSyncHistory() {
try {
const response = await apiRequest('/api/v2/sync/history?limit=100');
const history = response.history || [];
const historyList = document.getElementById('syncHistory');
if (!historyList) return;
if (history.length === 0) {
historyList.innerHTML = '<tr><td colspan="7" class="empty">暂无同步记录</td></tr>';
return;
}
historyList.innerHTML = history.map(h => {
const statusBadge = h.status === 'completed'
? '<span class="badge badge-success">完成</span>'
: '<span class="badge badge-danger">失败</span>';
const date = h.last_sync ? new Date(h.last_sync).toLocaleString() : '-';
return `<tr>
<td>${h.source_name || '-'}</td>
<td>手动同步</td>
<td>${statusBadge}</td>
<td>${h.files_synced || 0} / ${h.total_files || 0}</td>
<td>-</td>
<td>${date}</td>
<td>${h.error ? '<span style="color: var(--error);">'+ h.error +'</span>' : '-'}</td>
</tr>`;
}).join('');
} catch (e) {
console.error('加载同步历史失败:', e);
}
}
// 显示临时同步弹窗
function showTempSyncModal(sourceName) {
document.getElementById('tempSyncSource').value = sourceName;
document.getElementById('tempSyncPackages').value = '';
document.getElementById('tempSyncModal').style.display = 'flex';
}
// 执行临时同步
async function doTempSync() {
const source = document.getElementById('tempSyncSource').value;
const packagesStr = document.getElementById('tempSyncPackages').value.trim();
if (!packagesStr) {
showNotification('请输入要同步的包名', 'error');
return;
}
// 解析包名列表(支持逗号、空格、换行分隔)
const packages = packagesStr.split(/[, \n]+/).map(p => p.trim()).filter(p => p);
if (packages.length === 0) {
showNotification('请输入有效的包名', 'error');
return;
}
try {
const response = await apiRequest('/api/v2/sync/packages', {
method: 'POST',
body: JSON.stringify({ source: source, packages: packages })
});
if (response.success) {
showNotification('临时同步已启动: ' + packages.slice(0, 3).join(', ') + (packages.length > 3 ? '...' : ''), 'success');
document.getElementById('tempSyncModal').style.display = 'none';
// 刷新同步列表
loadSync();
} else {
showNotification(response.error || '启动同步失败', 'error');
}
} catch (e) {
showNotification('同步失败: ' + e.message, 'error');
}
}
// ============ 加速源 ============
// 显示添加弹窗
function showAddMirrorModal() {
document.getElementById('addMirrorModal').style.display = 'flex';
// 防止背景滚动
document.body.style.overflow = 'hidden';
// 清空表单
document.getElementById('newMirrorName').value = '';
document.getElementById('newMirrorType').value = 'http';
document.getElementById('newMirrorUrl').value = '';
document.getElementById('newMirrorTarget').value = '';
document.getElementById('newMirrorDesc').value = '';
document.getElementById('newMirrorEnabled').checked = true;
}
// 添加加速源
async function addMirror() {
const name = document.getElementById('newMirrorName').value.trim();
const type = document.getElementById('newMirrorType').value;
const url = document.getElementById('newMirrorUrl').value.trim();
const target = document.getElementById('newMirrorTarget').value.trim();
const storageDir = document.getElementById('newMirrorStorageDir').value.trim();
const desc = document.getElementById('newMirrorDesc').value.trim();
const enabled = document.getElementById('newMirrorEnabled').checked;
// 验证输入
if (!name) {
showNotification('请输入加速源名称', 'warning');
return;
}
// 验证名称格式
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(name)) {
showNotification('名称必须以字母开头,只能包含字母、数字、下划线和连字符', 'warning');
return;
}
if (!url) {
showNotification('请输入上游地址', 'warning');
return;
}
// 构建请求数据
const mirrorData = {
name: name,
type: type,
url: url,
target: target,
description: desc,
enabled: enabled,
custom: true
};
// 添加存储目录(如果用户指定了)
if (storageDir) {
mirrorData.storage_dir = storageDir;
}
try {
const result = await apiRequest('/api/v2/mirrors', {
method: 'POST',
body: JSON.stringify(mirrorData)
});
if (result.success) {
showNotification(`加速源 '${name}' 添加成功`, 'success');
closeAddMirrorModal();
loadMirrors();
} else {
showNotification(`添加失败: ${result.error}`, 'error');
}
} catch (e) {
showNotification(`添加失败: ${e.message}`, 'error');
}
}
// 关闭添加弹窗
function closeAddMirrorModal() {
document.getElementById('addMirrorModal').style.display = 'none';
// 恢复背景滚动
document.body.style.overflow = '';
}
// 加载镜像列表
async function loadMirrors() {
try {
const mirrors = await apiRequest('/api/v2/mirrors');
if (!mirrors || mirrors.error) {
document.getElementById('mirrorList').innerHTML = '<div style="text-align: center; padding: 40px; color: var(--danger);">加载失败: ' + (mirrors?.error || '未知错误') + '</div>';
return;
}
const mirrorData = mirrors?.mirrors || {};
const mirrorEntries = Object.entries(mirrorData);
if (mirrorEntries.length === 0) {
document.getElementById('mirrorList').innerHTML = '<div style="text-align: center; padding: 40px; color: var(--text-muted);"><i class="fa-solid fa-ghost" style="font-size: 32px; margin-bottom: 10px;"></i><div>暂无加速源</div><div style="margin-top: 10px;">点击右上角"添加加速源"按钮创建</div></div>';
return;
}
const typeIcons = {
'docker': '🐳',
'apt': '📦',
'yum': '🎯',
'pacman': '🔵',
'pypi': '🐍',
'npm': '📦',
'go': '🔷',
'maven': '🫙',
'gradle': '🔨',
'gem': '💎',
'cargo': '🦀',
'nuget': '📚',
'cocoapods': '🍎',
'cran': '📊',
'ctan': '📝',
'cuda': '🎮',
'http': '🌐'
};
const html = mirrorEntries.map(([name, data]) => {
const enabled = data?.enabled !== false;
const type = data?.type || 'http';
const url = data?.url || '';
const target = data?.target || '';
const desc = data?.description || '';
const safeName = name.replace(/'/g, "\\'");
return `
<div style="padding: 16px; background: var(--bg-input); border-radius: 8px; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center;">
<div style="flex: 1; min-width: 0;">
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 8px;">
<span style="font-size: 20px;">${typeIcons[type] || '🌐'}</span>
<strong style="font-size: 15px;">${name}</strong>
<span class="badge badge-${enabled ? 'success' : 'danger'}">${enabled ? '启用' : '禁用'}</span>
<span class="badge badge-info">${type}</span>
</div>
<div style="color: var(--text-muted); font-size: 12px; margin-bottom: 4px;">${desc || ''}</div>
${url ? '<div style="color: var(--info); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">' + url + '</div>' : ''}
</div>
<div style="display: flex; align-items: center; gap: 8px; margin-left: 16px;">
<button onclick="editMirror('${safeName}')" class="btn btn-secondary" style="padding: 6px 10px; font-size: 12px;" title="编辑">
<i class="fa-solid fa-pen"></i>
</button>
<label class="switch" title="启用/禁用">
<input type="checkbox" ${enabled ? 'checked' : ''} onchange="toggleMirror('${safeName}', this.checked)">
<span class="switch-slider"></span>
</label>
<button onclick="deleteMirror('${safeName}')" class="btn btn-danger" style="padding: 6px 10px; font-size: 12px;" title="删除">
<i class="fa-solid fa-trash"></i>
</button>
</div>
</div>
`;
}).join('');
document.getElementById('mirrorList').innerHTML = html;
// 生成使用说明
generateMirrorUsageGuide(mirrorData);
} catch (e) {
console.error('Failed to load mirrors:', e);
document.getElementById('mirrorList').innerHTML = '<div style="text-align: center; padding: 40px; color: var(--danger);">加载失败</div>';
}
}
// 生成加速源使用说明
function generateMirrorUsageGuide(mirrors) {
const container = document.getElementById('mirrorUsageGuide');
if (!container) return;
const serverUrl = window.location.protocol + '//' + window.location.host;
const enabledMirrors = Object.entries(mirrors).filter(([_, data]) => data?.enabled !== false);
if (enabledMirrors.length === 0) {
container.innerHTML = `
<div style="color: var(--text-muted); text-align: center; padding: 20px;">
<i class="fa-solid fa-info-circle" style="font-size: 24px; margin-bottom: 10px;"></i>
<div>添加加速源后,此处将显示对应的使用命令</div>
</div>
`;
return;
}
// 每种类型的使用命令模板
const usageTemplates = {
'docker': (name, data) => ({
title: `${name} - Docker 镜像`,
cmd: `docker pull ${serverUrl}/api/v2/mirrors/docker/library/ubuntu:22.04`
}),
'apt': (name, data) => ({
title: `${name} - APT (Ubuntu/Debian)`,
cmd: `echo "deb ${serverUrl}/api/v2/mirrors/apt/ubuntu/ jammy main" | sudo tee /etc/apt/sources.list`
}),
'yum': (name, data) => ({
title: `${name} - YUM/DNF (CentOS/RHEL)`,
cmd: `sudo sed -i 's|^baseurl=.*|baseurl=${serverUrl}/api/v2/mirrors/yum/centos/$releasever/|' /etc/yum.repos.d/*.repo`
}),
'pypi': (name, data) => ({
title: `${name} - PyPI`,
cmd: `pip config set global.index-url ${serverUrl}/api/v2/mirrors/pypi/simple`
}),
'npm': (name, data) => ({
title: `${name} - npm`,
cmd: `npm config set registry ${serverUrl}/api/v2/mirrors/npm/`
}),
'go': (name, data) => ({
title: `${name} - Go 模块`,
cmd: `export GOPROXY=${serverUrl}/api/v2/mirrors/go,direct`
}),
'maven': (name, data) => ({
title: `${name} - Maven`,
cmd: `<settings><mirrors><mirror><id>hyc</id><url>${serverUrl}/api/v2/mirrors/maven</url></mirror></mirrors></settings>`
}),
'gradle': (name, data) => ({
title: `${name} - Gradle`,
cmd: `gradle.properties: maven { url "${serverUrl}/api/v2/mirrors/maven" }`
}),
'gem': (name, data) => ({
title: `${name} - RubyGems`,
cmd: `gem sources --add ${serverUrl}/api/v2/mirrors/gem/ --remove https://rubygems.org/`
}),
'cargo': (name, data) => ({
title: `${name} - Cargo (Rust)`,
cmd: `~/.cargo/config.toml: [source.crates-io]\nreplace-with = "hyc"\n[source.hyc]\nregistry = "${serverUrl}/api/v2/mirrors/cargo"`
}),
'nuget': (name, data) => ({
title: `${name} - NuGet`,
cmd: `nuget config -AsPath http://${window.location.host}/api/v2/mirrors/nuget`
}),
'cocoapods': (name, data) => ({
title: `${name} - CocoaPods`,
cmd: `Podfile: source '${serverUrl}/cocoapods/'`
}),
'cran': (name, data) => ({
title: `${name} - CRAN (R)`,
cmd: `R: options(repos = c(CRAN = "${serverUrl}/cran/"))`
}),
'ctan': (name, data) => ({
title: `${name} - CTAN (LaTeX)`,
cmd: `tlmgr option repository ${serverUrl}/ctan/`
}),
'pacman': (name, data) => ({
title: `${name} - Pacman (Arch Linux)`,
cmd: `/etc/pacman.d/mirrorlist: Server = ${serverUrl}/pacman/$arch`
}),
'alpine': (name, data) => ({
title: `${name} - Alpine Linux`,
cmd: `/etc/apk/repositories: ${serverUrl}/alpine/v\${VERSION_ID}/main`
}),
'arch': (name, data) => ({
title: `${name} - Arch Linux`,
cmd: `/etc/pacman.d/mirrorlist: Server = ${serverUrl}/arch/$repo/os/$arch`
}),
'yarn': (name, data) => ({
title: `${name} - Yarn`,
cmd: `yarn config set registry ${serverUrl}/api/v2/mirrors/yarn/`
}),
'composer': (name, data) => ({
title: `${name} - Composer (PHP)`,
cmd: `composer config -g repo.packagist composer ${serverUrl}/api/v2/mirrors/composer/`
}),
'conan': (name, data) => ({
title: `${name} - Conan (C++)`,
cmd: `conan remote add hyc ${serverUrl}/api/v2/mirrors/conan False`
}),
'bower': (name, data) => ({
title: `${name} - Bower`,
cmd: `.bowerrc: { "registry": "${serverUrl}/api/v2/mirrors/bower/" }`
}),
'helm': (name, data) => ({
title: `${name} - Helm Charts`,
cmd: `helm repo add hyc ${serverUrl}/api/v2/mirrors/helm/`
}),
'flutter': (name, data) => ({
title: `${name} - Flutter`,
cmd: `flutter config --build-dir ${serverUrl}/api/v2/mirrors/flutter/`
}),
'homebrew': (name, data) => ({
title: `${name} - Homebrew`,
cmd: `echo 'BREW_REPO="${serverUrl}/api/v2/mirrors/homebrew/"' >> ~/.bashrc`
}),
'chocolatey': (name, data) => ({
title: `${name} - Chocolatey (Windows)`,
cmd: `choco source add -n=hyc -s="${serverUrl}/api/v2/mirrors/chocolatey/" --priority=1`
}),
'snap': (name, data) => ({
title: `${name} - Snap Store`,
cmd: `snap set system proxy.http=${serverUrl}`
}),
'nix': (name, data) => ({
title: `${name} - Nix/NixOS`,
cmd: `mkdir -p ~/.config/nix && echo 'substituters = ${serverUrl}/api/v2/mirrors/nix/' >> ~/.config/nix/nix.conf`
}),
'gentoo': (name, data) => ({
title: `${name} - Gentoo`,
cmd: `/etc/portage/make.conf: GENTOO_MIRRORS="${serverUrl}/api/v2/mirrors/gentoo/"`
}),
'centos': (name, data) => ({
title: `${name} - CentOS`,
cmd: `sudo sed -i 's|mirror.centos.org|${serverUrl}/api/v2/mirrors/centos|g' /etc/yum.repos.d/*.repo`
}),
'debian': (name, data) => ({
title: `${name} - Debian`,
cmd: `echo "deb ${serverUrl}/api/v2/mirrors/debian/ bullseye main" | sudo tee /etc/apt/sources.list`
}),
'fedora': (name, data) => ({
title: `${name} - Fedora`,
cmd: `sudo sed -i 's|download.fedoraproject.org|${serverUrl}/api/v2/mirrors/fedora|g' /etc/yum.repos.d/*.repo`
}),
'cuda': (name, data) => ({
title: `${name} - NVIDIA CUDA`,
cmd: `cuda-repos.conf: ${serverUrl}/cuda/`
}),
'custom': (name, data) => ({
title: `${name} - 自定义HTTP`,
cmd: `${serverUrl}/api/v2/mirrors/custom/`
}),
// 更多包管理器用法
'pip': (name, data) => ({
title: `${name} - pip`,
cmd: `pip config set global.index-url ${serverUrl}/api/v2/mirrors/pypi/simple`
}),
'pipenv': (name, data) => ({
title: `${name} - Pipenv`,
cmd: `[[source]]
url = "${serverUrl}/api/v2/mirrors/pypi/simple"
verify_ssl = true`
}),
'poetry': (name, data) => ({
title: `${name} - Poetry`,
cmd: `poetry source add hyc ${serverUrl}/api/v2/mirrors/pypi/simple`
}),
'pnpm': (name, data) => ({
title: `${name} - pnpm`,
cmd: `pnpm config set registry ${serverUrl}/api/v2/mirrors/pnpm/`
}),
'pub': (name, data) => ({
title: `${name} - Pub.dev`,
cmd: `dart pub cache add --global [package] --source ${serverUrl}/api/v2/mirrors/pub/`
}),
'dart': (name, data) => ({
title: `${name} - Dart SDK`,
cmd: `${serverUrl}/api/v2/mirrors/dart/`
}),
'elm': (name, data) => ({
title: `${name} - Elm`,
cmd: `elm install ${serverUrl}/api/v2/mirrors/elm/`
}),
'hackage': (name, data) => ({
title: `${name} - Hackage`,
cmd: `${serverUrl}/api/v2/mirrors/hackage/`
}),
'opam': (name, data) => ({
title: `${name} - OPAM`,
cmd: `opam repository add hyc ${serverUrl}/api/v2/mirrors/opam/`
}),
'dub': (name, data) => ({
title: `${name} - Dub`,
cmd: `dub addregistry ${serverUrl}/api/v2/mirrors/dub/`
}),
'nimble': (name, data) => ({
title: `${name} - Nimble`,
cmd: `${serverUrl}/api/v2/mirrors/nimble/`
}),
'julia': (name, data) => ({
title: `${name} - Julia`,
cmd: `Pkg.add("Package") # Set ENV["JULIA_PKG_SERVER"]="${serverUrl}/api/v2/mirrors/julia/"`
}),
'lua': (name, data) => ({
title: `${name} - LuaRocks`,
cmd: `luarocks install --server=${serverUrl}/api/v2/mirrors/lua/ [package]`
}),
'luarocks': (name, data) => ({
title: `${name} - LuaRocks`,
cmd: `luarocks config rocks_servers ${serverUrl}/api/v2/mirrors/luarocks/`
}),
// Linux 发行版
'alpine': (name, data) => ({
title: `${name} - Alpine`,
cmd: `echo "${serverUrl}/api/v2/mirrors/alpine/v\${VERSION_ID}/main" > /etc/apk/repositories`
}),
'arch': (name, data) => ({
title: `${name} - Arch Linux`,
cmd: `Server = ${serverUrl}/api/v2/mirrors/arch/\$repo/os/\$arch`
}),
'aur': (name, data) => ({
title: `${name} - AUR`,
cmd: `${serverUrl}/api/v2/mirrors/aur/`
}),
'centos': (name, data) => ({
title: `${name} - CentOS`,
cmd: `sed -i 's|mirror.centos.org|${serverUrl}/api/v2/mirrors/centos|g' /etc/yum.repos.d/*.repo`
}),
'debian': (name, data) => ({
title: `${name} - Debian`,
cmd: `echo "deb ${serverUrl}/api/v2/mirrors/debian/ bookworm main" > /etc/apt/sources.list`
}),
'fedora': (name, data) => ({
title: `${name} - Fedora`,
cmd: `sed -i 's|download.fedoraproject.org|${serverUrl}/api/v2/mirrors/fedora|g' /etc/yum.repos.d/*.repo`
}),
'gentoo': (name, data) => ({
title: `${name} - Gentoo`,
cmd: `GENTOO_MIRRORS="${serverUrl}/api/v2/mirrors/gentoo/"`
}),
'opensuse': (name, data) => ({
title: `${name} - openSUSE`,
cmd: `${serverUrl}/api/v2/mirrors/opensuse/`
}),
'void': (name, data) => ({
title: `${name} - Void Linux`,
cmd: `echo "repository=${serverUrl}/api/v2/mirrors/void/x86_64" >> /etc/xbps.d/00-repository-main.conf`
}),
'freebsd': (name, data) => ({
title: `${name} - FreeBSD`,
cmd: `echo "PACKAGESITE:${serverUrl}/api/v2/mirrors/freebsd/" >> /etc/pkg.conf`
}),
'netbsd': (name, data) => ({
title: `${name} - NetBSD`,
cmd: `${serverUrl}/api/v2/mirrors/netbsd/`
}),
'openbsd': (name, data) => ({
title: `${name} - OpenBSD`,
cmd: `echo "installpath=${serverUrl}/api/v2/mirrors/openbsd/" >> /etc/pkg.conf`
}),
'rocky': (name, data) => ({
title: `${name} - Rocky Linux`,
cmd: `sed -i 's|repo.rockylinux.org|${serverUrl}/api/v2/mirrors/rocky|g' /etc/yum.repos.d/*.repo`
}),
'alma': (name, data) => ({
title: `${name} - AlmaLinux`,
cmd: `sed -i 's|repo.almalinux.org|${serverUrl}/api/v2/mirrors/alma|g' /etc/yum.repos.d/*.repo`
}),
'kali': (name, data) => ({
title: `${name} - Kali Linux`,
cmd: `echo "deb ${serverUrl}/api/v2/mirrors/kali/ kali-rolling main" > /etc/apt/sources.list`
}),
'ubuntu': (name, data) => ({
title: `${name} - Ubuntu`,
cmd: `sed -i 's|archive.ubuntu.com|${serverUrl}/api/v2/mirrors/ubuntu|g' /etc/apt/sources.list`
}),
'mint': (name, data) => ({
title: `${name} - Linux Mint`,
cmd: `sed -i 's|packages.linuxmint.com|${serverUrl}/api/v2/mirrors/mint|g' /etc/apt/sources.list`
}),
// 开发工具
'jetbrains': (name, data) => ({
title: `${name} - JetBrains`,
cmd: `${serverUrl}/api/v2/mirrors/jetbrains/`
}),
'vscode': (name, data) => ({
title: `${name} - VS Code`,
cmd: `${serverUrl}/api/v2/mirrors/vscode/`
}),
// 语言运行时
'rustup': (name, data) => ({
title: `${name} - Rustup`,
cmd: `export RUSTUP_DIST_SERVER=${serverUrl}/api/v2/mirrors/rustup/`
}),
'dotnet': (name, data) => ({
title: `${name} - .NET SDK`,
cmd: `${serverUrl}/api/v2/mirrors/dotnet/`
}),
'node': (name, data) => ({
title: `${name} - Node.js`,
cmd: `${serverUrl}/api/v2/mirrors/node/`
}),
'python': (name, data) => ({
title: `${name} - Python`,
cmd: `${serverUrl}/api/v2/mirrors/python/`
}),
'ruby': (name, data) => ({
title: `${name} - Ruby`,
cmd: `${serverUrl}/api/v2/mirrors/ruby/`
}),
'java': (name, data) => ({
title: `${name} - Java JDK`,
cmd: `${serverUrl}/api/v2/mirrors/java/`
}),
// 系统包管理器
'homebrew': (name, data) => ({
title: `${name} - Homebrew`,
cmd: `export HOMEBREW_BREW_GIT_REMOTE="${serverUrl}/api/v2/mirrors/homebrew/"`
}),
'brew': (name, data) => ({
title: `${name} - Brew`,
cmd: `export HOMEBREW_BREW_GIT_REMOTE="${serverUrl}/api/v2/mirrors/brew/"`
}),
'chocolatey': (name, data) => ({
title: `${name} - Chocolatey`,
cmd: `choco source add -n=hyc -s="${serverUrl}/api/v2/mirrors/chocolatey/"`
}),
'snap': (name, data) => ({
title: `${name} - Snap`,
cmd: `snap set system proxy.http=${serverUrl}`
}),
'flatpak': (name, data) => ({
title: `${name} - Flatpak`,
cmd: `flatpak remote-add --if-not-exists flathub ${serverUrl}/api/v2/mirrors/flatpak/`
}),
'winget': (name, data) => ({
title: `${name} - winget`,
cmd: `${serverUrl}/api/v2/mirrors/winget/`
}),
'scoop': (name, data) => ({
title: `${name} - Scoop`,
cmd: `scoop bucket add hyc ${serverUrl}/api/v2/mirrors/scoop/`
}),
// 其他
'termux': (name, data) => ({
title: `${name} - Termux`,
cmd: `echo "deb ${serverUrl}/api/v2/mirrors/termux/ stable main" > $PREFIX/etc/apt/sources.list`
}),
'msys2': (name, data) => ({
title: `${name} - MSYS2`,
cmd: `${serverUrl}/api/v2/mirrors/msys2/`
}),
'nix': (name, data) => ({
title: `${name} - Nix`,
cmd: `substituters = ${serverUrl}/api/v2/mirrors/nix/`
}),
'guix': (name, data) => ({
title: `${name} - Guix`,
cmd: `${serverUrl}/api/v2/mirrors/guix/`
}),
'http': (name, data) => ({
title: `${name} - HTTP`,
cmd: `${serverUrl}/${data?.target || 'custom/'}`
})
};
let html = '';
for (const [name, data] of enabledMirrors) {
const type = data?.type || 'http';
const template = usageTemplates[type];
if (template) {
const usage = template(name, data);
html += `<div style="margin-bottom: 12px;"><strong>${usage.title}</strong><br><code style="color: var(--info); display: block; margin-top: 4px; white-space: pre-wrap;">${usage.cmd}</code></div>`;
}
}
if (!html) {
html = '<div style="color: var(--text-muted);">当前启用的加速源无可用命令示例</div>';
}
container.innerHTML = html;
}
async function toggleMirror(mirror, enabled) {
try {
const result = await apiRequest(`/api/v2/mirrors/${mirror}/enable?enabled=${enabled}`, { method: 'PUT' });
if (result.success) {
showNotification(`镜像 ${mirror} 已${enabled ? '启用' : '禁用'}`, 'success');
loadMirrors(); // 刷新列表和使用说明
} else {
showNotification(`保存失败: ${result.error}`, 'error');
loadMirrors();
}
} catch (e) {
showNotification(`操作失败: ${e.message}`, 'error');
loadMirrors();
loadMirrors();
}
}
async function deleteMirror(mirrorName) {
if (!confirm(`确定要删除加速源 '${mirrorName}' 吗?此操作不可恢复。`)) {
return;
}
try {
const result = await apiRequest(`/api/v2/mirrors/${mirrorName}`, {
method: 'DELETE'
});
if (result.success) {
showNotification(`加速源 '${mirrorName}' 已删除`, 'success');
// 刷新列表
loadMirrors();
} else {
showNotification(`删除失败: ${result.error}`, 'error');
}
} catch (e) {
showNotification(`删除失败: ${e.message}`, 'error');
}
}
let editingMirrorName = '';
function updateEditMirrorStatusBadge(enabled) {
const badge = document.getElementById('editMirrorStatusBadge');
if (badge) {
if (enabled) {
badge.className = 'status-badge enabled';
badge.innerHTML = '<i class="fa-solid fa-check"></i> 已启用';
} else {
badge.className = 'status-badge disabled';
badge.innerHTML = '<i class="fa-solid fa-xmark"></i> 已禁用';
}
}
}
async function editMirror(name) {
editingMirrorName = name;
try {
const mirrors = await apiRequest('/api/v2/mirrors');
const mirrorData = mirrors?.mirrors || {};
const mirror = mirrorData[name];
if (!mirror) {
showNotification('找不到该加速源', 'error');
return;
}
// 打开弹窗(会清空输入框)
showModal('editMirrorModal');
// 填充编辑表单
document.getElementById('editMirrorName').value = name;
document.getElementById('editMirrorName').disabled = true; // 不能修改名称
document.getElementById('editMirrorType').value = mirror.type || 'custom';
document.getElementById('editMirrorUrl').value = mirror.url || '';
document.getElementById('editMirrorStorageDir').value = mirror.storage_dir || '';
document.getElementById('editMirrorDesc').value = mirror.description || '';
document.getElementById('editMirrorPriority').value = mirror.priority || 0;
document.getElementById('editMirrorEnabled').checked = mirror.enabled !== false;
// 更新启用状态徽章
updateEditMirrorStatusBadge(mirror.enabled !== false);
// 绑定启用开关事件
document.getElementById('editMirrorEnabled').onchange = function() {
updateEditMirrorStatusBadge(this.checked);
};
} catch (e) {
showNotification('加载加速源信息失败: ' + e.message, 'error');
}
}
async function updateMirror() {
const name = editingMirrorName;
const type = document.getElementById('editMirrorType').value;
const url = document.getElementById('editMirrorUrl').value.trim();
const storageDir = document.getElementById('editMirrorStorageDir').value.trim();
const desc = document.getElementById('editMirrorDesc').value.trim();
const priority = parseInt(document.getElementById('editMirrorPriority').value) || 0;
const enabled = document.getElementById('editMirrorEnabled').checked;
if (!url) {
showNotification('请输入上游地址', 'warning');
return;
}
// 构建更新数据
const updateData = {
type: type,
url: url,
description: desc,
priority: priority,
enabled: enabled
};
// 添加存储目录(如果用户指定了)
if (storageDir) {
updateData.storage_dir = storageDir;
}
try {
const result = await apiRequest('/api/v2/mirrors/' + name, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updateData)
});
if (result.success) {
showNotification(`加速源 '${name}' 已更新`, 'success');
hideModal('editMirrorModal');
loadMirrors();
} else {
showNotification(`更新失败: ${result.error}`, 'error');
}
} catch (e) {
showNotification(`更新失败: ${e.message}`, 'error');
}
}
async function refreshMirror(name) {
try {
showNotification(`正在刷新加速源 '${name}'...`, 'info');
const result = await apiRequest(`/api/v2/mirrors/${name}/refresh`, { method: 'POST' });
if (result.success) {
showNotification(`加速源 '${name}' 刷新成功`, 'success');
loadMirrors();
} else {
showNotification(`刷新失败: ${result.error}`, 'error');
}
} catch (e) {
showNotification(`刷新失败: ${e.message}`, 'error');
}
}
// ============ 配置文件 ============
let originalConfig = '';
let configLoadError = false;
async function loadConfig() {
const editor = document.getElementById('configEditor');
editor.value = '加载中...';
editor.disabled = true;
try {
// 尝试使用 V2 API
let result = await apiRequest('/api/v2/config');
// 如果V2 API失败,尝试使用V1 API
if (!result || !result.success) {
console.warn('V2 config API failed, trying V1 API...');
result = await apiRequest('/api/v1/config');
if (result && typeof result === 'object') {
// V1 API 返回的是配置对象,需要转换为JSON字符串
editor.value = JSON.stringify(result, null, 4);
originalConfig = editor.value;
document.getElementById('configPath').textContent = 'settings.json';
document.getElementById('configStatus').textContent = '已加载 (V1 API)';
document.getElementById('configStatus').style.color = 'var(--success)';
editor.disabled = false;
configLoadError = false;
return;
}
}
if (result && result.success) {
editor.value = result.config;
originalConfig = result.config;
document.getElementById('configPath').textContent = result.path;
document.getElementById('configStatus').textContent = '已加载';
document.getElementById('configStatus').style.color = 'var(--success)';
validateJson(result.config);
editor.disabled = false;
configLoadError = false;
} else {
throw new Error(result?.error || 'Unknown error');
}
} catch (e) {
console.error('Failed to load config:', e);
editor.value = '加载配置失败\n\n错误信息: ' + e.message + '\n\n请确保服务器正在运行,并检查API是否可用。';
editor.disabled = false;
document.getElementById('configStatus').textContent = '加载失败';
document.getElementById('configStatus').style.color = 'var(--danger)';
configLoadError = true;
showNotification('加载配置失败: ' + e.message, 'error');
}
}
function validateJson(jsonStr) {
try {
JSON.parse(jsonStr);
document.getElementById('configJsonError').style.display = 'none';
return true;
} catch (e) {
document.getElementById('configJsonError').textContent = 'JSON 格式错误: ' + e.message;
document.getElementById('configJsonError').style.display = 'block';
return false;
}
}
function formatConfig() {
try {
const editor = document.getElementById('configEditor');
const parsed = JSON.parse(editor.value);
editor.value = JSON.stringify(parsed, null, 4);
document.getElementById('configStatus').textContent = '已格式化';
validateJson(editor.value);
} catch (e) {
showNotification('格式化失败: ' + e.message, 'error');
}
}
async function saveConfig() {
const editor = document.getElementById('configEditor');
const configContent = editor.value;
// 验证 JSON
if (!validateJson(configContent)) {
showNotification('JSON 格式错误,无法保存', 'error');
return;
}
if (!confirm('确定要保存配置吗?此操作会覆盖现有配置。')) {
return;
}
try {
const result = await apiRequest('/api/v2/config', {
method: 'PUT',
body: configContent
});
if (result.success) {
showNotification('配置已保存', 'success');
originalConfig = configContent;
document.getElementById('configStatus').textContent = '已保存';
} else {
showNotification('保存失败: ' + result.error, 'error');
}
} catch (e) {
showNotification('保存失败: ' + e.message, 'error');
}
}
function resetConfig() {
if (!confirm('确定要重置为原始配置吗?未保存的更改将会丢失。')) {
return;
}
document.getElementById('configEditor').value = originalConfig;
document.getElementById('configStatus').textContent = '已重置';
validateJson(originalConfig);
}
async function reloadConfig() {
try {
const result = await apiRequest('/api/v2/config');
if (result.success) {
document.getElementById('configEditor').value = result.config;
originalConfig = result.config;
document.getElementById('configStatus').textContent = '已重新加载';
validateJson(result.config);
showNotification('配置已重新加载', 'success');
} else {
showNotification('重新加载失败: ' + result.error, 'error');
}
} catch (e) {
showNotification('重新加载失败: ' + e.message, 'error');
}
}
// 监听配置变化
document.getElementById('configEditor').addEventListener('input', function() {
if (this.value !== originalConfig) {
document.getElementById('configStatus').textContent = '未保存';
document.getElementById('configStatus').style.color = 'var(--warning)';
}
validateJson(this.value);
});
// ============ 下载统计 ============
let downloadChart = null;
let currentChartType = 'line';
let currentPeriod = 'year';
// 初始化选择器
function initDownloadStatsSelectors() {
const now = new Date();
const yearSelect = document.getElementById('yearSelect');
const monthSelect = document.getElementById('monthSelect');
// 填充年份(当前年往前5年)
let years = [];
for (let i = 0; i <= 5; i++) {
years.push(now.getFullYear() - i);
}
yearSelect.innerHTML = years.map(y => `<option value="${y}" ${y === now.getFullYear() ? 'selected' : ''}>${y}年</option>`).join('');
// 填充月份
monthSelect.innerHTML = Array.from({length: 12}, (_, i) => i + 1).map(m => `<option value="${m}" ${m === now.getMonth() + 1 ? 'selected' : ''}>${m}月</option>`).join('');
// 初始化按钮状态
document.getElementById('lineChartBtn').classList.add('active');
}
// 切换图表类型
function switchChartType(type) {
currentChartType = type;
document.getElementById('lineChartBtn').classList.toggle('active', type === 'line');
document.getElementById('barChartBtn').classList.toggle('active', type === 'bar');
loadDownloadChartData();
}
// 切换统计周期
function changePeriod() {
const period = document.getElementById('periodSelect').value;
const year = document.getElementById('yearSelect').value;
const month = document.getElementById('monthSelect').value;
const monthSelect = document.getElementById('monthSelect');
currentPeriod = period;
// 根据周期显示/隐藏月份选择器
monthSelect.style.display = period === 'month' ? 'block' : 'none';
loadDownloadChartData();
}
// 加载图表数据
async function loadDownloadChartData() {
const period = document.getElementById('periodSelect').value;
const year = document.getElementById('yearSelect').value;
const month = document.getElementById('monthSelect').value;
const url = `/api/v2/stats/download-by-period?period=${period}&year=${year}&month=${month}`;
const data = await apiRequest(url);
if (data) {
renderEnhancedChart(data);
// 更新增强统计卡片
updateEnhancedStats(data, period);
}
}
// 更新增强统计卡片(只更新 yearDl,不修改 totalDl)
function updateEnhancedStats(data, period) {
const yearDl = document.getElementById('yearDl');
if (period === 'year') {
// 年度视图:显示年度总下载
yearDl.textContent = formatNum(data.total || 0);
} else if (period === 'month') {
// 月度视图
yearDl.textContent = '-';
} else {
// 日度视图
yearDl.textContent = '-';
}
}
// 渲染Chart.js图表
function renderEnhancedChart(data) {
const ctx = document.getElementById('downloadChartCanvas');
if (!ctx) return;
const labels = data.data.map(item => item.label);
const values = data.data.map(item => item.value);
// 如果已有图表实例,先销毁
if (downloadChart) {
downloadChart.destroy();
}
// 主题颜色
const primaryColor = '#e94560';
const primaryColorRgba = 'rgba(233, 69, 96, 0.1)';
downloadChart = new Chart(ctx, {
type: currentChartType,
data: {
labels: labels,
datasets: [{
label: '下载量',
data: values,
backgroundColor: currentChartType === 'bar' ? primaryColor : primaryColorRgba,
borderColor: primaryColor,
borderWidth: 2,
fill: currentChartType === 'line',
tension: 0.4,
pointBackgroundColor: primaryColor,
pointBorderColor: '#fff',
pointBorderWidth: 2,
pointRadius: 4,
pointHoverRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: 'rgba(26, 26, 46, 0.95)',
titleColor: '#fff',
bodyColor: '#aaa',
borderColor: primaryColor,
borderWidth: 1,
padding: 12,
displayColors: false,
callbacks: {
title: function(context) {
return context[0].label;
},
label: function(context) {
return '下载次数: ' + context.parsed.y;
}
}
}
},
scales: {
x: {
grid: {
color: 'rgba(255, 255, 255, 0.05)'
},
ticks: {
color: '#888',
maxRotation: 45,
minRotation: 0
}
},
y: {
beginAtZero: true,
grid: {
color: 'rgba(255, 255, 255, 0.05)'
},
ticks: {
color: '#888',
precision: 0
}
}
},
interaction: {
intersect: false,
mode: 'index'
}
}
});
}
async function loadDownloads() {
const stats = await apiRequest('/api/v1/stats');
if (stats) {
// 核心指标
document.getElementById('todayDl').textContent = formatNum(stats.downloads_today || 0);
document.getElementById('weekDl').textContent = formatNum(stats.downloads_week || 0);
// yearDl 由图表加载后更新(按年统计时显示年度总数)
document.getElementById('totalDl').textContent = formatNum(stats.total_downloads || 0);
} else {
console.warn('API返回为空');
}
// 初始化选择器
initDownloadStatsSelectors();
// 加载图表数据
await loadDownloadChartData();
// 加载下载排行
const rankData = await apiRequest('/api/v2/stats/rank?limit=20');
const rankList = document.getElementById('downloadRank');
if (rankData?.rank?.length > 0) {
rankList.innerHTML = rankData.rank.map(item => {
const sizeStr = formatSize(item.size || 0);
return `<tr>
<td style="text-align: center;"><span class="badge badge-${item.rank <= 3 ? 'warning' : 'info'}">#${item.rank}</span></td>
<td style="max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${item.file || '-'}">${item.file || '-'}</td>
<td style="text-align: center;">${item.downloads || 0}</td>
<td style="text-align: center;">${sizeStr}</td>
<td style="text-align: center;">
<div class="progress" style="width: 60px; height: 6px; margin: 0 auto;">
<div class="progress-bar" style="width: ${item.percentage || 0}%"></div>
</div>
<span style="font-size: 11px; color: var(--text-muted);">${item.percentage || 0}%</span>
</td>
</tr>`;
}).join('');
} else {
rankList.innerHTML = '<tr><td colspan="5" class="empty">暂无下载排行数据</td></tr>';
}
}
// ============ 系统设置 ============
let settingsConfig = null;
async function loadSettings() {
// 使用 v2 API 获取完整的配置文件
let config = {};
try {
const v2Config = await apiRequest('/api/v2/config');
if (v2Config && v2Config.success && v2Config.config) {
config = JSON.parse(v2Config.config);
}
} catch (e) {
console.warn('无法从 v2 API 获取配置,回退到 v1:', e);
config = await apiRequest('/api/v1/config');
}
const serverInfo = await apiRequest('/api/v1/health');
settingsConfig = config || {};
if (config && Object.keys(config).length > 0) {
// 安全设置
document.getElementById('ipWhitelist').value = (config.ip_whitelist || []).join('\n');
document.getElementById('ipBlacklist').value = (config.ip_blacklist || []).join('\n');
document.getElementById('rateLimit').value = config.rate_limit || 100;
document.getElementById('authType').value = config.auth_type || 'none';
document.getElementById('sessionTimeout').value = config.session_timeout || 60;
// 存储设置
document.getElementById('storagePath').value = config.base_dir || './downloads';
document.getElementById('serverName').value = config.server_name || 'HYC下载站';
// 数据库设置
const dbConfig = config.database || {};
document.getElementById('dbType').value = dbConfig.type || 'sqlite';
// SQLite
const sqliteConfig = dbConfig.sqlite || {};
document.getElementById('dbPath').value = sqliteConfig.path || './data/hyc.db';
// PostgreSQL
const pgConfig = dbConfig.postgresql || {};
document.getElementById('dbHost').value = pgConfig.host || 'localhost';
document.getElementById('dbPort').value = pgConfig.port || 5432;
document.getElementById('dbUser').value = pgConfig.user || 'postgres';
document.getElementById('dbPassword').value = pgConfig.password || '';
document.getElementById('dbName').value = pgConfig.database || 'hyc';
// MySQL
const mysqlConfig = dbConfig.mysql || {};
document.getElementById('mysqlHost').value = mysqlConfig.host || 'localhost';
document.getElementById('mysqlPort').value = mysqlConfig.port || 3306;
document.getElementById('mysqlUser').value = mysqlConfig.user || 'root';
document.getElementById('mysqlPassword').value = mysqlConfig.password || '';
document.getElementById('mysqlName').value = mysqlConfig.database || 'hyc';
// 切换数据库配置UI
switchDbType();
// 同步设置
document.getElementById('syncInterval').value = config.sync_interval || 60;
document.getElementById('autoSync').checked = config.auto_sync || false;
document.getElementById('autoClean').checked = config.auto_clean || false;
document.getElementById('cleanInterval').value = config.clean_interval || 7;
// 功能开关
document.getElementById('enableBrowse').checked = config.enable_browse !== false;
document.getElementById('showHidden').checked = config.show_hidden || false;
document.getElementById('showHash').checked = config.show_hash !== false;
document.getElementById('enableWebSocket').checked = config.enable_websocket !== false;
document.getElementById('enableSSE').checked = config.enable_sse !== false;
document.getElementById('enableMonitor').checked = config.enable_monitor !== false;
// 服务器配置
document.getElementById('serverHost').value = config.host || '0.0.0.0';
document.getElementById('serverPort').value = config.port || 8080;
document.getElementById('apiVersion').value = config.api_version || 'v1';
// 排序设置
document.getElementById('sortBy').value = config.sort_by || 'name';
document.getElementById('sortReverse').checked = config.sort_reverse || false;
// 搜索设置
document.getElementById('maxSearchResults').value = config.max_search_results || 100;
document.getElementById('calculateHash').checked = config.calculate_hash || false;
// SSL 设置
const sslConfig = config.ssl || {};
document.getElementById('enableSSL').checked = sslConfig.enabled || false;
document.getElementById('sslCert').value = sslConfig.cert || '';
document.getElementById('sslKey').value = sslConfig.key || '';
// 健康检查
const healthConfig = config.health_check || {};
document.getElementById('enableHealthCheck').checked = healthConfig.enabled !== false;
document.getElementById('healthCheckInterval').value = healthConfig.interval || 60;
document.getElementById('healthCheckTimeout').value = healthConfig.timeout || 10;
document.getElementById('healthCheckRetries').value = healthConfig.max_retries || 3;
// 故障转移
const failoverConfig = config.failover || {};
document.getElementById('enableFailover').checked = failoverConfig.enabled || false;
document.getElementById('failoverThreshold').value = failoverConfig.threshold || 3;
// 性能设置
document.getElementById('maxThreads').value = config.max_threads || 4;
document.getElementById('monitorInterval').value = config.monitor_interval || 3;
document.getElementById('healthInterval').value = config.health_check_interval || 30;
document.getElementById('maxUploadSize').value = config.max_upload_size ? Math.round(config.max_upload_size / (1024*1024)) : 1024;
document.getElementById('requestTimeout').value = config.request_timeout || 300;
// 告警设置
document.getElementById('enableAlerts').checked = config.enable_alerts !== false;
document.getElementById('diskWarning').value = config.disk_warning_threshold || 80;
document.getElementById('diskCritical').value = config.disk_critical_threshold || 90;
document.getElementById('smtpServer').value = config.smtp_server || '';
document.getElementById('smtpPort').value = config.smtp_port || 587;
document.getElementById('smtpFrom').value = config.smtp_from || '';
document.getElementById('alertEmail').value = config.alert_email || '';
// Webhook 设置
const webhooksConfig = config.webhooks || {};
document.getElementById('enableWebhook').checked = webhooksConfig.enabled || false;
document.getElementById('webhookUrl').value = webhooksConfig.url || '';
// 定时任务
const tasksConfig = config.scheduled_tasks || {};
const cleanupTask = tasksConfig.cleanup_db || {};
document.getElementById('enableCleanupTask').checked = cleanupTask.enabled || false;
document.getElementById('cleanupCron').value = cleanupTask.cron || '0 2 * * *';
// 日志/调试设置
document.getElementById('debugMode').checked = config.debug || false;
document.getElementById('debugHttp').checked = config.debug_http || false;
document.getElementById('debugAuth').checked = config.debug_auth || false;
document.getElementById('logFile').value = config.log_file || './logs/hyc.log';
document.getElementById('logLevel').value = config.log_level || 'INFO';
// 安全选项
document.getElementById('enableRange').checked = config.enable_range !== false;
document.getElementById('enableStats').checked = config.enable_stats !== false;
// 缓存设置
document.getElementById('cacheSize').value = config.cache_size ? Math.round(config.cache_size / (1024*1024)) : 1024;
document.getElementById('cacheTTL').value = config.cache_ttl || 3600;
// 连接池设置
document.getElementById('poolSize').value = config.pool_size || 5;
document.getElementById('maxOverflow').value = config.max_overflow || 10;
}
if (serverInfo?.server?.uptime) {
document.getElementById('serverUptime').textContent = formatUptime(serverInfo.server.uptime);
}
// 版本号从 config 获取
if (config?.version) {
document.getElementById('serverVersion').textContent = config.version;
}
}
// 修改密码
async function changePassword() {
// 优先使用输入框中的用户名,如果没有则使用保存的登录用户名
const savedUsername = localStorage.getItem('hyc_username') || '';
let username = document.getElementById('changePasswordUsername').value.trim();
if (!username && savedUsername) {
username = savedUsername;
document.getElementById('changePasswordUsername').value = username;
}
const oldPassword = document.getElementById('changePasswordOld').value;
const newPassword = document.getElementById('changePasswordNew').value;
const confirmPassword = document.getElementById('changePasswordConfirm').value;
if (!username || !newPassword) {
showNotification('用户名和新密码不能为空', 'error');
return;
}
if (newPassword !== confirmPassword) {
showNotification('两次输入的密码不一致', 'error');
return;
}
if (newPassword.length < 4) {
showNotification('密码长度至少4位', 'error');
return;
}
try {
const response = await apiRequest('/api/v2/user/password', {
method: 'POST',
body: JSON.stringify({
username: username,
old_password: oldPassword,
new_password: newPassword
})
});
if (response.success) {
showNotification('密码修改成功', 'success');
// 清空表单
document.getElementById('changePasswordOld').value = '';
document.getElementById('changePasswordNew').value = '';
document.getElementById('changePasswordConfirm').value = '';
} else {
showNotification(response.error || '密码修改失败', 'error');
}
} catch (e) {
showNotification('密码修改失败: ' + e.message, 'error');
}
}
// 加载登录日志
async function loadLoginLogs() {
try {
const response = await apiRequest('/api/v2/user/login-logs?limit=50');
const logsTable = document.getElementById('loginLogsTable');
if (!response.success || !response.logs || response.logs.length === 0) {
logsTable.innerHTML = '<tr><td colspan="5" class="empty">暂无登录记录</td></tr>';
return;
}
logsTable.innerHTML = response.logs.map(log => {
const time = log.created_at ? new Date(log.created_at * 1000).toLocaleString() : '-';
const statusClass = log.status === 'success' ? 'badge-success' : (log.status === 'locked' ? 'badge-danger' : 'badge-warning');
const statusText = log.status === 'success' ? '成功' : (log.status === 'locked' ? '锁定' : '失败');
return `<tr>
<td>${time}</td>
<td>${log.username || '-'}</td>
<td><span class="badge ${statusClass}">${statusText}</span></td>
<td>${log.ip_address || '-'}</td>
<td>${log.reason || '-'}</td>
</tr>`;
}).join('');
} catch (e) {
console.error('加载登录日志失败:', e);
}
}
async function saveSettings() {
if (!settingsConfig) {
showNotification('无法加载配置', 'error');
return;
}
// 保存前确认
if (!confirm('确定要保存所有设置吗?')) {
return;
}
// 验证数值范围
const rateLimit = parseInt(document.getElementById('rateLimit').value) || 100;
const sessionTimeout = parseInt(document.getElementById('sessionTimeout').value) || 60;
const syncInterval = parseInt(document.getElementById('syncInterval').value) || 60;
const cleanInterval = parseInt(document.getElementById('cleanInterval').value) || 7;
const maxThreads = parseInt(document.getElementById('maxThreads').value) || 4;
const monitorInterval = parseInt(document.getElementById('monitorInterval').value) || 3;
const maxUploadSize = parseInt(document.getElementById('maxUploadSize').value) || 1024;
const requestTimeout = parseInt(document.getElementById('requestTimeout').value) || 300;
const diskWarning = parseInt(document.getElementById('diskWarning').value) || 80;
const diskCritical = parseInt(document.getElementById('diskCritical').value) || 90;
// 基本验证
if (rateLimit < 1 || rateLimit > 10000) {
showNotification('请求速率限制应在 1-10000 之间', 'warning');
return;
}
if (sessionTimeout < 1 || sessionTimeout > 10080) {
showNotification('会话超时应在 1-10080 分钟之间', 'warning');
return;
}
if (syncInterval < 1 || syncInterval > 10080) {
showNotification('同步间隔应在 1-10080 分钟之间', 'warning');
return;
}
if (maxThreads < 1 || maxThreads > 32) {
showNotification('最大线程数应在 1-32 之间', 'warning');
return;
}
if (diskWarning >= diskCritical) {
showNotification('警告阈值必须小于严重阈值', 'warning');
return;
}
// 收集所有设置
const updates = {
// 安全设置
ip_whitelist: document.getElementById('ipWhitelist').value.split('\n').filter(line => line.trim()),
ip_blacklist: document.getElementById('ipBlacklist').value.split('\n').filter(line => line.trim()),
rate_limit: rateLimit,
auth_type: document.getElementById('authType').value || 'none',
session_timeout: sessionTimeout,
// 存储设置
base_dir: document.getElementById('storagePath').value || './downloads',
server_name: document.getElementById('serverName').value || 'HYC下载站',
// 这里的 host/port/api_version 移到功能开关部分保存
// 数据库设置 - 需要嵌套在 database 对象中
database: {
type: document.getElementById('dbType').value || 'sqlite',
sqlite: {
path: document.getElementById('dbPath').value || './data/hyc.db'
},
postgresql: {
host: document.getElementById('dbHost').value || 'localhost',
port: parseInt(document.getElementById('dbPort').value) || 5432,
user: document.getElementById('dbUser').value || 'postgres',
password: document.getElementById('dbPassword').value || '',
database: document.getElementById('dbName').value || 'hyc'
},
mysql: {
host: document.getElementById('mysqlHost').value || 'localhost',
port: parseInt(document.getElementById('mysqlPort').value) || 3306,
user: document.getElementById('mysqlUser').value || 'root',
password: document.getElementById('mysqlPassword').value || '',
database: document.getElementById('mysqlName').value || 'hyc'
}
},
// 同步设置
sync_interval: syncInterval,
auto_sync: document.getElementById('autoSync').checked,
auto_clean: document.getElementById('autoClean').checked,
clean_interval: cleanInterval,
// 功能开关
enable_browse: document.getElementById('enableBrowse').checked,
show_hidden: document.getElementById('showHidden').checked,
show_hash: document.getElementById('showHash').checked,
enable_websocket: document.getElementById('enableWebSocket').checked,
enable_sse: document.getElementById('enableSSE').checked,
enable_monitor: document.getElementById('enableMonitor').checked,
// 服务器配置
host: document.getElementById('serverHost').value || '0.0.0.0',
port: parseInt(document.getElementById('serverPort').value) || 8080,
api_version: document.getElementById('apiVersion').value || 'v1',
// 排序设置
sort_by: document.getElementById('sortBy').value || 'name',
sort_reverse: document.getElementById('sortReverse').checked,
// 搜索设置
max_search_results: parseInt(document.getElementById('maxSearchResults').value) || 100,
calculate_hash: document.getElementById('calculateHash').checked,
// SSL 设置
ssl: {
enabled: document.getElementById('enableSSL').checked,
cert: document.getElementById('sslCert').value || null,
key: document.getElementById('sslKey').value || null
},
// 健康检查
health_check: {
enabled: document.getElementById('enableHealthCheck').checked,
interval: parseInt(document.getElementById('healthCheckInterval').value) || 60,
timeout: parseInt(document.getElementById('healthCheckTimeout').value) || 10,
max_retries: parseInt(document.getElementById('healthCheckRetries').value) || 3
},
// 故障转移
failover: {
enabled: document.getElementById('enableFailover').checked,
threshold: parseInt(document.getElementById('failoverThreshold').value) || 3
},
// 性能设置
max_threads: maxThreads,
monitor_interval: monitorInterval,
health_check_interval: parseInt(document.getElementById('healthInterval').value) || 30,
max_upload_size: maxUploadSize * 1024 * 1024,
request_timeout: requestTimeout,
// 告警设置
enable_alerts: document.getElementById('enableAlerts').checked,
disk_warning_threshold: diskWarning,
disk_critical_threshold: diskCritical,
smtp_server: document.getElementById('smtpServer').value || '',
smtp_port: parseInt(document.getElementById('smtpPort').value) || 587,
smtp_from: document.getElementById('smtpFrom').value || '',
alert_email: document.getElementById('alertEmail').value || '',
// Webhook 设置
webhooks: {
enabled: document.getElementById('enableWebhook').checked,
url: document.getElementById('webhookUrl').value || ''
},
// 定时任务
scheduled_tasks: {
cleanup_db: {
type: 'cron',
config: {
cron: document.getElementById('cleanupCron').value || '0 2 * * *',
enabled: document.getElementById('enableCleanupTask').checked
}
}
},
// 日志/调试设置
debug: document.getElementById('debugMode').checked,
debug_http: document.getElementById('debugHttp').checked,
debug_auth: document.getElementById('debugAuth').checked,
log_file: document.getElementById('logFile').value || './logs/hyc.log',
log_level: document.getElementById('logLevel').value || 'INFO',
// 安全选项
enable_range: document.getElementById('enableRange').checked,
enable_stats: document.getElementById('enableStats').checked,
// 缓存设置
cache_size: parseInt(document.getElementById('cacheSize').value || 1024) * 1024 * 1024,
cache_ttl: parseInt(document.getElementById('cacheTTL').value) || 3600,
// 连接池设置
pool_size: parseInt(document.getElementById('poolSize').value) || 5,
max_overflow: parseInt(document.getElementById('maxOverflow').value) || 10
};
// 合并更新到配置
const newConfig = { ...settingsConfig, ...updates };
try {
const result = await apiRequest('/api/v2/config', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newConfig, null, 2)
});
if (result.success) {
showNotification('设置已保存成功', 'success');
settingsConfig = newConfig;
} else {
showNotification('保存失败: ' + (result.error || '未知错误'), 'error');
}
} catch (e) {
// 如果 PUT /api/v2/config 不可用,尝试使用其他方式
console.warn('保存配置失败,尝试使用备用方式:', e);
showNotification('配置已暂存,需要重新加载配置生效', 'info');
}
}
// ============ 工具函数 ============
function formatSize(bytes) {
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let i = 0;
while (bytes >= 1024 && i < units.length - 1) { bytes /= 1024; i++; }
return bytes.toFixed(1) + ' ' + units[i];
}
function formatUptime(seconds) {
if (!seconds) return '-';
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
let result = '';
if (days > 0) result += days + '天 ';
if (hours > 0) result += hours + '时 ';
if (mins > 0) result += mins + '分 ';
result += secs + '秒';
return result;
}
function formatNum(n) {
return n?.toLocaleString() || '0';
}
function getFileIcon(filename) {
const ext = filename.split('.').pop().toLowerCase();
const icons = {
// 压缩文件
zip: '<i class="fa-solid fa-file-zipper"></i>',
tar: '<i class="fa-solid fa-file-zipper"></i>',
gz: '<i class="fa-solid fa-file-zipper"></i>',
rar: '<i class="fa-solid fa-file-zipper"></i>',
'7z': '<i class="fa-solid fa-file-zipper"></i>',
iso: '<i class="fa-solid fa-compact-disc"></i>',
// 文档
pdf: '<i class="fa-solid fa-file-pdf"></i>',
doc: '<i class="fa-solid fa-file-word"></i>',
docx: '<i class="fa-solid fa-file-word"></i>',
xls: '<i class="fa-solid fa-file-excel"></i>',
xlsx: '<i class="fa-solid fa-file-excel"></i>',
ppt: '<i class="fa-solid fa-file-powerpoint"></i>',
pptx: '<i class="fa-solid fa-file-powerpoint"></i>',
// 代码/文本
txt: '<i class="fa-solid fa-file-lines"></i>',
md: '<i class="fa-brands fa-markdown"></i>',
json: '<i class="fa-solid fa-code"></i>',
xml: '<i class="fa-solid fa-code"></i>',
html: '<i class="fa-brands fa-html5"></i>',
htm: '<i class="fa-brands fa-html5"></i>',
css: '<i class="fa-brands fa-css3"></i>',
scss: '<i class="fa-brands fa-sass"></i>',
js: '<i class="fa-brands fa-js"></i>',
ts: '<i class="fa-brands fa-js"></i>',
py: '<i class="fa-brands fa-python"></i>',
java: '<i class="fa-brands fa-java"></i>',
c: '<i class="fa-solid fa-c"></i>',
cpp: '<i class="fa-solid fa-c"></i>',
h: '<i class="fa-solid fa-c"></i>',
cs: '<i class="fa-solid fa-c"></i>',
go: '<i class="fa-brands fa-golang"></i>',
rs: '<i class="fa-brands fa-rust"></i>',
rb: '<i class="fa-solid fa-gem"></i>',
php: '<i class="fa-brands fa-php"></i>',
swift: '<i class="fa-brands fa-swift"></i>',
kt: '<i class="fa-solid fa-k"></i>',
// 图片
img: '<i class="fa-solid fa-image"></i>',
jpg: '<i class="fa-solid fa-image"></i>',
jpeg: '<i class="fa-solid fa-image"></i>',
png: '<i class="fa-solid fa-image"></i>',
gif: '<i class="fa-solid fa-image"></i>',
bmp: '<i class="fa-solid fa-image"></i>',
svg: '<i class="fa-solid fa-vector-square"></i>',
ico: '<i class="fa-solid fa-image"></i>',
webp: '<i class="fa-solid fa-image"></i>',
// 音视频
mp4: '<i class="fa-solid fa-video"></i>',
avi: '<i class="fa-solid fa-video"></i>',
mkv: '<i class="fa-solid fa-video"></i>',
mov: '<i class="fa-solid fa-video"></i>',
mp3: '<i class="fa-solid fa-music"></i>',
wav: '<i class="fa-solid fa-music"></i>',
ogg: '<i class="fa-solid fa-music"></i>',
flac: '<i class="fa-solid fa-music"></i>',
// 可执行/系统
apk: '<i class="fa-solid fa-mobile-screen"></i>',
exe: '<i class="fa-solid fa-desktop"></i>',
sh: '<i class="fa-solid fa-terminal"></i>',
bat: '<i class="fa-solid fa-terminal"></i>',
cmd: '<i class="fa-solid fa-terminal"></i>',
// 数据库
sql: '<i class="fa-solid fa-database"></i>',
db: '<i class="fa-solid fa-database"></i>',
sqlite: '<i class="fa-solid fa-database"></i>',
mdb: '<i class="fa-solid fa-database"></i>',
// 配置
yaml: '<i class="fa-solid fa-gear"></i>',
yml: '<i class="fa-solid fa-gear"></i>',
ini: '<i class="fa-solid fa-gear"></i>',
conf: '<i class="fa-solid fa-gear"></i>',
config: '<i class="fa-solid fa-gear"></i>',
env: '<i class="fa-solid fa-gear"></i>',
toml: '<i class="fa-solid fa-gear"></i>',
// 其他
log: '<i class="fa-solid fa-clipboard-list"></i>',
csv: '<i class="fa-solid fa-file-csv"></i>',
dll: '<i class="fa-solid fa-gear"></i>',
so: '<i class="fa-solid fa-gear"></i>',
dylib: '<i class="fa-solid fa-gear"></i>',
tgz: '<i class="fa-solid fa-file-zipper"></i>',
xz: '<i class="fa-solid fa-file-zipper"></i>',
bak: '<i class="fa-solid fa-file-pen"></i>',
backup: '<i class="fa-solid fa-floppy-disk"></i>',
properties: '<i class="fa-solid fa-sliders"></i>',
gradle: '<i class="fa-brands fa-graduation-cap"></i>',
makefile: '<i class="fa-solid fa-hammer"></i>',
dockerfile: '<i class="fa-brands fa-docker"></i>',
nginx: '<i class="fa-solid fa-server"></i>',
apache: '<i class="fa-solid fa-server"></i>',
sql: '<i class="fa-solid fa-database"></i>',
};
return icons[ext] || '<i class="fa-regular fa-file"></i>';
}
// ============ 初始化 ============
// 页面加载完成后初始化
window.addEventListener('DOMContentLoaded', function() {
initApp();
});
</script>
</body>
</html>