// ==================================================================== // 登录验证 // ==================================================================== (function () { const ACCESS_KEY = 'oadmin123'; const overlay = document.getElementById('loginOverlay'); if (!overlay) return; // 已登录则直接隐藏 if (sessionStorage.getItem('authed') === '1') { overlay.classList.add('hidden'); return; } const pwdInput = document.getElementById('loginPassword'); const loginBtn = document.getElementById('loginBtn'); const errEl = document.getElementById('loginError'); function doLogin() { if (pwdInput.value === ACCESS_KEY) { sessionStorage.setItem('authed', '1'); overlay.classList.add('hidden'); errEl.textContent = ''; } else { errEl.textContent = '密码错误,请重试'; pwdInput.value = ''; pwdInput.focus(); } } loginBtn.addEventListener('click', doLogin); pwdInput.addEventListener('keydown', e => { if (e.key === 'Enter') doLogin(); }); pwdInput.focus(); })(); // Outlook 邮件管理器 — 账号管理 + 双栏邮件查看器 class MailManager { constructor() { // 账号管理状态 this.accounts = []; this.page = 1; this.pageSize = 15; this.total = 0; this.query = ''; // 邮件查看状态 this.currentEmail = ''; this.currentFolder = ''; this.messages = []; this.selectedId = null; this.claudePaymentStatuses = {}; this.paymentFilter = ''; this.init(); } init() { this.bindAccountEvents(); this.bindEmailEvents(); this.loadAccounts(); } // ==================================================================== // 事件绑定 — 账号视图 // ==================================================================== bindAccountEvents() { // 搜索 — 防抖 const searchInput = document.getElementById('accountSearch'); let searchTimer = null; searchInput.addEventListener('input', () => { clearTimeout(searchTimer); searchTimer = setTimeout(() => { this.query = searchInput.value.trim(); this.page = 1; this.loadAccounts(); }, 300); }); // 导入按钮 document.getElementById('importBtn').addEventListener('click', () => { document.getElementById('importText').value = ''; document.getElementById('importModal').classList.add('show'); }); // 取消导入 document.getElementById('cancelImportBtn').addEventListener('click', () => { document.getElementById('importModal').classList.remove('show'); }); // 点击模态框背景关闭 document.getElementById('importModal').addEventListener('click', (e) => { if (e.target === e.currentTarget) { e.currentTarget.classList.remove('show'); } }); // 确认导入 document.getElementById('doImportBtn').addEventListener('click', () => this.importAccounts()); // 导出 document.getElementById('exportBtn').addEventListener('click', () => this.exportAccounts()); // Claude支付检测 document.getElementById('claudePaymentBtn').addEventListener('click', () => this.startClaudePaymentCheck()); // 支付状态筛选 document.getElementById('paymentFilter').addEventListener('change', (e) => { this.paymentFilter = e.target.value; this.page = 1; this.loadAccounts(); }); // 刷新 document.getElementById('refreshAccountsBtn').addEventListener('click', () => this.loadAccounts()); // 复制按钮 — 事件委托 document.getElementById('accountTableBody').addEventListener('click', (e) => { const btn = e.target.closest('button[data-copy-field]'); if (!btn) return; e.preventDefault(); e.stopPropagation(); const field = btn.dataset.copyField; const key = btn.dataset.copyKey; if (field === 'email') { this.copyToClipboard(key); } else if (this._accountDataMap && this._accountDataMap[key]) { const map = { pwd: 'pwd', cid: 'cid', token: 'token' }; this.copyToClipboard(this._accountDataMap[key][map[field]] || ''); } }); // 退款到账按钮 — 事件委托 document.getElementById('accountTableBody').addEventListener('click', (e) => { const btn = e.target.closest('.refund-received-btn'); if (!btn) return; e.preventDefault(); e.stopPropagation(); this.toggleRefundReceived(btn.dataset.email); }); // 代理按钮 — 事件委托打开弹窗 + 复制代理 document.getElementById('accountTableBody').addEventListener('click', (e) => { // 复制代理 const copyBtn = e.target.closest('[data-copy-proxy]'); if (copyBtn) { e.preventDefault(); e.stopPropagation(); const em = copyBtn.dataset.copyProxy; const info = this.claudePaymentStatuses[em]; if (info && info.proxy) this.copyToClipboard(info.proxy); return; } // 打开弹窗 const btn = e.target.closest('.proxy-btn'); if (!btn) return; this.openProxyModal(btn.dataset.email); }); // 代理弹窗事件 document.getElementById('cancelProxyBtn').addEventListener('click', () => { document.getElementById('proxyModal').classList.remove('show'); }); document.getElementById('proxyModal').addEventListener('click', (e) => { if (e.target === e.currentTarget) e.currentTarget.classList.remove('show'); }); document.getElementById('saveProxyBtn').addEventListener('click', () => this.saveProxy()); document.getElementById('clearProxyBtn').addEventListener('click', () => this.clearProxy()); // 快速粘贴解析 document.getElementById('proxyRaw').addEventListener('input', (e) => this.parseProxyRaw(e.target.value)); // 字段变化时更新预览 for (const id of ['proxyHost', 'proxyPort', 'proxyUser', 'proxyPass']) { document.getElementById(id).addEventListener('input', () => this.updateProxyPreview()); } document.querySelectorAll('input[name="proxyProtocol"]').forEach(r => { r.addEventListener('change', () => this.updateProxyPreview()); }); // 有效期选项切换 document.querySelectorAll('input[name="proxyExpire"]').forEach(r => { r.addEventListener('change', () => { const customInput = document.getElementById('proxyExpireCustom'); customInput.style.display = r.value === 'custom' && r.checked ? '' : 'none'; this.calcProxyExpireDate(); }); }); document.getElementById('proxyExpireCustom').addEventListener('input', () => this.calcProxyExpireDate()); document.getElementById('proxyPurchaseDate').addEventListener('change', () => this.calcProxyExpireDate()); // 备注/卡号 — 点击打开弹窗 document.getElementById('accountTableBody').addEventListener('click', (e) => { const btn = e.target.closest('[data-note-email]'); if (!btn) return; this.openNoteModal(btn.dataset.noteEmail); }); // 备注弹窗 document.getElementById('cancelNoteBtn').addEventListener('click', () => { document.getElementById('noteModal').classList.remove('show'); }); document.getElementById('noteModal').addEventListener('click', (e) => { if (e.target === e.currentTarget) e.currentTarget.classList.remove('show'); }); document.getElementById('saveNoteBtn').addEventListener('click', () => this.saveNote()); // 分页 document.getElementById('prevPageBtn').addEventListener('click', () => { if (this.page > 1) { this.page--; this.loadAccounts(); } }); document.getElementById('nextPageBtn').addEventListener('click', () => { const maxPage = Math.ceil(this.total / this.pageSize) || 1; if (this.page < maxPage) { this.page++; this.loadAccounts(); } }); } // ==================================================================== // 事件绑定 — 邮件视图 // ==================================================================== bindEmailEvents() { document.getElementById('backToAccounts').addEventListener('click', () => this.showAccountView()); document.getElementById('mobileMailToggle').addEventListener('click', () => this.toggleMobileMailList()); document.getElementById('mobileOverlay').addEventListener('click', () => this.closeMobileMailList()); document.getElementById('refreshEmailsBtn').addEventListener('click', () => this.refreshEmails()); } // ==================================================================== // 视图切换 // ==================================================================== showAccountView() { document.getElementById('accountView').style.display = ''; document.getElementById('emailView').style.display = 'none'; this.loadAccounts(); } showEmailView(email, folder) { document.getElementById('accountView').style.display = 'none'; document.getElementById('emailView').style.display = ''; document.getElementById('topbarEmail').textContent = email; const folderLabel = folder === 'Junk' ? '垃圾箱' : '收件箱'; document.getElementById('topbarFolder').textContent = folderLabel; document.getElementById('emailListTitle').textContent = folderLabel; this.currentEmail = email; this.currentFolder = folder; this.messages = []; this.selectedId = null; this.showDetailEmpty(); this.loadEmails(); } // ==================================================================== // 账号管理 — 数据加载 // ==================================================================== async loadAccounts() { const tbody = document.getElementById('accountTableBody'); tbody.innerHTML = `
加载中...
`; try { // 有筛选时加载全部数据,否则分页加载 const params = new URLSearchParams( this.paymentFilter ? { page: 1, page_size: 9999 } : { page: this.page, page_size: this.pageSize } ); if (this.query) params.set('q', this.query); const resp = await fetch(`/api/accounts/detailed?${params}`); const result = await resp.json(); if (result.success && result.data) { this._allAccounts = result.data.items || []; this._allTotal = result.data.total || 0; await this.loadClaudePaymentStatuses(); this.applyFilterAndRender(); } else { tbody.innerHTML = `
${this.escapeHtml(result.message || '加载失败')}
`; } } catch (err) { console.error('加载账号失败:', err); tbody.innerHTML = `
网络错误
`; } } _matchPaymentFilter(email) { const info = this.claudePaymentStatuses[email]; switch (this.paymentFilter) { case 'paid': return info && !!info.payment_time; case 'unpaid': return !info || !info.payment_time; case 'refunded': return info && !!info.refund_time; case 'refund_received': return info && info.refund_received === '1'; case 'refund_not_received': return info && !!info.refund_time && info.refund_received !== '1'; case 'suspended': return info && !!info.suspended_time; case 'unchecked': return !info; default: return true; } } applyFilterAndRender() { if (this.paymentFilter) { const filtered = this._allAccounts.filter(acc => this._matchPaymentFilter(acc.email)); this.total = filtered.length; const start = (this.page - 1) * this.pageSize; this.accounts = filtered.slice(start, start + this.pageSize); } else { this.accounts = this._allAccounts; this.total = this._allTotal; } this.renderAccounts(); this.renderPager(); } renderAccounts() { const tbody = document.getElementById('accountTableBody'); if (!this.accounts.length) { tbody.innerHTML = `
暂无邮箱数据
`; return; } // 将账户数据存到map中,复制时通过索引取值 this._accountDataMap = {}; const startIdx = (this.page - 1) * this.pageSize; tbody.innerHTML = this.accounts.map((acc, i) => { const num = startIdx + i + 1; const email = acc.email || ''; const pwd = acc.password || ''; const cid = acc.client_id || ''; const token = acc.refresh_token || ''; this._accountDataMap[email] = { pwd, cid, token }; return ` ${num}
${this.escapeHtml(email)}
${pwd ? '••••••' : '-'} ${pwd ? `` : ''}
${this.truncate(cid, 16)} ${cid ? `` : ''}
${token ? this.truncate(token, 20) : '-'} ${token ? `` : ''}
${this.renderClaudeColumns(email)}
`; }).join(''); } renderPager() { const maxPage = Math.ceil(this.total / this.pageSize) || 1; document.getElementById('pagerInfo').textContent = `共 ${this.total} 条`; document.getElementById('pagerCurrent').textContent = `${this.page}/${maxPage} 页`; // 渲染页码按钮 const controlsContainer = document.getElementById('pagerControls'); let btns = ''; // 上一页 btns += ``; // 页码 const range = this.getPageRange(this.page, maxPage, 5); for (const p of range) { if (p === '...') { btns += `...`; } else { btns += ``; } } // 下一页 btns += ``; controlsContainer.innerHTML = btns; } getPageRange(current, total, maxButtons) { if (total <= maxButtons) { return Array.from({ length: total }, (_, i) => i + 1); } const pages = []; const half = Math.floor(maxButtons / 2); let start = Math.max(1, current - half); let end = Math.min(total, start + maxButtons - 1); if (end - start < maxButtons - 1) { start = Math.max(1, end - maxButtons + 1); } if (start > 1) { pages.push(1); if (start > 2) pages.push('...'); } for (let i = start; i <= end; i++) pages.push(i); if (end < total) { if (end < total - 1) pages.push('...'); pages.push(total); } return pages; } goPage(p) { const maxPage = Math.ceil(this.total / this.pageSize) || 1; if (p < 1 || p > maxPage) return; this.page = p; this.loadAccounts(); } // ==================================================================== // 账号管理 — 操作 // ==================================================================== async importAccounts() { const text = document.getElementById('importText').value.trim(); if (!text) { this.showToast('请粘贴账号数据', 'warning'); return; } const mergeMode = document.querySelector('input[name="mergeMode"]:checked').value; const btn = document.getElementById('doImportBtn'); btn.disabled = true; btn.innerHTML = ' 导入中...'; try { const resp = await fetch('/api/accounts/import', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text, merge_mode: mergeMode }) }); const result = await resp.json(); if (result.success) { this.showToast(result.message || '导入成功', 'success'); document.getElementById('importModal').classList.remove('show'); this.page = 1; this.loadAccounts(); } else { this.showToast(result.message || '导入失败', 'danger'); } } catch (err) { this.showToast('网络错误', 'danger'); } finally { btn.disabled = false; btn.innerHTML = '确定导入'; } } async exportAccounts() { try { const resp = await fetch('/api/export'); if (!resp.ok) throw new Error('导出失败'); const blob = await resp.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `outlook_accounts_${new Date().toISOString().slice(0, 10)}.txt`; a.click(); URL.revokeObjectURL(url); this.showToast('导出成功', 'success'); } catch (err) { this.showToast('导出失败', 'danger'); } } async deleteAccount(email) { if (!confirm(`确定要删除账号 ${email} 吗?`)) return; try { const resp = await fetch(`/api/account/${encodeURIComponent(email)}`, { method: 'DELETE' }); const result = await resp.json(); if (result.success) { this.showToast(`已删除 ${email}`, 'success'); this.loadAccounts(); } else { this.showToast(result.message || '删除失败', 'danger'); } } catch (err) { this.showToast('网络错误', 'danger'); } } openMailbox(email, folder) { this.showEmailView(email, folder); } // ==================================================================== // 邮件查看 — 数据 // ==================================================================== async refreshEmails() { const btn = document.getElementById('refreshEmailsBtn'); btn.classList.add('spinning'); btn.disabled = true; try { await this.loadEmails(true); } finally { btn.classList.remove('spinning'); btn.disabled = false; } } async loadEmails(refresh = false) { const container = document.getElementById('emailListContent'); container.innerHTML = '

加载邮件中...

'; try { const params = new URLSearchParams({ email: this.currentEmail, folder: this.currentFolder, top: 20 }); if (refresh) params.set('refresh', 'true'); const resp = await fetch(`/api/messages?${params}`); const result = await resp.json(); if (result.success && result.data && result.data.length > 0) { this.messages = result.data; this.renderMailList(); this.updateEmailCount(this.messages.length); this.selectEmail(this.messages[0].id); } else { this.messages = []; this.updateEmailCount(0); container.innerHTML = `

${this.escapeHtml(result.message || '暂无邮件')}

`; } } catch (err) { console.error('加载邮件失败:', err); container.innerHTML = '

网络错误

'; } } renderMailList() { const container = document.getElementById('emailListContent'); container.innerHTML = this.messages.map(msg => { const sender = msg.sender?.emailAddress || msg.from?.emailAddress || {}; const senderName = sender.name || sender.address || '未知'; const subject = msg.subject || '(无主题)'; const preview = (msg.bodyPreview || '').substring(0, 80); const time = this.formatDate(msg.receivedDateTime); return `
${this.escapeHtml(senderName)} ${time}
${this.escapeHtml(subject)}
${this.escapeHtml(preview)}
`; }).join(''); } selectEmail(id) { this.selectedId = id; document.querySelectorAll('.mail-item').forEach(el => { el.classList.toggle('selected', el.dataset.id === id); }); const msg = this.messages.find(m => m.id === id); if (msg) this.renderDetail(msg); this.closeMobileMailList(); } updateEmailCount(count) { const badge = document.getElementById('emailCountBadge'); badge.textContent = count; badge.style.display = count > 0 ? 'inline-flex' : 'none'; } // ==================================================================== // 邮件详情 // ==================================================================== renderDetail(msg) { const container = document.getElementById('detailContent'); const sender = msg.sender?.emailAddress || msg.from?.emailAddress || {}; const senderName = sender.name || sender.address || '未知发件人'; const senderAddr = sender.address || ''; const toRecipients = msg.toRecipients || []; const recipients = toRecipients .map(r => r.emailAddress?.name || r.emailAddress?.address) .filter(Boolean) .join(', ') || '未知收件人'; const subject = msg.subject || '(无主题)'; const date = this.formatDateFull(msg.receivedDateTime); const body = msg.body?.content || '(无内容)'; const contentType = msg.body?.contentType || 'text'; const isHtml = contentType === 'html' || body.includes('${this.escapeHtml(body)}`; } container.innerHTML = `
${bodySection}
`; if (isHtml) this.writeIframe(body); } writeIframe(htmlContent) { const iframe = document.getElementById('emailIframe'); if (!iframe) return; const sanitized = this.sanitizeHtml(htmlContent); iframe.onload = () => { try { const doc = iframe.contentDocument || iframe.contentWindow.document; const height = doc.documentElement.scrollHeight || doc.body.scrollHeight; iframe.style.height = Math.max(300, Math.min(height + 30, 2000)) + 'px'; } catch (e) { iframe.style.height = '500px'; } }; try { const doc = iframe.contentDocument || iframe.contentWindow.document; doc.open(); doc.write(`${sanitized}`); doc.close(); } catch (e) { console.error('写入iframe失败:', e); iframe.style.height = '500px'; } } showDetailEmpty() { const container = document.getElementById('detailContent'); container.innerHTML = '
选择一封邮件查看详情

从左侧列表中选择邮件

'; } // ==================================================================== // 移动端邮件列表 // ==================================================================== toggleMobileMailList() { const panel = document.getElementById('emailListPanel'); const overlay = document.getElementById('mobileOverlay'); panel.classList.toggle('mobile-open'); overlay.classList.toggle('show'); } closeMobileMailList() { const panel = document.getElementById('emailListPanel'); const overlay = document.getElementById('mobileOverlay'); panel.classList.remove('mobile-open'); overlay.classList.remove('show'); } // ==================================================================== // Claude 支付检测 // ==================================================================== async toggleRefundReceived(email) { try { const resp = await fetch(`/api/tools/refund-received/${encodeURIComponent(email)}`, { method: 'POST' }); const result = await resp.json(); if (result.success) { // 更新本地状态 if (this.claudePaymentStatuses[email]) { this.claudePaymentStatuses[email].refund_received = result.data.refund_received; } this.renderAccounts(); this.showToast(result.data.refund_received === '1' ? '已标记到账' : '已取消到账', 'success'); } else { this.showToast(result.message || '操作失败', 'danger'); } } catch (e) { this.showToast('网络错误', 'danger'); } } async loadClaudePaymentStatuses() { try { const resp = await fetch('/api/tools/claude-payment-status'); if (resp.ok) { const result = await resp.json(); if (result.success) this.claudePaymentStatuses = result.data || {}; } } catch (e) { console.error('加载Claude支付状态失败:', e); } } renderClaudeColumns(email) { const info = this.claudePaymentStatuses[email]; if (!info) { return `未检测 未检测 - - - - `; } const hasPaid = !!info.payment_time; const hasRefund = !!info.refund_time; const hasSuspended = !!info.suspended_time; const paidBadge = info.status === 'error' ? '错误' : hasPaid ? '已支付' : '未支付'; const refundBadge = info.status === 'error' ? '错误' : hasRefund ? '已退款' : '未退款'; const fmtTime = (t) => { if (!t) return '-'; try { const d = new Date(t); if (isNaN(d.getTime())) return t; return d.toLocaleString('zh-CN', { month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit' }); } catch(e) { return t; } }; const suspendedHtml = hasSuspended ? `${fmtTime(info.suspended_time)}` : '-'; const isRefundReceived = info.refund_received === '1'; const refundReceivedBtn = ``; return `${paidBadge} ${refundBadge} ${refundReceivedBtn} ${fmtTime(info.payment_time)} ${fmtTime(info.refund_time)} ${suspendedHtml} ${this.renderNoteCell(email, info)}
${info.proxy ? `` : ''}
${info.proxy ? this.renderProxyExpireBadge(info) : ''} `; } openProxyModal(email) { this._proxyEditEmail = email; const info = this.claudePaymentStatuses[email]; document.getElementById('proxyModalEmail').textContent = email; // 清空字段 document.getElementById('proxyHost').value = ''; document.getElementById('proxyPort').value = ''; document.getElementById('proxyUser').value = ''; document.getElementById('proxyPass').value = ''; document.getElementById('proxyRaw').value = ''; document.querySelector('input[name="proxyProtocol"][value="http"]').checked = true; // 有效期/类型默认值 const expireDays = info ? (info.proxy_expire_days || 30) : 30; const share = info ? (info.proxy_share || 'exclusive') : 'exclusive'; const purchaseDate = info ? (info.proxy_purchase_date || '') : ''; // 设置有效期 const customInput = document.getElementById('proxyExpireCustom'); if (expireDays === 10 || expireDays === 30) { document.querySelector(`input[name="proxyExpire"][value="${expireDays}"]`).checked = true; customInput.style.display = 'none'; } else { document.querySelector('input[name="proxyExpire"][value="custom"]').checked = true; customInput.style.display = ''; customInput.value = expireDays; } // 设置类型 const shareRadio = document.querySelector(`input[name="proxyShare"][value="${share}"]`); if (shareRadio) shareRadio.checked = true; // 设置购买日期 document.getElementById('proxyPurchaseDate').value = purchaseDate || new Date().toISOString().slice(0, 10); this.calcProxyExpireDate(); // 如果已有代理,解析填充 const existing = info ? (info.proxy || '') : ''; if (existing) { this.parseProxyRaw(existing); document.getElementById('proxyRaw').value = existing; } this.updateProxyPreview(); document.getElementById('proxyModal').classList.add('show'); setTimeout(() => document.getElementById('proxyHost').focus(), 100); } parseProxyRaw(raw) { raw = raw.trim(); if (!raw) return; let protocol = 'http', host = '', port = '', user = '', pass = ''; // 格式1: protocol://user:pass@host:port const urlMatch = raw.match(/^(https?|socks5):\/\/(?:([^:@]+):([^@]+)@)?([^:\/]+):(\d+)/i); if (urlMatch) { protocol = urlMatch[1].toLowerCase(); user = urlMatch[2] || ''; pass = urlMatch[3] || ''; host = urlMatch[4]; port = urlMatch[5]; } else { // 格式2: ip:port:user:pass 或 ip:port const parts = raw.split(':'); if (parts.length >= 2) { host = parts[0]; port = parts[1]; if (parts.length >= 4) { user = parts[2]; pass = parts[3]; } else if (parts.length === 3) { user = parts[2]; } } } document.getElementById('proxyHost').value = host; document.getElementById('proxyPort').value = port; document.getElementById('proxyUser').value = user; document.getElementById('proxyPass').value = pass; const radio = document.querySelector(`input[name="proxyProtocol"][value="${protocol}"]`); if (radio) radio.checked = true; this.updateProxyPreview(); } buildProxyString() { const host = document.getElementById('proxyHost').value.trim(); const port = document.getElementById('proxyPort').value.trim(); if (!host || !port) return ''; const protocol = document.querySelector('input[name="proxyProtocol"]:checked').value; const user = document.getElementById('proxyUser').value.trim(); const pass = document.getElementById('proxyPass').value.trim(); if (user && pass) { return `${protocol}://${user}:${pass}@${host}:${port}`; } else if (user) { return `${protocol}://${user}@${host}:${port}`; } return `${protocol}://${host}:${port}`; } updateProxyPreview() { const str = this.buildProxyString(); const el = document.getElementById('proxyPreview'); if (str) { el.innerHTML = `完整代理:${this.escapeHtml(str)}`; } else { el.innerHTML = ''; } } renderNoteCell(email, info) { const title = info.title || ''; const card = info.card_number || ''; const hasContent = title || card || info.remark; if (!hasContent) { return ``; } const maskedCard = card ? '****' + card.slice(-4) : ''; return `
${title ? `
${this.escapeHtml(title)}
` : ''} ${maskedCard ? `
${maskedCard}
` : ''}
`; } renderProxyExpireBadge(info) { if (!info.proxy_purchase_date) return ''; const days = info.proxy_expire_days || 30; const d = new Date(info.proxy_purchase_date); d.setDate(d.getDate() + days); const now = new Date(); now.setHours(0, 0, 0, 0); const remaining = Math.ceil((d - now) / 86400000); const shareLabel = info.proxy_share === 'shared' ? '共享' : '独享'; let cls = 'proxy-expire-ok'; let label = `${remaining}天`; if (remaining <= 0) { cls = 'proxy-expire-dead'; label = '已过期'; } else if (remaining <= 3) { cls = 'proxy-expire-warn'; } return `
${label}${shareLabel}
`; } getProxyExpireDays() { const checked = document.querySelector('input[name="proxyExpire"]:checked').value; if (checked === 'custom') { return parseInt(document.getElementById('proxyExpireCustom').value) || 30; } return parseInt(checked); } calcProxyExpireDate() { const purchase = document.getElementById('proxyPurchaseDate').value; const days = this.getProxyExpireDays(); const expireEl = document.getElementById('proxyExpireDate'); if (purchase && days > 0) { const d = new Date(purchase); d.setDate(d.getDate() + days); const now = new Date(); now.setHours(0, 0, 0, 0); const remaining = Math.ceil((d - now) / 86400000); const dateStr = d.toISOString().slice(0, 10); if (remaining <= 0) { expireEl.value = `${dateStr} (已过期)`; expireEl.style.color = '#e11d48'; } else if (remaining <= 3) { expireEl.value = `${dateStr} (剩${remaining}天)`; expireEl.style.color = '#d97706'; } else { expireEl.value = `${dateStr} (剩${remaining}天)`; expireEl.style.color = '#059669'; } } else { expireEl.value = ''; expireEl.style.color = '#64748b'; } } async saveProxy() { const email = this._proxyEditEmail; if (!email) return; const value = this.buildProxyString(); const expireDays = this.getProxyExpireDays(); const share = document.querySelector('input[name="proxyShare"]:checked').value; const purchaseDate = document.getElementById('proxyPurchaseDate').value; try { const resp = await fetch(`/api/tools/claude-payment-note/${encodeURIComponent(email)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ proxy: value, proxy_expire_days: expireDays, proxy_share: share, proxy_purchase_date: purchaseDate }) }); const result = await resp.json(); if (result.success) { if (!this.claudePaymentStatuses[email]) { this.claudePaymentStatuses[email] = { status: 'unknown' }; } this.claudePaymentStatuses[email].proxy = value; this.claudePaymentStatuses[email].proxy_expire_days = expireDays; this.claudePaymentStatuses[email].proxy_share = share; this.claudePaymentStatuses[email].proxy_purchase_date = purchaseDate; this.updateClaudeBadgeInTable(email); document.getElementById('proxyModal').classList.remove('show'); this.showToast('代理已保存', 'success'); } else { this.showToast(result.message || '保存失败', 'danger'); } } catch (e) { this.showToast('保存失败', 'danger'); } } async clearProxy() { const email = this._proxyEditEmail; if (!email) return; try { const resp = await fetch(`/api/tools/claude-payment-note/${encodeURIComponent(email)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ proxy: '' }) }); const result = await resp.json(); if (result.success) { if (this.claudePaymentStatuses[email]) { this.claudePaymentStatuses[email].proxy = ''; } this.updateClaudeBadgeInTable(email); document.getElementById('proxyModal').classList.remove('show'); this.showToast('代理已清除', 'success'); } } catch (e) { this.showToast('清除失败', 'danger'); } } openNoteModal(email) { this._noteEditEmail = email; const info = this.claudePaymentStatuses[email] || {}; document.getElementById('noteModalEmail').textContent = email; document.getElementById('noteTitle').value = info.title || ''; document.getElementById('noteCardNumber').value = info.card_number || ''; document.getElementById('noteRemark').value = info.remark || ''; document.getElementById('noteModal').classList.add('show'); setTimeout(() => document.getElementById('noteTitle').focus(), 100); } async saveNote() { const email = this._noteEditEmail; if (!email) return; const title = document.getElementById('noteTitle').value.trim(); const card_number = document.getElementById('noteCardNumber').value.trim(); const remark = document.getElementById('noteRemark').value.trim(); try { const resp = await fetch(`/api/tools/claude-payment-note/${encodeURIComponent(email)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title, card_number, remark }) }); const result = await resp.json(); if (result.success) { if (!this.claudePaymentStatuses[email]) { this.claudePaymentStatuses[email] = { status: 'unknown' }; } this.claudePaymentStatuses[email].title = title; this.claudePaymentStatuses[email].card_number = card_number; this.claudePaymentStatuses[email].remark = remark; this.updateClaudeBadgeInTable(email); document.getElementById('noteModal').classList.remove('show'); this.showToast('备注已保存', 'success'); } else { this.showToast(result.message || '保存失败', 'danger'); } } catch (e) { this.showToast('保存失败', 'danger'); } } async startClaudePaymentCheck() { const btn = document.getElementById('claudePaymentBtn'); if (!btn) return; btn.disabled = true; const origHtml = btn.innerHTML; btn.innerHTML = '检测中...'; try { const response = await fetch('/api/tools/check-claude-payment', { method: 'POST' }); const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop(); for (const line of lines) { if (!line.startsWith('data: ')) continue; try { const data = JSON.parse(line.slice(6)); if (data.type === 'progress') { btn.innerHTML = `${data.current}/${data.total}`; } else if (data.type === 'result') { this.claudePaymentStatuses[data.email] = { status: data.status, payment_time: data.payment_time || null, refund_time: data.refund_time || null, suspended_time: data.suspended_time || null, checked_at: new Date().toLocaleString('zh-CN') }; this.updateClaudeBadgeInTable(data.email); } else if (data.type === 'done') { btn.innerHTML = '完成'; setTimeout(() => { btn.innerHTML = origHtml; btn.disabled = false; }, 2000); } } catch (e) {} } } } catch (err) { console.error('Claude支付检测失败:', err); btn.innerHTML = '失败'; setTimeout(() => { btn.innerHTML = origHtml; btn.disabled = false; }, 2000); } } async checkSingleClaudePayment(email, btnEl) { if (!btnEl) return; btnEl.disabled = true; const origText = btnEl.textContent; btnEl.textContent = '检测中...'; try { const resp = await fetch(`/api/tools/check-claude-payment/${encodeURIComponent(email)}`, { method: 'POST' }); const result = await resp.json(); if (result.success && result.data) { this.claudePaymentStatuses[email] = { status: result.data.status, payment_time: result.data.payment_time || null, refund_time: result.data.refund_time || null, suspended_time: result.data.suspended_time || null, checked_at: new Date().toLocaleString('zh-CN') }; this.updateClaudeBadgeInTable(email); this.showToast(`${email} 检测完成: ${result.data.status}`, 'success'); } else { this.showToast(result.message || '检测失败', 'danger'); } } catch (err) { console.error('单账户Claude检测失败:', err); this.showToast('检测失败', 'danger'); } finally { btnEl.disabled = false; btnEl.textContent = origText; } } updateClaudeBadgeInTable(email) { const tbody = document.getElementById('accountTableBody'); if (!tbody) return; for (const row of tbody.querySelectorAll('tr')) { const firstTd = row.querySelector('td'); if (firstTd && firstTd.textContent.includes(email)) { const cells = row.querySelectorAll('td'); if (cells.length >= 13) { const tmp = document.createElement('tr'); tmp.innerHTML = this.renderClaudeColumns(email); const newCells = tmp.querySelectorAll('td'); for (let j = 0; j < 7 && j < newCells.length; j++) { cells[5 + j].innerHTML = newCells[j].innerHTML; cells[5 + j].className = newCells[j].className; } } break; } } } // ==================================================================== // 工具方法 // ==================================================================== copyToClipboard(text) { if (!text) return; const fallback = () => { const ta = document.createElement('textarea'); ta.value = text; ta.style.cssText = 'position:fixed;top:0;left:0;width:1px;height:1px;opacity:0.01'; document.body.appendChild(ta); ta.focus(); ta.select(); try { document.execCommand('copy'); } catch (e) {} document.body.removeChild(ta); this.showToast('已复制到剪贴板', 'success'); }; if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(text).then(() => { this.showToast('已复制到剪贴板', 'success'); }).catch(fallback); } else { fallback(); } } showToast(message, type = 'info') { const container = document.getElementById('toastContainer'); const iconMap = { success: 'bi-check-circle-fill', danger: 'bi-exclamation-triangle-fill', warning: 'bi-exclamation-circle-fill', info: 'bi-info-circle-fill' }; const toast = document.createElement('div'); toast.className = `app-toast toast-${type}`; toast.innerHTML = `${this.escapeHtml(message)}`; container.appendChild(toast); requestAnimationFrame(() => toast.classList.add('show')); setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 300); }, 2500); } truncate(text, maxLen) { if (!text) return '-'; return text.length > maxLen ? text.substring(0, maxLen) + '...' : text; } formatDate(dateString) { if (!dateString) return ''; try { const date = new Date(dateString); if (isNaN(date.getTime())) return dateString; const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const msgDay = new Date(date.getFullYear(), date.getMonth(), date.getDate()); const diff = Math.floor((today - msgDay) / 86400000); if (diff === 0) return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }); if (diff === 1) return '昨天'; if (diff < 7) return `${diff}天前`; return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' }); } catch (e) { return dateString; } } formatDateFull(dateString) { if (!dateString) return '未知时间'; try { const date = new Date(dateString); if (isNaN(date.getTime())) return dateString; return date.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); } catch (e) { return dateString; } } escapeHtml(text) { if (text == null) return ''; const str = String(text); const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return str.replace(/[&<>"']/g, m => map[m]); } escapeJs(text) { if (text == null) return ''; return String(text).replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$'); } sanitizeHtml(html) { let cleaned = html; cleaned = cleaned.replace(/)<[^<]*)*<\/script>/gi, ''); cleaned = cleaned.replace(/\s*on\w+\s*=\s*["'][^"']*["']/gi, ''); cleaned = cleaned.replace(/javascript\s*:/gi, ''); return cleaned; } } // 初始化 const app = new MailManager();