This commit is contained in:
cnlimiter
2026-03-14 16:51:57 +08:00
parent dc1334fbab
commit 9d3099fcd8
35 changed files with 9490 additions and 0 deletions

605
static/css/style.css Normal file
View File

@@ -0,0 +1,605 @@
/*
* OpenAI 注册系统 - 主样式表
* 轻量级、现代、响应式设计
*/
/* CSS 变量 */
:root {
--primary-color: #10a37f;
--primary-hover: #0d8a6a;
--secondary-color: #6b7280;
--danger-color: #ef4444;
--warning-color: #f59e0b;
--success-color: #22c55e;
--background: #f9fafb;
--surface: #ffffff;
--border: #e5e7eb;
--text-primary: #111827;
--text-secondary: #6b7280;
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
}
/* 重置样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-family);
background-color: var(--background);
color: var(--text-primary);
line-height: 1.6;
}
/* 容器 */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
/* 导航栏 */
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 0;
border-bottom: 1px solid var(--border);
background: var(--surface);
margin-bottom: 30px;
}
.nav-brand h1 {
font-size: 1.5rem;
color: var(--primary-color);
font-weight: 600;
}
.nav-links {
display: flex;
gap: 20px;
}
.nav-link {
text-decoration: none;
color: var(--text-secondary);
padding: 8px 16px;
border-radius: 6px;
transition: all 0.2s;
}
.nav-link:hover {
color: var(--primary-color);
background-color: rgba(16, 163, 127, 0.1);
}
.nav-link.active {
color: var(--primary-color);
background-color: rgba(16, 163, 127, 0.1);
font-weight: 500;
}
/* 主内容 */
.main-content {
padding-bottom: 50px;
}
.page-header {
margin-bottom: 30px;
}
.page-header h2 {
font-size: 1.75rem;
margin-bottom: 8px;
}
.subtitle {
color: var(--text-secondary);
}
/* 卡片 */
.card {
background: var(--surface);
border-radius: 8px;
border: 1px solid var(--border);
margin-bottom: 20px;
}
.card-header {
padding: 16px 20px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.card-header h3 {
font-size: 1rem;
font-weight: 600;
}
.card-body {
padding: 20px;
}
/* 表单 */
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: 500;
font-size: 0.875rem;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: 6px;
font-size: 0.875rem;
transition: border-color 0.2s;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(16, 163, 127, 0.1);
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.form-actions {
display: flex;
gap: 12px;
margin-top: 20px;
}
/* 按钮 */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10px 20px;
font-size: 0.875rem;
font-weight: 500;
border-radius: 6px;
border: none;
cursor: pointer;
transition: all 0.2s;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}
.btn-primary:hover:not(:disabled) {
background-color: var(--primary-hover);
}
.btn-secondary {
background-color: var(--border);
color: var(--text-primary);
}
.btn-secondary:hover:not(:disabled) {
background-color: #d1d5db;
}
.btn-danger {
background-color: var(--danger-color);
color: white;
}
.btn-danger:hover:not(:disabled) {
background-color: #dc2626;
}
.btn-warning {
background-color: var(--warning-color);
color: white;
}
.btn-warning:hover:not(:disabled) {
background-color: #d97706;
}
.btn-sm {
padding: 6px 12px;
font-size: 0.75rem;
}
/* 控制台日志 */
.console-log {
background-color: #1e1e1e;
color: #d4d4d4;
padding: 16px;
border-radius: 6px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.8rem;
height: 300px;
overflow-y: auto;
line-height: 1.5;
}
.log-line {
margin-bottom: 4px;
white-space: pre-wrap;
word-break: break-all;
}
.log-line.info { color: #4fc3f7; }
.log-line.success { color: #81c784; }
.log-line.error { color: #e57373; }
.log-line.warning { color: #ffb74d; }
/* 状态徽章 */
.status-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
background-color: var(--secondary-color);
color: white;
}
.status-badge.running {
background-color: var(--primary-color);
}
.status-badge.completed {
background-color: var(--success-color);
}
.status-badge.failed {
background-color: var(--danger-color);
}
/* 统计卡片 */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.stat-card {
background: var(--surface);
border-radius: 8px;
border: 1px solid var(--border);
padding: 20px;
text-align: center;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: var(--text-primary);
}
.stat-label {
color: var(--text-secondary);
font-size: 0.875rem;
margin-top: 4px;
}
.stat-card.success .stat-value { color: var(--success-color); }
.stat-card.warning .stat-value { color: var(--warning-color); }
.stat-card.danger .stat-value { color: var(--danger-color); }
/* 数据表格 */
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th,
.data-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid var(--border);
}
.data-table th {
font-weight: 600;
color: var(--text-secondary);
font-size: 0.75rem;
text-transform: uppercase;
}
.data-table tbody tr:hover {
background-color: #f9fafb;
}
/* 工具栏 */
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
}
.toolbar-left,
.toolbar-right {
display: flex;
align-items: center;
gap: 12px;
}
.form-select,
.form-input {
padding: 8px 12px;
border: 1px solid var(--border);
border-radius: 6px;
font-size: 0.875rem;
}
/* 分页 */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 16px;
margin-top: 20px;
}
#page-info {
color: var(--text-secondary);
font-size: 0.875rem;
}
/* 标签页 */
.tabs {
display: flex;
gap: 4px;
margin-bottom: 20px;
border-bottom: 1px solid var(--border);
padding-bottom: 0;
}
.tab-btn {
padding: 12px 20px;
background: none;
border: none;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
color: var(--text-secondary);
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: all 0.2s;
}
.tab-btn:hover {
color: var(--primary-color);
}
.tab-btn.active {
color: var(--primary-color);
border-bottom-color: var(--primary-color);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* 模态框 */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.active {
display: flex;
}
.modal-content {
background: var(--surface);
border-radius: 8px;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
padding: 16px 20px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
font-size: 1.125rem;
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--text-secondary);
}
.modal-close:hover {
color: var(--text-primary);
}
.modal-body {
padding: 20px;
}
/* 下拉菜单 */
.dropdown {
position: relative;
display: inline-block;
}
.dropdown-menu {
display: none;
position: absolute;
right: 0;
top: 100%;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 6px;
min-width: 150px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
z-index: 100;
}
.dropdown-menu.active {
display: block;
}
.dropdown-item {
display: block;
padding: 10px 16px;
color: var(--text-primary);
text-decoration: none;
font-size: 0.875rem;
}
.dropdown-item:hover {
background-color: #f9fafb;
}
/* 任务信息 */
.task-info {
display: grid;
gap: 12px;
}
.info-row {
display: flex;
gap: 12px;
}
.info-row .label {
color: var(--text-secondary);
min-width: 80px;
}
.info-row .value {
font-weight: 500;
}
/* 信息网格 */
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 20px;
}
.info-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.info-item .label {
color: var(--text-secondary);
font-size: 0.875rem;
}
.info-item .value {
font-size: 1.25rem;
font-weight: 600;
}
/* 进度条 */
.progress-bar-container {
background-color: var(--border);
border-radius: 9999px;
height: 12px;
overflow: hidden;
margin-bottom: 16px;
}
.progress-bar {
background-color: var(--primary-color);
height: 100%;
border-radius: 9999px;
transition: width 0.3s ease;
}
/* 批量统计 */
.batch-stats {
display: flex;
justify-content: space-around;
gap: 16px;
text-align: center;
}
.batch-stats span {
color: var(--text-secondary);
font-size: 0.875rem;
}
.batch-stats strong {
display: block;
font-size: 1.5rem;
color: var(--text-primary);
margin-top: 4px;
}
/* 响应式 */
@media (max-width: 768px) {
.navbar {
flex-direction: column;
gap: 16px;
}
.toolbar {
flex-direction: column;
align-items: stretch;
}
.toolbar-left,
.toolbar-right {
flex-direction: column;
align-items: stretch;
}
.form-row {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}

381
static/js/accounts.js Normal file
View File

@@ -0,0 +1,381 @@
/**
* 账号管理页面 JavaScript
*/
// API 基础路径
const API_BASE = '/api';
// 状态
let currentPage = 1;
let pageSize = 20;
let totalAccounts = 0;
let selectedAccounts = new Set();
// DOM 元素
const accountsTable = document.getElementById('accounts-table');
const totalAccountsEl = document.getElementById('total-accounts');
const activeAccountsEl = document.getElementById('active-accounts');
const expiredAccountsEl = document.getElementById('expired-accounts');
const failedAccountsEl = document.getElementById('failed-accounts');
const filterStatus = document.getElementById('filter-status');
const filterService = document.getElementById('filter-service');
const searchInput = document.getElementById('search-input');
const refreshBtn = document.getElementById('refresh-btn');
const batchDeleteBtn = document.getElementById('batch-delete-btn');
const exportBtn = document.getElementById('export-btn');
const exportMenu = document.getElementById('export-menu');
const selectAllCheckbox = document.getElementById('select-all');
const prevPageBtn = document.getElementById('prev-page');
const nextPageBtn = document.getElementById('next-page');
const pageInfo = document.getElementById('page-info');
const detailModal = document.getElementById('detail-modal');
const modalBody = document.getElementById('modal-body');
const closeModalBtn = document.getElementById('close-modal');
// 初始化
document.addEventListener('DOMContentLoaded', () => {
loadStats();
loadAccounts();
initEventListeners();
});
// 事件监听
function initEventListeners() {
// 筛选
filterStatus.addEventListener('change', () => {
currentPage = 1;
loadAccounts();
});
filterService.addEventListener('change', () => {
currentPage = 1;
loadAccounts();
});
// 搜索
let searchTimeout;
searchInput.addEventListener('input', () => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
currentPage = 1;
loadAccounts();
}, 300);
});
// 刷新
refreshBtn.addEventListener('click', () => {
loadStats();
loadAccounts();
});
// 批量删除
batchDeleteBtn.addEventListener('click', handleBatchDelete);
// 全选
selectAllCheckbox.addEventListener('change', (e) => {
const checkboxes = accountsTable.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(cb => {
cb.checked = e.target.checked;
const id = parseInt(cb.dataset.id);
if (e.target.checked) {
selectedAccounts.add(id);
} else {
selectedAccounts.delete(id);
}
});
updateBatchButtons();
});
// 分页
prevPageBtn.addEventListener('click', () => {
if (currentPage > 1) {
currentPage--;
loadAccounts();
}
});
nextPageBtn.addEventListener('click', () => {
const totalPages = Math.ceil(totalAccounts / pageSize);
if (currentPage < totalPages) {
currentPage++;
loadAccounts();
}
});
// 导出
exportBtn.addEventListener('click', (e) => {
e.stopPropagation();
exportMenu.classList.toggle('active');
});
document.querySelectorAll('#export-menu .dropdown-item').forEach(item => {
item.addEventListener('click', (e) => {
e.preventDefault();
const format = e.target.dataset.format;
exportAccounts(format);
exportMenu.classList.remove('active');
});
});
// 关闭模态框
closeModalBtn.addEventListener('click', () => {
detailModal.classList.remove('active');
});
detailModal.addEventListener('click', (e) => {
if (e.target === detailModal) {
detailModal.classList.remove('active');
}
});
// 点击其他地方关闭下拉菜单
document.addEventListener('click', () => {
exportMenu.classList.remove('active');
});
}
// 加载统计信息
async function loadStats() {
try {
const response = await fetch(`${API_BASE}/accounts/stats/summary`);
const data = await response.json();
totalAccountsEl.textContent = data.total || 0;
activeAccountsEl.textContent = data.by_status?.active || 0;
expiredAccountsEl.textContent = data.by_status?.expired || 0;
failedAccountsEl.textContent = data.by_status?.failed || 0;
} catch (error) {
console.error('加载统计信息失败:', error);
}
}
// 加载账号列表
async function loadAccounts() {
const params = new URLSearchParams({
page: currentPage,
page_size: pageSize,
});
if (filterStatus.value) {
params.append('status', filterStatus.value);
}
if (filterService.value) {
params.append('email_service', filterService.value);
}
if (searchInput.value.trim()) {
params.append('search', searchInput.value.trim());
}
try {
const response = await fetch(`${API_BASE}/accounts?${params}`);
const data = await response.json();
totalAccounts = data.total;
renderAccounts(data.accounts);
updatePagination();
} catch (error) {
console.error('加载账号列表失败:', error);
accountsTable.innerHTML = '<tr><td colspan="7" style="text-align: center;">加载失败</td></tr>';
}
}
// 渲染账号列表
function renderAccounts(accounts) {
if (accounts.length === 0) {
accountsTable.innerHTML = '<tr><td colspan="7" style="text-align: center;">暂无数据</td></tr>';
return;
}
accountsTable.innerHTML = accounts.map(account => `
<tr>
<td><input type="checkbox" data-id="${account.id}" ${selectedAccounts.has(account.id) ? 'checked' : ''}></td>
<td>${account.id}</td>
<td>${escapeHtml(account.email)}</td>
<td>${escapeHtml(account.email_service)}</td>
<td><span class="status-badge ${account.status}">${getStatusText(account.status)}</span></td>
<td>${formatDate(account.registered_at)}</td>
<td>
<button class="btn btn-sm btn-secondary" onclick="viewAccount(${account.id})">查看</button>
<button class="btn btn-sm btn-danger" onclick="deleteAccount(${account.id}, '${escapeHtml(account.email)}')">删除</button>
</td>
</tr>
`).join('');
// 绑定复选框事件
accountsTable.querySelectorAll('input[type="checkbox"]').forEach(cb => {
cb.addEventListener('change', (e) => {
const id = parseInt(e.target.dataset.id);
if (e.target.checked) {
selectedAccounts.add(id);
} else {
selectedAccounts.delete(id);
}
updateBatchButtons();
});
});
}
// 更新分页
function updatePagination() {
const totalPages = Math.ceil(totalAccounts / pageSize);
prevPageBtn.disabled = currentPage <= 1;
nextPageBtn.disabled = currentPage >= totalPages;
pageInfo.textContent = `${currentPage} 页 / 共 ${totalPages}`;
}
// 更新批量操作按钮
function updateBatchButtons() {
batchDeleteBtn.disabled = selectedAccounts.size === 0;
}
// 查看账号详情
async function viewAccount(id) {
try {
const response = await fetch(`${API_BASE}/accounts/${id}`);
const account = await response.json();
const tokensResponse = await fetch(`${API_BASE}/accounts/${id}/tokens`);
const tokens = await tokensResponse.json();
modalBody.innerHTML = `
<div class="info-grid">
<div class="info-item">
<span class="label">邮箱</span>
<span class="value">${escapeHtml(account.email)}</span>
</div>
<div class="info-item">
<span class="label">邮箱服务</span>
<span class="value">${escapeHtml(account.email_service)}</span>
</div>
<div class="info-item">
<span class="label">状态</span>
<span class="value">${getStatusText(account.status)}</span>
</div>
<div class="info-item">
<span class="label">注册时间</span>
<span class="value">${formatDate(account.registered_at)}</span>
</div>
<div class="info-item">
<span class="label">Account ID</span>
<span class="value">${escapeHtml(account.account_id || '-')}</span>
</div>
<div class="info-item">
<span class="label">Workspace ID</span>
<span class="value">${escapeHtml(account.workspace_id || '-')}</span>
</div>
<div class="info-item">
<span class="label">Access Token</span>
<span class="value" style="font-size: 0.75rem; word-break: break-all;">${escapeHtml(tokens.access_token || '-')}</span>
</div>
<div class="info-item">
<span class="label">Refresh Token</span>
<span class="value" style="font-size: 0.75rem; word-break: break-all;">${escapeHtml(tokens.refresh_token || '-')}</span>
</div>
</div>
`;
detailModal.classList.add('active');
} catch (error) {
alert('加载账号详情失败: ' + error.message);
}
}
// 删除账号
async function deleteAccount(id, email) {
if (!confirm(`确定要删除账号 ${email} 吗?`)) {
return;
}
try {
const response = await fetch(`${API_BASE}/accounts/${id}`, {
method: 'DELETE',
});
if (response.ok) {
loadStats();
loadAccounts();
} else {
const data = await response.json();
alert('删除失败: ' + (data.detail || '未知错误'));
}
} catch (error) {
alert('删除失败: ' + error.message);
}
}
// 批量删除
async function handleBatchDelete() {
if (selectedAccounts.size === 0) return;
if (!confirm(`确定要删除选中的 ${selectedAccounts.size} 个账号吗?`)) {
return;
}
try {
const response = await fetch(`${API_BASE}/accounts/batch-delete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
ids: Array.from(selectedAccounts),
}),
});
const data = await response.json();
if (response.ok) {
alert(`成功删除 ${data.deleted_count} 个账号`);
selectedAccounts.clear();
loadStats();
loadAccounts();
} else {
alert('删除失败: ' + (data.detail || '未知错误'));
}
} catch (error) {
alert('删除失败: ' + error.message);
}
}
// 导出账号
function exportAccounts(format) {
const params = new URLSearchParams();
if (filterStatus.value) {
params.append('status', filterStatus.value);
}
if (filterService.value) {
params.append('email_service', filterService.value);
}
window.location.href = `${API_BASE}/accounts/export/${format}?${params}`;
}
// 工具函数
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function getStatusText(status) {
const statusMap = {
'active': '活跃',
'expired': '过期',
'banned': '封禁',
'failed': '失败',
};
return statusMap[status] || status;
}
function formatDate(dateStr) {
if (!dateStr) return '-';
const date = new Date(dateStr);
return date.toLocaleString('zh-CN');
}

360
static/js/app.js Normal file
View File

@@ -0,0 +1,360 @@
/**
* 注册页面 JavaScript
*/
// API 基础路径
const API_BASE = '/api';
// 状态
let currentTask = null;
let currentBatch = null;
let logPollingInterval = null;
let batchPollingInterval = null;
let isBatchMode = false;
// DOM 元素
const registrationForm = document.getElementById('registration-form');
const emailServiceSelect = document.getElementById('email-service');
const proxyInput = document.getElementById('proxy');
const regModeSelect = document.getElementById('reg-mode');
const batchCountGroup = document.getElementById('batch-count-group');
const batchCountInput = document.getElementById('batch-count');
const batchOptions = document.getElementById('batch-options');
const intervalMinInput = document.getElementById('interval-min');
const intervalMaxInput = document.getElementById('interval-max');
const startBtn = document.getElementById('start-btn');
const cancelBtn = document.getElementById('cancel-btn');
const taskStatusCard = document.getElementById('task-status-card');
const batchStatusCard = document.getElementById('batch-status-card');
const consoleLog = document.getElementById('console-log');
const clearLogBtn = document.getElementById('clear-log-btn');
// 初始化
document.addEventListener('DOMContentLoaded', () => {
initEventListeners();
});
// 事件监听
function initEventListeners() {
// 注册表单提交
registrationForm.addEventListener('submit', handleStartRegistration);
// 注册模式切换
regModeSelect.addEventListener('change', handleModeChange);
// 取消按钮
cancelBtn.addEventListener('click', handleCancelTask);
// 清空日志
clearLogBtn.addEventListener('click', () => {
consoleLog.innerHTML = '<div class="log-line info">[*] 日志已清空</div>';
});
}
// 模式切换
function handleModeChange(e) {
const mode = e.target.value;
isBatchMode = mode === 'batch';
batchCountGroup.style.display = isBatchMode ? 'block' : 'none';
batchOptions.style.display = isBatchMode ? 'block' : 'none';
}
// 开始注册
async function handleStartRegistration(e) {
e.preventDefault();
const emailService = emailServiceSelect.value;
const proxy = proxyInput.value.trim() || null;
// 禁用开始按钮
startBtn.disabled = true;
cancelBtn.disabled = false;
// 清空日志
consoleLog.innerHTML = '';
if (isBatchMode) {
await handleBatchRegistration(emailService, proxy);
} else {
await handleSingleRegistration(emailService, proxy);
}
}
// 单次注册
async function handleSingleRegistration(emailService, proxy) {
addLog('info', '[*] 正在启动注册任务...');
try {
const response = await fetch(`${API_BASE}/registration/start`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email_service_type: emailService,
proxy: proxy,
}),
});
const data = await response.json();
if (response.ok) {
currentTask = data;
addLog('info', `[*] 任务已创建: ${data.task_uuid}`);
showTaskStatus(data);
// 开始轮询日志
startLogPolling(data.task_uuid);
} else {
addLog('error', `[Error] 启动失败: ${data.detail || '未知错误'}`);
resetButtons();
}
} catch (error) {
addLog('error', `[Error] 网络错误: ${error.message}`);
resetButtons();
}
}
// 批量注册
async function handleBatchRegistration(emailService, proxy) {
const count = parseInt(batchCountInput.value) || 5;
const intervalMin = parseInt(intervalMinInput.value) || 5;
const intervalMax = parseInt(intervalMaxInput.value) || 30;
addLog('info', `[*] 正在启动批量注册任务 (数量: ${count})...`);
try {
const response = await fetch(`${API_BASE}/registration/batch`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
count: count,
email_service_type: emailService,
proxy: proxy,
interval_min: intervalMin,
interval_max: intervalMax,
}),
});
const data = await response.json();
if (response.ok) {
currentBatch = data;
addLog('info', `[*] 批量任务已创建: ${data.batch_id}`);
addLog('info', `[*] 共 ${data.count} 个任务已加入队列`);
showBatchStatus(data);
// 开始轮询批量状态
startBatchPolling(data.batch_id);
} else {
addLog('error', `[Error] 启动失败: ${data.detail || '未知错误'}`);
resetButtons();
}
} catch (error) {
addLog('error', `[Error] 网络错误: ${error.message}`);
resetButtons();
}
}
// 取消任务
async function handleCancelTask() {
if (isBatchMode && currentBatch) {
try {
const response = await fetch(`${API_BASE}/registration/batch/${currentBatch.batch_id}/cancel`, {
method: 'POST',
});
if (response.ok) {
addLog('warning', '[!] 批量任务取消请求已提交');
stopBatchPolling();
resetButtons();
}
} catch (error) {
addLog('error', `[Error] 取消失败: ${error.message}`);
}
} else if (currentTask) {
try {
const response = await fetch(`${API_BASE}/registration/tasks/${currentTask.task_uuid}/cancel`, {
method: 'POST',
});
if (response.ok) {
addLog('warning', '[!] 任务已取消');
stopLogPolling();
resetButtons();
}
} catch (error) {
addLog('error', `[Error] 取消失败: ${error.message}`);
}
}
}
// 开始轮询日志
function startLogPolling(taskUuid) {
let lastLogLine = '';
logPollingInterval = setInterval(async () => {
try {
const response = await fetch(`${API_BASE}/registration/tasks/${taskUuid}/logs`);
const data = await response.json();
if (response.ok) {
// 更新任务状态
updateTaskStatus(data.status);
// 添加新日志
const logs = data.logs || [];
logs.forEach(log => {
if (log !== lastLogLine) {
const logType = getLogType(log);
addLog(logType, log);
lastLogLine = log;
}
});
// 检查任务是否完成
if (['completed', 'failed', 'cancelled'].includes(data.status)) {
stopLogPolling();
resetButtons();
if (data.status === 'completed') {
addLog('success', '[*] 注册成功!');
} else if (data.status === 'failed') {
addLog('error', '[Error] 注册失败');
}
}
}
} catch (error) {
console.error('轮询日志失败:', error);
}
}, 1000);
}
// 停止轮询日志
function stopLogPolling() {
if (logPollingInterval) {
clearInterval(logPollingInterval);
logPollingInterval = null;
}
}
// 开始轮询批量状态
function startBatchPolling(batchId) {
batchPollingInterval = setInterval(async () => {
try {
const response = await fetch(`${API_BASE}/registration/batch/${batchId}`);
const data = await response.json();
if (response.ok) {
updateBatchProgress(data);
// 检查是否完成
if (data.finished) {
stopBatchPolling();
resetButtons();
addLog('info', `[*] 批量任务完成!成功: ${data.success}, 失败: ${data.failed}`);
}
}
} catch (error) {
console.error('轮询批量状态失败:', error);
}
}, 2000);
}
// 停止轮询批量状态
function stopBatchPolling() {
if (batchPollingInterval) {
clearInterval(batchPollingInterval);
batchPollingInterval = null;
}
}
// 显示任务状态
function showTaskStatus(task) {
taskStatusCard.style.display = 'block';
batchStatusCard.style.display = 'none';
document.getElementById('task-id').textContent = task.task_uuid;
updateTaskStatus(task.status);
}
// 更新任务状态
function updateTaskStatus(status) {
const statusBadge = document.getElementById('task-status-badge');
const statusText = document.getElementById('task-status');
const statusMap = {
'pending': { text: '等待中', class: '' },
'running': { text: '运行中', class: 'running' },
'completed': { text: '已完成', class: 'completed' },
'failed': { text: '失败', class: 'failed' },
'cancelled': { text: '已取消', class: '' },
};
const info = statusMap[status] || { text: status, class: '' };
statusBadge.textContent = info.text;
statusBadge.className = 'status-badge ' + info.class;
statusText.textContent = info.text;
}
// 显示批量状态
function showBatchStatus(batch) {
batchStatusCard.style.display = 'block';
taskStatusCard.style.display = 'none';
document.getElementById('batch-progress').textContent = `0/${batch.count}`;
document.getElementById('progress-bar').style.width = '0%';
document.getElementById('batch-success').textContent = '0';
document.getElementById('batch-failed').textContent = '0';
document.getElementById('batch-remaining').textContent = batch.count;
}
// 更新批量进度
function updateBatchProgress(data) {
const progress = data.completed / data.total * 100;
document.getElementById('batch-progress').textContent = data.progress;
document.getElementById('progress-bar').style.width = `${progress}%`;
document.getElementById('batch-success').textContent = data.success;
document.getElementById('batch-failed').textContent = data.failed;
document.getElementById('batch-remaining').textContent = data.total - data.completed;
// 记录日志
if (data.completed > 0) {
addLog('info', `[*] 进度: ${data.progress}, 成功: ${data.success}, 失败: ${data.failed}`);
}
}
// 添加日志
function addLog(type, message) {
const line = document.createElement('div');
line.className = `log-line ${type}`;
line.textContent = message;
consoleLog.appendChild(line);
// 自动滚动到底部
consoleLog.scrollTop = consoleLog.scrollHeight;
}
// 获取日志类型
function getLogType(log) {
if (log.includes('[Error]') || log.includes('失败') || log.includes('错误')) {
return 'error';
}
if (log.includes('[!]') || log.includes('警告')) {
return 'warning';
}
if (log.includes('成功') || log.includes('完成')) {
return 'success';
}
return 'info';
}
// 重置按钮状态
function resetButtons() {
startBtn.disabled = false;
cancelBtn.disabled = true;
currentTask = null;
currentBatch = null;
}

513
static/js/settings.js Normal file
View File

@@ -0,0 +1,513 @@
/**
* 设置页面 JavaScript
*/
// API 基础路径
const API_BASE = '/api';
// DOM 元素
const tabBtns = document.querySelectorAll('.tab-btn');
const tabContents = document.querySelectorAll('.tab-content');
const proxyForm = document.getElementById('proxy-form');
const registrationForm = document.getElementById('registration-form');
const testProxyBtn = document.getElementById('test-proxy-btn');
const backupBtn = document.getElementById('backup-btn');
const cleanupBtn = document.getElementById('cleanup-btn');
const addEmailServiceBtn = document.getElementById('add-email-service-btn');
const addServiceModal = document.getElementById('add-service-modal');
const addServiceForm = document.getElementById('add-service-form');
const closeServiceModalBtn = document.getElementById('close-service-modal');
const cancelAddServiceBtn = document.getElementById('cancel-add-service');
const serviceTypeSelect = document.getElementById('service-type');
const serviceConfigFields = document.getElementById('service-config-fields');
const emailServicesTable = document.getElementById('email-services-table');
// Outlook 批量导入相关
const toggleImportBtn = document.getElementById('toggle-import-btn');
const outlookImportBody = document.getElementById('outlook-import-body');
const outlookImportBtn = document.getElementById('outlook-import-btn');
const clearImportBtn = document.getElementById('clear-import-btn');
const outlookImportData = document.getElementById('outlook-import-data');
const importResult = document.getElementById('import-result');
// 批量操作
const batchDeleteBtn = document.getElementById('batch-delete-btn');
const selectAllCheckbox = document.getElementById('select-all-services');
// 选中的服务 ID
let selectedServiceIds = [];
// 初始化
document.addEventListener('DOMContentLoaded', () => {
initTabs();
loadSettings();
loadEmailServices();
loadDatabaseInfo();
initEventListeners();
});
// 初始化标签页
function initTabs() {
tabBtns.forEach(btn => {
btn.addEventListener('click', () => {
const tab = btn.dataset.tab;
tabBtns.forEach(b => b.classList.remove('active'));
tabContents.forEach(c => c.classList.remove('active'));
btn.classList.add('active');
document.getElementById(`${tab}-tab`).classList.add('active');
});
});
}
// 事件监听
function initEventListeners() {
// 代理表单
proxyForm.addEventListener('submit', handleSaveProxy);
// 测试代理
testProxyBtn.addEventListener('click', handleTestProxy);
// 注册配置表单
registrationForm.addEventListener('submit', handleSaveRegistration);
// 备份数据库
backupBtn.addEventListener('click', handleBackup);
// 清理数据
cleanupBtn.addEventListener('click', handleCleanup);
// 添加邮箱服务
addEmailServiceBtn.addEventListener('click', () => {
addServiceModal.classList.add('active');
loadServiceConfigFields(serviceTypeSelect.value);
});
closeServiceModalBtn.addEventListener('click', () => {
addServiceModal.classList.remove('active');
});
cancelAddServiceBtn.addEventListener('click', () => {
addServiceModal.classList.remove('active');
});
addServiceModal.addEventListener('click', (e) => {
if (e.target === addServiceModal) {
addServiceModal.classList.remove('active');
}
});
// 服务类型切换
serviceTypeSelect.addEventListener('change', (e) => {
loadServiceConfigFields(e.target.value);
});
// 添加服务表单
addServiceForm.addEventListener('submit', handleAddService);
// Outlook 批量导入展开/折叠
if (toggleImportBtn) {
toggleImportBtn.addEventListener('click', () => {
const isHidden = outlookImportBody.style.display === 'none';
outlookImportBody.style.display = isHidden ? 'block' : 'none';
toggleImportBtn.textContent = isHidden ? '收起' : '展开';
});
}
// Outlook 批量导入
if (outlookImportBtn) {
outlookImportBtn.addEventListener('click', handleOutlookBatchImport);
}
// 清空导入数据
if (clearImportBtn) {
clearImportBtn.addEventListener('click', () => {
outlookImportData.value = '';
importResult.style.display = 'none';
});
}
// 全选/取消全选
if (selectAllCheckbox) {
selectAllCheckbox.addEventListener('change', (e) => {
const checkboxes = document.querySelectorAll('.service-checkbox');
checkboxes.forEach(cb => cb.checked = e.target.checked);
updateSelectedServices();
});
}
// 批量删除
if (batchDeleteBtn) {
batchDeleteBtn.addEventListener('click', handleBatchDelete);
}
}
// 加载设置
async function loadSettings() {
try {
const response = await fetch(`${API_BASE}/settings`);
const data = await response.json();
// 代理设置
document.getElementById('proxy-enabled').checked = data.proxy?.enabled || false;
document.getElementById('proxy-type').value = data.proxy?.type || 'http';
document.getElementById('proxy-host').value = data.proxy?.host || '127.0.0.1';
document.getElementById('proxy-port').value = data.proxy?.port || 7890;
document.getElementById('proxy-username').value = data.proxy?.username || '';
// 注册配置
document.getElementById('max-retries').value = data.registration?.max_retries || 3;
document.getElementById('timeout').value = data.registration?.timeout || 120;
document.getElementById('password-length').value = data.registration?.default_password_length || 12;
document.getElementById('sleep-min').value = data.registration?.sleep_min || 5;
document.getElementById('sleep-max').value = data.registration?.sleep_max || 30;
} catch (error) {
console.error('加载设置失败:', error);
}
}
// 加载邮箱服务
async function loadEmailServices() {
try {
const response = await fetch(`${API_BASE}/email-services`);
const data = await response.json();
renderEmailServices(data.services);
} catch (error) {
console.error('加载邮箱服务失败:', error);
}
}
// 渲染邮箱服务
function renderEmailServices(services) {
if (services.length === 0) {
emailServicesTable.innerHTML = '<tr><td colspan="7" style="text-align: center;">暂无配置</td></tr>';
batchDeleteBtn.style.display = 'none';
return;
}
emailServicesTable.innerHTML = services.map(service => `
<tr data-service-id="${service.id}">
<td><input type="checkbox" class="service-checkbox" data-id="${service.id}" onchange="updateSelectedServices()"></td>
<td>${escapeHtml(service.name)}</td>
<td>${getServiceTypeText(service.service_type)}</td>
<td><span class="status-badge ${service.enabled ? 'completed' : ''}">${service.enabled ? '已启用' : '已禁用'}</span></td>
<td>${service.priority}</td>
<td>${formatDate(service.last_used)}</td>
<td>
<button class="btn btn-sm btn-secondary" onclick="testService(${service.id})">测试</button>
<button class="btn btn-sm ${service.enabled ? 'btn-warning' : 'btn-primary'}" onclick="toggleService(${service.id}, ${!service.enabled})">
${service.enabled ? '禁用' : '启用'}
</button>
<button class="btn btn-sm btn-danger" onclick="deleteService(${service.id})">删除</button>
</td>
</tr>
`).join('');
// 更新批量删除按钮状态
updateSelectedServices();
}
// 加载数据库信息
async function loadDatabaseInfo() {
try {
const response = await fetch(`${API_BASE}/settings/database`);
const data = await response.json();
document.getElementById('db-size').textContent = `${data.database_size_mb} MB`;
document.getElementById('db-accounts').textContent = data.accounts_count;
document.getElementById('db-services').textContent = data.email_services_count;
document.getElementById('db-tasks').textContent = data.tasks_count;
} catch (error) {
console.error('加载数据库信息失败:', error);
}
}
// 保存代理设置
async function handleSaveProxy(e) {
e.preventDefault();
const data = {
enabled: document.getElementById('proxy-enabled').checked,
type: document.getElementById('proxy-type').value,
host: document.getElementById('proxy-host').value,
port: parseInt(document.getElementById('proxy-port').value),
username: document.getElementById('proxy-username').value || null,
password: document.getElementById('proxy-password').value || null,
};
try {
const response = await fetch(`${API_BASE}/settings/proxy`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (response.ok) {
alert('代理设置已保存');
} else {
const result = await response.json();
alert('保存失败: ' + (result.detail || '未知错误'));
}
} catch (error) {
alert('保存失败: ' + error.message);
}
}
// 测试代理
async function handleTestProxy() {
testProxyBtn.disabled = true;
testProxyBtn.textContent = '测试中...';
try {
// 这里应该调用一个测试代理的 API
// 暂时模拟
await new Promise(resolve => setTimeout(resolve, 1000));
alert('代理测试功能待实现');
} finally {
testProxyBtn.disabled = false;
testProxyBtn.textContent = '测试连接';
}
}
// 保存注册配置
async function handleSaveRegistration(e) {
e.preventDefault();
const data = {
max_retries: parseInt(document.getElementById('max-retries').value),
timeout: parseInt(document.getElementById('timeout').value),
default_password_length: parseInt(document.getElementById('password-length').value),
sleep_min: parseInt(document.getElementById('sleep-min').value),
sleep_max: parseInt(document.getElementById('sleep-max').value),
};
try {
const response = await fetch(`${API_BASE}/settings/registration`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (response.ok) {
alert('注册配置已保存');
} else {
const result = await response.json();
alert('保存失败: ' + (result.detail || '未知错误'));
}
} catch (error) {
alert('保存失败: ' + error.message);
}
}
// 备份数据库
async function handleBackup() {
backupBtn.disabled = true;
backupBtn.textContent = '备份中...';
try {
const response = await fetch(`${API_BASE}/settings/database/backup`, {
method: 'POST',
});
const data = await response.json();
if (response.ok) {
alert(`备份成功: ${data.backup_path}`);
} else {
alert('备份失败: ' + (data.detail || '未知错误'));
}
} catch (error) {
alert('备份失败: ' + error.message);
} finally {
backupBtn.disabled = false;
backupBtn.textContent = '备份数据库';
}
}
// 清理数据
async function handleCleanup() {
if (!confirm('确定要清理过期数据吗?此操作不可恢复。')) {
return;
}
cleanupBtn.disabled = true;
cleanupBtn.textContent = '清理中...';
try {
const response = await fetch(`${API_BASE}/settings/database/cleanup?days=30`, {
method: 'POST',
});
const data = await response.json();
if (response.ok) {
alert(data.message);
loadDatabaseInfo();
} else {
alert('清理失败: ' + (data.detail || '未知错误'));
}
} catch (error) {
alert('清理失败: ' + error.message);
} finally {
cleanupBtn.disabled = false;
cleanupBtn.textContent = '清理过期数据';
}
}
// 加载服务配置字段
async function loadServiceConfigFields(serviceType) {
try {
const response = await fetch(`${API_BASE}/email-services/types`);
const data = await response.json();
const typeInfo = data.types.find(t => t.value === serviceType);
if (!typeInfo) return;
serviceConfigFields.innerHTML = typeInfo.config_fields.map(field => `
<div class="form-group">
<label for="config-${field.name}">${field.label}</label>
<input type="${field.name.includes('password') || field.name.includes('token') ? 'password' : 'text'}"
id="config-${field.name}"
name="${field.name}"
value="${field.default || ''}"
${field.required ? 'required' : ''}>
</div>
`).join('');
} catch (error) {
console.error('加载配置字段失败:', error);
}
}
// 添加邮箱服务
async function handleAddService(e) {
e.preventDefault();
const formData = new FormData(addServiceForm);
const config = {};
serviceConfigFields.querySelectorAll('input').forEach(input => {
config[input.name] = input.value;
});
const data = {
service_type: formData.get('service_type'),
name: formData.get('name'),
config: config,
enabled: true,
priority: 0,
};
try {
const response = await fetch(`${API_BASE}/email-services`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (response.ok) {
addServiceModal.classList.remove('active');
addServiceForm.reset();
loadEmailServices();
alert('邮箱服务已添加');
} else {
const result = await response.json();
alert('添加失败: ' + (result.detail || '未知错误'));
}
} catch (error) {
alert('添加失败: ' + error.message);
}
}
// 测试服务
async function testService(id) {
try {
const response = await fetch(`${API_BASE}/email-services/${id}/test`, {
method: 'POST',
});
const data = await response.json();
if (data.success) {
alert('服务连接正常');
} else {
alert('服务连接失败: ' + data.message);
}
} catch (error) {
alert('测试失败: ' + error.message);
}
}
// 切换服务状态
async function toggleService(id, enabled) {
try {
const endpoint = enabled ? 'enable' : 'disable';
const response = await fetch(`${API_BASE}/email-services/${id}/${endpoint}`, {
method: 'POST',
});
if (response.ok) {
loadEmailServices();
} else {
const data = await response.json();
alert('操作失败: ' + (data.detail || '未知错误'));
}
} catch (error) {
alert('操作失败: ' + error.message);
}
}
// 删除服务
async function deleteService(id) {
if (!confirm('确定要删除此邮箱服务配置吗?')) {
return;
}
try {
const response = await fetch(`${API_BASE}/email-services/${id}`, {
method: 'DELETE',
});
if (response.ok) {
loadEmailServices();
} else {
const data = await response.json();
alert('删除失败: ' + (data.detail || '未知错误'));
}
} catch (error) {
alert('删除失败: ' + error.message);
}
}
// 工具函数
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function getServiceTypeText(type) {
const typeMap = {
'tempmail': 'Tempmail.lol',
'outlook': 'Outlook',
'custom_domain': '自定义域名',
};
return typeMap[type] || type;
}
function formatDate(dateStr) {
if (!dateStr) return '-';
const date = new Date(dateStr);
return date.toLocaleString('zh-CN');
}