- 修复添加账号按钮无反应问题
- 添加账号备注字段(可选)
- 添加账号设置按钮(修改密码/备注)
- 修复用户反馈���能
- 添加定时任务执行日志
- 修复容器重启后账号加载问题
- 修复所有JavaScript语法错误
- 优化账号加载机制(4层保障)
🤖 Generated with Claude Code
2213 lines
99 KiB
HTML
2213 lines
99 KiB
HTML
<!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('feedbacks')">反馈管理 <span id="feedbackBadge" style="background:#e74c3c; color:white; padding:2px 6px; border-radius:10px; font-size:11px; margin-left:3px; display:none;">0</span></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-feedbacks" class="tab-content">
|
||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px;">
|
||
<h3 style="font-size:16px;">用户反馈管理</h3>
|
||
<div style="display:flex; gap:10px;">
|
||
<select id="feedbackStatusFilter" onchange="loadFeedbacks()" style="padding:8px; border:1px solid #ddd; border-radius:5px;">
|
||
<option value="">全部状态</option>
|
||
<option value="pending">待处理</option>
|
||
<option value="replied">已回复</option>
|
||
<option value="closed">已关闭</option>
|
||
</select>
|
||
<button onclick="loadFeedbacks()" class="btn btn-primary" style="padding:8px 15px;">刷新</button>
|
||
</div>
|
||
</div>
|
||
<div id="feedbackStats" style="display:flex; gap:15px; margin-bottom:15px; padding:10px; background:#f5f5f5; border-radius:5px;">
|
||
<span>总计: <strong id="statTotal">0</strong></span>
|
||
<span style="color:#f39c12;">待处理: <strong id="statPending">0</strong></span>
|
||
<span style="color:#27ae60;">已回复: <strong id="statReplied">0</strong></span>
|
||
<span style="color:#95a5a6;">已关闭: <strong id="statClosed">0</strong></span>
|
||
</div>
|
||
<div id="feedbacksList"></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;">实时任务监控 <span id="taskMonitorStatus" style="font-size: 12px; color: #28a745; font-weight: normal;">● 实时更新中</span></h3>
|
||
|
||
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; margin-bottom: 15px;">
|
||
<div style="background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); text-align: center;">
|
||
<div style="font-size: 28px; font-weight: bold; color: #28a745;" id="runningTaskCount">0</div>
|
||
<div style="font-size: 13px; color: #666; margin-top: 5px;">运行中</div>
|
||
</div>
|
||
<div style="background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); text-align: center;">
|
||
<div style="font-size: 28px; font-weight: bold; color: #fd7e14;" id="queuingTaskCount">0</div>
|
||
<div style="font-size: 13px; color: #666; margin-top: 5px;">排队中</div>
|
||
</div>
|
||
<div style="background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); text-align: center;">
|
||
<div style="font-size: 28px; font-weight: bold; color: #6c757d;" id="maxConcurrentDisplay">-</div>
|
||
<div style="font-size: 13px; color: #666; margin-top: 5px;">最大并发</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 运行中的任务列表 -->
|
||
<div style="background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); margin-bottom: 15px;">
|
||
<div style="font-size: 14px; font-weight: bold; color: #28a745; margin-bottom: 10px;">🚀 运行中的任务</div>
|
||
<div id="runningTasksList" style="font-size: 13px;">
|
||
<div style="color: #999; text-align: center; padding: 10px;">暂无运行中的任务</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 排队中的任务列表 -->
|
||
<div style="background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); margin-bottom: 25px;">
|
||
<div style="font-size: 14px; font-weight: bold; color: #fd7e14; margin-bottom: 10px;">⏳ 排队中的任务</div>
|
||
<div id="queuingTasksList" style="font-size: 13px;">
|
||
<div style="color: #999; text-align: center; padding: 10px;">暂无排队中的任务</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; padding: 15px; background: #f8f9fa; border-radius: 8px;">
|
||
<div style="display: flex; gap: 10px; flex-wrap: wrap; align-items: center;">
|
||
<div style="display: flex; align-items: center; gap: 5px;">
|
||
<label style="font-size: 12px; color: #666;">日期:</label>
|
||
<input type="date" id="logDateFilter" style="padding: 6px 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 13px;">
|
||
</div>
|
||
<div style="display: flex; align-items: center; gap: 5px;">
|
||
<label style="font-size: 12px; color: #666;">状态:</label>
|
||
<select id="logStatusFilter" style="padding: 6px 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 13px;">
|
||
<option value="">全部</option>
|
||
<option value="success">成功</option>
|
||
<option value="failed">失败</option>
|
||
</select>
|
||
</div>
|
||
<div style="display: flex; align-items: center; gap: 5px;">
|
||
<label style="font-size: 12px; color: #666;">来源:</label>
|
||
<select id="logSourceFilter" style="padding: 6px 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 13px;">
|
||
<option value="">全部</option>
|
||
<option value="manual">手动</option>
|
||
<option value="scheduled">定时</option>
|
||
<option value="immediate">即时</option>
|
||
<option value="resumed">恢复</option>
|
||
</select>
|
||
</div>
|
||
<div style="display: flex; align-items: center; gap: 5px;">
|
||
<label style="font-size: 12px; color: #666;">用户:</label>
|
||
<select id="logUserFilter" style="padding: 6px 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 13px; min-width: 100px;">
|
||
<option value="">全部</option>
|
||
</select>
|
||
</div>
|
||
<div style="display: flex; align-items: center; gap: 5px;">
|
||
<label style="font-size: 12px; color: #666;">账号:</label>
|
||
<input type="text" id="logAccountFilter" placeholder="输入账号关键字" style="padding: 6px 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 13px; width: 120px;">
|
||
</div>
|
||
<button class="btn btn-primary" onclick="loadTaskLogs()" style="padding: 6px 15px;">筛选</button>
|
||
<button class="btn btn-secondary" onclick="resetLogFilters()" style="padding: 6px 15px;">重置</button>
|
||
<button class="btn btn-danger" onclick="clearOldLogs()" style="padding: 6px 15px;">清理旧日志</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 日志表格 -->
|
||
<div class="table-container">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 140px;">时间</th>
|
||
<th style="width: 60px;">来源</th>
|
||
<th style="width: 80px;">用户</th>
|
||
<th style="width: 100px;">账号</th>
|
||
<th style="width: 80px;">浏览类型</th>
|
||
<th style="width: 60px;">状态</th>
|
||
<th style="width: 90px;">内容/附件</th>
|
||
<th style="width: 70px;">用时</th>
|
||
<th>失败原因</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="taskLogsList">
|
||
<tr><td colspan="9" class="empty-message">加载中...</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- 分页控件 -->
|
||
<div id="logsPagination" style="margin-top: 15px; display: flex; justify-content: center; align-items: center; gap: 10px; flex-wrap: wrap;">
|
||
<button class="btn btn-secondary" onclick="goToLogPage(1)" id="logFirstBtn" disabled style="padding: 6px 12px;">首页</button>
|
||
<button class="btn btn-secondary" onclick="goToLogPage(currentLogPage - 1)" id="logPrevBtn" disabled style="padding: 6px 12px;">上一页</button>
|
||
<span id="logPageInfo" style="font-size: 13px; color: #666;">第 1 页 / 共 1 页</span>
|
||
<button class="btn btn-secondary" onclick="goToLogPage(currentLogPage + 1)" id="logNextBtn" disabled style="padding: 6px 12px;">下一页</button>
|
||
<button class="btn btn-secondary" onclick="goToLogPage(totalLogPages)" id="logLastBtn" disabled style="padding: 6px 12px;">末页</button>
|
||
<span style="font-size: 12px; color: #999; margin-left: 10px;">共 <span id="logTotalCount">0</span> 条记录</span>
|
||
</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>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 通知组件 -->
|
||
<div id="notification" class="notification"></div>
|
||
|
||
<script>
|
||
let allUsers = [];
|
||
let pendingUsers = [];
|
||
|
||
// 页面加载时初始化
|
||
window.addEventListener('load', () => {
|
||
loadStats();
|
||
loadPendingUsers();
|
||
loadAllUsers();
|
||
loadSystemConfig();
|
||
loadProxyConfig();
|
||
loadPasswordResets(); // 修复: 初始化时也加载密码重置申请
|
||
loadFeedbacks(); // 加载反馈统计更新徽章
|
||
|
||
// 恢复上次的标签页
|
||
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();
|
||
}
|
||
|
||
// 切换到反馈管理标签时加载反馈列表
|
||
if (tabName === 'feedbacks') {
|
||
loadFeedbacks();
|
||
}
|
||
}
|
||
|
||
// 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 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);
|
||
}
|
||
}
|
||
|
||
|
||
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 loadRunningTasks() {
|
||
try {
|
||
const response = await fetch('/yuyx/api/task/running');
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
|
||
// 更新计数
|
||
document.getElementById('runningTaskCount').textContent = data.running_count;
|
||
document.getElementById('queuingTaskCount').textContent = data.queuing_count;
|
||
document.getElementById('maxConcurrentDisplay').textContent = data.max_concurrent;
|
||
|
||
// 来源显示映射
|
||
const sourceMap = {
|
||
'manual': {text: '手动', color: '#28a745'},
|
||
'scheduled': {text: '定时', color: '#007bff'},
|
||
'immediate': {text: '即时', color: '#fd7e14'},
|
||
'resumed': {text: '恢复', color: '#6c757d'}
|
||
};
|
||
|
||
// 渲染运行中的任务
|
||
const runningList = document.getElementById('runningTasksList');
|
||
if (data.running.length === 0) {
|
||
runningList.innerHTML = '<div style="color: #999; text-align: center; padding: 10px;">暂无运行中的任务</div>';
|
||
} else {
|
||
runningList.innerHTML = data.running.map(task => {
|
||
const source = sourceMap[task.source] || {text: task.source, color: '#666'};
|
||
// 状态颜色映射
|
||
const statusColorMap = {
|
||
'初始化': '#6c757d',
|
||
'正在登录': '#fd7e14',
|
||
'正在浏览': '#28a745',
|
||
'浏览完成': '#007bff',
|
||
'正在截图': '#17a2b8'
|
||
};
|
||
const statusColor = statusColorMap[task.detail_status] || '#666';
|
||
// 进度显示
|
||
const progressText = task.progress_items > 0 || task.progress_attachments > 0
|
||
? `(${task.progress_items}/${task.progress_attachments})`
|
||
: '';
|
||
return `<div style="display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: #f8f9fa; border-radius: 5px; margin-bottom: 5px; border-left: 3px solid ${statusColor};">
|
||
<div style="flex: 1;">
|
||
<div style="display: flex; align-items: center; flex-wrap: wrap; gap: 5px;">
|
||
<span style="color: ${source.color}; font-weight: 500; font-size: 12px;">[${source.text}]</span>
|
||
<span style="color: #333;">${task.user_username}</span>
|
||
<span style="color: #666;">→</span>
|
||
<span style="color: #007bff; font-weight: 500;">${task.username}</span>
|
||
<span style="background: #e9ecef; padding: 2px 6px; border-radius: 3px; font-size: 11px; color: #666;">${task.browse_type}</span>
|
||
</div>
|
||
<div style="margin-top: 4px; display: flex; align-items: center; gap: 8px;">
|
||
<span style="color: ${statusColor}; font-weight: 500; font-size: 12px;">● ${task.detail_status}</span>
|
||
${progressText ? `<span style="color: #999; font-size: 11px;">内容/附件: ${progressText}</span>` : ''}
|
||
</div>
|
||
</div>
|
||
<div style="text-align: right; min-width: 70px;">
|
||
<div style="color: #28a745; font-weight: 500;">${task.elapsed_display}</div>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
// 渲染排队中的任务
|
||
const queuingList = document.getElementById('queuingTasksList');
|
||
if (data.queuing.length === 0) {
|
||
queuingList.innerHTML = '<div style="color: #999; text-align: center; padding: 10px;">暂无排队中的任务</div>';
|
||
} else {
|
||
queuingList.innerHTML = data.queuing.map(task => {
|
||
const source = sourceMap[task.source] || {text: task.source, color: '#666'};
|
||
return `<div style="display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: #fff8e6; border-radius: 5px; margin-bottom: 5px; border-left: 3px solid #fd7e14;">
|
||
<div style="flex: 1;">
|
||
<div style="display: flex; align-items: center; flex-wrap: wrap; gap: 5px;">
|
||
<span style="color: ${source.color}; font-weight: 500; font-size: 12px;">[${source.text}]</span>
|
||
<span style="color: #333;">${task.user_username}</span>
|
||
<span style="color: #666;">→</span>
|
||
<span style="color: #007bff; font-weight: 500;">${task.username}</span>
|
||
<span style="background: #ffeeba; padding: 2px 6px; border-radius: 3px; font-size: 11px; color: #856404;">${task.browse_type}</span>
|
||
</div>
|
||
<div style="margin-top: 4px;">
|
||
<span style="color: #fd7e14; font-size: 12px;">● ${task.detail_status || '等待资源'}</span>
|
||
</div>
|
||
</div>
|
||
<div style="text-align: right; min-width: 80px;">
|
||
<div style="color: #fd7e14; font-weight: 500;">等待 ${task.elapsed_display}</div>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('加载运行任务失败:', error);
|
||
}
|
||
}
|
||
|
||
// 任务统计和日志功能
|
||
let currentLogPage = 1;
|
||
let totalLogPages = 1;
|
||
let totalLogCount = 0;
|
||
const logsPerPage = 20;
|
||
|
||
// 加载用户列表用于筛选
|
||
async function loadLogUserOptions() {
|
||
try {
|
||
const response = await fetch('/yuyx/api/users');
|
||
if (response.ok) {
|
||
const users = await response.json();
|
||
const select = document.getElementById('logUserFilter');
|
||
select.innerHTML = '<option value="">全部</option>';
|
||
users.forEach(user => {
|
||
select.innerHTML += `<option value="${user.id}">${user.username}</option>`;
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('加载用户列表失败:', error);
|
||
}
|
||
}
|
||
|
||
// 重置筛选条件
|
||
function resetLogFilters() {
|
||
document.getElementById('logDateFilter').value = '';
|
||
document.getElementById('logStatusFilter').value = '';
|
||
document.getElementById('logSourceFilter').value = '';
|
||
document.getElementById('logUserFilter').value = '';
|
||
document.getElementById('logAccountFilter').value = '';
|
||
currentLogPage = 1;
|
||
loadTaskLogs();
|
||
}
|
||
|
||
// 跳转到指定页
|
||
function goToLogPage(page) {
|
||
if (page < 1 || page > totalLogPages) return;
|
||
currentLogPage = page;
|
||
loadTaskLogs();
|
||
}
|
||
|
||
// 更新分页控件状态
|
||
function updatePaginationUI() {
|
||
document.getElementById('logFirstBtn').disabled = currentLogPage <= 1;
|
||
document.getElementById('logPrevBtn').disabled = currentLogPage <= 1;
|
||
document.getElementById('logNextBtn').disabled = currentLogPage >= totalLogPages;
|
||
document.getElementById('logLastBtn').disabled = currentLogPage >= totalLogPages;
|
||
document.getElementById('logPageInfo').textContent = `第 ${currentLogPage} 页 / 共 ${totalLogPages} 页`;
|
||
document.getElementById('logTotalCount').textContent = totalLogCount;
|
||
}
|
||
|
||
async function loadTaskLogs() {
|
||
try {
|
||
const dateFilter = document.getElementById('logDateFilter').value;
|
||
const statusFilter = document.getElementById('logStatusFilter').value;
|
||
const sourceFilter = document.getElementById('logSourceFilter').value;
|
||
const userFilter = document.getElementById('logUserFilter').value;
|
||
const accountFilter = document.getElementById('logAccountFilter').value;
|
||
|
||
const offset = (currentLogPage - 1) * logsPerPage;
|
||
let url = `/yuyx/api/task/logs?limit=${logsPerPage}&offset=${offset}`;
|
||
if (dateFilter) url += `&date=${dateFilter}`;
|
||
if (statusFilter) url += `&status=${statusFilter}`;
|
||
if (sourceFilter) url += `&source=${sourceFilter}`;
|
||
if (userFilter) url += `&user_id=${userFilter}`;
|
||
if (accountFilter) url += `&account=${encodeURIComponent(accountFilter)}`;
|
||
|
||
const response = await fetch(url);
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
const logs = data.logs || data;
|
||
totalLogCount = data.total || logs.length;
|
||
totalLogPages = Math.max(1, Math.ceil(totalLogCount / logsPerPage));
|
||
|
||
const tbody = document.getElementById('taskLogsList');
|
||
|
||
if (logs.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="9" class="empty-message">暂无日志记录</td></tr>';
|
||
updatePaginationUI();
|
||
return;
|
||
}
|
||
|
||
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}秒`;
|
||
};
|
||
|
||
// 来源显示
|
||
const sourceMap = {
|
||
'manual': {text: '手动', color: '#28a745'},
|
||
'scheduled': {text: '定时', color: '#007bff'},
|
||
'immediate': {text: '即时', color: '#fd7e14'},
|
||
'resumed': {text: '恢复', color: '#6c757d'}
|
||
};
|
||
const sourceInfo = sourceMap[log.source] || {text: log.source || '手动', color: '#28a745'};
|
||
|
||
row.innerHTML = `
|
||
<td>${log.created_at}</td>
|
||
<td><span style="color: ${sourceInfo.color}; font-weight: 500;">${sourceInfo.text}</span></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);
|
||
});
|
||
|
||
updatePaginationUI();
|
||
}
|
||
} catch (error) {
|
||
console.error('加载任务日志失败:', error);
|
||
}
|
||
}
|
||
|
||
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函数,在切换到统计和日志标签时加载数据
|
||
let statsRefreshInterval = null;
|
||
const originalSwitchTab = switchTab;
|
||
switchTab = function(tabName) {
|
||
originalSwitchTab(tabName);
|
||
|
||
// 清除之前的定时刷新
|
||
if (statsRefreshInterval) {
|
||
clearInterval(statsRefreshInterval);
|
||
statsRefreshInterval = null;
|
||
}
|
||
|
||
if (tabName === 'stats') {
|
||
loadServerInfo();
|
||
loadDockerStats();
|
||
loadTaskStats();
|
||
// 每3秒自动刷新统计信息
|
||
statsRefreshInterval = setInterval(() => {
|
||
loadServerInfo();
|
||
loadDockerStats();
|
||
loadTaskStats();
|
||
loadRunningTasks();
|
||
}, 3000);
|
||
// 首次立即加载运行任务
|
||
loadRunningTasks();
|
||
} else if (tabName === 'logs') {
|
||
loadLogUserOptions();
|
||
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');
|
||
}
|
||
}
|
||
// ==================== 反馈管理功能 ====================
|
||
|
||
let feedbacksList = [];
|
||
|
||
// 加载反馈列表
|
||
async function loadFeedbacks() {
|
||
try {
|
||
const statusFilter = document.getElementById('feedbackStatusFilter').value;
|
||
let url = '/yuyx/api/feedbacks';
|
||
if (statusFilter) {
|
||
url += '?status=' + statusFilter;
|
||
}
|
||
|
||
const response = await fetch(url);
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
feedbacksList = data.feedbacks || [];
|
||
const stats = data.stats || {};
|
||
|
||
document.getElementById('statTotal').textContent = stats.total || 0;
|
||
document.getElementById('statPending').textContent = stats.pending || 0;
|
||
document.getElementById('statReplied').textContent = stats.replied || 0;
|
||
document.getElementById('statClosed').textContent = stats.closed || 0;
|
||
|
||
const badge = document.getElementById('feedbackBadge');
|
||
if (stats.pending > 0) {
|
||
badge.textContent = stats.pending;
|
||
badge.style.display = 'inline';
|
||
} else {
|
||
badge.style.display = 'none';
|
||
}
|
||
|
||
renderFeedbacks();
|
||
}
|
||
} catch (error) {
|
||
console.error('加载反馈列表失败:', error);
|
||
showNotification('加载反馈列表失败: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
function renderFeedbacks() {
|
||
const container = document.getElementById('feedbacksList');
|
||
|
||
if (feedbacksList.length === 0) {
|
||
container.innerHTML = '<div class="empty-message">暂无反馈记录</div>';
|
||
return;
|
||
}
|
||
|
||
const getStatusBadge = (status) => {
|
||
const statusMap = {
|
||
'pending': { text: '待处理', cls: 'status-pending' },
|
||
'replied': { text: '已回复', cls: 'status-approved' },
|
||
'closed': { text: '已关闭', cls: 'status-rejected' }
|
||
};
|
||
const s = statusMap[status] || { text: status, cls: '' };
|
||
return '<span class="status-badge ' + s.cls + '">' + s.text + '</span>';
|
||
};
|
||
|
||
let html = '<div class="table-container"><table><thead><tr>';
|
||
html += '<th>ID</th><th>用户</th><th>标题</th><th>描述</th><th>联系方式</th><th>状态</th><th>提交时间</th><th>回复</th><th>操作</th>';
|
||
html += '</tr></thead><tbody>';
|
||
|
||
feedbacksList.forEach(fb => {
|
||
html += '<tr>';
|
||
html += '<td>' + fb.id + '</td>';
|
||
html += '<td><strong>' + (fb.username || 'N/A') + '</strong></td>';
|
||
html += '<td style="max-width:150px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="' + (fb.title||'') + '">' + (fb.title||'') + '</td>';
|
||
html += '<td style="max-width:200px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="' + (fb.description||'') + '">' + (fb.description||'') + '</td>';
|
||
html += '<td>' + (fb.contact || '-') + '</td>';
|
||
html += '<td>' + getStatusBadge(fb.status) + '</td>';
|
||
html += '<td>' + fb.created_at + '</td>';
|
||
html += '<td style="max-width:150px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="' + (fb.admin_reply || '') + '">' + (fb.admin_reply || '-') + '</td>';
|
||
html += '<td><div class="action-buttons">';
|
||
if (fb.status !== 'closed') {
|
||
html += '<button class="btn btn-small btn-primary" onclick="replyFeedback(' + fb.id + ')">回复</button>';
|
||
html += '<button class="btn btn-small btn-secondary" onclick="closeFeedback(' + fb.id + ')">关闭</button>';
|
||
}
|
||
html += '<button class="btn btn-small btn-danger" onclick="deleteFeedback(' + fb.id + ')">删除</button>';
|
||
html += '</div></td></tr>';
|
||
});
|
||
|
||
html += '</tbody></table></div>';
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
async function replyFeedback(feedbackId) {
|
||
const reply = prompt('请输入回复内容:');
|
||
if (!reply || !reply.trim()) return;
|
||
|
||
try {
|
||
const response = await fetch('/yuyx/api/feedbacks/' + feedbackId + '/reply', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ reply: reply.trim() })
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (response.ok) {
|
||
showNotification('回复成功', 'success');
|
||
loadFeedbacks();
|
||
} else {
|
||
showNotification('回复失败: ' + (data.error || '未知错误'), 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('回复失败: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
async function closeFeedback(feedbackId) {
|
||
if (!confirm('确定要关闭这个反馈吗?')) return;
|
||
|
||
try {
|
||
const response = await fetch('/yuyx/api/feedbacks/' + feedbackId + '/close', {
|
||
method: 'POST'
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (response.ok) {
|
||
showNotification('反馈已关闭', 'success');
|
||
loadFeedbacks();
|
||
} else {
|
||
showNotification('关闭失败: ' + (data.error || '未知错误'), 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('关闭失败: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
async function deleteFeedback(feedbackId) {
|
||
if (!confirm('确定要删除这个反馈吗?此操作不可恢复!')) return;
|
||
|
||
try {
|
||
const response = await fetch('/yuyx/api/feedbacks/' + feedbackId, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (response.ok) {
|
||
showNotification('反馈已删除', 'success');
|
||
loadFeedbacks();
|
||
} else {
|
||
showNotification('删除失败: ' + (data.error || '未知错误'), 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('删除失败: ' + error.message, 'error');
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html> |