// 仪表板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 += `
| ${item.task_name} |
${statusIcon} ${this.getStatusText(item.status)} |
${this.formatDateTime(item.start_time)} |
${duration} |
${result} |
`;
});
html += `
`;
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 = `
${message}
`;
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();
}
});