// 全局状态
let csrfToken = null;
let currentAccount = null;
let currentGroupId = null;
let currentEmails = [];
let currentMethod = 'graph';
let currentFolder = 'inbox';
let isListVisible = true;
let groups = [];
let accountsCache = {};
let editingGroupId = null;
let selectedColor = '#B85C38';
let isTempEmailGroup = false;
let tempEmailGroupId = null;
let isLoadingMore = false;
let hasMoreEmails = true;
let currentSkip = 0;
let lastRefreshTime = null;
// 缓存与信任模式
let emailListCache = {};
let currentEmailDetail = null;
let isTrustedMode = false;
// 轮询相关
let pollingTimer = null;
let pollingCount = 0;
let maxPollingCount = 5;
let pollingInterval = 10;
let isPolling = false;
let knownEmailIds = new Set();
// 导航状态
let currentPage = 'dashboard';
// ==================== 主题 & 导航 ====================
function applyTheme(theme) {
document.documentElement.dataset.theme = theme;
localStorage.setItem('ol_theme', theme);
const btn = document.getElementById('themeToggleBtn');
if (btn) btn.textContent = theme === 'dark' ? '☀ 浅色模式' : '☾ 深色模式';
}
function toggleTheme() {
const current = document.documentElement.dataset.theme || 'light';
applyTheme(current === 'dark' ? 'light' : 'dark');
}
function navigate(page) {
currentPage = page;
// Hide all pages
document.querySelectorAll('.page').forEach(p => p.classList.add('page-hidden'));
const target = document.getElementById('page-' + page);
if (target) {
target.classList.remove('page-hidden');
target.style.display = '';
}
// Update nav active state
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
const navBtn = document.querySelector(`.nav-item[data-page="${page}"]`);
if (navBtn) navBtn.classList.add('active');
// Update topbar
updateTopbar(page);
// Close mobile sidebar
closeSidebar();
// Load page data
if (page === 'dashboard') loadDashboard();
if (page === 'mailbox') {
if (groups.length === 0) {
loadGroups();
} else if (currentGroupId) {
loadAccountsByGroup(currentGroupId);
}
}
if (page === 'temp-emails' && typeof loadTempEmails === 'function') loadTempEmails(true);
if (page === 'settings') loadSettings();
if (page === 'refresh-log') loadRefreshLogPage();
if (page === 'audit') loadAuditLogPage();
}
function updateTopbar(page) {
const titleEl = document.getElementById('topbarTitle');
const subtitleEl = document.getElementById('topbarSubtitle');
const actionsEl = document.getElementById('topbar-actions');
const titles = {
'dashboard': ['仪表盘', '系统概览'],
'mailbox': ['账号管理', '管理邮箱账号与查看邮件'],
'temp-emails': ['临时邮箱', '创建和管理临时邮箱'],
'refresh-log': ['刷新日志', 'Token 刷新历史记录'],
'settings': ['系统设置', '配置系统参数'],
'audit': ['审计日志', '系统操作记录']
};
const t = titles[page] || [page, ''];
if (titleEl) titleEl.textContent = t[0];
if (subtitleEl) subtitleEl.textContent = t[1];
// Context actions
if (actionsEl) {
if (page === 'mailbox') {
actionsEl.innerHTML = `
`;
} else if (page === 'temp-emails') {
actionsEl.innerHTML = `
`;
} else {
actionsEl.innerHTML = '';
}
}
}
function toggleSidebar() {
const isMobile = window.innerWidth <= 768;
if (isMobile) {
// Mobile: toggle drawer
const sidebar = document.getElementById('sidebar');
const backdrop = document.getElementById('sidebarBackdrop');
sidebar.classList.toggle('mob-open');
backdrop.classList.toggle('show');
} else {
// Desktop: toggle collapsed state
const app = document.getElementById('app');
app.classList.toggle('sidebar-collapsed');
try {
localStorage.setItem('ol_sidebar_collapsed', app.classList.contains('sidebar-collapsed'));
} catch(e) {}
}
}
function closeSidebar() {
const sidebar = document.getElementById('sidebar');
const backdrop = document.getElementById('sidebarBackdrop');
if (sidebar) sidebar.classList.remove('mob-open');
if (backdrop) backdrop.classList.remove('show');
}
function logout() {
if (!confirm('确认退出登录?')) return;
fetch('/logout', { method: 'POST' })
.then(() => window.location.href = '/login')
.catch(() => window.location.href = '/login');
}
// ==================== Dashboard ====================
async function loadDashboard() {
try {
const [groupsRes, tempRes] = await Promise.all([
fetch('/api/groups'),
fetch('/api/temp-emails').catch(() => ({ json: () => ({ success: false }) }))
]);
const groupsData = await groupsRes.json();
const tempData = await tempRes.json();
let totalAccounts = 0, validTokens = 0, expiredTokens = 0;
const groupSummary = [];
if (groupsData.success && groupsData.groups) {
for (const g of groupsData.groups) {
const accRes = await fetch(`/api/accounts?group_id=${g.id}`);
const accData = await accRes.json();
const accounts = accData.success ? (accData.accounts || []) : [];
totalAccounts += accounts.length;
accounts.forEach(a => {
if (a.last_refresh_status === 'failed') expiredTokens++;
else validTokens++;
});
groupSummary.push({ id: g.id, name: g.name, color: g.color || '#666', count: accounts.length, isTempGroup: g.name === '临时邮箱' });
}
}
// Update stat cards
const el = (id, val) => { const e = document.getElementById(id); if (e) e.textContent = val; };
el('statTotalAccounts', totalAccounts);
el('statValidTokens', validTokens);
el('statExpiredTokens', expiredTokens);
el('statTempEmails', tempData.success ? (tempData.emails || []).length : 0);
// Update topbar subtitle with summary
const sub = document.getElementById('topbarSubtitle');
if (sub && currentPage === 'dashboard') {
sub.textContent = `共 ${totalAccounts} 个账号 · ${validTokens} 个 Token 有效`;
}
// Group summary list
const groupListEl = document.getElementById('dashboardGroupList');
if (groupListEl) {
if (groupSummary.length === 0) {
groupListEl.innerHTML = '
暂无分组';
} else {
groupListEl.innerHTML = groupSummary.map(g => {
const clickAction = g.isTempGroup
? `navigate('temp-emails')`
: `navigate('mailbox'); setTimeout(function(){ selectGroup(${g.id}); }, 100)`;
return `${escapeHtml(g.name)}${g.count}`;
}).join('');
}
}
// Refresh log summary
const refreshListEl = document.getElementById('dashboardRefreshList');
if (refreshListEl) {
try {
const refreshRes = await fetch('/api/accounts/refresh-status');
const refreshData = await refreshRes.json();
if (refreshData.success && refreshData.refresh_status) {
const rs = refreshData.refresh_status;
refreshListEl.innerHTML = `
上次刷新时间${rs.last_refresh_time || '暂无'}
成功账号数${rs.success_count}
失败账号数${rs.failed_count}
`;
} else {
refreshListEl.innerHTML = '暂无刷新记录';
}
} catch (e) {
refreshListEl.innerHTML = '前往刷新日志查看详情';
}
}
} catch (e) {
console.error('Dashboard load error:', e);
}
}
// ==================== 分组搜索过滤 ====================
function filterGroups(query) {
const items = document.querySelectorAll('#groupList .group-item');
const q = query.toLowerCase();
items.forEach(item => {
const name = item.querySelector('.group-name');
if (name && name.textContent.toLowerCase().includes(q)) {
item.style.display = '';
} else {
item.style.display = q ? 'none' : '';
}
});
}
// ==================== 三栏拖拽调整 ====================
function initResizeHandles() {
document.querySelectorAll('.resize-handle').forEach(handle => {
handle.addEventListener('mousedown', function(e) {
e.preventDefault();
const leftId = this.dataset.left;
const rightId = this.dataset.right;
const leftPanel = document.getElementById(leftId);
const rightPanel = document.getElementById(rightId);
if (!leftPanel) return;
this.classList.add('active');
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
const startX = e.clientX;
const startWidth = leftPanel.offsetWidth;
function onMouseMove(ev) {
const delta = ev.clientX - startX;
const newWidth = Math.max(120, Math.min(startWidth + delta, 500));
leftPanel.style.width = newWidth + 'px';
}
function onMouseUp() {
handle.classList.remove('active');
document.body.style.cursor = '';
document.body.style.userSelect = '';
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
// Save widths to localStorage
try {
const widths = {};
document.querySelectorAll('.groups-column, .accounts-column').forEach(col => {
widths[col.id] = col.style.width;
});
localStorage.setItem('ol_column_widths', JSON.stringify(widths));
} catch(e) {}
}
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
});
// Restore saved widths
try {
const saved = JSON.parse(localStorage.getItem('ol_column_widths') || '{}');
Object.entries(saved).forEach(([id, width]) => {
const el = document.getElementById(id);
if (el && width) el.style.width = width;
});
} catch(e) {}
}
// ==================== 邮件详情显示控制 ====================
function showEmailDetailSection() {
const section = document.getElementById('emailDetailSection');
if (section) section.style.display = 'flex';
}
function hideEmailDetailSection() {
const section = document.getElementById('emailDetailSection');
if (section) section.style.display = 'none';
}
function stopRefresh() {
// Placeholder for stopping a bulk refresh operation
showToast('刷新已停止', 'warn');
const bar = document.getElementById('refreshProgressBar');
if (bar) bar.style.display = 'none';
}
// ==================== CSRF 防护 ====================
// 初始化 CSRF Token
async function initCSRFToken() {
try {
const response = await fetch('/api/csrf-token');
const data = await response.json();
csrfToken = data.csrf_token;
if (data.csrf_disabled) {
console.warn('CSRF protection is disabled. Install flask-wtf for better security.');
}
} catch (error) {
console.error('Failed to initialize CSRF token:', error);
}
}
// 包装 fetch 请求,自动添加 CSRF Token
const originalFetch = window.fetch;
window.fetch = function (url, options = {}) {
// 只对非 GET 请求添加 CSRF Token
if (options.method && options.method.toUpperCase() !== 'GET' && csrfToken) {
options.headers = options.headers || {};
if (options.headers instanceof Headers) {
options.headers.append('X-CSRFToken', csrfToken);
} else {
options.headers['X-CSRFToken'] = csrfToken;
}
}
return originalFetch(url, options);
};
// 初始化
document.addEventListener('DOMContentLoaded', async function () {
// 应用保存的主题
applyTheme(localStorage.getItem('ol_theme') || 'light');
// 恢复侧边栏折叠状态
try {
if (localStorage.getItem('ol_sidebar_collapsed') === 'true') {
document.getElementById('app').classList.add('sidebar-collapsed');
}
} catch(e) {}
// 初始化 CSRF Token
await initCSRFToken();
closeAllModals();
loadGroups();
if (typeof loadTags === 'function') {
loadTags();
}
initColorPicker();
initEmailListScroll();
initResizeHandles();
// 初始化轮询设置
initPollingSettings();
// 请求浏览器通知权限
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission();
}
// 绑定搜索框事件
const searchInput = document.getElementById('globalSearch');
if (searchInput) {
const debouncedSearch = debounce((e) => {
searchAccounts(e.target.value);
}, 300);
searchInput.addEventListener('input', debouncedSearch);
}
// 加载仪表盘
loadDashboard();
});
// 初始化颜色选择器
function initColorPicker() {
document.querySelectorAll('.color-option').forEach(option => {
option.addEventListener('click', function () {
document.querySelectorAll('.color-option').forEach(o => o.classList.remove('selected'));
this.classList.add('selected');
selectedColor = this.dataset.color;
// 同步更新自定义颜色输入框
document.getElementById('customColorInput').value = selectedColor;
document.getElementById('customColorHex').value = selectedColor;
});
});
}
// 初始化邮件列表滚动监听
function initEmailListScroll() {
const emailList = document.getElementById('emailList');
emailList.addEventListener('scroll', function () {
// 检查是否滚动到底部
if (emailList.scrollHeight - emailList.scrollTop <= emailList.clientHeight + 50) {
if (!isLoadingMore && hasMoreEmails && currentAccount && !isTempEmailGroup) {
loadMoreEmails();
}
}
});
}
// 加载更多邮件
async function loadMoreEmails() {
if (isLoadingMore || !hasMoreEmails) return;
isLoadingMore = true;
currentSkip += 20; // 每页20封
// 在列表底部显示加载状态
const emailList = document.getElementById('emailList');
const loadingDiv = document.createElement('div');
loadingDiv.className = 'loading-overlay';
loadingDiv.id = 'loadingMore';
loadingDiv.innerHTML = ' 加载更多…';
emailList.appendChild(loadingDiv);
// 禁用按钮
const refreshBtn = document.querySelector('.refresh-btn');
const folderTabs = document.querySelectorAll('.email-tab');
if (refreshBtn) {
refreshBtn.disabled = true;
}
folderTabs.forEach(tab => tab.disabled = true);
try {
const response = await fetch(
`/api/emails/${encodeURIComponent(currentAccount)}?method=${currentMethod}&folder=${currentFolder}&skip=${currentSkip}&top=20`
);
const data = await response.json();
if (data.success && data.emails.length > 0) {
// 追加新邮件到列表
currentEmails = currentEmails.concat(data.emails);
hasMoreEmails = data.has_more;
// 移除加载状态
const loadingEl = document.getElementById('loadingMore');
if (loadingEl) loadingEl.remove();
// 重新渲染邮件列表
renderEmailList(currentEmails);
// 更新邮件数量
document.getElementById('emailCount').textContent = `(${currentEmails.length})`;
// 更新缓存
if (currentAccount && !isTempEmailGroup) {
const cacheKey = `${currentAccount}_${currentFolder}`;
if (emailListCache[cacheKey]) {
emailListCache[cacheKey].emails = currentEmails;
emailListCache[cacheKey].has_more = hasMoreEmails;
emailListCache[cacheKey].skip = currentSkip;
}
}
} else {
hasMoreEmails = false;
// 显示"没有更多邮件"
const loadingEl = document.getElementById('loadingMore');
if (loadingEl) {
loadingEl.innerHTML = '没有更多邮件了
';
}
}
} catch (error) {
const loadingEl = document.getElementById('loadingMore');
if (loadingEl) loadingEl.remove();
showToast('加载失败', 'error');
} finally {
isLoadingMore = false;
// 启用按钮
if (refreshBtn) {
refreshBtn.disabled = false;
}
folderTabs.forEach(tab => tab.disabled = false);
}
}
// 切换文件夹(不触发查询)
function switchFolder(folder) {
if (currentFolder === folder) return;
currentFolder = folder;
// 更新按钮状态
document.querySelectorAll('.email-tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.folder === folder);
});
const cacheKey = `${currentAccount}_${folder}`;
// 检查是否有缓存
if (emailListCache[cacheKey]) {
const cache = emailListCache[cacheKey];
currentEmails = cache.emails;
hasMoreEmails = cache.has_more;
currentSkip = cache.skip;
currentMethod = cache.method || 'graph';
// 恢复 UI
const methodTag = document.getElementById('methodTag');
methodTag.textContent = currentMethod;
methodTag.style.display = 'inline';
document.getElementById('emailCount').textContent = `(${currentEmails.length})`;
renderEmailList(currentEmails);
} else {
// 清空邮件列表,显示提示
document.getElementById('emailList').innerHTML = `
📬
点击"获取邮件"按钮获取${folder === 'inbox' ? '收件箱' : '垃圾邮件'}
`;
document.getElementById('emailCount').textContent = '';
document.getElementById('methodTag').style.display = 'none';
// 重置分页状态
currentEmails = [];
currentSkip = 0;
hasMoreEmails = true;
}
}
// 选择自定义颜色(颜色选择器)
function selectCustomColor(color) {
selectedColor = color;
document.getElementById('customColorHex').value = color;
// 取消预设颜色的选中状态
document.querySelectorAll('.color-option').forEach(o => o.classList.remove('selected'));
}
// 选择自定义颜色(十六进制输入)
function selectCustomColorHex(value) {
// 验证十六进制颜色格式
const hexPattern = /^#[0-9A-Fa-f]{6}$/;
if (hexPattern.test(value)) {
selectedColor = value;
document.getElementById('customColorInput').value = value;
// 取消预设颜色的选中状态
document.querySelectorAll('.color-option').forEach(o => o.classList.remove('selected'));
} else {
showToast('请输入有效的十六进制颜色(如 #FF5500)', 'error');
}
}
// 显示消息提示
function showToast(message, type = 'info', errorDetail = null) {
let container = document.getElementById('toast-container');
if (!container) {
container = document.createElement('div');
container.id = 'toast-container';
container.setAttribute('aria-live', 'polite');
document.body.appendChild(container);
}
const toast = document.createElement('div');
toast.className = 'toast ' + type;
const messageSpan = document.createElement('span');
messageSpan.textContent = message;
toast.appendChild(messageSpan);
if (errorDetail && type === 'error') {
const detailLink = document.createElement('a');
detailLink.href = 'javascript:void(0)';
detailLink.textContent = ' [详情]';
detailLink.style.cssText = 'color:var(--clr-danger);text-decoration:underline;margin-left:8px;';
detailLink.onclick = function (e) {
e.stopPropagation();
showErrorDetailModal(errorDetail);
};
toast.appendChild(detailLink);
}
container.appendChild(toast);
const duration = (errorDetail && type === 'error') ? 8000 : 3000;
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateX(30px)';
setTimeout(() => toast.remove(), 300);
}, duration);
}
// 显示刷新错误信息
function showRefreshError(accountId, errorMessage, accountEmail) {
document.getElementById('refreshErrorModal').classList.add('show');
document.getElementById('refreshErrorEmail').textContent = `账号:${accountEmail || '未知'}`;
document.getElementById('refreshErrorMessage').textContent = errorMessage;
document.getElementById('editAccountFromErrorBtn').onclick = function () {
hideRefreshErrorModal();
showEditAccountModal(accountId);
};
}
// 隐藏刷新错误模态框
function hideRefreshErrorModal() {
document.getElementById('refreshErrorModal').classList.remove('show');
}
// ==================== 统一错误处理相关 ====================
// 显示统一错误详情模态框
function showErrorDetailModal(error) {
document.getElementById('errorDetailModal').classList.add('show');
document.getElementById('errorModalUserMessage').textContent = error.message || '发生未知错误';
document.getElementById('errorModalCode').textContent = error.code || '-';
document.getElementById('errorModalType').textContent = error.type || '-';
document.getElementById('errorModalStatus').textContent = error.status || '-';
document.getElementById('errorModalTraceId').textContent = error.trace_id || '-';
const detailsEl = document.getElementById('errorModalDetails');
const detailsContainer = document.getElementById('errorModalDetailsContainer');
const toggleBtn = document.getElementById('toggleTraceBtn');
detailsEl.textContent = error.details || '暂无详细技术堆栈信息';
// 重置堆栈显示状态
detailsContainer.style.display = 'none';
toggleBtn.textContent = '显示堆栈/细节';
}
// 隐藏统一错误详情模态框
function hideErrorDetailModal() {
document.getElementById('errorDetailModal').classList.remove('show');
}
// 邮件获取失败详情弹框
function showEmailFetchErrorModal(details) {
if (!details) return;
const methodNames = {
'graph': 'Graph API',
'imap_new': 'IMAP(新服务器)',
'imap_old': 'IMAP(旧服务器)'
};
function translateError(err) {
if (!err) return '未知错误';
// err 可能是 string 或 object
if (typeof err === 'string') return err;
const code = err.code || '';
const details = typeof err.details === 'string' ? err.details : JSON.stringify(err.details || '');
const msg = err.message || '';
// 翻译常见错误
if (code === 'GRAPH_TOKEN_EXCEPTION' && details.includes('ProxyError')) {
return '代理连接失败:无法连接到代理服务器,请检查代理地址是否正确以及代理是否在运行';
}
if (code === 'GRAPH_TOKEN_FAILED' || code === 'IMAP_TOKEN_FAILED') {
if (details.includes('invalid_grant')) {
return 'Token 已失效或权限不足:请重新授权登录或更换 refresh_token';
}
if (details.includes('invalid_client')) {
return 'Client ID 无效:请检查 client_id 配置是否正确';
}
return `令牌获取失败:${msg}`;
}
if (code === 'EMAIL_FETCH_FAILED') {
return `获取邮件失败:${msg}`;
}
if (code === 'IMAP_CONNECTION_FAILED') {
return 'IMAP 连接失败:无法连接到邮件服务器';
}
return msg || details || '未知错误';
}
let html = '';
const methods = ['graph', 'imap_new', 'imap_old'];
methods.forEach(method => {
const err = details[method];
if (err !== undefined) {
const name = methodNames[method] || method;
const reason = translateError(err);
const codeText = (err && typeof err === 'object') ? (err.code || '-') : '-';
html += `
${name}
${reason}
错误代码: ${codeText}
`;
}
});
if (!html) {
html = '无详细错误信息
';
}
document.getElementById('emailFetchErrorContent').innerHTML = html;
document.getElementById('emailFetchErrorModal').classList.add('show');
}
function hideEmailFetchErrorModal() {
document.getElementById('emailFetchErrorModal').classList.remove('show');
}
// 切换堆栈信息的显示/隐藏
function toggleStackTrace() {
const container = document.getElementById('errorModalDetailsContainer');
const btn = document.getElementById('toggleTraceBtn');
if (container.style.display === 'none') {
container.style.display = 'block';
btn.textContent = '隐藏堆栈/细节';
} else {
container.style.display = 'none';
btn.textContent = '显示堆栈/细节';
}
}
// 复制错误详情到剪贴板
function copyErrorDetails() {
const userMessage = document.getElementById('errorModalUserMessage').textContent;
const details = document.getElementById('errorModalDetails').textContent;
const code = document.getElementById('errorModalCode').textContent;
const type = document.getElementById('errorModalType').textContent;
const status = document.getElementById('errorModalStatus').textContent;
const traceId = document.getElementById('errorModalTraceId').textContent;
const fullErrorText = `
【用户错误信息】
${userMessage}
【错误详情】
Code: ${code}
Type: ${type}
Status: ${status}
Trace ID: ${traceId}
【技术堆栈/细节】
${details}
`.trim();
navigator.clipboard.writeText(fullErrorText).then(() => {
showToast('错误详情已复制', 'success');
}).catch(() => {
// 降级方案
const textarea = document.createElement('textarea');
textarea.value = fullErrorText;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
showToast('错误详情已复制', 'success');
});
}
// 统一处理 API 响应错误
function handleApiError(data, defaultMessage = '请求失败') {
if (!data.success) {
// 检查是否是统一错误格式
if (data.error && data.error.message) {
const error = data.error;
// 使用后端提供的 message 作为用户友好信息
const userMessage = error.message;
// 调用 showToast 携带完整的错误对象
showToast(userMessage, 'error', error);
} else {
// 兼容旧的或非标准错误格式
const errorMessage = data.error || defaultMessage;
showToast(errorMessage, 'error');
}
return true;
}
return false;
}
function escapeJs(str) {
if (!str) return '';
return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r');
}
// ==================== 工具函数 ====================
// HTML 转义
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 格式化日期
function formatDate(dateStr) {
if (!dateStr) return '';
try {
const date = new Date(dateStr);
if (isNaN(date.getTime())) return dateStr;
const now = new Date();
const isToday = date.toDateString() === now.toDateString();
if (isToday) {
return '今天 ' + date.toLocaleTimeString('zh-CN', {
timeZone: 'Asia/Shanghai',
hour: '2-digit',
minute: '2-digit'
});
} else {
return date.toLocaleDateString('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: 'long',
day: 'numeric'
}) + ' ' + date.toLocaleTimeString('zh-CN', {
timeZone: 'Asia/Shanghai',
hour: '2-digit',
minute: '2-digit'
});
}
} catch (e) {
return dateStr;
}
}
// ==================== OAuth Refresh Token 相关 ====================
// 显示获取 Refresh Token 模态框
async function showGetRefreshTokenModal() {
document.getElementById('getRefreshTokenModal').classList.add('show');
// 重置表单
document.getElementById('redirectUrlInput').value = '';
document.getElementById('refreshTokenResult').style.display = 'none';
document.getElementById('refreshTokenOutput').value = '';
// 重置按钮状态
const btn = document.getElementById('exchangeTokenBtn');
btn.disabled = false;
btn.textContent = '换取 Token';
btn.style.display = '';
// 获取授权 URL
try {
const response = await fetch('/api/oauth/auth-url');
const data = await response.json();
if (data.success) {
document.getElementById('authUrlInput').value = data.auth_url;
} else {
showToast('获取授权链接失败', 'error');
}
} catch (error) {
showToast('获取授权链接失败', 'error');
}
}
// 隐藏获取 Refresh Token 模态框
function hideGetRefreshTokenModal() {
document.getElementById('getRefreshTokenModal').classList.remove('show');
}
// 复制授权 URL
function copyAuthUrl() {
const input = document.getElementById('authUrlInput');
input.select();
document.execCommand('copy');
showToast('授权链接已复制到剪贴板', 'success');
}
// 打开授权 URL
function openAuthUrl() {
const url = document.getElementById('authUrlInput').value;
if (url) {
window.open(url, '_blank');
showToast('已在新窗口打开授权页面', 'info');
}
}
// 换取 Token
async function exchangeToken() {
const redirectUrl = document.getElementById('redirectUrlInput').value.trim();
if (!redirectUrl) {
showToast('请先粘贴授权后的完整 URL', 'error');
return;
}
const btn = document.getElementById('exchangeTokenBtn');
btn.disabled = true;
btn.textContent = '⏳ 换取中...';
try {
const response = await fetch('/api/oauth/exchange-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
redirected_url: redirectUrl
})
});
const data = await response.json();
if (data.success) {
btn.disabled = false;
btn.textContent = '换取 Token';
// 关闭 Token 弹窗,打开添加账号弹窗并预填充
hideGetRefreshTokenModal();
showAddAccountModal();
const inputEl = document.getElementById('accountInput');
if (inputEl) {
inputEl.value = `your@outlook.com----yourpassword----${data.client_id}----${data.refresh_token}`;
inputEl.select();
}
showToast('✅ Token 获取成功!请将邮箱和密码替换后点导入', 'success');
} else {
handleApiError(data, '换取 Token 失败');
btn.disabled = false;
btn.textContent = '换取 Token';
}
} catch (error) {
showToast('换取 Token 失败: ' + error.message, 'error');
btn.disabled = false;
btn.textContent = '换取 Token';
}
}
// ==================== 设置相关 ====================
// 显示设置模态框
async function showSettingsModal() {
document.getElementById('settingsModal').classList.add('show');
await loadSettings();
}
// 隐藏设置模态框
function hideSettingsModal() {
document.getElementById('settingsModal').classList.remove('show');
// 清空密码输入框
document.getElementById('settingsPassword').value = '';
}
// 加载设置
async function loadSettings() {
try {
const response = await fetch('/api/settings');
const data = await response.json();
if (data.success) {
// 密码不显示,只显示 API Key
document.getElementById('settingsApiKey').value = data.settings.gptmail_api_key || '';
// 密码框留空
document.getElementById('settingsPassword').value = '';
// 加载刷新配置
document.getElementById('refreshIntervalDays').value = data.settings.refresh_interval_days || '30';
document.getElementById('refreshDelaySeconds').value = data.settings.refresh_delay_seconds || '5';
document.getElementById('refreshCron').value = data.settings.refresh_cron || '0 2 * * *';
// 设置定时刷新开关
const enableScheduled = data.settings.enable_scheduled_refresh !== 'false';
document.getElementById('enableScheduledRefresh').checked = enableScheduled;
// 设置刷新策略单选框
const useCron = data.settings.use_cron_schedule === 'true';
document.querySelector('input[name="refreshStrategy"][value="' + (useCron ? 'cron' : 'days') + '"]').checked = true;
toggleRefreshStrategy();
// 加载轮询设置(后端返回 boolean,兼容处理)
const enablePolling = data.settings.enable_auto_polling === true || data.settings.enable_auto_polling === 'true';
document.getElementById('enableAutoPolling').checked = enablePolling;
document.getElementById('pollingInterval').value = data.settings.polling_interval || '10';
document.getElementById('pollingCount').value = data.settings.polling_count || '5';
}
} catch (error) {
showToast('加载设置失败', 'error');
}
}
// 切换刷新策略
function toggleRefreshStrategy() {
const strategy = document.querySelector('input[name="refreshStrategy"]:checked').value;
document.getElementById('daysStrategyContainer').style.display = strategy === 'days' ? 'block' : 'none';
document.getElementById('cronStrategyContainer').style.display = strategy === 'cron' ? 'block' : 'none';
}
// 选择 Cron 样例
async function selectCronExample(cronExpr) {
document.getElementById('refreshCron').value = cronExpr;
await validateCronExpression();
}
// 验证 Cron 表达式
async function validateCronExpression() {
const cronExpr = document.getElementById('refreshCron').value.trim();
const resultEl = document.getElementById('cronValidationResult');
if (!cronExpr) {
resultEl.innerHTML = '';
return;
}
try {
const response = await fetch('/api/settings/validate-cron', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cron_expression: cronExpr })
});
const data = await response.json();
if (data.success && data.valid) {
const nextRun = new Date(data.next_run).toLocaleString('zh-CN');
resultEl.innerHTML = `
✓ 表达式有效
下次执行: ${nextRun}
`;
} else {
resultEl.innerHTML = `
✗ ${data.error && data.error.message ? data.error.message : (data.error || '表达式无效')}
`;
}
} catch (error) {
resultEl.innerHTML = `
✗ 验证失败: ${error.message}
`;
}
}
// 保存设置
async function saveSettings() {
const password = document.getElementById('settingsPassword').value;
const apiKey = document.getElementById('settingsApiKey').value.trim();
const refreshDays = document.getElementById('refreshIntervalDays').value;
const refreshDelay = document.getElementById('refreshDelaySeconds').value;
const refreshCron = document.getElementById('refreshCron').value.trim();
const strategy = document.querySelector('input[name="refreshStrategy"]:checked').value;
const enableScheduled = document.getElementById('enableScheduledRefresh').checked;
// 轮询设置
const enablePolling = document.getElementById('enableAutoPolling').checked;
const pollingInterval = document.getElementById('pollingInterval').value;
const pollingCount = document.getElementById('pollingCount').value;
const settings = {};
// 只有输入了密码才更新密码
if (password) {
settings.login_password = password;
}
// API Key 可以为空(清除)
settings.gptmail_api_key = apiKey;
// 刷新配置
const days = parseInt(refreshDays);
const delay = parseInt(refreshDelay);
if (isNaN(days) || days < 1 || days > 90) {
showToast('刷新周期必须在 1-90 天之间', 'error');
return;
}
if (isNaN(delay) || delay < 0 || delay > 60) {
showToast('刷新间隔必须在 0-60 秒之间', 'error');
return;
}
settings.refresh_interval_days = days;
settings.refresh_delay_seconds = delay;
settings.use_cron_schedule = strategy === 'cron';
settings.enable_scheduled_refresh = enableScheduled;
if (strategy === 'cron') {
if (!refreshCron) {
showToast('请输入 Cron 表达式', 'error');
return;
}
settings.refresh_cron = refreshCron;
}
// 轮询配置验证
const pInterval = parseInt(pollingInterval);
const pCount = parseInt(pollingCount);
if (isNaN(pInterval) || pInterval < 5 || pInterval > 300) {
showToast('轮询间隔必须在 5-300 秒之间', 'error');
return;
}
// 0 表示持续轮询,1-100 表示有限次数
if (isNaN(pCount) || pCount < 0 || pCount > 100) {
showToast('轮询次数必须在 0-100 次之间(0 表示持续轮询)', 'error');
return;
}
settings.enable_auto_polling = enablePolling;
settings.polling_interval = pInterval;
settings.polling_count = pCount;
try {
const response = await fetch('/api/settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings)
});
const data = await response.json();
if (data.success) {
showToast('设置已保存,重启应用后生效', 'success');
hideSettingsModal();
} else {
handleApiError(data, '保存设置失败');
}
} catch (error) {
showToast('保存设置失败', 'error');
}
}
// ==================== 自动轮询功能 ====================
// 初始化轮询设置
async function initPollingSettings() {
try {
const response = await fetch('/api/settings');
const data = await response.json();
if (data.success) {
pollingInterval = parseInt(data.settings.polling_interval) || 10;
maxPollingCount = parseInt(data.settings.polling_count) || 5;
// 如果启用了自动轮询,则开始轮询(后端返回 boolean,兼容处理)
const enablePolling = data.settings.enable_auto_polling === true || data.settings.enable_auto_polling === 'true';
if (enablePolling && currentAccount) {
startPolling();
}
}
} catch (error) {
console.error('初始化轮询设置失败:', error);
}
}
// 开始轮询
function startPolling() {
if (isPolling || !currentAccount) {
console.log('[轮询] 无法启动: isPolling=', isPolling, ', currentAccount=', currentAccount);
return;
}
isPolling = true;
pollingCount = 0;
pollingErrorCount = 0; // 重置错误计数
// 初始化已知邮件ID集合
knownEmailIds = new Set(currentEmails.map(email => email.id));
console.log('[轮询] 已启动,间隔:', pollingInterval, '秒,最大次数:', maxPollingCount);
// 显示轮询状态指示器
showPollingStatusIndicator();
// 立即执行一次
pollForNewEmails();
// 设置定时器
pollingTimer = setInterval(pollForNewEmails, pollingInterval * 1000);
}
// 显示轮询状态指示器
function showPollingStatusIndicator() {
let indicator = document.getElementById('pollingStatusIndicator');
if (!indicator) {
indicator = document.createElement('div');
indicator.id = 'pollingStatusIndicator';
indicator.style.cssText = `
position: fixed;
bottom: 24px;
right: 24px;
background-color: #28a745;
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
box-shadow: 0 2px 8px rgba(40, 167, 69, 0.3);
z-index: 1000;
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
`;
indicator.innerHTML = `🔄 轮询中`;
indicator.onclick = () => {
if (confirm('是否停止轮询?')) {
stopPolling(false);
}
};
document.body.appendChild(indicator);
}
indicator.style.display = 'flex';
}
// 隐藏轮询状态指示器
function hidePollingStatusIndicator() {
const indicator = document.getElementById('pollingStatusIndicator');
if (indicator) {
indicator.style.display = 'none';
}
}
// 停止轮询
function stopPolling(silent = false) {
console.log('[轮询] 停止轮询, silent=', silent);
if (pollingTimer) {
clearInterval(pollingTimer);
pollingTimer = null;
}
isPolling = false;
pollingCount = 0;
pollingErrorCount = 0;
hideNewEmailIndicator();
hideAccountNewEmailDot();
hidePollingStatusIndicator();
// 用户主动停止时显示提示
if (!silent) {
showToast('已停止轮询', 'info');
}
}
// 轮询检查新邮件
let pollingErrorCount = 0; // 连续错误计数
const MAX_POLLING_ERRORS = 3; // 最大连续错误次数
async function pollForNewEmails() {
if (!currentAccount) {
stopPolling(true);
return;
}
pollingCount++;
try {
const response = await fetch(`/api/emails/${encodeURIComponent(currentAccount)}?skip=0&limit=10&folder=${currentFolder}`);
const data = await response.json();
if (data.success && data.emails) {
pollingErrorCount = 0; // 重置错误计数
const newEmails = data.emails.filter(email => !knownEmailIds.has(email.id));
if (newEmails.length > 0) {
// 发现新邮件
showNewEmailNotification(newEmails);
// 更新已知邮件ID集合
newEmails.forEach(email => knownEmailIds.add(email.id));
// 更新邮件列表(在现有列表前面插入新邮件)
currentEmails = [...newEmails, ...currentEmails];
renderEmailList(currentEmails);
document.getElementById('emailCount').textContent = `(${currentEmails.length})`;
// 显示新邮件指示器(账号列表项红点)
showAccountNewEmailDot(newEmails.length);
}
} else {
pollingErrorCount++;
}
} catch (error) {
console.error('轮询新邮件失败:', error);
pollingErrorCount++;
}
// 连续错误超过阈值,停止轮询并提示
if (pollingErrorCount >= MAX_POLLING_ERRORS) {
stopPolling(true);
showToast('轮询连续失败,已自动停止', 'error');
return;
}
// 检查是否达到最大轮询次数(0 表示持续轮询,不检查次数)
if (maxPollingCount > 0 && pollingCount >= maxPollingCount) {
stopPolling(true);
}
}
// 显示新邮件通知(包含邮箱地址和邮件主题)
function showNewEmailNotification(newEmails) {
const count = newEmails.length;
const firstEmail = newEmails[0];
const subject = firstEmail?.subject || '无主题';
// 请求浏览器通知权限
if ('Notification' in window && Notification.permission === 'granted') {
const title = count === 1 ? `新邮件 - ${currentAccount}` : `新邮件 (${count}封) - ${currentAccount}`;
const body = count === 1 ? subject : `${subject} 等 ${count} 封新邮件`;
new Notification(title, {
body: body,
icon: '/static/favicon.ico'
});
} else if ('Notification' in window && Notification.permission !== 'denied') {
Notification.requestPermission();
}
// 显示页面内通知(包含邮箱和主题)
const message = count === 1
? `📬 ${currentAccount}: ${subject}`
: `📬 ${currentAccount}: ${subject} 等 ${count} 封新邮件`;
showToast(message, 'success');
}
// 显示账号列表项上的红点
function showAccountNewEmailDot(count) {
// 在当前选中的账号项上显示红点
const activeItem = document.querySelector('.account-card.active');
if (activeItem) {
let dot = activeItem.querySelector('.new-email-badge');
if (!dot) {
dot = document.createElement('span');
dot.className = 'new-email-badge';
dot.style.cssText = `
position: absolute;
top: 8px;
right: 8px;
background-color: #dc3545;
color: white;
padding: 2px 6px;
border-radius: 10px;
font-size: 11px;
font-weight: 500;
min-width: 18px;
text-align: center;
`;
activeItem.style.position = 'relative';
activeItem.appendChild(dot);
}
dot.textContent = count;
dot.style.display = 'block';
}
// 同时保留右上角指示器(作为备用)
showNewEmailIndicator(count);
}
// 隐藏账号列表项上的红点
function hideAccountNewEmailDot() {
const dots = document.querySelectorAll('.new-email-badge');
dots.forEach(dot => dot.style.display = 'none');
}
// 显示新邮件指示器(红点)- 移到右下角
function showNewEmailIndicator(count) {
let indicator = document.getElementById('newEmailIndicator');
if (!indicator) {
indicator = document.createElement('div');
indicator.id = 'newEmailIndicator';
indicator.style.cssText = `
position: fixed;
bottom: 70px;
right: 24px;
background-color: #dc3545;
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
box-shadow: 0 2px 8px rgba(220, 53, 69, 0.3);
z-index: 1000;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
animation: slideIn 0.3s ease;
`;
indicator.innerHTML = ` 新邮件 (${count})`;
indicator.onclick = () => {
hideNewEmailIndicator();
hideAccountNewEmailDot();
// 滚动到邮件列表顶部
document.getElementById('emailList').scrollTop = 0;
};
document.body.appendChild(indicator);
// 添加动画样式
if (!document.getElementById('newEmailIndicatorStyles')) {
const style = document.createElement('style');
style.id = 'newEmailIndicatorStyles';
style.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
.new-email-dot {
width: 8px;
height: 8px;
background-color: white;
border-radius: 50%;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
`;
document.head.appendChild(style);
}
} else {
indicator.innerHTML = ` 新邮件 (${count})`;
indicator.style.display = 'flex';
}
}
// 隐藏新邮件指示器
function hideNewEmailIndicator() {
const indicator = document.getElementById('newEmailIndicator');
if (indicator) {
indicator.style.display = 'none';
}
}
// 切换轮询状态
function togglePolling() {
if (isPolling) {
stopPolling();
} else {
startPolling();
}
}
// ==================== 工具函数 ====================
// 相对时间格式化
function formatRelativeTime(timestamp) {
if (!timestamp) return '从未刷新';
const now = new Date();
// 如果时间戳不包含时区信息,假定为 UTC 时间并添加 Z
let dateStr = timestamp;
if (typeof dateStr === 'string' && !dateStr.includes('Z') && !dateStr.includes('+') && !dateStr.includes('-', 10)) {
dateStr = dateStr + 'Z';
}
const past = new Date(dateStr);
const diffMs = now - past;
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return '刚刚';
if (diffMins < 60) return `${diffMins} 分钟前`;
if (diffHours < 24) return `${diffHours} 小时前`;
if (diffDays < 30) return `${diffDays} 天前`;
return `${Math.floor(diffDays / 30)} 月前`;
}
// ==================== Token 刷新管理 ====================
// 显示刷新模态框
async function showRefreshModal() {
document.getElementById('refreshModal').classList.add('show');
// 加载统计数据
await loadRefreshStats();
// 自动加载失败列表(如果有失败记录)
await autoLoadFailedListIfNeeded();
}
// 自动加载失败列表(如果有失败记录)
async function autoLoadFailedListIfNeeded() {
try {
const response = await fetch('/api/accounts/refresh-logs/failed');
const data = await response.json();
if (data.success && data.logs && data.logs.length > 0) {
// 有失败记录,自动显示失败列表
showFailedListFromData(data.logs.map(log => ({
id: log.account_id,
email: log.account_email,
error: log.error_message
})));
}
} catch (error) {
console.error('自动加载失败列表失败:', error);
}
}
// 隐藏刷新模态框
function hideRefreshModal() {
const modal = document.getElementById('refreshModal');
modal.classList.remove('show');
// 确保所有内容都被隐藏,防止残留
const progress = document.getElementById('refreshProgress');
if (progress) {
progress.style.display = 'none';
}
const failedList = document.getElementById('failedListContainer');
if (failedList) {
failedList.style.display = 'none';
}
const logsContainer = document.getElementById('refreshLogsContainer');
if (logsContainer) {
logsContainer.style.display = 'none';
}
// 重置按钮状态
const refreshAllBtn = document.getElementById('refreshAllBtn');
if (refreshAllBtn) {
refreshAllBtn.disabled = false;
refreshAllBtn.textContent = '🔄 全量刷新';
}
const retryFailedBtn = document.getElementById('retryFailedBtn');
if (retryFailedBtn) {
retryFailedBtn.disabled = false;
retryFailedBtn.textContent = '🔁 重试失败';
}
}
// 加载刷新统计
async function loadRefreshStats() {
try {
const response = await fetch('/api/accounts/refresh-stats');
const data = await response.json();
console.log('刷新统计数据:', data);
if (data.success) {
const stats = data.stats;
// 优先使用保存的本地刷新时间
if (lastRefreshTime && lastRefreshTime instanceof Date) {
document.getElementById('lastRefreshTime').textContent = formatDateTime(lastRefreshTime.toISOString());
} else if (stats.last_refresh_time) {
document.getElementById('lastRefreshTime').textContent = formatDateTime(stats.last_refresh_time);
} else {
document.getElementById('lastRefreshTime').textContent = '-';
}
document.getElementById('totalRefreshCount').textContent = stats.total;
document.getElementById('successRefreshCount').textContent = stats.success_count;
document.getElementById('failedRefreshCount').textContent = stats.failed_count;
console.log('统计数据已更新到页面');
}
} catch (error) {
console.error('加载刷新统计失败:', error);
}
}
// 全量刷新所有账号
async function refreshAllAccounts() {
const btn = document.getElementById('refreshAllBtn');
const progress = document.getElementById('refreshProgress');
const progressText = document.getElementById('refreshProgressText');
if (btn.disabled) return;
if (!confirm('确定要刷新所有账号的 Token 吗?')) {
return;
}
btn.disabled = true;
btn.textContent = '刷新中...';
progress.style.display = 'block';
progressText.innerHTML = '正在初始化...';
try {
const eventSource = new EventSource('/api/accounts/trigger-scheduled-refresh?force=true');
let totalCount = 0;
let successCount = 0;
let failedCount = 0;
eventSource.onmessage = function (event) {
try {
const data = JSON.parse(event.data);
if (data.type === 'start') {
totalCount = data.total;
const delayInfo = data.delay_seconds > 0 ? `(间隔 ${data.delay_seconds} 秒)` : '';
progressText.innerHTML = `总共 ${totalCount} 个账号${delayInfo},准备开始刷新...`;
// 初始化统计
document.getElementById('totalRefreshCount').textContent = totalCount;
document.getElementById('successRefreshCount').textContent = '0';
document.getElementById('failedRefreshCount').textContent = '0';
} else if (data.type === 'progress') {
successCount = data.success_count;
failedCount = data.failed_count;
// 实时更新统计
document.getElementById('successRefreshCount').textContent = successCount;
document.getElementById('failedRefreshCount').textContent = failedCount;
progressText.innerHTML = `
正在处理: ${data.email}
进度: ${data.current}/${data.total} |
成功: ${successCount} |
失败: ${failedCount}
`;
} else if (data.type === 'delay') {
progressText.innerHTML += `
等待 ${data.seconds} 秒后继续...`;
} else if (data.type === 'complete') {
eventSource.close();
progress.style.display = 'none';
btn.disabled = false;
btn.textContent = '🔄 全量刷新';
// 直接更新统计数据,使用本地时间
const now = new Date();
lastRefreshTime = now; // 保存刷新时间
document.getElementById('lastRefreshTime').textContent = '刚刚';
document.getElementById('totalRefreshCount').textContent = data.total;
document.getElementById('successRefreshCount').textContent = data.success_count;
document.getElementById('failedRefreshCount').textContent = data.failed_count;
showToast(`刷新完成!成功: ${data.success_count}, 失败: ${data.failed_count}`,
data.failed_count > 0 ? 'warning' : 'success');
// 如果有失败的,显示失败列表
if (data.failed_count > 0) {
showFailedListFromData(data.failed_list);
}
// 刷新账号列表以更新刷新时间
if (currentGroupId) {
loadAccountsByGroup(currentGroupId, true);
}
}
} catch (e) {
console.error('解析进度数据失败:', e);
}
};
eventSource.onerror = function (error) {
console.error('EventSource 错误:', error);
eventSource.close();
progress.style.display = 'none';
btn.disabled = false;
btn.textContent = '🔄 全量刷新';
showToast('刷新过程中出现错误', 'error');
};
} catch (error) {
progress.style.display = 'none';
btn.disabled = false;
btn.textContent = '🔄 全量刷新';
showToast('刷新请求失败', 'error');
}
}
// 重试失败的账号
async function retryFailedAccounts() {
const btn = document.getElementById('retryFailedBtn');
const progress = document.getElementById('refreshProgress');
const progressText = document.getElementById('refreshProgressText');
if (btn.disabled) return;
btn.disabled = true;
btn.textContent = '重试中...';
progress.style.display = 'block';
progressText.textContent = '正在重试失败的账号...';
try {
const response = await fetch('/api/accounts/refresh-failed', {
method: 'POST'
});
const data = await response.json();
progress.style.display = 'none';
btn.disabled = false;
btn.textContent = '🔁 重试失败';
if (data.success) {
if (data.total === 0) {
showToast('没有需要重试的失败账号', 'info');
} else {
showToast(`重试完成!成功: ${data.success_count}, 失败: ${data.failed_count}`,
data.failed_count > 0 ? 'warning' : 'success');
// 刷新统计
loadRefreshStats();
// 如果还有失败的,显示失败列表
if (data.failed_count > 0) {
showFailedListFromData(data.failed_list);
} else {
hideFailedList();
}
}
} else {
handleApiError(data, '重试失败');
}
} catch (error) {
progress.style.display = 'none';
btn.disabled = false;
btn.textContent = '🔁 重试失败';
showToast('重试请求失败', 'error');
}
}
// 单个账号重试
async function retrySingleAccount(accountId, accountEmail) {
try {
const response = await fetch(`/api/accounts/${accountId}/retry-refresh`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showToast(`${accountEmail} 刷新成功`, 'success');
loadRefreshStats();
// 刷新失败列表
loadFailedLogs();
} else {
handleApiError(data, `${accountEmail} 刷新失败`);
}
} catch (error) {
handleApiError({ success: false, error: { message: '刷新请求失败', details: error.message, code: 'NETWORK_ERROR', type: 'Frontend' } });
}
}
// 显示失败列表(从数据)
function showFailedListFromData(failedList) {
const container = document.getElementById('failedListContainer');
const listEl = document.getElementById('failedList');
// 隐藏其他列表
hideRefreshLogs();
if (!failedList || failedList.length === 0) {
container.style.display = 'none';
return;
}
let html = '';
failedList.forEach(item => {
html += `
${escapeHtml(item.email)}
${escapeHtml(item.error || '未知错误')}
`;
});
listEl.innerHTML = html;
container.style.display = 'block';
}
// 隐藏失败列表
function hideFailedList() {
document.getElementById('failedListContainer').style.display = 'none';
}
// 加载失败日志
async function loadFailedLogs() {
const container = document.getElementById('failedListContainer');
const listEl = document.getElementById('failedList');
hideRefreshLogs();
try {
const response = await fetch('/api/accounts/refresh-logs/failed');
const data = await response.json();
if (data.success) {
if (data.logs.length === 0) {
listEl.innerHTML = '暂无失败状态的邮箱
';
} else {
let html = '';
data.logs.forEach(log => {
html += `
${escapeHtml(log.account_email)}
${escapeHtml(log.error_message || '未知错误')}
最后刷新: ${formatDateTime(log.created_at)}
`;
});
listEl.innerHTML = html;
}
container.style.display = 'block';
}
} catch (error) {
showToast('加载失败邮箱列表失败', 'error');
}
}
// 加载刷新历史
async function loadRefreshLogs() {
const container = document.getElementById('refreshLogsContainer');
const listEl = document.getElementById('refreshLogsList');
try {
const response = await fetch('/api/accounts/refresh-logs?limit=1000');
const data = await response.json();
if (data.success) {
if (data.logs.length === 0) {
listEl.innerHTML = '暂无全量刷新历史
';
} else {
listEl.innerHTML = `近半年刷新历史(共 ${data.logs.length} 条)
`;
let html = '';
data.logs.forEach(log => {
const statusColor = log.status === 'success' ? '#28a745' : '#dc3545';
const statusText = log.status === 'success' ? '成功' : '失败';
const typeText = log.refresh_type === 'manual' ? '手动' : '自动';
const typeColor = log.refresh_type === 'manual' ? '#007bff' : '#28a745';
const typeBgColor = log.refresh_type === 'manual' ? '#e7f3ff' : '#e8f5e9';
html += `
${escapeHtml(log.account_email)}
${typeText}
${statusText}
${formatDateTime(log.created_at)}
${log.error_message ? `
${escapeHtml(log.error_message)}
` : ''}
`;
});
listEl.innerHTML += html;
}
container.style.display = 'block';
}
} catch (error) {
showToast('加载刷新历史失败', 'error');
}
}
// 隐藏刷新历史
function hideRefreshLogs() {
document.getElementById('refreshLogsContainer').style.display = 'none';
}
// ==================== 页面级:刷新日志 ====================
async function loadRefreshLogPage() {
const container = document.getElementById('refreshLogContainer');
if (!container) return;
container.innerHTML = ' 加载中…
';
try {
const response = await fetch('/api/accounts/refresh-logs?limit=200');
const data = await response.json();
if (data.success && data.logs && data.logs.length > 0) {
container.innerHTML = `
共 ${data.logs.length} 条记录
${data.logs.map(log => {
const isSuccess = log.status === 'success';
const statusBadge = isSuccess
? '
成功'
: '
失败';
const typeText = log.refresh_type === 'manual' ? '手动' : (log.refresh_type === 'scheduled' ? '定时' : log.refresh_type || '-');
return `
${escapeHtml(log.account_email || '-')}
${formatDateTime(log.created_at)} · ${escapeHtml(typeText)}
${log.error_message ? `
${escapeHtml(log.error_message)}
` : ''}
${statusBadge}
`;
}).join('')}
`;
} else {
container.innerHTML = '';
}
} catch (error) {
container.innerHTML = '';
}
}
// ==================== 页面级:审计日志 ====================
async function loadAuditLogPage() {
const container = document.getElementById('auditLogContainer');
if (!container) return;
container.innerHTML = ' 加载中…
';
try {
const response = await fetch('/api/audit-logs?limit=200');
const data = await response.json();
if (data.success && data.logs && data.logs.length > 0) {
container.innerHTML = `
共 ${data.total || data.logs.length} 条记录
${data.logs.map(log => {
const actionColor = log.action === 'delete' ? 'var(--clr-danger)' : (log.action === 'create' ? 'var(--clr-jade)' : 'var(--clr-primary)');
return `
${escapeHtml(log.action || '-')}
${escapeHtml(log.resource_type || '-')}
${formatDateTime(log.created_at)}
${escapeHtml(log.resource_id || '-')}
${log.details ? `
${escapeHtml(log.details).substring(0, 200)}
` : ''}
IP: ${escapeHtml(log.user_ip || '-')} ${log.trace_id ? '· trace: ' + escapeHtml(log.trace_id) : ''}
`;
}).join('')}
`;
} else {
container.innerHTML = '';
}
} catch (error) {
container.innerHTML = '';
}
}
// 格式化日期时间
function formatDateTime(dateStr) {
if (!dateStr) return '-';
let date;
if (dateStr instanceof Date) {
date = dateStr;
} else {
// 如果字符串不包含时区信息,假定为 UTC 时间
if (!dateStr.includes('Z') && !dateStr.includes('+') && !dateStr.includes('-', 10)) {
dateStr = dateStr + 'Z';
}
date = new Date(dateStr);
}
const now = new Date();
const diff = now - date;
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(diff / 3600000);
const days = Math.floor(diff / 86400000);
if (minutes < 1) return '刚刚';
if (minutes < 60) return `${minutes}分钟前`;
if (hours < 24) return `${hours}小时前`;
if (days < 7) return `${days}天前`;
return date.toLocaleString('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
// 统一关闭所有模态框的函数 (修复 bug:防止模态框意外残留)
function closeAllModals() {
hideAddGroupModal();
hideAddAccountModal();
hideEditAccountModal();
hideExportModal();
hideSettingsModal();
hideRefreshModal();
hideRefreshErrorModal();
hideErrorDetailModal();
hideGetRefreshTokenModal();
closeFullscreenEmail();
}
// HTML 转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 键盘快捷键
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') {
hideAddGroupModal();
hideAddAccountModal();
hideEditAccountModal();
hideExportModal();
hideSettingsModal();
hideRefreshModal();
hideRefreshErrorModal();
hideErrorDetailModal();
hideGetRefreshTokenModal();
closeFullscreenEmail();
}
});
// ==================== 标签管理 ====================
let allTags = [];
// 显示标签管理模态框
async function showTagManagementModal() {
document.getElementById('tagManagementModal').classList.add('show');
await loadTags();
}
// 隐藏标签管理模态框
function hideTagManagementModal() {
document.getElementById('tagManagementModal').classList.remove('show');
}
// 加载标签列表
async function loadTags() {
try {
const response = await fetch('/api/tags');
const data = await response.json();
if (data.success) {
allTags = data.tags;
renderTagList();
updateTagFilter(); // Update Filter Dropdown
}
} catch (error) {
showToast('加载标签失败', 'error');
}
}
// 更新标签筛选下拉框
function updateTagFilter() {
const container = document.getElementById('tagFilterContainer');
if (!container) return;
if (allTags.length === 0) {
container.style.display = 'none';
return;
}
container.style.display = 'flex';
let html = '';
allTags.forEach(tag => {
html += `
`;
});
container.innerHTML = html;
/* Old dropdown code removed */
}
// 渲染标签列表
function renderTagList() {
const listEl = document.getElementById('tagList');
if (!allTags.length) {
listEl.innerHTML = '暂无标签
';
return;
}
let html = '';
allTags.forEach(tag => {
html += `
${escapeHtml(tag.name)}
`;
});
listEl.innerHTML = html;
}
// 创建标签
async function createTag() {
const nameInput = document.getElementById('newTagName');
const colorInput = document.getElementById('newTagColor');
const name = nameInput.value.trim();
const color = colorInput.value;
if (!name) {
showToast('请输入标签名称', 'error');
return;
}
try {
const response = await fetch('/api/tags', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, color })
});
const data = await response.json();
if (data.success) {
nameInput.value = '';
showToast('标签创建成功', 'success');
await loadTags();
// 刷新账号列表以重新加载标签(如果是在查看列表时添加标签,可能不需要立即刷新列表,但为了保持一致性可以刷新)
// 但通常添加标签不影响当前列表显示,除非是给账号打标
} else {
showToast(data.error || '创建失败', 'error');
}
} catch (error) {
showToast('创建标签失败', 'error');
}
}
// 删除标签
async function deleteTag(id) {
if (!confirm('确定要删除这个标签吗?')) return;
try {
const response = await fetch(`/api/tags/${id}`, { method: 'DELETE' });
const data = await response.json();
if (data.success) {
showToast('标签已删除', 'success');
await loadTags();
// 刷新账号列表以更新标签显示
if (currentGroupId) {
loadAccountsByGroup(currentGroupId, true);
}
} else {
showToast(data.error || '删除失败', 'error');
}
} catch (error) {
showToast('删除标签失败', 'error');
}
}
// ==================== 批量操作 ====================
// 全局选中的账号 ID 集合(跨分组保持)
let selectedAccountIds = new Set();
// 更新批量操作栏状态
function updateBatchActionBar() {
// 同步 DOM 复选框状态到全局 Set
const allCheckboxes = document.querySelectorAll('.account-select-checkbox');
allCheckboxes.forEach(cb => {
const id = parseInt(cb.value);
if (cb.checked) {
selectedAccountIds.add(id);
} else {
selectedAccountIds.delete(id);
}
});
const bar = document.getElementById('batchActionBar');
const countSpan = document.getElementById('selectedCount');
if (selectedAccountIds.size > 0) {
bar.style.display = 'flex';
countSpan.textContent = `已选 ${selectedAccountIds.size} 项`;
} else {
bar.style.display = 'none';
}
}
// 显示批量删除确认
function showBatchDeleteConfirm() {
if (selectedAccountIds.size === 0) {
showToast('请选择要删除的账号', 'error');
return;
}
if (!confirm(`确定要删除选中的 ${selectedAccountIds.size} 个账号吗?此操作不可恢复!`)) {
return;
}
batchDeleteAccounts();
}
// 批量删除账号
async function batchDeleteAccounts() {
const accountIds = Array.from(selectedAccountIds);
// 确保使用最新的 CSRF token
await initCSRFToken();
try {
const response = await fetch('/api/accounts/batch-delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ account_ids: accountIds })
});
const data = await response.json();
if (data.success) {
showToast(data.message, 'success');
// 清空选中状态
selectedAccountIds.clear();
// 刷新分组和邮箱列表
loadGroups();
if (currentGroupId) {
delete accountsCache[currentGroupId];
loadAccountsByGroup(currentGroupId, true);
}
// 更新批量操作栏
updateBatchActionBar();
} else {
showToast(data.error || '删除失败', 'error');
}
} catch (error) {
showToast('删除失败', 'error');
}
}
let batchActionType = ''; // 'add' or 'remove'
// 显示批量打标模态框
async function showBatchTagModal(type) {
batchActionType = type;
document.getElementById('batchTagTitle').textContent = type === 'add' ? '批量添加标签' : '批量移除标签';
document.getElementById('batchTagModal').classList.add('show');
// 加载标签选项
await loadTagsForSelect();
}
function hideBatchTagModal() {
document.getElementById('batchTagModal').classList.remove('show');
}
// 加载标签到下拉框
async function loadTagsForSelect() {
const select = document.getElementById('batchTagSelect');
select.innerHTML = '';
try {
const response = await fetch('/api/tags');
const data = await response.json();
if (data.success) {
let html = '';
data.tags.forEach(tag => {
html += ``;
});
select.innerHTML = html;
}
} catch (error) {
select.innerHTML = '';
}
}
// 确认批量打标
async function confirmBatchTag() {
const tagId = document.getElementById('batchTagSelect').value;
if (!tagId) {
showToast('请选择标签', 'error');
return;
}
const accountIds = Array.from(selectedAccountIds);
if (accountIds.length === 0) return;
try {
const response = await fetch('/api/accounts/tags', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
account_ids: accountIds,
tag_id: parseInt(tagId),
action: batchActionType
})
});
const data = await response.json();
if (data.success) {
showToast(data.message, 'success');
hideBatchTagModal();
// 清空选中状态
selectedAccountIds.clear();
// 刷新列表
loadGroups();
if (currentGroupId) {
delete accountsCache[currentGroupId];
loadAccountsByGroup(currentGroupId, true);
}
updateBatchActionBar();
} else {
showToast(data.error || '操作失败', 'error');
}
} catch (error) {
showToast('请求失败', 'error');
}
}
// ==================== 批量移动分组 ====================
// 显示批量移动分组模态框
async function showBatchMoveGroupModal() {
document.getElementById('batchMoveGroupModal').classList.add('show');
await loadGroupsForBatchMove();
}
function hideBatchMoveGroupModal() {
document.getElementById('batchMoveGroupModal').classList.remove('show');
}
// 加载分组到下拉框
async function loadGroupsForBatchMove() {
const select = document.getElementById('batchMoveGroupSelect');
select.innerHTML = '';
try {
const response = await fetch('/api/groups');
const data = await response.json();
if (data.success) {
let html = '';
data.groups.filter(g => !g.is_system).forEach(group => {
html += ``;
});
select.innerHTML = html;
}
} catch (error) {
select.innerHTML = '';
}
}
// 确认批量移动分组
async function confirmBatchMoveGroup() {
const groupId = document.getElementById('batchMoveGroupSelect').value;
if (!groupId) {
showToast('请选择目标分组', 'error');
return;
}
const accountIds = Array.from(selectedAccountIds);
if (accountIds.length === 0) return;
try {
const response = await fetch('/api/accounts/batch-update-group', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
account_ids: accountIds,
group_id: parseInt(groupId)
})
});
const data = await response.json();
if (data.success) {
showToast(data.message, 'success');
hideBatchMoveGroupModal();
// 清空选中状态
selectedAccountIds.clear();
// 刷新分组列表
loadGroups();
// 刷新当前分组的邮箱列表
if (currentGroupId) {
delete accountsCache[currentGroupId];
loadAccountsByGroup(currentGroupId, true);
}
updateBatchActionBar();
} else {
showToast(data.error || '操作失败', 'error');
}
} catch (error) {
showToast('请求失败', 'error');
}
}