/** * 账号管理页面 JavaScript * 使用 utils.js 中的工具库 */ // 状态 let currentPage = 1; let pageSize = 20; let totalAccounts = 0; let selectedAccounts = new Set(); let isLoading = false; let selectAllPages = false; // 是否选中了全部页 let currentFilters = { status: '', email_service: '', search: '' }; // 当前筛选条件 const refreshingAccountIds = new Set(); let isBatchValidating = false; // DOM 元素 const elements = { table: document.getElementById('accounts-table'), totalAccounts: document.getElementById('total-accounts'), activeAccounts: document.getElementById('active-accounts'), expiredAccounts: document.getElementById('expired-accounts'), failedAccounts: document.getElementById('failed-accounts'), filterStatus: document.getElementById('filter-status'), filterService: document.getElementById('filter-service'), searchInput: document.getElementById('search-input'), refreshBtn: document.getElementById('refresh-btn'), batchRefreshBtn: document.getElementById('batch-refresh-btn'), batchValidateBtn: document.getElementById('batch-validate-btn'), batchUploadBtn: document.getElementById('batch-upload-btn'), batchCheckSubBtn: document.getElementById('batch-check-sub-btn'), batchDeleteBtn: document.getElementById('batch-delete-btn'), exportBtn: document.getElementById('export-btn'), exportMenu: document.getElementById('export-menu'), selectAll: document.getElementById('select-all'), prevPage: document.getElementById('prev-page'), nextPage: document.getElementById('next-page'), pageInfo: document.getElementById('page-info'), detailModal: document.getElementById('detail-modal'), modalBody: document.getElementById('modal-body'), closeModal: document.getElementById('close-modal') }; // 初始化 document.addEventListener('DOMContentLoaded', () => { loadStats(); loadAccounts(); initEventListeners(); updateBatchButtons(); // 初始化按钮状态 renderSelectAllBanner(); }); // 事件监听 function initEventListeners() { // 筛选 elements.filterStatus.addEventListener('change', () => { currentPage = 1; resetSelectAllPages(); loadAccounts(); }); elements.filterService.addEventListener('change', () => { currentPage = 1; resetSelectAllPages(); loadAccounts(); }); // 搜索(防抖) elements.searchInput.addEventListener('input', debounce(() => { currentPage = 1; resetSelectAllPages(); loadAccounts(); }, 300)); // 快捷键聚焦搜索 elements.searchInput.addEventListener('keydown', (e) => { if (e.key === 'Escape') { elements.searchInput.blur(); elements.searchInput.value = ''; resetSelectAllPages(); loadAccounts(); } }); // 刷新 elements.refreshBtn.addEventListener('click', () => { loadStats(); loadAccounts(); toast.info('已刷新'); }); // 批量刷新Token elements.batchRefreshBtn.addEventListener('click', handleBatchRefresh); // 批量验证Token elements.batchValidateBtn.addEventListener('click', handleBatchValidate); // 批量检测订阅 elements.batchCheckSubBtn.addEventListener('click', handleBatchCheckSubscription); // 上传下拉菜单 const uploadMenu = document.getElementById('upload-menu'); elements.batchUploadBtn.addEventListener('click', (e) => { e.stopPropagation(); uploadMenu.classList.toggle('active'); }); document.getElementById('batch-upload-cpa-item').addEventListener('click', (e) => { e.preventDefault(); uploadMenu.classList.remove('active'); handleBatchUploadCpa(); }); document.getElementById('batch-upload-sub2api-item').addEventListener('click', (e) => { e.preventDefault(); uploadMenu.classList.remove('active'); handleBatchUploadSub2Api(); }); document.getElementById('batch-upload-tm-item').addEventListener('click', (e) => { e.preventDefault(); uploadMenu.classList.remove('active'); handleBatchUploadTm(); }); // 批量删除 elements.batchDeleteBtn.addEventListener('click', handleBatchDelete); // 全选(当前页) elements.selectAll.addEventListener('change', (e) => { const checkboxes = elements.table.querySelectorAll('input[type="checkbox"][data-id]'); checkboxes.forEach(cb => { cb.checked = e.target.checked; const id = parseInt(cb.dataset.id); if (e.target.checked) { selectedAccounts.add(id); } else { selectedAccounts.delete(id); } }); if (!e.target.checked) { selectAllPages = false; } updateBatchButtons(); renderSelectAllBanner(); }); // 分页 elements.prevPage.addEventListener('click', () => { if (currentPage > 1 && !isLoading) { currentPage--; loadAccounts(); } }); elements.nextPage.addEventListener('click', () => { const totalPages = Math.ceil(totalAccounts / pageSize); if (currentPage < totalPages && !isLoading) { currentPage++; loadAccounts(); } }); // 导出 elements.exportBtn.addEventListener('click', (e) => { e.stopPropagation(); elements.exportMenu.classList.toggle('active'); }); delegate(elements.exportMenu, 'click', '.dropdown-item', (e, target) => { e.preventDefault(); const format = target.dataset.format; exportAccounts(format); elements.exportMenu.classList.remove('active'); }); // 关闭模态框 elements.closeModal.addEventListener('click', () => { elements.detailModal.classList.remove('active'); }); elements.detailModal.addEventListener('click', (e) => { if (e.target === elements.detailModal) { elements.detailModal.classList.remove('active'); } }); // 点击其他地方关闭下拉菜单 document.addEventListener('click', () => { elements.exportMenu.classList.remove('active'); uploadMenu.classList.remove('active'); document.querySelectorAll('#accounts-table .dropdown-menu.active').forEach(m => m.classList.remove('active')); }); } // 加载统计信息 async function loadStats() { try { const data = await api.get('/accounts/stats/summary'); elements.totalAccounts.textContent = format.number(data.total || 0); elements.activeAccounts.textContent = format.number(data.by_status?.active || 0); elements.expiredAccounts.textContent = format.number(data.by_status?.expired || 0); elements.failedAccounts.textContent = format.number(data.by_status?.failed || 0); // 添加动画效果 animateValue(elements.totalAccounts, data.total || 0); } catch (error) { console.error('加载统计信息失败:', error); } } // 数字动画 function animateValue(element, value) { element.style.transition = 'transform 0.2s ease'; element.style.transform = 'scale(1.1)'; setTimeout(() => { element.style.transform = 'scale(1)'; }, 200); } // 加载账号列表 async function loadAccounts() { if (isLoading) return; isLoading = true; // 显示加载状态 elements.table.innerHTML = `
`; // 记录当前筛选条件 currentFilters.status = elements.filterStatus.value; currentFilters.email_service = elements.filterService.value; currentFilters.search = elements.searchInput.value.trim(); const params = new URLSearchParams({ page: currentPage, page_size: pageSize, }); if (currentFilters.status) { params.append('status', currentFilters.status); } if (currentFilters.email_service) { params.append('email_service', currentFilters.email_service); } if (currentFilters.search) { params.append('search', currentFilters.search); } try { const data = await api.get(`/accounts?${params}`); totalAccounts = data.total; renderAccounts(data.accounts); updatePagination(); } catch (error) { console.error('加载账号列表失败:', error); elements.table.innerHTML = `
加载失败
请检查网络连接后重试
`; } finally { isLoading = false; } } // 渲染账号列表 function renderAccounts(accounts) { if (accounts.length === 0) { elements.table.innerHTML = `
📭
暂无数据
没有找到符合条件的账号记录
`; return; } elements.table.innerHTML = accounts.map(account => ` ${account.id} ${escapeHtml(account.email)} ${account.password ? ` ${escapeHtml(account.password.substring(0, 4) + '****')} ` : '-'} ${getServiceTypeText(account.email_service)} ${getStatusIcon(account.status)}
${account.cpa_uploaded ? `` : `-`}
${account.subscription_type ? `${account.subscription_type}` : `-`}
${format.date(account.last_refresh) || '-'}
`).join(''); // 绑定复选框事件 elements.table.querySelectorAll('input[type="checkbox"][data-id]').forEach(cb => { cb.addEventListener('change', (e) => { const id = parseInt(e.target.dataset.id); if (e.target.checked) { selectedAccounts.add(id); } else { selectedAccounts.delete(id); selectAllPages = false; } // 同步全选框状态 const allChecked = elements.table.querySelectorAll('input[type="checkbox"][data-id]'); const checkedCount = elements.table.querySelectorAll('input[type="checkbox"][data-id]:checked').length; elements.selectAll.checked = allChecked.length > 0 && checkedCount === allChecked.length; elements.selectAll.indeterminate = checkedCount > 0 && checkedCount < allChecked.length; updateBatchButtons(); renderSelectAllBanner(); }); }); // 绑定复制邮箱按钮 elements.table.querySelectorAll('.copy-email-btn').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); copyToClipboard(btn.dataset.email); }); }); // 绑定复制密码按钮 elements.table.querySelectorAll('.copy-pwd-btn').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); copyToClipboard(btn.dataset.pwd); }); }); // 渲染后同步全选框状态 const allCbs = elements.table.querySelectorAll('input[type="checkbox"][data-id]'); const checkedCbs = elements.table.querySelectorAll('input[type="checkbox"][data-id]:checked'); elements.selectAll.checked = allCbs.length > 0 && checkedCbs.length === allCbs.length; elements.selectAll.indeterminate = checkedCbs.length > 0 && checkedCbs.length < allCbs.length; renderSelectAllBanner(); } // 切换密码显示 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)); elements.prevPage.disabled = currentPage <= 1; elements.nextPage.disabled = currentPage >= totalPages; elements.pageInfo.textContent = `第 ${currentPage} 页 / 共 ${totalPages} 页`; } // 重置全选所有页状态 function resetSelectAllPages() { selectAllPages = false; selectedAccounts.clear(); updateBatchButtons(); renderSelectAllBanner(); } // 构建批量请求体(含 select_all 和筛选参数) function buildBatchPayload(extraFields = {}) { if (selectAllPages) { return { ids: [], select_all: true, status_filter: currentFilters.status || null, email_service_filter: currentFilters.email_service || null, search_filter: currentFilters.search || null, ...extraFields }; } return { ids: Array.from(selectedAccounts), ...extraFields }; } // 获取有效选中数量(select_all 时用总数) function getEffectiveCount() { return selectAllPages ? totalAccounts : selectedAccounts.size; } // 渲染全选横幅 function renderSelectAllBanner() { let banner = document.getElementById('select-all-banner'); const totalPages = Math.ceil(totalAccounts / pageSize); const currentPageSize = elements.table.querySelectorAll('input[type="checkbox"][data-id]').length; const checkedOnPage = elements.table.querySelectorAll('input[type="checkbox"][data-id]:checked').length; const allPageSelected = currentPageSize > 0 && checkedOnPage === currentPageSize; // 只在全选了当前页且有多页时显示横幅 if (!allPageSelected || totalPages <= 1 || totalAccounts <= pageSize) { if (banner) banner.remove(); return; } if (!banner) { banner = document.createElement('div'); banner.id = 'select-all-banner'; banner.style.cssText = 'background:var(--primary-light,#e8f0fe);color:var(--primary-color,#1a73e8);padding:8px 16px;text-align:center;font-size:0.875rem;border-bottom:1px solid var(--border-color);'; const tableContainer = document.querySelector('.table-container'); if (tableContainer) tableContainer.insertAdjacentElement('beforebegin', banner); } if (selectAllPages) { banner.innerHTML = `已选中全部 ${totalAccounts} 条记录。`; } else { banner.innerHTML = `当前页已全选 ${checkedOnPage} 条。`; } } // 选中所有页 function selectAllPagesAction() { selectAllPages = true; updateBatchButtons(); renderSelectAllBanner(); } // 更新批量操作按钮 function updateBatchButtons() { const count = getEffectiveCount(); elements.batchDeleteBtn.disabled = count === 0; elements.batchRefreshBtn.disabled = count === 0; elements.batchValidateBtn.disabled = count === 0; elements.batchUploadBtn.disabled = count === 0; elements.batchCheckSubBtn.disabled = count === 0; elements.exportBtn.disabled = count === 0; elements.batchDeleteBtn.textContent = count > 0 ? `🗑️ 删除 (${count})` : '🗑️ 批量删除'; elements.batchRefreshBtn.textContent = count > 0 ? `🔄 刷新 (${count})` : '🔄 刷新Token'; elements.batchValidateBtn.textContent = count > 0 ? `✅ 验证 (${count})` : '✅ 验证Token'; elements.batchUploadBtn.textContent = count > 0 ? `☁️ 上传 (${count})` : '☁️ 上传'; elements.batchCheckSubBtn.textContent = count > 0 ? `🔍 检测 (${count})` : '🔍 检测订阅'; } // 刷新单个账号Token async function refreshToken(id) { if (refreshingAccountIds.has(id)) { toast.info('该账号正在刷新,请稍候...'); return; } refreshingAccountIds.add(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); } finally { refreshingAccountIds.delete(id); } } // 批量刷新Token async function handleBatchRefresh() { const count = getEffectiveCount(); if (count === 0) return; const confirmed = await confirm(`确定要刷新选中的 ${count} 个账号的Token吗?`); if (!confirmed) return; elements.batchRefreshBtn.disabled = true; elements.batchRefreshBtn.textContent = '刷新中...'; try { const result = await api.post('/accounts/batch-refresh', buildBatchPayload()); toast.success(`成功刷新 ${result.success_count} 个,失败 ${result.failed_count} 个`); loadAccounts(); } catch (error) { toast.error('批量刷新失败: ' + error.message); } finally { updateBatchButtons(); } } // 批量验证Token async function handleBatchValidate() { if (getEffectiveCount() === 0) return; if (isBatchValidating) { toast.info('批量验证进行中,请稍候...'); return; } isBatchValidating = true; elements.batchValidateBtn.disabled = true; elements.batchValidateBtn.textContent = '验证中...'; try { const result = await api.post('/accounts/batch-validate', buildBatchPayload(), { timeoutMs: 120000 }); toast.info(`有效: ${result.valid_count},无效: ${result.invalid_count}`); loadAccounts(); } catch (error) { toast.error('批量验证失败: ' + error.message); } finally { isBatchValidating = false; updateBatchButtons(); } } // 查看账号详情 async function viewAccount(id) { try { const account = await api.get(`/accounts/${id}`); const tokens = await api.get(`/accounts/${id}/tokens`); elements.modalBody.innerHTML = `
邮箱 ${escapeHtml(account.email)}
密码 ${account.password ? `${escapeHtml(account.password)} ` : '-'}
邮箱服务 ${getServiceTypeText(account.email_service)}
状态 ${getStatusText('account', account.status)}
注册时间 ${format.date(account.registered_at)}
最后刷新 ${format.date(account.last_refresh) || '-'}
Account ID ${escapeHtml(account.account_id || '-')}
Workspace ID ${escapeHtml(account.workspace_id || '-')}
Client ID ${escapeHtml(account.client_id || '-')}
Access Token
${escapeHtml(tokens.access_token || '-')} ${tokens.access_token ? `` : ''}
Refresh Token
${escapeHtml(tokens.refresh_token || '-')} ${tokens.refresh_token ? `` : ''}
Cookies(支付用)
`; elements.detailModal.classList.add('active'); } catch (error) { toast.error('加载账号详情失败: ' + error.message); } } // 复制邮箱 function copyEmail(email) { copyToClipboard(email); } // 删除账号 async function deleteAccount(id, email) { const confirmed = await confirm(`确定要删除账号 ${email} 吗?此操作不可恢复。`); if (!confirmed) return; try { await api.delete(`/accounts/${id}`); toast.success('账号已删除'); selectedAccounts.delete(id); loadStats(); loadAccounts(); } catch (error) { toast.error('删除失败: ' + error.message); } } // 批量删除 async function handleBatchDelete() { const count = getEffectiveCount(); if (count === 0) return; const confirmed = await confirm(`确定要删除选中的 ${count} 个账号吗?此操作不可恢复。`); if (!confirmed) return; try { const result = await api.post('/accounts/batch-delete', buildBatchPayload()); toast.success(`成功删除 ${result.deleted_count} 个账号`); selectedAccounts.clear(); selectAllPages = false; loadStats(); loadAccounts(); } catch (error) { toast.error('删除失败: ' + error.message); } } // 导出账号 async function exportAccounts(format) { const count = getEffectiveCount(); if (count === 0) { toast.warning('请先选择要导出的账号'); return; } toast.info(`正在导出 ${count} 个账号...`); try { const response = await fetch('/api/accounts/export/' + format, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(buildBatchPayload()) }); if (!response.ok) { throw new Error(`导出失败: HTTP ${response.status}`); } // 获取文件内容 const blob = await response.blob(); // 从 Content-Disposition 获取文件名 const disposition = response.headers.get('Content-Disposition'); let filename = `accounts_${Date.now()}.${(format === 'cpa' || format === 'sub2api') ? 'json' : format}`; if (disposition) { const match = disposition.match(/filename=(.+)/); if (match) { filename = match[1]; } } // 创建下载链接 const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); a.remove(); toast.success('导出成功'); } catch (error) { console.error('导出失败:', error); toast.error('导出失败: ' + error.message); } } // HTML 转义 function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // ============== CPA 服务选择 ============== // 弹出 CPA 服务选择框,返回 Promise<{cpa_service_id: number|null}|null> // null 表示用户取消,{cpa_service_id: null} 表示使用全局配置 function selectCpaService() { return new Promise(async (resolve) => { const modal = document.getElementById('cpa-service-modal'); const listEl = document.getElementById('cpa-service-list'); const closeBtn = document.getElementById('close-cpa-modal'); const cancelBtn = document.getElementById('cancel-cpa-modal-btn'); const globalBtn = document.getElementById('cpa-use-global-btn'); // 加载服务列表 listEl.innerHTML = '
加载中...
'; modal.classList.add('active'); let services = []; try { services = await api.get('/cpa-services?enabled=true'); } catch (e) { services = []; } if (services.length === 0) { listEl.innerHTML = '
暂无已启用的 CPA 服务,将使用全局配置
'; } else { listEl.innerHTML = services.map(s => `
${escapeHtml(s.name)}
${escapeHtml(s.api_url)}
选择
`).join(''); listEl.querySelectorAll('.cpa-service-item').forEach(item => { item.addEventListener('mouseenter', () => item.style.background = 'var(--surface-hover)'); item.addEventListener('mouseleave', () => item.style.background = ''); item.addEventListener('click', () => { cleanup(); resolve({ cpa_service_id: parseInt(item.dataset.id) }); }); }); } function cleanup() { modal.classList.remove('active'); closeBtn.removeEventListener('click', onCancel); cancelBtn.removeEventListener('click', onCancel); globalBtn.removeEventListener('click', onGlobal); } function onCancel() { cleanup(); resolve(null); } function onGlobal() { cleanup(); resolve({ cpa_service_id: null }); } closeBtn.addEventListener('click', onCancel); cancelBtn.addEventListener('click', onCancel); globalBtn.addEventListener('click', onGlobal); }); } // 统一上传入口:弹出目标选择 async function uploadAccount(id) { const targets = [ { label: '☁️ 上传到 CPA', value: 'cpa' }, { label: '🔗 上传到 Sub2API', value: 'sub2api' }, { label: '🚀 上传到 Team Manager', value: 'tm' }, ]; const choice = await new Promise((resolve) => { const modal = document.createElement('div'); modal.className = 'modal active'; modal.innerHTML = ` `; document.body.appendChild(modal); modal.querySelector('#_upload-close').addEventListener('click', () => { modal.remove(); resolve(null); }); modal.addEventListener('click', (e) => { if (e.target === modal) { modal.remove(); resolve(null); } }); modal.querySelectorAll('button[data-val]').forEach(btn => { btn.addEventListener('click', () => { modal.remove(); resolve(btn.dataset.val); }); }); }); if (!choice) return; if (choice === 'cpa') return uploadToCpa(id); if (choice === 'sub2api') return uploadToSub2Api(id); if (choice === 'tm') return uploadToTm(id); } // 上传单个账号到CPA async function uploadToCpa(id) { const choice = await selectCpaService(); if (choice === null) return; // 用户取消 try { toast.info('正在上传到CPA...'); const payload = {}; if (choice.cpa_service_id != null) payload.cpa_service_id = choice.cpa_service_id; const result = await api.post(`/accounts/${id}/upload-cpa`, payload); if (result.success) { toast.success('上传成功'); loadAccounts(); } else { toast.error('上传失败: ' + (result.error || '未知错误')); } } catch (error) { toast.error('上传失败: ' + error.message); } } // 批量上传到CPA async function handleBatchUploadCpa() { const count = getEffectiveCount(); if (count === 0) return; const choice = await selectCpaService(); if (choice === null) return; // 用户取消 const confirmed = await confirm(`确定要将选中的 ${count} 个账号上传到CPA吗?`); if (!confirmed) return; elements.batchUploadBtn.disabled = true; elements.batchUploadBtn.textContent = '上传中...'; try { const payload = buildBatchPayload(); if (choice.cpa_service_id != null) payload.cpa_service_id = choice.cpa_service_id; const result = await api.post('/accounts/batch-upload-cpa', payload); let message = `成功: ${result.success_count}`; if (result.failed_count > 0) message += `, 失败: ${result.failed_count}`; if (result.skipped_count > 0) message += `, 跳过: ${result.skipped_count}`; toast.success(message); loadAccounts(); } catch (error) { toast.error('批量上传失败: ' + error.message); } finally { updateBatchButtons(); } } // ============== 订阅状态 ============== // 手动标记订阅类型 async function markSubscription(id) { const type = prompt('请输入订阅类型 (plus / team / free):', 'plus'); if (!type) return; if (!['plus', 'team', 'free'].includes(type.trim().toLowerCase())) { toast.error('无效的订阅类型,请输入 plus、team 或 free'); return; } try { await api.post(`/payment/accounts/${id}/mark-subscription`, { subscription_type: type.trim().toLowerCase() }); toast.success('订阅状态已更新'); loadAccounts(); } catch (e) { toast.error('标记失败: ' + e.message); } } // 批量检测订阅状态 async function handleBatchCheckSubscription() { const count = getEffectiveCount(); if (count === 0) return; const confirmed = await confirm(`确定要检测选中的 ${count} 个账号的订阅状态吗?`); if (!confirmed) return; elements.batchCheckSubBtn.disabled = true; elements.batchCheckSubBtn.textContent = '检测中...'; try { const result = await api.post('/payment/accounts/batch-check-subscription', buildBatchPayload()); let message = `成功: ${result.success_count}`; if (result.failed_count > 0) message += `, 失败: ${result.failed_count}`; toast.success(message); loadAccounts(); } catch (e) { toast.error('批量检测失败: ' + e.message); } finally { updateBatchButtons(); } } // ============== Sub2API 上传 ============== // 弹出 Sub2API 服务选择框,返回 Promise<{service_id: number|null}|null> // null 表示用户取消,{service_id: null} 表示自动选择 function selectSub2ApiService() { return new Promise(async (resolve) => { const modal = document.getElementById('sub2api-service-modal'); const listEl = document.getElementById('sub2api-service-list'); const closeBtn = document.getElementById('close-sub2api-modal'); const cancelBtn = document.getElementById('cancel-sub2api-modal-btn'); const autoBtn = document.getElementById('sub2api-use-auto-btn'); listEl.innerHTML = '
加载中...
'; modal.classList.add('active'); let services = []; try { services = await api.get('/sub2api-services?enabled=true'); } catch (e) { services = []; } if (services.length === 0) { listEl.innerHTML = '
暂无已启用的 Sub2API 服务,将自动选择第一个
'; } else { listEl.innerHTML = services.map(s => `
${escapeHtml(s.name)}
${escapeHtml(s.api_url)}
选择
`).join(''); listEl.querySelectorAll('.sub2api-service-item').forEach(item => { item.addEventListener('mouseenter', () => item.style.background = 'var(--surface-hover)'); item.addEventListener('mouseleave', () => item.style.background = ''); item.addEventListener('click', () => { cleanup(); resolve({ service_id: parseInt(item.dataset.id) }); }); }); } function cleanup() { modal.classList.remove('active'); closeBtn.removeEventListener('click', onCancel); cancelBtn.removeEventListener('click', onCancel); autoBtn.removeEventListener('click', onAuto); } function onCancel() { cleanup(); resolve(null); } function onAuto() { cleanup(); resolve({ service_id: null }); } closeBtn.addEventListener('click', onCancel); cancelBtn.addEventListener('click', onCancel); autoBtn.addEventListener('click', onAuto); }); } // 批量上传到 Sub2API async function handleBatchUploadSub2Api() { const count = getEffectiveCount(); if (count === 0) return; const choice = await selectSub2ApiService(); if (choice === null) return; // 用户取消 const confirmed = await confirm(`确定要将选中的 ${count} 个账号上传到 Sub2API 吗?`); if (!confirmed) return; elements.batchUploadBtn.disabled = true; elements.batchUploadBtn.textContent = '上传中...'; try { const payload = buildBatchPayload(); if (choice.service_id != null) payload.service_id = choice.service_id; const result = await api.post('/accounts/batch-upload-sub2api', payload); let message = `成功: ${result.success_count}`; if (result.failed_count > 0) message += `, 失败: ${result.failed_count}`; if (result.skipped_count > 0) message += `, 跳过: ${result.skipped_count}`; toast.success(message); loadAccounts(); } catch (error) { toast.error('批量上传失败: ' + error.message); } finally { updateBatchButtons(); } } // ============== Team Manager 上传 ============== // 上传单账号到 Sub2API async function uploadToSub2Api(id) { const choice = await selectSub2ApiService(); if (choice === null) return; try { toast.info('正在上传到 Sub2API...'); const payload = {}; if (choice.service_id != null) payload.service_id = choice.service_id; const result = await api.post(`/accounts/${id}/upload-sub2api`, payload); if (result.success) { toast.success('上传成功'); loadAccounts(); } else { toast.error('上传失败: ' + (result.error || result.message || '未知错误')); } } catch (e) { toast.error('上传失败: ' + e.message); } } // 弹出 Team Manager 服务选择框,返回 Promise<{service_id: number|null}|null> // null 表示用户取消,{service_id: null} 表示自动选择 function selectTmService() { return new Promise(async (resolve) => { const modal = document.getElementById('tm-service-modal'); const listEl = document.getElementById('tm-service-list'); const closeBtn = document.getElementById('close-tm-modal'); const cancelBtn = document.getElementById('cancel-tm-modal-btn'); const autoBtn = document.getElementById('tm-use-auto-btn'); listEl.innerHTML = '
加载中...
'; modal.classList.add('active'); let services = []; try { services = await api.get('/tm-services?enabled=true'); } catch (e) { services = []; } if (services.length === 0) { listEl.innerHTML = '
暂无已启用的 Team Manager 服务,将自动选择第一个
'; } else { listEl.innerHTML = services.map(s => `
${escapeHtml(s.name)}
${escapeHtml(s.api_url)}
选择
`).join(''); listEl.querySelectorAll('.tm-service-item').forEach(item => { item.addEventListener('mouseenter', () => item.style.background = 'var(--surface-hover)'); item.addEventListener('mouseleave', () => item.style.background = ''); item.addEventListener('click', () => { cleanup(); resolve({ service_id: parseInt(item.dataset.id) }); }); }); } function cleanup() { modal.classList.remove('active'); closeBtn.removeEventListener('click', onCancel); cancelBtn.removeEventListener('click', onCancel); autoBtn.removeEventListener('click', onAuto); } function onCancel() { cleanup(); resolve(null); } function onAuto() { cleanup(); resolve({ service_id: null }); } closeBtn.addEventListener('click', onCancel); cancelBtn.addEventListener('click', onCancel); autoBtn.addEventListener('click', onAuto); }); } // 上传单账号到 Team Manager async function uploadToTm(id) { const choice = await selectTmService(); if (choice === null) return; try { toast.info('正在上传到 Team Manager...'); const payload = {}; if (choice.service_id != null) payload.service_id = choice.service_id; const result = await api.post(`/accounts/${id}/upload-tm`, payload); if (result.success) { toast.success('上传成功'); } else { toast.error('上传失败: ' + (result.message || '未知错误')); } } catch (e) { toast.error('上传失败: ' + e.message); } } // 批量上传到 Team Manager async function handleBatchUploadTm() { const count = getEffectiveCount(); if (count === 0) return; const choice = await selectTmService(); if (choice === null) return; // 用户取消 const confirmed = await confirm(`确定要将选中的 ${count} 个账号上传到 Team Manager 吗?`); if (!confirmed) return; elements.batchUploadBtn.disabled = true; elements.batchUploadBtn.textContent = '上传中...'; try { const payload = buildBatchPayload(); if (choice.service_id != null) payload.service_id = choice.service_id; const result = await api.post('/accounts/batch-upload-tm', payload); let message = `成功: ${result.success_count}`; if (result.failed_count > 0) message += `, 失败: ${result.failed_count}`; if (result.skipped_count > 0) message += `, 跳过: ${result.skipped_count}`; toast.success(message); loadAccounts(); } catch (e) { toast.error('批量上传失败: ' + e.message); } finally { updateBatchButtons(); } } // 更多菜单切换 function toggleMoreMenu(btn) { const menu = btn.nextElementSibling; const isActive = menu.classList.contains('active'); // 关闭所有其他更多菜单 document.querySelectorAll('.dropdown-menu.active').forEach(m => m.classList.remove('active')); if (!isActive) menu.classList.add('active'); } function closeMoreMenu(el) { const menu = el.closest('.dropdown-menu'); if (menu) menu.classList.remove('active'); } // 保存账号 Cookies async function saveCookies(id) { const textarea = document.getElementById(`cookies-input-${id}`); if (!textarea) return; const cookiesValue = textarea.value.trim(); try { await api.patch(`/accounts/${id}`, { cookies: cookiesValue }); toast.success('Cookies 已保存'); } catch (e) { toast.error('保存 Cookies 失败: ' + e.message); } } // 查询收件箱验证码 async function checkInboxCode(id) { toast.info('正在查询收件箱...'); try { const result = await api.post(`/accounts/${id}/inbox-code`); if (result.success) { showInboxCodeResult(result.code, result.email); } else { toast.error('查询失败: ' + (result.error || '未收到验证码')); } } catch (error) { toast.error('查询失败: ' + error.message); } } function showInboxCodeResult(code, email) { elements.modalBody.innerHTML = `
${escapeHtml(email)} 最新验证码
${escapeHtml(code)}
`; elements.detailModal.classList.add('active'); }