修复所有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>
|
||||
1850
templates/admin.html.backup_all
Normal file
1850
templates/admin.html.backup_all
Normal file
File diff suppressed because it is too large
Load Diff
1855
templates/admin.html.backup_broken
Normal file
1855
templates/admin.html.backup_broken
Normal file
File diff suppressed because it is too large
Load Diff
1850
templates/admin.html.before_sed
Normal file
1850
templates/admin.html.before_sed
Normal file
File diff suppressed because it is too large
Load Diff
@@ -156,7 +156,7 @@
|
||||
<div id="errorMessage" class="error-message"></div>
|
||||
<div id="successMessage" class="success-message"></div>
|
||||
|
||||
<form id="loginForm" onsubmit="handleLogin(event)">
|
||||
<form id="loginForm" method="POST" action="/yuyx/api/login" onsubmit="handleLogin(event)">
|
||||
<div class="form-group">
|
||||
<label for="username">管理员账号</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
@@ -216,6 +216,7 @@
|
||||
try {
|
||||
const response = await fetch('/yuyx/api/login', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin', // 确保发送和接收cookies
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
@@ -233,9 +234,10 @@
|
||||
if (response.ok) {
|
||||
successDiv.textContent = '登录成功,正在跳转...';
|
||||
successDiv.style.display = 'block';
|
||||
setTimeout(() => {
|
||||
window.location.href = '/yuyx/admin';
|
||||
}, 500);
|
||||
// 等待1秒确保cookie设置完成
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
// 使用replace避免返回按钮回到登录页
|
||||
window.location.replace(data.redirect || '/yuyx/admin');
|
||||
} else {
|
||||
errorDiv.textContent = data.error || '登录失败';
|
||||
errorDiv.style.display = 'block';
|
||||
|
||||
3598
templates/index.html
3598
templates/index.html
File diff suppressed because it is too large
Load Diff
1474
templates/index.html.backup2
Normal file
1474
templates/index.html.backup2
Normal file
File diff suppressed because it is too large
Load Diff
1478
templates/index.html.backup_20251210_013401
Normal file
1478
templates/index.html.backup_20251210_013401
Normal file
File diff suppressed because it is too large
Load Diff
1474
templates/index.html.backup_20251210_102119
Normal file
1474
templates/index.html.backup_20251210_102119
Normal file
File diff suppressed because it is too large
Load Diff
1439
templates/index.html.mobile_backup
Normal file
1439
templates/index.html.mobile_backup
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,480 +1,240 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>用户登录 - 知识管理平台</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #56CCF2 0%, #2F80ED 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||||
width: 400px;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
font-size: 28px;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #2F80ED;
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: linear-gradient(135deg, #56CCF2 0%, #2F80ED 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.btn-login:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-login:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.register-link {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.register-link a {
|
||||
color: #2F80ED;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.register-link a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #ffe6e6;
|
||||
color: #d63031;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 20px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
background: #e6ffe6;
|
||||
color: #27ae60;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 20px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.forgot-password-link {
|
||||
text-align: right;
|
||||
margin-top: -10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.forgot-password-link a {
|
||||
color: #2F80ED;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.forgot-password-link a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 30px;
|
||||
width: 90%;
|
||||
max-width: 400px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
font-size: 22px;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.modal-header p {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
background: linear-gradient(135deg, #56CCF2 0%, #2F80ED 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="login-header">
|
||||
<h1>用户登录</h1>
|
||||
</div>
|
||||
|
||||
<div id="errorMessage" class="error-message"></div>
|
||||
<div id="successMessage" class="success-message"></div>
|
||||
|
||||
<form id="loginForm" onsubmit="handleLogin(event)">
|
||||
<div class="form-group">
|
||||
<label for="username">用户名</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">密码</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
|
||||
<!-- 验证码区域(第一次失败后显示) -->
|
||||
<div id="captchaGroup" class="form-group" style="display: none;">
|
||||
<label for="captcha">验证码</label>
|
||||
<div style="display: flex; gap: 10px; align-items: center;">
|
||||
<input type="text" id="captcha" name="captcha" placeholder="请输入验证码" style="flex: 1;">
|
||||
<span id="captchaCode" style="font-size: 20px; font-weight: bold; letter-spacing: 5px; color: #4CAF50; user-select: none;">----</span>
|
||||
<button type="button" onclick="refreshCaptcha()" style="padding: 8px 15px; background: #f0f0f0; border: 1px solid #ddd; border-radius: 4px; cursor: pointer;">刷新</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="forgot-password-link">
|
||||
<a href="#" onclick="showForgotPassword(event)">忘记密码?</a>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-login">登录</button>
|
||||
</form>
|
||||
|
||||
<div class="register-link">
|
||||
还没有账号? <a href="/register">立即注册</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 忘记密码弹窗 -->
|
||||
<div id="forgotPasswordModal" class="modal" onclick="closeModal(event)">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>重置密码</h2>
|
||||
<p>请填写您的用户名和新密码,管理员审核通过后生效</p>
|
||||
</div>
|
||||
|
||||
<div id="modalErrorMessage" class="error-message"></div>
|
||||
<div id="modalSuccessMessage" class="success-message"></div>
|
||||
|
||||
<form id="resetPasswordForm" onsubmit="handleResetPassword(event)">
|
||||
<div class="form-group">
|
||||
<label for="resetUsername">用户名</label>
|
||||
<input type="text" id="resetUsername" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="resetEmail">邮箱(可选)</label>
|
||||
<input type="email" id="resetEmail" placeholder="用于验证身份">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="resetNewPassword">新密码</label>
|
||||
<input type="password" id="resetNewPassword" required>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn-secondary" onclick="closeForgotPassword()">取消</button>
|
||||
<button type="submit" class="btn-primary">提交申请</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 全局变量存储验证码session
|
||||
let captchaSession = '';
|
||||
let needCaptcha = false;
|
||||
|
||||
async function handleLogin(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const password = document.getElementById('password').value.trim();
|
||||
const captchaInput = document.getElementById('captcha');
|
||||
const captcha = captchaInput ? captchaInput.value.trim() : '';
|
||||
const errorDiv = document.getElementById('errorMessage');
|
||||
const successDiv = document.getElementById('successMessage');
|
||||
|
||||
errorDiv.style.display = 'none';
|
||||
successDiv.style.display = 'none';
|
||||
|
||||
if (!username || !password) {
|
||||
errorDiv.textContent = '用户名和密码不能为空';
|
||||
errorDiv.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果需要验证码但没有输入
|
||||
if (needCaptcha && !captcha) {
|
||||
errorDiv.textContent = '请输入验证码';
|
||||
errorDiv.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
password,
|
||||
captcha_session: captchaSession,
|
||||
captcha: captcha,
|
||||
need_captcha: needCaptcha
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
successDiv.textContent = '登录成功,正在跳转...';
|
||||
successDiv.style.display = 'block';
|
||||
setTimeout(() => {
|
||||
window.location.href = '/app';
|
||||
}, 500);
|
||||
} else {
|
||||
// 显示详细错误信息
|
||||
errorDiv.textContent = data.error || '登录失败';
|
||||
errorDiv.style.display = 'block';
|
||||
|
||||
// 如果返回需要验证码,显示验证码区域并生成验证码
|
||||
if (data.need_captcha) {
|
||||
needCaptcha = true;
|
||||
document.getElementById('captchaGroup').style.display = 'block';
|
||||
await generateCaptcha();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
errorDiv.textContent = '网络错误,请稍后重试';
|
||||
errorDiv.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// 忘记密码功能
|
||||
function showForgotPassword(event) {
|
||||
event.preventDefault();
|
||||
document.getElementById('forgotPasswordModal').classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeForgotPassword() {
|
||||
document.getElementById('forgotPasswordModal').classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
// 清空表单
|
||||
document.getElementById('resetPasswordForm').reset();
|
||||
document.getElementById('modalErrorMessage').style.display = 'none';
|
||||
document.getElementById('modalSuccessMessage').style.display = 'none';
|
||||
}
|
||||
|
||||
function closeModal(event) {
|
||||
if (event.target.id === 'forgotPasswordModal') {
|
||||
closeForgotPassword();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleResetPassword(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const username = document.getElementById('resetUsername').value.trim();
|
||||
const email = document.getElementById('resetEmail').value.trim();
|
||||
const newPassword = document.getElementById('resetNewPassword').value.trim();
|
||||
const modalErrorDiv = document.getElementById('modalErrorMessage');
|
||||
const modalSuccessDiv = document.getElementById('modalSuccessMessage');
|
||||
|
||||
modalErrorDiv.style.display = 'none';
|
||||
modalSuccessDiv.style.display = 'none';
|
||||
|
||||
if (!username || !newPassword) {
|
||||
modalErrorDiv.textContent = '用户名和新密码不能为空';
|
||||
modalErrorDiv.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
modalErrorDiv.textContent = '密码长度至少6位';
|
||||
modalErrorDiv.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/reset_password_request', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ username, email, new_password: newPassword })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
modalSuccessDiv.textContent = '密码重置申请已提交,请等待管理员审核';
|
||||
modalSuccessDiv.style.display = 'block';
|
||||
setTimeout(() => {
|
||||
closeForgotPassword();
|
||||
}, 2000);
|
||||
} else {
|
||||
modalErrorDiv.textContent = data.error || '申请失败';
|
||||
modalErrorDiv.style.display = 'block';
|
||||
}
|
||||
} catch (error) {
|
||||
modalErrorDiv.textContent = '网络错误,请稍后重试';
|
||||
modalErrorDiv.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// ESC键关闭弹窗
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
closeForgotPassword();
|
||||
}
|
||||
});
|
||||
// 生成验证码
|
||||
async function generateCaptcha() {
|
||||
try {
|
||||
const response = await fetch('/api/generate_captcha', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.session_id && data.captcha) {
|
||||
captchaSession = data.session_id;
|
||||
document.getElementById('captchaCode').textContent = data.captcha;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('生成验证码失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新验证码
|
||||
async function refreshCaptcha() {
|
||||
await generateCaptcha();
|
||||
document.getElementById('captcha').value = '';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>登录 - 知识管理平台</title>
|
||||
<style>
|
||||
:root {
|
||||
--md-primary: #1976D2;
|
||||
--md-primary-dark: #1565C0;
|
||||
--md-primary-light: #BBDEFB;
|
||||
--md-background: #FAFAFA;
|
||||
--md-surface: #FFFFFF;
|
||||
--md-error: #B00020;
|
||||
--md-success: #4CAF50;
|
||||
--md-on-primary: #FFFFFF;
|
||||
--md-on-surface: #212121;
|
||||
--md-on-surface-medium: #666666;
|
||||
--md-shadow-lg: 0 8px 30px rgba(0,0,0,0.12);
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.login-card {
|
||||
background: var(--md-surface);
|
||||
border-radius: 16px;
|
||||
box-shadow: var(--md-shadow-lg);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.login-header {
|
||||
background: var(--md-primary);
|
||||
color: var(--md-on-primary);
|
||||
padding: 32px 24px;
|
||||
text-align: center;
|
||||
}
|
||||
.login-header .logo { font-size: 48px; margin-bottom: 12px; }
|
||||
.login-header h1 { font-size: 24px; font-weight: 500; margin-bottom: 4px; }
|
||||
.login-header p { font-size: 14px; opacity: 0.9; }
|
||||
.login-body { padding: 32px 24px; }
|
||||
.form-group { margin-bottom: 24px; }
|
||||
.form-group label { display: block; font-size: 14px; font-weight: 500; color: var(--md-on-surface-medium); margin-bottom: 8px; }
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
border: 2px solid #E0E0E0;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
transition: all 0.2s;
|
||||
background: #FAFAFA;
|
||||
}
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: var(--md-primary);
|
||||
background: var(--md-surface);
|
||||
box-shadow: 0 0 0 3px var(--md-primary-light);
|
||||
}
|
||||
.captcha-row { display: flex; gap: 12px; align-items: center; }
|
||||
.captcha-row input { flex: 1; }
|
||||
.captcha-code {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 4px;
|
||||
color: var(--md-primary);
|
||||
padding: 10px 16px;
|
||||
background: var(--md-primary-light);
|
||||
border-radius: 8px;
|
||||
user-select: none;
|
||||
}
|
||||
.captcha-refresh {
|
||||
padding: 10px 16px;
|
||||
background: #F5F5F5;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
}
|
||||
.captcha-refresh:hover { background: #EEEEEE; }
|
||||
.forgot-link { text-align: right; margin-top: -16px; margin-bottom: 24px; }
|
||||
.forgot-link a { color: var(--md-primary); text-decoration: none; font-size: 14px; font-weight: 500; }
|
||||
.forgot-link a:hover { text-decoration: underline; }
|
||||
.btn-login {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
background: var(--md-primary);
|
||||
color: var(--md-on-primary);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.btn-login:hover { background: var(--md-primary-dark); box-shadow: 0 4px 12px rgba(25, 118, 210, 0.4); }
|
||||
.register-link {
|
||||
text-align: center;
|
||||
margin-top: 24px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid #E0E0E0;
|
||||
color: var(--md-on-surface-medium);
|
||||
font-size: 14px;
|
||||
}
|
||||
.register-link a { color: var(--md-primary); text-decoration: none; font-weight: 600; }
|
||||
.register-link a:hover { text-decoration: underline; }
|
||||
.message { padding: 12px 16px; border-radius: 8px; margin-bottom: 20px; font-size: 14px; display: none; }
|
||||
.message.error { background: #FFEBEE; color: var(--md-error); border: 1px solid #FFCDD2; }
|
||||
.message.success { background: #E8F5E9; color: var(--md-success); border: 1px solid #C8E6C9; }
|
||||
.modal-overlay {
|
||||
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex; justify-content: center; align-items: center;
|
||||
opacity: 0; visibility: hidden; transition: all 0.3s; z-index: 1000; padding: 20px;
|
||||
}
|
||||
.modal-overlay.active { opacity: 1; visibility: visible; }
|
||||
.modal {
|
||||
background: var(--md-surface);
|
||||
border-radius: 16px;
|
||||
width: 100%; max-width: 400px;
|
||||
box-shadow: var(--md-shadow-lg);
|
||||
transform: translateY(-20px); transition: transform 0.3s;
|
||||
}
|
||||
.modal-overlay.active .modal { transform: translateY(0); }
|
||||
.modal-header { padding: 24px; border-bottom: 1px solid #E0E0E0; }
|
||||
.modal-header h2 { font-size: 20px; font-weight: 500; margin-bottom: 4px; }
|
||||
.modal-header p { font-size: 14px; color: var(--md-on-surface-medium); }
|
||||
.modal-body { padding: 24px; }
|
||||
.modal-footer { padding: 16px 24px; border-top: 1px solid #E0E0E0; display: flex; gap: 12px; justify-content: flex-end; }
|
||||
.btn-secondary { padding: 12px 24px; background: #F5F5F5; color: var(--md-on-surface); border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; }
|
||||
.btn-secondary:hover { background: #EEEEEE; }
|
||||
.btn-primary { padding: 12px 24px; background: var(--md-primary); color: var(--md-on-primary); border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; }
|
||||
.btn-primary:hover { background: var(--md-primary-dark); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-card">
|
||||
<div class="login-header">
|
||||
<div class="logo">📚</div>
|
||||
<h1>知识管理平台</h1>
|
||||
<p>自动化浏览学习内容</p>
|
||||
</div>
|
||||
<div class="login-body">
|
||||
<div id="errorMessage" class="message error"></div>
|
||||
<div id="successMessage" class="message success"></div>
|
||||
<form id="loginForm" onsubmit="handleLogin(event)">
|
||||
<div class="form-group">
|
||||
<label for="username">用户名</label>
|
||||
<input type="text" id="username" name="username" placeholder="请输入用户名" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">密码</label>
|
||||
<input type="password" id="password" name="password" placeholder="请输入密码" required>
|
||||
</div>
|
||||
<div id="captchaGroup" class="form-group" style="display: none;">
|
||||
<label for="captcha">验证码</label>
|
||||
<div class="captcha-row">
|
||||
<input type="text" id="captcha" name="captcha" placeholder="请输入验证码">
|
||||
<span id="captchaCode" class="captcha-code">----</span>
|
||||
<button type="button" class="captcha-refresh" onclick="refreshCaptcha()">🔄</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="forgot-link"><a href="#" onclick="showForgotPassword(event)">忘记密码?</a></div>
|
||||
<button type="submit" class="btn-login">登 录</button>
|
||||
</form>
|
||||
<div class="register-link">还没有账号? <a href="/register">立即注册</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="forgotPasswordModal" class="modal-overlay" onclick="if(event.target===this)closeForgotPassword()">
|
||||
<div class="modal">
|
||||
<div class="modal-header"><h2>重置密码</h2><p>填写信息后等待管理员审核</p></div>
|
||||
<div class="modal-body">
|
||||
<div id="modalErrorMessage" class="message error"></div>
|
||||
<div id="modalSuccessMessage" class="message success"></div>
|
||||
<form id="resetPasswordForm" onsubmit="handleResetPassword(event)">
|
||||
<div class="form-group"><label>用户名</label><input type="text" id="resetUsername" placeholder="请输入用户名" required></div>
|
||||
<div class="form-group"><label>邮箱(可选)</label><input type="email" id="resetEmail" placeholder="用于验证身份"></div>
|
||||
<div class="form-group"><label>新密码</label><input type="password" id="resetNewPassword" placeholder="至少6位" required></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn-secondary" onclick="closeForgotPassword()">取消</button>
|
||||
<button type="button" class="btn-primary" onclick="document.getElementById('resetPasswordForm').dispatchEvent(new Event('submit'))">提交申请</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
let captchaSession = '';
|
||||
let needCaptcha = false;
|
||||
async function handleLogin(event) {
|
||||
event.preventDefault();
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const password = document.getElementById('password').value.trim();
|
||||
const captcha = document.getElementById('captcha') ? document.getElementById('captcha').value.trim() : '';
|
||||
const errorDiv = document.getElementById('errorMessage');
|
||||
const successDiv = document.getElementById('successMessage');
|
||||
errorDiv.style.display = 'none';
|
||||
successDiv.style.display = 'none';
|
||||
if (!username || !password) { errorDiv.textContent = '用户名和密码不能为空'; errorDiv.style.display = 'block'; return; }
|
||||
if (needCaptcha && !captcha) { errorDiv.textContent = '请输入验证码'; errorDiv.style.display = 'block'; return; }
|
||||
try {
|
||||
const response = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password, captcha_session: captchaSession, captcha, need_captcha: needCaptcha }) });
|
||||
const data = await response.json();
|
||||
if (response.ok) { successDiv.textContent = '登录成功,正在跳转...'; successDiv.style.display = 'block'; setTimeout(() => { window.location.href = '/app'; }, 500); }
|
||||
else { errorDiv.textContent = data.error || '登录失败'; errorDiv.style.display = 'block'; if (data.need_captcha) { needCaptcha = true; document.getElementById('captchaGroup').style.display = 'block'; await generateCaptcha(); } }
|
||||
} catch (error) { errorDiv.textContent = '网络错误'; errorDiv.style.display = 'block'; }
|
||||
}
|
||||
function showForgotPassword(event) { event.preventDefault(); document.getElementById('forgotPasswordModal').classList.add('active'); }
|
||||
function closeForgotPassword() { document.getElementById('forgotPasswordModal').classList.remove('active'); document.getElementById('resetPasswordForm').reset(); document.getElementById('modalErrorMessage').style.display = 'none'; document.getElementById('modalSuccessMessage').style.display = 'none'; }
|
||||
async function handleResetPassword(event) {
|
||||
event.preventDefault();
|
||||
const username = document.getElementById('resetUsername').value.trim();
|
||||
const email = document.getElementById('resetEmail').value.trim();
|
||||
const newPassword = document.getElementById('resetNewPassword').value.trim();
|
||||
const errorDiv = document.getElementById('modalErrorMessage');
|
||||
const successDiv = document.getElementById('modalSuccessMessage');
|
||||
errorDiv.style.display = 'none'; successDiv.style.display = 'none';
|
||||
if (!username || !newPassword) { errorDiv.textContent = '用户名和新密码不能为空'; errorDiv.style.display = 'block'; return; }
|
||||
if (newPassword.length < 6) { errorDiv.textContent = '密码长度至少6位'; errorDiv.style.display = 'block'; return; }
|
||||
try {
|
||||
const response = await fetch('/api/reset_password_request', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, email, new_password: newPassword }) });
|
||||
const data = await response.json();
|
||||
if (response.ok) { successDiv.textContent = '申请已提交,请等待审核'; successDiv.style.display = 'block'; setTimeout(closeForgotPassword, 2000); }
|
||||
else { errorDiv.textContent = data.error || '申请失败'; errorDiv.style.display = 'block'; }
|
||||
} catch (error) { errorDiv.textContent = '网络错误'; errorDiv.style.display = 'block'; }
|
||||
}
|
||||
async function generateCaptcha() { try { const response = await fetch('/api/generate_captcha', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const data = await response.json(); if (data.session_id && data.captcha) { captchaSession = data.session_id; document.getElementById('captchaCode').textContent = data.captcha; } } catch (error) { console.error('生成验证码失败:', error); } }
|
||||
async function refreshCaptcha() { await generateCaptcha(); document.getElementById('captcha').value = ''; }
|
||||
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeForgotPassword(); });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user