5950 lines
301 KiB
HTML
5950 lines
301 KiB
HTML
<!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'">×</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()">×</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 192.168.1.0/24 10.0.0.0/8"></textarea>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>IP 黑名单 (每行一个)</label>
|
||
<textarea id="ipBlacklist" rows="5" placeholder="1.2.3.4 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>
|