// ====================================================================
// 登录验证
// ====================================================================
(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(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(/