mirror of
https://github.com/cnlimiter/codex-register.git
synced 2026-06-02 14:10:50 +08:00
4
This commit is contained in:
@@ -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');
|
||||
|
||||
124
static/js/app.js
124
static/js/app.js
@@ -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
503
static/js/email_services.js
Normal 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;
|
||||
}
|
||||
@@ -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
495
static/js/utils.js
Normal 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()">×</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;
|
||||
Reference in New Issue
Block a user