// 仪表板JavaScript class Dashboard { constructor() { this.charts = {}; this.refreshInterval = null; this.init(); } async init() { console.log('初始化仪表板...'); // 初始化图表 this.initCharts(); // 加载初始数据 await this.loadDashboardData(); // 启动自动刷新 this.startAutoRefresh(); // 更新时间显示 this.updateCurrentTime(); setInterval(() => this.updateCurrentTime(), 1000); console.log('仪表板初始化完成'); } // 初始化图表 initCharts() { // 代理趋势图 const ctx = document.getElementById('proxyTrendChart'); if (ctx) { this.charts.proxyTrend = new Chart(ctx, { type: 'line', data: { labels: [], datasets: [{ label: '总代理数', data: [], borderColor: '#007bff', backgroundColor: 'rgba(0, 123, 255, 0.1)', tension: 0.4, fill: true }, { label: '可用代理', data: [], borderColor: '#28a745', backgroundColor: 'rgba(40, 167, 69, 0.1)', tension: 0.4, fill: true }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'top', }, tooltip: { mode: 'index', intersect: false, } }, scales: { y: { beginAtZero: true, ticks: { precision: 0 } } } } }); } } // 加载仪表板数据 async loadDashboardData() { try { // 显示加载状态 this.showLoading(true); // 获取统计数据 const statsResponse = await fetch('/api/dashboard/stats'); const statsData = await statsResponse.json(); if (statsData.success) { this.updateStatistics(statsData.data); this.updateCharts(statsData.data.charts); this.updateRecentHistory(statsData.data.latest); } // 获取实时状态 const statusResponse = await fetch('/api/dashboard/status'); const statusData = await statusResponse.json(); if (statusData.success) { this.updateSystemInfo(statusData.data); } } catch (error) { console.error('加载仪表板数据失败:', error); this.showAlert('加载数据失败: ' + error.message, 'danger'); } finally { this.showLoading(false); } } // 更新统计信息 updateStatistics(data) { document.getElementById('totalProxies').textContent = data.proxies.total || 0; document.getElementById('validProxies').textContent = data.proxies.valid || 0; document.getElementById('invalidProxies').textContent = data.proxies.invalid || 0; document.getElementById('validRate').textContent = data.proxies.validRate || '0%'; } // 更新图表 updateCharts(chartData) { // 更新代理趋势图 if (this.charts.proxyTrend && chartData.daily_proxies) { const labels = chartData.daily_proxies.map(item => new Date(item.date).toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' }) ); this.charts.proxyTrend.data.labels = labels; this.charts.proxyTrend.data.datasets[0].data = chartData.daily_proxies.map(item => item.total_added); this.charts.proxyTrend.data.datasets[1].data = chartData.daily_proxies.map(item => item.valid_added); this.charts.proxyTrend.update(); } } // 更新最近历史记录 updateRecentHistory(latest) { // 更新抓取历史 if (latest.scrape && latest.scrape.length > 0) { const scrapeHtml = this.generateHistoryTable(latest.scrape, 'scrape'); document.getElementById('recentScrapeHistory').innerHTML = scrapeHtml; } else { document.getElementById('recentScrapeHistory').innerHTML = this.generateEmptyState('暂无抓取历史'); } // 更新验证历史 if (latest.validation && latest.validation.length > 0) { const validationHtml = this.generateHistoryTable(latest.validation, 'validation'); document.getElementById('recentValidationHistory').innerHTML = validationHtml; } else { document.getElementById('recentValidationHistory').innerHTML = this.generateEmptyState('暂无验证历史'); } } // 生成历史记录表格 generateHistoryTable(history, type) { if (!history || history.length === 0) { return this.generateEmptyState('暂无历史记录'); } let html = `
`; history.forEach(item => { const statusClass = this.getStatusClass(item.status); const statusIcon = this.getStatusIcon(item.status); const duration = item.duration ? this.formatDuration(item.duration) : '-'; const result = this.getResultSummary(item.details, type); html += ` `; }); html += `
任务名称 状态 开始时间 执行时长 结果
${item.task_name} ${statusIcon} ${this.getStatusText(item.status)} ${this.formatDateTime(item.start_time)} ${duration} ${result}
`; return html; } // 生成空状态 generateEmptyState(message) { return `
${message}
等待任务执行...
`; } // 更新系统信息 updateSystemInfo(data) { // 更新运行时间 document.getElementById('systemUptime').textContent = this.formatUptime(data.uptime); // 更新内存使用 const memoryHtml = `
已使用: ${data.memory.heapUsed}MB / ${data.memory.heapTotal}MB
`; document.getElementById('memoryUsage').innerHTML = memoryHtml; // 更新今日任务 const todayScrape = data.today_tasks.scrape; const todayValidation = data.today_tasks.validation; document.getElementById('todayScrape').innerHTML = `
成功: ${todayScrape.success} ${todayScrape.success_rate}%
`; document.getElementById('todayValidation').innerHTML = `
成功: ${todayValidation.success} ${todayValidation.success_rate}%
`; // 更新下次执行时间 if (data.next_runs) { document.getElementById('nextScrape').textContent = this.formatTime(data.next_runs.scrape); document.getElementById('nextValidation').textContent = this.formatTime(data.next_runs.validation); } } // 开始抓取任务 async startScrape() { try { this.showLoading(true, '正在启动抓取任务...'); const response = await fetch('/api/dashboard/actions/scrape', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ pages: 5 }) }); const result = await response.json(); if (result.success) { this.showAlert('抓取任务已启动', 'success'); // 延迟刷新数据 setTimeout(() => this.loadDashboardData(), 2000); } else { this.showAlert('启动抓取任务失败: ' + result.error, 'danger'); } } catch (error) { console.error('启动抓取任务失败:', error); this.showAlert('启动抓取任务失败: ' + error.message, 'danger'); } finally { this.showLoading(false); } } // 开始验证任务 async startValidate() { try { this.showLoading(true, '正在启动验证任务...'); const response = await fetch('/api/dashboard/actions/validate', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ limit: 50 }) }); const result = await response.json(); if (result.success) { this.showAlert('验证任务已启动', 'success'); // 延迟刷新数据 setTimeout(() => this.loadDashboardData(), 2000); } else { this.showAlert('启动验证任务失败: ' + result.error, 'danger'); } } catch (error) { console.error('启动验证任务失败:', error); this.showAlert('启动验证任务失败: ' + error.message, 'danger'); } finally { this.showLoading(false); } } // 清理无效代理 async cleanupInvalid() { if (!confirm('确定要清理所有无效代理吗?此操作不可恢复。')) { return; } try { const response = await fetch('/api/proxies/cleanup', { method: 'DELETE' }); const result = await response.json(); if (result.success) { this.showAlert(result.message, 'success'); await this.loadDashboardData(); } else { this.showAlert('清理失败: ' + result.error, 'danger'); } } catch (error) { console.error('清理无效代理失败:', error); this.showAlert('清理无效代理失败: ' + error.message, 'danger'); } } // 导出代理数据 async exportProxies() { try { this.showLoading(true, '正在导出数据...'); const response = await fetch('/api/proxies?limit=1000'); const result = await response.json(); if (result.success) { const csv = this.convertToCSV(result.data); this.downloadCSV(csv, `proxies_${new Date().toISOString().slice(0, 10)}.csv`); this.showAlert('数据导出成功', 'success'); } else { this.showAlert('导出数据失败: ' + result.error, 'danger'); } } catch (error) { console.error('导出数据失败:', error); this.showAlert('导出数据失败: ' + error.message, 'danger'); } finally { this.showLoading(false); } } // 工具函数 getStatusClass(status) { const statusClasses = { 'success': 'bg-success', 'failed': 'bg-danger', 'running': 'bg-warning', 'pending': 'bg-secondary' }; return statusClasses[status] || 'bg-secondary'; } getStatusIcon(status) { const statusIcons = { 'success': '✓', 'failed': '✗', 'running': '⏳', 'pending': '⏸' }; return statusIcons[status] || '?'; } getStatusText(status) { const statusTexts = { 'success': '成功', 'failed': '失败', 'running': '运行中', 'pending': '等待中' }; return statusTexts[status] || status; } getResultSummary(details, type) { if (!details) return '-'; if (type === 'scrape') { return `抓取 ${details.scraped || 0} 个,可用 ${details.valid || 0} 个`; } else if (type === 'validation') { return `验证 ${details.validated || 0} 个,有效 ${details.valid || 0} 个`; } return '-'; } formatDateTime(dateString) { const date = new Date(dateString); return date.toLocaleString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } formatTime(dateString) { const date = new Date(dateString); return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }); } formatDuration(ms) { if (!ms) return '-'; if (ms < 1000) { return ms + 'ms'; } else if (ms < 60000) { return (ms / 1000).toFixed(1) + 's'; } else { return (ms / 60000).toFixed(1) + 'min'; } } formatUptime(seconds) { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); return `${hours}h ${minutes}m`; } updateCurrentTime() { const now = new Date(); const timeString = now.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); const timeElement = document.getElementById('currentTime'); if (timeElement) { timeElement.textContent = timeString; } } showLoading(show, message = '加载中...') { // 实现加载状态显示 const loadingElements = document.querySelectorAll('.spinner-border'); loadingElements.forEach(element => { element.style.display = show ? 'inline-block' : 'none'; }); } showAlert(message, type = 'info') { const alertContainer = document.getElementById('alertContainer'); if (!alertContainer) return; const alertId = 'alert-' + Date.now(); const alertHtml = ` `; alertContainer.insertAdjacentHTML('beforeend', alertHtml); // 自动移除提示 setTimeout(() => { const alertElement = document.getElementById(alertId); if (alertElement) { alertElement.remove(); } }, 5000); } getAlertIcon(type) { const icons = { 'success': 'check-circle-fill', 'danger': 'exclamation-triangle-fill', 'warning': 'exclamation-triangle-fill', 'info': 'info-circle-fill' }; return icons[type] || 'info-circle-fill'; } convertToCSV(data) { if (!data || data.length === 0) return ''; const headers = ['IP', '端口', '位置', '响应时间', '是否可用', '创建时间']; const csvRows = [headers.join(',')]; data.forEach(proxy => { const row = [ proxy.ip, proxy.port, `"${proxy.location || ''}"`, proxy.response_time || '', proxy.is_valid ? '是' : '否', proxy.created_at ]; csvRows.push(row.join(',')); }); return csvRows.join('\n'); } downloadCSV(csv, filename) { const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); link.setAttribute('href', url); link.setAttribute('download', filename); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); } startAutoRefresh() { // 每30秒刷新一次数据 this.refreshInterval = setInterval(() => { this.loadDashboardData(); }, 30000); } stopAutoRefresh() { if (this.refreshInterval) { clearInterval(this.refreshInterval); this.refreshInterval = null; } } } // 全局函数(供HTML调用) let dashboard; async function refreshData() { if (dashboard) { await dashboard.loadDashboardData(); dashboard.showAlert('数据已刷新', 'info'); } } async function startScrape() { if (dashboard) { await dashboard.startScrape(); } } async function startValidate() { if (dashboard) { await dashboard.startValidate(); } } async function cleanupInvalid() { if (dashboard) { await dashboard.cleanupInvalid(); } } async function exportProxies() { if (dashboard) { await dashboard.exportProxies(); } } // 页面加载完成后初始化 document.addEventListener('DOMContentLoaded', () => { dashboard = new Dashboard(); }); // 页面卸载时清理 window.addEventListener('beforeunload', () => { if (dashboard) { dashboard.stopAutoRefresh(); } });