修复所有bug并添加新功能

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

🤖 Generated with Claude Code
This commit is contained in:
Yu Yon
2025-12-10 11:19:16 +08:00
parent 0fd7137cea
commit b5344cd55e
67 changed files with 38235 additions and 3271 deletions

View File

@@ -530,6 +530,7 @@
<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>
@@ -550,26 +551,57 @@
<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;">
<input type="number" id="maxConcurrent" min="1" value="2" style="max-width: 200px;">
<div style="font-size: 12px; color: #666; margin-top: 5px;">
说明:同时最多运行的账号数量。服务器内存1.7GB建议设置2-3。每个浏览器约占用200MB内存
说明:同时最多运行的账号数量。浏览任务使用API方式资源占用极低截图任务会启动浏览器。建议设置2-5
</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;">
<input type="number" id="maxConcurrentPerAccount" min="1" value="1" style="max-width: 200px;">
<div style="font-size: 12px; color: #666; margin-top: 5px;">
说明单个账号同时最多运行的任务数量。建议设置1-2。
</div>
</div>
<div class="form-group">
<label>截图最大并发数 (1-5)</label>
<input type="number" id="maxScreenshotConcurrent" min="1" value="3" style="max-width: 200px;">
<div style="font-size: 12px; color: #666; margin-top: 5px;">
说明同时进行截图的最大数量。截图使用浏览器建议设置2-3。
</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>
@@ -638,8 +670,11 @@
</div>
</div>
<div id="scheduleActions" style="margin-top: 15px;">
<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>
<!-- ========== 代理设置 ========== -->
@@ -683,127 +718,249 @@
<!-- 统计 -->
<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 style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; padding: 20px; margin-bottom: 20px; color: white;">
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px;">
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 24px;">💻</span>
<div>
<div style="font-size: 20px; font-weight: bold;" id="serverCpu">-</div>
<div style="font-size: 12px; opacity: 0.8;">CPU</div>
</div>
</div>
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 24px;">🧠</span>
<div>
<div style="font-size: 20px; font-weight: bold;" id="serverMemory">-</div>
<div style="font-size: 12px; opacity: 0.8;">内存</div>
</div>
</div>
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 24px;">💾</span>
<div>
<div style="font-size: 20px; font-weight: bold;" id="serverDisk">-</div>
<div style="font-size: 12px; opacity: 0.8;">磁盘</div>
</div>
</div>
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 24px;">🐳</span>
<div>
<div style="font-size: 20px; font-weight: bold;" id="dockerMemory">-</div>
<div style="font-size: 12px; opacity: 0.8;">容器</div>
</div>
</div>
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 24px;">⏱️</span>
<div>
<div style="font-size: 20px; font-weight: bold;" id="serverUptime">-</div>
<div style="font-size: 12px; opacity: 0.8;">运行</div>
</div>
</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>
<!-- 隐藏的元素保持JS兼容性 -->
<div style="display:none;">
<span id="dockerContainerName"></span>
<span id="dockerUptime"></span>
<span id="dockerStatus"></span>
</div>
<h3 style="margin: 25px 0 15px 0; font-size: 16px;">当日任务统计</h3>
<!-- 实时任务监控 -->
<div style="background: white; border-radius: 12px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 12px rgba(0,0,0,0.08);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<h3 style="margin: 0; font-size: 16px; display: flex; align-items: center; gap: 8px;">
<span>📊</span> 实时监控
</h3>
<span id="taskMonitorStatus" style="font-size: 12px; color: #28a745;">● 实时更新</span>
</div>
<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 style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; margin-bottom: 15px;">
<div style="background: linear-gradient(135deg, #28a745 0%, #20c997 100%); padding: 15px; border-radius: 10px; text-align: center; color: white;">
<div style="font-size: 32px; font-weight: bold;" id="runningTaskCount">0</div>
<div style="font-size: 12px; opacity: 0.9; margin-top: 3px;">🚀 运行中</div>
</div>
<div style="background: linear-gradient(135deg, #fd7e14 0%, #ffc107 100%); padding: 15px; border-radius: 10px; text-align: center; color: white;">
<div style="font-size: 32px; font-weight: bold;" id="queuingTaskCount">0</div>
<div style="font-size: 12px; opacity: 0.9; margin-top: 3px;">⏳ 排队中</div>
</div>
<div style="background: linear-gradient(135deg, #6c757d 0%, #495057 100%); padding: 15px; border-radius: 10px; text-align: center; color: white;">
<div style="font-size: 32px; font-weight: bold;" id="maxConcurrentDisplay">-</div>
<div style="font-size: 12px; opacity: 0.9; margin-top: 3px;">⚡ 最大并发</div>
</div>
</div>
<!-- 任务列表(折叠式) -->
<details style="margin-top: 10px;" open>
<summary style="cursor: pointer; font-size: 13px; color: #666; padding: 8px 0;">展开查看任务详情</summary>
<div style="margin-top: 10px; padding: 10px; background: #f8f9fa; border-radius: 8px;">
<div style="font-size: 13px; font-weight: bold; color: #28a745; margin-bottom: 8px;">运行中</div>
<div id="runningTasksList" style="font-size: 12px; margin-bottom: 10px;">
<div style="color: #999;">暂无</div>
</div>
<div style="font-size: 13px; font-weight: bold; color: #fd7e14; margin-bottom: 8px;">排队中</div>
<div id="queuingTasksList" style="font-size: 12px;">
<div style="color: #999;">暂无</div>
</div>
</div>
</details>
</div>
<h3 style="margin: 25px 0 15px 0; font-size: 16px;">历史累计统计</h3>
<!-- 任务统计 - 合并当日和累计 -->
<div style="background: white; border-radius: 12px; padding: 20px; box-shadow: 0 2px 12px rgba(0,0,0,0.08);">
<h3 style="margin: 0 0 15px 0; font-size: 16px; display: flex; align-items: center; gap: 8px;">
<span>📈</span> 任务统计
</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 style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px;">
<!-- 成功任务 -->
<div style="background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%); padding: 15px; border-radius: 10px;">
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 8px;">
<span style="font-size: 20px;"></span>
<span style="font-size: 13px; color: #155724; font-weight: bold;">成功任务</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: baseline;">
<div>
<span style="font-size: 28px; font-weight: bold; color: #155724;" id="todaySuccessTasks">0</span>
<span style="font-size: 12px; color: #155724;">今日</span>
</div>
<div style="text-align: right;">
<span style="font-size: 16px; color: #28a745;" id="totalSuccessTasks">0</span>
<span style="font-size: 11px; color: #666;">累计</span>
</div>
</div>
</div>
<!-- 失败任务 -->
<div style="background: linear-gradient(135deg, #f8d7da 0%, #f5c6cb 100%); padding: 15px; border-radius: 10px;">
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 8px;">
<span style="font-size: 20px;"></span>
<span style="font-size: 13px; color: #721c24; font-weight: bold;">失败任务</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: baseline;">
<div>
<span style="font-size: 28px; font-weight: bold; color: #721c24;" id="todayFailedTasks">0</span>
<span style="font-size: 12px; color: #721c24;">今日</span>
</div>
<div style="text-align: right;">
<span style="font-size: 16px; color: #dc3545;" id="totalFailedTasks">0</span>
<span style="font-size: 11px; color: #666;">累计</span>
</div>
</div>
</div>
<!-- 浏览内容 -->
<div style="background: linear-gradient(135deg, #cce5ff 0%, #b8daff 100%); padding: 15px; border-radius: 10px;">
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 8px;">
<span style="font-size: 20px;">📄</span>
<span style="font-size: 13px; color: #004085; font-weight: bold;">浏览内容</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: baseline;">
<div>
<span style="font-size: 28px; font-weight: bold; color: #004085;" id="todayTotalItems">0</span>
<span style="font-size: 12px; color: #004085;">今日</span>
</div>
<div style="text-align: right;">
<span style="font-size: 16px; color: #007bff;" id="totalTotalItems">0</span>
<span style="font-size: 11px; color: #666;">累计</span>
</div>
</div>
</div>
<!-- 查看附件 -->
<div style="background: linear-gradient(135deg, #d1ecf1 0%, #bee5eb 100%); padding: 15px; border-radius: 10px;">
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 8px;">
<span style="font-size: 20px;">📎</span>
<span style="font-size: 13px; color: #0c5460; font-weight: bold;">查看附件</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: baseline;">
<div>
<span style="font-size: 28px; font-weight: bold; color: #0c5460;" id="todayTotalAttachments">0</span>
<span style="font-size: 12px; color: #0c5460;">今日</span>
</div>
<div style="text-align: right;">
<span style="font-size: 16px; color: #17a2b8;" id="totalTotalAttachments">0</span>
<span style="font-size: 11px; color: #666;">累计</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 任务日志 -->
<div id="tab-logs" class="tab-content">
<div style="margin-bottom: 15px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center;">
<input type="date" id="logDateFilter" style="padding: 8px; border: 1px solid #ddd; border-radius: 5px; font-size: 13px;">
<select id="logStatusFilter" style="padding: 8px; border: 1px solid #ddd; border-radius: 5px; font-size: 13px;">
<option value="">全部状态</option>
<option value="success">成功</option>
<option value="failed">失败</option>
</select>
<button class="btn btn-primary" onclick="loadTaskLogs()">筛选</button>
<button class="btn btn-secondary" onclick="clearOldLogs()">清理旧日志</button>
<!-- 筛选区域 -->
<div 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>时间</th>
<th>用户</th>
<th>账号</th>
<th>浏览类型</th>
<th>状态</th>
<th>内容数/附件数</th>
<th>执行用时</th>
<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="8" class="empty-message">加载中...</td></tr>
<tr><td colspan="9" class="empty-message">加载中...</td></tr>
</tbody>
</table>
</div>
<div style="margin-top: 15px; text-align: center;">
<button class="btn btn-secondary" onclick="loadMoreLogs()" id="loadMoreBtn">加载更多</button>
<!-- 分页控件 -->
<div 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>
@@ -841,6 +998,14 @@
loadSystemConfig();
loadProxyConfig();
loadPasswordResets(); // 修复: 初始化时也加载密码重置申请
loadFeedbacks(); // 加载反馈统计更新徽章
// 恢复上次的标签页
const lastTab = localStorage.getItem('admin_current_tab') || 'pending';
const tabButton = document.querySelector(`.tab[onclick*="${lastTab}"]`);
if (tabButton) {
tabButton.click();
}
});
// 切换标签
@@ -855,6 +1020,9 @@
});
document.getElementById(`tab-${tabName}`).classList.add('active');
// 保存当前标签到localStorage
localStorage.setItem('admin_current_tab', tabName);
// 切换到统计标签时加载服务器信息和任务统计
if (tabName === 'stats') {
loadServerInfo();
@@ -866,6 +1034,11 @@
if (tabName === 'logs') {
loadTaskLogs();
}
// 切换到反馈管理标签时加载反馈列表
if (tabName === 'feedbacks') {
loadFeedbacks();
}
}
// VIP functions
@@ -1200,7 +1373,7 @@
setTimeout(() => {
notification.style.display = 'none';
}, 3000);
}, 1000);
}
// 系统配置功能
@@ -1211,6 +1384,7 @@
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('maxScreenshotConcurrent').value = config.max_screenshot_concurrent || 3;
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 || '应读';
@@ -1333,6 +1507,7 @@
async function updateConcurrency() {
const maxConcurrent = parseInt(document.getElementById('maxConcurrent').value);
const maxConcurrentPerAccount = parseInt(document.getElementById('maxConcurrentPerAccount').value);
const maxScreenshotConcurrent = parseInt(document.getElementById('maxScreenshotConcurrent').value);
if (maxConcurrent < 1 || maxConcurrent > 20) {
showNotification('全局并发数必须在1-20之间', 'error');
@@ -1344,7 +1519,12 @@
return;
}
if (!confirm(`确定更新并发配置吗?\n\n全局并发数: ${maxConcurrent}\n单账号并发数: ${maxConcurrentPerAccount}\n\n建议值服务器内存1.7GB时全局设置2-3单账号设置1-2`)) return;
if (maxScreenshotConcurrent < 1 || maxScreenshotConcurrent > 5) {
showNotification('截图并发数必须在1-5之间', 'error');
return;
}
if (!confirm(`确定更新并发配置吗?\n\n全局并发数: ${maxConcurrent}\n单账号并发数: ${maxConcurrentPerAccount}\n\n建议并发数影响任务执行速度过高可能触发目标服务器限制。全局建议2-5单账号建议1-2`)) return;
try {
const response = await fetch('/yuyx/api/system/config', {
@@ -1352,7 +1532,8 @@
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
max_concurrent_global: maxConcurrent,
max_concurrent_per_account: maxConcurrentPerAccount
max_concurrent_per_account: maxConcurrentPerAccount,
max_screenshot_concurrent: maxScreenshotConcurrent
})
});
@@ -1416,9 +1597,46 @@
}
}
// 任务统计和日志功能
let logsOffset = 0;
const logsLimit = 50;
// 立即执行定时任务
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 = '⚡ 立即执行';
}, 1000);
} catch (error) {
showNotification('执行失败: ' + error.message, 'error');
// 恢复按钮状态
setTimeout(() => {
button.disabled = false;
button.textContent = '⚡ 立即执行';
}, 1000);
}
}
async function loadServerInfo() {
try {
@@ -1495,33 +1713,181 @@
}
}
async function loadTaskLogs(reset = true) {
async function loadRunningTasks() {
try {
if (reset) {
logsOffset = 0;
}
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;
let url = `/yuyx/api/task/logs?limit=${logsLimit}&offset=${logsOffset}`;
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 logs = await response.json();
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 && reset) {
tbody.innerHTML = '<tr><td colspan="8" class="empty-message">暂无日志记录</td></tr>';
document.getElementById('loadMoreBtn').style.display = 'none';
if (logs.length === 0) {
tbody.innerHTML = '<tr><td colspan="9" class="empty-message">暂无日志记录</td></tr>';
updatePaginationUI();
return;
}
if (reset) {
tbody.innerHTML = '';
}
tbody.innerHTML = '';
logs.forEach(log => {
const row = document.createElement('tr');
@@ -1537,8 +1903,18 @@
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>
@@ -1550,19 +1926,13 @@
tbody.appendChild(row);
});
// 显示/隐藏加载更多按钮
document.getElementById('loadMoreBtn').style.display = logs.length < logsLimit ? 'none' : 'inline-block';
updatePaginationUI();
}
} catch (error) {
console.error('加载任务日志失败:', error);
}
}
function loadMoreLogs() {
logsOffset += logsLimit;
loadTaskLogs(false);
}
async function clearOldLogs() {
const days = prompt('请输入要清理多少天前的日志默认30天:', '30');
if (days === null) return;
@@ -1595,14 +1965,32 @@
}
// 修改switchTab函数在切换到统计和日志标签时加载数据
let statsRefreshInterval = null;
const originalSwitchTab = switchTab;
switchTab = function(tabName) {
originalSwitchTab(tabName);
// 清除之前的定时刷新
if (statsRefreshInterval) {
clearInterval(statsRefreshInterval);
statsRefreshInterval = null;
}
if (tabName === 'stats') {
loadServerInfo();
loadDockerStats();
loadTaskStats();
// 每1秒自动刷新统计信息
statsRefreshInterval = setInterval(() => {
loadServerInfo();
loadDockerStats();
loadTaskStats();
loadRunningTasks();
}, 1000);
// 首次立即加载运行任务
loadRunningTasks();
} else if (tabName === 'logs') {
loadLogUserOptions();
loadTaskLogs();
} else if (tabName === 'pending') {
loadPasswordResets();
@@ -1739,6 +2127,153 @@
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>
</html>