590 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			590 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
// 仪表板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();
 | 
						||
    }
 | 
						||
}); |