This commit is contained in:
cnlimiter
2026-03-14 17:36:55 +08:00
parent 9d3099fcd8
commit 0688f4ca7e
8 changed files with 2066 additions and 882 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,36 +1,37 @@
/**
* 账号管理页面 JavaScript
* 使用 utils.js 中的工具库
*/
// API 基础路径
const API_BASE = '/api';
// 状态
let currentPage = 1;
let pageSize = 20;
let totalAccounts = 0;
let selectedAccounts = new Set();
let isLoading = false;
// 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');
const elements = {
table: document.getElementById('accounts-table'),
totalAccounts: document.getElementById('total-accounts'),
activeAccounts: document.getElementById('active-accounts'),
expiredAccounts: document.getElementById('expired-accounts'),
failedAccounts: document.getElementById('failed-accounts'),
filterStatus: document.getElementById('filter-status'),
filterService: document.getElementById('filter-service'),
searchInput: document.getElementById('search-input'),
refreshBtn: document.getElementById('refresh-btn'),
batchDeleteBtn: document.getElementById('batch-delete-btn'),
exportBtn: document.getElementById('export-btn'),
exportMenu: document.getElementById('export-menu'),
selectAll: document.getElementById('select-all'),
prevPage: document.getElementById('prev-page'),
nextPage: document.getElementById('next-page'),
pageInfo: document.getElementById('page-info'),
detailModal: document.getElementById('detail-modal'),
modalBody: document.getElementById('modal-body'),
closeModal: document.getElementById('close-modal')
};
// 初始化
document.addEventListener('DOMContentLoaded', () => {
@@ -42,38 +43,44 @@ document.addEventListener('DOMContentLoaded', () => {
// 事件监听
function initEventListeners() {
// 筛选
filterStatus.addEventListener('change', () => {
elements.filterStatus.addEventListener('change', () => {
currentPage = 1;
loadAccounts();
});
filterService.addEventListener('change', () => {
elements.filterService.addEventListener('change', () => {
currentPage = 1;
loadAccounts();
});
// 搜索
let searchTimeout;
searchInput.addEventListener('input', () => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
currentPage = 1;
// 搜索(防抖)
elements.searchInput.addEventListener('input', debounce(() => {
currentPage = 1;
loadAccounts();
}, 300));
// 快捷键聚焦搜索
elements.searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
elements.searchInput.blur();
elements.searchInput.value = '';
loadAccounts();
}, 300);
}
});
// 刷新
refreshBtn.addEventListener('click', () => {
elements.refreshBtn.addEventListener('click', () => {
loadStats();
loadAccounts();
toast.info('已刷新');
});
// 批量删除
batchDeleteBtn.addEventListener('click', handleBatchDelete);
elements.batchDeleteBtn.addEventListener('click', handleBatchDelete);
// 全选
selectAllCheckbox.addEventListener('change', (e) => {
const checkboxes = accountsTable.querySelectorAll('input[type="checkbox"]');
elements.selectAll.addEventListener('change', (e) => {
const checkboxes = elements.table.querySelectorAll('input[type="checkbox"][data-id]');
checkboxes.forEach(cb => {
cb.checked = e.target.checked;
const id = parseInt(cb.dataset.id);
@@ -87,124 +94,189 @@ function initEventListeners() {
});
// 分页
prevPageBtn.addEventListener('click', () => {
if (currentPage > 1) {
elements.prevPage.addEventListener('click', () => {
if (currentPage > 1 && !isLoading) {
currentPage--;
loadAccounts();
}
});
nextPageBtn.addEventListener('click', () => {
elements.nextPage.addEventListener('click', () => {
const totalPages = Math.ceil(totalAccounts / pageSize);
if (currentPage < totalPages) {
if (currentPage < totalPages && !isLoading) {
currentPage++;
loadAccounts();
}
});
// 导出
exportBtn.addEventListener('click', (e) => {
elements.exportBtn.addEventListener('click', (e) => {
e.stopPropagation();
exportMenu.classList.toggle('active');
elements.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');
});
delegate(elements.exportMenu, 'click', '.dropdown-item', (e, target) => {
e.preventDefault();
const format = target.dataset.format;
exportAccounts(format);
elements.exportMenu.classList.remove('active');
});
// 关闭模态框
closeModalBtn.addEventListener('click', () => {
detailModal.classList.remove('active');
elements.closeModal.addEventListener('click', () => {
elements.detailModal.classList.remove('active');
});
detailModal.addEventListener('click', (e) => {
if (e.target === detailModal) {
detailModal.classList.remove('active');
elements.detailModal.addEventListener('click', (e) => {
if (e.target === elements.detailModal) {
elements.detailModal.classList.remove('active');
}
});
// 点击其他地方关闭下拉菜单
document.addEventListener('click', () => {
exportMenu.classList.remove('active');
elements.exportMenu.classList.remove('active');
});
}
// 加载统计信息
async function loadStats() {
try {
const response = await fetch(`${API_BASE}/accounts/stats/summary`);
const data = await response.json();
const data = await api.get('/accounts/stats/summary');
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;
elements.totalAccounts.textContent = format.number(data.total || 0);
elements.activeAccounts.textContent = format.number(data.by_status?.active || 0);
elements.expiredAccounts.textContent = format.number(data.by_status?.expired || 0);
elements.failedAccounts.textContent = format.number(data.by_status?.failed || 0);
// 添加动画效果
animateValue(elements.totalAccounts, data.total || 0);
} catch (error) {
console.error('加载统计信息失败:', error);
}
}
// 数字动画
function animateValue(element, value) {
element.style.transition = 'transform 0.2s ease';
element.style.transform = 'scale(1.1)';
setTimeout(() => {
element.style.transform = 'scale(1)';
}, 200);
}
// 加载账号列表
async function loadAccounts() {
if (isLoading) return;
isLoading = true;
// 显示加载状态
elements.table.innerHTML = `
<tr>
<td colspan="7">
<div class="empty-state">
<div class="skeleton skeleton-text" style="width: 60%;"></div>
<div class="skeleton skeleton-text" style="width: 80%;"></div>
<div class="skeleton skeleton-text" style="width: 40%;"></div>
</div>
</td>
</tr>
`;
const params = new URLSearchParams({
page: currentPage,
page_size: pageSize,
});
if (filterStatus.value) {
params.append('status', filterStatus.value);
if (elements.filterStatus.value) {
params.append('status', elements.filterStatus.value);
}
if (filterService.value) {
params.append('email_service', filterService.value);
if (elements.filterService.value) {
params.append('email_service', elements.filterService.value);
}
if (searchInput.value.trim()) {
params.append('search', searchInput.value.trim());
if (elements.searchInput.value.trim()) {
params.append('search', elements.searchInput.value.trim());
}
try {
const response = await fetch(`${API_BASE}/accounts?${params}`);
const data = await response.json();
const data = await api.get(`/accounts?${params}`);
totalAccounts = data.total;
renderAccounts(data.accounts);
updatePagination();
} catch (error) {
console.error('加载账号列表失败:', error);
accountsTable.innerHTML = '<tr><td colspan="7" style="text-align: center;">加载失败</td></tr>';
elements.table.innerHTML = `
<tr>
<td colspan="7">
<div class="empty-state">
<div class="empty-state-icon">❌</div>
<div class="empty-state-title">加载失败</div>
<div class="empty-state-description">请检查网络连接后重试</div>
</div>
</td>
</tr>
`;
} finally {
isLoading = false;
}
}
// 渲染账号列表
function renderAccounts(accounts) {
if (accounts.length === 0) {
accountsTable.innerHTML = '<tr><td colspan="7" style="text-align: center;">暂无数据</td></tr>';
elements.table.innerHTML = `
<tr>
<td colspan="7">
<div class="empty-state">
<div class="empty-state-icon">📭</div>
<div class="empty-state-title">暂无数据</div>
<div class="empty-state-description">没有找到符合条件的账号记录</div>
</div>
</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>
elements.table.innerHTML = accounts.map(account => `
<tr data-id="${account.id}">
<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>
<input type="checkbox" data-id="${account.id}"
${selectedAccounts.has(account.id) ? 'checked' : ''}>
</td>
<td>${account.id}</td>
<td>
<span class="email-cell" title="${escapeHtml(account.email)}">
${escapeHtml(account.email)}
</span>
</td>
<td>${getServiceTypeText(account.email_service)}</td>
<td>
<span class="status-badge ${getStatusClass('account', account.status)}">
${getStatusText('account', account.status)}
</span>
</td>
<td>${format.date(account.registered_at)}</td>
<td>
<div class="action-buttons">
<button class="btn btn-ghost btn-sm" onclick="viewAccount(${account.id})" title="查看详情">
👁️
</button>
<button class="btn btn-ghost btn-sm" onclick="copyEmail('${escapeHtml(account.email)}')" title="复制邮箱">
📋
</button>
<button class="btn btn-ghost btn-sm" onclick="deleteAccount(${account.id}, '${escapeHtml(account.email)}')" title="删除">
🗑️
</button>
</div>
</td>
</tr>
`).join('');
// 绑定复选框事件
accountsTable.querySelectorAll('input[type="checkbox"]').forEach(cb => {
elements.table.querySelectorAll('input[type="checkbox"][data-id]').forEach(cb => {
cb.addEventListener('change', (e) => {
const id = parseInt(e.target.dataset.id);
if (e.target.checked) {
@@ -219,91 +291,107 @@ function renderAccounts(accounts) {
// 更新分页
function updatePagination() {
const totalPages = Math.ceil(totalAccounts / pageSize);
const totalPages = Math.max(1, Math.ceil(totalAccounts / pageSize));
prevPageBtn.disabled = currentPage <= 1;
nextPageBtn.disabled = currentPage >= totalPages;
elements.prevPage.disabled = currentPage <= 1;
elements.nextPage.disabled = currentPage >= totalPages;
pageInfo.textContent = `${currentPage} 页 / 共 ${totalPages}`;
elements.pageInfo.textContent = `${currentPage} 页 / 共 ${totalPages}`;
}
// 更新批量操作按钮
function updateBatchButtons() {
batchDeleteBtn.disabled = selectedAccounts.size === 0;
const count = selectedAccounts.size;
elements.batchDeleteBtn.disabled = count === 0;
elements.batchDeleteBtn.textContent = count > 0 ? `🗑️ 删除选中 (${count})` : '🗑️ 批量删除';
}
// 查看账号详情
async function viewAccount(id) {
try {
const response = await fetch(`${API_BASE}/accounts/${id}`);
const account = await response.json();
const account = await api.get(`/accounts/${id}`);
const tokens = await api.get(`/accounts/${id}/tokens`);
const tokensResponse = await fetch(`${API_BASE}/accounts/${id}/tokens`);
const tokens = await tokensResponse.json();
modalBody.innerHTML = `
elements.modalBody.innerHTML = `
<div class="info-grid">
<div class="info-item">
<span class="label">邮箱</span>
<span class="value">${escapeHtml(account.email)}</span>
<span class="value">
${escapeHtml(account.email)}
<button class="btn btn-ghost btn-sm" onclick="copyToClipboard('${escapeHtml(account.email)}')" title="复制">
📋
</button>
</span>
</div>
<div class="info-item">
<span class="label">邮箱服务</span>
<span class="value">${escapeHtml(account.email_service)}</span>
<span class="value">${getServiceTypeText(account.email_service)}</span>
</div>
<div class="info-item">
<span class="label">状态</span>
<span class="value">${getStatusText(account.status)}</span>
<span class="value">
<span class="status-badge ${getStatusClass('account', account.status)}">
${getStatusText('account', account.status)}
</span>
</span>
</div>
<div class="info-item">
<span class="label">注册时间</span>
<span class="value">${formatDate(account.registered_at)}</span>
<span class="value">${format.date(account.registered_at)}</span>
</div>
<div class="info-item">
<div class="info-item" style="grid-column: span 2;">
<span class="label">Account ID</span>
<span class="value">${escapeHtml(account.account_id || '-')}</span>
<span class="value" style="font-size: 0.75rem; word-break: break-all;">
${escapeHtml(account.account_id || '-')}
</span>
</div>
<div class="info-item">
<div class="info-item" style="grid-column: span 2;">
<span class="label">Workspace ID</span>
<span class="value">${escapeHtml(account.workspace_id || '-')}</span>
<span class="value" style="font-size: 0.75rem; word-break: break-all;">
${escapeHtml(account.workspace_id || '-')}
</span>
</div>
<div class="info-item">
<div class="info-item" style="grid-column: span 2;">
<span class="label">Access Token</span>
<span class="value" style="font-size: 0.75rem; word-break: break-all;">${escapeHtml(tokens.access_token || '-')}</span>
<div class="value" style="font-size: 0.7rem; word-break: break-all; font-family: var(--font-mono); background: var(--surface-hover); padding: 8px; border-radius: 4px;">
${escapeHtml(tokens.access_token || '-')}
${tokens.access_token ? `<button class="btn btn-ghost btn-sm" onclick="copyToClipboard('${escapeHtml(tokens.access_token)}')" style="margin-left: 8px;">📋</button>` : ''}
</div>
</div>
<div class="info-item">
<div class="info-item" style="grid-column: span 2;">
<span class="label">Refresh Token</span>
<span class="value" style="font-size: 0.75rem; word-break: break-all;">${escapeHtml(tokens.refresh_token || '-')}</span>
<div class="value" style="font-size: 0.7rem; word-break: break-all; font-family: var(--font-mono); background: var(--surface-hover); padding: 8px; border-radius: 4px;">
${escapeHtml(tokens.refresh_token || '-')}
${tokens.refresh_token ? `<button class="btn btn-ghost btn-sm" onclick="copyToClipboard('${escapeHtml(tokens.refresh_token)}')" style="margin-left: 8px;">📋</button>` : ''}
</div>
</div>
</div>
`;
detailModal.classList.add('active');
elements.detailModal.classList.add('active');
} catch (error) {
alert('加载账号详情失败: ' + error.message);
toast.error('加载账号详情失败: ' + error.message);
}
}
// 复制邮箱
function copyEmail(email) {
copyToClipboard(email);
}
// 删除账号
async function deleteAccount(id, email) {
if (!confirm(`确定要删除账号 ${email} 吗?`)) {
return;
}
const confirmed = await confirm(`确定要删除账号 ${email} 吗?此操作不可恢复。`);
if (!confirmed) 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 || '未知错误'));
}
await api.delete(`/accounts/${id}`);
toast.success('账号已删除');
selectedAccounts.delete(id);
loadStats();
loadAccounts();
} catch (error) {
alert('删除失败: ' + error.message);
toast.error('删除失败: ' + error.message);
}
}
@@ -311,33 +399,20 @@ async function deleteAccount(id, email) {
async function handleBatchDelete() {
if (selectedAccounts.size === 0) return;
if (!confirm(`确定要删除选中的 ${selectedAccounts.size} 个账号吗?`)) {
return;
}
const confirmed = await confirm(`确定要删除选中的 ${selectedAccounts.size} 个账号吗?此操作不可恢复。`);
if (!confirmed) 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 result = await api.post('/accounts/batch-delete', {
ids: Array.from(selectedAccounts)
});
const data = await response.json();
if (response.ok) {
alert(`成功删除 ${data.deleted_count} 个账号`);
selectedAccounts.clear();
loadStats();
loadAccounts();
} else {
alert('删除失败: ' + (data.detail || '未知错误'));
}
toast.success(`成功删除 ${result.deleted_count} 个账号`);
selectedAccounts.clear();
loadStats();
loadAccounts();
} catch (error) {
alert('删除失败: ' + error.message);
toast.error('删除失败: ' + error.message);
}
}
@@ -345,37 +420,22 @@ async function handleBatchDelete() {
function exportAccounts(format) {
const params = new URLSearchParams();
if (filterStatus.value) {
params.append('status', filterStatus.value);
if (elements.filterStatus.value) {
params.append('status', elements.filterStatus.value);
}
if (filterService.value) {
params.append('email_service', filterService.value);
if (elements.filterService.value) {
params.append('email_service', elements.filterService.value);
}
window.location.href = `${API_BASE}/accounts/export/${format}?${params}`;
window.location.href = `/api/accounts/export/${format}?${params}`;
toast.info('正在导出...');
}
// 工具函数
// HTML 转义
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');
}

View File

@@ -1,160 +1,303 @@
/**
* 注册页面 JavaScript
* 使用 utils.js 中的工具库
*/
// API 基础路径
const API_BASE = '/api';
// 状态
let currentTask = null;
let currentBatch = null;
let logPollingInterval = null;
let batchPollingInterval = null;
let isBatchMode = false;
let availableServices = {
tempmail: { available: true, services: [] },
outlook: { available: false, services: [] },
custom_domain: { available: false, services: [] }
};
// 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');
const elements = {
form: document.getElementById('registration-form'),
emailService: document.getElementById('email-service'),
proxy: document.getElementById('proxy'),
regMode: document.getElementById('reg-mode'),
batchCountGroup: document.getElementById('batch-count-group'),
batchCount: document.getElementById('batch-count'),
batchOptions: document.getElementById('batch-options'),
intervalMin: document.getElementById('interval-min'),
intervalMax: document.getElementById('interval-max'),
startBtn: document.getElementById('start-btn'),
cancelBtn: document.getElementById('cancel-btn'),
taskStatusCard: document.getElementById('task-status-card'),
batchStatusCard: document.getElementById('batch-status-card'),
consoleLog: document.getElementById('console-log'),
clearLogBtn: document.getElementById('clear-log-btn'),
// 任务状态
taskId: document.getElementById('task-id'),
taskEmail: document.getElementById('task-email'),
taskStatus: document.getElementById('task-status'),
taskService: document.getElementById('task-service'),
taskStatusBadge: document.getElementById('task-status-badge'),
// 批量状态
batchProgress: document.getElementById('batch-progress'),
progressBar: document.getElementById('progress-bar'),
batchSuccess: document.getElementById('batch-success'),
batchFailed: document.getElementById('batch-failed'),
batchRemaining: document.getElementById('batch-remaining')
};
// 初始化
document.addEventListener('DOMContentLoaded', () => {
initEventListeners();
loadSavedProxy();
loadAvailableServices();
});
// 事件监听
function initEventListeners() {
// 注册表单提交
registrationForm.addEventListener('submit', handleStartRegistration);
elements.form.addEventListener('submit', handleStartRegistration);
// 注册模式切换
regModeSelect.addEventListener('change', handleModeChange);
elements.regMode.addEventListener('change', handleModeChange);
// 邮箱服务切换
elements.emailService.addEventListener('change', handleServiceChange);
// 取消按钮
cancelBtn.addEventListener('click', handleCancelTask);
elements.cancelBtn.addEventListener('click', handleCancelTask);
// 清空日志
clearLogBtn.addEventListener('click', () => {
consoleLog.innerHTML = '<div class="log-line info">[*] 日志已清空</div>';
elements.clearLogBtn.addEventListener('click', () => {
elements.consoleLog.innerHTML = '<div class="log-line info">[系统] 日志已清空</div>';
});
}
// 加载保存的代理设置
async function loadSavedProxy() {
try {
const settings = await api.get('/settings');
if (settings.proxy?.host) {
elements.proxy.value = `${settings.proxy.type}://${settings.proxy.host}:${settings.proxy.port}`;
}
} catch (error) {
// 忽略错误
}
}
// 加载可用的邮箱服务
async function loadAvailableServices() {
try {
const data = await api.get('/registration/available-services');
availableServices = data;
// 更新邮箱服务选择框
updateEmailServiceOptions();
addLog('info', '[系统] 邮箱服务列表已加载');
} catch (error) {
console.error('加载邮箱服务列表失败:', error);
addLog('warning', '[警告] 加载邮箱服务列表失败');
}
}
// 更新邮箱服务选择框
function updateEmailServiceOptions() {
const select = elements.emailService;
select.innerHTML = '';
// Tempmail
if (availableServices.tempmail.available) {
const optgroup = document.createElement('optgroup');
optgroup.label = '🌐 临时邮箱';
availableServices.tempmail.services.forEach(service => {
const option = document.createElement('option');
option.value = `tempmail:${service.id || 'default'}`;
option.textContent = service.name;
option.dataset.type = 'tempmail';
optgroup.appendChild(option);
});
select.appendChild(optgroup);
}
// Outlook
if (availableServices.outlook.available) {
const optgroup = document.createElement('optgroup');
optgroup.label = `📧 Outlook (${availableServices.outlook.count} 个账户)`;
availableServices.outlook.services.forEach(service => {
const option = document.createElement('option');
option.value = `outlook:${service.id}`;
option.textContent = service.name + (service.has_oauth ? ' (OAuth)' : '');
option.dataset.type = 'outlook';
option.dataset.serviceId = service.id;
optgroup.appendChild(option);
});
select.appendChild(optgroup);
} else {
const optgroup = document.createElement('optgroup');
optgroup.label = '📧 Outlook (未配置)';
const option = document.createElement('option');
option.value = '';
option.textContent = '请先在设置中导入账户';
option.disabled = true;
optgroup.appendChild(option);
select.appendChild(optgroup);
}
// 自定义域名
if (availableServices.custom_domain.available) {
const optgroup = document.createElement('optgroup');
optgroup.label = `🔗 自定义域名 (${availableServices.custom_domain.count} 个服务)`;
availableServices.custom_domain.services.forEach(service => {
const option = document.createElement('option');
option.value = `custom_domain:${service.id || 'default'}`;
option.textContent = service.name;
option.dataset.type = 'custom_domain';
if (service.id) {
option.dataset.serviceId = service.id;
}
optgroup.appendChild(option);
});
select.appendChild(optgroup);
} else {
const optgroup = document.createElement('optgroup');
optgroup.label = '🔗 自定义域名 (未配置)';
const option = document.createElement('option');
option.value = '';
option.textContent = '请先在设置中添加服务';
option.disabled = true;
optgroup.appendChild(option);
select.appendChild(optgroup);
}
}
// 处理邮箱服务切换
function handleServiceChange(e) {
const value = e.target.value;
if (!value) return;
const [type, id] = value.split(':');
const selectedOption = e.target.options[e.target.selectedIndex];
// 显示服务信息
if (type === 'outlook') {
const service = availableServices.outlook.services.find(s => s.id == id);
if (service) {
addLog('info', `[系统] 已选择 Outlook 账户: ${service.name}`);
}
} else if (type === 'custom_domain') {
const service = availableServices.custom_domain.services.find(s => s.id == id);
if (service) {
addLog('info', `[系统] 已选择自定义域名服务: ${service.name}`);
}
}
}
// 模式切换
function handleModeChange(e) {
const mode = e.target.value;
isBatchMode = mode === 'batch';
batchCountGroup.style.display = isBatchMode ? 'block' : 'none';
batchOptions.style.display = isBatchMode ? 'block' : 'none';
elements.batchCountGroup.style.display = isBatchMode ? 'block' : 'none';
elements.batchOptions.style.display = isBatchMode ? 'block' : 'none';
}
// 开始注册
async function handleStartRegistration(e) {
e.preventDefault();
const emailService = emailServiceSelect.value;
const proxy = proxyInput.value.trim() || null;
const selectedValue = elements.emailService.value;
if (!selectedValue) {
toast.error('请选择一个邮箱服务');
return;
}
const [emailServiceType, serviceId] = selectedValue.split(':');
const proxy = elements.proxy.value.trim() || null;
// 禁用开始按钮
startBtn.disabled = true;
cancelBtn.disabled = false;
elements.startBtn.disabled = true;
elements.cancelBtn.disabled = false;
// 清空日志
consoleLog.innerHTML = '';
elements.consoleLog.innerHTML = '';
// 构建请求数据
const requestData = {
email_service_type: emailServiceType,
proxy: proxy
};
// 如果选择了数据库中的服务,传递 service_id
if (serviceId && serviceId !== 'default') {
requestData.email_service_id = parseInt(serviceId);
}
if (isBatchMode) {
await handleBatchRegistration(emailService, proxy);
await handleBatchRegistration(requestData);
} else {
await handleSingleRegistration(emailService, proxy);
await handleSingleRegistration(requestData);
}
}
// 单次注册
async function handleSingleRegistration(emailService, proxy) {
addLog('info', '[*] 正在启动注册任务...');
async function handleSingleRegistration(requestData) {
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 api.post('/registration/start', requestData);
const data = await response.json();
currentTask = data;
addLog('info', `[系统] 任务已创建: ${data.task_uuid}`);
showTaskStatus(data);
updateTaskStatus('running');
if (response.ok) {
currentTask = data;
addLog('info', `[*] 任务已创建: ${data.task_uuid}`);
showTaskStatus(data);
// 开始轮询日志
startLogPolling(data.task_uuid);
// 开始轮询日志
startLogPolling(data.task_uuid);
} else {
addLog('error', `[Error] 启动失败: ${data.detail || '未知错误'}`);
resetButtons();
}
} catch (error) {
addLog('error', `[Error] 网络错误: ${error.message}`);
addLog('error', `[错误] 启动失败: ${error.message}`);
toast.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;
async function handleBatchRegistration(requestData) {
const count = parseInt(elements.batchCount.value) || 5;
const intervalMin = parseInt(elements.intervalMin.value) || 5;
const intervalMax = parseInt(elements.intervalMax.value) || 30;
addLog('info', `[*] 正在启动批量注册任务 (数量: ${count})...`);
requestData.count = count;
requestData.interval_min = intervalMin;
requestData.interval_max = intervalMax;
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 api.post('/registration/batch', requestData);
const data = await response.json();
currentBatch = data;
addLog('info', `[系统] 批量任务已创建: ${data.batch_id}`);
addLog('info', `[系统] 共 ${data.count} 个任务已加入队列`);
showBatchStatus(data);
if (response.ok) {
currentBatch = data;
addLog('info', `[*] 批量任务已创建: ${data.batch_id}`);
addLog('info', `[*] 共 ${data.count} 个任务已加入队列`);
showBatchStatus(data);
// 开始轮询批量状态
startBatchPolling(data.batch_id);
// 开始轮询批量状态
startBatchPolling(data.batch_id);
} else {
addLog('error', `[Error] 启动失败: ${data.detail || '未知错误'}`);
resetButtons();
}
} catch (error) {
addLog('error', `[Error] 网络错误: ${error.message}`);
addLog('error', `[错误] 启动失败: ${error.message}`);
toast.error(error.message);
resetButtons();
}
}
@@ -163,68 +306,70 @@ async function handleBatchRegistration(emailService, proxy) {
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();
}
await api.post(`/registration/batch/${currentBatch.batch_id}/cancel`);
addLog('warning', '[警告] 批量任务取消请求已提交');
toast.info('任务取消请求已提交');
stopBatchPolling();
resetButtons();
} catch (error) {
addLog('error', `[Error] 取消失败: ${error.message}`);
addLog('error', `[错误] 取消失败: ${error.message}`);
toast.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();
}
await api.post(`/registration/tasks/${currentTask.task_uuid}/cancel`);
addLog('warning', '[警告] 任务已取消');
toast.info('任务已取消');
stopLogPolling();
resetButtons();
} catch (error) {
addLog('error', `[Error] 取消失败: ${error.message}`);
addLog('error', `[错误] 取消失败: ${error.message}`);
toast.error(error.message);
}
}
}
// 开始轮询日志
function startLogPolling(taskUuid) {
let lastLogLine = '';
let lastLogIndex = 0;
logPollingInterval = setInterval(async () => {
try {
const response = await fetch(`${API_BASE}/registration/tasks/${taskUuid}/logs`);
const data = await response.json();
const data = await api.get(`/registration/tasks/${taskUuid}/logs`);
if (response.ok) {
// 更新任务状态
updateTaskStatus(data.status);
// 更新任务状态
updateTaskStatus(data.status);
// 添加新日志
const logs = data.logs || [];
logs.forEach(log => {
if (log !== lastLogLine) {
const logType = getLogType(log);
addLog(logType, log);
lastLogLine = log;
}
});
// 更新邮箱信息
if (data.email) {
elements.taskEmail.textContent = data.email;
}
if (data.email_service) {
elements.taskService.textContent = getServiceTypeText(data.email_service);
}
// 检查任务是否完成
if (['completed', 'failed', 'cancelled'].includes(data.status)) {
stopLogPolling();
resetButtons();
// 添加新日志
const logs = data.logs || [];
for (let i = lastLogIndex; i < logs.length; i++) {
const log = logs[i];
const logType = getLogType(log);
addLog(logType, log);
}
lastLogIndex = logs.length;
if (data.status === 'completed') {
addLog('success', '[*] 注册成功!');
} else if (data.status === 'failed') {
addLog('error', '[Error] 注册失败');
}
// 检查任务是否完成
if (['completed', 'failed', 'cancelled'].includes(data.status)) {
stopLogPolling();
resetButtons();
if (data.status === 'completed') {
addLog('success', '[成功] 注册成功!');
toast.success('注册成功!');
} else if (data.status === 'failed') {
addLog('error', '[错误] 注册失败');
toast.error('注册失败');
} else if (data.status === 'cancelled') {
addLog('warning', '[警告] 任务已取消');
}
}
} catch (error) {
@@ -245,18 +390,19 @@ function stopLogPolling() {
function startBatchPolling(batchId) {
batchPollingInterval = setInterval(async () => {
try {
const response = await fetch(`${API_BASE}/registration/batch/${batchId}`);
const data = await response.json();
const data = await api.get(`/registration/batch/${batchId}`);
updateBatchProgress(data);
if (response.ok) {
updateBatchProgress(data);
// 检查是否完成
if (data.finished) {
stopBatchPolling();
resetButtons();
// 检查是否完成
if (data.finished) {
stopBatchPolling();
resetButtons();
addLog('info', `[*] 批量任务完成!成功: ${data.success}, 失败: ${data.failed}`);
addLog('info', `[完成] 批量任务完成!成功: ${data.success}, 失败: ${data.failed}`);
if (data.success > 0) {
toast.success(`批量注册完成,成功 ${data.success}`);
} else {
toast.warning('批量注册完成,但没有成功注册任何账号');
}
}
} catch (error) {
@@ -275,54 +421,67 @@ function stopBatchPolling() {
// 显示任务状态
function showTaskStatus(task) {
taskStatusCard.style.display = 'block';
batchStatusCard.style.display = 'none';
document.getElementById('task-id').textContent = task.task_uuid;
updateTaskStatus(task.status);
elements.taskStatusCard.style.display = 'block';
elements.batchStatusCard.style.display = 'none';
elements.taskId.textContent = task.task_uuid;
elements.taskEmail.textContent = '-';
elements.taskService.textContent = '-';
}
// 更新任务状态
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 statusInfo = {
pending: { text: '等待中', class: 'pending' },
running: { text: '运行中', class: 'running' },
completed: { text: '已完成', class: 'completed' },
failed: { text: '失败', class: 'failed' },
cancelled: { text: '已取消', class: 'disabled' }
};
const info = statusMap[status] || { text: status, class: '' };
statusBadge.textContent = info.text;
statusBadge.className = 'status-badge ' + info.class;
statusText.textContent = info.text;
const info = statusInfo[status] || { text: status, class: '' };
elements.taskStatusBadge.textContent = info.text;
elements.taskStatusBadge.className = `status-badge ${info.class}`;
elements.taskStatus.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;
elements.batchStatusCard.style.display = 'block';
elements.taskStatusCard.style.display = 'none';
elements.batchProgress.textContent = `0/${batch.count}`;
elements.progressBar.style.width = '0%';
elements.batchSuccess.textContent = '0';
elements.batchFailed.textContent = '0';
elements.batchRemaining.textContent = batch.count;
// 重置计数器
elements.batchSuccess.dataset.last = '0';
elements.batchFailed.dataset.last = '0';
}
// 更新批量进度
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;
const progress = (data.completed / data.total * 100).toFixed(0);
elements.batchProgress.textContent = data.progress || `${data.completed}/${data.total}`;
elements.progressBar.style.width = `${progress}%`;
elements.batchSuccess.textContent = data.success;
elements.batchFailed.textContent = data.failed;
elements.batchRemaining.textContent = data.total - data.completed;
// 记录日志
// 记录日志(避免重复)
if (data.completed > 0) {
addLog('info', `[*] 进度: ${data.progress}, 成功: ${data.success}, 失败: ${data.failed}`);
const lastSuccess = parseInt(elements.batchSuccess.dataset.last || '0');
const lastFailed = parseInt(elements.batchFailed.dataset.last || '0');
if (data.success > lastSuccess) {
addLog('success', `[成功] 第 ${data.success} 个账号注册成功`);
}
if (data.failed > lastFailed) {
addLog('error', `[失败] 第 ${data.failed} 个账号注册失败`);
}
elements.batchSuccess.dataset.last = data.success;
elements.batchFailed.dataset.last = data.failed;
}
}
@@ -330,22 +489,39 @@ function updateBatchProgress(data) {
function addLog(type, message) {
const line = document.createElement('div');
line.className = `log-line ${type}`;
line.textContent = message;
consoleLog.appendChild(line);
// 添加时间戳
const timestamp = new Date().toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
line.innerHTML = `<span class="timestamp">[${timestamp}]</span>${escapeHtml(message)}`;
elements.consoleLog.appendChild(line);
// 自动滚动到底部
consoleLog.scrollTop = consoleLog.scrollHeight;
elements.consoleLog.scrollTop = elements.consoleLog.scrollHeight;
// 限制日志行数
const lines = elements.consoleLog.querySelectorAll('.log-line');
if (lines.length > 500) {
lines[0].remove();
}
}
// 获取日志类型
function getLogType(log) {
if (log.includes('[Error]') || log.includes('失败') || log.includes('错误')) {
if (typeof log !== 'string') return 'info';
const lowerLog = log.toLowerCase();
if (lowerLog.includes('error') || lowerLog.includes('失败') || lowerLog.includes('错误')) {
return 'error';
}
if (log.includes('[!]') || log.includes('警告')) {
if (lowerLog.includes('warning') || lowerLog.includes('警告')) {
return 'warning';
}
if (log.includes('成功') || log.includes('完成')) {
if (lowerLog.includes('success') || lowerLog.includes('成功') || lowerLog.includes('完成')) {
return 'success';
}
return 'info';
@@ -353,8 +529,16 @@ function getLogType(log) {
// 重置按钮状态
function resetButtons() {
startBtn.disabled = false;
cancelBtn.disabled = true;
elements.startBtn.disabled = false;
elements.cancelBtn.disabled = true;
currentTask = null;
currentBatch = null;
}
// HTML 转义
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}

View File

@@ -1,41 +1,38 @@
/**
* 设置页面 JavaScript
* 使用 utils.js 中的工具库
*/
// 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');
const elements = {
tabs: document.querySelectorAll('.tab-btn'),
tabContents: document.querySelectorAll('.tab-content'),
proxyForm: document.getElementById('proxy-form'),
registrationForm: document.getElementById('registration-settings-form'),
testProxyBtn: document.getElementById('test-proxy-btn'),
backupBtn: document.getElementById('backup-btn'),
cleanupBtn: document.getElementById('cleanup-btn'),
addEmailServiceBtn: document.getElementById('add-email-service-btn'),
addServiceModal: document.getElementById('add-service-modal'),
addServiceForm: document.getElementById('add-service-form'),
closeServiceModal: document.getElementById('close-service-modal'),
cancelAddService: document.getElementById('cancel-add-service'),
serviceType: document.getElementById('service-type'),
serviceConfigFields: document.getElementById('service-config-fields'),
emailServicesTable: document.getElementById('email-services-table'),
// Outlook 导入
toggleImportBtn: document.getElementById('toggle-import-btn'),
outlookImportBody: document.getElementById('outlook-import-body'),
outlookImportBtn: document.getElementById('outlook-import-btn'),
clearImportBtn: document.getElementById('clear-import-btn'),
outlookImportData: document.getElementById('outlook-import-data'),
importResult: document.getElementById('import-result'),
// 批量操作
selectAllServices: document.getElementById('select-all-services')
};
// 选中的服务 ID
let selectedServiceIds = [];
let selectedServiceIds = new Set();
// 初始化
document.addEventListener('DOMContentLoaded', () => {
@@ -48,12 +45,12 @@ document.addEventListener('DOMContentLoaded', () => {
// 初始化标签页
function initTabs() {
tabBtns.forEach(btn => {
elements.tabs.forEach(btn => {
btn.addEventListener('click', () => {
const tab = btn.dataset.tab;
tabBtns.forEach(b => b.classList.remove('active'));
tabContents.forEach(c => c.classList.remove('active'));
elements.tabs.forEach(b => b.classList.remove('active'));
elements.tabContents.forEach(c => c.classList.remove('active'));
btn.classList.add('active');
document.getElementById(`${tab}-tab`).classList.add('active');
@@ -64,90 +61,84 @@ function initTabs() {
// 事件监听
function initEventListeners() {
// 代理表单
proxyForm.addEventListener('submit', handleSaveProxy);
elements.proxyForm.addEventListener('submit', handleSaveProxy);
// 测试代理
testProxyBtn.addEventListener('click', handleTestProxy);
elements.testProxyBtn.addEventListener('click', handleTestProxy);
// 注册配置表单
registrationForm.addEventListener('submit', handleSaveRegistration);
elements.registrationForm.addEventListener('submit', handleSaveRegistration);
// 备份数据库
backupBtn.addEventListener('click', handleBackup);
elements.backupBtn.addEventListener('click', handleBackup);
// 清理数据
cleanupBtn.addEventListener('click', handleCleanup);
elements.cleanupBtn.addEventListener('click', handleCleanup);
// 添加邮箱服务
addEmailServiceBtn.addEventListener('click', () => {
addServiceModal.classList.add('active');
loadServiceConfigFields(serviceTypeSelect.value);
elements.addEmailServiceBtn.addEventListener('click', () => {
elements.addServiceModal.classList.add('active');
loadServiceConfigFields(elements.serviceType.value);
});
closeServiceModalBtn.addEventListener('click', () => {
addServiceModal.classList.remove('active');
elements.closeServiceModal.addEventListener('click', () => {
elements.addServiceModal.classList.remove('active');
});
cancelAddServiceBtn.addEventListener('click', () => {
addServiceModal.classList.remove('active');
elements.cancelAddService.addEventListener('click', () => {
elements.addServiceModal.classList.remove('active');
});
addServiceModal.addEventListener('click', (e) => {
if (e.target === addServiceModal) {
addServiceModal.classList.remove('active');
elements.addServiceModal.addEventListener('click', (e) => {
if (e.target === elements.addServiceModal) {
elements.addServiceModal.classList.remove('active');
}
});
// 服务类型切换
serviceTypeSelect.addEventListener('change', (e) => {
elements.serviceType.addEventListener('change', (e) => {
loadServiceConfigFields(e.target.value);
});
// 添加服务表单
addServiceForm.addEventListener('submit', handleAddService);
elements.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 ? '收起' : '展开';
if (elements.toggleImportBtn) {
elements.toggleImportBtn.addEventListener('click', () => {
const isHidden = elements.outlookImportBody.style.display === 'none';
elements.outlookImportBody.style.display = isHidden ? 'block' : 'none';
elements.toggleImportBtn.textContent = isHidden ? '收起' : '展开';
});
}
// Outlook 批量导入
if (outlookImportBtn) {
outlookImportBtn.addEventListener('click', handleOutlookBatchImport);
if (elements.outlookImportBtn) {
elements.outlookImportBtn.addEventListener('click', handleOutlookBatchImport);
}
// 清空导入数据
if (clearImportBtn) {
clearImportBtn.addEventListener('click', () => {
outlookImportData.value = '';
importResult.style.display = 'none';
if (elements.clearImportBtn) {
elements.clearImportBtn.addEventListener('click', () => {
elements.outlookImportData.value = '';
elements.importResult.style.display = 'none';
});
}
// 全选/取消全选
if (selectAllCheckbox) {
selectAllCheckbox.addEventListener('change', (e) => {
if (elements.selectAllServices) {
elements.selectAllServices.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();
const data = await api.get('/settings');
// 代理设置
document.getElementById('proxy-enabled').checked = data.proxy?.enabled || false;
@@ -165,61 +156,88 @@ async function loadSettings() {
} catch (error) {
console.error('加载设置失败:', error);
toast.error('加载设置失败');
}
}
// 加载邮箱服务
async function loadEmailServices() {
try {
const response = await fetch(`${API_BASE}/email-services`);
const data = await response.json();
const data = await api.get('/email-services');
renderEmailServices(data.services);
} catch (error) {
console.error('加载邮箱服务失败:', error);
elements.emailServicesTable.innerHTML = `
<tr>
<td colspan="7">
<div class="empty-state">
<div class="empty-state-icon">❌</div>
<div class="empty-state-title">加载失败</div>
</div>
</td>
</tr>
`;
}
}
// 渲染邮箱服务
function renderEmailServices(services) {
if (services.length === 0) {
emailServicesTable.innerHTML = '<tr><td colspan="7" style="text-align: center;">暂无配置</td></tr>';
batchDeleteBtn.style.display = 'none';
elements.emailServicesTable.innerHTML = `
<tr>
<td colspan="7">
<div class="empty-state">
<div class="empty-state-icon">📭</div>
<div class="empty-state-title">暂无配置</div>
<div class="empty-state-description">点击上方"添加服务"按钮添加邮箱服务</div>
</div>
</td>
</tr>
`;
return;
}
emailServicesTable.innerHTML = services.map(service => `
elements.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>
<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>
<span class="status-badge ${service.enabled ? 'active' : 'disabled'}">
${service.enabled ? '已启用' : '已禁用'}
</span>
</td>
<td>${service.priority}</td>
<td>${format.date(service.last_used)}</td>
<td>
<div class="action-buttons">
<button class="btn btn-ghost btn-sm" onclick="testService(${service.id})" title="测试">
🔌
</button>
<button class="btn btn-ghost btn-sm" onclick="toggleService(${service.id}, ${!service.enabled})" title="${service.enabled ? '禁用' : '启用'}">
${service.enabled ? '🔒' : '🔓'}
</button>
<button class="btn btn-ghost btn-sm" onclick="deleteService(${service.id})" title="删除">
🗑️
</button>
</div>
</td>
</tr>
`).join('');
// 更新批量删除按钮状态
updateSelectedServices();
}
// 加载数据库信息
async function loadDatabaseInfo() {
try {
const response = await fetch(`${API_BASE}/settings/database`);
const data = await response.json();
const data = await api.get('/settings/database');
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;
document.getElementById('db-accounts').textContent = format.number(data.accounts_count);
document.getElementById('db-services').textContent = format.number(data.email_services_count);
document.getElementById('db-tasks').textContent = format.number(data.tasks_count);
} catch (error) {
console.error('加载数据库信息失败:', error);
@@ -240,38 +258,25 @@ async function handleSaveProxy(e) {
};
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 || '未知错误'));
}
await api.post('/settings/proxy', data);
toast.success('代理设置已保存');
} catch (error) {
alert('保存失败: ' + error.message);
toast.error('保存失败: ' + error.message);
}
}
// 测试代理
async function handleTestProxy() {
testProxyBtn.disabled = true;
testProxyBtn.textContent = '测试中...';
elements.testProxyBtn.disabled = true;
elements.testProxyBtn.innerHTML = '<span class="loading-spinner"></span> 测试中...';
try {
// 这里应该调用一个测试代理的 API
// 暂时模拟
await new Promise(resolve => setTimeout(resolve, 1000));
alert('代理测试功能待实现');
// TODO: 实现代理测试 API
await new Promise(resolve => setTimeout(resolve, 1500));
toast.info('代理测试功能待实现');
} finally {
testProxyBtn.disabled = false;
testProxyBtn.textContent = '测试连接';
elements.testProxyBtn.disabled = false;
elements.testProxyBtn.textContent = '🔌 测试连接';
}
}
@@ -288,96 +293,68 @@ async function handleSaveRegistration(e) {
};
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 || '未知错误'));
}
await api.post('/settings/registration', data);
toast.success('注册配置已保存');
} catch (error) {
alert('保存失败: ' + error.message);
toast.error('保存失败: ' + error.message);
}
}
// 备份数据库
async function handleBackup() {
backupBtn.disabled = true;
backupBtn.textContent = '备份中...';
elements.backupBtn.disabled = true;
elements.backupBtn.innerHTML = '<span class="loading-spinner"></span> 备份中...';
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 || '未知错误'));
}
const data = await api.post('/settings/database/backup');
toast.success(`备份成功: ${data.backup_path}`);
} catch (error) {
alert('备份失败: ' + error.message);
toast.error('备份失败: ' + error.message);
} finally {
backupBtn.disabled = false;
backupBtn.textContent = '备份数据库';
elements.backupBtn.disabled = false;
elements.backupBtn.textContent = '💾 备份数据库';
}
}
// 清理数据
async function handleCleanup() {
if (!confirm('确定要清理过期数据吗?此操作不可恢复。')) {
return;
}
const confirmed = await confirm('确定要清理过期数据吗?此操作不可恢复。');
if (!confirmed) return;
cleanupBtn.disabled = true;
cleanupBtn.textContent = '清理中...';
elements.cleanupBtn.disabled = true;
elements.cleanupBtn.innerHTML = '<span class="loading-spinner"></span> 清理中...';
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 || '未知错误'));
}
const data = await api.post('/settings/database/cleanup?days=30');
toast.success(data.message);
loadDatabaseInfo();
} catch (error) {
alert('清理失败: ' + error.message);
toast.error('清理失败: ' + error.message);
} finally {
cleanupBtn.disabled = false;
cleanupBtn.textContent = '清理过期数据';
elements.cleanupBtn.disabled = false;
elements.cleanupBtn.textContent = '🧹 清理过期数据';
}
}
// 加载服务配置字段
async function loadServiceConfigFields(serviceType) {
try {
const response = await fetch(`${API_BASE}/email-services/types`);
const data = await response.json();
const data = await api.get('/email-services/types');
const typeInfo = data.types.find(t => t.value === serviceType);
if (!typeInfo) return;
serviceConfigFields.innerHTML = typeInfo.config_fields.map(field => `
if (!typeInfo) {
elements.serviceConfigFields.innerHTML = '';
return;
}
elements.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 || ''}"
placeholder="${field.label}"
${field.required ? 'required' : ''}>
</div>
`).join('');
@@ -391,10 +368,10 @@ async function loadServiceConfigFields(serviceType) {
async function handleAddService(e) {
e.preventDefault();
const formData = new FormData(addServiceForm);
const formData = new FormData(elements.addServiceForm);
const config = {};
serviceConfigFields.querySelectorAll('input').forEach(input => {
elements.serviceConfigFields.querySelectorAll('input').forEach(input => {
config[input.name] = input.value;
});
@@ -407,44 +384,27 @@ async function handleAddService(e) {
};
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 || '未知错误'));
}
await api.post('/email-services', data);
toast.success('邮箱服务已添加');
elements.addServiceModal.classList.remove('active');
elements.addServiceForm.reset();
loadEmailServices();
} catch (error) {
alert('添加失败: ' + error.message);
toast.error('添加失败: ' + error.message);
}
}
// 测试服务
async function testService(id) {
try {
const response = await fetch(`${API_BASE}/email-services/${id}/test`, {
method: 'POST',
});
const data = await response.json();
const data = await api.post(`/email-services/${id}/test`);
if (data.success) {
alert('服务连接正常');
toast.success('服务连接正常');
} else {
alert('服务连接失败: ' + data.message);
toast.warning('服务连接失败: ' + data.message);
}
} catch (error) {
alert('测试失败: ' + error.message);
toast.error('测试失败: ' + error.message);
}
}
@@ -452,62 +412,134 @@ async function testService(id) {
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 || '未知错误'));
}
await api.post(`/email-services/${id}/${endpoint}`);
toast.success(enabled ? '服务已启用' : '服务已禁用');
loadEmailServices();
} catch (error) {
alert('操作失败: ' + error.message);
toast.error('操作失败: ' + error.message);
}
}
// 删除服务
async function deleteService(id) {
if (!confirm('确定要删除此邮箱服务配置吗?')) {
return;
}
const confirmed = await confirm('确定要删除此邮箱服务配置吗?');
if (!confirmed) 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 || '未知错误'));
}
await api.delete(`/email-services/${id}`);
toast.success('服务已删除');
loadEmailServices();
} catch (error) {
alert('删除失败: ' + error.message);
toast.error('删除失败: ' + error.message);
}
}
// 工具函数
// 更新选中的服务
function updateSelectedServices() {
selectedServiceIds.clear();
document.querySelectorAll('.service-checkbox:checked').forEach(cb => {
selectedServiceIds.add(parseInt(cb.dataset.id));
});
}
// Outlook 批量导入
async function handleOutlookBatchImport() {
const data = elements.outlookImportData.value.trim();
if (!data) {
toast.warning('请输入要导入的数据');
return;
}
const enabled = document.getElementById('outlook-import-enabled').checked;
const priority = parseInt(document.getElementById('outlook-import-priority').value) || 0;
// 解析数据
const lines = data.split('\n').filter(line => line.trim() && !line.trim().startsWith('#'));
const accounts = [];
const errors = [];
lines.forEach((line, index) => {
const parts = line.split('----').map(p => p.trim());
if (parts.length < 2) {
errors.push(`${index + 1} 行格式错误`);
return;
}
const account = {
email: parts[0],
password: parts[1],
client_id: parts[2] || null,
refresh_token: parts[3] || null,
enabled: enabled,
priority: priority
};
if (!account.email.includes('@')) {
errors.push(`${index + 1} 行邮箱格式错误: ${account.email}`);
return;
}
accounts.push(account);
});
if (errors.length > 0) {
elements.importResult.style.display = 'block';
elements.importResult.innerHTML = `
<div class="import-errors">${errors.map(e => `<div>${e}</div>`).join('')}</div>
`;
return;
}
elements.outlookImportBtn.disabled = true;
elements.outlookImportBtn.innerHTML = '<span class="loading-spinner"></span> 导入中...';
let successCount = 0;
let failCount = 0;
try {
for (const account of accounts) {
try {
await api.post('/email-services', {
service_type: 'outlook',
name: account.email,
config: {
email: account.email,
password: account.password,
client_id: account.client_id,
refresh_token: account.refresh_token
},
enabled: account.enabled,
priority: account.priority
});
successCount++;
} catch {
failCount++;
}
}
elements.importResult.style.display = 'block';
elements.importResult.innerHTML = `
<div class="import-stats">
<span>✅ 成功: ${successCount}</span>
<span>❌ 失败: ${failCount}</span>
</div>
`;
toast.success(`导入完成,成功 ${successCount}`);
loadEmailServices();
} catch (error) {
toast.error('导入失败: ' + error.message);
} finally {
elements.outlookImportBtn.disabled = false;
elements.outlookImportBtn.textContent = '📥 开始导入';
}
}
// HTML 转义
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');
}