3469 lines
155 KiB
HTML
3469 lines
155 KiB
HTML
<!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>
|
||
|
||
<h3 style="margin-top: 30px; margin-bottom: 15px; font-size: 16px;">密码重置审核</h3>
|
||
<div id="passwordResetsList"></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;">
|
||
说明:同时进行截图的最大数量。每个浏览器约占用200MB内存。
|
||
</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>
|
||
<option value="未读">未读</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 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 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>
|
||
</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;">×</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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
// 页面加载时初始化
|
||
window.addEventListener('load', () => {
|
||
loadStats();
|
||
loadPendingUsers();
|
||
loadAllUsers();
|
||
loadAnnouncements();
|
||
loadSystemConfig();
|
||
loadProxyConfig();
|
||
loadPasswordResets(); // 修复: 初始化时也加载密码重置申请
|
||
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: 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.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 = '';
|
||
}
|
||
|
||
function viewAnnouncement(id) {
|
||
const announcement = announcements.find(a => a.id === id);
|
||
if (!announcement) return;
|
||
alert(`标题:${announcement.title || ''}\n\n内容:\n${announcement.content || ''}`);
|
||
}
|
||
|
||
async function createAnnouncement(isActive) {
|
||
const title = (document.getElementById('announcementTitle')?.value || '').trim();
|
||
const content = (document.getElementById('announcementContent')?.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, 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 isVip(user) {
|
||
if (!user.vip_expire_time) return false;
|
||
const expireTime = new Date(user.vip_expire_time);
|
||
return expireTime > new Date();
|
||
}
|
||
|
||
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 = new Date(user.vip_expire_time);
|
||
const daysLeft = Math.ceil((expireTime - new Date()) / (1000*60*60*24));
|
||
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>${user.username}</strong> ${getVipBadge(user)}</div>
|
||
${getVipExpire(user)}
|
||
</td>
|
||
<td>${user.email || '-'}</td>
|
||
<td>${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>${user.username}</strong> ${getVipBadge(user)}</div>
|
||
${getVipExpire(user)}
|
||
${user.email ? '<div class="user-info">'+user.email+'</div>' : ''}
|
||
</td>
|
||
<td>
|
||
<span class="status-badge status-${user.status}">
|
||
${user.status === 'pending' ? '待审核' : user.status === 'approved' ? '已通过' : '已拒绝'}
|
||
</span>
|
||
</td>
|
||
<td>
|
||
${user.created_at}
|
||
${user.approved_at ? '<div class="user-info">审核:'+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 < 6) {
|
||
showNotification('密码至少6个字符', '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 || '应读';
|
||
|
||
// 加载星期选择
|
||
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');
|
||
|
||
if (enabled) {
|
||
timeGroup.style.display = 'block';
|
||
browseTypeGroup.style.display = 'block';
|
||
weekdaysGroup.style.display = 'block';
|
||
} else {
|
||
timeGroup.style.display = 'none';
|
||
browseTypeGroup.style.display = 'none';
|
||
weekdaysGroup.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 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\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
|
||
})
|
||
});
|
||
|
||
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 source = sourceMap[task.source] || {text: task.source, color: '#666'};
|
||
// 状态颜色映射
|
||
const statusColorMap = {
|
||
'初始化': '#6c757d',
|
||
'正在登录': '#fd7e14',
|
||
'正在浏览': '#28a745',
|
||
'浏览完成': '#007bff',
|
||
'正在截图': '#17a2b8'
|
||
};
|
||
const statusColor = statusColorMap[task.detail_status] || '#666';
|
||
// 进度显示
|
||
const progressText = task.progress_items > 0 || task.progress_attachments > 0
|
||
? `(${task.progress_items}/${task.progress_attachments})`
|
||
: '';
|
||
return `<div style="display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: #f8f9fa; border-radius: 5px; margin-bottom: 5px; border-left: 3px solid ${statusColor};">
|
||
<div style="flex: 1;">
|
||
<div style="display: flex; align-items: center; flex-wrap: wrap; gap: 5px;">
|
||
<span style="color: ${source.color}; font-weight: 500; font-size: 12px;">[${source.text}]</span>
|
||
<span style="color: #333;">${task.user_username}</span>
|
||
<span style="color: #666;">→</span>
|
||
<span style="color: #007bff; font-weight: 500;">${task.username}</span>
|
||
<span style="background: #e9ecef; padding: 2px 6px; border-radius: 3px; font-size: 11px; color: #666;">${task.browse_type}</span>
|
||
</div>
|
||
<div style="margin-top: 4px; display: flex; align-items: center; gap: 8px;">
|
||
<span style="color: ${statusColor}; font-weight: 500; font-size: 12px;">● ${task.detail_status}</span>
|
||
${progressText ? `<span style="color: #999; font-size: 11px;">内容/附件: ${progressText}</span>` : ''}
|
||
</div>
|
||
</div>
|
||
<div style="text-align: right; min-width: 70px;">
|
||
<div style="color: #28a745; font-weight: 500;">${task.elapsed_display}</div>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
// 渲染排队中的任务
|
||
const queuingList = document.getElementById('queuingTasksList');
|
||
if (data.queuing.length === 0) {
|
||
queuingList.innerHTML = '<div style="color: #999; text-align: center; padding: 10px;">暂无排队中的任务</div>';
|
||
} else {
|
||
queuingList.innerHTML = data.queuing.map(task => {
|
||
const source = sourceMap[task.source] || {text: task.source, color: '#666'};
|
||
return `<div style="display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: #fff8e6; border-radius: 5px; margin-bottom: 5px; border-left: 3px solid #fd7e14;">
|
||
<div style="flex: 1;">
|
||
<div style="display: flex; align-items: center; flex-wrap: wrap; gap: 5px;">
|
||
<span style="color: ${source.color}; font-weight: 500; font-size: 12px;">[${source.text}]</span>
|
||
<span style="color: #333;">${task.user_username}</span>
|
||
<span style="color: #666;">→</span>
|
||
<span style="color: #007bff; font-weight: 500;">${task.username}</span>
|
||
<span style="background: #ffeeba; padding: 2px 6px; border-radius: 3px; font-size: 11px; color: #856404;">${task.browse_type}</span>
|
||
</div>
|
||
<div style="margin-top: 4px;">
|
||
<span style="color: #fd7e14; font-size: 12px;">● ${task.detail_status || '等待资源'}</span>
|
||
</div>
|
||
</div>
|
||
<div style="text-align: right; min-width: 80px;">
|
||
<div style="color: #fd7e14; font-weight: 500;">等待 ${task.elapsed_display}</div>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('加载运行任务失败:', error);
|
||
}
|
||
}
|
||
|
||
// 任务统计和日志功能
|
||
let currentLogPage = 1;
|
||
let totalLogPages = 1;
|
||
let totalLogCount = 0;
|
||
const logsPerPage = 20;
|
||
|
||
// 加载用户列表用于筛选
|
||
async function loadLogUserOptions() {
|
||
try {
|
||
const response = await fetch('/yuyx/api/users');
|
||
if (response.ok) {
|
||
const users = await response.json();
|
||
const select = document.getElementById('logUserFilter');
|
||
select.innerHTML = '<option value="">全部</option>';
|
||
users.forEach(user => {
|
||
select.innerHTML += `<option value="${user.id}">${user.username}</option>`;
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('加载用户列表失败:', error);
|
||
}
|
||
}
|
||
|
||
// 重置筛选条件
|
||
function resetLogFilters() {
|
||
document.getElementById('logDateFilter').value = '';
|
||
document.getElementById('logStatusFilter').value = '';
|
||
document.getElementById('logSourceFilter').value = '';
|
||
document.getElementById('logUserFilter').value = '';
|
||
document.getElementById('logAccountFilter').value = '';
|
||
currentLogPage = 1;
|
||
loadTaskLogs();
|
||
}
|
||
|
||
// 跳转到指定页
|
||
function goToLogPage(page) {
|
||
if (page < 1 || page > totalLogPages) return;
|
||
currentLogPage = page;
|
||
loadTaskLogs();
|
||
}
|
||
|
||
// 更新分页控件状态
|
||
function updatePaginationUI() {
|
||
document.getElementById('logFirstBtn').disabled = currentLogPage <= 1;
|
||
document.getElementById('logPrevBtn').disabled = currentLogPage <= 1;
|
||
document.getElementById('logNextBtn').disabled = currentLogPage >= totalLogPages;
|
||
document.getElementById('logLastBtn').disabled = currentLogPage >= totalLogPages;
|
||
document.getElementById('logPageInfo').textContent = `第 ${currentLogPage} 页 / 共 ${totalLogPages} 页`;
|
||
document.getElementById('logTotalCount').textContent = totalLogCount;
|
||
}
|
||
|
||
async function loadTaskLogs() {
|
||
try {
|
||
const dateFilter = document.getElementById('logDateFilter').value;
|
||
const statusFilter = document.getElementById('logStatusFilter').value;
|
||
const sourceFilter = document.getElementById('logSourceFilter').value;
|
||
const userFilter = document.getElementById('logUserFilter').value;
|
||
const accountFilter = document.getElementById('logAccountFilter').value;
|
||
|
||
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 sourceInfo = sourceMap[log.source] || {text: log.source || '手动', color: '#28a745'};
|
||
|
||
row.innerHTML = `
|
||
<td>${log.created_at}</td>
|
||
<td><span style="color: ${sourceInfo.color}; font-weight: 500;">${sourceInfo.text}</span></td>
|
||
<td>${log.user_username || 'N/A'}</td>
|
||
<td>${log.username}</td>
|
||
<td>${log.browse_type}</td>
|
||
<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;">${log.error_message || '-'}</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();
|
||
} else if (tabName === 'pending') {
|
||
loadPasswordResets();
|
||
}
|
||
};
|
||
|
||
// ==================== 密码重置功能 ====================
|
||
|
||
let passwordResets = [];
|
||
|
||
// 加载密码重置申请列表
|
||
async function loadPasswordResets() {
|
||
try {
|
||
const response = await fetch('/yuyx/api/password_resets');
|
||
if (response.ok) {
|
||
passwordResets = await response.json();
|
||
renderPasswordResets();
|
||
}
|
||
} catch (error) {
|
||
console.error('加载密码重置申请失败:', error);
|
||
}
|
||
}
|
||
|
||
// 渲染密码重置申请列表
|
||
function renderPasswordResets() {
|
||
const container = document.getElementById('passwordResetsList');
|
||
|
||
if (passwordResets.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>
|
||
${passwordResets.map(reset => `
|
||
<tr>
|
||
<td>${reset.id}</td>
|
||
<td><strong>${reset.username}</strong></td>
|
||
<td>${reset.email || '-'}</td>
|
||
<td>${reset.created_at}</td>
|
||
<td>
|
||
<div class="action-buttons">
|
||
<button class="btn btn-small btn-success" onclick="approvePasswordReset(${reset.id})">批准</button>
|
||
<button class="btn btn-small btn-danger" onclick="rejectPasswordReset(${reset.id})">拒绝</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// 批准密码重置申请
|
||
async function approvePasswordReset(requestId) {
|
||
if (!confirm('确定批准该密码重置申请吗?')) return;
|
||
|
||
try {
|
||
const response = await fetch(`/yuyx/api/password_resets/${requestId}/approve`, {
|
||
method: 'POST'
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (response.ok) {
|
||
showNotification('密码重置申请已批准', 'success');
|
||
loadPasswordResets();
|
||
} else {
|
||
showNotification('批准失败: ' + data.error, 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('批准失败: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
// 拒绝密码重置申请
|
||
async function rejectPasswordReset(requestId) {
|
||
if (!confirm('确定拒绝该密码重置申请吗?')) return;
|
||
|
||
try {
|
||
const response = await fetch(`/yuyx/api/password_resets/${requestId}/reject`, {
|
||
method: 'POST'
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (response.ok) {
|
||
showNotification('密码重置申请已拒绝', 'success');
|
||
loadPasswordResets();
|
||
} else {
|
||
showNotification('拒绝失败: ' + data.error, 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('拒绝失败: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
// 管理员直接重置用户密码
|
||
async function resetUserPassword(userId) {
|
||
const newPassword = prompt('请输入新密码(至少6位):');
|
||
if (!newPassword) return;
|
||
|
||
if (newPassword.length < 6) {
|
||
showNotification('密码长度至少6位', '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>' + (fb.username || 'N/A') + '</strong></td>';
|
||
html += '<td style="max-width:150px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="' + (fb.title||'') + '">' + (fb.title||'') + '</td>';
|
||
html += '<td style="max-width:200px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="' + (fb.description||'') + '">' + (fb.description||'') + '</td>';
|
||
html += '<td>' + (fb.contact || '-') + '</td>';
|
||
html += '<td>' + getStatusBadge(fb.status) + '</td>';
|
||
html += '<td>' + fb.created_at + '</td>';
|
||
html += '<td style="max-width:150px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="' + (fb.admin_reply || '') + '">' + (fb.admin_reply || '-') + '</td>';
|
||
html += '<td><div class="action-buttons">';
|
||
if (fb.status !== 'closed') {
|
||
html += '<button class="btn btn-small btn-primary" onclick="replyFeedback(' + fb.id + ')">回复</button>';
|
||
html += '<button class="btn btn-small btn-secondary" onclick="closeFeedback(' + fb.id + ')">关闭</button>';
|
||
}
|
||
html += '<button class="btn btn-small btn-danger" onclick="deleteFeedback(' + fb.id + ')">删除</button>';
|
||
html += '</div></td></tr>';
|
||
});
|
||
|
||
html += '</tbody></table></div>';
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
async function replyFeedback(feedbackId) {
|
||
const reply = prompt('请输入回复内容:');
|
||
if (!reply || !reply.trim()) return;
|
||
|
||
try {
|
||
const response = await fetch('/yuyx/api/feedbacks/' + feedbackId + '/reply', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ reply: reply.trim() })
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (response.ok) {
|
||
showNotification('回复成功', 'success');
|
||
loadFeedbacks();
|
||
} else {
|
||
showNotification('回复失败: ' + (data.error || '未知错误'), 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('回复失败: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
async function closeFeedback(feedbackId) {
|
||
if (!confirm('确定要关闭这个反馈吗?')) return;
|
||
|
||
try {
|
||
const response = await fetch('/yuyx/api/feedbacks/' + feedbackId + '/close', {
|
||
method: 'POST'
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (response.ok) {
|
||
showNotification('反馈已关闭', 'success');
|
||
loadFeedbacks();
|
||
} else {
|
||
showNotification('关闭失败: ' + (data.error || '未知错误'), 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('关闭失败: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
async function deleteFeedback(feedbackId) {
|
||
if (!confirm('确定要删除这个反馈吗?此操作不可恢复!')) return;
|
||
|
||
try {
|
||
const response = await fetch('/yuyx/api/feedbacks/' + feedbackId, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (response.ok) {
|
||
showNotification('反馈已删除', 'success');
|
||
loadFeedbacks();
|
||
} else {
|
||
showNotification('删除失败: ' + (data.error || '未知错误'), 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('删除失败: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
// ==================== 邮件配置功能 ====================
|
||
|
||
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': '任务完成'
|
||
};
|
||
|
||
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>';
|
||
html += '</tr></thead><tbody>';
|
||
|
||
logs.forEach(log => {
|
||
const statusClass = log.status === 'success' ? 'color: #27ae60;' : 'color: #e74c3c;';
|
||
const statusText = log.status === 'success' ? '成功' : '失败';
|
||
|
||
html += '<tr>';
|
||
html += `<td style="white-space: nowrap;">${log.created_at}</td>`;
|
||
html += `<td>${log.email_to}</td>`;
|
||
html += `<td>${typeMap[log.email_type] || log.email_type}</td>`;
|
||
html += `<td style="max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${log.subject}">${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="${log.error_message || ''}">${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>
|