dailiip/public/js/dashboard.js
2025-10-30 23:05:24 +08:00

590 lines
18 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 仪表板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 = `
<div class="table-responsive">
<table class="table table-sm history-table">
<thead>
<tr>
<th>任务名称</th>
<th>状态</th>
<th>开始时间</th>
<th>执行时长</th>
<th>结果</th>
</tr>
</thead>
<tbody>
`;
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 += `
<tr>
<td class="task-name">${item.task_name}</td>
<td><span class="badge ${statusClass}">${statusIcon} ${this.getStatusText(item.status)}</span></td>
<td>${this.formatDateTime(item.start_time)}</td>
<td class="duration">${duration}</td>
<td>${result}</td>
</tr>
`;
});
html += `
</tbody>
</table>
</div>
`;
return html;
}
// 生成空状态
generateEmptyState(message) {
return `
<div class="empty-state">
<i class="bi bi-inbox"></i>
<h6>${message}</h6>
<small class="text-muted">等待任务执行...</small>
</div>
`;
}
// 更新系统信息
updateSystemInfo(data) {
// 更新运行时间
document.getElementById('systemUptime').textContent = this.formatUptime(data.uptime);
// 更新内存使用
const memoryHtml = `
<div>已使用: ${data.memory.heapUsed}MB / ${data.memory.heapTotal}MB</div>
<div class="memory-bar">
<div class="memory-bar-fill" style="width: ${(data.memory.heapUsed / data.memory.heapTotal * 100)}%"></div>
</div>
`;
document.getElementById('memoryUsage').innerHTML = memoryHtml;
// 更新今日任务
const todayScrape = data.today_tasks.scrape;
const todayValidation = data.today_tasks.validation;
document.getElementById('todayScrape').innerHTML = `
<div class="d-flex justify-content-between align-items-center">
<span>成功: ${todayScrape.success}</span>
<span class="badge bg-success">${todayScrape.success_rate}%</span>
</div>
`;
document.getElementById('todayValidation').innerHTML = `
<div class="d-flex justify-content-between align-items-center">
<span>成功: ${todayValidation.success}</span>
<span class="badge bg-success">${todayValidation.success_rate}%</span>
</div>
`;
// 更新下次执行时间
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 = `
<div class="alert alert-${type} alert-dismissible fade show" role="alert" id="${alertId}">
<i class="bi bi-${this.getAlertIcon(type)}"></i>
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
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();
}
});