Detect refund/suspension status changes and generate notifications stored in Redis. Bell icon in navbar shows unread count badge, click to expand dropdown with dismiss per-item or read-all. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1491 lines
63 KiB
JavaScript
1491 lines
63 KiB
JavaScript
// ====================================================================
|
||
// 登录验证
|
||
// ====================================================================
|
||
(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.bindNotificationEvents();
|
||
this.loadAccounts();
|
||
this.loadNotifications();
|
||
}
|
||
|
||
// ====================================================================
|
||
// 事件绑定 — 账号视图
|
||
// ====================================================================
|
||
|
||
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();
|
||
this.updateClaudeBtnLabel();
|
||
});
|
||
|
||
// 刷新
|
||
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('closeCredentialBtn').addEventListener('click', () => {
|
||
document.getElementById('credentialModal').classList.remove('show');
|
||
});
|
||
document.getElementById('credentialModal').addEventListener('click', (e) => {
|
||
if (e.target === e.currentTarget) e.currentTarget.classList.remove('show');
|
||
});
|
||
document.getElementById('copyCredClientIdBtn').addEventListener('click', () => {
|
||
this.copyToClipboard(document.getElementById('credClientId').textContent);
|
||
});
|
||
document.getElementById('copyCredTokenBtn').addEventListener('click', () => {
|
||
this.copyToClipboard(document.getElementById('credToken').textContent);
|
||
});
|
||
|
||
// 凭证查看按钮 — 事件委托
|
||
document.getElementById('accountTableBody').addEventListener('click', (e) => {
|
||
const btn = e.target.closest('.credential-btn');
|
||
if (!btn) return;
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
this.showCredentialModal(btn.dataset.credEmail);
|
||
});
|
||
|
||
// 退款到账按钮 — 事件委托
|
||
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 = `<tr><td colspan="13"><div class="table-loading"><div class="spinner-border spinner-border-sm"></div> 加载中...</div></td></tr>`;
|
||
|
||
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 = `<tr><td colspan="13"><div class="no-data"><i class="bi bi-exclamation-circle"></i><div>${this.escapeHtml(result.message || '加载失败')}</div></div></td></tr>`;
|
||
}
|
||
} catch (err) {
|
||
console.error('加载账号失败:', err);
|
||
tbody.innerHTML = `<tr><td colspan="13"><div class="no-data"><i class="bi bi-wifi-off"></i><div>网络错误</div></div></td></tr>`;
|
||
}
|
||
}
|
||
|
||
_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 = `<tr><td colspan="13"><div class="no-data"><i class="bi bi-inbox"></i><div>暂无邮箱数据</div></div></td></tr>`;
|
||
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 `
|
||
<tr>
|
||
<td class="col-num">${num}</td>
|
||
<td>
|
||
<div class="value-container">
|
||
<span class="value-text" title="${this.escapeHtml(email)}">${this.escapeHtml(email)}</span>
|
||
<button class="copy-icon-btn" data-copy-field="email" data-copy-key="${this.escapeHtml(email)}" title="复制">
|
||
<i class="bi bi-clipboard"></i>
|
||
</button>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<div class="value-container">
|
||
<span class="value-text value-secret">${pwd ? '••••••' : '-'}</span>
|
||
${pwd ? `<button class="copy-icon-btn" data-copy-field="pwd" data-copy-key="${this.escapeHtml(email)}" title="复制"><i class="bi bi-clipboard"></i></button>` : ''}
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<button class="note-cell-btn credential-btn" data-cred-email="${this.escapeHtml(email)}"><i class="bi bi-key"></i> 查看</button>
|
||
</td>
|
||
${this.renderClaudeColumns(email)}
|
||
<td>
|
||
<div class="actions">
|
||
<button class="view" onclick="app.openMailbox('${this.escapeHtml(email)}', 'INBOX')">收件箱</button>
|
||
<button class="view" onclick="app.openMailbox('${this.escapeHtml(email)}', 'Junk')">垃圾箱</button>
|
||
<button class="view claude-check-btn" onclick="app.checkSingleClaudePayment('${this.escapeHtml(email)}', this)">Claude</button>
|
||
${this.renderRefundBtn(email)}
|
||
<button class="delete" onclick="app.deleteAccount('${this.escapeHtml(email)}')">删除</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}).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 += `<button class="pagination-btn" ${this.page <= 1 ? 'disabled' : ''} onclick="app.goPage(${this.page - 1})" title="上一页"><i class="bi bi-chevron-left"></i></button>`;
|
||
|
||
// 页码
|
||
const range = this.getPageRange(this.page, maxPage, 5);
|
||
for (const p of range) {
|
||
if (p === '...') {
|
||
btns += `<span style="color:#94a3b8;padding:0 4px;">...</span>`;
|
||
} else {
|
||
btns += `<button class="pagination-btn ${p === this.page ? 'active' : ''}" onclick="app.goPage(${p})">${p}</button>`;
|
||
}
|
||
}
|
||
|
||
// 下一页
|
||
btns += `<button class="pagination-btn" ${this.page >= maxPage ? 'disabled' : ''} onclick="app.goPage(${this.page + 1})" title="下一页"><i class="bi bi-chevron-right"></i></button>`;
|
||
|
||
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 = '<span class="spinner-border spinner-border-sm"></span> 导入中...';
|
||
|
||
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 = '<i class="bi bi-check2"></i><span>确定导入</span>';
|
||
}
|
||
}
|
||
|
||
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) {
|
||
this.showConfirm(`确定要删除账号 ${email} 吗?`, () => this._doDeleteAccount(email), '删除确认');
|
||
}
|
||
|
||
async _doDeleteAccount(email) {
|
||
|
||
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 = '<div class="loading-state"><div class="spinner-border spinner-border-sm"></div><p>加载邮件中...</p></div>';
|
||
|
||
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 = `<div class="empty-state"><i class="bi bi-inbox"></i><p>${this.escapeHtml(result.message || '暂无邮件')}</p></div>`;
|
||
}
|
||
} catch (err) {
|
||
console.error('加载邮件失败:', err);
|
||
container.innerHTML = '<div class="empty-state"><i class="bi bi-wifi-off"></i><p>网络错误</p></div>';
|
||
}
|
||
}
|
||
|
||
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 `
|
||
<div class="mail-item" data-id="${this.escapeHtml(msg.id)}" onclick="app.selectEmail('${this.escapeHtml(msg.id)}')">
|
||
<div class="mail-item-header">
|
||
<span class="mail-sender">${this.escapeHtml(senderName)}</span>
|
||
<span class="mail-time">${time}</span>
|
||
</div>
|
||
<div class="mail-subject">${this.escapeHtml(subject)}</div>
|
||
<div class="mail-preview">${this.escapeHtml(preview)}</div>
|
||
</div>
|
||
`;
|
||
}).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('<html') ||
|
||
body.includes('<body') ||
|
||
body.includes('<div') ||
|
||
body.includes('<table');
|
||
|
||
let bodySection = '';
|
||
if (isHtml) {
|
||
bodySection = '<div class="email-body-container"><iframe class="email-iframe" id="emailIframe" sandbox="allow-same-origin" frameborder="0"></iframe></div>';
|
||
} else {
|
||
bodySection = `<div class="email-body-text">${this.escapeHtml(body)}</div>`;
|
||
}
|
||
|
||
container.innerHTML = `
|
||
<div class="email-detail-wrapper">
|
||
<div class="email-detail-header">
|
||
<div class="email-detail-subject">${this.escapeHtml(subject)}</div>
|
||
<div class="email-detail-meta">
|
||
<div class="meta-row">
|
||
<span class="meta-label">发件人</span>
|
||
<span class="meta-value">${this.escapeHtml(senderName)}</span>
|
||
${senderAddr ? `<span class="meta-badge"><i class="bi bi-envelope-fill me-1"></i>${this.escapeHtml(senderAddr)}</span>` : ''}
|
||
</div>
|
||
<div class="meta-row">
|
||
<span class="meta-label">收件人</span>
|
||
<span class="meta-value">${this.escapeHtml(recipients)}</span>
|
||
</div>
|
||
<div class="meta-row">
|
||
<span class="meta-label">时间</span>
|
||
<span class="meta-value">${date}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
${bodySection}
|
||
</div>
|
||
`;
|
||
|
||
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(`<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;font-size:14px;line-height:1.6;color:#334155;margin:12px;word-wrap:break-word;overflow-wrap:break-word}img{max-width:100%;height:auto}table{max-width:100%}a{color:#667eea}pre{white-space:pre-wrap;word-wrap:break-word}</style></head><body>${sanitized}</body></html>`);
|
||
doc.close();
|
||
} catch (e) {
|
||
console.error('写入iframe失败:', e);
|
||
iframe.style.height = '500px';
|
||
}
|
||
}
|
||
|
||
showDetailEmpty() {
|
||
const container = document.getElementById('detailContent');
|
||
container.innerHTML = '<div class="empty-detail"><i class="bi bi-envelope-open"></i><h6>选择一封邮件查看详情</h6><p>从左侧列表中选择邮件</p></div>';
|
||
}
|
||
|
||
// ====================================================================
|
||
// 移动端邮件列表
|
||
// ====================================================================
|
||
|
||
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 支付检测
|
||
// ====================================================================
|
||
|
||
renderRefundBtn(email) {
|
||
const info = this.claudePaymentStatuses[email];
|
||
// 只有已退款状态才显示退款到账按钮
|
||
if (!info || !info.refund_time) return '';
|
||
const isReceived = info.refund_received === '1';
|
||
if (isReceived) {
|
||
return `<button class="view" style="background:rgba(16,185,129,0.1);color:#059669;border-color:rgba(16,185,129,0.15);" onclick="app.confirmRefundToggle('${this.escapeHtml(email)}')">已到账</button>`;
|
||
}
|
||
return `<button class="view" style="background:rgba(245,158,11,0.1);color:#d97706;border-color:rgba(245,158,11,0.15);" onclick="app.confirmRefundToggle('${this.escapeHtml(email)}')">未到账</button>`;
|
||
}
|
||
|
||
confirmRefundToggle(email) {
|
||
const info = this.claudePaymentStatuses[email];
|
||
const isReceived = info && info.refund_received === '1';
|
||
const msg = isReceived
|
||
? `确定将 ${email} 的退款状态改为【未到账】吗?`
|
||
: `确定将 ${email} 标记为【退款已到账】吗?`;
|
||
this.showConfirm(msg, () => this.toggleRefundReceived(email));
|
||
}
|
||
|
||
showConfirm(message, onOk, title = '确认操作') {
|
||
const modal = document.getElementById('confirmModal');
|
||
document.getElementById('confirmTitle').textContent = title;
|
||
document.getElementById('confirmMessage').textContent = message;
|
||
modal.classList.add('show');
|
||
|
||
const okBtn = document.getElementById('confirmOkBtn');
|
||
const cancelBtn = document.getElementById('confirmCancelBtn');
|
||
const close = () => modal.classList.remove('show');
|
||
|
||
const onConfirm = () => { close(); onOk(); };
|
||
okBtn.onclick = onConfirm;
|
||
cancelBtn.onclick = close;
|
||
modal.onclick = (e) => { if (e.target === modal) close(); };
|
||
}
|
||
|
||
showCredentialModal(email) {
|
||
const data = this._accountDataMap[email];
|
||
if (!data) return;
|
||
const cid = data.cid || '-';
|
||
const token = data.token || '-';
|
||
// 复用 paste-modal 样式
|
||
const modal = document.getElementById('credentialModal');
|
||
document.getElementById('credModalEmail').textContent = email;
|
||
document.getElementById('credClientId').textContent = cid;
|
||
document.getElementById('credToken').textContent = token;
|
||
modal.classList.add('show');
|
||
}
|
||
|
||
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.claudePaymentStatuses[email].refund_received_at = result.data.refund_received_at || '';
|
||
}
|
||
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 `<td><span class="claude-badge claude-unknown">未检测</span></td>
|
||
<td><span class="claude-badge claude-unknown">未检测</span></td>
|
||
<td class="claude-time">-</td>
|
||
<td class="claude-time">-</td>
|
||
<td class="claude-time">-</td>
|
||
<td class="claude-time">-</td>
|
||
<td><button class="note-cell-btn" data-note-email="${this.escapeHtml(email)}"><i class="bi bi-pencil"></i> 编辑</button></td>
|
||
<td><button class="proxy-btn" data-email="${this.escapeHtml(email)}"><i class="bi bi-globe"></i> 设置</button></td>`;
|
||
|
||
}
|
||
|
||
const hasPaid = !!info.payment_time;
|
||
const hasRefund = !!info.refund_time;
|
||
const hasSuspended = !!info.suspended_time;
|
||
|
||
const paidBadge = info.status === 'error'
|
||
? '<span class="claude-badge claude-error">错误</span>'
|
||
: hasPaid
|
||
? '<span class="claude-badge claude-paid">已支付</span>'
|
||
: '<span class="claude-badge claude-unknown">未支付</span>';
|
||
|
||
const refundBadge = info.status === 'error'
|
||
? '<span class="claude-badge claude-error">错误</span>'
|
||
: hasRefund
|
||
? '<span class="claude-badge claude-refunded">已退款</span>'
|
||
: '<span class="claude-badge claude-unknown">未退款</span>';
|
||
|
||
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
|
||
? `<span class="claude-badge claude-suspended">${fmtTime(info.suspended_time)}</span>`
|
||
: '-';
|
||
|
||
const refundReceivedTime = info.refund_received === '1' && info.refund_received_at
|
||
? `<span class="claude-badge claude-paid">${fmtTime(info.refund_received_at)}</span>`
|
||
: '-';
|
||
|
||
return `<td>${paidBadge}</td>
|
||
<td>${refundBadge}</td>
|
||
<td class="claude-time">${fmtTime(info.payment_time)}</td>
|
||
<td class="claude-time">${fmtTime(info.refund_time)}</td>
|
||
<td>${suspendedHtml}</td>
|
||
<td class="claude-time">${refundReceivedTime}</td>
|
||
<td>${this.renderNoteCell(email, info)}</td>
|
||
<td class="proxy-cell">
|
||
<div class="proxy-cell-inner">
|
||
<button class="proxy-btn ${info.proxy ? 'has-proxy' : ''}" data-email="${this.escapeHtml(email)}">${info.proxy ? '<i class="bi bi-globe2"></i> ' + this.escapeHtml(this.truncate(info.proxy, 16)) : '<i class="bi bi-globe"></i> 设置'}</button>
|
||
${info.proxy ? `<button class="copy-icon-btn" data-copy-proxy="${this.escapeHtml(email)}" title="复制代理"><i class="bi bi-clipboard"></i></button>` : ''}
|
||
</div>
|
||
${info.proxy ? this.renderProxyExpireBadge(info) : ''}
|
||
</td>`;
|
||
}
|
||
|
||
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 = `<span class="proxy-preview-label">完整代理:</span><code class="proxy-preview-value">${this.escapeHtml(str)}</code>`;
|
||
} else {
|
||
el.innerHTML = '';
|
||
}
|
||
}
|
||
|
||
renderNoteCell(email, info) {
|
||
const title = info.title || '';
|
||
const card = info.card_number || '';
|
||
const hasContent = title || card || info.remark;
|
||
if (!hasContent) {
|
||
return `<button class="note-cell-btn" data-note-email="${this.escapeHtml(email)}"><i class="bi bi-pencil"></i> 编辑</button>`;
|
||
}
|
||
const maskedCard = card ? '****' + card.slice(-4) : '';
|
||
return `<div class="note-cell-display" data-note-email="${this.escapeHtml(email)}">
|
||
${title ? `<div class="note-title">${this.escapeHtml(title)}</div>` : ''}
|
||
${maskedCard ? `<div class="note-card"><i class="bi bi-credit-card"></i> ${maskedCard}</div>` : ''}
|
||
</div>`;
|
||
}
|
||
|
||
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 `<div class="proxy-expire-info"><span class="proxy-expire-badge ${cls}">${label}</span><span class="proxy-share-tag">${shareLabel}</span></div>`;
|
||
}
|
||
|
||
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');
|
||
}
|
||
}
|
||
|
||
updateClaudeBtnLabel() {
|
||
const btn = document.getElementById('claudePaymentBtn');
|
||
if (!btn || btn.disabled) return;
|
||
const filterSelect = document.getElementById('paymentFilter');
|
||
const filterText = filterSelect.options[filterSelect.selectedIndex].text;
|
||
if (this.paymentFilter) {
|
||
btn.innerHTML = `<i class="bi bi-credit-card"></i><span>检测(${filterText})</span>`;
|
||
} else {
|
||
btn.innerHTML = '<i class="bi bi-credit-card"></i><span>Claude检测</span>';
|
||
}
|
||
}
|
||
|
||
async startClaudePaymentCheck() {
|
||
const btn = document.getElementById('claudePaymentBtn');
|
||
if (!btn) return;
|
||
|
||
// 根据筛选条件决定检测范围
|
||
let targetEmails = [];
|
||
if (this.paymentFilter && this._allAccounts) {
|
||
targetEmails = this._allAccounts
|
||
.filter(acc => this._matchPaymentFilter(acc.email))
|
||
.map(acc => acc.email);
|
||
}
|
||
|
||
const label = this.paymentFilter
|
||
? `检测 ${targetEmails.length} 个`
|
||
: '全部检测';
|
||
|
||
if (targetEmails.length === 0 && this.paymentFilter) {
|
||
this.showToast('当前筛选条件下没有账号', 'warning');
|
||
return;
|
||
}
|
||
|
||
btn.disabled = true;
|
||
const origHtml = btn.innerHTML;
|
||
btn.innerHTML = '<i class="bi bi-hourglass-split"></i><span>检测中...</span>';
|
||
|
||
try {
|
||
const response = await fetch('/api/tools/check-claude-payment', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ emails: targetEmails })
|
||
});
|
||
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 = `<i class="bi bi-hourglass-split"></i><span>${data.current}/${data.total}</span>`;
|
||
} 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 = '<i class="bi bi-check-circle"></i><span>完成</span>';
|
||
setTimeout(() => { btn.innerHTML = origHtml; btn.disabled = false; }, 2000);
|
||
this.loadNotifications();
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.error('Claude支付检测失败:', err);
|
||
btn.innerHTML = '<i class="bi bi-exclamation-triangle"></i><span>失败</span>';
|
||
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');
|
||
this.loadNotifications();
|
||
} 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;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ====================================================================
|
||
// 通知系统
|
||
// ====================================================================
|
||
|
||
bindNotificationEvents() {
|
||
const btn = document.getElementById('notificationBtn');
|
||
const dropdown = document.getElementById('notifDropdown');
|
||
|
||
// 铃铛点击切换下拉面板
|
||
btn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
const visible = dropdown.style.display !== 'none';
|
||
dropdown.style.display = visible ? 'none' : 'flex';
|
||
});
|
||
|
||
// 点击页面其他位置关闭
|
||
document.addEventListener('click', (e) => {
|
||
if (!e.target.closest('.notification-wrapper')) {
|
||
dropdown.style.display = 'none';
|
||
}
|
||
});
|
||
|
||
// 全部已读
|
||
document.getElementById('notifReadAll').addEventListener('click', () => this.dismissAllNotifications());
|
||
}
|
||
|
||
async loadNotifications() {
|
||
try {
|
||
const resp = await fetch('/api/notifications');
|
||
const result = await resp.json();
|
||
if (result.success) {
|
||
this.renderNotifications(result.data || []);
|
||
}
|
||
} catch (e) {
|
||
console.error('加载通知失败:', e);
|
||
}
|
||
}
|
||
|
||
renderNotifications(list) {
|
||
const badge = document.getElementById('notifBadge');
|
||
const listEl = document.getElementById('notifList');
|
||
|
||
// 更新角标
|
||
if (list.length > 0) {
|
||
badge.textContent = list.length;
|
||
badge.style.display = 'flex';
|
||
} else {
|
||
badge.style.display = 'none';
|
||
}
|
||
|
||
// 渲染列表
|
||
if (list.length === 0) {
|
||
listEl.innerHTML = '<div class="notif-empty"><i class="bi bi-bell-slash"></i>暂无通知</div>';
|
||
return;
|
||
}
|
||
|
||
listEl.innerHTML = list.map(n => {
|
||
const icon = n.type === 'suspension' ? 'bi-exclamation-triangle-fill' : 'bi-arrow-return-left';
|
||
return `
|
||
<div class="notif-item ${n.type}">
|
||
<div class="notif-icon"><i class="bi ${icon}"></i></div>
|
||
<div class="notif-content">
|
||
<div class="notif-message">${this.escapeHtml(n.message)}</div>
|
||
<div class="notif-time">${n.created_at}</div>
|
||
</div>
|
||
<button class="notif-close" onclick="app.dismissNotification('${n.id}')" title="已读">
|
||
<i class="bi bi-x"></i>
|
||
</button>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
async dismissNotification(id) {
|
||
try {
|
||
await fetch('/api/notifications/dismiss', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ id })
|
||
});
|
||
this.loadNotifications();
|
||
} catch (e) {
|
||
console.error('移除通知失败:', e);
|
||
}
|
||
}
|
||
|
||
async dismissAllNotifications() {
|
||
try {
|
||
await fetch('/api/notifications/dismiss', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ all: true })
|
||
});
|
||
this.loadNotifications();
|
||
document.getElementById('notifDropdown').style.display = 'none';
|
||
} catch (e) {
|
||
console.error('清除通知失败:', e);
|
||
}
|
||
}
|
||
|
||
// ====================================================================
|
||
// 工具方法
|
||
// ====================================================================
|
||
|
||
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 = `<i class="bi ${iconMap[type] || iconMap.info}"></i><span>${this.escapeHtml(message)}</span>`;
|
||
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\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
|
||
cleaned = cleaned.replace(/\s*on\w+\s*=\s*["'][^"']*["']/gi, '');
|
||
cleaned = cleaned.replace(/javascript\s*:/gi, '');
|
||
return cleaned;
|
||
}
|
||
}
|
||
|
||
// 初始化
|
||
const app = new MailManager();
|