Files
zsglpt/templates/admin.html
Yu Yon 0fd7137cea 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
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>后台管理 v2.0 - 知识管理平台</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: #f5f6fa;
min-height: 100vh;
}
.header {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
padding: 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>