751 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			751 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
// 执行历史页面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();
 | 
						||
}); |