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

604 lines
21 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 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();
}
});