修复所有bug并添加新功能
- 修复添加账号按钮无反应问题
- 添加账号备注字段(可选)
- 添加账号设置按钮(修改密码/备注)
- 修复用户反馈���能
- 添加定时任务执行日志
- 修复容器重启后账号加载问题
- 修复所有JavaScript语法错误
- 优化账号加载机制(4层保障)
🤖 Generated with Claude Code
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user