Files
cursor-auto-register/index.html
2025-03-27 09:41:06 +08:00

1793 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">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Cursor账号管理系统</title>
<!-- Bootstrap 5 CSS -->
<link href="static/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome 图标 -->
<link rel="stylesheet" href="static/css/all.min.css">
<!-- 自定义样式 -->
<style>
body {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
font-family: 'Inter', system-ui, -apple-system, sans-serif;
color: #212529;
min-height: 100vh;
}
/* 组合相似的选择器,减少重复 */
.card, .table, .alert {
border-radius: 12px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
.card {
border: none;
transition: all 0.3s ease;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
}
.card-header {
background: rgba(255, 255, 255, 0.9);
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
font-weight: 600;
color: #111827;
}
/* 优化按钮相关样式 */
.btn {
transition: all 0.3s ease;
position: relative;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.15);
}
.btn:active {
transform: translateY(1px);
}
/* 优化按钮波纹效果 */
.btn::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 5px;
height: 5px;
background: rgba(255, 255, 255, 0.5);
opacity: 0;
border-radius: 100%;
transform: scale(1) translate(-50%, -50%);
transform-origin: 0 0;
}
.btn:focus:not(:active)::after {
animation: ripple 1s ease-out;
}
@keyframes ripple {
0% { transform: scale(0); opacity: 0.6; }
100% { transform: scale(25); opacity: 0; }
}
/* 优化表格样式 */
.table {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
width: 100%;
margin-bottom: 1rem;
}
.table td, .table th {
padding: 0.75rem 1rem;
vertical-align: middle;
}
/* 简化表格悬停效果 */
.table tbody tr:hover {
background-color: rgba(241, 245, 249, 0.7);
transform: scale(1.01);
}
/* 合并加载遮罩样式 */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: none;
justify-content: center;
align-items: center;
z-index: 10000;
pointer-events: none;
}
.loading-overlay.show {
display: flex;
pointer-events: auto;
}
.spinner-container {
background-color: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
text-align: center;
}
.stat-number {
font-size: 2rem;
font-weight: 700;
background: linear-gradient(120deg, #4f46e5, #7c3aed);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 5px;
}
.stat-label {
font-size: 0.9rem;
color: #1f2937;
font-weight: 500;
}
.badge {
font-weight: 500;
padding: 0.35em 0.6em;
border-radius: 4px;
}
.operation-column .btn {
margin: 0.15rem;
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
.email-column {
max-width: 250px;
word-break: break-all;
}
.password-cell, .token-cell {
position: relative;
}
.copy-btn {
margin-left: 0.25rem !important;
}
.container {
max-width: 1400px;
padding: 1.5rem;
}
.table th {
background-color: #f3f4f6;
font-weight: 600;
color: #111827;
border-top: none;
}
.table tbody tr {
transition: all 0.2s ease;
}
#fireworks-canvas {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 9999;
pointer-events: none;
display: none;
}
#alert-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 10000;
max-width: 400px;
}
.alert {
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15);
border-radius: 10px;
border-left: 4px solid;
}
.alert-success {
border-left-color: #10b981;
}
.alert-danger {
border-left-color: #ef4444;
}
.special-alert {
background: linear-gradient(135deg, #10b981, #059669);
color: white;
font-weight: 500;
transform-origin: center;
}
.text-muted {
color: #6c757d !important;
}
/* 调整卡片内部间距,使内容更紧凑 */
.card-body {
padding: 1.25rem;
}
/* 调整统计图表容器高度 */
#usage-chart-container {
height: 200px; /* 从250px减小到200px */
}
/* 调整任务控制详情部分 */
#task-details .card {
margin-bottom: 0.5rem; /* 减小底部边距 */
}
#task-details .card .card-body {
padding: 0.5rem; /* 减小内部填充 */
}
/* 调整操作按钮大小和间距 */
.task-control-buttons .btn {
padding: 0.375rem 0.75rem;
}
/* 调整任务状态信息间距 */
.task-status-info {
margin-bottom: 0.75rem;
}
/* 调整图表大小 */
#usage-chart {
max-height: 160px; /* 从200px减小到160px */
}
/* 使用情况进度条样式更新 */
.usage-progress-container {
display: flex;
flex-direction: column;
width: 100%;
margin-bottom: 5px;
}
/* 使用情况列样式优化 */
.usage-info {
min-width: 140px;
}
.usage-numbers {
font-size: 0.8rem;
margin-bottom: 3px;
white-space: nowrap;
}
.used-count {
font-weight: 500;
color: #333;
}
.total-count {
color: #666;
}
.remaining-count {
font-size: 0.75rem;
color: #28a745;
margin-left: 4px;
}
/* 修改进度条背景为灰色 */
.usage-progress {
height: 8px;
background-color: #e9ecef;
border-radius: 4px;
overflow: hidden;
}
/* 修改进度条填充颜色为浅红色 */
.usage-progress-bar {
height: 100%;
background: linear-gradient(90deg, #ff8a8a, #ffb3b3);
border-radius: 4px;
transition: width 0.5s ease;
}
/* 调整表格列宽 */
.usage-column {
min-width: 180px;
}
.email-column {
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
}
/* 高亮显示当前行 */
.table tbody tr.active {
background-color: rgba(79, 70, 229, 0.05);
}
/* 调整使用量查询按钮 */
.get-usage-btn {
white-space: nowrap;
}
/* 保留操作列样式,但可以调整 */
.operation-column {
min-width: 120px; /* 由于按钮组,宽度需要调整 */
white-space: nowrap;
}
/* 标题和面板间距优化 */
h2, h3, h4, h5 {
margin-bottom: 1rem;
font-weight: 600;
}
/* 添加鼠标悬停手形光标效果 */
.copy-btn,
.toggle-password,
.toggle-token,
.toggle-username,
.btn,
.status-action,
.get-usage-btn,
.delete-account-btn {
cursor: pointer;
}
/* 为复制和显示/隐藏图标添加悬停效果 */
.copy-btn:hover,
.toggle-password:hover,
.toggle-token:hover,
.toggle-username:hover {
color: #4f46e5;
transform: scale(1.1);
transition: all 0.2s ease;
}
/* 添加额度进度条样式 */
.battery-progress {
display: flex;
width: 100%;
height: 20px;
/* background-color: #e9ecef; */
border-radius: 5px;
overflow: hidden;
position: relative;
/* border: 1px solid #dee2e6; */
}
/* 电池内部格子线 */
.battery-grid {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
pointer-events: none;
}
/* 电池格子 */
.battery-cell {
flex: 1;
height: 2px;
border-right: 1px solid rgba(0,0,0,0.1);
}
.battery-cell:last-child {
border-right: none;
}
/* 已使用的部分 - 浅红色 */
.battery-used {
height: 100%;
background: linear-gradient(90deg, #ff8a8a, #ffb3b3);
transition: width 0.5s ease;
}
/* 额度信息文本 */
.usage-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 0.75rem;
font-weight: 600;
color: #495057;
text-shadow: 0 0 2px rgba(255,255,255,0.7);
white-space: nowrap;
}
/* 使用量显示样式 */
.usage-display {
width: 100%;
}
.usage-text {
font-weight: 500;
font-size: 0.9rem;
}
.progress {
height: 8px !important;
border-radius: 4px;
background-color: transparent !important;
overflow: hidden;
}
/* 自定义颜色 */
.bg-danger-soft {
background-color: #ffcccc !important;
}
.bg-success-soft {
background-color: #d1ffd1 !important;
}
/* 新的额度显示样式 - 参考图片精确复刻 */
.usage-bar-container {
display: flex;
align-items: center;
width: 100%;
padding: 10px 0;
}
.usage-bar {
flex-grow: 1;
height: 15px;
background-color: #e8f7e8; /* 浅绿背景 */
border-radius: 10px;
margin-right: 15px;
position: relative;
overflow: hidden;
}
.usage-bar-used {
position: absolute;
left: 0;
top: 0;
height: 100%;
background-color: #ffcccc; /* 浅红色 */
border-radius: 10px 0 0 10px;
}
.usage-percent {
font-size: 14px;
color: #666;
font-weight: normal;
white-space: nowrap;
min-width: 55px;
text-align: right;
}
/* 使用情况列样式 */
.usage-info {
white-space: nowrap;
}
.usage-numbers {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 5px;
}
.used-count {
font-weight: 600;
color: #0d6efd;
}
.separator {
color: #6c757d;
}
.total-count {
font-weight: 600;
color: #212529;
}
.remaining-count {
color: #28a745;
font-size: 0.85em;
margin-left: 5px;
}
/* 电池式进度指示器 - 修改颜色逻辑 */
.battery-progress {
display: flex;
align-items: center;
gap: 8px;
}
.battery-bars {
display: flex;
gap: 2px;
align-items: center;
position: relative;
padding: 0 2px;
}
.battery-bars::before {
content: '';
position: absolute;
left: -3px;
height: 8px;
width: 3px;
background-color: #adb5bd;
border-radius: 2px 0 0 2px;
}
.battery-bar {
width: 6px;
height: 14px;
background-color: rgba(40, 167, 69, 0.2); /* 默认未使用为浅绿色 */
display: inline-block;
border-radius: 1px;
transition: background-color 0.3s ease;
}
/* 根据进度点亮电池条 - 反转逻辑,显示已用部分 */
.battery-progress[data-percent="0"] .battery-bar:nth-child(n) {
background-color: rgba(40, 167, 69, 0.2); /* 全未用 - 全部浅绿色 */
}
/* 已使用部分为浅红色 */
.battery-progress[data-percent="10"] .battery-bar:nth-child(1),
.battery-progress[data-percent="20"] .battery-bar:nth-child(-n+2),
.battery-progress[data-percent="30"] .battery-bar:nth-child(-n+3),
.battery-progress[data-percent="40"] .battery-bar:nth-child(-n+4),
.battery-progress[data-percent="50"] .battery-bar:nth-child(-n+5),
.battery-progress[data-percent="60"] .battery-bar:nth-child(-n+6),
.battery-progress[data-percent="70"] .battery-bar:nth-child(-n+7),
.battery-progress[data-percent="80"] .battery-bar:nth-child(-n+8),
.battery-progress[data-percent="90"] .battery-bar:nth-child(-n+9),
.battery-progress[data-percent="100"] .battery-bar:nth-child(-n+10) {
background-color: rgba(220, 53, 69, 0.2); /* 浅红色表示已使用 */
}
/* 删除旧的根据进度值设置颜色的规则,因为现在使用统一的红绿对比 */
.battery-percent {
font-size: 0.75rem;
color: #6c757d;
min-width: 40px;
}
#registration-status {
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
}
</style>
<!-- 添加现代字体 -->
<link href="static/css/css2" rel="stylesheet">
<!-- 添加动画库 -->
<link href="static/css/animate.min.css" rel="stylesheet">
</head>
<body>
<!-- 烟花动画画布 -->
<canvas id="fireworks-canvas"></canvas>
<!-- 加载中遮罩 -->
<div class="loading-overlay" id="loading-overlay">
<div class="spinner-container">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">加载中...</span>
</div>
<p class="mt-2 mb-0">加载中,请稍候...</p>
</div>
</div>
<!-- 通知容器 -->
<div id="alert-container"></div>
<div class="container py-4">
<div class="row mb-4">
<!-- 将左侧任务控制卡片调整为全宽 -->
<div class="col-12">
<div class="card h-100">
<div class="card-header bg-white">
<h5 class="mb-0">任务控制</h5>
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3 task-status-info">
<div>
<span class="me-2">当前状态:</span>
<span class="badge status-not-started" id="registration-status">未启动</span>
</div>
<div class="task-control-buttons">
<button id="start-registration" class="btn btn-primary btn-sm me-2">
<i class="fas fa-play me-1"></i> 启动任务
</button>
<button id="stop-registration" class="btn btn-danger btn-sm" disabled>
<i class="fas fa-stop me-1"></i> 停止任务
</button>
</div>
</div>
<div class="mb-3">
<p class="text-muted" id="status-message">任务未启动</p>
</div>
<div id="task-details">
<div class="row">
<div class="col-md-6 mb-3">
<div class="card bg-light">
<div class="card-body py-2">
<div class="d-flex justify-content-between">
<span>上次运行:</span>
<span id="last-run">-</span>
</div>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card bg-light">
<div class="card-body py-2">
<div class="d-flex justify-content-between">
<span>下次运行:</span>
<span id="next-run">-</span>
</div>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card bg-light">
<div class="card-body py-2">
<div class="d-flex justify-content-between">
<span>成功次数:</span>
<span id="successful-runs">-</span>
</div>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card bg-light">
<div class="card-body py-2">
<div class="d-flex justify-content-between">
<span>失败次数:</span>
<span id="failed-runs">-</span>
</div>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card bg-light">
<div class="card-body py-2">
<div class="d-flex justify-content-between">
<span>总运行次数:</span>
<span id="total-runs">-</span>
</div>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card bg-light">
<div class="card-body py-2">
<div class="d-flex justify-content-between">
<span>成功率:</span>
<span id="success-rate">-</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header bg-white">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0">账号列表</h5>
<div class="input-group" style="width: 300px;">
<input type="text" class="form-control" id="search-input" placeholder="搜索账号...">
<button class="btn btn-outline-secondary" type="button" id="search-btn">
<i class="fas fa-search"></i>
</button>
</div>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th scope="col" class="d-none d-md-table-cell">#</th>
<th scope="col">邮箱</th>
<th scope="col" class="d-none d-lg-table-cell">密码</th>
<th scope="col">Token</th>
<th scope="col" class="d-none d-lg-table-cell">额度</th>
<th scope="col" class="d-none d-lg-table-cell">注册时间</th>
<th scope="col">用量查询</th>
<th scope="col">操作</th>
</tr>
</thead>
<tbody id="accounts-tbody">
<!-- 账号数据将通过JavaScript动态填充 -->
</tbody>
</table>
</div>
<div id="pagination" class="d-flex justify-content-center mt-4">
<!-- 分页控件将通过JavaScript动态填充 -->
</div>
</div>
</div>
</div>
<!-- Bootstrap JS -->
<script src="static/js/bootstrap.bundle.min.js"></script>
<!-- jQuery -->
<script src="static/js/jquery-3.6.0.min.js"></script>
<!-- 业务逻辑 -->
<script>
// 全局变量
const REFRESH_INTERVAL = 10000; // 10秒
let currentPage = 1;
const itemsPerPage = 10;
// 页面加载完成后执行
$(document).ready(function() {
// 初始化应用
initializeApplication();
});
// 应用初始化函数 - 提高代码组织性
function initializeApplication() {
// 初始加载数据
loadAllData();
// 设置定时刷新
setupTaskRefresh();
// 绑定所有事件处理函数
bindEventHandlers();
}
// 事件处理绑定函数 - 将所有事件绑定集中在一起
function bindEventHandlers() {
// 按钮事件监听
$("#refresh-btn").click(function() {
showLoading();
loadAllData();
});
$("#start-registration").click(function() {
startTaskManually();
});
$("#stop-registration").click(function() {
stopTaskManually();
});
$("#search-btn").click(function() {
filterAccounts();
});
$("#search-input").keypress(function(e) {
if (e.which === 13) {
filterAccounts();
}
});
// 可以添加更多事件绑定...
}
// 全局变量
let accounts = [];
let filteredAccounts = [];
let refreshTimer;
// 显示加载遮罩
function showLoading() {
const loadingOverlay = document.getElementById('loading-overlay');
loadingOverlay.classList.add('show');
}
// 隐藏加载遮罩
function hideLoading() {
const loadingOverlay = document.getElementById('loading-overlay');
loadingOverlay.classList.remove('show');
}
// 加载所有数据
function loadAllData() {
showLoading();
// 获取账号数据
fetch('/accounts')
.then(res => res.json())
.then(data => {
console.log('获取到账号数据:', data); // 添加调试日志
// 检查并处理返回的数据结构
if (Array.isArray(data)) {
accounts = data; // 如果直接是数组
} else if (data.accounts && Array.isArray(data.accounts)) {
accounts = data.accounts; // 如果是包装在accounts属性中
} else if (data.data && Array.isArray(data.data)) {
accounts = data.data; // 如果是包装在data属性中
} else {
console.error('账号数据格式不正确:', data);
accounts = [];
}
console.log('处理后的账号数据:', accounts);
filteredAccounts = [...accounts];
// 渲染账号表格
renderAccountsTable();
// 然后获取任务状态
return fetch('/registration/status');
})
.then(res => res.json())
.then(data => {
console.log('获取到任务状态:', data);
updateTaskStatusUI(data);
hideLoading();
})
.catch(error => {
console.error('加载数据失败:', error);
hideLoading();
showAlert('加载数据失败,请刷新页面重试', 'danger');
});
}
// 启动定时刷新
function setupTaskRefresh() {
// 清除可能存在的旧定时器
if (window.taskRefreshInterval) {
clearInterval(window.taskRefreshInterval);
}
// 创建新的10秒定时器
window.taskRefreshInterval = setInterval(function() {
// 只刷新任务状态,而不是整个页面
refreshTaskStatus();
}, 10000); // 10秒
}
// 只刷新任务状态部分的函数
function refreshTaskStatus() {
fetch('/registration/status')
.then(res => res.json())
.then(data => {
// 只更新任务状态区域,不重新加载账号列表
updateTaskStatusUI(data);
})
.catch(error => {
console.error('刷新任务状态时发生错误:', error);
});
}
// 更新任务状态UI函数 - 适配实际API返回的数据结构
function updateTaskStatusUI(data) {
console.log('收到任务状态数据:', data); // 保留调试日志
// 数据格式兼容性处理
const taskStatus = data || {};
// 从正确的嵌套位置获取运行状态和任务状态
const isRunning = taskStatus.task_status === 'running' || taskStatus.task_status === 'monitoring';
const taskStatusValue = taskStatus.task_status;
// 更新状态指示器,添加监控模式的样式
if ($('#registration-status').length) {
$('#registration-status').removeClass().addClass('badge');
if (taskStatusValue === 'running') {
$('#registration-status').addClass('bg-success');
} else if (taskStatusValue === 'monitoring') {
$('#registration-status').addClass('bg-info');
} else {
$('#registration-status').addClass('bg-secondary');
}
}
// 更新状态文本,添加监控模式文本
if ($('#registration-status').length) {
let statusText = '已停止';
if (taskStatusValue === 'running') {
statusText = '正在运行';
} else if (taskStatusValue === 'monitoring') {
statusText = '监控模式';
}
$('#registration-status').text(statusText);
}
// 更新其他状态信息
if ($('#last-run').length && taskStatus.registration_details?.last_run) {
$('#last-run').text(new Date(taskStatus.registration_details.last_run).toLocaleString());
}
if ($('#last-status').length && taskStatus.registration_details?.last_status) {
$('#last-status').text(taskStatus.registration_details.last_status);
}
if ($('#next-run').length && taskStatus.registration_details?.next_run) {
// next_run是Unix时间戳需要转换
$('#next-run').text(new Date(taskStatus.registration_details.next_run * 1000).toLocaleString());
}
// 更新按钮状态 - 监控模式下也禁用启动按钮
if ($('#start-registration').length && $('#stop-registration').length) {
if (isRunning) {
$('#start-registration').addClass('disabled').prop('disabled', true);
$('#stop-registration').removeClass('disabled').prop('disabled', false);
} else {
$('#start-registration').removeClass('disabled').prop('disabled', false);
$('#stop-registration').addClass('disabled').prop('disabled', true);
}
}
// 更新统计信息
const stats = taskStatus.registration_details?.statistics || {};
if ($('#total-runs').length) {
$('#total-runs').text(stats.total_runs || 0);
}
if ($('#successful-runs').length) {
$('#successful-runs').text(stats.successful_runs || 0);
}
if ($('#failed-runs').length) {
$('#failed-runs').text(stats.failed_runs || 0);
}
if ($('#success-rate').length) {
$('#success-rate').text(stats.success_rate || '0%');
}
// 更新剩余槽位信息
if ($('#remaining-slots').length) {
$('#remaining-slots').text(taskStatus.remaining_slots || 0);
}
if ($('#current-count').length) {
$('#current-count').text(taskStatus.active_count || taskStatus.current_count || 0);
}
if ($('#max-accounts').length) {
$('#max-accounts').text(taskStatus.max_accounts || 0);
}
// 添加状态消息显示
if ($('#status-message').length) {
if (taskStatus.status_message) {
$('#status-message').text(taskStatus.status_message);
} else {
// 根据任务状态设置默认消息
let defaultMessage = '任务未启动';
if (taskStatusValue === 'running') {
defaultMessage = '正在注册新账号...';
} else if (taskStatusValue === 'monitoring') {
defaultMessage = '已达到最大账号数量,正在监控账号数量变化...';
} else if (taskStatus.registration_details?.last_status === 'completed') {
defaultMessage = '任务已完成';
} else if (taskStatus.registration_details?.last_status === 'error') {
defaultMessage = '任务执行出错,请检查日志';
}
$('#status-message').text(defaultMessage);
}
}
}
// 渲染账号表格
function renderAccountsTable() {
const accountsBody = $('#accounts-tbody');
accountsBody.empty();
console.log(`准备渲染账号表格,共${filteredAccounts.length}条数据`);
if (filteredAccounts.length === 0) {
// 添加空状态提示
accountsBody.html(`
<tr>
<td colspan="7" class="text-center py-4">
<div class="py-5">
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
<p class="text-muted">暂无账号数据</p>
</div>
</td>
</tr>
`);
return;
}
// 计算当前页的数据
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = Math.min(startIndex + itemsPerPage, filteredAccounts.length);
const currentPageData = filteredAccounts.slice(startIndex, endIndex);
console.log(`当前页数据: ${currentPageData.length}条 (第${currentPage}页)`);
// 渲染每行数据
currentPageData.forEach((account, index) => {
console.log(`渲染账号: ${account.email}`);
// 完整的行模板,包含所有单元格内容
const row = `
<tr id="account-row-${account.id}" data-status="${account.status}"
class="${account.status === 'deleted' ? 'table-danger' : account.status === 'disabled' ? 'table-warning' : ''}">
<td class="d-none d-md-table-cell">${startIndex + index + 1}</td>
<td class="email-column">
${account.email}
<span class="badge ${account.status === 'active' ? 'bg-success' : account.status === 'disabled' ? 'bg-warning' : 'bg-danger'} ms-2">
${account.status === 'active' ? '正常' : account.status === 'disabled' ? '停用' : '删除'}
</span>
</td>
<td class="d-none d-lg-table-cell password-cell">
<span class="password-text">${maskPassword(account.password)}</span>
<i class="fas fa-eye toggle-password" data-password="${account.password}" title="显示/隐藏密码"></i>
<i class="fas fa-copy copy-btn ms-1" data-copy="${account.password}" title="复制密码"></i>
</td>
${renderTokenColumn(account.token, account.id)}
${renderUsageProgress(account.usage_limit)}
<td class="d-none d-lg-table-cell">
${account.created_at || '未知'}
</td>
<td>
<button class="btn btn-sm btn-outline-primary get-usage-btn" data-email="${account.email}" title="查询使用量">
<i class="fas fa-chart-pie"></i>
</button>
</td>
<td class="operation-column">
<div class="d-flex flex-wrap gap-1">
${account.status !== 'active' ?
`<button class="btn btn-sm btn-outline-success status-action" data-email="${account.email}" data-id="${account.id}" data-status="active" title="设为正常">
<i class="fas fa-check-circle"></i>
</button>` : ''}
${account.status !== 'disabled' ?
`<button class="btn btn-sm btn-outline-warning status-action" data-email="${account.email}" data-id="${account.id}" data-status="disabled" title="停用账号">
<i class="fas fa-pause-circle"></i>
</button>` : ''}
${account.status !== 'deleted' ?
`<button class="btn btn-sm btn-outline-danger status-action" data-email="${account.email}" data-id="${account.id}" data-status="deleted" title="标记删除">
<i class="fas fa-times-circle"></i>
</button>` : ''}
<button class="btn btn-sm btn-danger delete-account-btn" data-email="${account.email}" data-id="${account.id}" title="永久删除">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</td>
</tr>
`;
accountsBody.append(row);
});
// 绑定事件
bindTableEvents();
renderPagination();
}
// 渲染分页
function renderPagination() {
const totalPages = Math.ceil(filteredAccounts.length / itemsPerPage);
const pagination = $("#pagination");
pagination.empty();
if (totalPages <= 1) {
return;
}
const paginationNav = $('<nav aria-label="Page navigation"></nav>');
const paginationUl = $('<ul class="pagination"></ul>');
// 上一页按钮
paginationUl.append(`
<li class="page-item ${currentPage === 1 ? 'disabled' : ''}">
<a class="page-link" href="#" aria-label="Previous" ${currentPage !== 1 ? 'onclick="changePage(' + (currentPage - 1) + '); return false;"' : ''}>
<span aria-hidden="true">&laquo;</span>
</a>
</li>
`);
// 页码按钮
for (let i = 1; i <= totalPages; i++) {
paginationUl.append(`
<li class="page-item ${currentPage === i ? 'active' : ''}">
<a class="page-link" href="#" onclick="changePage(${i}); return false;">${i}</a>
</li>·
`);
}
// 下一页按钮
paginationUl.append(`
<li class="page-item ${currentPage === totalPages ? 'disabled' : ''}">
<a class="page-link" href="#" aria-label="Next" ${currentPage !== totalPages ? 'onclick="changePage(' + (currentPage + 1) + '); return false;"' : ''}>
<span aria-hidden="true">&raquo;</span>
</a>
</li>
`);
paginationNav.append(paginationUl);
pagination.append(paginationNav);
}
// 更改页码
function changePage(page) {
currentPage = page;
renderAccountsTable();
}
// 过滤账号
function filterAccounts() {
const searchTerm = $("#search-input").val().toLowerCase();
if (!searchTerm) {
filteredAccounts = [...accounts];
} else {
filteredAccounts = accounts.filter(account =>
account.email.toLowerCase().includes(searchTerm) ||
account.user.toLowerCase().includes(searchTerm)
);
}
currentPage = 1;
renderAccountsTable();
}
// 获取账号用量详情并更新数据库
function getAccountUsage(email) {
showLoading();
fetch(`/account/${encodeURIComponent(email)}/usage`)
.then(res => res.json())
.then(data => {
hideLoading();
if (data.success) {
// 创建并显示用量信息模态框
const modal = $(`
<div class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">账号用量信息</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<strong>邮箱:</strong> ${data.email}
</div>
<div class="mb-3">
<strong>剩余额度:</strong> ${data.usage.remaining_balance !== null ? data.usage.remaining_balance : '未知'}
</div>
<div class="mb-3">
<strong>剩余天数:</strong> ${data.usage.remaining_days !== null ? data.usage.remaining_days : '未知'}
</div>
<div class="mb-3">
<strong>状态:</strong>
<span class="badge ${data.usage.status === 'active' ? 'bg-success' : 'bg-danger'}">
${data.usage.status === 'active' ? '活跃' : '不活跃'}
</span>
</div>
<div class="mt-3 text-muted small">
<strong>更新时间:</strong> ${formatDateTime(data.timestamp)}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
`);
$('body').append(modal);
const modalInstance = new bootstrap.Modal(modal[0]);
modalInstance.show();
// 模态框关闭时移除DOM
modal[0].addEventListener('hidden.bs.modal', function() {
modal.remove();
});
}
})
.catch(error => {
console.error('获取账号用量失败:', error);
showAlert('获取账号用量失败', 'danger');
hideLoading();
});
}
// 更新账号用量到数据库
function updateAccountUsageLimit(email, usageLimit) {
fetch(`/account/${encodeURIComponent(email)}/update-usage`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ usage_limit: usageLimit })
})
.then(res => res.json())
.then(data => {
if (data.success) {
console.log(`账号 ${email} 用量数据已更新到数据库`);
} else {
console.error(`更新账号 ${email} 用量数据失败:`, data.message);
}
})
.catch(error => {
console.error(`更新账号 ${email} 用量数据时发生错误:`, error);
});
}
// 修复任务状态更新问题
function startTaskManually() {
showLoading();
fetch('/registration/start', {
method: 'GET'
})
.then(res => res.json())
.then(data => {
hideLoading();
if (data.success) {
showAlert('定时任务已成功启动', 'success');
// 立即更新任务状态 - 添加这段代码
fetch('/registration/status')
.then(res => res.json())
.then(statusData => {
updateTaskStatusUI(statusData);
});
} else {
showAlert(`启动任务失败: ${data.message || '未知错误'}`, 'danger');
}
})
.catch(error => {
console.error('启动任务时发生错误:', error);
hideLoading();
showAlert('启动任务失败,请稍后重试', 'danger');
});
}
// 同样添加到停止任务函数
function stopTaskManually() {
showLoading();
fetch('/registration/stop', {
method: 'GET'
})
.then(res => res.json())
.then(data => {
hideLoading();
if (data.success) {
showAlert('定时任务已成功停止', 'success');
// 立即更新任务状态 - 添加这段代码
fetch('/registration/status')
.then(res => res.json())
.then(statusData => {
updateTaskStatusUI(statusData);
});
} else {
showAlert(`停止任务失败: ${data.message || '未知错误'}`, 'danger');
}
})
.catch(error => {
console.error('停止任务时发生错误:', error);
hideLoading();
showAlert('停止任务失败,请稍后重试', 'danger');
});
}
// 复制到剪贴板
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
showAlert('复制成功Token已复制到剪贴板', 'success');
}).catch(err => {
console.error('复制失败:', err);
showAlert('复制失败', 'danger');
});
}
// 显示通知
function showAlert(message, type, isSpecial = false) {
const alertId = 'alert-' + Date.now();
const alertClass = isSpecial ?
`alert-${type} special-alert animate__animated animate__bounceIn` :
`alert-${type} animate__animated animate__fadeInRight`;
const alert = $(`
<div id="${alertId}" class="alert ${alertClass} alert-dismissible fade show" role="alert">
${isSpecial ? '<i class="fas fa-star me-2"></i>' : ''}${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
`);
$("#alert-container").append(alert);
// 5秒后自动消失
setTimeout(() => {
$(`#${alertId}`).alert('close');
}, 5000);
}
// 日期时间格式化
function formatDateTime(dateTimeString) {
if (!dateTimeString) return '-';
try {
const date = new Date(dateTimeString);
if (isNaN(date.getTime())) return dateTimeString;
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
const seconds = date.getSeconds().toString().padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
} catch (error) {
return dateTimeString;
}
}
// 修改掩码函数,增加对用户名的特殊处理
function maskText(text, showChars = 6, isUsername = false) {
if (!text) return '';
// 用户名特殊处理 - 只显示前1/3
if (isUsername) {
const showLength = Math.ceil(text.length / 3);
if (text.length <= showLength) return text;
return `${text.substring(0, showLength)}...`;
}
// 其他文本使用标准处理
if (text.length <= showChars) return text;
return `${text.substring(0, showChars)}...`;
}
// 隐藏密码
function maskPassword(password) {
if (!password) return '';
return '•'.repeat(password.length);
}
// 页面加载动画
document.addEventListener('DOMContentLoaded', function() {
// 修改动画类添加代码,删除对已删除元素的引用
const elements = [
{selector: '.card', animation: 'animate__fadeIn', delay: 0.2}
];
elements.forEach(item => {
const elems = document.querySelectorAll(item.selector);
elems.forEach((el, index) => {
el.classList.add('animate__animated', item.animation);
if (item.delay) {
const delay = item.stagger ? item.delay * (index + 1) : item.delay;
el.style.animationDelay = `${delay}s`;
}
});
});
});
// 烟花动画实现
const Fireworks = {
canvas: null,
ctx: null,
particles: [],
init: function() {
this.canvas = document.getElementById('fireworks-canvas');
this.ctx = this.canvas.getContext('2d');
this.resizeCanvas();
window.addEventListener('resize', () => this.resizeCanvas());
},
resizeCanvas: function() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
},
start: function() {
this.canvas.style.display = 'block';
this.particles = [];
// 创建5次烟花间隔300ms
for (let i = 0; i < 5; i++) {
setTimeout(() => {
const x = Math.random() * this.canvas.width;
const y = Math.random() * this.canvas.height * 0.6;
this.createParticles(x, y);
}, i * 300);
}
this.animate();
// 5秒后停止动画
setTimeout(() => {
this.canvas.style.display = 'none';
}, 5000);
},
createParticles: function(x, y) {
const colors = ['#ff595e', '#ffca3a', '#8ac926', '#1982c4', '#6a4c93'];
for (let i = 0; i < 80; i++) {
const particle = {
x: x,
y: y,
size: Math.random() * 4 + 1,
color: colors[Math.floor(Math.random() * colors.length)],
velocity: {
x: (Math.random() - 0.5) * 8,
y: (Math.random() - 0.5) * 8
},
alpha: 1,
decay: Math.random() * 0.02 + 0.01
};
this.particles.push(particle);
}
},
animate: function() {
if (this.particles.length === 0) return;
requestAnimationFrame(() => this.animate());
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
for (let i = 0; i < this.particles.length; i++) {
const p = this.particles[i];
// 添加重力
p.velocity.y += 0.05;
// 更新位置
p.x += p.velocity.x;
p.y += p.velocity.y;
// 减少透明度
p.alpha -= p.decay;
// 绘制粒子
this.ctx.save();
this.ctx.globalAlpha = p.alpha;
this.ctx.fillStyle = p.color;
this.ctx.beginPath();
this.ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
this.ctx.fill();
this.ctx.restore();
// 移除消失的粒子
if (p.alpha <= 0) {
this.particles.splice(i, 1);
i--;
}
}
}
};
// 初始化烟花
Fireworks.init();
// 绑定表格交互事件
function bindTableEvents() {
// 显示/隐藏用户名
$('.toggle-username').off('click').on('click', function() {
const username = $(this).data('username');
const usernameText = $(this).prev('.username-text');
if (usernameText.text() === username) {
usernameText.text(maskText(username, 6, true));
} else {
usernameText.text(username);
}
});
// 显示/隐藏密码
$('.toggle-password').off('click').on('click', function() {
const password = $(this).data('password');
const passwordText = $(this).prev('.password-text');
if (passwordText.text() === password) {
passwordText.text(maskPassword(password));
} else {
passwordText.text(password);
}
});
// 显示/隐藏Token
$('.toggle-token').off('click').on('click', function() {
const token = $(this).data('token');
const tokenText = $(this).prev('.token-text');
if (tokenText.text() === token) {
tokenText.text(maskText(token));
} else {
tokenText.text(token);
}
});
// 复制按钮
$('.copy-btn').off('click').on('click', function() {
const textToCopy = $('#tokenFullText').val();
copyToClipboard(textToCopy);
});
// 获取用量按钮
$('.get-usage-btn').off('click').on('click', function() {
const email = $(this).data('email');
getAccountUsage(email);
});
// 删除按钮
$('.delete-account-btn').off('click').on('click', function() {
const email = $(this).data('email');
const id = $(this).data('id');
$('#deleteEmailConfirm').text(email);
$('#deleteIdConfirm').text(id || '无');
// 重置并重新绑定确认删除按钮事件
$('#confirmDeleteBtn').off('click').on('click', function() {
deleteAccount(email, id, true);
});
const deleteModal = new bootstrap.Modal(document.getElementById('deleteConfirmModal'));
deleteModal.show();
});
// 状态操作按钮
$('.status-action').off('click').on('click', function(e) {
e.preventDefault();
const email = $(this).data('email');
const id = $(this).data('id');
const status = $(this).data('status');
updateAccountStatus(email, id, status);
});
// 查看Token按钮
$('.view-token-btn').off('click').on('click', function() {
const token = $(this).data('token');
const accountId = $(this).data('account-id');
$('#tokenFullText').val(token);
$('#useTokenBtn').data('account-id', accountId);
new bootstrap.Modal(document.getElementById('tokenViewModal')).show();
});
// 使用Token按钮
$('#useTokenBtn').off('click').on('click', function() {
const accountId = $(this).data('account-id');
if (!accountId) {
showAlert('账号ID无效', 'danger');
return;
}
showLoading();
fetch(`/account/use-token/${accountId}`, {
method: 'POST'
})
.then(res => res.json())
.then(data => {
hideLoading();
if (data.success) {
showAlert(data.message, 'success');
$('#tokenViewModal').modal('hide');
} else {
showAlert(`使用Token失败: ${data.message || '未知错误'}`, 'danger');
}
})
.catch(error => {
console.error('使用Token时发生错误:', error);
hideLoading();
showAlert('使用Token失败请稍后重试', 'danger');
});
});
}
// 更新删除确认按钮事件处理
$('#confirmDeleteBtn').click(function() {
const email = $(this).data('email');
const id = $(this).data('id');
deleteAccount(email, id, true);
});
// 修改updateAccountStatus函数确保正确发送请求体
function updateAccountStatus(email, id, status) {
showLoading();
// 优先使用ID API如果ID存在的话
const apiUrl = id ?
`/account/id/${id}/status` :
`/account/${encodeURIComponent(email)}/status`;
fetch(apiUrl, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ status: status }) // 确保这里的字段名是status
})
.then(res => res.json())
.then(data => {
hideLoading();
if (data.success) {
let statusText = '';
if (status === 'active') statusText = '正常';
else if (status === 'disabled') statusText = '停用';
else if (status === 'deleted') statusText = '删除';
showAlert(`账号${id ? '(ID:'+id+')' : ''} ${email} 已成功设置为${statusText}状态`, 'success');
loadAllData();
} else {
showAlert(`更新账号状态失败: ${data.message || '未知错误'}`, 'danger');
}
})
.catch(error => {
console.error('更新账号状态时发生错误:', error);
hideLoading();
showAlert('更新账号状态失败,请稍后重试', 'danger');
});
}
// 修改deleteAccount函数支持通过ID删除
function deleteAccount(email, id, hardDelete = true) {
showLoading();
// 优先使用ID API如果ID存在的话
const apiUrl = id ?
`/account/id/${id}${hardDelete ? '?hard_delete=true' : ''}` :
`/account/${encodeURIComponent(email)}${hardDelete ? '?hard_delete=true' : ''}`;
fetch(apiUrl, {
method: 'DELETE'
})
.then(res => res.json())
.then(data => {
hideLoading();
if (data.success) {
showAlert(`账号${id ? '(ID:'+id+')' : ''} ${email} 已成功删除`, 'success');
// 关闭模态框
$('#deleteConfirmModal').modal('hide');
// 重新加载账号列表
loadAllData();
} else {
showAlert(`删除账号失败: ${data.message || '未知错误'}`, 'danger');
}
})
.catch(error => {
console.error('删除账号时发生错误:', error);
hideLoading();
showAlert('删除账号失败,请稍后重试', 'danger');
});
}
// 添加强制刷新函数
function forceRefreshData() {
window.forceRefresh = true;
loadAllData();
}
// 完全重构额度显示函数,精确匹配参考代码
function renderUsageProgress(usageLimit) {
// 计算使用进度
const premiumUsed = 150 - usageLimit;
const premiumTotal = 150;
const premiumRemaining = premiumTotal - premiumUsed;
const premiumPercent = Math.round((premiumUsed / premiumTotal) * 100);
return `
<td class="usage-info">
<div class="usage-numbers">
<span class="used-count">${premiumUsed}</span>
<span class="separator">/</span>
<span class="total-count">${premiumTotal}</span>
<span class="remaining-count">(剩余: ${premiumRemaining})</span>
</div>
<div class="battery-progress" data-percent="${Math.round(premiumPercent / 10) * 10}">
<div class="battery-bars">
<span class="battery-bar"></span>
<span class="battery-bar"></span>
<span class="battery-bar"></span>
<span class="battery-bar"></span>
<span class="battery-bar"></span>
<span class="battery-bar"></span>
<span class="battery-bar"></span>
<span class="battery-bar"></span>
<span class="battery-bar"></span>
<span class="battery-bar"></span>
</div>
<span class="battery-percent">${premiumPercent}%</span>
</div>
</td>
`;
}
// 修改Token列的渲染方式
function renderTokenColumn(token, accountId) {
return `
<td class="token-column">
<button class="btn btn-sm btn-outline-info view-token-btn" data-token="${token}" data-account-id="${accountId}">
<i class="fas fa-eye"></i> 查看Token
</button>
</td>
`;
}
</script>
<!-- 添加删除确认对话框 -->
<div class="modal fade" id="deleteConfirmModal" tabindex="-1" aria-labelledby="deleteConfirmModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteConfirmModalLabel">确认删除</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>确定要删除此账号吗?此操作不可恢复。</p>
<p class="text-danger fw-bold">邮箱: <span id="deleteEmailConfirm"></span></p>
<p class="text-muted">ID: <span id="deleteIdConfirm"></span></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">删除</button>
</div>
</div>
</div>
</div>
<!-- 在页面底部添加自动刷新指示器 -->
<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 5">
<div class="d-flex align-items-center small text-muted">
<span class="me-2">自动刷新10秒/次)</span>
<div class="spinner-border spinner-border-sm text-secondary" role="status" style="width: 0.8rem; height: 0.8rem;">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
<!-- 添加Token查看模态框 -->
<div class="modal fade" id="tokenViewModal" tabindex="-1" aria-labelledby="tokenViewModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="tokenViewModalLabel">查看Token</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="tokenFullText" class="form-label">完整Token:</label>
<div class="input-group">
<textarea id="tokenFullText" class="form-control" rows="5" readonly></textarea>
<button class="btn btn-outline-primary copy-btn" type="button" id="copyTokenBtn" title="复制Token">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
<div class="text-muted small">此Token用于Cursor客户端身份验证请妥善保管</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" id="useTokenBtn" data-account-id="">
<i class="fas fa-plug"></i> 使用此Token
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
</body>
</html>