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

751 lines
26 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 HistoryManager {
constructor() {
this.currentTaskPage = 1;
this.currentLogsPage = 1;
this.pageSize = 20;
this.charts = {};
this.searchParams = {
taskType: '',
taskStatus: '',
startDate: '',
endDate: ''
};
this.logSearchParams = {
level: '',
category: '',
keyword: ''
};
this.init();
}
async init() {
console.log('初始化执行历史页面...');
// 绑定事件
this.bindEvents();
// 初始化图表
this.initCharts();
// 加载初始数据
await this.loadTaskHistory();
await this.loadSystemLogs();
await this.loadStatistics();
console.log('执行历史页面初始化完成');
}
bindEvents() {
// 任务历史筛选
document.getElementById('taskFilterForm').addEventListener('submit', (e) => {
e.preventDefault();
this.searchParams.taskType = document.getElementById('taskTypeFilter').value;
this.searchParams.taskStatus = document.getElementById('taskStatusFilter').value;
this.searchParams.startDate = document.getElementById('startDate').value;
this.searchParams.endDate = document.getElementById('endDate').value;
this.currentTaskPage = 1;
this.loadTaskHistory();
});
// 系统日志筛选
document.getElementById('logFilterForm').addEventListener('submit', (e) => {
e.preventDefault();
this.logSearchParams.level = document.getElementById('logLevelFilter').value;
this.logSearchParams.category = document.getElementById('logCategoryFilter').value;
this.logSearchParams.keyword = document.getElementById('logSearchKeyword').value;
this.currentLogsPage = 1;
this.loadSystemLogs();
});
// 选项卡切换事件
document.getElementById('stats-tab').addEventListener('shown.bs.tab', () => {
this.loadStatistics();
this.updateCharts();
});
}
initCharts() {
// 任务趋势图
const taskTrendCtx = document.getElementById('taskTrendChart');
if (taskTrendCtx) {
this.charts.taskTrend = new Chart(taskTrendCtx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: '成功任务',
data: [],
borderColor: '#28a745',
backgroundColor: 'rgba(40, 167, 69, 0.1)',
tension: 0.4
}, {
label: '失败任务',
data: [],
borderColor: '#dc3545',
backgroundColor: 'rgba(220, 53, 69, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top',
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
precision: 0
}
}
}
}
});
}
// 日志级别分布图
const logLevelCtx = document.getElementById('logLevelChart');
if (logLevelCtx) {
this.charts.logLevel = new Chart(logLevelCtx, {
type: 'doughnut',
data: {
labels: ['错误', '警告', '信息', '调试'],
datasets: [{
data: [0, 0, 0, 0],
backgroundColor: ['#dc3545', '#ffc107', '#17a2b8', '#6c757d']
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
}
}
}
});
}
}
async loadTaskHistory() {
try {
this.showTaskLoading(true);
// 构建查询参数
const params = new URLSearchParams({
limit: this.pageSize,
offset: (this.currentTaskPage - 1) * this.pageSize
});
if (this.searchParams.taskType) params.append('taskType', this.searchParams.taskType);
if (this.searchParams.taskStatus) params.append('status', this.searchParams.taskStatus);
const response = await fetch(`/api/history?${params}`);
const result = await response.json();
if (result.success) {
this.renderTaskHistoryTable(result.data);
this.renderTaskPagination(result.pagination);
} else {
this.showAlert('加载任务历史失败: ' + result.error, 'danger');
}
} catch (error) {
console.error('加载任务历史失败:', error);
this.showAlert('加载任务历史失败: ' + error.message, 'danger');
} finally {
this.showTaskLoading(false);
}
}
async loadSystemLogs() {
try {
this.showLogsLoading(true);
// 构建查询参数
const params = new URLSearchParams({
limit: this.pageSize,
offset: (this.currentLogsPage - 1) * this.pageSize
});
if (this.logSearchParams.level) params.append('level', this.logSearchParams.level);
if (this.logSearchParams.category) params.append('category', this.logSearchParams.category);
let response;
if (this.logSearchParams.keyword) {
response = await fetch(`/api/history/logs/search?keyword=${encodeURIComponent(this.logSearchParams.keyword)}&limit=${this.pageSize}`);
} else {
response = await fetch(`/api/history/logs/system?${params}`);
}
const result = await response.json();
if (result.success) {
this.renderSystemLogsTable(result.data);
this.renderLogsPagination(result.pagination);
} else {
this.showAlert('加载系统日志失败: ' + result.error, 'danger');
}
} catch (error) {
console.error('加载系统日志失败:', error);
this.showAlert('加载系统日志失败: ' + error.message, 'danger');
} finally {
this.showLogsLoading(false);
}
}
async loadStatistics() {
try {
// 加载任务统计
const taskStatsResponse = await fetch('/api/history/stats/summary');
const taskStatsResult = await taskStatsResponse.json();
if (taskStatsResult.success) {
this.updateTaskStatistics(taskStatsResult.data.summary);
this.renderTaskStats(taskStatsResult.data);
}
// 加载日志统计
const logStatsResponse = await fetch('/api/history/logs/stats');
const logStatsResult = await logStatsResponse.json();
if (logStatsResult.success) {
this.updateLogStatistics(logStatsResult.data.summary);
this.renderLogStats(logStatsResult.data);
}
} catch (error) {
console.error('加载统计数据失败:', error);
this.showAlert('加载统计数据失败: ' + error.message, 'danger');
}
}
renderTaskHistoryTable(tasks) {
const tbody = document.getElementById('taskHistoryTableBody');
if (tasks.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="8" class="text-center p-4">
<i class="bi bi-inbox fs-1 text-muted"></i>
<p class="text-muted mt-2">暂无任务历史</p>
</td>
</tr>
`;
return;
}
tbody.innerHTML = tasks.map(task => `
<tr>
<td>
<span class="badge bg-secondary">${this.getTaskTypeLabel(task.task_type)}</span>
</td>
<td>${task.task_name}</td>
<td>
<span class="badge ${this.getStatusClass(task.status)}">
<i class="bi bi-${this.getStatusIcon(task.status)}"></i>
${this.getStatusText(task.status)}
</span>
</td>
<td>
<small class="text-muted">${this.formatDateTime(task.start_time)}</small>
</td>
<td>
<small class="text-muted">${task.end_time ? this.formatDateTime(task.end_time) : '-'}</small>
</td>
<td>
<small class="text-muted">${task.duration ? this.formatDuration(task.duration) : '-'}</small>
</td>
<td>
<small class="text-muted">${task.result_summary || '-'}</small>
</td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="historyManager.showTaskDetail(${task.id})">
<i class="bi bi-eye"></i> 详情
</button>
</td>
</tr>
`).join('');
}
renderSystemLogsTable(logs) {
const tbody = document.getElementById('systemLogsTableBody');
if (logs.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="5" class="text-center p-4">
<i class="bi bi-inbox fs-1 text-muted"></i>
<p class="text-muted mt-2">暂无系统日志</p>
</td>
</tr>
`;
return;
}
tbody.innerHTML = logs.map(log => `
<tr>
<td>
<small class="text-muted">${this.formatDateTime(log.timestamp)}</small>
</td>
<td>
<span class="badge ${this.getLogLevelClass(log.level)}">${log.level.toUpperCase()}</span>
</td>
<td>
<small>${log.category || '-'}</small>
</td>
<td>
<small>${log.message}</small>
</td>
<td>
<small class="text-muted">${log.source}</small>
</td>
</tr>
`).join('');
}
renderTaskPagination(pagination) {
const paginationElement = document.getElementById('taskPagination');
this.renderPagination(paginationElement, pagination, this.currentTaskPage, (page) => {
this.currentTaskPage = page;
this.loadTaskHistory();
});
}
renderLogsPagination(pagination) {
const paginationElement = document.getElementById('logsPagination');
this.renderPagination(paginationElement, pagination, this.currentLogsPage, (page) => {
this.currentLogsPage = page;
this.loadSystemLogs();
});
}
renderPagination(container, pagination, currentPage, onPageChange) {
const totalPages = Math.ceil(pagination.total / pagination.limit);
if (totalPages <= 1) {
container.innerHTML = '';
return;
}
let paginationHTML = '';
// 上一页
paginationHTML += `
<li class="page-item ${currentPage === 1 ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="historyManager.goToTaskPage(${currentPage - 1}); return false;">
<i class="bi bi-chevron-left"></i>
</a>
</li>
`;
// 页码
const startPage = Math.max(1, currentPage - 2);
const endPage = Math.min(totalPages, currentPage + 2);
for (let i = startPage; i <= endPage; i++) {
paginationHTML += `
<li class="page-item ${i === currentPage ? 'active' : ''}">
<a class="page-link" href="#" onclick="historyManager.goToTaskPage(${i}); return false;">${i}</a>
</li>
`;
}
// 下一页
paginationHTML += `
<li class="page-item ${currentPage === totalPages ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="historyManager.goToTaskPage(${currentPage + 1}); return false;">
<i class="bi bi-chevron-right"></i>
</a>
</li>
`;
container.innerHTML = paginationHTML;
}
updateTaskStatistics(stats) {
document.getElementById('totalTasks').textContent = stats.total;
document.getElementById('successTasks').textContent = stats.success;
document.getElementById('failedTasks').textContent = stats.failed;
document.getElementById('successRate').textContent = stats.success_rate + '%';
}
updateLogStatistics(stats) {
// 更新日志级别图表
if (this.charts.logLevel) {
this.charts.logLevel.data.datasets[0].data = [
stats.error,
stats.warning,
stats.info,
stats.debug
];
this.charts.logLevel.update();
}
}
renderTaskStats(data) {
const content = document.getElementById('taskStatsContent');
if (!data.daily || data.daily.length === 0) {
content.innerHTML = '<p class="text-muted">暂无数据</p>';
return;
}
let html = '<table class="table table-sm">';
html += '<thead><tr><th>日期</th><th>总任务</th><th>成功</th><th>失败</th><th>成功率</th></tr></thead><tbody>';
data.daily.slice(0, 7).forEach(day => {
const successRate = day.total > 0 ? ((day.success / day.total) * 100).toFixed(1) : '0';
html += `
<tr>
<td>${this.formatDate(day.date)}</td>
<td>${day.total}</td>
<td class="text-success">${day.success}</td>
<td class="text-danger">${day.failed}</td>
<td>${successRate}%</td>
</tr>
`;
});
html += '</tbody></table>';
content.innerHTML = html;
}
renderLogStats(data) {
const content = document.getElementById('logStatsContent');
const summary = data.summary;
let html = '<div class="row">';
html += '<div class="col-6"><small class="text-muted">总日志数:</small><div><strong>' + summary.total + '</strong></div></div>';
html += '<div class="col-6"><small class="text-muted">错误:</small><div><strong class="text-danger">' + summary.error + '</strong></div></div>';
html += '<div class="col-6"><small class="text-muted">警告:</small><div><strong class="text-warning">' + summary.warning + '</strong></div></div>';
html += '<div class="col-6"><small class="text-muted">信息:</small><div><strong class="text-info">' + summary.info + '</strong></div></div>';
html += '</div>';
content.innerHTML = html;
}
async updateCharts() {
try {
// 加载每日任务统计
const response = await fetch('/api/dashboard/charts/tasks');
const result = await response.json();
if (result.success && this.charts.taskTrend) {
const labels = result.data.map(item => this.formatDate(item.date));
const successData = result.data.map(item => item.scrape_success + item.validation_success);
const failedData = result.data.map(item => item.scrape_failed + item.validation_failed);
this.charts.taskTrend.data.labels = labels;
this.charts.taskTrend.data.datasets[0].data = successData;
this.charts.taskTrend.data.datasets[1].data = failedData;
this.charts.taskTrend.update();
}
} catch (error) {
console.error('更新图表失败:', error);
}
}
async showTaskDetail(taskId) {
try {
const response = await fetch(`/api/history/${taskId}`);
const result = await response.json();
if (result.success) {
const task = result.data;
const content = document.getElementById('taskDetailContent');
content.innerHTML = `
<div class="row">
<div class="col-md-6">
<strong>任务类型:</strong><br>
<span class="badge bg-secondary">${this.getTaskTypeLabel(task.task_type)}</span>
</div>
<div class="col-md-6">
<strong>任务名称:</strong><br>
${task.task_name}
</div>
</div>
<hr>
<div class="row">
<div class="col-md-6">
<strong>状态:</strong><br>
<span class="badge ${this.getStatusClass(task.status)}">
${this.getStatusText(task.status)}
</span>
</div>
<div class="col-md-6">
<strong>执行时长:</strong><br>
${task.duration ? this.formatDuration(task.duration) : '-'}
</div>
</div>
<hr>
<div class="row">
<div class="col-md-6">
<strong>开始时间:</strong><br>
${this.formatDateTime(task.start_time)}
</div>
<div class="col-md-6">
<strong>结束时间:</strong><br>
${task.end_time ? this.formatDateTime(task.end_time) : '-'}
</div>
</div>
<hr>
<div class="row">
<div class="col-12">
<strong>结果摘要:</strong><br>
<p>${task.result_summary || '暂无摘要'}</p>
</div>
</div>
${task.error_message ? `
<div class="row">
<div class="col-12">
<strong>错误信息:</strong><br>
<div class="alert alert-danger">${task.error_message}</div>
</div>
</div>
` : ''}
${task.details ? `
<div class="row">
<div class="col-12">
<strong>详细信息:</strong><br>
<pre class="bg-light p-2 rounded"><code>${JSON.stringify(task.details, null, 2)}</code></pre>
</div>
</div>
` : ''}
`;
// 显示模态框
const modal = new bootstrap.Modal(document.getElementById('taskDetailModal'));
modal.show();
} else {
this.showAlert('获取任务详情失败: ' + result.error, 'danger');
}
} catch (error) {
console.error('获取任务详情失败:', error);
this.showAlert('获取任务详情失败: ' + error.message, 'danger');
}
}
async exportTaskHistory() {
try {
const params = new URLSearchParams();
if (this.searchParams.taskType) params.append('taskType', this.searchParams.taskType);
if (this.searchParams.taskStatus) params.append('status', this.searchParams.taskStatus);
const response = await fetch(`/api/history/export?${params}&format=csv`);
if (response.ok) {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `task_history_${new Date().toISOString().slice(0, 10)}.csv`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
this.showAlert('任务历史导出成功', 'success');
} else {
this.showAlert('导出失败', 'danger');
}
} catch (error) {
console.error('导出任务历史失败:', error);
this.showAlert('导出任务历史失败: ' + error.message, 'danger');
}
}
async cleanupLogs() {
if (!confirm('确定要清理30天前的历史记录吗此操作不可恢复。')) {
return;
}
try {
const response = await fetch('/api/history/cleanup', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ days: 30, type: 'all' })
});
const result = await response.json();
if (result.success) {
this.showAlert(result.message, 'success');
await this.loadSystemLogs();
await this.loadStatistics();
} else {
this.showAlert('清理失败: ' + result.error, 'danger');
}
} catch (error) {
console.error('清理历史记录失败:', error);
this.showAlert('清理历史记录失败: ' + error.message, 'danger');
}
}
// 工具函数
getTaskTypeLabel(type) {
const labels = {
'scrape': '抓取任务',
'validation': '验证任务',
'health_check': '健康检查'
};
return labels[type] || type;
}
getStatusClass(status) {
const classes = {
'success': 'bg-success',
'failed': 'bg-danger',
'running': 'bg-warning',
'pending': 'bg-secondary'
};
return classes[status] || 'bg-secondary';
}
getStatusIcon(status) {
const icons = {
'success': 'check-circle',
'failed': 'x-circle',
'running': 'clock',
'pending': 'pause-circle'
};
return icons[status] || 'question-circle';
}
getStatusText(status) {
const texts = {
'success': '成功',
'failed': '失败',
'running': '运行中',
'pending': '等待中'
};
return texts[status] || status;
}
getLogLevelClass(level) {
const classes = {
'error': 'bg-danger',
'warning': 'bg-warning',
'info': 'bg-info',
'debug': 'bg-secondary'
};
return classes[level] || 'bg-secondary';
}
formatDateTime(dateString) {
const date = new Date(dateString);
return date.toLocaleString('zh-CN');
}
formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' });
}
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';
}
}
showTaskLoading(show) {
const tbody = document.getElementById('taskHistoryTableBody');
const spinner = tbody.querySelector('.spinner-border');
if (spinner) {
spinner.parentElement.style.display = show ? 'block' : 'none';
}
}
showLogsLoading(show) {
const tbody = document.getElementById('systemLogsTableBody');
const spinner = tbody.querySelector('.spinner-border');
if (spinner) {
spinner.parentElement.style.display = show ? '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';
}
goToTaskPage(page) {
this.currentTaskPage = page;
this.loadTaskHistory();
}
}
// 全局函数供HTML调用
let historyManager;
async function refreshHistory() {
if (historyManager) {
await Promise.all([
historyManager.loadTaskHistory(),
historyManager.loadSystemLogs(),
historyManager.loadStatistics()
]);
historyManager.showAlert('历史数据已刷新', 'info');
}
}
async function exportTaskHistory() {
if (historyManager) {
await historyManager.exportTaskHistory();
}
}
async function cleanupLogs() {
if (historyManager) {
await historyManager.cleanupLogs();
}
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
historyManager = new HistoryManager();
});