Files
zsglpt/templates/admin_legacy.html
yuyx 4c492122dd feat: support announcement image upload
# Conflicts:
#	database.py
#	db/migrations.py
#	routes/admin_api/core.py
#	static/admin/.vite/manifest.json
#	static/admin/assets/AnnouncementsPage-Btl9JP7M.js
#	static/admin/assets/EmailPage-CwqlBGU2.js
#	static/admin/assets/FeedbacksPage-B_qDNL3q.js
#	static/admin/assets/LogsPage-DzdymdrQ.js
#	static/admin/assets/ReportPage-Bp26gOA-.js
#	static/admin/assets/SettingsPage-__r25pN8.js
#	static/admin/assets/SystemPage-C1OfxrU-.js
#	static/admin/assets/UsersPage-DhnABKcY.js
#	static/admin/assets/email-By53DCWv.js
#	static/admin/assets/email-ByiJ74rd.js
#	static/admin/assets/email-DkWacopQ.js
#	static/admin/assets/index-D5wU2pVd.js
#	static/admin/assets/tasks-1acmkoIX.js
#	static/admin/assets/update-DdQLVpC3.js
#	static/admin/assets/users-B1w166uc.js
#	static/admin/assets/users-CPJP5r-B.js
#	static/admin/assets/users-CnIyvFWm.js
#	static/admin/index.html
#	static/app/.vite/manifest.json
#	static/app/assets/AccountsPage-C48gJL8c.js
#	static/app/assets/AccountsPage-D387XNsv.js
#	static/app/assets/AccountsPage-DBJCAsJz.js
#	static/app/assets/LoginPage-BgK_Vl6X.js
#	static/app/assets/RegisterPage-CwADxWfe.js
#	static/app/assets/ResetPasswordPage-CVfZX_5z.js
#	static/app/assets/SchedulesPage-CWuZpJ5h.js
#	static/app/assets/SchedulesPage-Dw-mXbG5.js
#	static/app/assets/SchedulesPage-DwzGOBuc.js
#	static/app/assets/ScreenshotsPage-C6vX2U3V.js
#	static/app/assets/ScreenshotsPage-CreOSjVc.js
#	static/app/assets/ScreenshotsPage-DuTeRzLR.js
#	static/app/assets/VerifyResultPage-BzGlCgtE.js
#	static/app/assets/VerifyResultPage-CN_nr4V6.js
#	static/app/assets/VerifyResultPage-CNbQc83z.js
#	static/app/assets/accounts-BFaVMUve.js
#	static/app/assets/accounts-BYq3lLev.js
#	static/app/assets/accounts-Bc9j2moH.js
#	static/app/assets/auth-Dk_ApO4B.js
#	static/app/assets/index-BIng7uZJ.css
#	static/app/assets/index-CDxVo_1Z.js
#	static/app/index.html
2026-01-06 12:15:16 +08:00

3521 lines
159 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>后台管理 v2.0 - 知识管理平台</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: #f5f6fa;
min-height: 100vh;
}
.header {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
padding: 12px 15px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.header-content {
max-width: 1400px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.header h1 {
font-size: 16px;
white-space: nowrap;
}
.header-actions {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
.header-actions > span {
display: inline;
font-size: 12px;
}
.btn {
padding: 6px 12px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
white-space: nowrap;
}
.btn-logout {
background: rgba(255,255,255,0.2);
color: white;
border: 1px solid white;
}
.btn-logout:hover {
background: rgba(255,255,255,0.3);
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 15px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-bottom: 20px;
}
.stat-card {
background: white;
padding: 15px 12px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
text-align: center;
}
.stat-value {
font-size: 28px;
font-weight: bold;
color: #f5576c;
margin-bottom: 5px;
}
.stat-label {
color: #666;
font-size: 12px;
}
.panel {
background: white;
border-radius: 10px;
padding: 15px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
margin-bottom: 15px;
}
.panel-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
color: #333;
border-bottom: 2px solid #f5576c;
padding-bottom: 8px;
}
.tabs {
display: flex;
gap: 5px;
margin-bottom: 15px;
border-bottom: 1px solid #ddd;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.tab {
padding: 10px 15px;
background: none;
border: none;
border-bottom: 3px solid transparent;
cursor: pointer;
font-size: 13px;
color: #666;
transition: all 0.3s;
white-space: nowrap;
flex-shrink: 0;
}
.tab.active {
color: #f5576c;
border-bottom-color: #f5576c;
font-weight: bold;
}
.tab:hover {
color: #f5576c;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.table-container {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
table {
width: 100%;
border-collapse: collapse;
min-width: 600px;
}
th {
background: #f8f9fa;
padding: 10px 8px;
text-align: left;
color: #666;
font-weight: bold;
font-size: 12px;
position: sticky;
top: 0;
}
td {
padding: 10px 8px;
border-bottom: 1px solid #f0f0f0;
font-size: 12px;
}
tr:hover {
background: #f8f9fa;
}
.status-badge {
padding: 3px 10px;
border-radius: 12px;
font-size: 11px;
font-weight: bold;
white-space: nowrap;
}
.status-pending {
background: #fff3cd;
color: #856404;
}
.status-approved {
background: #d4edda;
color: #155724;
}
.status-rejected {
background: #f8d7da;
color: #721c24;
}
.btn-small {
padding: 5px 8px;
font-size: 11px;
margin: 2px;
display: inline-block;
}
.btn-success {
background: #28a745;
color: white;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-primary {
background: #f5576c;
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 6px;
color: #333;
font-weight: bold;
font-size: 13px;
}
.form-group input {
width: 100%;
max-width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 14px;
}
.form-group textarea {
width: 100%;
max-width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 14px;
font-family: inherit;
resize: vertical;
min-height: 120px;
}
.empty-message {
text-align: center;
padding: 30px 15px;
color: #999;
font-size: 13px;
}
.notification {
position: fixed;
top: 15px;
right: 15px;
left: 15px;
padding: 12px 15px;
background: white;
border-radius: 5px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
display: none;
z-index: 1000;
font-size: 13px;
}
.notification.success {
border-left: 4px solid #28a745;
}
.notification.error {
border-left: 4px solid #dc3545;
}
.user-info {
font-size: 11px;
color: #666;
margin-top: 3px;
}
.vip-badge-inline {
background: linear-gradient(135deg,#667eea 0%,#764ba2 100%);
color: white;
padding: 2px 8px;
border-radius: 10px;
font-size: 10px;
margin-left: 3px;
display: inline-block;
}
.normal-badge-inline {
background: #e0e0e0;
color: #666;
padding: 2px 8px;
border-radius: 10px;
font-size: 10px;
margin-left: 3px;
display: inline-block;
}
.action-buttons {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
/* 移动端优化 */
@media (max-width: 767px) {
* { -webkit-tap-highlight-color: transparent; }
body { overflow-x: hidden; }
.header {
padding: 8px 10px;
min-height: auto;
}
.header-content {
flex-wrap: wrap;
gap: 6px;
}
.header h1 {
font-size: 14px;
flex: 1 1 auto;
min-width: 0;
}
.header-actions {
gap: 4px;
flex-wrap: wrap;
justify-content: flex-end;
flex: 0 0 auto;
}
.header-actions > span {
font-size: 10px;
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
}
.btn {
padding: 4px 8px;
font-size: 10px;
white-space: nowrap;
}
.btn-logout {
padding: 4px 8px;
font-size: 10px;
}
.container {
padding: 10px;
max-width: 100%;
overflow-x: hidden;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
gap: 6px;
margin-bottom: 12px;
}
.stat-card { padding: 10px 8px; }
.stat-value { font-size: 20px; }
.stat-label { font-size: 10px; }
.panel {
padding: 10px;
margin-bottom: 10px;
overflow-x: hidden;
}
.panel-title {
font-size: 14px;
margin-bottom: 10px;
padding-bottom: 6px;
}
.tabs {
gap: 4px;
margin-bottom: 10px;
padding-bottom: 0;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
display: flex;
flex-wrap: nowrap;
}
.tab {
padding: 7px 10px;
font-size: 11px;
flex-shrink: 0;
white-space: nowrap;
}
.table-container {
margin: -10px;
padding: 10px 0;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
table {
min-width: 800px;
font-size: 10px;
}
th { padding: 6px 4px; font-size: 10px; }
td { padding: 6px 4px; font-size: 10px; }
.status-badge {
padding: 2px 6px;
font-size: 9px;
white-space: nowrap;
}
.btn-small {
padding: 3px 5px;
font-size: 9px;
margin: 1px;
white-space: nowrap;
}
.action-buttons {
gap: 2px;
display: flex;
flex-wrap: wrap;
}
.form-group { margin-bottom: 10px; }
.form-group label {
font-size: 12px;
margin-bottom: 4px;
}
.form-group input,
.form-group select {
padding: 8px;
font-size: 14px;
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
.notification {
top: 10px;
right: 10px;
left: 10px;
padding: 8px 10px;
font-size: 11px;
max-width: calc(100% - 20px);
}
.user-info {
font-size: 9px;
margin-top: 2px;
}
.vip-badge-inline, .normal-badge-inline {
font-size: 8px;
padding: 2px 5px;
}
.empty-message {
padding: 16px 8px;
font-size: 11px;
}
/* 系统状态卡片优化 */
[style*="display: flex"][style*="justify-content: space-between"] {
flex-wrap: wrap !important;
gap: 8px !important;
}
/* 系统配置表单优化 */
#scheduleWeekdaysGroup {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
#scheduleWeekdaysGroup label {
font-size: 11px;
padding: 4px 7px;
flex: 0 0 auto;
}
/* 分页控件优化 */
#logsPagination {
gap: 4px;
padding: 8px 0;
flex-wrap: wrap;
justify-content: center;
}
#logsPagination button {
padding: 4px 7px;
font-size: 10px;
}
#logsPagination span {
font-size: 10px;
}
/* 模态窗口优化 */
.modal {
max-width: calc(100% - 20px) !important;
}
/* 操作按钮组优化 */
td .action-buttons,
.panel .action-buttons {
justify-content: flex-start;
}
}
/* 平板及以上屏幕 */
@media (min-width: 768px) {
.header {
padding: 20px 25px;
}
.header h1 {
font-size: 22px;
}
.header-actions > span {
display: inline;
}
.btn {
padding: 8px 14px;
font-size: 13px;
}
.container {
margin: 25px auto;
padding: 0 20px;
}
.stats-grid {
grid-template-columns: repeat(5, 1fr);
gap: 15px;
margin-bottom: 25px;
}
.stat-card {
padding: 20px 15px;
}
.stat-value {
font-size: 32px;
margin-bottom: 8px;
}
.stat-label {
font-size: 13px;
}
.panel {
padding: 20px;
margin-bottom: 20px;
}
.panel-title {
font-size: 18px;
margin-bottom: 18px;
}
.tabs {
gap: 8px;
margin-bottom: 18px;
}
.tab {
padding: 12px 20px;
font-size: 14px;
}
table {
min-width: auto;
}
th {
padding: 12px;
font-size: 13px;
}
td {
padding: 12px;
font-size: 13px;
}
.status-badge {
padding: 4px 12px;
font-size: 12px;
}
.btn-small {
padding: 6px 10px;
font-size: 12px;
margin-right: 3px;
}
.form-group label {
font-size: 14px;
margin-bottom: 8px;
}
.form-group input {
max-width: 400px;
}
.notification {
right: 20px;
left: auto;
max-width: 400px;
padding: 15px 20px;
font-size: 14px;
}
.user-info {
font-size: 12px;
}
.vip-badge-inline, .normal-badge-inline {
font-size: 11px;
padding: 3px 10px;
}
}
/* PC屏幕 */
@media (min-width: 1024px) {
.header h1 {
font-size: 24px;
}
.btn {
padding: 8px 16px;
font-size: 14px;
}
.stat-card {
padding: 25px;
}
.stat-value {
font-size: 36px;
margin-bottom: 10px;
}
.stat-label {
font-size: 14px;
}
.panel {
padding: 25px;
}
.panel-title {
font-size: 20px;
margin-bottom: 20px;
padding-bottom: 10px;
}
.tabs {
gap: 10px;
margin-bottom: 20px;
}
.tab {
padding: 12px 24px;
font-size: 15px;
}
.btn-small {
padding: 6px 12px;
font-size: 12px;
margin-right: 5px;
}
}
</style>
</head>
<body>
<div class="header">
<div class="header-content">
<h1>后台管理系统</h1>
<div class="header-actions">
<span>管理员:</span><span id="admin-username" style="font-weight: bold;"></span>
<button class="btn btn-logout" onclick="logout()">退出</button>
</div>
</div>
</div>
<div class="container">
<!-- 统计面板 -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" id="totalUsers">0</div>
<div class="stat-label">总用户数</div>
</div>
<div class="stat-card">
<div class="stat-value" id="approvedUsers">0</div>
<div class="stat-label">已审核</div>
</div>
<div class="stat-card">
<div class="stat-value" id="pendingUsers">0</div>
<div class="stat-label">待审核</div>
</div>
<div class="stat-card">
<div class="stat-value" id="totalAccounts">0</div>
<div class="stat-label">总账号数</div>
</div>
<div class="stat-card">
<div class="stat-value" id="vipUsers">0</div>
<div class="stat-label">VIP用户</div>
</div>
</div>
<!-- 主面板 -->
<div class="panel">
<div class="tabs">
<button class="tab active" onclick="switchTab('pending')">待审核</button>
<button class="tab" onclick="switchTab('all')">所有用户</button>
<button class="tab" onclick="switchTab('feedbacks')">反馈管理 <span id="feedbackBadge" style="background:#e74c3c; color:white; padding:2px 6px; border-radius:10px; font-size:11px; margin-left:3px; display:none;">0</span></button>
<button class="tab" onclick="switchTab('stats')">统计</button>
<button class="tab" onclick="switchTab('logs')">任务日志</button>
<button class="tab" onclick="switchTab('announcements')">公告管理</button>
<button class="tab" onclick="switchTab('email')">邮件配置</button>
<button class="tab" onclick="switchTab('system')">系统配置</button>
<button class="tab" onclick="switchTab('settings')">设置</button>
</div>
<!-- 待审核用户 -->
<div id="tab-pending" class="tab-content active">
<h3 style="margin-bottom: 15px; font-size: 16px;">用户注册审核</h3>
<div id="pendingUsersList"></div>
</div>
<!-- 所有用户 -->
<div id="tab-all" class="tab-content">
<div id="allUsersList"></div>
</div>
<!-- 反馈管理 -->
<div id="tab-feedbacks" class="tab-content">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px;">
<h3 style="font-size:16px;">用户反馈管理</h3>
<div style="display:flex; gap:10px;">
<select id="feedbackStatusFilter" onchange="loadFeedbacks()" style="padding:8px; border:1px solid #ddd; border-radius:5px;">
<option value="">全部状态</option>
<option value="pending">待处理</option>
<option value="replied">已回复</option>
<option value="closed">已关闭</option>
</select>
<button onclick="loadFeedbacks()" class="btn btn-primary" style="padding:8px 15px;">刷新</button>
</div>
</div>
<div id="feedbackStats" style="display:flex; gap:15px; margin-bottom:15px; padding:10px; background:#f5f5f5; border-radius:5px;">
<span>总计: <strong id="statTotal">0</strong></span>
<span style="color:#f39c12;">待处理: <strong id="statPending">0</strong></span>
<span style="color:#27ae60;">已回复: <strong id="statReplied">0</strong></span>
<span style="color:#95a5a6;">已关闭: <strong id="statClosed">0</strong></span>
</div>
<div id="feedbacksList"></div>
</div>
<!-- 系统配置 -->
<div id="tab-system" class="tab-content">
<h3 style="margin-bottom: 15px; font-size: 16px;">系统并发配置</h3>
<div class="form-group">
<label>全局最大并发数</label>
<input type="number" id="maxConcurrent" min="1" value="2" style="max-width: 200px;">
<div style="font-size: 12px; color: #666; margin-top: 5px;">
说明同时最多运行的账号数量。浏览任务使用API方式资源占用极低。
</div>
</div>
<div class="form-group">
<label>单账号最大并发数</label>
<input type="number" id="maxConcurrentPerAccount" min="1" value="1" style="max-width: 200px;">
<div style="font-size: 12px; color: #666; margin-top: 5px;">
说明:单个账号同时最多运行的任务数量。
</div>
</div>
<div class="form-group">
<label>截图最大并发数</label>
<input type="number" id="maxScreenshotConcurrent" min="1" value="3" style="max-width: 200px;">
<div style="font-size: 12px; color: #666; margin-top: 5px;">
说明同时进行截图的最大数量。wkhtmltoimage 资源占用较低,可按需提高。
</div>
</div>
<button class="btn btn-primary" style="margin-top: 10px;" onclick="updateConcurrency()">保存并发配置</button>
<h3 style="margin: 25px 0 15px 0; font-size: 16px;">定时任务配置</h3>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="scheduleEnabled" onchange="toggleSchedule(this.checked)" style="width: auto; max-width: none;">
启用定时任务
</label>
<div style="font-size: 12px; color: #666; margin-top: 5px;">
开启后,系统将在指定时间自动执行所有账号的浏览任务,是否截图由下方开关决定。
</div>
</div>
<div class="form-group" id="scheduleTimeGroup" style="display: none;">
<label>执行时间</label>
<input type="time" id="scheduleTime" value="02:00" style="max-width: 200px;">
<div style="font-size: 12px; color: #666; margin-top: 5px;">
每天在此时间自动执行所有账号任务
</div>
</div>
<div class="form-group" id="scheduleBrowseTypeGroup" style="display: none;">
<label>浏览类型</label>
<select id="scheduleBrowseType" style="max-width: 200px; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px;">
<option value="注册前未读">注册前未读</option>
<option value="应读" selected>应读</option>
</select>
</div>
<div class="form-group" id="scheduleWeekdaysGroup" style="display: none;">
<label>执行日期</label>
<div style="display: flex; gap: 10px; flex-wrap: wrap; margin-top: 10px;">
<label style="display: flex; align-items: center; gap: 5px; cursor: pointer;">
<input type="checkbox" class="weekday-checkbox" value="1" checked>
<span>周一</span>
</label>
<label style="display: flex; align-items: center; gap: 5px; cursor: pointer;">
<input type="checkbox" class="weekday-checkbox" value="2" checked>
<span>周二</span>
</label>
<label style="display: flex; align-items: center; gap: 5px; cursor: pointer;">
<input type="checkbox" class="weekday-checkbox" value="3" checked>
<span>周三</span>
</label>
<label style="display: flex; align-items: center; gap: 5px; cursor: pointer;">
<input type="checkbox" class="weekday-checkbox" value="4" checked>
<span>周四</span>
</label>
<label style="display: flex; align-items: center; gap: 5px; cursor: pointer;">
<input type="checkbox" class="weekday-checkbox" value="5" checked>
<span>周五</span>
</label>
<label style="display: flex; align-items: center; gap: 5px; cursor: pointer;">
<input type="checkbox" class="weekday-checkbox" value="6" checked>
<span>周六</span>
</label>
<label style="display: flex; align-items: center; gap: 5px; cursor: pointer;">
<input type="checkbox" class="weekday-checkbox" value="7" checked>
<span>周日</span>
</label>
</div>
<div style="font-size: 12px; color: #666; margin-top: 5px;">
选择定时任务在哪些天执行
</div>
</div>
<div class="form-group" id="scheduleScreenshotGroup" style="display: none;">
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="enableScreenshot" style="width: auto; max-width: none;">
定时任务截图
</label>
<div style="font-size: 12px; color: #666; margin-top: 5px;">
开启后,定时任务执行时会生成截图。
</div>
</div>
<div id="scheduleActions" style="margin-top: 15px; display: flex; gap: 10px;">
<button class="btn btn-primary" onclick="updateSchedule()">保存定时任务配置</button>
<button class="btn btn-success" onclick="executeScheduleNow()" style="background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);">
⚡ 立即执行
</button>
</div>
<!-- ========== 代理设置 ========== -->
<div style="border-top: 2px solid #f0f0f0; margin-top: 40px; padding-top: 25px; background-color: #fff9e6; padding: 20px; border-radius: 8px;">
<h3 style="margin-bottom: 15px; font-size: 16px;">🌐 代理设置</h3>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="proxyEnabled" style="width: auto; max-width: none;">
启用IP代理
</label>
<div style="font-size: 12px; color: #666; margin-top: 5px;">
开启后所有浏览任务将通过代理IP访问失败自动重试3次
</div>
</div>
<div class="form-group">
<label>代理API地址</label>
<input type="text" id="proxyApiUrl" placeholder="http://api.xydaili.net:2022/Tools/IP.ashx?..." style="width: 100%; max-width: 600px; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px;">
<div style="font-size: 11px; color: #666; margin-top: 5px;">
API应返回格式: <code>IP:PORT</code> (例如: 123.45.67.89:8888)
</div>
</div>
<div class="form-group">
<label>代理有效期(分钟)</label>
<input type="number" id="proxyExpireMinutes" min="1" max="60" value="3" style="max-width: 200px; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px;">
<div style="font-size: 11px; color: #666; margin-top: 5px;">
代理IP的有效使用时长根据你的代理服务商设置
</div>
</div>
<div style="display: flex; gap: 10px; margin-top: 15px;">
<button class="btn btn-primary" onclick="saveProxyConfig()">💾 保存代理配置</button>
<button class="btn btn-secondary" onclick="testProxy()">🧪 测试代理</button>
</div>
</div>
<!-- ========== 代理设置结束 ========== -->
<!-- ========== 注册自动审核设置 ========== -->
<div style="border-top: 2px solid #f0f0f0; margin-top: 40px; padding-top: 25px; background-color: #e8f5e9; padding: 20px; border-radius: 8px;">
<h3 style="margin-bottom: 15px; font-size: 16px;">✅ 注册自动审核</h3>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="autoApproveEnabled" style="width: auto; max-width: none;">
启用自动审核
</label>
<div style="font-size: 12px; color: #666; margin-top: 5px;">
开启后,新用户注册将自动通过审核,无需管理员手动审批
</div>
</div>
<div class="form-group">
<label>每小时注册限制</label>
<input type="number" id="autoApproveHourlyLimit" min="1" value="10" style="max-width: 200px; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px;">
<div style="font-size: 12px; color: #666; margin-top: 5px;">
限制每小时内最多允许注册的用户数量,防止恶意注册
</div>
</div>
<div class="form-group">
<label>注册赠送VIP天数</label>
<input type="number" id="autoApproveVipDays" min="0" value="7" style="max-width: 200px; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px;">
<div style="font-size: 12px; color: #666; margin-top: 5px;">
新用户注册成功后自动赠送的VIP天数设为0表示不赠送
</div>
</div>
<div style="margin-top: 15px;">
<button class="btn btn-primary" onclick="saveAutoApproveConfig()">💾 保存自动审核配置</button>
</div>
</div>
<!-- ========== 注册自动审核结束 ========== -->
</div>
</div>
<!-- 统计 -->
<div id="tab-stats" class="tab-content">
<!-- 系统状态概览 - 精简合并版 -->
<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>
<!-- 隐藏的元素保持JS兼容性 -->
<div style="display:none;">
<span id="dockerContainerName"></span>
<span id="dockerUptime"></span>
<span id="dockerStatus"></span>
</div>
<!-- 实时任务监控 -->
<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(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>
<!-- 任务统计 - 合并当日和累计 -->
<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: 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; padding: 15px; background: #f8f9fa; border-radius: 8px;">
<div style="display: flex; gap: 10px; flex-wrap: wrap; align-items: center;">
<div style="display: flex; align-items: center; gap: 5px;">
<label style="font-size: 12px; color: #666;">日期:</label>
<input type="date" id="logDateFilter" style="padding: 6px 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 13px;">
</div>
<div style="display: flex; align-items: center; gap: 5px;">
<label style="font-size: 12px; color: #666;">状态:</label>
<select id="logStatusFilter" style="padding: 6px 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 13px;">
<option value="">全部</option>
<option value="success">成功</option>
<option value="failed">失败</option>
</select>
</div>
<div style="display: flex; align-items: center; gap: 5px;">
<label style="font-size: 12px; color: #666;">来源:</label>
<select id="logSourceFilter" style="padding: 6px 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 13px;">
<option value="">全部</option>
<option value="manual">手动</option>
<option value="scheduled">定时</option>
<option value="immediate">即时</option>
<option value="resumed">恢复</option>
</select>
</div>
<div style="display: flex; align-items: center; gap: 5px;">
<label style="font-size: 12px; color: #666;">用户:</label>
<select id="logUserFilter" style="padding: 6px 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 13px; min-width: 100px;">
<option value="">全部</option>
</select>
</div>
<div style="display: flex; align-items: center; gap: 5px;">
<label style="font-size: 12px; color: #666;">账号:</label>
<input type="text" id="logAccountFilter" placeholder="输入账号关键字" style="padding: 6px 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 13px; width: 120px;">
</div>
<button class="btn btn-primary" onclick="loadTaskLogs()" style="padding: 6px 15px;">筛选</button>
<button class="btn btn-secondary" onclick="resetLogFilters()" style="padding: 6px 15px;">重置</button>
<button class="btn btn-danger" onclick="clearOldLogs()" style="padding: 6px 15px;">清理旧日志</button>
</div>
</div>
<!-- 日志表格 -->
<div class="table-container">
<table>
<thead>
<tr>
<th style="width: 140px;">时间</th>
<th style="width: 60px;">来源</th>
<th style="width: 80px;">用户</th>
<th style="width: 100px;">账号</th>
<th style="width: 80px;">浏览类型</th>
<th style="width: 60px;">状态</th>
<th style="width: 90px;">内容/附件</th>
<th style="width: 70px;">用时</th>
<th>失败原因</th>
</tr>
</thead>
<tbody id="taskLogsList">
<tr><td colspan="9" class="empty-message">加载中...</td></tr>
</tbody>
</table>
</div>
<!-- 分页控件 -->
<div id="logsPagination" style="margin-top: 15px; display: flex; justify-content: center; align-items: center; gap: 10px; flex-wrap: wrap;">
<button class="btn btn-secondary" onclick="goToLogPage(1)" id="logFirstBtn" disabled style="padding: 6px 12px;">首页</button>
<button class="btn btn-secondary" onclick="goToLogPage(currentLogPage - 1)" id="logPrevBtn" disabled style="padding: 6px 12px;">上一页</button>
<span id="logPageInfo" style="font-size: 13px; color: #666;">第 1 页 / 共 1 页</span>
<button class="btn btn-secondary" onclick="goToLogPage(currentLogPage + 1)" id="logNextBtn" disabled style="padding: 6px 12px;">下一页</button>
<button class="btn btn-secondary" onclick="goToLogPage(totalLogPages)" id="logLastBtn" disabled style="padding: 6px 12px;">末页</button>
<span style="font-size: 12px; color: #999; margin-left: 10px;"><span id="logTotalCount">0</span> 条记录</span>
</div>
</div>
<!-- 公告管理 -->
<div id="tab-announcements" class="tab-content">
<h3 style="margin-bottom: 15px; font-size: 16px;">公告管理</h3>
<!-- 创建公告 -->
<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
<div class="form-group">
<label>公告标题</label>
<input type="text" id="announcementTitle" placeholder="请输入公告标题">
</div>
<div class="form-group">
<label>公告内容</label>
<textarea id="announcementContent" rows="5" placeholder="请输入公告内容(将以弹窗形式展示)"></textarea>
</div>
<div class="form-group">
<label>公告图片(可选)</label>
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
<button class="btn btn-secondary" onclick="triggerAnnouncementImageUpload()">+ 上传图片</button>
<button class="btn" onclick="clearAnnouncementImage()" style="background: #eee;">移除</button>
<input type="file" id="announcementImageFile" accept="image/*" style="display: none;" onchange="uploadAnnouncementImageFile()">
<input type="text" id="announcementImageUrl" placeholder="上传后自动填充" readonly style="flex: 1; min-width: 220px;">
</div>
<div id="announcementImagePreview" style="display: none; margin-top: 8px;">
<img id="announcementImagePreviewImg" src="" alt="公告图片预览" style="max-width: 260px; max-height: 160px; border-radius: 8px; border: 1px solid #e5e7eb; object-fit: contain;">
</div>
</div>
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<button class="btn btn-primary" onclick="createAnnouncement(true)">发布并启用</button>
<button class="btn btn-secondary" onclick="createAnnouncement(false)">保存但不启用</button>
<button class="btn" onclick="clearAnnouncementForm()" style="background: #eee;">清空</button>
</div>
<div style="font-size: 12px; color: #666; margin-top: 10px;">
说明:启用公告后,用户登录进入系统将弹窗提示;用户可选择“当次关闭”或“永久关闭本次公告”。
</div>
</div>
<!-- 公告列表 -->
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px; flex-wrap: wrap; gap: 10px;">
<h4 style="font-size: 14px; margin: 0;">公告列表</h4>
<button class="btn btn-primary" onclick="loadAnnouncements()" style="padding:8px 15px;">刷新</button>
</div>
<div id="announcementsList"></div>
</div>
<!-- 邮件配置 -->
<div id="tab-email" class="tab-content">
<h3 style="margin-bottom: 15px; font-size: 16px;">邮件功能设置</h3>
<!-- 全局设置 -->
<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
<div class="form-group" style="margin-bottom: 10px;">
<div style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="emailEnabled" onchange="updateEmailSettings()" style="width: auto; max-width: none; cursor: pointer;">
<span>启用邮件功能</span>
</div>
<div style="font-size: 12px; color: #666; margin-top: 5px; margin-left: 28px;">
开启后,系统将支持邮箱验证、密码重置邮件、任务完成通知等功能
</div>
</div>
<div class="form-group" style="margin-bottom: 10px;">
<div style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="failoverEnabled" onchange="updateEmailSettings()" style="width: auto; max-width: none; cursor: pointer;">
<span>启用故障转移</span>
</div>
<div style="font-size: 12px; color: #666; margin-top: 5px; margin-left: 28px;">
开启后主SMTP配置发送失败时自动切换到备用配置
</div>
</div>
<div class="form-group" style="margin-bottom: 10px;">
<div style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="registerVerifyEnabled" onchange="updateEmailSettings()" style="width: auto; max-width: none; cursor: pointer;">
<span>启用注册邮箱验证</span>
</div>
<div style="font-size: 12px; color: #666; margin-top: 5px; margin-left: 28px;">
开启后,新用户注册需通过邮箱验证才能激活账号(优先级高于自动审核)
</div>
</div>
<div class="form-group" style="margin-bottom: 10px;">
<div style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="taskNotifyEnabled" onchange="updateEmailSettings()" style="width: auto; max-width: none; cursor: pointer;">
<span>启用任务完成通知</span>
</div>
<div style="font-size: 12px; color: #666; margin-top: 5px; margin-left: 28px;">
开启后,定时任务完成时将发送邮件通知给用户(用户需已设置邮箱)
</div>
</div>
<div class="form-group" style="margin-bottom: 0;">
<label style="display: block; margin-bottom: 5px;">网站基础URL</label>
<input type="text" id="baseUrl" placeholder="例如: https://example.com" style="width: 100%;" onblur="updateEmailSettings()">
<div style="font-size: 12px; color: #666; margin-top: 5px;">
用于生成邮件中的验证链接,留空则使用默认配置
</div>
</div>
</div>
<!-- SMTP配置列表 -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<h4 style="font-size: 14px; margin: 0;">SMTP配置列表</h4>
<button class="btn btn-primary" onclick="showSmtpModal()" style="padding: 8px 15px;">+ 添加配置</button>
</div>
<div id="smtpConfigsList" style="margin-bottom: 20px;">
<div style="text-align: center; padding: 30px; color: #999;">加载中...</div>
</div>
<!-- 邮件统计 -->
<h4 style="font-size: 14px; margin: 20px 0 15px 0;">邮件发送统计</h4>
<div id="emailStats" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; margin-bottom: 20px;">
<div style="background: #f8f9fa; padding: 12px; border-radius: 8px; text-align: center;">
<div style="font-size: 24px; font-weight: bold; color: #667eea;" id="statTotalSent">0</div>
<div style="font-size: 12px; color: #666;">总发送</div>
</div>
<div style="background: #f8f9fa; padding: 12px; border-radius: 8px; text-align: center;">
<div style="font-size: 24px; font-weight: bold; color: #27ae60;" id="statTotalSuccess">0</div>
<div style="font-size: 12px; color: #666;">成功</div>
</div>
<div style="background: #f8f9fa; padding: 12px; border-radius: 8px; text-align: center;">
<div style="font-size: 24px; font-weight: bold; color: #e74c3c;" id="statTotalFailed">0</div>
<div style="font-size: 12px; color: #666;">失败</div>
</div>
<div style="background: #f8f9fa; padding: 12px; border-radius: 8px; text-align: center;">
<div style="font-size: 24px; font-weight: bold; color: #3498db;" id="statSuccessRate">0%</div>
<div style="font-size: 12px; color: #666;">成功率</div>
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 10px; margin-bottom: 20px;">
<div style="background: #fff3e0; padding: 10px; border-radius: 6px; text-align: center;">
<div style="font-size: 18px; font-weight: bold;" id="statRegister">0</div>
<div style="font-size: 11px; color: #666;">注册验证</div>
</div>
<div style="background: #e3f2fd; padding: 10px; border-radius: 6px; text-align: center;">
<div style="font-size: 18px; font-weight: bold;" id="statReset">0</div>
<div style="font-size: 11px; color: #666;">密码重置</div>
</div>
<div style="background: #f3e5f5; padding: 10px; border-radius: 6px; text-align: center;">
<div style="font-size: 18px; font-weight: bold;" id="statBind">0</div>
<div style="font-size: 11px; color: #666;">邮箱绑定</div>
</div>
<div style="background: #e8f5e9; padding: 10px; border-radius: 6px; text-align: center;">
<div style="font-size: 18px; font-weight: bold;" id="statTaskComplete">0</div>
<div style="font-size: 11px; color: #666;">任务完成</div>
</div>
</div>
<!-- 邮件日志 -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<h4 style="font-size: 14px; margin: 0;">邮件发送日志</h4>
<div style="display: flex; gap: 10px; align-items: center;">
<select id="emailLogTypeFilter" onchange="loadEmailLogs()" style="padding: 6px 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 12px;">
<option value="">全部类型</option>
<option value="register">注册验证</option>
<option value="reset">密码重置</option>
<option value="bind">邮箱绑定</option>
<option value="task_complete">任务完成</option>
<option value="security_alert">安全告警</option>
</select>
<select id="emailLogStatusFilter" onchange="loadEmailLogs()" style="padding: 6px 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 12px;">
<option value="">全部状态</option>
<option value="success">成功</option>
<option value="failed">失败</option>
</select>
<button class="btn btn-secondary" onclick="cleanupEmailLogs()" style="padding: 6px 12px; font-size: 12px;">清理日志</button>
</div>
</div>
<div id="emailLogsList" style="max-height: 400px; overflow-y: auto;">
<div style="text-align: center; padding: 30px; color: #999;">加载中...</div>
</div>
<!-- 邮件日志分页 -->
<div id="emailLogsPagination" style="margin-top: 15px; display: flex; justify-content: center; align-items: center; gap: 10px; flex-wrap: wrap;">
</div>
</div>
<!-- 系统设置 -->
<div id="tab-settings" class="tab-content">
<h3 style="margin-bottom: 15px; font-size: 16px;">管理员账号设置</h3>
<div class="form-group">
<label>修改管理员用户名</label>
<input type="text" id="newUsername" placeholder="输入新用户名">
<button class="btn btn-primary" style="margin-top: 10px;" onclick="updateUsername()">保存用户名</button>
</div>
<div class="form-group" style="margin-top: 25px;">
<label>修改管理员密码</label>
<input type="password" id="newPassword" placeholder="输入新密码">
<button class="btn btn-primary" style="margin-top: 10px;" onclick="updatePassword()">保存密码</button>
</div>
</div>
</div>
</div>
<!-- SMTP配置弹窗 -->
<div id="smtpModal" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 1000; overflow-y: auto;">
<div style="background: white; max-width: 500px; margin: 50px auto; border-radius: 10px; overflow: hidden;">
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 15px 20px; display: flex; justify-content: space-between; align-items: center;">
<h3 style="margin: 0; font-size: 16px;" id="smtpModalTitle">添加SMTP配置</h3>
<button onclick="hideSmtpModal()" style="background: none; border: none; color: white; font-size: 20px; cursor: pointer;">&times;</button>
</div>
<div style="padding: 20px;">
<input type="hidden" id="smtpConfigId">
<div class="form-group">
<label>配置名称</label>
<input type="text" id="smtpName" placeholder="如QQ邮箱、163邮箱">
</div>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="smtpEnabled" checked style="width: auto; max-width: none;">
启用此配置
</label>
</div>
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 10px;">
<div class="form-group">
<label>SMTP服务器</label>
<input type="text" id="smtpHost" placeholder="如smtp.qq.com">
</div>
<div class="form-group">
<label>端口</label>
<input type="number" id="smtpPort" value="465">
</div>
</div>
<div class="form-group">
<label>用户名</label>
<input type="text" id="smtpUsername" placeholder="SMTP账号">
</div>
<div class="form-group">
<label>密码/授权码</label>
<div style="display: flex; gap: 10px;">
<input type="password" id="smtpPassword" placeholder="SMTP密码或授权码" style="flex: 1;">
<button type="button" onclick="togglePasswordVisibility('smtpPassword')" class="btn btn-secondary" style="padding: 8px 12px;">显示</button>
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<div class="form-group">
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="smtpUseSsl" checked style="width: auto; max-width: none;">
使用SSL
</label>
</div>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="smtpUseTls" style="width: auto; max-width: none;">
使用TLS
</label>
</div>
</div>
<div class="form-group">
<label>发件人名称</label>
<input type="text" id="smtpSenderName" placeholder="如:知识管理平台" value="知识管理平台">
</div>
<div class="form-group">
<label>发件人邮箱</label>
<input type="text" id="smtpSenderEmail" placeholder="留空则使用用户名">
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<div class="form-group">
<label>每日限额</label>
<input type="number" id="smtpDailyLimit" value="0" min="0">
<div style="font-size: 11px; color: #666; margin-top: 3px;">0表示无限制</div>
</div>
<div class="form-group">
<label>优先级</label>
<input type="number" id="smtpPriority" value="0" min="0">
<div style="font-size: 11px; color: #666; margin-top: 3px;">数字越小越优先</div>
</div>
</div>
<div style="display: flex; gap: 10px; margin-top: 20px; flex-wrap: wrap;">
<button class="btn btn-secondary" onclick="testSmtpConfig()" style="flex: 1;">测试连接</button>
<button class="btn btn-primary" onclick="saveSmtpConfig()" style="flex: 1;">保存</button>
<button class="btn" onclick="hideSmtpModal()" style="flex: 1; background: #eee;">取消</button>
</div>
<div id="smtpEditActions" style="display: none; margin-top: 15px; padding-top: 15px; border-top: 1px solid #eee;">
<div style="display: flex; gap: 10px;">
<button class="btn" onclick="setPrimarySmtp()" style="flex: 1; background: #fff3e0; color: #e65100;">设为主配置</button>
<button class="btn" onclick="deleteSmtpConfig()" style="flex: 1; background: #ffebee; color: #c62828;">删除配置</button>
</div>
</div>
</div>
</div>
</div>
<!-- 通知组件 -->
<div id="notification" class="notification"></div>
<script>
let allUsers = [];
let pendingUsers = [];
let announcements = [];
function escapeHtml(text) {
return String(text ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function getCsrfToken() {
const match = document.cookie.match(/(?:^|; )csrf_token=([^;]+)/);
return match ? decodeURIComponent(match[1]) : '';
}
const originalFetch = window.fetch.bind(window);
window.fetch = (input, init = {}) => {
const method = String(init.method || 'GET').toUpperCase();
if (!['GET', 'HEAD', 'OPTIONS'].includes(method)) {
const headers = new Headers(init.headers || {});
const token = getCsrfToken();
if (token) headers.set('X-CSRF-Token', token);
init = { ...init, headers };
}
return originalFetch(input, init);
};
// 页面加载时初始化
window.addEventListener('load', () => {
loadStats();
loadPendingUsers();
loadAllUsers();
loadAnnouncements();
loadSystemConfig();
loadProxyConfig();
loadFeedbacks(); // 加载反馈统计更新徽章
// 恢复上次的标签页
const lastTab = localStorage.getItem('admin_current_tab') || 'pending';
const tabButton = document.querySelector(`.tab[onclick*="${lastTab}"]`);
if (tabButton) {
tabButton.click();
}
});
// 切换标签
function switchTab(tabName) {
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active');
});
event.target.classList.add('active');
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
document.getElementById(`tab-${tabName}`).classList.add('active');
// 保存当前标签到localStorage
localStorage.setItem('admin_current_tab', tabName);
// 切换到统计标签时加载服务器信息和任务统计
if (tabName === 'stats') {
loadServerInfo();
loadDockerStats();
loadTaskStats();
}
// 切换到日志标签时加载任务日志
if (tabName === 'logs') {
loadTaskLogs();
}
// 切换到反馈管理标签时加载反馈列表
if (tabName === 'feedbacks') {
loadFeedbacks();
}
// 切换到公告管理标签时加载公告
if (tabName === 'announcements') {
loadAnnouncements();
}
// 切换到邮件配置标签时加载邮件相关数据
if (tabName === 'email') {
loadEmailSettings();
loadSmtpConfigs();
loadEmailStats();
loadEmailLogs();
}
}
// ==================== 公告管理 ====================
async function loadAnnouncements() {
try {
const response = await fetch('/yuyx/api/announcements');
if (!response.ok) {
showNotification('加载公告失败', 'error');
return;
}
announcements = await response.json();
renderAnnouncements();
} catch (e) {
showNotification('加载公告失败', 'error');
}
}
function renderAnnouncements() {
const container = document.getElementById('announcementsList');
if (!container) return;
if (!announcements || announcements.length === 0) {
container.innerHTML = '<div class="empty-message">暂无公告</div>';
return;
}
container.innerHTML = `
<div class="table-container">
<table>
<thead>
<tr>
<th style="width: 70px;">ID</th>
<th>标题</th>
<th style="width: 90px;">状态</th>
<th style="width: 70px;">图片</th>
<th style="width: 170px;">创建时间</th>
<th style="width: 220px;">操作</th>
</tr>
</thead>
<tbody>
${announcements.map(a => `
<tr>
<td>${a.id}</td>
<td>${escapeHtml(a.title || '')}</td>
<td>
<span class="status-badge ${a.is_active ? 'status-approved' : 'status-rejected'}">
${a.is_active ? '启用' : '停用'}
</span>
</td>
<td>${a.image_url ? '有图' : '-'}</td>
<td>${a.created_at || '-'}</td>
<td>
<div class="action-buttons">
<button class="btn btn-small btn-secondary" onclick="viewAnnouncement(${a.id})">查看</button>
${a.is_active
? `<button class="btn btn-small btn-secondary" onclick="deactivateAnnouncement(${a.id})">停用</button>`
: `<button class="btn btn-small btn-success" onclick="activateAnnouncement(${a.id})">启用</button>`
}
<button class="btn btn-small btn-danger" onclick="deleteAnnouncement(${a.id})">删除</button>
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
}
function clearAnnouncementForm() {
const title = document.getElementById('announcementTitle');
const content = document.getElementById('announcementContent');
if (title) title.value = '';
if (content) content.value = '';
clearAnnouncementImage();
}
function triggerAnnouncementImageUpload() {
const input = document.getElementById('announcementImageFile');
if (input) input.click();
}
async function uploadAnnouncementImageFile() {
const input = document.getElementById('announcementImageFile');
const urlInput = document.getElementById('announcementImageUrl');
const file = input?.files?.[0];
if (!file || !urlInput) return;
if (file.type && !String(file.type).startsWith('image/')) {
showNotification('请选择图片文件', 'error');
input.value = '';
return;
}
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/yuyx/api/announcements/upload_image', {
method: 'POST',
body: formData
});
const data = await response.json();
if (!response.ok || !data?.success) {
showNotification(data?.error || '上传失败', 'error');
return;
}
urlInput.value = data.url || '';
updateAnnouncementImagePreview();
showNotification('上传成功', 'success');
} catch (e) {
showNotification('上传失败', 'error');
} finally {
input.value = '';
}
}
function clearAnnouncementImage() {
const imageUrl = document.getElementById('announcementImageUrl');
const imageFile = document.getElementById('announcementImageFile');
if (imageUrl) imageUrl.value = '';
if (imageFile) imageFile.value = '';
updateAnnouncementImagePreview();
}
function updateAnnouncementImagePreview() {
const imageUrl = document.getElementById('announcementImageUrl');
const previewWrap = document.getElementById('announcementImagePreview');
const previewImg = document.getElementById('announcementImagePreviewImg');
if (!imageUrl || !previewWrap || !previewImg) return;
const url = String(imageUrl.value || '').trim();
if (url) {
previewImg.src = url;
previewWrap.style.display = 'block';
} else {
previewImg.removeAttribute('src');
previewWrap.style.display = 'none';
}
}
function viewAnnouncement(id) {
const announcement = announcements.find(a => a.id === id);
if (!announcement) return;
const imageLine = announcement.image_url ? `\n图片:${announcement.image_url}` : '';
alert(`标题:${announcement.title || ''}${imageLine}\n\n内容:\n${announcement.content || ''}`);
}
async function createAnnouncement(isActive) {
const title = (document.getElementById('announcementTitle')?.value || '').trim();
const content = (document.getElementById('announcementContent')?.value || '').trim();
const image_url = (document.getElementById('announcementImageUrl')?.value || '').trim();
if (!title || !content) {
showNotification('标题和内容不能为空', 'error');
return;
}
try {
const response = await fetch('/yuyx/api/announcements', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content, image_url, is_active: !!isActive })
});
const data = await response.json();
if (!response.ok) {
showNotification(data.error || '发布失败', 'error');
return;
}
showNotification('保存成功', 'success');
clearAnnouncementForm();
await loadAnnouncements();
} catch (e) {
showNotification('发布失败', 'error');
}
}
async function activateAnnouncement(id) {
if (!confirm('确定启用该公告吗?启用后将自动停用其他公告。')) return;
try {
const response = await fetch(`/yuyx/api/announcements/${id}/activate`, { method: 'POST' });
const data = await response.json();
if (!response.ok || !data.success) {
showNotification(data.error || '启用失败', 'error');
return;
}
showNotification('已启用', 'success');
await loadAnnouncements();
} catch (e) {
showNotification('启用失败', 'error');
}
}
async function deactivateAnnouncement(id) {
if (!confirm('确定停用该公告吗?')) return;
try {
const response = await fetch(`/yuyx/api/announcements/${id}/deactivate`, { method: 'POST' });
const data = await response.json();
if (!response.ok || !data.success) {
showNotification(data.error || '停用失败', 'error');
return;
}
showNotification('已停用', 'success');
await loadAnnouncements();
} catch (e) {
showNotification('停用失败', 'error');
}
}
async function deleteAnnouncement(id) {
if (!confirm('确定删除该公告吗?删除后无法恢复。')) return;
try {
const response = await fetch(`/yuyx/api/announcements/${id}`, { method: 'DELETE' });
const data = await response.json();
if (!response.ok || !data.success) {
showNotification(data.error || '删除失败', 'error');
return;
}
showNotification('已删除', 'success');
await loadAnnouncements();
} catch (e) {
showNotification('删除失败', 'error');
}
}
// VIP functions
function parseBeijingDateTime(value) {
if (!value) return null;
const str = String(value).trim();
if (!str) return null;
let iso = str.includes('T') ? str : str.replace(' ', 'T');
// 统一按北京时间解析(除非字符串本身已带时区)
const hasTimezone = /([zZ]|[+-]\d{2}:\d{2})$/.test(iso);
if (!hasTimezone) iso = iso + '+08:00';
const dt = new Date(iso);
if (Number.isNaN(dt.getTime())) return null;
return dt;
}
function isVip(user) {
if (!user.vip_expire_time) return false;
const expireTime = parseBeijingDateTime(user.vip_expire_time);
return expireTime ? expireTime > new Date() : false;
}
function getVipBadge(user) {
if (isVip(user)) {
return '<span class="vip-badge-inline">VIP</span>';
}
return '<span class="normal-badge-inline">普通</span>';
}
function getVipExpire(user) {
if (!isVip(user)) return '';
const expireTime = parseBeijingDateTime(user.vip_expire_time);
const daysLeft = expireTime ? Math.ceil((expireTime - new Date()) / (1000*60*60*24)) : 0;
if (user.vip_expire_time === '2099-12-31 23:59:59') {
return '<div class="user-info" style="color:#667eea;">永久VIP</div>';
}
return '<div class="user-info">到期: ' + user.vip_expire_time + ' (剩' + daysLeft + '天)</div>';
}
async function setVip(userId, days) {
const dayText = {7:'一周',30:'一个月',365:'一年',999999:'永久'}[days];
if (!confirm('确定要为该用户开通 ' + dayText + ' VIP吗?')) return;
try {
const response = await fetch('/yuyx/api/users/' + userId + '/vip', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({days: days})
});
const data = await response.json();
if (response.ok) {
showNotification(data.message, 'success');
loadAllUsers();
loadStats();
} else {
showNotification('设置失败: ' + data.error, 'error');
}
} catch (error) {
showNotification('设置失败: ' + error.message, 'error');
}
}
async function removeVip(userId) {
if (!confirm('确定要移除该用户的VIP吗?')) return;
try {
const response = await fetch('/yuyx/api/users/' + userId + '/vip', {
method: 'DELETE'
});
const data = await response.json();
if (response.ok) {
showNotification(data.message, 'success');
loadAllUsers();
loadStats();
} else {
showNotification('移除失败: ' + data.error, 'error');
}
} catch (error) {
showNotification('移除失败: ' + error.message, 'error');
}
}
async function loadStats() {
const response = await fetch('/yuyx/api/stats');
const stats = await response.json();
document.getElementById('totalUsers').textContent = stats.total_users;
document.getElementById('approvedUsers').textContent = stats.approved_users;
document.getElementById('pendingUsers').textContent = stats.pending_users;
document.getElementById('totalAccounts').textContent = stats.total_accounts;
document.getElementById('vipUsers').textContent = stats.vip_users || 0;
// 显示管理员用户名
if (stats.admin_username) {
document.getElementById('admin-username').textContent = stats.admin_username;
}
}
async function loadPendingUsers() {
const response = await fetch('/yuyx/api/users/pending');
pendingUsers = await response.json();
renderPendingUsers();
}
function renderPendingUsers() {
const container = document.getElementById('pendingUsersList');
if (pendingUsers.length === 0) {
container.innerHTML = '<div class="empty-message">暂无待审核用户</div>';
return;
}
container.innerHTML = `
<div class="table-container">
<table>
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>邮箱</th>
<th>注册时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${pendingUsers.map(user => `
<tr>
<td>${user.id}</td>
<td>
<div><strong>${escapeHtml(user.username)}</strong> ${getVipBadge(user)}</div>
${getVipExpire(user)}
</td>
<td>${escapeHtml(user.email || '-')}</td>
<td>${escapeHtml(user.created_at)}</td>
<td>
<div class="action-buttons">
<button class="btn btn-small btn-success" onclick="approveUser(${user.id})">通过</button>
<button class="btn btn-small btn-danger" onclick="rejectUser(${user.id})">拒绝</button>
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
}
async function loadAllUsers() {
const response = await fetch('/yuyx/api/users');
allUsers = await response.json();
renderAllUsers();
}
function renderAllUsers() {
const container = document.getElementById('allUsersList');
if (allUsers.length === 0) {
container.innerHTML = '<div class="empty-message">暂无用户</div>';
return;
}
container.innerHTML = `
<div class="table-container">
<table>
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>状态</th>
<th>注册时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${allUsers.map(user => `
<tr>
<td>${user.id}</td>
<td>
<div><strong>${escapeHtml(user.username)}</strong> ${getVipBadge(user)}</div>
${getVipExpire(user)}
${user.email ? '<div class="user-info">'+escapeHtml(user.email)+'</div>' : ''}
</td>
<td>
<span class="status-badge status-${user.status}">
${user.status === 'pending' ? '待审核' : user.status === 'approved' ? '已通过' : '已拒绝'}
</span>
</td>
<td>
${escapeHtml(user.created_at)}
${user.approved_at ? '<div class="user-info">审核:'+escapeHtml(user.approved_at)+'</div>' : ''}
</td>
<td>
<div class="action-buttons">
${user.status === 'pending' ? `
<button class="btn btn-small btn-success" onclick="approveUser(${user.id})">通过</button>
<button class="btn btn-small btn-danger" onclick="rejectUser(${user.id})">拒绝</button>
` : ''}
<button class="btn btn-small btn-secondary" onclick="deleteUser(${user.id})">删除</button>
${isVip(user) ?
`<button class="btn btn-small btn-danger" onclick="removeVip(${user.id})">移除VIP</button>` :
`<button class="btn btn-small btn-primary" onclick="setVip(${user.id}, 7)">一周</button>
<button class="btn btn-small btn-primary" onclick="setVip(${user.id}, 30)">一月</button>
<button class="btn btn-small btn-primary" onclick="setVip(${user.id}, 365)">一年</button>
<button class="btn btn-small btn-primary" onclick="setVip(${user.id}, 999999)">永久</button>`
}
<button class="btn btn-small btn-secondary" onclick="resetUserPassword(${user.id})">重置密码</button>
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
}
async function approveUser(userId) {
if (!confirm('确定通过该用户的注册申请吗?')) return;
const response = await fetch(`/yuyx/api/users/${userId}/approve`, {
method: 'POST'
});
if (response.ok) {
showNotification('用户审核通过', 'success');
loadStats();
loadPendingUsers();
loadAllUsers();
} else {
showNotification('审核失败', 'error');
}
}
async function rejectUser(userId) {
if (!confirm('确定拒绝该用户的注册申请吗?')) return;
const response = await fetch(`/yuyx/api/users/${userId}/reject`, {
method: 'POST'
});
if (response.ok) {
showNotification('已拒绝用户', 'success');
loadStats();
loadPendingUsers();
loadAllUsers();
} else {
showNotification('操作失败', 'error');
}
}
async function deleteUser(userId) {
if (!confirm('确定删除该用户吗?此操作将删除该用户的所有数据,不可恢复!')) return;
const response = await fetch(`/yuyx/api/users/${userId}`, {
method: 'DELETE'
});
if (response.ok) {
showNotification('用户已删除', 'success');
loadStats();
loadPendingUsers();
loadAllUsers();
} else {
showNotification('删除失败', 'error');
}
}
async function updateUsername() {
const newUsername = document.getElementById('newUsername').value.trim();
if (!newUsername) {
showNotification('请输入新用户名', 'error');
return;
}
if (!confirm(`确定将管理员用户名修改为 "${newUsername}" 吗?`)) return;
const response = await fetch('/yuyx/api/admin/username', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ new_username: newUsername })
});
if (response.ok) {
showNotification('用户名修改成功,请重新登录', 'success');
document.getElementById('newUsername').value = '';
setTimeout(() => {
logout();
}, 2000);
} else {
const data = await response.json();
showNotification(data.error || '修改失败', 'error');
}
}
async function updatePassword() {
const newPassword = document.getElementById('newPassword').value.trim();
if (!newPassword) {
showNotification('请输入新密码', 'error');
return;
}
if (newPassword.length < 8) {
showNotification('密码长度至少8位', 'error');
return;
}
if (!/[a-zA-Z]/.test(newPassword) || !/\d/.test(newPassword)) {
showNotification('密码必须包含字母和数字', 'error');
return;
}
if (!confirm('确定修改管理员密码吗?')) return;
const response = await fetch('/yuyx/api/admin/password', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ new_password: newPassword })
});
if (response.ok) {
showNotification('密码修改成功,请重新登录', 'success');
document.getElementById('newPassword').value = '';
setTimeout(() => {
logout();
}, 2000);
} else {
showNotification('修改失败', 'error');
}
}
async function logout() {
const response = await fetch('/yuyx/api/logout', {
method: 'POST'
});
if (response.ok) {
window.location.href = '/yuyx';
}
}
function showNotification(message, type) {
const notification = document.getElementById('notification');
notification.textContent = message;
notification.className = `notification ${type}`;
notification.style.display = 'block';
setTimeout(() => {
notification.style.display = 'none';
}, 1000);
}
// 系统配置功能
async function loadSystemConfig() {
try {
const response = await fetch('/yuyx/api/system/config');
if (response.ok) {
const config = await response.json();
document.getElementById('maxConcurrent').value = config.max_concurrent_global || 2;
document.getElementById('maxConcurrentPerAccount').value = config.max_concurrent_per_account || 1;
document.getElementById('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 || '应读';
var enableScreenshot = config.enable_screenshot;
document.getElementById('enableScreenshot').checked = enableScreenshot === 1 || enableScreenshot === true || enableScreenshot === undefined;
// 加载星期选择
const weekdays = config.schedule_weekdays || '1,2,3,4,5,6,7';
const weekdayArray = weekdays.split(',').map(d => d.trim());
document.querySelectorAll('.weekday-checkbox').forEach(checkbox => {
checkbox.checked = weekdayArray.includes(checkbox.value);
});
// 显示/隐藏定时任务选项
toggleSchedule(config.schedule_enabled === 1);
// 加载自动审核配置
document.getElementById('autoApproveEnabled').checked = config.auto_approve_enabled === 1;
document.getElementById('autoApproveHourlyLimit').value = config.auto_approve_hourly_limit || 10;
document.getElementById('autoApproveVipDays').value = config.auto_approve_vip_days || 7;
}
} catch (error) {
console.error('加载系统配置失败:', error);
}
}
function toggleSchedule(enabled) {
const timeGroup = document.getElementById('scheduleTimeGroup');
const browseTypeGroup = document.getElementById('scheduleBrowseTypeGroup');
const weekdaysGroup = document.getElementById('scheduleWeekdaysGroup');
const screenshotGroup = document.getElementById('scheduleScreenshotGroup');
if (enabled) {
timeGroup.style.display = 'block';
browseTypeGroup.style.display = 'block';
weekdaysGroup.style.display = 'block';
screenshotGroup.style.display = 'block';
} else {
timeGroup.style.display = 'none';
browseTypeGroup.style.display = 'none';
weekdaysGroup.style.display = 'none';
screenshotGroup.style.display = 'none';
}
// 保存按钮始终显示,无论是开启还是关闭定时任务
}
// ==================== 代理配置功能 ====================
async function loadProxyConfig() {
try {
const response = await fetch('/yuyx/api/proxy/config');
if (response.ok) {
const data = await response.json();
document.getElementById('proxyEnabled').checked = data.proxy_enabled === 1;
document.getElementById('proxyApiUrl').value = data.proxy_api_url || '';
document.getElementById('proxyExpireMinutes').value = data.proxy_expire_minutes || 3;
}
} catch (error) {
console.error('加载代理配置失败:', error);
}
}
async function saveProxyConfig() {
const proxyEnabled = document.getElementById('proxyEnabled').checked ? 1 : 0;
const proxyApiUrl = document.getElementById('proxyApiUrl').value.trim();
const proxyExpireMinutes = parseInt(document.getElementById('proxyExpireMinutes').value) || 3;
if (proxyEnabled && !proxyApiUrl) {
alert('❌ 启用代理时API地址不能为空');
return;
}
try {
const response = await fetch('/yuyx/api/proxy/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
proxy_enabled: proxyEnabled,
proxy_api_url: proxyApiUrl,
proxy_expire_minutes: proxyExpireMinutes
})
});
const data = await response.json();
if (data.message) {
showNotification('✓ ' + data.message, 'success');
loadProxyConfig();
} else if (data.error) {
showNotification('✗ ' + data.error, 'error');
}
} catch (error) {
showNotification('保存失败: ' + error.message, 'error');
}
}
async function saveAutoApproveConfig() {
const autoApproveEnabled = document.getElementById('autoApproveEnabled').checked ? 1 : 0;
const autoApproveHourlyLimit = parseInt(document.getElementById('autoApproveHourlyLimit').value) || 10;
const autoApproveVipDays = parseInt(document.getElementById('autoApproveVipDays').value) || 0;
if (autoApproveHourlyLimit < 1) {
alert('❌ 每小时注册限制必须大于0');
return;
}
if (autoApproveVipDays < 0) {
alert('❌ VIP天数不能为负数');
return;
}
try {
const response = await fetch('/yuyx/api/system/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
auto_approve_enabled: autoApproveEnabled,
auto_approve_hourly_limit: autoApproveHourlyLimit,
auto_approve_vip_days: autoApproveVipDays
})
});
const data = await response.json();
if (data.message) {
showNotification('✓ 自动审核配置已保存', 'success');
} else if (data.error) {
showNotification('✗ ' + data.error, 'error');
}
} catch (error) {
showNotification('保存失败: ' + error.message, 'error');
}
}
async function testProxy() {
const apiUrl = document.getElementById('proxyApiUrl').value.trim();
if (!apiUrl) {
alert('❌ 请先输入代理API地址');
return;
}
const button = event.target;
const originalText = button.textContent;
button.textContent = '⏳ 测试中...';
button.disabled = true;
try {
const response = await fetch('/yuyx/api/proxy/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ api_url: apiUrl })
});
const data = await response.json();
if (data.success) {
alert(`${data.message}\n\n获取到的代理: ${data.proxy}`);
} else {
alert(`${data.message}`);
}
} catch (error) {
alert('❌ 测试失败: ' + error.message);
} finally {
button.textContent = originalText;
button.disabled = false;
}
}
async function updateConcurrency() {
const maxConcurrent = parseInt(document.getElementById('maxConcurrent').value);
const maxConcurrentPerAccount = parseInt(document.getElementById('maxConcurrentPerAccount').value);
const maxScreenshotConcurrent = parseInt(document.getElementById('maxScreenshotConcurrent').value);
if (maxConcurrent < 1) {
showNotification('全局并发数必须大于0', 'error');
return;
}
if (maxConcurrentPerAccount < 1) {
showNotification('单账号并发数必须大于0', 'error');
return;
}
if (maxScreenshotConcurrent < 1) {
showNotification('截图并发数必须大于0', 'error');
return;
}
if (!confirm(`确定更新并发配置吗?\n\n全局并发数: ${maxConcurrent}\n单账号并发数: ${maxConcurrentPerAccount}\n截图并发数: ${maxScreenshotConcurrent}`)) return;
try {
const response = await fetch('/yuyx/api/system/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
max_concurrent_global: maxConcurrent,
max_concurrent_per_account: maxConcurrentPerAccount,
max_screenshot_concurrent: maxScreenshotConcurrent
})
});
const data = await response.json();
if (response.ok) {
showNotification('并发配置已更新,将在下次任务启动时生效', 'success');
} else {
showNotification(data.error || '更新失败', 'error');
}
} catch (error) {
showNotification('更新失败: ' + error.message, 'error');
}
}
async function updateSchedule() {
const enabled = document.getElementById('scheduleEnabled').checked;
const time = document.getElementById('scheduleTime').value;
const browseType = document.getElementById('scheduleBrowseType').value;
const enableScreenshot = document.getElementById('enableScreenshot').checked;
// 获取选中的星期
const selectedWeekdays = [];
document.querySelectorAll('.weekday-checkbox:checked').forEach(checkbox => {
selectedWeekdays.push(checkbox.value);
});
if (enabled && selectedWeekdays.length === 0) {
showNotification('请至少选择一个执行日期', 'error');
return;
}
const weekdaysStr = selectedWeekdays.join(',');
const weekdayNames = ['', '周一', '周二', '周三', '周四', '周五', '周六', '周日'];
const weekdayDisplay = selectedWeekdays.map(d => weekdayNames[parseInt(d)]).join('、');
const message = enabled
? `确定启用定时任务吗?\n\n执行时间: 每天 ${time}\n执行日期: ${weekdayDisplay}\n浏览类型: ${browseType}\n截图: ${enableScreenshot ? '截图' : '不截图'}\n\n系统将自动执行所有账号的浏览任务`
: `确定关闭定时任务吗?`;
if (!confirm(message)) return;
try {
const response = await fetch('/yuyx/api/system/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
schedule_enabled: enabled ? 1 : 0,
schedule_time: time,
schedule_browse_type: browseType,
schedule_weekdays: weekdaysStr,
enable_screenshot: enableScreenshot ? 1 : 0
})
});
const data = await response.json();
if (response.ok) {
showNotification(enabled ? '定时任务已启用' : '定时任务已关闭', 'success');
} else {
showNotification(data.error || '更新失败', 'error');
}
} catch (error) {
showNotification('更新失败: ' + error.message, 'error');
}
}
// 立即执行定时任务
async function executeScheduleNow() {
const browseType = document.getElementById('scheduleBrowseType').value;
const message = `确定要立即执行定时任务吗?\n\n这将执行所有账号的浏览任务\n浏览类型: ${browseType}\n\n注意:无视定时时间和执行日期配置,立即开始执行!`;
if (!confirm(message)) return;
try {
// 禁用按钮,防止重复点击
const button = event.target;
button.disabled = true;
button.textContent = '⏳ 执行中...';
const response = await fetch('/yuyx/api/schedule/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
const data = await response.json();
if (response.ok) {
showNotification(data.message || '定时任务已开始执行', 'success');
} else {
showNotification(data.error || '执行失败', 'error');
}
// 恢复按钮状态
setTimeout(() => {
button.disabled = false;
button.textContent = '⚡ 立即执行';
}, 1000);
} catch (error) {
showNotification('执行失败: ' + error.message, 'error');
// 恢复按钮状态
setTimeout(() => {
button.disabled = false;
button.textContent = '⚡ 立即执行';
}, 1000);
}
}
async function loadServerInfo() {
try {
const response = await fetch('/yuyx/api/server/info');
if (response.ok) {
const info = await response.json();
// 更新服务器信息
document.getElementById('serverCpu').textContent = info.cpu_percent + '%';
document.getElementById('serverMemory').textContent = info.memory_used + ' / ' + info.memory_total;
document.getElementById('serverDisk').textContent = info.disk_used + ' / ' + info.disk_total;
document.getElementById('serverUptime').textContent = info.uptime;
}
} catch (error) {
console.error('加载服务器信息失败:', error);
}
}
async function loadDockerStats() {
try {
const response = await fetch('/yuyx/api/docker_stats');
if (response.ok) {
const docker = await response.json();
// 更新Docker信息
document.getElementById('dockerContainerName').textContent = docker.container_name || 'N/A';
document.getElementById('dockerUptime').textContent = docker.uptime || 'N/A';
document.getElementById('dockerMemory').textContent = docker.memory_usage || 'N/A';
// 更新状态(带颜色)
const statusElement = document.getElementById('dockerStatus');
statusElement.textContent = docker.status || 'Unknown';
if (docker.status === 'Running') {
statusElement.style.color = '#28a745'; // 绿色
} else {
statusElement.style.color = '#dc3545'; // 红色
}
// 如果有内存限制和百分比,显示更详细信息
if (docker.memory_limit && docker.memory_limit !== 'N/A') {
const memoryText = docker.memory_usage + ' / ' + docker.memory_limit;
if (docker.memory_percent && docker.memory_percent !== 'N/A') {
document.getElementById('dockerMemory').textContent = memoryText + ' (' + docker.memory_percent + ')';
} else {
document.getElementById('dockerMemory').textContent = memoryText;
}
}
}
} catch (error) {
console.error('加载Docker状态失败:', error);
}
}
async function loadTaskStats() {
try {
const response = await fetch('/yuyx/api/task/stats');
if (response.ok) {
const stats = await response.json();
// 更新当日统计
document.getElementById('todaySuccessTasks').textContent = stats.today.success_tasks;
document.getElementById('todayFailedTasks').textContent = stats.today.failed_tasks;
document.getElementById('todayTotalItems').textContent = stats.today.total_items;
document.getElementById('todayTotalAttachments').textContent = stats.today.total_attachments;
// 更新累计统计
document.getElementById('totalSuccessTasks').textContent = stats.total.success_tasks;
document.getElementById('totalFailedTasks').textContent = stats.total.failed_tasks;
document.getElementById('totalTotalItems').textContent = stats.total.total_items;
document.getElementById('totalTotalAttachments').textContent = stats.total.total_attachments;
}
} catch (error) {
console.error('加载任务统计失败:', error);
}
}
async function loadRunningTasks() {
try {
const response = await fetch('/yuyx/api/task/running');
if (response.ok) {
const data = await response.json();
// 更新计数
document.getElementById('runningTaskCount').textContent = data.running_count;
document.getElementById('queuingTaskCount').textContent = data.queuing_count;
document.getElementById('maxConcurrentDisplay').textContent = data.max_concurrent;
// 来源显示映射
const sourceMap = {
'manual': {text: '手动', color: '#28a745'},
'scheduled': {text: '定时', color: '#007bff'},
'immediate': {text: '即时', color: '#fd7e14'},
'resumed': {text: '恢复', color: '#6c757d'}
};
// 渲染运行中的任务
const runningList = document.getElementById('runningTasksList');
if (data.running.length === 0) {
runningList.innerHTML = '<div style="color: #999; text-align: center; padding: 10px;">暂无运行中的任务</div>';
} else {
runningList.innerHTML = data.running.map(task => {
const sourceKey = String(task.source || '');
const source = sourceMap[sourceKey] || {text: escapeHtml(sourceKey), color: '#666'};
// 状态颜色映射
const statusColorMap = {
'初始化': '#6c757d',
'正在登录': '#fd7e14',
'正在浏览': '#28a745',
'浏览完成': '#007bff',
'正在截图': '#17a2b8'
};
const statusColor = statusColorMap[task.detail_status] || '#666';
const safeUser = escapeHtml(task.user_username || '');
const safeAccount = escapeHtml(task.username || '');
const safeBrowse = escapeHtml(task.browse_type || '');
const safeDetail = escapeHtml(task.detail_status || '');
const safeElapsed = escapeHtml(task.elapsed_display || '');
// 进度显示
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;">${safeUser}</span>
<span style="color: #666;">→</span>
<span style="color: #007bff; font-weight: 500;">${safeAccount}</span>
<span style="background: #e9ecef; padding: 2px 6px; border-radius: 3px; font-size: 11px; color: #666;">${safeBrowse}</span>
</div>
<div style="margin-top: 4px; display: flex; align-items: center; gap: 8px;">
<span style="color: ${statusColor}; font-weight: 500; font-size: 12px;">● ${safeDetail}</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;">${safeElapsed}</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 sourceKey = String(task.source || '');
const source = sourceMap[sourceKey] || {text: escapeHtml(sourceKey), color: '#666'};
const safeUser = escapeHtml(task.user_username || '');
const safeAccount = escapeHtml(task.username || '');
const safeBrowse = escapeHtml(task.browse_type || '');
const safeDetail = escapeHtml(task.detail_status || '等待资源');
const safeElapsed = escapeHtml(task.elapsed_display || '');
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;">${safeUser}</span>
<span style="color: #666;">→</span>
<span style="color: #007bff; font-weight: 500;">${safeAccount}</span>
<span style="background: #ffeeba; padding: 2px 6px; border-radius: 3px; font-size: 11px; color: #856404;">${safeBrowse}</span>
</div>
<div style="margin-top: 4px;">
<span style="color: #fd7e14; font-size: 12px;">● ${safeDetail}</span>
</div>
</div>
<div style="text-align: right; min-width: 80px;">
<div style="color: #fd7e14; font-weight: 500;">等待 ${safeElapsed}</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}">${escapeHtml(user.username)}</option>`;
});
}
} catch (error) {
console.error('加载用户列表失败:', error);
}
}
// 重置筛选条件
function resetLogFilters() {
document.getElementById('logDateFilter').value = '';
document.getElementById('logStatusFilter').value = '';
document.getElementById('logSourceFilter').value = '';
document.getElementById('logUserFilter').value = '';
document.getElementById('logAccountFilter').value = '';
currentLogPage = 1;
loadTaskLogs();
}
// 跳转到指定页
function goToLogPage(page) {
if (page < 1 || page > totalLogPages) return;
currentLogPage = page;
loadTaskLogs();
}
// 更新分页控件状态
function updatePaginationUI() {
document.getElementById('logFirstBtn').disabled = currentLogPage <= 1;
document.getElementById('logPrevBtn').disabled = currentLogPage <= 1;
document.getElementById('logNextBtn').disabled = currentLogPage >= totalLogPages;
document.getElementById('logLastBtn').disabled = currentLogPage >= totalLogPages;
document.getElementById('logPageInfo').textContent = `${currentLogPage} 页 / 共 ${totalLogPages}`;
document.getElementById('logTotalCount').textContent = totalLogCount;
}
async function loadTaskLogs() {
try {
const dateFilter = document.getElementById('logDateFilter').value;
const statusFilter = document.getElementById('logStatusFilter').value;
const sourceFilter = document.getElementById('logSourceFilter').value;
const userFilter = document.getElementById('logUserFilter').value;
const accountFilter = document.getElementById('logAccountFilter').value;
const offset = (currentLogPage - 1) * logsPerPage;
let url = `/yuyx/api/task/logs?limit=${logsPerPage}&offset=${offset}`;
if (dateFilter) url += `&date=${dateFilter}`;
if (statusFilter) url += `&status=${statusFilter}`;
if (sourceFilter) url += `&source=${sourceFilter}`;
if (userFilter) url += `&user_id=${userFilter}`;
if (accountFilter) url += `&account=${encodeURIComponent(accountFilter)}`;
const response = await fetch(url);
if (response.ok) {
const data = await response.json();
const logs = data.logs || data;
totalLogCount = data.total || logs.length;
totalLogPages = Math.max(1, Math.ceil(totalLogCount / logsPerPage));
const tbody = document.getElementById('taskLogsList');
if (logs.length === 0) {
tbody.innerHTML = '<tr><td colspan="9" class="empty-message">暂无日志记录</td></tr>';
updatePaginationUI();
return;
}
tbody.innerHTML = '';
logs.forEach(log => {
const row = document.createElement('tr');
const statusClass = log.status === 'success' ? 'status-approved' : 'status-rejected';
const statusText = log.status === 'success' ? '成功' : '失败';
// 格式化执行时间
const formatDuration = (seconds) => {
if (!seconds && seconds !== 0) return '-';
if (seconds < 60) return `${seconds}`;
const minutes = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${minutes}${secs}`;
};
// 来源显示
const sourceMap = {
'manual': {text: '手动', color: '#28a745'},
'scheduled': {text: '定时', color: '#007bff'},
'immediate': {text: '即时', color: '#fd7e14'},
'resumed': {text: '恢复', color: '#6c757d'}
};
const sourceKey = log.source || 'manual';
const sourceInfo = sourceMap[sourceKey] || {text: escapeHtml(sourceKey), color: '#28a745'};
const safeCreatedAt = escapeHtml(log.created_at || '');
const safeUser = escapeHtml(log.user_username || 'N/A');
const safeAccount = escapeHtml(log.username || '');
const safeBrowse = escapeHtml(log.browse_type || '');
const safeError = escapeHtml(log.error_message || '-');
row.innerHTML = `
<td>${safeCreatedAt}</td>
<td><span style="color: ${sourceInfo.color}; font-weight: 500;">${sourceInfo.text}</span></td>
<td>${safeUser}</td>
<td>${safeAccount}</td>
<td>${safeBrowse}</td>
<td><span class="status-badge ${statusClass}">${statusText}</span></td>
<td>${log.total_items} / ${log.total_attachments}</td>
<td style="color: #2F80ED; font-weight: 500;">${formatDuration(log.duration)}</td>
<td style="color: #dc3545; font-size: 11px;">${safeError}</td>
`;
tbody.appendChild(row);
});
updatePaginationUI();
}
} catch (error) {
console.error('加载任务日志失败:', error);
}
}
async function clearOldLogs() {
const days = prompt('请输入要清理多少天前的日志默认30天:', '30');
if (days === null) return;
const daysNum = parseInt(days);
if (isNaN(daysNum) || daysNum < 1) {
alert('请输入有效的天数大于0的整数');
return;
}
if (!confirm(`确定要删除${daysNum}天前的所有日志吗?此操作不可恢复!`)) return;
try {
const response = await fetch('/yuyx/api/task/logs/clear', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ days: daysNum })
});
const data = await response.json();
if (response.ok) {
showNotification(data.message, 'success');
loadTaskLogs();
} else {
showNotification(data.error || '清理失败', 'error');
}
} catch (error) {
showNotification('清理失败: ' + error.message, 'error');
}
}
// 修改switchTab函数在切换到统计和日志标签时加载数据
let statsRefreshInterval = null;
const originalSwitchTab = switchTab;
switchTab = function(tabName) {
originalSwitchTab(tabName);
// 清除之前的定时刷新
if (statsRefreshInterval) {
clearInterval(statsRefreshInterval);
statsRefreshInterval = null;
}
if (tabName === 'stats') {
loadServerInfo();
loadDockerStats();
loadTaskStats();
// 每1秒自动刷新统计信息
statsRefreshInterval = setInterval(() => {
loadServerInfo();
loadDockerStats();
loadTaskStats();
loadRunningTasks();
}, 1000);
// 首次立即加载运行任务
loadRunningTasks();
} else if (tabName === 'logs') {
loadLogUserOptions();
loadTaskLogs();
}
};
// 管理员直接重置用户密码
async function resetUserPassword(userId) {
const newPassword = prompt('请输入新密码至少8位且包含字母和数字:');
if (!newPassword) return;
if (newPassword.length < 8) {
showNotification('密码长度至少8位', 'error');
return;
}
if (!/[a-zA-Z]/.test(newPassword) || !/\d/.test(newPassword)) {
showNotification('密码必须包含字母和数字', 'error');
return;
}
if (!confirm(`确定要将该用户密码重置为: ${newPassword}?`)) return;
try {
const response = await fetch(`/yuyx/api/users/${userId}/reset_password`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({new_password: newPassword})
});
const data = await response.json();
if (response.ok) {
showNotification('密码重置成功', 'success');
} else {
showNotification('重置失败: ' + data.error, 'error');
}
} catch (error) {
showNotification('重置失败: ' + error.message, 'error');
}
}
// ==================== 反馈管理功能 ====================
let feedbacksList = [];
// 加载反馈列表
async function loadFeedbacks() {
try {
const statusFilter = document.getElementById('feedbackStatusFilter').value;
let url = '/yuyx/api/feedbacks';
if (statusFilter) {
url += '?status=' + statusFilter;
}
const response = await fetch(url);
if (response.ok) {
const data = await response.json();
feedbacksList = data.feedbacks || [];
const stats = data.stats || {};
document.getElementById('statTotal').textContent = stats.total || 0;
document.getElementById('statPending').textContent = stats.pending || 0;
document.getElementById('statReplied').textContent = stats.replied || 0;
document.getElementById('statClosed').textContent = stats.closed || 0;
const badge = document.getElementById('feedbackBadge');
if (stats.pending > 0) {
badge.textContent = stats.pending;
badge.style.display = 'inline';
} else {
badge.style.display = 'none';
}
renderFeedbacks();
}
} catch (error) {
console.error('加载反馈列表失败:', error);
showNotification('加载反馈列表失败: ' + error.message, 'error');
}
}
function renderFeedbacks() {
const container = document.getElementById('feedbacksList');
if (feedbacksList.length === 0) {
container.innerHTML = '<div class="empty-message">暂无反馈记录</div>';
return;
}
const getStatusBadge = (status) => {
const statusMap = {
'pending': { text: '待处理', cls: 'status-pending' },
'replied': { text: '已回复', cls: 'status-approved' },
'closed': { text: '已关闭', cls: 'status-rejected' }
};
const s = statusMap[status] || { text: status, cls: '' };
return '<span class="status-badge ' + s.cls + '">' + s.text + '</span>';
};
let html = '<div class="table-container"><table><thead><tr>';
html += '<th>ID</th><th>用户</th><th>标题</th><th>描述</th><th>联系方式</th><th>状态</th><th>提交时间</th><th>回复</th><th>操作</th>';
html += '</tr></thead><tbody>';
feedbacksList.forEach(fb => {
html += '<tr>';
html += '<td>' + fb.id + '</td>';
html += '<td><strong>' + escapeHtml(fb.username || 'N/A') + '</strong></td>';
html += '<td style="max-width:150px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="' + escapeHtml(fb.title || '') + '">' + escapeHtml(fb.title || '') + '</td>';
html += '<td style="max-width:200px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="' + escapeHtml(fb.description || '') + '">' + escapeHtml(fb.description || '') + '</td>';
html += '<td>' + escapeHtml(fb.contact || '-') + '</td>';
html += '<td>' + getStatusBadge(fb.status) + '</td>';
html += '<td>' + escapeHtml(fb.created_at) + '</td>';
html += '<td style="max-width:150px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="' + escapeHtml(fb.admin_reply || '') + '">' + escapeHtml(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');
}
}
// ==================== 邮件配置功能 ====================
let smtpConfigs = [];
let currentEmailLogPage = 1;
let totalEmailLogPages = 1;
// 加载邮件设置
async function loadEmailSettings() {
try {
const response = await fetch('/yuyx/api/email/settings');
if (response.ok) {
const data = await response.json();
document.getElementById('emailEnabled').checked = data.enabled;
document.getElementById('failoverEnabled').checked = data.failover_enabled;
document.getElementById('registerVerifyEnabled').checked = data.register_verify_enabled || false;
document.getElementById('taskNotifyEnabled').checked = data.task_notify_enabled || false;
document.getElementById('baseUrl').value = data.base_url || '';
}
} catch (error) {
console.error('加载邮件设置失败:', error);
}
}
// 更新邮件设置
async function updateEmailSettings() {
try {
const response = await fetch('/yuyx/api/email/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
enabled: document.getElementById('emailEnabled').checked,
failover_enabled: document.getElementById('failoverEnabled').checked,
register_verify_enabled: document.getElementById('registerVerifyEnabled').checked,
task_notify_enabled: document.getElementById('taskNotifyEnabled').checked,
base_url: document.getElementById('baseUrl').value.trim()
})
});
if (response.ok) {
showNotification('邮件设置已更新', 'success');
} else {
const data = await response.json();
showNotification('更新失败: ' + data.error, 'error');
}
} catch (error) {
showNotification('更新失败: ' + error.message, 'error');
}
}
// 加载SMTP配置列表
async function loadSmtpConfigs() {
try {
const response = await fetch('/yuyx/api/smtp/configs');
if (response.ok) {
smtpConfigs = await response.json();
renderSmtpConfigs();
}
} catch (error) {
console.error('加载SMTP配置失败:', error);
document.getElementById('smtpConfigsList').innerHTML =
'<div style="text-align: center; padding: 30px; color: #e74c3c;">加载失败</div>';
}
}
// 渲染SMTP配置列表
function renderSmtpConfigs() {
const container = document.getElementById('smtpConfigsList');
if (smtpConfigs.length === 0) {
container.innerHTML = '<div style="text-align: center; padding: 30px; color: #999; background: #f8f9fa; border-radius: 8px;">暂无SMTP配置请点击"添加配置"创建</div>';
return;
}
let html = '<div class="table-container"><table style="width: 100%;"><thead><tr>';
html += '<th style="width: 60px;">状态</th>';
html += '<th>名称</th>';
html += '<th>服务器</th>';
html += '<th>今日/限额</th>';
html += '<th>成功率</th>';
html += '<th style="width: 80px;">操作</th>';
html += '</tr></thead><tbody>';
smtpConfigs.forEach(config => {
const statusIcon = config.is_primary ? '⭐主' :
(config.enabled ? '✓备用' : '✗禁用');
const statusClass = config.is_primary ? 'color: #f39c12;' :
(config.enabled ? 'color: #27ae60;' : 'color: #95a5a6;');
const dailyText = config.daily_limit > 0 ?
`${config.daily_sent}/${config.daily_limit}` : `${config.daily_sent}/∞`;
html += '<tr>';
html += `<td style="${statusClass} font-weight: bold;">${statusIcon}</td>`;
html += `<td><strong>${config.name}</strong></td>`;
html += `<td>${config.host}:${config.port}</td>`;
html += `<td>${dailyText}</td>`;
html += `<td>${config.success_rate}%</td>`;
html += `<td><button class="btn btn-small btn-secondary" onclick="editSmtpConfig(${config.id})">编辑</button></td>`;
html += '</tr>';
});
html += '</tbody></table></div>';
container.innerHTML = html;
}
// 显示SMTP配置弹窗
function showSmtpModal(configId = null) {
document.getElementById('smtpModal').style.display = 'block';
if (configId) {
// 编辑模式
document.getElementById('smtpModalTitle').textContent = '编辑SMTP配置';
document.getElementById('smtpEditActions').style.display = 'block';
const config = smtpConfigs.find(c => c.id === configId);
if (config) {
document.getElementById('smtpConfigId').value = config.id;
document.getElementById('smtpName').value = config.name;
document.getElementById('smtpEnabled').checked = config.enabled;
document.getElementById('smtpHost').value = config.host;
document.getElementById('smtpPort').value = config.port;
document.getElementById('smtpUsername').value = config.username;
document.getElementById('smtpPassword').value = '';
document.getElementById('smtpPassword').placeholder = config.has_password ? '留空保持不变' : 'SMTP密码或授权码';
document.getElementById('smtpUseSsl').checked = config.use_ssl;
document.getElementById('smtpUseTls').checked = config.use_tls;
document.getElementById('smtpSenderName').value = config.sender_name;
document.getElementById('smtpSenderEmail').value = config.sender_email;
document.getElementById('smtpDailyLimit').value = config.daily_limit;
document.getElementById('smtpPriority').value = config.priority;
}
} else {
// 添加模式
document.getElementById('smtpModalTitle').textContent = '添加SMTP配置';
document.getElementById('smtpEditActions').style.display = 'none';
document.getElementById('smtpConfigId').value = '';
document.getElementById('smtpName').value = '';
document.getElementById('smtpEnabled').checked = true;
document.getElementById('smtpHost').value = '';
document.getElementById('smtpPort').value = 465;
document.getElementById('smtpUsername').value = '';
document.getElementById('smtpPassword').value = '';
document.getElementById('smtpPassword').placeholder = 'SMTP密码或授权码';
document.getElementById('smtpUseSsl').checked = true;
document.getElementById('smtpUseTls').checked = false;
document.getElementById('smtpSenderName').value = '知识管理平台';
document.getElementById('smtpSenderEmail').value = '';
document.getElementById('smtpDailyLimit').value = 0;
document.getElementById('smtpPriority').value = 0;
}
}
// 隐藏SMTP配置弹窗
function hideSmtpModal() {
document.getElementById('smtpModal').style.display = 'none';
}
// 编辑SMTP配置
function editSmtpConfig(configId) {
showSmtpModal(configId);
}
// 保存SMTP配置
async function saveSmtpConfig() {
const configId = document.getElementById('smtpConfigId').value;
const host = document.getElementById('smtpHost').value.trim();
const username = document.getElementById('smtpUsername').value.trim();
if (!host) {
showNotification('请输入SMTP服务器地址', 'error');
return;
}
if (!username) {
showNotification('请输入SMTP用户名', 'error');
return;
}
const data = {
name: document.getElementById('smtpName').value.trim() || '默认配置',
enabled: document.getElementById('smtpEnabled').checked,
host: host,
port: parseInt(document.getElementById('smtpPort').value) || 465,
username: username,
use_ssl: document.getElementById('smtpUseSsl').checked,
use_tls: document.getElementById('smtpUseTls').checked,
sender_name: document.getElementById('smtpSenderName').value.trim(),
sender_email: document.getElementById('smtpSenderEmail').value.trim(),
daily_limit: parseInt(document.getElementById('smtpDailyLimit').value) || 0,
priority: parseInt(document.getElementById('smtpPriority').value) || 0
};
const password = document.getElementById('smtpPassword').value;
if (password) {
data.password = password;
}
try {
let response;
if (configId) {
// 更新
response = await fetch('/yuyx/api/smtp/configs/' + configId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
} else {
// 新增
if (!password) {
showNotification('新建配置需要输入密码', 'error');
return;
}
response = await fetch('/yuyx/api/smtp/configs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
}
const result = await response.json();
if (response.ok) {
showNotification(configId ? '配置已更新' : '配置已添加', 'success');
hideSmtpModal();
loadSmtpConfigs();
} else {
showNotification('保存失败: ' + result.error, 'error');
}
} catch (error) {
showNotification('保存失败: ' + error.message, 'error');
}
}
// 测试SMTP配置
async function testSmtpConfig() {
const configId = document.getElementById('smtpConfigId').value;
if (!configId) {
showNotification('请先保存配置再测试', 'error');
return;
}
const testEmail = prompt('请输入测试接收邮箱:');
if (!testEmail) return;
showNotification('正在发送测试邮件...', 'info');
try {
const response = await fetch('/yuyx/api/smtp/configs/' + configId + '/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: testEmail })
});
const result = await response.json();
if (result.success) {
showNotification('测试邮件发送成功!请检查收件箱', 'success');
} else {
showNotification('测试失败: ' + result.error, 'error');
}
} catch (error) {
showNotification('测试失败: ' + error.message, 'error');
}
}
// 设为主配置
async function setPrimarySmtp() {
const configId = document.getElementById('smtpConfigId').value;
if (!configId) return;
if (!confirm('确定要将此配置设为主配置吗?')) return;
try {
const response = await fetch('/yuyx/api/smtp/configs/' + configId + '/primary', {
method: 'POST'
});
if (response.ok) {
showNotification('已设为主配置', 'success');
hideSmtpModal();
loadSmtpConfigs();
} else {
const result = await response.json();
showNotification('设置失败: ' + result.error, 'error');
}
} catch (error) {
showNotification('设置失败: ' + error.message, 'error');
}
}
// 删除SMTP配置
async function deleteSmtpConfig() {
const configId = document.getElementById('smtpConfigId').value;
if (!configId) return;
if (!confirm('确定要删除此SMTP配置吗此操作不可恢复')) return;
try {
const response = await fetch('/yuyx/api/smtp/configs/' + configId, {
method: 'DELETE'
});
if (response.ok) {
showNotification('配置已删除', 'success');
hideSmtpModal();
loadSmtpConfigs();
} else {
const result = await response.json();
showNotification('删除失败: ' + result.error, 'error');
}
} catch (error) {
showNotification('删除失败: ' + error.message, 'error');
}
}
// 加载邮件统计
async function loadEmailStats() {
try {
const response = await fetch('/yuyx/api/email/stats');
if (response.ok) {
const stats = await response.json();
document.getElementById('statTotalSent').textContent = stats.total_sent || 0;
document.getElementById('statTotalSuccess').textContent = stats.total_success || 0;
document.getElementById('statTotalFailed').textContent = stats.total_failed || 0;
document.getElementById('statSuccessRate').textContent = (stats.success_rate || 0) + '%';
document.getElementById('statRegister').textContent = stats.register_sent || 0;
document.getElementById('statReset').textContent = stats.reset_sent || 0;
document.getElementById('statBind').textContent = stats.bind_sent || 0;
document.getElementById('statTaskComplete').textContent = stats.task_complete_sent || 0;
}
} catch (error) {
console.error('加载邮件统计失败:', error);
}
}
// 加载邮件日志
async function loadEmailLogs(page = 1) {
currentEmailLogPage = page;
const typeFilter = document.getElementById('emailLogTypeFilter').value;
const statusFilter = document.getElementById('emailLogStatusFilter').value;
let url = `/yuyx/api/email/logs?page=${page}&page_size=15`;
if (typeFilter) url += `&type=${typeFilter}`;
if (statusFilter) url += `&status=${statusFilter}`;
try {
const response = await fetch(url);
if (response.ok) {
const data = await response.json();
totalEmailLogPages = data.total_pages;
renderEmailLogs(data.logs);
renderEmailLogsPagination(data);
}
} catch (error) {
console.error('加载邮件日志失败:', error);
document.getElementById('emailLogsList').innerHTML =
'<div style="text-align: center; padding: 30px; color: #e74c3c;">加载失败</div>';
}
}
// 渲染邮件日志
function renderEmailLogs(logs) {
const container = document.getElementById('emailLogsList');
if (!logs || logs.length === 0) {
container.innerHTML = '<div style="text-align: center; padding: 30px; color: #999;">暂无邮件日志</div>';
return;
}
const typeMap = {
'register': '注册验证',
'reset': '密码重置',
'bind': '邮箱绑定',
'task_complete': '任务完成',
'security_alert': '安全告警'
};
let html = '<div class="table-container"><table style="width: 100%; font-size: 12px;"><thead><tr>';
html += '<th>时间</th><th>收件人</th><th>来源用户</th><th>类型</th><th>主题</th><th>状态</th><th>错误</th>';
html += '</tr></thead><tbody>';
logs.forEach(log => {
const statusClass = log.status === 'success' ? 'color: #27ae60;' : 'color: #e74c3c;';
const statusText = log.status === 'success' ? '成功' : '失败';
const userLabel = log.username
? `${log.username} (#${log.user_id})`
: (log.user_id ? `用户#${log.user_id}` : '系统');
html += '<tr>';
html += `<td style="white-space: nowrap;">${escapeHtml(log.created_at)}</td>`;
html += `<td>${escapeHtml(log.email_to)}</td>`;
html += `<td>${escapeHtml(userLabel)}</td>`;
html += `<td>${escapeHtml(typeMap[log.email_type] || log.email_type)}</td>`;
html += `<td style="max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${escapeHtml(log.subject)}">${escapeHtml(log.subject)}</td>`;
html += `<td style="${statusClass} font-weight: bold;">${statusText}</td>`;
html += `<td style="max-width: 100px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #999;" title="${escapeHtml(log.error_message || '')}">${escapeHtml(log.error_message || '-')}</td>`;
html += '</tr>';
});
html += '</tbody></table></div>';
container.innerHTML = html;
}
// 渲染邮件日志分页
function renderEmailLogsPagination(data) {
const container = document.getElementById('emailLogsPagination');
if (data.total_pages <= 1) {
container.innerHTML = `<span style="font-size: 12px; color: #999;">共 ${data.total} 条记录</span>`;
return;
}
let html = '';
html += `<button class="btn btn-secondary" onclick="loadEmailLogs(1)" ${data.page <= 1 ? 'disabled' : ''} style="padding: 6px 12px;">首页</button>`;
html += `<button class="btn btn-secondary" onclick="loadEmailLogs(${data.page - 1})" ${data.page <= 1 ? 'disabled' : ''} style="padding: 6px 12px;">上一页</button>`;
html += `<span style="font-size: 13px; color: #666;">第 ${data.page} 页 / 共 ${data.total_pages} 页</span>`;
html += `<button class="btn btn-secondary" onclick="loadEmailLogs(${data.page + 1})" ${data.page >= data.total_pages ? 'disabled' : ''} style="padding: 6px 12px;">下一页</button>`;
html += `<button class="btn btn-secondary" onclick="loadEmailLogs(${data.total_pages})" ${data.page >= data.total_pages ? 'disabled' : ''} style="padding: 6px 12px;">末页</button>`;
html += `<span style="font-size: 12px; color: #999; margin-left: 10px;">共 ${data.total} 条记录</span>`;
container.innerHTML = html;
}
// 清理邮件日志
async function cleanupEmailLogs() {
const days = prompt('请输入保留天数(将删除该天数之前的日志):', '30');
if (!days) return;
const daysNum = parseInt(days);
if (isNaN(daysNum) || daysNum < 7) {
showNotification('天数必须大于等于7', 'error');
return;
}
if (!confirm(`确定要删除 ${daysNum} 天之前的邮件日志吗?`)) return;
try {
const response = await fetch('/yuyx/api/email/logs/cleanup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ days: daysNum })
});
const result = await response.json();
if (response.ok) {
showNotification(`已清理 ${result.deleted} 条日志`, 'success');
loadEmailLogs();
} else {
showNotification('清理失败: ' + result.error, 'error');
}
} catch (error) {
showNotification('清理失败: ' + error.message, 'error');
}
}
// 切换密码显示
function togglePasswordVisibility(inputId) {
const input = document.getElementById(inputId);
if (input.type === 'password') {
input.type = 'text';
} else {
input.type = 'password';
}
}
</script>
</body>
</html>