This commit is contained in:
cnlimiter
2026-03-14 20:36:03 +08:00
parent 0688f4ca7e
commit 6891b9f11d
22 changed files with 3882 additions and 299 deletions

View File

@@ -21,6 +21,8 @@ const elements = {
filterService: document.getElementById('filter-service'),
searchInput: document.getElementById('search-input'),
refreshBtn: document.getElementById('refresh-btn'),
batchRefreshBtn: document.getElementById('batch-refresh-btn'),
batchValidateBtn: document.getElementById('batch-validate-btn'),
batchDeleteBtn: document.getElementById('batch-delete-btn'),
exportBtn: document.getElementById('export-btn'),
exportMenu: document.getElementById('export-menu'),
@@ -75,6 +77,12 @@ function initEventListeners() {
toast.info('已刷新');
});
// 批量刷新Token
elements.batchRefreshBtn.addEventListener('click', handleBatchRefresh);
// 批量验证Token
elements.batchValidateBtn.addEventListener('click', handleBatchValidate);
// 批量删除
elements.batchDeleteBtn.addEventListener('click', handleBatchDelete);
@@ -173,7 +181,7 @@ async function loadAccounts() {
// 显示加载状态
elements.table.innerHTML = `
<tr>
<td colspan="7">
<td colspan="8">
<div class="empty-state">
<div class="skeleton skeleton-text" style="width: 60%;"></div>
<div class="skeleton skeleton-text" style="width: 80%;"></div>
@@ -209,7 +217,7 @@ async function loadAccounts() {
console.error('加载账号列表失败:', error);
elements.table.innerHTML = `
<tr>
<td colspan="7">
<td colspan="8">
<div class="empty-state">
<div class="empty-state-icon">❌</div>
<div class="empty-state-title">加载失败</div>
@@ -228,7 +236,7 @@ function renderAccounts(accounts) {
if (accounts.length === 0) {
elements.table.innerHTML = `
<tr>
<td colspan="7">
<td colspan="8">
<div class="empty-state">
<div class="empty-state-icon">📭</div>
<div class="empty-state-title">暂无数据</div>
@@ -252,21 +260,30 @@ function renderAccounts(accounts) {
${escapeHtml(account.email)}
</span>
</td>
<td class="password-cell">
${account.password
? `<span class="password-hidden" onclick="togglePassword(this, '${escapeHtml(account.password)}')" title="点击查看">${escapeHtml(account.password.substring(0, 4) + '****')}</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>${format.date(account.last_refresh) || '-'}</td>
<td>
<div class="action-buttons">
<button class="btn btn-ghost btn-sm" onclick="refreshToken(${account.id})" title="刷新Token">
🔄
</button>
<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>
${account.password ? `<button class="btn btn-ghost btn-sm" onclick="copyToClipboard('${escapeHtml(account.password)}')" title="复制密码">🔑</button>` : ''}
<button class="btn btn-ghost btn-sm" onclick="deleteAccount(${account.id}, '${escapeHtml(account.email)}')" title="删除">
🗑️
</button>
@@ -289,6 +306,19 @@ function renderAccounts(accounts) {
});
}
// 切换密码显示
function togglePassword(element, password) {
if (element.dataset.revealed === 'true') {
element.textContent = password.substring(0, 4) + '****';
element.classList.add('password-hidden');
element.dataset.revealed = 'false';
} else {
element.textContent = password;
element.classList.remove('password-hidden');
element.dataset.revealed = 'true';
}
}
// 更新分页
function updatePagination() {
const totalPages = Math.max(1, Math.ceil(totalAccounts / pageSize));
@@ -303,7 +333,74 @@ function updatePagination() {
function updateBatchButtons() {
const count = selectedAccounts.size;
elements.batchDeleteBtn.disabled = count === 0;
elements.batchDeleteBtn.textContent = count > 0 ? `🗑️ 删除选中 (${count})` : '🗑️ 批量删除';
elements.batchRefreshBtn.disabled = count === 0;
elements.batchValidateBtn.disabled = count === 0;
elements.batchDeleteBtn.textContent = count > 0 ? `🗑️ 删除 (${count})` : '🗑️ 批量删除';
elements.batchRefreshBtn.textContent = count > 0 ? `🔄 刷新 (${count})` : '🔄 刷新Token';
elements.batchValidateBtn.textContent = count > 0 ? `✅ 验证 (${count})` : '✅ 验证Token';
}
// 刷新单个账号Token
async function refreshToken(id) {
try {
toast.info('正在刷新Token...');
const result = await api.post(`/accounts/${id}/refresh`);
if (result.success) {
toast.success('Token刷新成功');
loadAccounts();
} else {
toast.error('刷新失败: ' + (result.error || '未知错误'));
}
} catch (error) {
toast.error('刷新失败: ' + error.message);
}
}
// 批量刷新Token
async function handleBatchRefresh() {
if (selectedAccounts.size === 0) return;
const confirmed = await confirm(`确定要刷新选中的 ${selectedAccounts.size} 个账号的Token吗`);
if (!confirmed) return;
elements.batchRefreshBtn.disabled = true;
elements.batchRefreshBtn.textContent = '刷新中...';
try {
const result = await api.post('/accounts/batch-refresh', {
ids: Array.from(selectedAccounts)
});
toast.success(`成功刷新 ${result.success_count} 个,失败 ${result.failed_count}`);
loadAccounts();
} catch (error) {
toast.error('批量刷新失败: ' + error.message);
} finally {
updateBatchButtons();
}
}
// 批量验证Token
async function handleBatchValidate() {
if (selectedAccounts.size === 0) return;
elements.batchValidateBtn.disabled = true;
elements.batchValidateBtn.textContent = '验证中...';
try {
const result = await api.post('/accounts/batch-validate', {
ids: Array.from(selectedAccounts)
});
toast.info(`有效: ${result.valid_count},无效: ${result.invalid_count}`);
loadAccounts();
} catch (error) {
toast.error('批量验证失败: ' + error.message);
} finally {
updateBatchButtons();
}
}
// 查看账号详情
@@ -323,6 +420,15 @@ async function viewAccount(id) {
</button>
</span>
</div>
<div class="info-item">
<span class="label">密码</span>
<span class="value">
${account.password
? `<code style="font-size: 0.75rem;">${escapeHtml(account.password)}</code>
<button class="btn btn-ghost btn-sm" onclick="copyToClipboard('${escapeHtml(account.password)}')" title="复制">📋</button>`
: '-'}
</span>
</div>
<div class="info-item">
<span class="label">邮箱服务</span>
<span class="value">${getServiceTypeText(account.email_service)}</span>
@@ -339,6 +445,10 @@ async function viewAccount(id) {
<span class="label">注册时间</span>
<span class="value">${format.date(account.registered_at)}</span>
</div>
<div class="info-item">
<span class="label">最后刷新</span>
<span class="value">${format.date(account.last_refresh) || '-'}</span>
</div>
<div class="info-item" style="grid-column: span 2;">
<span class="label">Account ID</span>
<span class="value" style="font-size: 0.75rem; word-break: break-all;">
@@ -351,6 +461,12 @@ async function viewAccount(id) {
${escapeHtml(account.workspace_id || '-')}
</span>
</div>
<div class="info-item" style="grid-column: span 2;">
<span class="label">Client ID</span>
<span class="value" style="font-size: 0.75rem; word-break: break-all;">
${escapeHtml(account.client_id || '-')}
</span>
</div>
<div class="info-item" style="grid-column: span 2;">
<span class="label">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;">
@@ -366,6 +482,11 @@ async function viewAccount(id) {
</div>
</div>
</div>
<div style="margin-top: var(--spacing-lg); display: flex; gap: var(--spacing-sm);">
<button class="btn btn-primary" onclick="refreshToken(${id}); elements.detailModal.classList.remove('active');">
🔄 刷新Token
</button>
</div>
`;
elements.detailModal.classList.add('active');

View File

@@ -8,6 +8,7 @@ let currentTask = null;
let currentBatch = null;
let logPollingInterval = null;
let batchPollingInterval = null;
let accountsPollingInterval = null;
let isBatchMode = false;
let availableServices = {
tempmail: { available: true, services: [] },
@@ -19,7 +20,6 @@ let availableServices = {
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'),
@@ -28,8 +28,8 @@ const elements = {
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'),
taskStatusRow: document.getElementById('task-status-row'),
batchProgressSection: document.getElementById('batch-progress-section'),
consoleLog: document.getElementById('console-log'),
clearLogBtn: document.getElementById('clear-log-btn'),
// 任务状态
@@ -39,18 +39,23 @@ const elements = {
taskService: document.getElementById('task-service'),
taskStatusBadge: document.getElementById('task-status-badge'),
// 批量状态
batchProgress: document.getElementById('batch-progress'),
batchProgressText: document.getElementById('batch-progress-text'),
batchProgressPercent: document.getElementById('batch-progress-percent'),
progressBar: document.getElementById('progress-bar'),
batchSuccess: document.getElementById('batch-success'),
batchFailed: document.getElementById('batch-failed'),
batchRemaining: document.getElementById('batch-remaining')
batchRemaining: document.getElementById('batch-remaining'),
// 已注册账号
recentAccountsTable: document.getElementById('recent-accounts-table'),
refreshAccountsBtn: document.getElementById('refresh-accounts-btn')
};
// 初始化
document.addEventListener('DOMContentLoaded', () => {
initEventListeners();
loadSavedProxy();
loadAvailableServices();
loadRecentAccounts();
startAccountsPolling();
});
// 事件监听
@@ -71,18 +76,12 @@ function initEventListeners() {
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) {
// 忽略错误
}
// 刷新账号列表
elements.refreshAccountsBtn.addEventListener('click', () => {
loadRecentAccounts();
toast.info('已刷新');
});
}
// 加载可用的邮箱服务
@@ -143,7 +142,7 @@ function updateEmailServiceOptions() {
const option = document.createElement('option');
option.value = '';
option.textContent = '请先在设置中导入账户';
option.textContent = '请先在邮箱服务页面导入账户';
option.disabled = true;
optgroup.appendChild(option);
@@ -173,7 +172,7 @@ function updateEmailServiceOptions() {
const option = document.createElement('option');
option.value = '';
option.textContent = '请先在设置中添加服务';
option.textContent = '请先在邮箱服务页面添加服务';
option.disabled = true;
optgroup.appendChild(option);
@@ -223,7 +222,6 @@ async function handleStartRegistration(e) {
}
const [emailServiceType, serviceId] = selectedValue.split(':');
const proxy = elements.proxy.value.trim() || null;
// 禁用开始按钮
elements.startBtn.disabled = true;
@@ -232,10 +230,9 @@ async function handleStartRegistration(e) {
// 清空日志
elements.consoleLog.innerHTML = '';
// 构建请求数据
// 构建请求数据(代理从设置中自动获取)
const requestData = {
email_service_type: emailServiceType,
proxy: proxy
email_service_type: emailServiceType
};
// 如果选择了数据库中的服务,传递 service_id
@@ -365,6 +362,8 @@ function startLogPolling(taskUuid) {
if (data.status === 'completed') {
addLog('success', '[成功] 注册成功!');
toast.success('注册成功!');
// 刷新账号列表
loadRecentAccounts();
} else if (data.status === 'failed') {
addLog('error', '[错误] 注册失败');
toast.error('注册失败');
@@ -401,6 +400,8 @@ function startBatchPolling(batchId) {
addLog('info', `[完成] 批量任务完成!成功: ${data.success}, 失败: ${data.failed}`);
if (data.success > 0) {
toast.success(`批量注册完成,成功 ${data.success}`);
// 刷新账号列表
loadRecentAccounts();
} else {
toast.warning('批量注册完成,但没有成功注册任何账号');
}
@@ -421,9 +422,10 @@ function stopBatchPolling() {
// 显示任务状态
function showTaskStatus(task) {
elements.taskStatusCard.style.display = 'block';
elements.batchStatusCard.style.display = 'none';
elements.taskId.textContent = task.task_uuid;
elements.taskStatusRow.style.display = 'grid';
elements.batchProgressSection.style.display = 'none';
elements.taskStatusBadge.style.display = 'inline-flex';
elements.taskId.textContent = task.task_uuid.substring(0, 8) + '...';
elements.taskEmail.textContent = '-';
elements.taskService.textContent = '-';
}
@@ -446,9 +448,11 @@ function updateTaskStatus(status) {
// 显示批量状态
function showBatchStatus(batch) {
elements.batchStatusCard.style.display = 'block';
elements.taskStatusCard.style.display = 'none';
elements.batchProgress.textContent = `0/${batch.count}`;
elements.batchProgressSection.style.display = 'block';
elements.taskStatusRow.style.display = 'none';
elements.taskStatusBadge.style.display = 'none';
elements.batchProgressText.textContent = `0/${batch.count}`;
elements.batchProgressPercent.textContent = '0%';
elements.progressBar.style.width = '0%';
elements.batchSuccess.textContent = '0';
elements.batchFailed.textContent = '0';
@@ -461,8 +465,9 @@ function showBatchStatus(batch) {
// 更新批量进度
function updateBatchProgress(data) {
const progress = (data.completed / data.total * 100).toFixed(0);
elements.batchProgress.textContent = data.progress || `${data.completed}/${data.total}`;
const progress = ((data.completed / data.total) * 100).toFixed(0);
elements.batchProgressText.textContent = `${data.completed}/${data.total}`;
elements.batchProgressPercent.textContent = `${progress}%`;
elements.progressBar.style.width = `${progress}%`;
elements.batchSuccess.textContent = data.success;
elements.batchFailed.textContent = data.failed;
@@ -485,6 +490,63 @@ function updateBatchProgress(data) {
}
}
// 加载最近注册的账号
async function loadRecentAccounts() {
try {
const data = await api.get('/accounts?page=1&page_size=10');
if (data.accounts.length === 0) {
elements.recentAccountsTable.innerHTML = `
<tr>
<td colspan="5">
<div class="empty-state" style="padding: var(--spacing-md);">
<div class="empty-state-icon">📭</div>
<div class="empty-state-title">暂无已注册账号</div>
</div>
</td>
</tr>
`;
return;
}
elements.recentAccountsTable.innerHTML = data.accounts.map(account => `
<tr data-id="${account.id}">
<td>${account.id}</td>
<td>
<span title="${escapeHtml(account.email)}">${escapeHtml(account.email)}</span>
</td>
<td class="password-cell">
${account.password ? `<span class="password-hidden" title="点击查看">${escapeHtml(account.password.substring(0, 8))}...</span>` : '-'}
</td>
<td>
<span class="status-badge ${getStatusClass('account', account.status)}" style="font-size: 0.7rem;">
${getStatusText('account', account.status)}
</span>
</td>
<td>
<div class="action-buttons">
<button class="btn btn-ghost btn-sm" onclick="copyToClipboard('${escapeHtml(account.email)}')" title="复制邮箱">
📋
</button>
${account.password ? `<button class="btn btn-ghost btn-sm" onclick="copyToClipboard('${escapeHtml(account.password)}')" title="复制密码">🔑</button>` : ''}
</div>
</td>
</tr>
`).join('');
} catch (error) {
console.error('加载账号列表失败:', error);
}
}
// 开始账号列表轮询
function startAccountsPolling() {
// 每30秒刷新一次账号列表
accountsPollingInterval = setInterval(() => {
loadRecentAccounts();
}, 30000);
}
// 添加日志
function addLog(type, message) {
const line = document.createElement('div');

503
static/js/email_services.js Normal file
View File

@@ -0,0 +1,503 @@
/**
* 邮箱服务页面 JavaScript
*/
// 状态
let outlookServices = [];
let customServices = [];
let selectedOutlook = new Set();
let selectedCustom = new Set();
// DOM 元素
const elements = {
// 统计
outlookCount: document.getElementById('outlook-count'),
customCount: document.getElementById('custom-count'),
tempmailStatus: document.getElementById('tempmail-status'),
totalEnabled: document.getElementById('total-enabled'),
// Outlook 导入
toggleOutlookImport: document.getElementById('toggle-outlook-import'),
outlookImportBody: document.getElementById('outlook-import-body'),
outlookImportData: document.getElementById('outlook-import-data'),
outlookImportEnabled: document.getElementById('outlook-import-enabled'),
outlookImportPriority: document.getElementById('outlook-import-priority'),
outlookImportBtn: document.getElementById('outlook-import-btn'),
clearImportBtn: document.getElementById('clear-import-btn'),
importResult: document.getElementById('import-result'),
// Outlook 列表
outlookTable: document.getElementById('outlook-accounts-table'),
selectAllOutlook: document.getElementById('select-all-outlook'),
batchDeleteOutlookBtn: document.getElementById('batch-delete-outlook-btn'),
// 自定义域名
customTable: document.getElementById('custom-services-table'),
addCustomBtn: document.getElementById('add-custom-btn'),
selectAllCustom: document.getElementById('select-all-custom'),
// 临时邮箱
tempmailForm: document.getElementById('tempmail-form'),
tempmailApi: document.getElementById('tempmail-api'),
tempmailEnabled: document.getElementById('tempmail-enabled'),
testTempmailBtn: document.getElementById('test-tempmail-btn'),
// 模态框
addCustomModal: document.getElementById('add-custom-modal'),
addCustomForm: document.getElementById('add-custom-form'),
closeCustomModal: document.getElementById('close-custom-modal'),
cancelAddCustom: document.getElementById('cancel-add-custom')
};
// 初始化
document.addEventListener('DOMContentLoaded', () => {
loadStats();
loadOutlookServices();
loadCustomServices();
loadTempmailConfig();
initEventListeners();
});
// 事件监听
function initEventListeners() {
// Outlook 导入展开/收起
elements.toggleOutlookImport.addEventListener('click', () => {
const isHidden = elements.outlookImportBody.style.display === 'none';
elements.outlookImportBody.style.display = isHidden ? 'block' : 'none';
elements.toggleOutlookImport.textContent = isHidden ? '收起' : '展开';
});
// Outlook 导入
elements.outlookImportBtn.addEventListener('click', handleOutlookImport);
elements.clearImportBtn.addEventListener('click', () => {
elements.outlookImportData.value = '';
elements.importResult.style.display = 'none';
});
// Outlook 全选
elements.selectAllOutlook.addEventListener('change', (e) => {
const checkboxes = elements.outlookTable.querySelectorAll('input[type="checkbox"][data-id]');
checkboxes.forEach(cb => {
cb.checked = e.target.checked;
const id = parseInt(cb.dataset.id);
if (e.target.checked) {
selectedOutlook.add(id);
} else {
selectedOutlook.delete(id);
}
});
updateBatchButtons();
});
// Outlook 批量删除
elements.batchDeleteOutlookBtn.addEventListener('click', handleBatchDeleteOutlook);
// 添加自定义域名
elements.addCustomBtn.addEventListener('click', () => {
elements.addCustomModal.classList.add('active');
});
elements.closeCustomModal.addEventListener('click', () => {
elements.addCustomModal.classList.remove('active');
});
elements.cancelAddCustom.addEventListener('click', () => {
elements.addCustomModal.classList.remove('active');
});
elements.addCustomForm.addEventListener('submit', handleAddCustom);
// 自定义域名全选
elements.selectAllCustom.addEventListener('change', (e) => {
const checkboxes = elements.customTable.querySelectorAll('input[type="checkbox"][data-id]');
checkboxes.forEach(cb => {
cb.checked = e.target.checked;
const id = parseInt(cb.dataset.id);
if (e.target.checked) {
selectedCustom.add(id);
} else {
selectedCustom.delete(id);
}
});
});
// 临时邮箱配置
elements.tempmailForm.addEventListener('submit', handleSaveTempmail);
elements.testTempmailBtn.addEventListener('click', handleTestTempmail);
}
// 加载统计信息
async function loadStats() {
try {
const data = await api.get('/email-services/stats');
elements.outlookCount.textContent = data.outlook_count || 0;
elements.customCount.textContent = data.custom_count || 0;
elements.tempmailStatus.textContent = data.tempmail_available ? '可用' : '不可用';
elements.totalEnabled.textContent = data.enabled_count || 0;
} catch (error) {
console.error('加载统计信息失败:', error);
}
}
// 加载 Outlook 服务
async function loadOutlookServices() {
try {
const data = await api.get('/email-services?service_type=outlook');
outlookServices = data.services || [];
if (outlookServices.length === 0) {
elements.outlookTable.innerHTML = `
<tr>
<td colspan="7">
<div class="empty-state">
<div class="empty-state-icon">📭</div>
<div class="empty-state-title">暂无 Outlook 账户</div>
<div class="empty-state-description">请使用上方导入功能添加账户</div>
</div>
</td>
</tr>
`;
return;
}
elements.outlookTable.innerHTML = outlookServices.map(service => `
<tr data-id="${service.id}">
<td>
<input type="checkbox" data-id="${service.id}"
${selectedOutlook.has(service.id) ? 'checked' : ''}>
</td>
<td>${escapeHtml(service.config?.email || service.name)}</td>
<td>
<span class="status-badge ${service.config?.has_oauth ? 'active' : 'pending'}">
${service.config?.has_oauth ? 'OAuth' : '密码'}
</span>
</td>
<td>
<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="toggleService(${service.id}, ${!service.enabled})" title="${service.enabled ? '禁用' : '启用'}">
${service.enabled ? '🔇' : '🔊'}
</button>
<button class="btn btn-ghost btn-sm" onclick="testService(${service.id})" title="测试">
🔌
</button>
<button class="btn btn-ghost btn-sm" onclick="deleteService(${service.id}, '${escapeHtml(service.name)}')" title="删除">
🗑️
</button>
</div>
</td>
</tr>
`).join('');
// 绑定复选框事件
elements.outlookTable.querySelectorAll('input[type="checkbox"][data-id]').forEach(cb => {
cb.addEventListener('change', (e) => {
const id = parseInt(e.target.dataset.id);
if (e.target.checked) {
selectedOutlook.add(id);
} else {
selectedOutlook.delete(id);
}
updateBatchButtons();
});
});
} catch (error) {
console.error('加载 Outlook 服务失败:', error);
elements.outlookTable.innerHTML = `
<tr>
<td colspan="7">
<div class="empty-state">
<div class="empty-state-icon">❌</div>
<div class="empty-state-title">加载失败</div>
</div>
</td>
</tr>
`;
}
}
// 加载自定义域名服务
async function loadCustomServices() {
try {
const data = await api.get('/email-services?service_type=custom_domain');
customServices = data.services || [];
if (customServices.length === 0) {
elements.customTable.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;
}
elements.customTable.innerHTML = customServices.map(service => `
<tr data-id="${service.id}">
<td>
<input type="checkbox" data-id="${service.id}"
${selectedCustom.has(service.id) ? 'checked' : ''}>
</td>
<td>${escapeHtml(service.name)}</td>
<td style="font-size: 0.75rem;">${escapeHtml(service.config?.api_url || '-')}</td>
<td>
<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="toggleService(${service.id}, ${!service.enabled})" title="${service.enabled ? '禁用' : '启用'}">
${service.enabled ? '🔇' : '🔊'}
</button>
<button class="btn btn-ghost btn-sm" onclick="testService(${service.id})" title="测试">
🔌
</button>
<button class="btn btn-ghost btn-sm" onclick="deleteService(${service.id}, '${escapeHtml(service.name)}')" title="删除">
🗑️
</button>
</div>
</td>
</tr>
`).join('');
// 绑定复选框事件
elements.customTable.querySelectorAll('input[type="checkbox"][data-id]').forEach(cb => {
cb.addEventListener('change', (e) => {
const id = parseInt(e.target.dataset.id);
if (e.target.checked) {
selectedCustom.add(id);
} else {
selectedCustom.delete(id);
}
});
});
} catch (error) {
console.error('加载自定义域名服务失败:', error);
}
}
// 加载临时邮箱配置
async function loadTempmailConfig() {
try {
const settings = await api.get('/settings');
if (settings.tempmail) {
elements.tempmailApi.value = settings.tempmail.api_url || '';
elements.tempmailEnabled.checked = settings.tempmail.enabled !== false;
}
} catch (error) {
// 忽略错误
}
}
// Outlook 导入
async function handleOutlookImport() {
const data = elements.outlookImportData.value.trim();
if (!data) {
toast.error('请输入导入数据');
return;
}
elements.outlookImportBtn.disabled = true;
elements.outlookImportBtn.textContent = '导入中...';
try {
const result = await api.post('/email-services/outlook/batch-import', {
data: data,
enabled: elements.outlookImportEnabled.checked,
priority: parseInt(elements.outlookImportPriority.value) || 0
});
elements.importResult.style.display = 'block';
elements.importResult.innerHTML = `
<div class="import-stats">
<span>✅ 成功导入: <strong>${result.success_count || 0}</strong></span>
<span>❌ 失败: <strong>${result.failed_count || 0}</strong></span>
</div>
${result.errors?.length ? `
<div class="import-errors" style="margin-top: var(--spacing-sm);">
<strong>错误详情:</strong>
<ul>
${result.errors.map(e => `<li>${escapeHtml(e)}</li>`).join('')}
</ul>
</div>
` : ''}
`;
if (result.success_count > 0) {
toast.success(`成功导入 ${result.success_count} 个账户`);
loadOutlookServices();
loadStats();
elements.outlookImportData.value = '';
}
} catch (error) {
toast.error('导入失败: ' + error.message);
} finally {
elements.outlookImportBtn.disabled = false;
elements.outlookImportBtn.textContent = '📥 开始导入';
}
}
// 添加自定义域名服务
async function handleAddCustom(e) {
e.preventDefault();
const formData = new FormData(e.target);
const data = {
service_type: 'custom_domain',
name: formData.get('name'),
config: {
api_url: formData.get('api_url'),
api_key: formData.get('api_key'),
domain: formData.get('domain')
},
enabled: formData.get('enabled') === 'on',
priority: parseInt(formData.get('priority')) || 0
};
try {
await api.post('/email-services', data);
toast.success('服务添加成功');
elements.addCustomModal.classList.remove('active');
e.target.reset();
loadCustomServices();
loadStats();
} catch (error) {
toast.error('添加失败: ' + error.message);
}
}
// 切换服务状态
async function toggleService(id, enabled) {
try {
await api.patch(`/email-services/${id}`, { enabled });
toast.success(enabled ? '已启用' : '已禁用');
loadOutlookServices();
loadCustomServices();
loadStats();
} catch (error) {
toast.error('操作失败: ' + error.message);
}
}
// 测试服务
async function testService(id) {
try {
const result = await api.post(`/email-services/${id}/test`);
if (result.success) {
toast.success('测试成功');
} else {
toast.error('测试失败: ' + (result.error || '未知错误'));
}
} catch (error) {
toast.error('测试失败: ' + error.message);
}
}
// 删除服务
async function deleteService(id, name) {
const confirmed = await confirm(`确定要删除 "${name}" 吗?`);
if (!confirmed) return;
try {
await api.delete(`/email-services/${id}`);
toast.success('已删除');
selectedOutlook.delete(id);
selectedCustom.delete(id);
loadOutlookServices();
loadCustomServices();
loadStats();
} catch (error) {
toast.error('删除失败: ' + error.message);
}
}
// 批量删除 Outlook
async function handleBatchDeleteOutlook() {
if (selectedOutlook.size === 0) return;
const confirmed = await confirm(`确定要删除选中的 ${selectedOutlook.size} 个账户吗?`);
if (!confirmed) return;
try {
const result = await api.request('/email-services/outlook/batch', {
method: 'DELETE',
body: Array.from(selectedOutlook)
});
toast.success(`成功删除 ${result.deleted || selectedOutlook.size} 个账户`);
selectedOutlook.clear();
loadOutlookServices();
loadStats();
} catch (error) {
toast.error('删除失败: ' + error.message);
}
}
// 保存临时邮箱配置
async function handleSaveTempmail(e) {
e.preventDefault();
try {
await api.post('/settings/tempmail', {
api_url: elements.tempmailApi.value,
enabled: elements.tempmailEnabled.checked
});
toast.success('配置已保存');
} catch (error) {
toast.error('保存失败: ' + error.message);
}
}
// 测试临时邮箱
async function handleTestTempmail() {
elements.testTempmailBtn.disabled = true;
elements.testTempmailBtn.textContent = '测试中...';
try {
const result = await api.post('/email-services/test-tempmail', {
api_url: elements.tempmailApi.value
});
if (result.success) {
toast.success('临时邮箱连接正常');
} else {
toast.error('连接失败: ' + (result.error || '未知错误'));
}
} catch (error) {
toast.error('测试失败: ' + error.message);
} finally {
elements.testTempmailBtn.disabled = false;
elements.testTempmailBtn.textContent = '🔌 测试连接';
}
}
// 更新批量按钮
function updateBatchButtons() {
const count = selectedOutlook.size;
elements.batchDeleteOutlookBtn.disabled = count === 0;
elements.batchDeleteOutlookBtn.textContent = count > 0 ? `🗑️ 删除选中 (${count})` : '🗑️ 批量删除';
}
// HTML 转义
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}

View File

@@ -28,7 +28,16 @@ const elements = {
outlookImportData: document.getElementById('outlook-import-data'),
importResult: document.getElementById('import-result'),
// 批量操作
selectAllServices: document.getElementById('select-all-services')
selectAllServices: document.getElementById('select-all-services'),
// 代理列表
proxiesTable: document.getElementById('proxies-table'),
addProxyBtn: document.getElementById('add-proxy-btn'),
testAllProxiesBtn: document.getElementById('test-all-proxies-btn'),
addProxyModal: document.getElementById('add-proxy-modal'),
proxyItemForm: document.getElementById('proxy-item-form'),
closeProxyModal: document.getElementById('close-proxy-modal'),
cancelProxyBtn: document.getElementById('cancel-proxy-btn'),
proxyModalTitle: document.getElementById('proxy-modal-title')
};
// 选中的服务 ID
@@ -40,6 +49,7 @@ document.addEventListener('DOMContentLoaded', () => {
loadSettings();
loadEmailServices();
loadDatabaseInfo();
loadProxies();
initEventListeners();
});
@@ -61,47 +71,69 @@ function initTabs() {
// 事件监听
function initEventListeners() {
// 代理表单
elements.proxyForm.addEventListener('submit', handleSaveProxy);
if (elements.proxyForm) {
elements.proxyForm.addEventListener('submit', handleSaveProxy);
}
// 测试代理
elements.testProxyBtn.addEventListener('click', handleTestProxy);
if (elements.testProxyBtn) {
elements.testProxyBtn.addEventListener('click', handleTestProxy);
}
// 注册配置表单
elements.registrationForm.addEventListener('submit', handleSaveRegistration);
if (elements.registrationForm) {
elements.registrationForm.addEventListener('submit', handleSaveRegistration);
}
// 备份数据库
elements.backupBtn.addEventListener('click', handleBackup);
if (elements.backupBtn) {
elements.backupBtn.addEventListener('click', handleBackup);
}
// 清理数据
elements.cleanupBtn.addEventListener('click', handleCleanup);
if (elements.cleanupBtn) {
elements.cleanupBtn.addEventListener('click', handleCleanup);
}
// 添加邮箱服务
elements.addEmailServiceBtn.addEventListener('click', () => {
elements.addServiceModal.classList.add('active');
loadServiceConfigFields(elements.serviceType.value);
});
if (elements.addEmailServiceBtn) {
elements.addEmailServiceBtn.addEventListener('click', () => {
elements.addServiceModal.classList.add('active');
loadServiceConfigFields(elements.serviceType.value);
});
}
elements.closeServiceModal.addEventListener('click', () => {
elements.addServiceModal.classList.remove('active');
});
elements.cancelAddService.addEventListener('click', () => {
elements.addServiceModal.classList.remove('active');
});
elements.addServiceModal.addEventListener('click', (e) => {
if (e.target === elements.addServiceModal) {
if (elements.closeServiceModal) {
elements.closeServiceModal.addEventListener('click', () => {
elements.addServiceModal.classList.remove('active');
}
});
});
}
if (elements.cancelAddService) {
elements.cancelAddService.addEventListener('click', () => {
elements.addServiceModal.classList.remove('active');
});
}
if (elements.addServiceModal) {
elements.addServiceModal.addEventListener('click', (e) => {
if (e.target === elements.addServiceModal) {
elements.addServiceModal.classList.remove('active');
}
});
}
// 服务类型切换
elements.serviceType.addEventListener('change', (e) => {
loadServiceConfigFields(e.target.value);
});
if (elements.serviceType) {
elements.serviceType.addEventListener('change', (e) => {
loadServiceConfigFields(e.target.value);
});
}
// 添加服务表单
elements.addServiceForm.addEventListener('submit', handleAddService);
if (elements.addServiceForm) {
elements.addServiceForm.addEventListener('submit', handleAddService);
}
// Outlook 批量导入展开/折叠
if (elements.toggleImportBtn) {
@@ -133,6 +165,35 @@ function initEventListeners() {
updateSelectedServices();
});
}
// 代理列表相关
if (elements.addProxyBtn) {
elements.addProxyBtn.addEventListener('click', () => openProxyModal());
}
if (elements.testAllProxiesBtn) {
elements.testAllProxiesBtn.addEventListener('click', handleTestAllProxies);
}
if (elements.closeProxyModal) {
elements.closeProxyModal.addEventListener('click', closeProxyModal);
}
if (elements.cancelProxyBtn) {
elements.cancelProxyBtn.addEventListener('click', closeProxyModal);
}
if (elements.addProxyModal) {
elements.addProxyModal.addEventListener('click', (e) => {
if (e.target === elements.addProxyModal) {
closeProxyModal();
}
});
}
if (elements.proxyItemForm) {
elements.proxyItemForm.addEventListener('submit', handleSaveProxyItem);
}
}
// 加载设置
@@ -162,26 +223,34 @@ async function loadSettings() {
// 加载邮箱服务
async function loadEmailServices() {
// 检查元素是否存在
if (!elements.emailServicesTable) return;
try {
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>
`;
if (elements.emailServicesTable) {
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 (!elements.emailServicesTable) return;
if (services.length === 0) {
elements.emailServicesTable.innerHTML = `
<tr>
@@ -271,9 +340,24 @@ async function handleTestProxy() {
elements.testProxyBtn.innerHTML = '<span class="loading-spinner"></span> 测试中...';
try {
// TODO: 实现代理测试 API
await new Promise(resolve => setTimeout(resolve, 1500));
toast.info('代理测试功能待实现');
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,
};
const result = await api.post('/settings/proxy/test', data);
if (result.success) {
toast.success(result.message);
} else {
toast.error(result.message);
}
} catch (error) {
toast.error('测试失败: ' + error.message);
} finally {
elements.testProxyBtn.disabled = false;
elements.testProxyBtn.textContent = '🔌 测试连接';
@@ -543,3 +627,199 @@ function escapeHtml(text) {
div.textContent = text;
return div.innerHTML;
}
// ============================================================================
// 代理列表管理
// ============================================================================
// 加载代理列表
async function loadProxies() {
try {
const data = await api.get('/settings/proxies');
renderProxies(data.proxies);
} catch (error) {
console.error('加载代理列表失败:', error);
elements.proxiesTable.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 renderProxies(proxies) {
if (!proxies || proxies.length === 0) {
elements.proxiesTable.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;
}
elements.proxiesTable.innerHTML = proxies.map(proxy => `
<tr data-proxy-id="${proxy.id}">
<td>${proxy.id}</td>
<td>${escapeHtml(proxy.name)}</td>
<td><span class="badge">${proxy.type.toUpperCase()}</span></td>
<td><code>${escapeHtml(proxy.host)}:${proxy.port}</code></td>
<td>
<span class="status-badge ${proxy.enabled ? 'active' : 'disabled'}">
${proxy.enabled ? '已启用' : '已禁用'}
</span>
</td>
<td>${format.date(proxy.last_used)}</td>
<td>
<div class="action-buttons">
<button class="btn btn-ghost btn-sm" onclick="testProxyItem(${proxy.id})" title="测试">
🔌
</button>
<button class="btn btn-ghost btn-sm" onclick="editProxyItem(${proxy.id})" title="编辑">
✏️
</button>
<button class="btn btn-ghost btn-sm" onclick="toggleProxyItem(${proxy.id}, ${!proxy.enabled})" title="${proxy.enabled ? '禁用' : '启用'}">
${proxy.enabled ? '🔒' : '🔓'}
</button>
<button class="btn btn-ghost btn-sm" onclick="deleteProxyItem(${proxy.id})" title="删除">
🗑️
</button>
</div>
</td>
</tr>
`).join('');
}
// 打开代理模态框
function openProxyModal(proxy = null) {
elements.proxyModalTitle.textContent = proxy ? '编辑代理' : '添加代理';
elements.proxyItemForm.reset();
document.getElementById('proxy-item-id').value = proxy ? proxy.id : '';
if (proxy) {
document.getElementById('proxy-item-name').value = proxy.name || '';
document.getElementById('proxy-item-type').value = proxy.type || 'http';
document.getElementById('proxy-item-host').value = proxy.host || '';
document.getElementById('proxy-item-port').value = proxy.port || '';
document.getElementById('proxy-item-username').value = proxy.username || '';
document.getElementById('proxy-item-password').value = '';
}
elements.addProxyModal.classList.add('active');
}
// 关闭代理模态框
function closeProxyModal() {
elements.addProxyModal.classList.remove('active');
elements.proxyItemForm.reset();
}
// 保存代理
async function handleSaveProxyItem(e) {
e.preventDefault();
const proxyId = document.getElementById('proxy-item-id').value;
const data = {
name: document.getElementById('proxy-item-name').value,
type: document.getElementById('proxy-item-type').value,
host: document.getElementById('proxy-item-host').value,
port: parseInt(document.getElementById('proxy-item-port').value),
username: document.getElementById('proxy-item-username').value || null,
password: document.getElementById('proxy-item-password').value || null,
enabled: true
};
try {
if (proxyId) {
await api.patch(`/settings/proxies/${proxyId}`, data);
toast.success('代理已更新');
} else {
await api.post('/settings/proxies', data);
toast.success('代理已添加');
}
closeProxyModal();
loadProxies();
} catch (error) {
toast.error('保存失败: ' + error.message);
}
}
// 编辑代理
async function editProxyItem(id) {
try {
const proxy = await api.get(`/settings/proxies/${id}`);
openProxyModal(proxy);
} catch (error) {
toast.error('获取代理信息失败');
}
}
// 测试单个代理
async function testProxyItem(id) {
try {
const result = await api.post(`/settings/proxies/${id}/test`);
if (result.success) {
toast.success(result.message);
} else {
toast.error(result.message);
}
} catch (error) {
toast.error('测试失败: ' + error.message);
}
}
// 切换代理状态
async function toggleProxyItem(id, enabled) {
try {
const endpoint = enabled ? 'enable' : 'disable';
await api.post(`/settings/proxies/${id}/${endpoint}`);
toast.success(enabled ? '代理已启用' : '代理已禁用');
loadProxies();
} catch (error) {
toast.error('操作失败: ' + error.message);
}
}
// 删除代理
async function deleteProxyItem(id) {
const confirmed = await confirm('确定要删除此代理吗?');
if (!confirmed) return;
try {
await api.delete(`/settings/proxies/${id}`);
toast.success('代理已删除');
loadProxies();
} catch (error) {
toast.error('删除失败: ' + error.message);
}
}
// 测试所有代理
async function handleTestAllProxies() {
elements.testAllProxiesBtn.disabled = true;
elements.testAllProxiesBtn.innerHTML = '<span class="loading-spinner"></span> 测试中...';
try {
const result = await api.post('/settings/proxies/test-all');
toast.info(`测试完成: 成功 ${result.success}, 失败 ${result.failed}`);
loadProxies();
} catch (error) {
toast.error('测试失败: ' + error.message);
} finally {
elements.testAllProxiesBtn.disabled = false;
elements.testAllProxiesBtn.textContent = '🔌 测试全部';
}
}

495
static/js/utils.js Normal file
View File

@@ -0,0 +1,495 @@
/**
* 通用工具库
* 包含 Toast 通知、主题切换、工具函数等
*/
// ============================================
// Toast 通知系统
// ============================================
class ToastManager {
constructor() {
this.container = null;
this.init();
}
init() {
this.container = document.createElement('div');
this.container.className = 'toast-container';
document.body.appendChild(this.container);
}
show(message, type = 'info', duration = 4000) {
const toast = document.createElement('div');
toast.className = `toast ${type}`;
const icon = this.getIcon(type);
toast.innerHTML = `
<span class="toast-icon">${icon}</span>
<span class="toast-message">${this.escapeHtml(message)}</span>
<button class="toast-close" onclick="this.parentElement.remove()">&times;</button>
`;
this.container.appendChild(toast);
// 自动移除
setTimeout(() => {
toast.style.animation = 'slideOut 0.3s ease forwards';
setTimeout(() => toast.remove(), 300);
}, duration);
return toast;
}
getIcon(type) {
const icons = {
success: '✓',
error: '✕',
warning: '⚠',
info: ''
};
return icons[type] || icons.info;
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
success(message, duration) {
return this.show(message, 'success', duration);
}
error(message, duration) {
return this.show(message, 'error', duration);
}
warning(message, duration) {
return this.show(message, 'warning', duration);
}
info(message, duration) {
return this.show(message, 'info', duration);
}
}
// 全局 Toast 实例
const toast = new ToastManager();
// ============================================
// 主题管理
// ============================================
class ThemeManager {
constructor() {
this.theme = this.loadTheme();
this.applyTheme();
}
loadTheme() {
return localStorage.getItem('theme') || 'light';
}
saveTheme(theme) {
localStorage.setItem('theme', theme);
}
applyTheme() {
document.documentElement.setAttribute('data-theme', this.theme);
this.updateToggleButtons();
}
toggle() {
this.theme = this.theme === 'light' ? 'dark' : 'light';
this.saveTheme(this.theme);
this.applyTheme();
}
setTheme(theme) {
this.theme = theme;
this.saveTheme(theme);
this.applyTheme();
}
updateToggleButtons() {
const buttons = document.querySelectorAll('.theme-toggle');
buttons.forEach(btn => {
btn.innerHTML = this.theme === 'light' ? '🌙' : '☀️';
btn.title = this.theme === 'light' ? '切换到暗色模式' : '切换到亮色模式';
});
}
}
// 全局主题实例
const theme = new ThemeManager();
// ============================================
// 加载状态管理
// ============================================
class LoadingManager {
constructor() {
this.activeLoaders = new Set();
}
show(element, text = '加载中...') {
if (typeof element === 'string') {
element = document.getElementById(element);
}
if (!element) return;
element.classList.add('loading');
element.dataset.originalText = element.innerHTML;
element.innerHTML = `<span class="loading-spinner"></span> ${text}`;
element.disabled = true;
this.activeLoaders.add(element);
}
hide(element) {
if (typeof element === 'string') {
element = document.getElementById(element);
}
if (!element) return;
element.classList.remove('loading');
if (element.dataset.originalText) {
element.innerHTML = element.dataset.originalText;
delete element.dataset.originalText;
}
element.disabled = false;
this.activeLoaders.delete(element);
}
hideAll() {
this.activeLoaders.forEach(element => this.hide(element));
}
}
const loading = new LoadingManager();
// ============================================
// API 请求封装
// ============================================
class ApiClient {
constructor(baseUrl = '/api') {
this.baseUrl = baseUrl;
}
async request(path, options = {}) {
const url = `${this.baseUrl}${path}`;
const defaultOptions = {
headers: {
'Content-Type': 'application/json',
},
};
const finalOptions = { ...defaultOptions, ...options };
if (finalOptions.body && typeof finalOptions.body === 'object') {
finalOptions.body = JSON.stringify(finalOptions.body);
}
try {
const response = await fetch(url, finalOptions);
const data = await response.json().catch(() => ({}));
if (!response.ok) {
const error = new Error(data.detail || `HTTP ${response.status}`);
error.response = response;
error.data = data;
throw error;
}
return data;
} catch (error) {
// 网络错误处理
if (!error.response) {
toast.error('网络连接失败,请检查网络');
}
throw error;
}
}
get(path, options = {}) {
return this.request(path, { ...options, method: 'GET' });
}
post(path, body, options = {}) {
return this.request(path, { ...options, method: 'POST', body });
}
put(path, body, options = {}) {
return this.request(path, { ...options, method: 'PUT', body });
}
delete(path, options = {}) {
return this.request(path, { ...options, method: 'DELETE' });
}
}
const api = new ApiClient();
// ============================================
// 事件委托助手
// ============================================
function delegate(element, eventType, selector, handler) {
element.addEventListener(eventType, (e) => {
const target = e.target.closest(selector);
if (target && element.contains(target)) {
handler.call(target, e, target);
}
});
}
// ============================================
// 防抖和节流
// ============================================
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
function throttle(func, limit) {
let inThrottle;
return function executedFunction(...args) {
if (!inThrottle) {
func(...args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// ============================================
// 格式化工具
// ============================================
const format = {
date(dateStr) {
if (!dateStr) return '-';
const date = new Date(dateStr);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
},
dateShort(dateStr) {
if (!dateStr) return '-';
const date = new Date(dateStr);
return date.toLocaleDateString('zh-CN');
},
relativeTime(dateStr) {
if (!dateStr) return '-';
const date = new Date(dateStr);
const now = new Date();
const diff = now - date;
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (seconds < 60) return '刚刚';
if (minutes < 60) return `${minutes} 分钟前`;
if (hours < 24) return `${hours} 小时前`;
if (days < 7) return `${days} 天前`;
return this.dateShort(dateStr);
},
bytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
},
number(num) {
if (num === null || num === undefined) return '-';
return num.toLocaleString('zh-CN');
}
};
// ============================================
// 状态映射
// ============================================
const statusMap = {
account: {
active: { text: '活跃', class: 'active' },
expired: { text: '过期', class: 'expired' },
banned: { text: '封禁', class: 'banned' },
failed: { text: '失败', class: 'failed' }
},
task: {
pending: { text: '等待中', class: 'pending' },
running: { text: '运行中', class: 'running' },
completed: { text: '已完成', class: 'completed' },
failed: { text: '失败', class: 'failed' },
cancelled: { text: '已取消', class: 'disabled' }
},
service: {
tempmail: 'Tempmail.lol',
outlook: 'Outlook',
custom_domain: '自定义域名'
}
};
function getStatusText(type, status) {
return statusMap[type]?.[status]?.text || status;
}
function getStatusClass(type, status) {
return statusMap[type]?.[status]?.class || '';
}
function getServiceTypeText(type) {
return statusMap.service[type] || type;
}
// ============================================
// 确认对话框
// ============================================
function confirm(message, title = '确认操作') {
return new Promise((resolve) => {
const modal = document.createElement('div');
modal.className = 'modal active';
modal.innerHTML = `
<div class="modal-content" style="max-width: 400px;">
<div class="modal-header">
<h3>${title}</h3>
</div>
<div class="modal-body">
<p style="margin-bottom: var(--spacing-lg);">${message}</p>
<div class="form-actions" style="margin-top: 0; padding-top: 0; border-top: none;">
<button class="btn btn-secondary" id="confirm-cancel">取消</button>
<button class="btn btn-danger" id="confirm-ok">确认</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
const cancelBtn = modal.querySelector('#confirm-cancel');
const okBtn = modal.querySelector('#confirm-ok');
cancelBtn.onclick = () => {
modal.remove();
resolve(false);
};
okBtn.onclick = () => {
modal.remove();
resolve(true);
};
modal.onclick = (e) => {
if (e.target === modal) {
modal.remove();
resolve(false);
}
};
});
}
// ============================================
// 复制到剪贴板
// ============================================
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
toast.success('已复制到剪贴板');
return true;
} catch (err) {
toast.error('复制失败');
return false;
}
}
// ============================================
// 本地存储助手
// ============================================
const storage = {
get(key, defaultValue = null) {
try {
const value = localStorage.getItem(key);
return value ? JSON.parse(value) : defaultValue;
} catch {
return defaultValue;
}
},
set(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch {
return false;
}
},
remove(key) {
localStorage.removeItem(key);
}
};
// ============================================
// 页面初始化
// ============================================
document.addEventListener('DOMContentLoaded', () => {
// 初始化主题
theme.applyTheme();
// 全局键盘快捷键
document.addEventListener('keydown', (e) => {
// Ctrl/Cmd + K: 聚焦搜索
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
const searchInput = document.querySelector('#search-input, [type="search"]');
if (searchInput) searchInput.focus();
}
// Escape: 关闭模态框
if (e.key === 'Escape') {
const activeModal = document.querySelector('.modal.active');
if (activeModal) activeModal.classList.remove('active');
}
});
});
// 导出全局对象
window.toast = toast;
window.theme = theme;
window.loading = loading;
window.api = api;
window.format = format;
window.confirm = confirm;
window.copyToClipboard = copyToClipboard;
window.storage = storage;
window.delegate = delegate;
window.debounce = debounce;
window.throttle = throttle;
window.getStatusText = getStatusText;
window.getStatusClass = getStatusClass;
window.getServiceTypeText = getServiceTypeText;