Files
zsglpt/templates/admin.html
T
Yu Yon 1723e35fbc Initial commit: 知识管理平台
主要功能:
- 多用户管理系统
- 浏览器自动化(Playwright)
- 任务编排和执行
- Docker容器化部署
- 数据持久化和日志管理

技术栈:
- Flask 3.0.0
- Playwright 1.40.0
- SQLite with connection pooling
- Docker + Docker Compose

部署说明详见README.md
2025-11-16 19:03:07 +08:00

1745 lines
69 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: 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: 10px;
}
.header h1 {
font-size: 18px;
}
.header-actions {
display: flex;
gap: 10px;
align-items: center;
}
.header-actions > span {
display: inline;
font-size: 13px;
}
.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;
}
.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: 3px;
}
/* 平板及以上屏幕 */
@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('stats')">统计</button>
<button class="tab" onclick="switchTab('logs')">任务日志</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-system" class="tab-content">
<h3 style="margin-bottom: 15px; font-size: 16px;">系统并发配置</h3>
<div class="form-group">
<label>全局最大并发数 (1-20)</label>
<input type="number" id="maxConcurrent" min="1" max="20" value="2" style="max-width: 200px;">
<div style="font-size: 12px; color: #666; margin-top: 5px;">
说明:同时最多运行的账号数量。服务器内存1.7GB,建议设置2-3。每个浏览器约占用200MB内存。
</div>
</div>
<div class="form-group">
<label>单账号最大并发数 (1-5)</label>
<input type="number" id="maxConcurrentPerAccount" min="1" max="5" value="1" style="max-width: 200px;">
<div style="font-size: 12px; color: #666; margin-top: 5px;">
说明:单个账号同时最多运行的任务数量。建议设置1-2。
</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;">
<button class="btn btn-primary" onclick="updateSchedule()">保存定时任务配置</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>
</div>
<!-- 统计 -->
<div id="tab-stats" class="tab-content">
<h3 style="margin-bottom: 15px; font-size: 16px;">服务器信息</h3>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin-bottom: 25px;">
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 24px; font-weight: bold; color: #f5576c;" id="serverCpu">-</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">CPU使用率</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 24px; font-weight: bold; color: #f093fb;" id="serverMemory">-</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">内存使用</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 24px; font-weight: bold; color: #764ba2;" id="serverDisk">-</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">磁盘使用</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 24px; font-weight: bold; color: #17a2b8;" id="serverUptime">-</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">运行时长</div>
</div>
</div>
<h3 style="margin: 25px 0 15px 0; font-size: 16px;">Docker容器状态</h3>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin-bottom: 25px;">
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 20px; font-weight: bold; color: #28a745;" id="dockerContainerName">-</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">容器名称</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 20px; font-weight: bold; color: #17a2b8;" id="dockerUptime">-</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">容器运行时间</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 20px; font-weight: bold; color: #f093fb;" id="dockerMemory">-</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">容器内存使用</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 20px; font-weight: bold;" id="dockerStatus" style="color: #28a745;">-</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">运行状态</div>
</div>
</div>
<h3 style="margin: 25px 0 15px 0; font-size: 16px;">当日任务统计</h3>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin-bottom: 25px;">
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 32px; font-weight: bold; color: #28a745;" id="todaySuccessTasks">0</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">成功任务</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 32px; font-weight: bold; color: #dc3545;" id="todayFailedTasks">0</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">失败任务</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 32px; font-weight: bold; color: #007bff;" id="todayTotalItems">0</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">浏览内容数</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 32px; font-weight: bold; color: #17a2b8;" id="todayTotalAttachments">0</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">查看附件数</div>
</div>
</div>
<h3 style="margin: 25px 0 15px 0; font-size: 16px;">历史累计统计</h3>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px;">
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 32px; font-weight: bold; color: #28a745;" id="totalSuccessTasks">0</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">累计成功任务</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 32px; font-weight: bold; color: #dc3545;" id="totalFailedTasks">0</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">累计失败任务</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 32px; font-weight: bold; color: #007bff;" id="totalTotalItems">0</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">累计浏览内容</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 32px; font-weight: bold; color: #17a2b8;" id="totalTotalAttachments">0</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">累计查看附件</div>
</div>
</div>
</div>
<!-- 任务日志 -->
<div id="tab-logs" class="tab-content">
<div style="margin-bottom: 15px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center;">
<input type="date" id="logDateFilter" style="padding: 8px; border: 1px solid #ddd; border-radius: 5px; font-size: 13px;">
<select id="logStatusFilter" style="padding: 8px; border: 1px solid #ddd; border-radius: 5px; font-size: 13px;">
<option value="">全部状态</option>
<option value="success">成功</option>
<option value="failed">失败</option>
</select>
<button class="btn btn-primary" onclick="loadTaskLogs()">筛选</button>
<button class="btn btn-secondary" onclick="clearOldLogs()">清理旧日志</button>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>时间</th>
<th>用户</th>
<th>账号</th>
<th>浏览类型</th>
<th>状态</th>
<th>内容数/附件数</th>
<th>执行用时</th>
<th>失败原因</th>
</tr>
</thead>
<tbody id="taskLogsList">
<tr><td colspan="8" class="empty-message">加载中...</td></tr>
</tbody>
</table>
</div>
<div style="margin-top: 15px; text-align: center;">
<button class="btn btn-secondary" onclick="loadMoreLogs()" id="loadMoreBtn">加载更多</button>
</div>
</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>
<!-- 通知组件 -->
<div id="notification" class="notification"></div>
<script>
let allUsers = [];
let pendingUsers = [];
// 页面加载时初始化
window.addEventListener('load', () => {
loadStats();
loadPendingUsers();
loadAllUsers();
loadSystemConfig();
loadProxyConfig();
loadPasswordResets(); // 修复: 初始化时也加载密码重置申请
});
// 切换标签
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');
// 切换到统计标签时加载服务器信息和任务统计
if (tabName === 'stats') {
loadServerInfo();
loadDockerStats();
loadTaskStats();
}
// 切换到日志标签时加载任务日志
if (tabName === 'logs') {
loadTaskLogs();
}
}
// 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';
}, 3000);
}
// 系统配置功能
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('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);
}
} 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 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);
if (maxConcurrent < 1 || maxConcurrent > 20) {
showNotification('全局并发数必须在1-20之间', 'error');
return;
}
if (maxConcurrentPerAccount < 1 || maxConcurrentPerAccount > 5) {
showNotification('单账号并发数必须在1-5之间', 'error');
return;
}
if (!confirm(`确定更新并发配置吗?\n\n全局并发数: ${maxConcurrent}\n单账号并发数: ${maxConcurrentPerAccount}\n\n建议值:服务器内存1.7GB时全局设置2-3,单账号设置1-2`)) 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
})
});
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');
}
}
// 任务统计和日志功能
let logsOffset = 0;
const logsLimit = 50;
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 loadTaskLogs(reset = true) {
try {
if (reset) {
logsOffset = 0;
}
const dateFilter = document.getElementById('logDateFilter').value;
const statusFilter = document.getElementById('logStatusFilter').value;
let url = `/yuyx/api/task/logs?limit=${logsLimit}&offset=${logsOffset}`;
if (dateFilter) url += `&date=${dateFilter}`;
if (statusFilter) url += `&status=${statusFilter}`;
const response = await fetch(url);
if (response.ok) {
const logs = await response.json();
const tbody = document.getElementById('taskLogsList');
if (logs.length === 0 && reset) {
tbody.innerHTML = '<tr><td colspan="8" class="empty-message">暂无日志记录</td></tr>';
document.getElementById('loadMoreBtn').style.display = 'none';
return;
}
if (reset) {
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}秒`;
};
row.innerHTML = `
<td>${log.created_at}</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);
});
// 显示/隐藏加载更多按钮
document.getElementById('loadMoreBtn').style.display = logs.length < logsLimit ? 'none' : 'inline-block';
}
} catch (error) {
console.error('加载任务日志失败:', error);
}
}
function loadMoreLogs() {
logsOffset += logsLimit;
loadTaskLogs(false);
}
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函数,在切换到统计和日志标签时加载数据
const originalSwitchTab = switchTab;
switchTab = function(tabName) {
originalSwitchTab(tabName);
if (tabName === 'stats') {
loadDockerStats();
loadTaskStats();
} else if (tabName === 'logs') {
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');
}
}
</script>
</body>
</html>