Files
zsglpt/admin.html
Yu Yon b5344cd55e 修复所有bug并添加新功能
- 修复添加账号按钮无反应问题
- 添加账号备注字段(可选)
- 添加账号设置按钮(修改密码/备注)
- 修复用户反馈���能
- 添加定时任务执行日志
- 修复容器重启后账号加载问题
- 修复所有JavaScript语法错误
- 优化账号加载机制(4层保障)

🤖 Generated with Claude Code
2025-12-10 11:19:16 +08:00

2213 lines
99 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, 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>