604 lines
21 KiB
JavaScript
604 lines
21 KiB
JavaScript
// 系统监控页面JavaScript
|
||
|
||
class SystemMonitor {
|
||
constructor() {
|
||
this.charts = {};
|
||
this.refreshInterval = null;
|
||
this.refreshProgressInterval = null;
|
||
this.isRefreshing = false;
|
||
|
||
this.init();
|
||
}
|
||
|
||
async init() {
|
||
console.log('初始化系统监控页面...');
|
||
|
||
// 初始化图表
|
||
this.initCharts();
|
||
|
||
// 加载初始数据
|
||
await this.loadSystemStatus();
|
||
await this.loadRecentData();
|
||
|
||
// 启动自动刷新
|
||
this.startAutoRefresh();
|
||
|
||
// 启动刷新进度条
|
||
this.startRefreshProgress();
|
||
|
||
console.log('系统监控页面初始化完成');
|
||
}
|
||
|
||
initCharts() {
|
||
// 代理池趋势图
|
||
const proxyPoolCtx = document.getElementById('proxyPoolChart');
|
||
if (proxyPoolCtx) {
|
||
this.charts.proxyPool = new Chart(proxyPoolCtx, {
|
||
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',
|
||
}
|
||
},
|
||
scales: {
|
||
y: {
|
||
beginAtZero: true,
|
||
ticks: {
|
||
precision: 0
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// 任务执行率图
|
||
const taskRateCtx = document.getElementById('taskRateChart');
|
||
if (taskRateCtx) {
|
||
this.charts.taskRate = new Chart(taskRateCtx, {
|
||
type: 'line',
|
||
data: {
|
||
labels: [],
|
||
datasets: [{
|
||
label: '成功率 (%)',
|
||
data: [],
|
||
borderColor: '#17a2b8',
|
||
backgroundColor: 'rgba(23, 162, 184, 0.1)',
|
||
tension: 0.4,
|
||
fill: true
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: {
|
||
position: 'top',
|
||
}
|
||
},
|
||
scales: {
|
||
y: {
|
||
beginAtZero: true,
|
||
max: 100,
|
||
ticks: {
|
||
callback: function(value) {
|
||
return value + '%';
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
async loadSystemStatus() {
|
||
try {
|
||
this.setRefreshing(true);
|
||
|
||
// 获取系统状态
|
||
const statusResponse = await fetch('/api/dashboard/status');
|
||
const statusData = await statusResponse.json();
|
||
|
||
if (statusData.success) {
|
||
this.updateSystemOverview(statusData.data);
|
||
this.updateTasksStatus(statusData.data);
|
||
this.updateProxyPoolStatus(statusData.data.proxies);
|
||
this.updateResourceUsage(statusData.data);
|
||
}
|
||
|
||
// 更新最后刷新时间
|
||
this.updateLastRefreshTime();
|
||
|
||
} catch (error) {
|
||
console.error('加载系统状态失败:', error);
|
||
this.showAlert('加载系统状态失败: ' + error.message, 'danger');
|
||
} finally {
|
||
this.setRefreshing(false);
|
||
}
|
||
}
|
||
|
||
async loadRecentData() {
|
||
try {
|
||
// 加载图表数据
|
||
await this.updateCharts();
|
||
|
||
// 加载最近日志
|
||
await this.loadRecentLogs();
|
||
|
||
// 加载最近事件
|
||
await this.loadRecentEvents();
|
||
|
||
} catch (error) {
|
||
console.error('加载最近数据失败:', error);
|
||
}
|
||
}
|
||
|
||
updateSystemOverview(data) {
|
||
// 更新系统状态指示器
|
||
const indicator = document.getElementById('systemStatusIndicator');
|
||
const statusText = document.getElementById('systemStatusText');
|
||
const healthBadge = document.getElementById('systemHealth');
|
||
|
||
if (data.proxies && data.proxies.valid > 0) {
|
||
indicator.className = 'status-indicator online';
|
||
statusText.textContent = '在线';
|
||
healthBadge.textContent = '健康';
|
||
healthBadge.className = 'badge bg-success';
|
||
} else {
|
||
indicator.className = 'status-indicator offline';
|
||
statusText.textContent = '异常';
|
||
healthBadge.textContent = '异常';
|
||
healthBadge.className = 'badge bg-danger';
|
||
}
|
||
|
||
// 更新运行时间
|
||
document.getElementById('systemUptime').textContent = this.formatUptime(data.uptime);
|
||
|
||
// 更新内存使用
|
||
const memoryHtml = `
|
||
<div>已使用: ${data.memory.heapUsed}MB</div>
|
||
<div class="progress mt-1" style="height: 8px;">
|
||
<div class="progress-bar" style="width: ${(data.memory.heapUsed / data.memory.heapTotal * 100)}%"></div>
|
||
</div>
|
||
<small class="text-muted">总计: ${data.memory.heapTotal}MB</small>
|
||
`;
|
||
document.getElementById('memoryUsage').innerHTML = memoryHtml;
|
||
|
||
// 更新定时任务状态
|
||
if (data.scheduler) {
|
||
const taskCount = data.scheduler.taskCount || 0;
|
||
document.getElementById('schedulerStatus').innerHTML = `
|
||
<div>${taskCount} 个任务运行中</div>
|
||
<small class="text-muted">自动调度正常</small>
|
||
`;
|
||
}
|
||
}
|
||
|
||
updateTasksStatus(data) {
|
||
const container = document.getElementById('tasksStatus');
|
||
|
||
if (data.today_tasks) {
|
||
const scrape = data.today_tasks.scrape;
|
||
const validation = data.today_tasks.validation;
|
||
|
||
container.innerHTML = `
|
||
<div class="row">
|
||
<div class="col-6">
|
||
<div class="card border-primary">
|
||
<div class="card-body text-center p-2">
|
||
<h6 class="card-title mb-1">抓取任务</h6>
|
||
<div class="d-flex justify-content-between">
|
||
<small>成功: <span class="text-success">${scrape.success}</span></small>
|
||
<small>失败: <span class="text-danger">${scrape.failed}</span></small>
|
||
</div>
|
||
<div class="progress mt-1" style="height: 6px;">
|
||
<div class="progress-bar bg-success" style="width: ${scrape.success_rate}%"></div>
|
||
</div>
|
||
<small class="text-muted">成功率: ${scrape.success_rate}%</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-6">
|
||
<div class="card border-success">
|
||
<div class="card-body text-center p-2">
|
||
<h6 class="card-title mb-1">验证任务</h6>
|
||
<div class="d-flex justify-content-between">
|
||
<small>成功: <span class="text-success">${validation.success}</span></small>
|
||
<small>失败: <span class="text-danger">${validation.failed}</span></small>
|
||
</div>
|
||
<div class="progress mt-1" style="height: 6px;">
|
||
<div class="progress-bar bg-success" style="width: ${validation.success_rate}%"></div>
|
||
</div>
|
||
<small class="text-muted">成功率: ${validation.success_rate}%</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
updateProxyPoolStatus(proxies) {
|
||
const container = document.getElementById('proxyPoolStatus');
|
||
|
||
container.innerHTML = `
|
||
<div class="row text-center">
|
||
<div class="col-4">
|
||
<div class="p-2">
|
||
<h4 class="text-primary mb-0">${proxies.total}</h4>
|
||
<small class="text-muted">总代理数</small>
|
||
</div>
|
||
</div>
|
||
<div class="col-4">
|
||
<div class="p-2 border-start border-end">
|
||
<h4 class="text-success mb-0">${proxies.valid}</h4>
|
||
<small class="text-muted">可用代理</small>
|
||
</div>
|
||
</div>
|
||
<div class="col-4">
|
||
<div class="p-2">
|
||
<h4 class="text-danger mb-0">${proxies.invalid}</h4>
|
||
<small class="text-muted">无效代理</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<hr>
|
||
<div class="text-center">
|
||
<div class="badge bg-info fs-6">可用率: ${proxies.validRate}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
updateResourceUsage(data) {
|
||
if (data.memory) {
|
||
const memoryPercent = Math.round((data.memory.heapUsed / data.memory.heapTotal) * 100);
|
||
document.getElementById('memoryPercent').textContent = memoryPercent + '%';
|
||
document.getElementById('memoryProgressBar').style.width = memoryPercent + '%';
|
||
}
|
||
|
||
if (data.proxies) {
|
||
const validRate = parseFloat(data.proxies.validRate);
|
||
document.getElementById('proxyValidRate').textContent = data.proxies.validRate;
|
||
document.getElementById('proxyValidProgressBar').style.width = validRate + '%';
|
||
}
|
||
|
||
// 模拟CPU使用率(实际项目中可以从系统API获取)
|
||
const cpuUsage = Math.round(Math.random() * 30 + 10); // 10-40%
|
||
document.getElementById('cpuUsage').textContent = cpuUsage + '%';
|
||
document.getElementById('cpuProgressBar').style.width = cpuUsage + '%';
|
||
}
|
||
|
||
async updateCharts() {
|
||
try {
|
||
// 更新代理池趋势图
|
||
const proxyResponse = await fetch('/api/dashboard/charts/proxies');
|
||
const proxyResult = await proxyResponse.json();
|
||
|
||
if (proxyResult.success && this.charts.proxyPool) {
|
||
const labels = proxyResult.data.map(item =>
|
||
new Date(item.date).toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' })
|
||
);
|
||
|
||
// 计算累计数据
|
||
let totalRunning = 0;
|
||
let validRunning = 0;
|
||
const totalData = [];
|
||
const validData = [];
|
||
|
||
proxyResult.data.forEach(item => {
|
||
totalRunning += item.total_added || 0;
|
||
validRunning += item.valid_added || 0;
|
||
totalData.push(totalRunning);
|
||
validData.push(validRunning);
|
||
});
|
||
|
||
this.charts.proxyPool.data.labels = labels;
|
||
this.charts.proxyPool.data.datasets[0].data = totalData;
|
||
this.charts.proxyPool.data.datasets[1].data = validData;
|
||
this.charts.proxyPool.update();
|
||
}
|
||
|
||
// 更新任务执行率图
|
||
const taskResponse = await fetch('/api/history/stats/summary');
|
||
const taskResult = await taskResponse.json();
|
||
|
||
if (taskResult.success && this.charts.taskRate) {
|
||
// 使用最近7天的数据
|
||
const labels = [];
|
||
const successRates = [];
|
||
|
||
for (let i = 6; i >= 0; i--) {
|
||
const date = new Date();
|
||
date.setDate(date.getDate() - i);
|
||
labels.push(date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' }));
|
||
successRates.push(Math.random() * 30 + 70); // 模拟70-100%成功率
|
||
}
|
||
|
||
this.charts.taskRate.data.labels = labels;
|
||
this.charts.taskRate.data.datasets[0].data = successRates;
|
||
this.charts.taskRate.update();
|
||
}
|
||
} catch (error) {
|
||
console.error('更新图表失败:', error);
|
||
}
|
||
}
|
||
|
||
async loadRecentLogs() {
|
||
try {
|
||
const response = await fetch('/api/history/logs/system?limit=10');
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
this.renderRecentLogs(result.data);
|
||
}
|
||
} catch (error) {
|
||
console.error('加载最近日志失败:', error);
|
||
}
|
||
}
|
||
|
||
async loadRecentEvents() {
|
||
try {
|
||
const response = await fetch('/api/history?limit=10');
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
this.renderRecentEvents(result.data);
|
||
}
|
||
} catch (error) {
|
||
console.error('加载最近事件失败:', error);
|
||
}
|
||
}
|
||
|
||
renderRecentLogs(logs) {
|
||
const container = document.getElementById('recentLogs');
|
||
|
||
if (!logs || logs.length === 0) {
|
||
container.innerHTML = '<div class="text-center text-muted p-4">暂无日志</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = logs.map(log => `
|
||
<div class="d-flex align-items-start mb-2 p-2 border-bottom">
|
||
<div class="me-2">
|
||
<span class="badge ${this.getLogLevelClass(log.level)}">${log.level.toUpperCase()}</span>
|
||
</div>
|
||
<div class="flex-grow-1">
|
||
<div class="small">${log.message}</div>
|
||
<div class="text-muted" style="font-size: 0.75rem;">
|
||
${this.formatDateTime(log.timestamp)} - ${log.source}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
renderRecentEvents(events) {
|
||
const container = document.getElementById('recentEvents');
|
||
|
||
if (!events || events.length === 0) {
|
||
container.innerHTML = '<div class="text-center text-muted p-4">暂无事件</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = events.map(event => `
|
||
<div class="d-flex align-items-start mb-2 p-2 border-bottom">
|
||
<div class="me-2">
|
||
<i class="bi bi-${this.getTaskIcon(event.task_type)} text-${this.getTaskColor(event.status)}"></i>
|
||
</div>
|
||
<div class="flex-grow-1">
|
||
<div class="small fw-bold">${event.task_name}</div>
|
||
<div class="text-muted" style="font-size: 0.75rem;">
|
||
${this.formatDateTime(event.start_time)} - ${this.getStatusText(event.status)}
|
||
</div>
|
||
${event.result_summary ? `<div class="small text-muted">${event.result_summary}</div>` : ''}
|
||
</div>
|
||
<div>
|
||
<span class="badge ${this.getStatusClass(event.status)}">${this.getStatusText(event.status)}</span>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
startAutoRefresh() {
|
||
// 每30秒刷新一次数据
|
||
this.refreshInterval = setInterval(() => {
|
||
this.loadSystemStatus();
|
||
}, 30000);
|
||
|
||
// 每5分钟刷新最近数据
|
||
setInterval(() => {
|
||
this.loadRecentData();
|
||
}, 300000);
|
||
}
|
||
|
||
startRefreshProgress() {
|
||
this.refreshProgressInterval = setInterval(() => {
|
||
const progressBar = document.getElementById('refreshProgress');
|
||
if (progressBar) {
|
||
const currentWidth = parseFloat(progressBar.style.width) || 100;
|
||
const newWidth = Math.max(0, currentWidth - 3.33); // 30秒内从100%到0%
|
||
progressBar.style.width = newWidth + '%';
|
||
}
|
||
}, 1000);
|
||
}
|
||
|
||
updateLastRefreshTime() {
|
||
const now = new Date();
|
||
const timeString = now.toLocaleTimeString('zh-CN');
|
||
document.getElementById('lastUpdateTime').textContent = timeString;
|
||
}
|
||
|
||
setRefreshing(refreshing) {
|
||
this.isRefreshing = refreshing;
|
||
if (refreshing) {
|
||
// 重置进度条
|
||
const progressBar = document.getElementById('refreshProgress');
|
||
if (progressBar) {
|
||
progressBar.style.width = '100%';
|
||
}
|
||
}
|
||
}
|
||
|
||
// 工具函数
|
||
formatUptime(seconds) {
|
||
const hours = Math.floor(seconds / 3600);
|
||
const minutes = Math.floor((seconds % 3600) / 60);
|
||
|
||
if (hours > 24) {
|
||
const days = Math.floor(hours / 24);
|
||
const remainingHours = hours % 24;
|
||
return `${days}天 ${remainingHours}小时 ${minutes}分钟`;
|
||
}
|
||
|
||
return `${hours}小时 ${minutes}分钟`;
|
||
}
|
||
|
||
formatDateTime(dateString) {
|
||
const date = new Date(dateString);
|
||
return date.toLocaleString('zh-CN', {
|
||
month: 'short',
|
||
day: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
}
|
||
|
||
getLogLevelClass(level) {
|
||
const classes = {
|
||
'error': 'bg-danger',
|
||
'warning': 'bg-warning',
|
||
'info': 'bg-info',
|
||
'debug': 'bg-secondary'
|
||
};
|
||
return classes[level] || 'bg-secondary';
|
||
}
|
||
|
||
getStatusClass(status) {
|
||
const classes = {
|
||
'success': 'bg-success',
|
||
'failed': 'bg-danger',
|
||
'running': 'bg-warning',
|
||
'pending': 'bg-secondary'
|
||
};
|
||
return classes[status] || 'bg-secondary';
|
||
}
|
||
|
||
getStatusText(status) {
|
||
const texts = {
|
||
'success': '成功',
|
||
'failed': '失败',
|
||
'running': '运行中',
|
||
'pending': '等待中'
|
||
};
|
||
return texts[status] || status;
|
||
}
|
||
|
||
getTaskIcon(taskType) {
|
||
const icons = {
|
||
'scrape': 'download',
|
||
'validation': 'check2-square',
|
||
'health_check': 'heart-pulse'
|
||
};
|
||
return icons[taskType] || 'gear';
|
||
}
|
||
|
||
getTaskColor(status) {
|
||
const colors = {
|
||
'success': 'success',
|
||
'failed': 'danger',
|
||
'running': 'warning',
|
||
'pending': 'secondary'
|
||
};
|
||
return colors[status] || 'secondary';
|
||
}
|
||
|
||
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';
|
||
}
|
||
|
||
stopAutoRefresh() {
|
||
if (this.refreshInterval) {
|
||
clearInterval(this.refreshInterval);
|
||
this.refreshInterval = null;
|
||
}
|
||
if (this.refreshProgressInterval) {
|
||
clearInterval(this.refreshProgressInterval);
|
||
this.refreshProgressInterval = null;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 全局函数(供HTML调用)
|
||
let systemMonitor;
|
||
|
||
async function refreshMonitoring() {
|
||
if (systemMonitor && !systemMonitor.isRefreshing) {
|
||
await systemMonitor.loadSystemStatus();
|
||
await systemMonitor.loadRecentData();
|
||
systemMonitor.showAlert('监控数据已刷新', 'info');
|
||
}
|
||
}
|
||
|
||
// 页面加载完成后初始化
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
systemMonitor = new SystemMonitor();
|
||
});
|
||
|
||
// 页面卸载时清理
|
||
window.addEventListener('beforeunload', () => {
|
||
if (systemMonitor) {
|
||
systemMonitor.stopAutoRefresh();
|
||
}
|
||
}); |