修复所有bug并添加新功能

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

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

View File

@@ -530,6 +530,7 @@
<div class="tabs">
<button class="tab active" onclick="switchTab('pending')">待审核</button>
<button class="tab" onclick="switchTab('all')">所有用户</button>
<button class="tab" onclick="switchTab('feedbacks')">反馈管理 <span id="feedbackBadge" style="background:#e74c3c; color:white; padding:2px 6px; border-radius:10px; font-size:11px; margin-left:3px; display:none;">0</span></button>
<button class="tab" onclick="switchTab('stats')">统计</button>
<button class="tab" onclick="switchTab('logs')">任务日志</button>
<button class="tab" onclick="switchTab('system')">系统配置</button>
@@ -550,26 +551,57 @@
<div id="allUsersList"></div>
</div>
<!-- 反馈管理 -->
<div id="tab-feedbacks" class="tab-content">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px;">
<h3 style="font-size:16px;">用户反馈管理</h3>
<div style="display:flex; gap:10px;">
<select id="feedbackStatusFilter" onchange="loadFeedbacks()" style="padding:8px; border:1px solid #ddd; border-radius:5px;">
<option value="">全部状态</option>
<option value="pending">待处理</option>
<option value="replied">已回复</option>
<option value="closed">已关闭</option>
</select>
<button onclick="loadFeedbacks()" class="btn btn-primary" style="padding:8px 15px;">刷新</button>
</div>
</div>
<div id="feedbackStats" style="display:flex; gap:15px; margin-bottom:15px; padding:10px; background:#f5f5f5; border-radius:5px;">
<span>总计: <strong id="statTotal">0</strong></span>
<span style="color:#f39c12;">待处理: <strong id="statPending">0</strong></span>
<span style="color:#27ae60;">已回复: <strong id="statReplied">0</strong></span>
<span style="color:#95a5a6;">已关闭: <strong id="statClosed">0</strong></span>
</div>
<div id="feedbacksList"></div>
</div>
<!-- 系统配置 -->
<div id="tab-system" class="tab-content">
<h3 style="margin-bottom: 15px; font-size: 16px;">系统并发配置</h3>
<div class="form-group">
<label>全局最大并发数 (1-20)</label>
<input type="number" id="maxConcurrent" min="1" max="20" value="2" style="max-width: 200px;">
<input type="number" id="maxConcurrent" min="1" value="2" style="max-width: 200px;">
<div style="font-size: 12px; color: #666; margin-top: 5px;">
说明:同时最多运行的账号数量。服务器内存1.7GB建议设置2-3。每个浏览器约占用200MB内存
说明:同时最多运行的账号数量。浏览任务使用API方式资源占用极低截图任务会启动浏览器。建议设置2-5
</div>
</div>
<div class="form-group">
<label>单账号最大并发数 (1-5)</label>
<input type="number" id="maxConcurrentPerAccount" min="1" max="5" value="1" style="max-width: 200px;">
<input type="number" id="maxConcurrentPerAccount" min="1" value="1" style="max-width: 200px;">
<div style="font-size: 12px; color: #666; margin-top: 5px;">
说明单个账号同时最多运行的任务数量。建议设置1-2。
</div>
</div>
<div class="form-group">
<label>截图最大并发数 (1-5)</label>
<input type="number" id="maxScreenshotConcurrent" min="1" value="3" style="max-width: 200px;">
<div style="font-size: 12px; color: #666; margin-top: 5px;">
说明同时进行截图的最大数量。截图使用浏览器建议设置2-3。
</div>
</div>
<button class="btn btn-primary" style="margin-top: 10px;" onclick="updateConcurrency()">保存并发配置</button>
<h3 style="margin: 25px 0 15px 0; font-size: 16px;">定时任务配置</h3>
@@ -638,8 +670,11 @@
</div>
</div>
<div id="scheduleActions" style="margin-top: 15px;">
<div id="scheduleActions" style="margin-top: 15px; display: flex; gap: 10px;">
<button class="btn btn-primary" onclick="updateSchedule()">保存定时任务配置</button>
<button class="btn btn-success" onclick="executeScheduleNow()" style="background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);">
⚡ 立即执行
</button>
</div>
<!-- ========== 代理设置 ========== -->
@@ -683,127 +718,249 @@
<!-- 统计 -->
<div id="tab-stats" class="tab-content">
<h3 style="margin-bottom: 15px; font-size: 16px;">服务器信息</h3>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin-bottom: 25px;">
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 24px; font-weight: bold; color: #f5576c;" id="serverCpu">-</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">CPU使用率</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 24px; font-weight: bold; color: #f093fb;" id="serverMemory">-</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">内存使用</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 24px; font-weight: bold; color: #764ba2;" id="serverDisk">-</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">磁盘使用</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 24px; font-weight: bold; color: #17a2b8;" id="serverUptime">-</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">运行时长</div>
<!-- 系统状态概览 - 精简合并版 -->
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; padding: 20px; margin-bottom: 20px; color: white;">
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px;">
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 24px;">💻</span>
<div>
<div style="font-size: 20px; font-weight: bold;" id="serverCpu">-</div>
<div style="font-size: 12px; opacity: 0.8;">CPU</div>
</div>
</div>
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 24px;">🧠</span>
<div>
<div style="font-size: 20px; font-weight: bold;" id="serverMemory">-</div>
<div style="font-size: 12px; opacity: 0.8;">内存</div>
</div>
</div>
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 24px;">💾</span>
<div>
<div style="font-size: 20px; font-weight: bold;" id="serverDisk">-</div>
<div style="font-size: 12px; opacity: 0.8;">磁盘</div>
</div>
</div>
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 24px;">🐳</span>
<div>
<div style="font-size: 20px; font-weight: bold;" id="dockerMemory">-</div>
<div style="font-size: 12px; opacity: 0.8;">容器</div>
</div>
</div>
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 24px;">⏱️</span>
<div>
<div style="font-size: 20px; font-weight: bold;" id="serverUptime">-</div>
<div style="font-size: 12px; opacity: 0.8;">运行</div>
</div>
</div>
</div>
</div>
<h3 style="margin: 25px 0 15px 0; font-size: 16px;">Docker容器状态</h3>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin-bottom: 25px;">
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 20px; font-weight: bold; color: #28a745;" id="dockerContainerName">-</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">容器名称</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 20px; font-weight: bold; color: #17a2b8;" id="dockerUptime">-</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">容器运行时间</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 20px; font-weight: bold; color: #f093fb;" id="dockerMemory">-</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">容器内存使用</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 20px; font-weight: bold;" id="dockerStatus" style="color: #28a745;">-</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">运行状态</div>
</div>
<!-- 隐藏的元素保持JS兼容性 -->
<div style="display:none;">
<span id="dockerContainerName"></span>
<span id="dockerUptime"></span>
<span id="dockerStatus"></span>
</div>
<h3 style="margin: 25px 0 15px 0; font-size: 16px;">当日任务统计</h3>
<!-- 实时任务监控 -->
<div style="background: white; border-radius: 12px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 12px rgba(0,0,0,0.08);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<h3 style="margin: 0; font-size: 16px; display: flex; align-items: center; gap: 8px;">
<span>📊</span> 实时监控
</h3>
<span id="taskMonitorStatus" style="font-size: 12px; color: #28a745;">● 实时更新</span>
</div>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin-bottom: 25px;">
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 32px; font-weight: bold; color: #28a745;" id="todaySuccessTasks">0</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">成功任务</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 32px; font-weight: bold; color: #dc3545;" id="todayFailedTasks">0</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">失败任务</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 32px; font-weight: bold; color: #007bff;" id="todayTotalItems">0</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">浏览内容数</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 32px; font-weight: bold; color: #17a2b8;" id="todayTotalAttachments">0</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">查看附件数</div>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; margin-bottom: 15px;">
<div style="background: linear-gradient(135deg, #28a745 0%, #20c997 100%); padding: 15px; border-radius: 10px; text-align: center; color: white;">
<div style="font-size: 32px; font-weight: bold;" id="runningTaskCount">0</div>
<div style="font-size: 12px; opacity: 0.9; margin-top: 3px;">🚀 运行中</div>
</div>
<div style="background: linear-gradient(135deg, #fd7e14 0%, #ffc107 100%); padding: 15px; border-radius: 10px; text-align: center; color: white;">
<div style="font-size: 32px; font-weight: bold;" id="queuingTaskCount">0</div>
<div style="font-size: 12px; opacity: 0.9; margin-top: 3px;">⏳ 排队中</div>
</div>
<div style="background: linear-gradient(135deg, #6c757d 0%, #495057 100%); padding: 15px; border-radius: 10px; text-align: center; color: white;">
<div style="font-size: 32px; font-weight: bold;" id="maxConcurrentDisplay">-</div>
<div style="font-size: 12px; opacity: 0.9; margin-top: 3px;">⚡ 最大并发</div>
</div>
</div>
<!-- 任务列表(折叠式) -->
<details style="margin-top: 10px;" open>
<summary style="cursor: pointer; font-size: 13px; color: #666; padding: 8px 0;">展开查看任务详情</summary>
<div style="margin-top: 10px; padding: 10px; background: #f8f9fa; border-radius: 8px;">
<div style="font-size: 13px; font-weight: bold; color: #28a745; margin-bottom: 8px;">运行中</div>
<div id="runningTasksList" style="font-size: 12px; margin-bottom: 10px;">
<div style="color: #999;">暂无</div>
</div>
<div style="font-size: 13px; font-weight: bold; color: #fd7e14; margin-bottom: 8px;">排队中</div>
<div id="queuingTasksList" style="font-size: 12px;">
<div style="color: #999;">暂无</div>
</div>
</div>
</details>
</div>
<h3 style="margin: 25px 0 15px 0; font-size: 16px;">历史累计统计</h3>
<!-- 任务统计 - 合并当日和累计 -->
<div style="background: white; border-radius: 12px; padding: 20px; box-shadow: 0 2px 12px rgba(0,0,0,0.08);">
<h3 style="margin: 0 0 15px 0; font-size: 16px; display: flex; align-items: center; gap: 8px;">
<span>📈</span> 任务统计
</h3>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px;">
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 32px; font-weight: bold; color: #28a745;" id="totalSuccessTasks">0</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">累计成功任务</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 32px; font-weight: bold; color: #dc3545;" id="totalFailedTasks">0</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">累计失败任务</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 32px; font-weight: bold; color: #007bff;" id="totalTotalItems">0</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">累计浏览内容</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 32px; font-weight: bold; color: #17a2b8;" id="totalTotalAttachments">0</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">累计查看附件</div>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px;">
<!-- 成功任务 -->
<div style="background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%); padding: 15px; border-radius: 10px;">
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 8px;">
<span style="font-size: 20px;"></span>
<span style="font-size: 13px; color: #155724; font-weight: bold;">成功任务</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: baseline;">
<div>
<span style="font-size: 28px; font-weight: bold; color: #155724;" id="todaySuccessTasks">0</span>
<span style="font-size: 12px; color: #155724;">今日</span>
</div>
<div style="text-align: right;">
<span style="font-size: 16px; color: #28a745;" id="totalSuccessTasks">0</span>
<span style="font-size: 11px; color: #666;">累计</span>
</div>
</div>
</div>
<!-- 失败任务 -->
<div style="background: linear-gradient(135deg, #f8d7da 0%, #f5c6cb 100%); padding: 15px; border-radius: 10px;">
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 8px;">
<span style="font-size: 20px;"></span>
<span style="font-size: 13px; color: #721c24; font-weight: bold;">失败任务</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: baseline;">
<div>
<span style="font-size: 28px; font-weight: bold; color: #721c24;" id="todayFailedTasks">0</span>
<span style="font-size: 12px; color: #721c24;">今日</span>
</div>
<div style="text-align: right;">
<span style="font-size: 16px; color: #dc3545;" id="totalFailedTasks">0</span>
<span style="font-size: 11px; color: #666;">累计</span>
</div>
</div>
</div>
<!-- 浏览内容 -->
<div style="background: linear-gradient(135deg, #cce5ff 0%, #b8daff 100%); padding: 15px; border-radius: 10px;">
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 8px;">
<span style="font-size: 20px;">📄</span>
<span style="font-size: 13px; color: #004085; font-weight: bold;">浏览内容</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: baseline;">
<div>
<span style="font-size: 28px; font-weight: bold; color: #004085;" id="todayTotalItems">0</span>
<span style="font-size: 12px; color: #004085;">今日</span>
</div>
<div style="text-align: right;">
<span style="font-size: 16px; color: #007bff;" id="totalTotalItems">0</span>
<span style="font-size: 11px; color: #666;">累计</span>
</div>
</div>
</div>
<!-- 查看附件 -->
<div style="background: linear-gradient(135deg, #d1ecf1 0%, #bee5eb 100%); padding: 15px; border-radius: 10px;">
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 8px;">
<span style="font-size: 20px;">📎</span>
<span style="font-size: 13px; color: #0c5460; font-weight: bold;">查看附件</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: baseline;">
<div>
<span style="font-size: 28px; font-weight: bold; color: #0c5460;" id="todayTotalAttachments">0</span>
<span style="font-size: 12px; color: #0c5460;">今日</span>
</div>
<div style="text-align: right;">
<span style="font-size: 16px; color: #17a2b8;" id="totalTotalAttachments">0</span>
<span style="font-size: 11px; color: #666;">累计</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 任务日志 -->
<div id="tab-logs" class="tab-content">
<div style="margin-bottom: 15px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center;">
<input type="date" id="logDateFilter" style="padding: 8px; border: 1px solid #ddd; border-radius: 5px; font-size: 13px;">
<select id="logStatusFilter" style="padding: 8px; border: 1px solid #ddd; border-radius: 5px; font-size: 13px;">
<option value="">全部状态</option>
<option value="success">成功</option>
<option value="failed">失败</option>
</select>
<button class="btn btn-primary" onclick="loadTaskLogs()">筛选</button>
<button class="btn btn-secondary" onclick="clearOldLogs()">清理旧日志</button>
<!-- 筛选区域 -->
<div style="margin-bottom: 15px; padding: 15px; background: #f8f9fa; border-radius: 8px;">
<div style="display: flex; gap: 10px; flex-wrap: wrap; align-items: center;">
<div style="display: flex; align-items: center; gap: 5px;">
<label style="font-size: 12px; color: #666;">日期:</label>
<input type="date" id="logDateFilter" style="padding: 6px 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 13px;">
</div>
<div style="display: flex; align-items: center; gap: 5px;">
<label style="font-size: 12px; color: #666;">状态:</label>
<select id="logStatusFilter" style="padding: 6px 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 13px;">
<option value="">全部</option>
<option value="success">成功</option>
<option value="failed">失败</option>
</select>
</div>
<div style="display: flex; align-items: center; gap: 5px;">
<label style="font-size: 12px; color: #666;">来源:</label>
<select id="logSourceFilter" style="padding: 6px 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 13px;">
<option value="">全部</option>
<option value="manual">手动</option>
<option value="scheduled">定时</option>
<option value="immediate">即时</option>
<option value="resumed">恢复</option>
</select>
</div>
<div style="display: flex; align-items: center; gap: 5px;">
<label style="font-size: 12px; color: #666;">用户:</label>
<select id="logUserFilter" style="padding: 6px 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 13px; min-width: 100px;">
<option value="">全部</option>
</select>
</div>
<div style="display: flex; align-items: center; gap: 5px;">
<label style="font-size: 12px; color: #666;">账号:</label>
<input type="text" id="logAccountFilter" placeholder="输入账号关键字" style="padding: 6px 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 13px; width: 120px;">
</div>
<button class="btn btn-primary" onclick="loadTaskLogs()" style="padding: 6px 15px;">筛选</button>
<button class="btn btn-secondary" onclick="resetLogFilters()" style="padding: 6px 15px;">重置</button>
<button class="btn btn-danger" onclick="clearOldLogs()" style="padding: 6px 15px;">清理旧日志</button>
</div>
</div>
<!-- 日志表格 -->
<div class="table-container">
<table>
<thead>
<tr>
<th>时间</th>
<th>用户</th>
<th>账号</th>
<th>浏览类型</th>
<th>状态</th>
<th>内容数/附件数</th>
<th>执行用时</th>
<th style="width: 140px;">时间</th>
<th style="width: 60px;">来源</th>
<th style="width: 80px;">用户</th>
<th style="width: 100px;">账号</th>
<th style="width: 80px;">浏览类型</th>
<th style="width: 60px;">状态</th>
<th style="width: 90px;">内容/附件</th>
<th style="width: 70px;">用时</th>
<th>失败原因</th>
</tr>
</thead>
<tbody id="taskLogsList">
<tr><td colspan="8" class="empty-message">加载中...</td></tr>
<tr><td colspan="9" class="empty-message">加载中...</td></tr>
</tbody>
</table>
</div>
<div style="margin-top: 15px; text-align: center;">
<button class="btn btn-secondary" onclick="loadMoreLogs()" id="loadMoreBtn">加载更多</button>
<!-- 分页控件 -->
<div id="logsPagination" style="margin-top: 15px; display: flex; justify-content: center; align-items: center; gap: 10px; flex-wrap: wrap;">
<button class="btn btn-secondary" onclick="goToLogPage(1)" id="logFirstBtn" disabled style="padding: 6px 12px;">首页</button>
<button class="btn btn-secondary" onclick="goToLogPage(currentLogPage - 1)" id="logPrevBtn" disabled style="padding: 6px 12px;">上一页</button>
<span id="logPageInfo" style="font-size: 13px; color: #666;">第 1 页 / 共 1 页</span>
<button class="btn btn-secondary" onclick="goToLogPage(currentLogPage + 1)" id="logNextBtn" disabled style="padding: 6px 12px;">下一页</button>
<button class="btn btn-secondary" onclick="goToLogPage(totalLogPages)" id="logLastBtn" disabled style="padding: 6px 12px;">末页</button>
<span style="font-size: 12px; color: #999; margin-left: 10px;"><span id="logTotalCount">0</span> 条记录</span>
</div>
</div>
@@ -841,6 +998,14 @@
loadSystemConfig();
loadProxyConfig();
loadPasswordResets(); // 修复: 初始化时也加载密码重置申请
loadFeedbacks(); // 加载反馈统计更新徽章
// 恢复上次的标签页
const lastTab = localStorage.getItem('admin_current_tab') || 'pending';
const tabButton = document.querySelector(`.tab[onclick*="${lastTab}"]`);
if (tabButton) {
tabButton.click();
}
});
// 切换标签
@@ -855,6 +1020,9 @@
});
document.getElementById(`tab-${tabName}`).classList.add('active');
// 保存当前标签到localStorage
localStorage.setItem('admin_current_tab', tabName);
// 切换到统计标签时加载服务器信息和任务统计
if (tabName === 'stats') {
loadServerInfo();
@@ -866,6 +1034,11 @@
if (tabName === 'logs') {
loadTaskLogs();
}
// 切换到反馈管理标签时加载反馈列表
if (tabName === 'feedbacks') {
loadFeedbacks();
}
}
// VIP functions
@@ -1200,7 +1373,7 @@
setTimeout(() => {
notification.style.display = 'none';
}, 3000);
}, 1000);
}
// 系统配置功能
@@ -1211,6 +1384,7 @@
const config = await response.json();
document.getElementById('maxConcurrent').value = config.max_concurrent_global || 2;
document.getElementById('maxConcurrentPerAccount').value = config.max_concurrent_per_account || 1;
document.getElementById('maxScreenshotConcurrent').value = config.max_screenshot_concurrent || 3;
document.getElementById('scheduleEnabled').checked = config.schedule_enabled === 1;
document.getElementById('scheduleTime').value = config.schedule_time || '02:00';
document.getElementById('scheduleBrowseType').value = config.schedule_browse_type || '应读';
@@ -1333,6 +1507,7 @@
async function updateConcurrency() {
const maxConcurrent = parseInt(document.getElementById('maxConcurrent').value);
const maxConcurrentPerAccount = parseInt(document.getElementById('maxConcurrentPerAccount').value);
const maxScreenshotConcurrent = parseInt(document.getElementById('maxScreenshotConcurrent').value);
if (maxConcurrent < 1 || maxConcurrent > 20) {
showNotification('全局并发数必须在1-20之间', 'error');
@@ -1344,7 +1519,12 @@
return;
}
if (!confirm(`确定更新并发配置吗?\n\n全局并发数: ${maxConcurrent}\n单账号并发数: ${maxConcurrentPerAccount}\n\n建议值服务器内存1.7GB时全局设置2-3单账号设置1-2`)) return;
if (maxScreenshotConcurrent < 1 || maxScreenshotConcurrent > 5) {
showNotification('截图并发数必须在1-5之间', 'error');
return;
}
if (!confirm(`确定更新并发配置吗?\n\n全局并发数: ${maxConcurrent}\n单账号并发数: ${maxConcurrentPerAccount}\n\n建议并发数影响任务执行速度过高可能触发目标服务器限制。全局建议2-5单账号建议1-2`)) return;
try {
const response = await fetch('/yuyx/api/system/config', {
@@ -1352,7 +1532,8 @@
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
max_concurrent_global: maxConcurrent,
max_concurrent_per_account: maxConcurrentPerAccount
max_concurrent_per_account: maxConcurrentPerAccount,
max_screenshot_concurrent: maxScreenshotConcurrent
})
});
@@ -1416,9 +1597,46 @@
}
}
// 任务统计和日志功能
let logsOffset = 0;
const logsLimit = 50;
// 立即执行定时任务
async function executeScheduleNow() {
const browseType = document.getElementById('scheduleBrowseType').value;
const message = `确定要立即执行定时任务吗?\n\n这将执行所有账号的浏览任务\n浏览类型: ${browseType}\n\n注意:无视定时时间和执行日期配置,立即开始执行!`;
if (!confirm(message)) return;
try {
// 禁用按钮,防止重复点击
const button = event.target;
button.disabled = true;
button.textContent = '⏳ 执行中...';
const response = await fetch('/yuyx/api/schedule/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
const data = await response.json();
if (response.ok) {
showNotification(data.message || '定时任务已开始执行', 'success');
} else {
showNotification(data.error || '执行失败', 'error');
}
// 恢复按钮状态
setTimeout(() => {
button.disabled = false;
button.textContent = '⚡ 立即执行';
}, 1000);
} catch (error) {
showNotification('执行失败: ' + error.message, 'error');
// 恢复按钮状态
setTimeout(() => {
button.disabled = false;
button.textContent = '⚡ 立即执行';
}, 1000);
}
}
async function loadServerInfo() {
try {
@@ -1495,33 +1713,181 @@
}
}
async function loadTaskLogs(reset = true) {
async function loadRunningTasks() {
try {
if (reset) {
logsOffset = 0;
}
const response = await fetch('/yuyx/api/task/running');
if (response.ok) {
const data = await response.json();
// 更新计数
document.getElementById('runningTaskCount').textContent = data.running_count;
document.getElementById('queuingTaskCount').textContent = data.queuing_count;
document.getElementById('maxConcurrentDisplay').textContent = data.max_concurrent;
// 来源显示映射
const sourceMap = {
'manual': {text: '手动', color: '#28a745'},
'scheduled': {text: '定时', color: '#007bff'},
'immediate': {text: '即时', color: '#fd7e14'},
'resumed': {text: '恢复', color: '#6c757d'}
};
// 渲染运行中的任务
const runningList = document.getElementById('runningTasksList');
if (data.running.length === 0) {
runningList.innerHTML = '<div style="color: #999; text-align: center; padding: 10px;">暂无运行中的任务</div>';
} else {
runningList.innerHTML = data.running.map(task => {
const source = sourceMap[task.source] || {text: task.source, color: '#666'};
// 状态颜色映射
const statusColorMap = {
'初始化': '#6c757d',
'正在登录': '#fd7e14',
'正在浏览': '#28a745',
'浏览完成': '#007bff',
'正在截图': '#17a2b8'
};
const statusColor = statusColorMap[task.detail_status] || '#666';
// 进度显示
const progressText = task.progress_items > 0 || task.progress_attachments > 0
? `(${task.progress_items}/${task.progress_attachments})`
: '';
return `<div style="display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: #f8f9fa; border-radius: 5px; margin-bottom: 5px; border-left: 3px solid ${statusColor};">
<div style="flex: 1;">
<div style="display: flex; align-items: center; flex-wrap: wrap; gap: 5px;">
<span style="color: ${source.color}; font-weight: 500; font-size: 12px;">[${source.text}]</span>
<span style="color: #333;">${task.user_username}</span>
<span style="color: #666;">→</span>
<span style="color: #007bff; font-weight: 500;">${task.username}</span>
<span style="background: #e9ecef; padding: 2px 6px; border-radius: 3px; font-size: 11px; color: #666;">${task.browse_type}</span>
</div>
<div style="margin-top: 4px; display: flex; align-items: center; gap: 8px;">
<span style="color: ${statusColor}; font-weight: 500; font-size: 12px;">● ${task.detail_status}</span>
${progressText ? `<span style="color: #999; font-size: 11px;">内容/附件: ${progressText}</span>` : ''}
</div>
</div>
<div style="text-align: right; min-width: 70px;">
<div style="color: #28a745; font-weight: 500;">${task.elapsed_display}</div>
</div>
</div>`;
}).join('');
}
// 渲染排队中的任务
const queuingList = document.getElementById('queuingTasksList');
if (data.queuing.length === 0) {
queuingList.innerHTML = '<div style="color: #999; text-align: center; padding: 10px;">暂无排队中的任务</div>';
} else {
queuingList.innerHTML = data.queuing.map(task => {
const source = sourceMap[task.source] || {text: task.source, color: '#666'};
return `<div style="display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: #fff8e6; border-radius: 5px; margin-bottom: 5px; border-left: 3px solid #fd7e14;">
<div style="flex: 1;">
<div style="display: flex; align-items: center; flex-wrap: wrap; gap: 5px;">
<span style="color: ${source.color}; font-weight: 500; font-size: 12px;">[${source.text}]</span>
<span style="color: #333;">${task.user_username}</span>
<span style="color: #666;">→</span>
<span style="color: #007bff; font-weight: 500;">${task.username}</span>
<span style="background: #ffeeba; padding: 2px 6px; border-radius: 3px; font-size: 11px; color: #856404;">${task.browse_type}</span>
</div>
<div style="margin-top: 4px;">
<span style="color: #fd7e14; font-size: 12px;">● ${task.detail_status || '等待资源'}</span>
</div>
</div>
<div style="text-align: right; min-width: 80px;">
<div style="color: #fd7e14; font-weight: 500;">等待 ${task.elapsed_display}</div>
</div>
</div>`;
}).join('');
}
}
} catch (error) {
console.error('加载运行任务失败:', error);
}
}
// 任务统计和日志功能
let currentLogPage = 1;
let totalLogPages = 1;
let totalLogCount = 0;
const logsPerPage = 20;
// 加载用户列表用于筛选
async function loadLogUserOptions() {
try {
const response = await fetch('/yuyx/api/users');
if (response.ok) {
const users = await response.json();
const select = document.getElementById('logUserFilter');
select.innerHTML = '<option value="">全部</option>';
users.forEach(user => {
select.innerHTML += `<option value="${user.id}">${user.username}</option>`;
});
}
} catch (error) {
console.error('加载用户列表失败:', error);
}
}
// 重置筛选条件
function resetLogFilters() {
document.getElementById('logDateFilter').value = '';
document.getElementById('logStatusFilter').value = '';
document.getElementById('logSourceFilter').value = '';
document.getElementById('logUserFilter').value = '';
document.getElementById('logAccountFilter').value = '';
currentLogPage = 1;
loadTaskLogs();
}
// 跳转到指定页
function goToLogPage(page) {
if (page < 1 || page > totalLogPages) return;
currentLogPage = page;
loadTaskLogs();
}
// 更新分页控件状态
function updatePaginationUI() {
document.getElementById('logFirstBtn').disabled = currentLogPage <= 1;
document.getElementById('logPrevBtn').disabled = currentLogPage <= 1;
document.getElementById('logNextBtn').disabled = currentLogPage >= totalLogPages;
document.getElementById('logLastBtn').disabled = currentLogPage >= totalLogPages;
document.getElementById('logPageInfo').textContent = `${currentLogPage} 页 / 共 ${totalLogPages}`;
document.getElementById('logTotalCount').textContent = totalLogCount;
}
async function loadTaskLogs() {
try {
const dateFilter = document.getElementById('logDateFilter').value;
const statusFilter = document.getElementById('logStatusFilter').value;
const sourceFilter = document.getElementById('logSourceFilter').value;
const userFilter = document.getElementById('logUserFilter').value;
const accountFilter = document.getElementById('logAccountFilter').value;
let url = `/yuyx/api/task/logs?limit=${logsLimit}&offset=${logsOffset}`;
const offset = (currentLogPage - 1) * logsPerPage;
let url = `/yuyx/api/task/logs?limit=${logsPerPage}&offset=${offset}`;
if (dateFilter) url += `&date=${dateFilter}`;
if (statusFilter) url += `&status=${statusFilter}`;
if (sourceFilter) url += `&source=${sourceFilter}`;
if (userFilter) url += `&user_id=${userFilter}`;
if (accountFilter) url += `&account=${encodeURIComponent(accountFilter)}`;
const response = await fetch(url);
if (response.ok) {
const logs = await response.json();
const data = await response.json();
const logs = data.logs || data;
totalLogCount = data.total || logs.length;
totalLogPages = Math.max(1, Math.ceil(totalLogCount / logsPerPage));
const tbody = document.getElementById('taskLogsList');
if (logs.length === 0 && reset) {
tbody.innerHTML = '<tr><td colspan="8" class="empty-message">暂无日志记录</td></tr>';
document.getElementById('loadMoreBtn').style.display = 'none';
if (logs.length === 0) {
tbody.innerHTML = '<tr><td colspan="9" class="empty-message">暂无日志记录</td></tr>';
updatePaginationUI();
return;
}
if (reset) {
tbody.innerHTML = '';
}
tbody.innerHTML = '';
logs.forEach(log => {
const row = document.createElement('tr');
@@ -1537,8 +1903,18 @@
return `${minutes}${secs}`;
};
// 来源显示
const sourceMap = {
'manual': {text: '手动', color: '#28a745'},
'scheduled': {text: '定时', color: '#007bff'},
'immediate': {text: '即时', color: '#fd7e14'},
'resumed': {text: '恢复', color: '#6c757d'}
};
const sourceInfo = sourceMap[log.source] || {text: log.source || '手动', color: '#28a745'};
row.innerHTML = `
<td>${log.created_at}</td>
<td><span style="color: ${sourceInfo.color}; font-weight: 500;">${sourceInfo.text}</span></td>
<td>${log.user_username || 'N/A'}</td>
<td>${log.username}</td>
<td>${log.browse_type}</td>
@@ -1550,19 +1926,13 @@
tbody.appendChild(row);
});
// 显示/隐藏加载更多按钮
document.getElementById('loadMoreBtn').style.display = logs.length < logsLimit ? 'none' : 'inline-block';
updatePaginationUI();
}
} catch (error) {
console.error('加载任务日志失败:', error);
}
}
function loadMoreLogs() {
logsOffset += logsLimit;
loadTaskLogs(false);
}
async function clearOldLogs() {
const days = prompt('请输入要清理多少天前的日志默认30天:', '30');
if (days === null) return;
@@ -1595,14 +1965,32 @@
}
// 修改switchTab函数在切换到统计和日志标签时加载数据
let statsRefreshInterval = null;
const originalSwitchTab = switchTab;
switchTab = function(tabName) {
originalSwitchTab(tabName);
// 清除之前的定时刷新
if (statsRefreshInterval) {
clearInterval(statsRefreshInterval);
statsRefreshInterval = null;
}
if (tabName === 'stats') {
loadServerInfo();
loadDockerStats();
loadTaskStats();
// 每1秒自动刷新统计信息
statsRefreshInterval = setInterval(() => {
loadServerInfo();
loadDockerStats();
loadTaskStats();
loadRunningTasks();
}, 1000);
// 首次立即加载运行任务
loadRunningTasks();
} else if (tabName === 'logs') {
loadLogUserOptions();
loadTaskLogs();
} else if (tabName === 'pending') {
loadPasswordResets();
@@ -1739,6 +2127,153 @@
showNotification('重置失败: ' + error.message, 'error');
}
}
// ==================== 反馈管理功能 ====================
let feedbacksList = [];
// 加载反馈列表
async function loadFeedbacks() {
try {
const statusFilter = document.getElementById('feedbackStatusFilter').value;
let url = '/yuyx/api/feedbacks';
if (statusFilter) {
url += '?status=' + statusFilter;
}
const response = await fetch(url);
if (response.ok) {
const data = await response.json();
feedbacksList = data.feedbacks || [];
const stats = data.stats || {};
document.getElementById('statTotal').textContent = stats.total || 0;
document.getElementById('statPending').textContent = stats.pending || 0;
document.getElementById('statReplied').textContent = stats.replied || 0;
document.getElementById('statClosed').textContent = stats.closed || 0;
const badge = document.getElementById('feedbackBadge');
if (stats.pending > 0) {
badge.textContent = stats.pending;
badge.style.display = 'inline';
} else {
badge.style.display = 'none';
}
renderFeedbacks();
}
} catch (error) {
console.error('加载反馈列表失败:', error);
showNotification('加载反馈列表失败: ' + error.message, 'error');
}
}
function renderFeedbacks() {
const container = document.getElementById('feedbacksList');
if (feedbacksList.length === 0) {
container.innerHTML = '<div class="empty-message">暂无反馈记录</div>';
return;
}
const getStatusBadge = (status) => {
const statusMap = {
'pending': { text: '待处理', cls: 'status-pending' },
'replied': { text: '已回复', cls: 'status-approved' },
'closed': { text: '已关闭', cls: 'status-rejected' }
};
const s = statusMap[status] || { text: status, cls: '' };
return '<span class="status-badge ' + s.cls + '">' + s.text + '</span>';
};
let html = '<div class="table-container"><table><thead><tr>';
html += '<th>ID</th><th>用户</th><th>标题</th><th>描述</th><th>联系方式</th><th>状态</th><th>提交时间</th><th>回复</th><th>操作</th>';
html += '</tr></thead><tbody>';
feedbacksList.forEach(fb => {
html += '<tr>';
html += '<td>' + fb.id + '</td>';
html += '<td><strong>' + (fb.username || 'N/A') + '</strong></td>';
html += '<td style="max-width:150px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="' + (fb.title||'') + '">' + (fb.title||'') + '</td>';
html += '<td style="max-width:200px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="' + (fb.description||'') + '">' + (fb.description||'') + '</td>';
html += '<td>' + (fb.contact || '-') + '</td>';
html += '<td>' + getStatusBadge(fb.status) + '</td>';
html += '<td>' + fb.created_at + '</td>';
html += '<td style="max-width:150px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="' + (fb.admin_reply || '') + '">' + (fb.admin_reply || '-') + '</td>';
html += '<td><div class="action-buttons">';
if (fb.status !== 'closed') {
html += '<button class="btn btn-small btn-primary" onclick="replyFeedback(' + fb.id + ')">回复</button>';
html += '<button class="btn btn-small btn-secondary" onclick="closeFeedback(' + fb.id + ')">关闭</button>';
}
html += '<button class="btn btn-small btn-danger" onclick="deleteFeedback(' + fb.id + ')">删除</button>';
html += '</div></td></tr>';
});
html += '</tbody></table></div>';
container.innerHTML = html;
}
async function replyFeedback(feedbackId) {
const reply = prompt('请输入回复内容:');
if (!reply || !reply.trim()) return;
try {
const response = await fetch('/yuyx/api/feedbacks/' + feedbackId + '/reply', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reply: reply.trim() })
});
const data = await response.json();
if (response.ok) {
showNotification('回复成功', 'success');
loadFeedbacks();
} else {
showNotification('回复失败: ' + (data.error || '未知错误'), 'error');
}
} catch (error) {
showNotification('回复失败: ' + error.message, 'error');
}
}
async function closeFeedback(feedbackId) {
if (!confirm('确定要关闭这个反馈吗?')) return;
try {
const response = await fetch('/yuyx/api/feedbacks/' + feedbackId + '/close', {
method: 'POST'
});
const data = await response.json();
if (response.ok) {
showNotification('反馈已关闭', 'success');
loadFeedbacks();
} else {
showNotification('关闭失败: ' + (data.error || '未知错误'), 'error');
}
} catch (error) {
showNotification('关闭失败: ' + error.message, 'error');
}
}
async function deleteFeedback(feedbackId) {
if (!confirm('确定要删除这个反馈吗?此操作不可恢复!')) return;
try {
const response = await fetch('/yuyx/api/feedbacks/' + feedbackId, {
method: 'DELETE'
});
const data = await response.json();
if (response.ok) {
showNotification('反馈已删除', 'success');
loadFeedbacks();
} else {
showNotification('删除失败: ' + (data.error || '未知错误'), 'error');
}
} catch (error) {
showNotification('删除失败: ' + error.message, 'error');
}
}
</script>
</body>
</html>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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';

File diff suppressed because it is too large Load Diff

1474
templates/index.html.backup2 Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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>