- 修复添加账号按钮无反应问题
- 添加账号备注字段(可选)
- 添加账号设置按钮(修改密码/备注)
- 修复用户反馈���能
- 添加定时任务执行日志
- 修复容器重启后账号加载问题
- 修复所有JavaScript语法错误
- 优化账号加载机制(4层保障)
🤖 Generated with Claude Code
1856 lines
76 KiB
Plaintext
1856 lines
76 KiB
Plaintext
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||
<title>后台管理 v2.0 - 知识管理平台</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||
background: #f5f6fa;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
.header {
|
||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||
color: white;
|
||
padding: 15px;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.header-content {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.header h1 {
|
||
font-size: 18px;
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
}
|
||
|
||
.header-actions > span {
|
||
display: inline;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.btn {
|
||
padding: 6px 12px;
|
||
border: none;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
transition: all 0.2s;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.btn-logout {
|
||
background: rgba(255,255,255,0.2);
|
||
color: white;
|
||
border: 1px solid white;
|
||
}
|
||
|
||
.btn-logout:hover {
|
||
background: rgba(255,255,255,0.3);
|
||
}
|
||
|
||
.container {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
padding: 15px;
|
||
}
|
||
|
||
.stats-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 10px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.stat-card {
|
||
background: white;
|
||
padding: 15px 12px;
|
||
border-radius: 10px;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
||
text-align: center;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 28px;
|
||
font-weight: bold;
|
||
color: #f5576c;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.stat-label {
|
||
color: #666;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.panel {
|
||
background: white;
|
||
border-radius: 10px;
|
||
padding: 15px;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.panel-title {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
margin-bottom: 15px;
|
||
color: #333;
|
||
border-bottom: 2px solid #f5576c;
|
||
padding-bottom: 8px;
|
||
}
|
||
|
||
.tabs {
|
||
display: flex;
|
||
gap: 5px;
|
||
margin-bottom: 15px;
|
||
border-bottom: 1px solid #ddd;
|
||
overflow-x: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
|
||
.tab {
|
||
padding: 10px 15px;
|
||
background: none;
|
||
border: none;
|
||
border-bottom: 3px solid transparent;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
color: #666;
|
||
transition: all 0.3s;
|
||
white-space: nowrap;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.tab.active {
|
||
color: #f5576c;
|
||
border-bottom-color: #f5576c;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.tab:hover {
|
||
color: #f5576c;
|
||
}
|
||
|
||
.tab-content {
|
||
display: none;
|
||
}
|
||
|
||
.tab-content.active {
|
||
display: block;
|
||
}
|
||
|
||
.table-container {
|
||
overflow-x: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
min-width: 600px;
|
||
}
|
||
|
||
th {
|
||
background: #f8f9fa;
|
||
padding: 10px 8px;
|
||
text-align: left;
|
||
color: #666;
|
||
font-weight: bold;
|
||
font-size: 12px;
|
||
position: sticky;
|
||
top: 0;
|
||
}
|
||
|
||
td {
|
||
padding: 10px 8px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
font-size: 12px;
|
||
}
|
||
|
||
tr:hover {
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
.status-badge {
|
||
padding: 3px 10px;
|
||
border-radius: 12px;
|
||
font-size: 11px;
|
||
font-weight: bold;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.status-pending {
|
||
background: #fff3cd;
|
||
color: #856404;
|
||
}
|
||
|
||
.status-approved {
|
||
background: #d4edda;
|
||
color: #155724;
|
||
}
|
||
|
||
.status-rejected {
|
||
background: #f8d7da;
|
||
color: #721c24;
|
||
}
|
||
|
||
.btn-small {
|
||
padding: 5px 8px;
|
||
font-size: 11px;
|
||
margin: 2px;
|
||
display: inline-block;
|
||
}
|
||
|
||
.btn-success {
|
||
background: #28a745;
|
||
color: white;
|
||
}
|
||
|
||
.btn-danger {
|
||
background: #dc3545;
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: #f5576c;
|
||
color: white;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: #6c757d;
|
||
color: white;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 6px;
|
||
color: #333;
|
||
font-weight: bold;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.form-group input {
|
||
width: 100%;
|
||
max-width: 100%;
|
||
padding: 10px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 5px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.empty-message {
|
||
text-align: center;
|
||
padding: 30px 15px;
|
||
color: #999;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.notification {
|
||
position: fixed;
|
||
top: 15px;
|
||
right: 15px;
|
||
left: 15px;
|
||
padding: 12px 15px;
|
||
background: white;
|
||
border-radius: 5px;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||
display: none;
|
||
z-index: 1000;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.notification.success {
|
||
border-left: 4px solid #28a745;
|
||
}
|
||
|
||
.notification.error {
|
||
border-left: 4px solid #dc3545;
|
||
}
|
||
|
||
.user-info {
|
||
font-size: 11px;
|
||
color: #666;
|
||
margin-top: 3px;
|
||
}
|
||
|
||
.vip-badge-inline {
|
||
background: linear-gradient(135deg,#667eea 0%,#764ba2 100%);
|
||
color: white;
|
||
padding: 2px 8px;
|
||
border-radius: 10px;
|
||
font-size: 10px;
|
||
margin-left: 3px;
|
||
display: inline-block;
|
||
}
|
||
|
||
.normal-badge-inline {
|
||
background: #e0e0e0;
|
||
color: #666;
|
||
padding: 2px 8px;
|
||
border-radius: 10px;
|
||
font-size: 10px;
|
||
margin-left: 3px;
|
||
display: inline-block;
|
||
}
|
||
|
||
.action-buttons {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 3px;
|
||
}
|
||
|
||
/* 平板及以上屏幕 */
|
||
@media (min-width: 768px) {
|
||
.header {
|
||
padding: 20px 25px;
|
||
}
|
||
|
||
.header h1 {
|
||
font-size: 22px;
|
||
}
|
||
|
||
.header-actions > span {
|
||
display: inline;
|
||
}
|
||
|
||
.btn {
|
||
padding: 8px 14px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.container {
|
||
margin: 25px auto;
|
||
padding: 0 20px;
|
||
}
|
||
|
||
.stats-grid {
|
||
grid-template-columns: repeat(5, 1fr);
|
||
gap: 15px;
|
||
margin-bottom: 25px;
|
||
}
|
||
|
||
.stat-card {
|
||
padding: 20px 15px;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 32px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 13px;
|
||
}
|
||
|
||
.panel {
|
||
padding: 20px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.panel-title {
|
||
font-size: 18px;
|
||
margin-bottom: 18px;
|
||
}
|
||
|
||
.tabs {
|
||
gap: 8px;
|
||
margin-bottom: 18px;
|
||
}
|
||
|
||
.tab {
|
||
padding: 12px 20px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
table {
|
||
min-width: auto;
|
||
}
|
||
|
||
th {
|
||
padding: 12px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
td {
|
||
padding: 12px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.status-badge {
|
||
padding: 4px 12px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.btn-small {
|
||
padding: 6px 10px;
|
||
font-size: 12px;
|
||
margin-right: 3px;
|
||
}
|
||
|
||
.form-group label {
|
||
font-size: 14px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.form-group input {
|
||
max-width: 400px;
|
||
}
|
||
|
||
.notification {
|
||
right: 20px;
|
||
left: auto;
|
||
max-width: 400px;
|
||
padding: 15px 20px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.user-info {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.vip-badge-inline, .normal-badge-inline {
|
||
font-size: 11px;
|
||
padding: 3px 10px;
|
||
}
|
||
}
|
||
|
||
/* PC屏幕 */
|
||
@media (min-width: 1024px) {
|
||
.header h1 {
|
||
font-size: 24px;
|
||
}
|
||
|
||
.btn {
|
||
padding: 8px 16px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.stat-card {
|
||
padding: 25px;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 36px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.panel {
|
||
padding: 25px;
|
||
}
|
||
|
||
.panel-title {
|
||
font-size: 20px;
|
||
margin-bottom: 20px;
|
||
padding-bottom: 10px;
|
||
}
|
||
|
||
.tabs {
|
||
gap: 10px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.tab {
|
||
padding: 12px 24px;
|
||
font-size: 15px;
|
||
}
|
||
|
||
.btn-small {
|
||
padding: 6px 12px;
|
||
font-size: 12px;
|
||
margin-right: 5px;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="header">
|
||
<div class="header-content">
|
||
<h1>后台管理系统</h1>
|
||
<div class="header-actions">
|
||
<span>管理员:</span><span id="admin-username" style="font-weight: bold;"></span>
|
||
<button class="btn btn-logout" onclick="logout()">退出</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="container">
|
||
<!-- 统计面板 -->
|
||
<div class="stats-grid">
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="totalUsers">0</div>
|
||
<div class="stat-label">总用户数</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="approvedUsers">0</div>
|
||
<div class="stat-label">已审核</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="pendingUsers">0</div>
|
||
<div class="stat-label">待审核</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="totalAccounts">0</div>
|
||
<div class="stat-label">总账号数</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="vipUsers">0</div>
|
||
<div class="stat-label">VIP用户</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 主面板 -->
|
||
<div class="panel">
|
||
<div class="tabs">
|
||
<button class="tab active" onclick="switchTab('pending')">待审核</button>
|
||
<button class="tab" onclick="switchTab('all')">所有用户</button>
|
||
<button class="tab" onclick="switchTab('stats')">统计</button>
|
||
<button class="tab" onclick="switchTab('logs')">任务日志</button>
|
||
<button class="tab" onclick="switchTab('system')">系统配置</button>
|
||
<button class="tab" onclick="switchTab('settings')">设置</button>
|
||
</div>
|
||
|
||
<!-- 待审核用户 -->
|
||
<div id="tab-pending" class="tab-content active">
|
||
<h3 style="margin-bottom: 15px; font-size: 16px;">用户注册审核</h3>
|
||
<div id="pendingUsersList"></div>
|
||
|
||
<h3 style="margin-top: 30px; margin-bottom: 15px; font-size: 16px;">密码重置审核</h3>
|
||
<div id="passwordResetsList"></div>
|
||
</div>
|
||
|
||
<!-- 所有用户 -->
|
||
<div id="tab-all" class="tab-content">
|
||
<div id="allUsersList"></div>
|
||
</div>
|
||
|
||
<!-- 系统配置 -->
|
||
<div id="tab-system" class="tab-content">
|
||
<h3 style="margin-bottom: 15px; font-size: 16px;">系统并发配置</h3>
|
||
|
||
<div class="form-group">
|
||
<label>全局最大并发数 (1-20)</label>
|
||
<input type="number" id="maxConcurrent" min="1" max="20" value="2" style="max-width: 200px;">
|
||
<div style="font-size: 12px; color: #666; margin-top: 5px;">
|
||
说明:同时最多运行的账号数量。服务器内存1.7GB,建议设置2-3。每个浏览器约占用200MB内存。
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>单账号最大并发数 (1-5)</label>
|
||
<input type="number" id="maxConcurrentPerAccount" min="1" max="5" value="1" style="max-width: 200px;">
|
||
<div style="font-size: 12px; color: #666; margin-top: 5px;">
|
||
说明:单个账号同时最多运行的任务数量。建议设置1-2。
|
||
</div>
|
||
</div>
|
||
|
||
<button class="btn btn-primary" style="margin-top: 10px;" onclick="updateConcurrency()">保存并发配置</button>
|
||
|
||
<h3 style="margin: 25px 0 15px 0; font-size: 16px;">定时任务配置</h3>
|
||
|
||
<div class="form-group">
|
||
<label style="display: flex; align-items: center; gap: 10px;">
|
||
<input type="checkbox" id="scheduleEnabled" onchange="toggleSchedule(this.checked)" style="width: auto; max-width: none;">
|
||
启用定时任务
|
||
</label>
|
||
<div style="font-size: 12px; color: #666; margin-top: 5px;">
|
||
开启后,系统将在指定时间自动执行所有账号的浏览任务(不包含截图)
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group" id="scheduleTimeGroup" style="display: none;">
|
||
<label>执行时间</label>
|
||
<input type="time" id="scheduleTime" value="02:00" style="max-width: 200px;">
|
||
<div style="font-size: 12px; color: #666; margin-top: 5px;">
|
||
每天在此时间自动执行所有账号任务
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group" id="scheduleBrowseTypeGroup" style="display: none;">
|
||
<label>浏览类型</label>
|
||
<select id="scheduleBrowseType" style="max-width: 200px; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px;">
|
||
<option value="注册前未读">注册前未读</option>
|
||
<option value="应读" selected>应读</option>
|
||
<option value="未读">未读</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group" id="scheduleWeekdaysGroup" style="display: none;">
|
||
<label>执行日期</label>
|
||
<div style="display: flex; gap: 10px; flex-wrap: wrap; margin-top: 10px;">
|
||
<label style="display: flex; align-items: center; gap: 5px; cursor: pointer;">
|
||
<input type="checkbox" class="weekday-checkbox" value="1" checked>
|
||
<span>周一</span>
|
||
</label>
|
||
<label style="display: flex; align-items: center; gap: 5px; cursor: pointer;">
|
||
<input type="checkbox" class="weekday-checkbox" value="2" checked>
|
||
<span>周二</span>
|
||
</label>
|
||
<label style="display: flex; align-items: center; gap: 5px; cursor: pointer;">
|
||
<input type="checkbox" class="weekday-checkbox" value="3" checked>
|
||
<span>周三</span>
|
||
</label>
|
||
<label style="display: flex; align-items: center; gap: 5px; cursor: pointer;">
|
||
<input type="checkbox" class="weekday-checkbox" value="4" checked>
|
||
<span>周四</span>
|
||
</label>
|
||
<label style="display: flex; align-items: center; gap: 5px; cursor: pointer;">
|
||
<input type="checkbox" class="weekday-checkbox" value="5" checked>
|
||
<span>周五</span>
|
||
</label>
|
||
<label style="display: flex; align-items: center; gap: 5px; cursor: pointer;">
|
||
<input type="checkbox" class="weekday-checkbox" value="6" checked>
|
||
<span>周六</span>
|
||
</label>
|
||
<label style="display: flex; align-items: center; gap: 5px; cursor: pointer;">
|
||
<input type="checkbox" class="weekday-checkbox" value="7" checked>
|
||
<span>周日</span>
|
||
</label>
|
||
</div>
|
||
<div style="font-size: 12px; color: #666; margin-top: 5px;">
|
||
选择定时任务在哪些天执行
|
||
</div>
|
||
</div>
|
||
|
||
<div id="scheduleActions" style="margin-top: 15px; display: flex; gap: 10px;">
|
||
<button class="btn btn-primary" onclick="updateSchedule()">保存定时任务配置</button>
|
||
<button class="btn btn-success" onclick="executeScheduleNow()" style="background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);">
|
||
⚡ 立即执行
|
||
</button>
|
||
</div>
|
||
|
||
<!-- ========== 代理设置 ========== -->
|
||
<div style="border-top: 2px solid #f0f0f0; margin-top: 40px; padding-top: 25px; background-color: #fff9e6; padding: 20px; border-radius: 8px;">
|
||
<h3 style="margin-bottom: 15px; font-size: 16px;">🌐 代理设置</h3>
|
||
|
||
<div class="form-group">
|
||
<label style="display: flex; align-items: center; gap: 10px;">
|
||
<input type="checkbox" id="proxyEnabled" style="width: auto; max-width: none;">
|
||
启用IP代理
|
||
</label>
|
||
<div style="font-size: 12px; color: #666; margin-top: 5px;">
|
||
开启后,所有浏览任务将通过代理IP访问(失败自动重试3次)
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>代理API地址</label>
|
||
<input type="text" id="proxyApiUrl" placeholder="http://api.xydaili.net:2022/Tools/IP.ashx?..." style="width: 100%; max-width: 600px; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px;">
|
||
<div style="font-size: 11px; color: #666; margin-top: 5px;">
|
||
API应返回格式: <code>IP:PORT</code> (例如: 123.45.67.89:8888)
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>代理有效期(分钟)</label>
|
||
<input type="number" id="proxyExpireMinutes" min="1" max="60" value="3" style="max-width: 200px; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px;">
|
||
<div style="font-size: 11px; color: #666; margin-top: 5px;">
|
||
代理IP的有效使用时长,根据你的代理服务商设置
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display: flex; gap: 10px; margin-top: 15px;">
|
||
<button class="btn btn-primary" onclick="saveProxyConfig()">💾 保存代理配置</button>
|
||
<button class="btn btn-secondary" onclick="testProxy()">🧪 测试代理</button>
|
||
</div>
|
||
</div>
|
||
<!-- ========== 代理设置结束 ========== -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 统计 -->
|
||
<div id="tab-stats" class="tab-content">
|
||
<h3 style="margin-bottom: 15px; font-size: 16px;">服务器信息</h3>
|
||
|
||
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin-bottom: 25px;">
|
||
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||
<div style="font-size: 24px; font-weight: bold; color: #f5576c;" id="serverCpu">-</div>
|
||
<div style="font-size: 14px; color: #666; margin-top: 5px;">CPU使用率</div>
|
||
</div>
|
||
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||
<div style="font-size: 24px; font-weight: bold; color: #f093fb;" id="serverMemory">-</div>
|
||
<div style="font-size: 14px; color: #666; margin-top: 5px;">内存使用</div>
|
||
</div>
|
||
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||
<div style="font-size: 24px; font-weight: bold; color: #764ba2;" id="serverDisk">-</div>
|
||
<div style="font-size: 14px; color: #666; margin-top: 5px;">磁盘使用</div>
|
||
</div>
|
||
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||
<div style="font-size: 24px; font-weight: bold; color: #17a2b8;" id="serverUptime">-</div>
|
||
<div style="font-size: 14px; color: #666; margin-top: 5px;">运行时长</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<h3 style="margin: 25px 0 15px 0; font-size: 16px;">Docker容器状态</h3>
|
||
|
||
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin-bottom: 25px;">
|
||
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||
<div style="font-size: 20px; font-weight: bold; color: #28a745;" id="dockerContainerName">-</div>
|
||
<div style="font-size: 14px; color: #666; margin-top: 5px;">容器名称</div>
|
||
</div>
|
||
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||
<div style="font-size: 20px; font-weight: bold; color: #17a2b8;" id="dockerUptime">-</div>
|
||
<div style="font-size: 14px; color: #666; margin-top: 5px;">容器运行时间</div>
|
||
</div>
|
||
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||
<div style="font-size: 20px; font-weight: bold; color: #f093fb;" id="dockerMemory">-</div>
|
||
<div style="font-size: 14px; color: #666; margin-top: 5px;">容器内存使用</div>
|
||
</div>
|
||
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||
<div style="font-size: 20px; font-weight: bold;" id="dockerStatus" style="color: #28a745;">-</div>
|
||
<div style="font-size: 14px; color: #666; margin-top: 5px;">运行状态</div>
|
||
</div>
|
||
</div>
|
||
|
||
<h3 style="margin: 25px 0 15px 0; font-size: 16px;">当日任务统计</h3>
|
||
|
||
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin-bottom: 25px;">
|
||
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||
<div style="font-size: 32px; font-weight: bold; color: #28a745;" id="todaySuccessTasks">0</div>
|
||
<div style="font-size: 14px; color: #666; margin-top: 5px;">成功任务</div>
|
||
</div>
|
||
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||
<div style="font-size: 32px; font-weight: bold; color: #dc3545;" id="todayFailedTasks">0</div>
|
||
<div style="font-size: 14px; color: #666; margin-top: 5px;">失败任务</div>
|
||
</div>
|
||
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||
<div style="font-size: 32px; font-weight: bold; color: #007bff;" id="todayTotalItems">0</div>
|
||
<div style="font-size: 14px; color: #666; margin-top: 5px;">浏览内容数</div>
|
||
</div>
|
||
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||
<div style="font-size: 32px; font-weight: bold; color: #17a2b8;" id="todayTotalAttachments">0</div>
|
||
<div style="font-size: 14px; color: #666; margin-top: 5px;">查看附件数</div>
|
||
</div>
|
||
</div>
|
||
|
||
<h3 style="margin: 25px 0 15px 0; font-size: 16px;">历史累计统计</h3>
|
||
|
||
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px;">
|
||
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||
<div style="font-size: 32px; font-weight: bold; color: #28a745;" id="totalSuccessTasks">0</div>
|
||
<div style="font-size: 14px; color: #666; margin-top: 5px;">累计成功任务</div>
|
||
</div>
|
||
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||
<div style="font-size: 32px; font-weight: bold; color: #dc3545;" id="totalFailedTasks">0</div>
|
||
<div style="font-size: 14px; color: #666; margin-top: 5px;">累计失败任务</div>
|
||
</div>
|
||
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||
<div style="font-size: 32px; font-weight: bold; color: #007bff;" id="totalTotalItems">0</div>
|
||
<div style="font-size: 14px; color: #666; margin-top: 5px;">累计浏览内容</div>
|
||
</div>
|
||
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||
<div style="font-size: 32px; font-weight: bold; color: #17a2b8;" id="totalTotalAttachments">0</div>
|
||
<div style="font-size: 14px; color: #666; margin-top: 5px;">累计查看附件</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 任务日志 -->
|
||
<div id="tab-logs" class="tab-content">
|
||
<div style="margin-bottom: 15px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center;">
|
||
<input type="date" id="logDateFilter" style="padding: 8px; border: 1px solid #ddd; border-radius: 5px; font-size: 13px;">
|
||
<select id="logStatusFilter" style="padding: 8px; border: 1px solid #ddd; border-radius: 5px; font-size: 13px;">
|
||
<option value="">全部状态</option>
|
||
<option value="success">成功</option>
|
||
<option value="failed">失败</option>
|
||
</select>
|
||
<button class="btn btn-primary" onclick="loadTaskLogs()">筛选</button>
|
||
<button class="btn btn-secondary" onclick="clearOldLogs()">清理旧日志</button>
|
||
</div>
|
||
|
||
<div class="table-container">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>时间</th>
|
||
<th>用户</th>
|
||
<th>账号</th>
|
||
<th>浏览类型</th>
|
||
<th>状态</th>
|
||
<th>内容数/附件数</th>
|
||
<th>执行用时</th>
|
||
<th>失败原因</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="taskLogsList">
|
||
<tr><td colspan="8" class="empty-message">加载中...</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div style="margin-top: 15px; text-align: center;">
|
||
<button class="btn btn-secondary" onclick="loadMoreLogs()" id="loadMoreBtn">加载更多</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 系统设置 -->
|
||
<div id="tab-settings" class="tab-content">
|
||
<h3 style="margin-bottom: 15px; font-size: 16px;">管理员账号设置</h3>
|
||
|
||
<div class="form-group">
|
||
<label>修改管理员用户名</label>
|
||
<input type="text" id="newUsername" placeholder="输入新用户名">
|
||
<button class="btn btn-primary" style="margin-top: 10px;" onclick="updateUsername()">保存用户名</button>
|
||
</div>
|
||
|
||
<div class="form-group" style="margin-top: 25px;">
|
||
<label>修改管理员密码</label>
|
||
<input type="password" id="newPassword" placeholder="输入新密码">
|
||
<button class="btn btn-primary" style="margin-top: 10px;" onclick="updatePassword()">保存密码</button>
|
||
|
||
<h3 style="margin-top: 40px; margin-bottom: 15px; font-size: 16px;">系统管理</h3>
|
||
|
||
<div class="form-group">
|
||
<label>Docker容器管理</label>
|
||
<p style="color: #666; font-size: 13px; margin: 8px 0;">
|
||
重启容器可以清理僵尸进程、释放内存,建议每周重启一次
|
||
</p>
|
||
<button class="btn btn-danger" onclick="restartDocker()" style="margin-top: 10px;">
|
||
<span style="margin-right: 5px;">🔄</span>重启Docker容器
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 通知组件 -->
|
||
<div id="notification" class="notification"></div>
|
||
|
||
<script>
|
||
let allUsers = [];
|
||
let pendingUsers = [];
|
||
|
||
// 页面加载时初始化
|
||
window.addEventListener('load', () => {
|
||
loadStats();
|
||
loadPendingUsers();
|
||
loadAllUsers();
|
||
loadSystemConfig();
|
||
loadProxyConfig();
|
||
loadPasswordResets(); // 修复: 初始化时也加载密码重置申请
|
||
|
||
// 恢复上次的标签页
|
||
const lastTab = localStorage.getItem('admin_current_tab') || 'pending';
|
||
const tabButton = document.querySelector(`.tab[onclick*="${lastTab}"]`);
|
||
if (tabButton) {
|
||
tabButton.click();
|
||
}
|
||
});
|
||
|
||
// 切换标签
|
||
function switchTab(tabName) {
|
||
document.querySelectorAll('.tab').forEach(tab => {
|
||
tab.classList.remove('active');
|
||
});
|
||
event.target.classList.add('active');
|
||
|
||
document.querySelectorAll('.tab-content').forEach(content => {
|
||
content.classList.remove('active');
|
||
});
|
||
document.getElementById(`tab-${tabName}`).classList.add('active');
|
||
|
||
// 保存当前标签到localStorage
|
||
localStorage.setItem('admin_current_tab', tabName);
|
||
|
||
// 切换到统计标签时加载服务器信息和任务统计
|
||
if (tabName === 'stats') {
|
||
loadServerInfo();
|
||
loadDockerStats();
|
||
loadTaskStats();
|
||
}
|
||
|
||
// 切换到日志标签时加载任务日志
|
||
if (tabName === 'logs') {
|
||
loadTaskLogs();
|
||
}
|
||
}
|
||
|
||
// VIP functions
|
||
function isVip(user) {
|
||
if (!user.vip_expire_time) return false;
|
||
const expireTime = new Date(user.vip_expire_time);
|
||
return expireTime > new Date();
|
||
}
|
||
|
||
function getVipBadge(user) {
|
||
if (isVip(user)) {
|
||
return '<span class="vip-badge-inline">VIP</span>';
|
||
}
|
||
return '<span class="normal-badge-inline">普通</span>';
|
||
}
|
||
|
||
function getVipExpire(user) {
|
||
if (!isVip(user)) return '';
|
||
const expireTime = new Date(user.vip_expire_time);
|
||
const daysLeft = Math.ceil((expireTime - new Date()) / (1000*60*60*24));
|
||
if (user.vip_expire_time === '2099-12-31 23:59:59') {
|
||
return '<div class="user-info" style="color:#667eea;">永久VIP</div>';
|
||
}
|
||
return '<div class="user-info">到期: ' + user.vip_expire_time + ' (剩' + daysLeft + '天)</div>';
|
||
}
|
||
|
||
async function setVip(userId, days) {
|
||
const dayText = {7:'一周',30:'一个月',365:'一年',999999:'永久'}[days];
|
||
if (!confirm('确定要为该用户开通 ' + dayText + ' VIP吗?')) return;
|
||
try {
|
||
const response = await fetch('/yuyx/api/users/' + userId + '/vip', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({days: days})
|
||
});
|
||
const data = await response.json();
|
||
if (response.ok) {
|
||
showNotification(data.message, 'success');
|
||
loadAllUsers();
|
||
loadStats();
|
||
} else {
|
||
showNotification('设置失败: ' + data.error, 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('设置失败: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
async function removeVip(userId) {
|
||
if (!confirm('确定要移除该用户的VIP吗?')) return;
|
||
try {
|
||
const response = await fetch('/yuyx/api/users/' + userId + '/vip', {
|
||
method: 'DELETE'
|
||
});
|
||
const data = await response.json();
|
||
if (response.ok) {
|
||
showNotification(data.message, 'success');
|
||
loadAllUsers();
|
||
loadStats();
|
||
} else {
|
||
showNotification('移除失败: ' + data.error, 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('移除失败: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
async function loadStats() {
|
||
const response = await fetch('/yuyx/api/stats');
|
||
const stats = await response.json();
|
||
|
||
document.getElementById('totalUsers').textContent = stats.total_users;
|
||
document.getElementById('approvedUsers').textContent = stats.approved_users;
|
||
document.getElementById('pendingUsers').textContent = stats.pending_users;
|
||
document.getElementById('totalAccounts').textContent = stats.total_accounts;
|
||
document.getElementById('vipUsers').textContent = stats.vip_users || 0;
|
||
// 显示管理员用户名
|
||
if (stats.admin_username) {
|
||
document.getElementById('admin-username').textContent = stats.admin_username;
|
||
}
|
||
}
|
||
|
||
async function loadPendingUsers() {
|
||
const response = await fetch('/yuyx/api/users/pending');
|
||
pendingUsers = await response.json();
|
||
renderPendingUsers();
|
||
}
|
||
|
||
function renderPendingUsers() {
|
||
const container = document.getElementById('pendingUsersList');
|
||
|
||
if (pendingUsers.length === 0) {
|
||
container.innerHTML = '<div class="empty-message">暂无待审核用户</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = `
|
||
<div class="table-container">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>ID</th>
|
||
<th>用户名</th>
|
||
<th>邮箱</th>
|
||
<th>注册时间</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${pendingUsers.map(user => `
|
||
<tr>
|
||
<td>${user.id}</td>
|
||
<td>
|
||
<div><strong>${user.username}</strong> ${getVipBadge(user)}</div>
|
||
${getVipExpire(user)}
|
||
</td>
|
||
<td>${user.email || '-'}</td>
|
||
<td>${user.created_at}</td>
|
||
<td>
|
||
<div class="action-buttons">
|
||
<button class="btn btn-small btn-success" onclick="approveUser(${user.id})">通过</button>
|
||
<button class="btn btn-small btn-danger" onclick="rejectUser(${user.id})">拒绝</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
async function loadAllUsers() {
|
||
const response = await fetch('/yuyx/api/users');
|
||
allUsers = await response.json();
|
||
renderAllUsers();
|
||
}
|
||
|
||
function renderAllUsers() {
|
||
const container = document.getElementById('allUsersList');
|
||
|
||
if (allUsers.length === 0) {
|
||
container.innerHTML = '<div class="empty-message">暂无用户</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = `
|
||
<div class="table-container">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>ID</th>
|
||
<th>用户名</th>
|
||
<th>状态</th>
|
||
<th>注册时间</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${allUsers.map(user => `
|
||
<tr>
|
||
<td>${user.id}</td>
|
||
<td>
|
||
<div><strong>${user.username}</strong> ${getVipBadge(user)}</div>
|
||
${getVipExpire(user)}
|
||
${user.email ? '<div class="user-info">'+user.email+'</div>' : ''}
|
||
</td>
|
||
<td>
|
||
<span class="status-badge status-${user.status}">
|
||
${user.status === 'pending' ? '待审核' : user.status === 'approved' ? '已通过' : '已拒绝'}
|
||
</span>
|
||
</td>
|
||
<td>
|
||
${user.created_at}
|
||
${user.approved_at ? '<div class="user-info">审核:'+user.approved_at+'</div>' : ''}
|
||
</td>
|
||
<td>
|
||
<div class="action-buttons">
|
||
${user.status === 'pending' ? `
|
||
<button class="btn btn-small btn-success" onclick="approveUser(${user.id})">通过</button>
|
||
<button class="btn btn-small btn-danger" onclick="rejectUser(${user.id})">拒绝</button>
|
||
` : ''}
|
||
<button class="btn btn-small btn-secondary" onclick="deleteUser(${user.id})">删除</button>
|
||
${isVip(user) ?
|
||
`<button class="btn btn-small btn-danger" onclick="removeVip(${user.id})">移除VIP</button>` :
|
||
`<button class="btn btn-small btn-primary" onclick="setVip(${user.id}, 7)">一周</button>
|
||
<button class="btn btn-small btn-primary" onclick="setVip(${user.id}, 30)">一月</button>
|
||
<button class="btn btn-small btn-primary" onclick="setVip(${user.id}, 365)">一年</button>
|
||
<button class="btn btn-small btn-primary" onclick="setVip(${user.id}, 999999)">永久</button>`
|
||
}
|
||
<button class="btn btn-small btn-secondary" onclick="resetUserPassword(${user.id})">重置密码</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
async function approveUser(userId) {
|
||
if (!confirm('确定通过该用户的注册申请吗?')) return;
|
||
|
||
const response = await fetch(`/yuyx/api/users/${userId}/approve`, {
|
||
method: 'POST'
|
||
});
|
||
|
||
if (response.ok) {
|
||
showNotification('用户审核通过', 'success');
|
||
loadStats();
|
||
loadPendingUsers();
|
||
loadAllUsers();
|
||
} else {
|
||
showNotification('审核失败', 'error');
|
||
}
|
||
}
|
||
|
||
async function rejectUser(userId) {
|
||
if (!confirm('确定拒绝该用户的注册申请吗?')) return;
|
||
|
||
const response = await fetch(`/yuyx/api/users/${userId}/reject`, {
|
||
method: 'POST'
|
||
});
|
||
|
||
if (response.ok) {
|
||
showNotification('已拒绝用户', 'success');
|
||
loadStats();
|
||
loadPendingUsers();
|
||
loadAllUsers();
|
||
} else {
|
||
showNotification('操作失败', 'error');
|
||
}
|
||
}
|
||
|
||
async function deleteUser(userId) {
|
||
if (!confirm('确定删除该用户吗?此操作将删除该用户的所有数据,不可恢复!')) return;
|
||
|
||
const response = await fetch(`/yuyx/api/users/${userId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
if (response.ok) {
|
||
showNotification('用户已删除', 'success');
|
||
loadStats();
|
||
loadPendingUsers();
|
||
loadAllUsers();
|
||
} else {
|
||
showNotification('删除失败', 'error');
|
||
}
|
||
}
|
||
|
||
async function updateUsername() {
|
||
const newUsername = document.getElementById('newUsername').value.trim();
|
||
|
||
if (!newUsername) {
|
||
showNotification('请输入新用户名', 'error');
|
||
return;
|
||
}
|
||
|
||
if (!confirm(`确定将管理员用户名修改为 "${newUsername}" 吗?`)) return;
|
||
|
||
const response = await fetch('/yuyx/api/admin/username', {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({ new_username: newUsername })
|
||
});
|
||
|
||
if (response.ok) {
|
||
showNotification('用户名修改成功,请重新登录', 'success');
|
||
document.getElementById('newUsername').value = '';
|
||
setTimeout(() => {
|
||
logout();
|
||
}, 2000);
|
||
} else {
|
||
const data = await response.json();
|
||
showNotification(data.error || '修改失败', 'error');
|
||
}
|
||
}
|
||
|
||
async function updatePassword() {
|
||
const newPassword = document.getElementById('newPassword').value.trim();
|
||
|
||
if (!newPassword) {
|
||
showNotification('请输入新密码', 'error');
|
||
return;
|
||
}
|
||
|
||
if (newPassword.length < 6) {
|
||
showNotification('密码至少6个字符', 'error');
|
||
return;
|
||
}
|
||
|
||
if (!confirm('确定修改管理员密码吗?')) return;
|
||
|
||
const response = await fetch('/yuyx/api/admin/password', {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({ new_password: newPassword })
|
||
});
|
||
|
||
if (response.ok) {
|
||
showNotification('密码修改成功,请重新登录', 'success');
|
||
document.getElementById('newPassword').value = '';
|
||
setTimeout(() => {
|
||
logout();
|
||
}, 2000);
|
||
} else {
|
||
showNotification('修改失败', 'error');
|
||
}
|
||
}
|
||
|
||
async function restartDocker() {
|
||
if (\!confirm('确定要重启Docker容器吗?
|
||
|
||
重启后:
|
||
- 所有僵尸进程将被清理
|
||
- 内存使用将被优化
|
||
- 服务将在约10秒后恢复
|
||
|
||
请确认!')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/yuyx/api/docker/restart', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (response.ok && data.success) {
|
||
showNotification(data.message, 'success');
|
||
|
||
// 显示倒计时
|
||
let countdown = 10;
|
||
const interval = setInterval(() => {
|
||
countdown--;
|
||
if (countdown > 0) {
|
||
showNotification(\, 'success');
|
||
} else {
|
||
clearInterval(interval);
|
||
window.location.reload();
|
||
}
|
||
}, 1000);
|
||
} else {
|
||
showNotification(data.error || '重启失败', 'error');
|
||
}
|
||
} catch (error) {
|
||
console.error('重启容器失败:', error);
|
||
showNotification('重启失败: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
|
||
async function logout() {
|
||
const response = await fetch('/yuyx/api/logout', {
|
||
method: 'POST'
|
||
});
|
||
|
||
if (response.ok) {
|
||
window.location.href = '/yuyx';
|
||
}
|
||
}
|
||
|
||
function showNotification(message, type) {
|
||
const notification = document.getElementById('notification');
|
||
notification.textContent = message;
|
||
notification.className = `notification ${type}`;
|
||
notification.style.display = 'block';
|
||
|
||
setTimeout(() => {
|
||
notification.style.display = 'none';
|
||
}, 3000);
|
||
}
|
||
|
||
// 系统配置功能
|
||
async function loadSystemConfig() {
|
||
try {
|
||
const response = await fetch('/yuyx/api/system/config');
|
||
if (response.ok) {
|
||
const config = await response.json();
|
||
document.getElementById('maxConcurrent').value = config.max_concurrent_global || 2;
|
||
document.getElementById('maxConcurrentPerAccount').value = config.max_concurrent_per_account || 1;
|
||
document.getElementById('scheduleEnabled').checked = config.schedule_enabled === 1;
|
||
document.getElementById('scheduleTime').value = config.schedule_time || '02:00';
|
||
document.getElementById('scheduleBrowseType').value = config.schedule_browse_type || '应读';
|
||
|
||
// 加载星期选择
|
||
const weekdays = config.schedule_weekdays || '1,2,3,4,5,6,7';
|
||
const weekdayArray = weekdays.split(',').map(d => d.trim());
|
||
document.querySelectorAll('.weekday-checkbox').forEach(checkbox => {
|
||
checkbox.checked = weekdayArray.includes(checkbox.value);
|
||
});
|
||
|
||
// 显示/隐藏定时任务选项
|
||
toggleSchedule(config.schedule_enabled === 1);
|
||
}
|
||
} catch (error) {
|
||
console.error('加载系统配置失败:', error);
|
||
}
|
||
}
|
||
|
||
function toggleSchedule(enabled) {
|
||
const timeGroup = document.getElementById('scheduleTimeGroup');
|
||
const browseTypeGroup = document.getElementById('scheduleBrowseTypeGroup');
|
||
const weekdaysGroup = document.getElementById('scheduleWeekdaysGroup');
|
||
|
||
if (enabled) {
|
||
timeGroup.style.display = 'block';
|
||
browseTypeGroup.style.display = 'block';
|
||
weekdaysGroup.style.display = 'block';
|
||
} else {
|
||
timeGroup.style.display = 'none';
|
||
browseTypeGroup.style.display = 'none';
|
||
weekdaysGroup.style.display = 'none';
|
||
}
|
||
// 保存按钮始终显示,无论是开启还是关闭定时任务
|
||
}
|
||
|
||
|
||
// ==================== 代理配置功能 ====================
|
||
|
||
async function loadProxyConfig() {
|
||
try {
|
||
const response = await fetch('/yuyx/api/proxy/config');
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
document.getElementById('proxyEnabled').checked = data.proxy_enabled === 1;
|
||
document.getElementById('proxyApiUrl').value = data.proxy_api_url || '';
|
||
document.getElementById('proxyExpireMinutes').value = data.proxy_expire_minutes || 3;
|
||
}
|
||
} catch (error) {
|
||
console.error('加载代理配置失败:', error);
|
||
}
|
||
}
|
||
|
||
async function saveProxyConfig() {
|
||
const proxyEnabled = document.getElementById('proxyEnabled').checked ? 1 : 0;
|
||
const proxyApiUrl = document.getElementById('proxyApiUrl').value.trim();
|
||
const proxyExpireMinutes = parseInt(document.getElementById('proxyExpireMinutes').value) || 3;
|
||
|
||
if (proxyEnabled && !proxyApiUrl) {
|
||
alert('❌ 启用代理时,API地址不能为空');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/yuyx/api/proxy/config', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
proxy_enabled: proxyEnabled,
|
||
proxy_api_url: proxyApiUrl,
|
||
proxy_expire_minutes: proxyExpireMinutes
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (data.message) {
|
||
showNotification('✓ ' + data.message, 'success');
|
||
loadProxyConfig();
|
||
} else if (data.error) {
|
||
showNotification('✗ ' + data.error, 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('保存失败: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
async function testProxy() {
|
||
const apiUrl = document.getElementById('proxyApiUrl').value.trim();
|
||
|
||
if (!apiUrl) {
|
||
alert('❌ 请先输入代理API地址');
|
||
return;
|
||
}
|
||
|
||
const button = event.target;
|
||
const originalText = button.textContent;
|
||
button.textContent = '⏳ 测试中...';
|
||
button.disabled = true;
|
||
|
||
try {
|
||
const response = await fetch('/yuyx/api/proxy/test', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ api_url: apiUrl })
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
alert(`✅ ${data.message}\n\n获取到的代理: ${data.proxy}`);
|
||
} else {
|
||
alert(`❌ ${data.message}`);
|
||
}
|
||
} catch (error) {
|
||
alert('❌ 测试失败: ' + error.message);
|
||
} finally {
|
||
button.textContent = originalText;
|
||
button.disabled = false;
|
||
}
|
||
}
|
||
async function updateConcurrency() {
|
||
const maxConcurrent = parseInt(document.getElementById('maxConcurrent').value);
|
||
const maxConcurrentPerAccount = parseInt(document.getElementById('maxConcurrentPerAccount').value);
|
||
|
||
if (maxConcurrent < 1 || maxConcurrent > 20) {
|
||
showNotification('全局并发数必须在1-20之间', 'error');
|
||
return;
|
||
}
|
||
|
||
if (maxConcurrentPerAccount < 1 || maxConcurrentPerAccount > 5) {
|
||
showNotification('单账号并发数必须在1-5之间', 'error');
|
||
return;
|
||
}
|
||
|
||
if (!confirm(`确定更新并发配置吗?\n\n全局并发数: ${maxConcurrent}\n单账号并发数: ${maxConcurrentPerAccount}\n\n建议值:服务器内存1.7GB时全局设置2-3,单账号设置1-2`)) return;
|
||
|
||
try {
|
||
const response = await fetch('/yuyx/api/system/config', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
max_concurrent_global: maxConcurrent,
|
||
max_concurrent_per_account: maxConcurrentPerAccount
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (response.ok) {
|
||
showNotification('并发配置已更新,将在下次任务启动时生效', 'success');
|
||
} else {
|
||
showNotification(data.error || '更新失败', 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('更新失败: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
async function updateSchedule() {
|
||
const enabled = document.getElementById('scheduleEnabled').checked;
|
||
const time = document.getElementById('scheduleTime').value;
|
||
const browseType = document.getElementById('scheduleBrowseType').value;
|
||
|
||
// 获取选中的星期
|
||
const selectedWeekdays = [];
|
||
document.querySelectorAll('.weekday-checkbox:checked').forEach(checkbox => {
|
||
selectedWeekdays.push(checkbox.value);
|
||
});
|
||
|
||
if (enabled && selectedWeekdays.length === 0) {
|
||
showNotification('请至少选择一个执行日期', 'error');
|
||
return;
|
||
}
|
||
|
||
const weekdaysStr = selectedWeekdays.join(',');
|
||
const weekdayNames = ['', '周一', '周二', '周三', '周四', '周五', '周六', '周日'];
|
||
const weekdayDisplay = selectedWeekdays.map(d => weekdayNames[parseInt(d)]).join('、');
|
||
|
||
const message = enabled
|
||
? `确定启用定时任务吗?\n\n执行时间: 每天 ${time}\n执行日期: ${weekdayDisplay}\n浏览类型: ${browseType}\n\n系统将自动执行所有账号的浏览任务(不包含截图)`
|
||
: `确定关闭定时任务吗?`;
|
||
|
||
if (!confirm(message)) return;
|
||
|
||
try {
|
||
const response = await fetch('/yuyx/api/system/config', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
schedule_enabled: enabled ? 1 : 0,
|
||
schedule_time: time,
|
||
schedule_browse_type: browseType,
|
||
schedule_weekdays: weekdaysStr
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (response.ok) {
|
||
showNotification(enabled ? '定时任务已启用' : '定时任务已关闭', 'success');
|
||
} else {
|
||
showNotification(data.error || '更新失败', 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('更新失败: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
// 立即执行定时任务
|
||
async function executeScheduleNow() {
|
||
const browseType = document.getElementById('scheduleBrowseType').value;
|
||
const message = `确定要立即执行定时任务吗?\n\n这将执行所有账号的浏览任务\n浏览类型: ${browseType}\n\n注意:无视定时时间和执行日期配置,立即开始执行!`;
|
||
|
||
if (!confirm(message)) return;
|
||
|
||
try {
|
||
// 禁用按钮,防止重复点击
|
||
const button = event.target;
|
||
button.disabled = true;
|
||
button.textContent = '⏳ 执行中...';
|
||
|
||
const response = await fetch('/yuyx/api/schedule/execute', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' }
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (response.ok) {
|
||
showNotification(data.message || '定时任务已开始执行', 'success');
|
||
} else {
|
||
showNotification(data.error || '执行失败', 'error');
|
||
}
|
||
|
||
// 恢复按钮状态
|
||
setTimeout(() => {
|
||
button.disabled = false;
|
||
button.textContent = '⚡ 立即执行';
|
||
}, 3000);
|
||
} catch (error) {
|
||
showNotification('执行失败: ' + error.message, 'error');
|
||
// 恢复按钮状态
|
||
setTimeout(() => {
|
||
button.disabled = false;
|
||
button.textContent = '⚡ 立即执行';
|
||
}, 3000);
|
||
}
|
||
}
|
||
|
||
// 任务统计和日志功能
|
||
let logsOffset = 0;
|
||
const logsLimit = 50;
|
||
|
||
async function loadServerInfo() {
|
||
try {
|
||
const response = await fetch('/yuyx/api/server/info');
|
||
if (response.ok) {
|
||
const info = await response.json();
|
||
|
||
// 更新服务器信息
|
||
document.getElementById('serverCpu').textContent = info.cpu_percent + '%';
|
||
document.getElementById('serverMemory').textContent = info.memory_used + ' / ' + info.memory_total;
|
||
document.getElementById('serverDisk').textContent = info.disk_used + ' / ' + info.disk_total;
|
||
document.getElementById('serverUptime').textContent = info.uptime;
|
||
}
|
||
} catch (error) {
|
||
console.error('加载服务器信息失败:', error);
|
||
}
|
||
}
|
||
|
||
async function loadDockerStats() {
|
||
try {
|
||
const response = await fetch('/yuyx/api/docker_stats');
|
||
if (response.ok) {
|
||
const docker = await response.json();
|
||
|
||
// 更新Docker信息
|
||
document.getElementById('dockerContainerName').textContent = docker.container_name || 'N/A';
|
||
document.getElementById('dockerUptime').textContent = docker.uptime || 'N/A';
|
||
document.getElementById('dockerMemory').textContent = docker.memory_usage || 'N/A';
|
||
|
||
// 更新状态(带颜色)
|
||
const statusElement = document.getElementById('dockerStatus');
|
||
statusElement.textContent = docker.status || 'Unknown';
|
||
if (docker.status === 'Running') {
|
||
statusElement.style.color = '#28a745'; // 绿色
|
||
} else {
|
||
statusElement.style.color = '#dc3545'; // 红色
|
||
}
|
||
|
||
// 如果有内存限制和百分比,显示更详细信息
|
||
if (docker.memory_limit && docker.memory_limit !== 'N/A') {
|
||
const memoryText = docker.memory_usage + ' / ' + docker.memory_limit;
|
||
if (docker.memory_percent && docker.memory_percent !== 'N/A') {
|
||
document.getElementById('dockerMemory').textContent = memoryText + ' (' + docker.memory_percent + ')';
|
||
} else {
|
||
document.getElementById('dockerMemory').textContent = memoryText;
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('加载Docker状态失败:', error);
|
||
}
|
||
}
|
||
|
||
async function loadTaskStats() {
|
||
try {
|
||
const response = await fetch('/yuyx/api/task/stats');
|
||
if (response.ok) {
|
||
const stats = await response.json();
|
||
|
||
// 更新当日统计
|
||
document.getElementById('todaySuccessTasks').textContent = stats.today.success_tasks;
|
||
document.getElementById('todayFailedTasks').textContent = stats.today.failed_tasks;
|
||
document.getElementById('todayTotalItems').textContent = stats.today.total_items;
|
||
document.getElementById('todayTotalAttachments').textContent = stats.today.total_attachments;
|
||
|
||
// 更新累计统计
|
||
document.getElementById('totalSuccessTasks').textContent = stats.total.success_tasks;
|
||
document.getElementById('totalFailedTasks').textContent = stats.total.failed_tasks;
|
||
document.getElementById('totalTotalItems').textContent = stats.total.total_items;
|
||
document.getElementById('totalTotalAttachments').textContent = stats.total.total_attachments;
|
||
}
|
||
} catch (error) {
|
||
console.error('加载任务统计失败:', error);
|
||
}
|
||
}
|
||
|
||
async function loadTaskLogs(reset = true) {
|
||
try {
|
||
if (reset) {
|
||
logsOffset = 0;
|
||
}
|
||
|
||
const dateFilter = document.getElementById('logDateFilter').value;
|
||
const statusFilter = document.getElementById('logStatusFilter').value;
|
||
|
||
let url = `/yuyx/api/task/logs?limit=${logsLimit}&offset=${logsOffset}`;
|
||
if (dateFilter) url += `&date=${dateFilter}`;
|
||
if (statusFilter) url += `&status=${statusFilter}`;
|
||
|
||
const response = await fetch(url);
|
||
if (response.ok) {
|
||
const logs = await response.json();
|
||
const tbody = document.getElementById('taskLogsList');
|
||
|
||
if (logs.length === 0 && reset) {
|
||
tbody.innerHTML = '<tr><td colspan="8" class="empty-message">暂无日志记录</td></tr>';
|
||
document.getElementById('loadMoreBtn').style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
if (reset) {
|
||
tbody.innerHTML = '';
|
||
}
|
||
|
||
logs.forEach(log => {
|
||
const row = document.createElement('tr');
|
||
const statusClass = log.status === 'success' ? 'status-approved' : 'status-rejected';
|
||
const statusText = log.status === 'success' ? '成功' : '失败';
|
||
|
||
// 格式化执行时间
|
||
const formatDuration = (seconds) => {
|
||
if (!seconds && seconds !== 0) return '-';
|
||
if (seconds < 60) return `${seconds}秒`;
|
||
const minutes = Math.floor(seconds / 60);
|
||
const secs = seconds % 60;
|
||
return `${minutes}分${secs}秒`;
|
||
};
|
||
|
||
row.innerHTML = `
|
||
<td>${log.created_at}</td>
|
||
<td>${log.user_username || 'N/A'}</td>
|
||
<td>${log.username}</td>
|
||
<td>${log.browse_type}</td>
|
||
<td><span class="status-badge ${statusClass}">${statusText}</span></td>
|
||
<td>${log.total_items} / ${log.total_attachments}</td>
|
||
<td style="color: #2F80ED; font-weight: 500;">${formatDuration(log.duration)}</td>
|
||
<td style="color: #dc3545; font-size: 11px;">${log.error_message || '-'}</td>
|
||
`;
|
||
tbody.appendChild(row);
|
||
});
|
||
|
||
// 显示/隐藏加载更多按钮
|
||
document.getElementById('loadMoreBtn').style.display = logs.length < logsLimit ? 'none' : 'inline-block';
|
||
}
|
||
} catch (error) {
|
||
console.error('加载任务日志失败:', error);
|
||
}
|
||
}
|
||
|
||
function loadMoreLogs() {
|
||
logsOffset += logsLimit;
|
||
loadTaskLogs(false);
|
||
}
|
||
|
||
async function clearOldLogs() {
|
||
const days = prompt('请输入要清理多少天前的日志(默认30天):', '30');
|
||
if (days === null) return;
|
||
|
||
const daysNum = parseInt(days);
|
||
if (isNaN(daysNum) || daysNum < 1) {
|
||
alert('请输入有效的天数(大于0的整数)');
|
||
return;
|
||
}
|
||
|
||
if (!confirm(`确定要删除${daysNum}天前的所有日志吗?此操作不可恢复!`)) return;
|
||
|
||
try {
|
||
const response = await fetch('/yuyx/api/task/logs/clear', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ days: daysNum })
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (response.ok) {
|
||
showNotification(data.message, 'success');
|
||
loadTaskLogs();
|
||
} else {
|
||
showNotification(data.error || '清理失败', 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('清理失败: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
// 修改switchTab函数,在切换到统计和日志标签时加载数据
|
||
const originalSwitchTab = switchTab;
|
||
switchTab = function(tabName) {
|
||
originalSwitchTab(tabName);
|
||
|
||
if (tabName === 'stats') {
|
||
loadDockerStats();
|
||
loadTaskStats();
|
||
} else if (tabName === 'logs') {
|
||
loadTaskLogs();
|
||
} else if (tabName === 'pending') {
|
||
loadPasswordResets();
|
||
}
|
||
};
|
||
|
||
// ==================== 密码重置功能 ====================
|
||
|
||
let passwordResets = [];
|
||
|
||
// 加载密码重置申请列表
|
||
async function loadPasswordResets() {
|
||
try {
|
||
const response = await fetch('/yuyx/api/password_resets');
|
||
if (response.ok) {
|
||
passwordResets = await response.json();
|
||
renderPasswordResets();
|
||
}
|
||
} catch (error) {
|
||
console.error('加载密码重置申请失败:', error);
|
||
}
|
||
}
|
||
|
||
// 渲染密码重置申请列表
|
||
function renderPasswordResets() {
|
||
const container = document.getElementById('passwordResetsList');
|
||
|
||
if (passwordResets.length === 0) {
|
||
container.innerHTML = '<div class="empty-message">暂无密码重置申请</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = `
|
||
<div class="table-container">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>申请ID</th>
|
||
<th>用户名</th>
|
||
<th>邮箱</th>
|
||
<th>申请时间</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${passwordResets.map(reset => `
|
||
<tr>
|
||
<td>${reset.id}</td>
|
||
<td><strong>${reset.username}</strong></td>
|
||
<td>${reset.email || '-'}</td>
|
||
<td>${reset.created_at}</td>
|
||
<td>
|
||
<div class="action-buttons">
|
||
<button class="btn btn-small btn-success" onclick="approvePasswordReset(${reset.id})">批准</button>
|
||
<button class="btn btn-small btn-danger" onclick="rejectPasswordReset(${reset.id})">拒绝</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// 批准密码重置申请
|
||
async function approvePasswordReset(requestId) {
|
||
if (!confirm('确定批准该密码重置申请吗?')) return;
|
||
|
||
try {
|
||
const response = await fetch(`/yuyx/api/password_resets/${requestId}/approve`, {
|
||
method: 'POST'
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (response.ok) {
|
||
showNotification('密码重置申请已批准', 'success');
|
||
loadPasswordResets();
|
||
} else {
|
||
showNotification('批准失败: ' + data.error, 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('批准失败: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
// 拒绝密码重置申请
|
||
async function rejectPasswordReset(requestId) {
|
||
if (!confirm('确定拒绝该密码重置申请吗?')) return;
|
||
|
||
try {
|
||
const response = await fetch(`/yuyx/api/password_resets/${requestId}/reject`, {
|
||
method: 'POST'
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (response.ok) {
|
||
showNotification('密码重置申请已拒绝', 'success');
|
||
loadPasswordResets();
|
||
} else {
|
||
showNotification('拒绝失败: ' + data.error, 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('拒绝失败: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
// 管理员直接重置用户密码
|
||
async function resetUserPassword(userId) {
|
||
const newPassword = prompt('请输入新密码(至少6位):');
|
||
if (!newPassword) return;
|
||
|
||
if (newPassword.length < 6) {
|
||
showNotification('密码长度至少6位', 'error');
|
||
return;
|
||
}
|
||
|
||
if (!confirm(`确定要将该用户密码重置为: ${newPassword}?`)) return;
|
||
|
||
try {
|
||
const response = await fetch(`/yuyx/api/users/${userId}/reset_password`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({new_password: newPassword})
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (response.ok) {
|
||
showNotification('密码重置成功', 'success');
|
||
} else {
|
||
showNotification('重置失败: ' + data.error, 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('重置失败: ' + error.message, 'error');
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|