init
This commit is contained in:
590
public/js/dashboard.js
Normal file
590
public/js/dashboard.js
Normal file
@@ -0,0 +1,590 @@
|
||||
// 仪表板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();
|
||||
}
|
||||
});
|
||||
751
public/js/history.js
Normal file
751
public/js/history.js
Normal file
@@ -0,0 +1,751 @@
|
||||
// 执行历史页面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();
|
||||
});
|
||||
604
public/js/monitoring.js
Normal file
604
public/js/monitoring.js
Normal file
@@ -0,0 +1,604 @@
|
||||
// 系统监控页面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();
|
||||
}
|
||||
});
|
||||
760
public/js/proxies.js
Normal file
760
public/js/proxies.js
Normal file
@@ -0,0 +1,760 @@
|
||||
// 代理管理页面JavaScript
|
||||
|
||||
class ProxyManager {
|
||||
constructor() {
|
||||
this.currentPage = 1;
|
||||
this.pageSize = 20;
|
||||
this.totalCount = 0;
|
||||
this.proxies = [];
|
||||
this.selectedProxies = new Set();
|
||||
this.searchParams = {
|
||||
ip: '',
|
||||
port: '',
|
||||
location: '',
|
||||
status: '',
|
||||
sortBy: 'created_at',
|
||||
order: 'DESC'
|
||||
};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
console.log('初始化代理管理页面...');
|
||||
|
||||
// 绑定事件
|
||||
this.bindEvents();
|
||||
|
||||
// 加载代理列表
|
||||
await this.loadProxies();
|
||||
|
||||
console.log('代理管理页面初始化完成');
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// 搜索表单
|
||||
document.getElementById('searchForm').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
this.searchParams.ip = document.getElementById('searchIp').value;
|
||||
this.searchParams.port = document.getElementById('searchPort').value;
|
||||
this.searchParams.location = document.getElementById('searchLocation').value;
|
||||
this.searchParams.status = document.getElementById('filterStatus').value;
|
||||
this.searchParams.sortBy = document.getElementById('sortBy').value;
|
||||
this.currentPage = 1;
|
||||
this.loadProxies();
|
||||
});
|
||||
|
||||
// 单个代理验证
|
||||
document.getElementById('validateSingleProxy').addEventListener('click', () => {
|
||||
const proxyId = document.getElementById('validateSingleProxy').dataset.proxyId;
|
||||
if (proxyId) {
|
||||
this.validateSingleProxy(proxyId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async loadProxies() {
|
||||
try {
|
||||
this.showLoading(true);
|
||||
|
||||
// 构建查询参数
|
||||
const params = new URLSearchParams({
|
||||
limit: this.pageSize,
|
||||
offset: (this.currentPage - 1) * this.pageSize,
|
||||
sortBy: this.searchParams.sortBy,
|
||||
order: this.searchParams.order
|
||||
});
|
||||
|
||||
if (this.searchParams.ip) params.append('ip', this.searchParams.ip);
|
||||
if (this.searchParams.port) params.append('port', this.searchParams.port);
|
||||
if (this.searchParams.location) params.append('location', this.searchParams.location);
|
||||
if (this.searchParams.status !== '') params.append('validOnly', this.searchParams.status === '1');
|
||||
|
||||
const response = await fetch(`/api/proxies?${params}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.proxies = result.data;
|
||||
this.totalCount = result.pagination.total;
|
||||
this.renderProxyTable();
|
||||
this.renderPagination();
|
||||
this.updateStatistics();
|
||||
} else {
|
||||
this.showAlert('加载代理列表失败: ' + result.error, 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载代理列表失败:', error);
|
||||
this.showAlert('加载代理列表失败: ' + error.message, 'danger');
|
||||
} finally {
|
||||
this.showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
renderProxyTable() {
|
||||
const tbody = document.getElementById('proxyTableBody');
|
||||
|
||||
if (this.proxies.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="9" class="text-center p-4">
|
||||
<i class="bi bi-inbox fs-1 text-muted"></i>
|
||||
<p class="text-muted mt-2">暂无代理数据</p>
|
||||
<button class="btn btn-primary" onclick="proxyManager.startScrape()">
|
||||
<i class="bi bi-download"></i> 立即抓取代理
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = this.proxies.map(proxy => `
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" class="form-check-input proxy-checkbox"
|
||||
value="${proxy.id}" onchange="proxyManager.toggleProxySelection(${proxy.id})">
|
||||
</td>
|
||||
<td>
|
||||
<code>${proxy.ip}</code>
|
||||
</td>
|
||||
<td>${proxy.port}</td>
|
||||
<td>
|
||||
<small class="text-muted">${proxy.location || '-'}</small>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge ${proxy.is_valid ? 'bg-success' : 'bg-danger'}">
|
||||
<i class="bi bi-${proxy.is_valid ? 'check-circle' : 'x-circle'}"></i>
|
||||
${proxy.is_valid ? '可用' : '不可用'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">
|
||||
${proxy.response_time ? proxy.response_time + 'ms' : '-'}
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">
|
||||
${proxy.last_check_time ? this.formatDateTime(proxy.last_check_time) : '-'}
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">
|
||||
${this.formatDateTime(proxy.created_at)}
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-primary" onclick="proxyManager.showProxyDetail(${proxy.id})" title="查看详情">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-success" onclick="proxyManager.validateSingleProxy(${proxy.id})" title="验证">
|
||||
<i class="bi bi-check2"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-danger" onclick="proxyManager.deleteProxy(${proxy.id})" title="删除">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
renderPagination() {
|
||||
const pagination = document.getElementById('pagination');
|
||||
const totalPages = Math.ceil(this.totalCount / this.pageSize);
|
||||
|
||||
if (totalPages <= 1) {
|
||||
pagination.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
let paginationHTML = '';
|
||||
|
||||
// 上一页
|
||||
paginationHTML += `
|
||||
<li class="page-item ${this.currentPage === 1 ? 'disabled' : ''}">
|
||||
<a class="page-link" href="#" onclick="proxyManager.goToPage(${this.currentPage - 1}); return false;">
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
// 页码
|
||||
const startPage = Math.max(1, this.currentPage - 2);
|
||||
const endPage = Math.min(totalPages, this.currentPage + 2);
|
||||
|
||||
if (startPage > 1) {
|
||||
paginationHTML += `
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="#" onclick="proxyManager.goToPage(1); return false;">1</a>
|
||||
</li>
|
||||
`;
|
||||
if (startPage > 2) {
|
||||
paginationHTML += '<li class="page-item disabled"><a class="page-link" href="#">...</a></li>';
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
paginationHTML += `
|
||||
<li class="page-item ${i === this.currentPage ? 'active' : ''}">
|
||||
<a class="page-link" href="#" onclick="proxyManager.goToPage(${i}); return false;">${i}</a>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
|
||||
if (endPage < totalPages) {
|
||||
if (endPage < totalPages - 1) {
|
||||
paginationHTML += '<li class="page-item disabled"><a class="page-link" href="#">...</a></li>';
|
||||
}
|
||||
paginationHTML += `
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="#" onclick="proxyManager.goToPage(${totalPages}); return false;">${totalPages}</a>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
|
||||
// 下一页
|
||||
paginationHTML += `
|
||||
<li class="page-item ${this.currentPage === totalPages ? 'disabled' : ''}">
|
||||
<a class="page-link" href="#" onclick="proxyManager.goToPage(${this.currentPage + 1}); return false;">
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
pagination.innerHTML = paginationHTML;
|
||||
}
|
||||
|
||||
updateStatistics() {
|
||||
document.getElementById('totalCount').textContent = this.totalCount;
|
||||
document.getElementById('validCount').textContent = this.proxies.filter(p => p.is_valid).length;
|
||||
document.getElementById('invalidCount').textContent = this.proxies.filter(p => !p.is_valid).length;
|
||||
document.getElementById('showingCount').textContent = this.proxies.length;
|
||||
}
|
||||
|
||||
async showProxyDetail(proxyId) {
|
||||
try {
|
||||
const proxy = this.proxies.find(p => p.id === proxyId);
|
||||
if (!proxy) return;
|
||||
|
||||
const content = document.getElementById('proxyDetailContent');
|
||||
content.innerHTML = `
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<strong>IP地址:</strong><br>
|
||||
<code>${proxy.ip}</code>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<strong>端口:</strong><br>
|
||||
${proxy.port}
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<strong>位置:</strong><br>
|
||||
${proxy.location || '-'}
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<strong>状态:</strong><br>
|
||||
<span class="badge ${proxy.is_valid ? 'bg-success' : 'bg-danger'}">
|
||||
${proxy.is_valid ? '可用' : '不可用'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<strong>响应时间:</strong><br>
|
||||
${proxy.response_time ? proxy.response_time + 'ms' : '-'}
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<strong>最后验证:</strong><br>
|
||||
${proxy.last_check_time ? this.formatDateTime(proxy.last_check_time) : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<strong>创建时间:</strong><br>
|
||||
${this.formatDateTime(proxy.created_at)}
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<strong>更新时间:</strong><br>
|
||||
${this.formatDateTime(proxy.updated_at)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 设置验证按钮的数据属性
|
||||
document.getElementById('validateSingleProxy').dataset.proxyId = proxyId;
|
||||
|
||||
// 显示模态框
|
||||
const modal = new bootstrap.Modal(document.getElementById('proxyDetailModal'));
|
||||
modal.show();
|
||||
} catch (error) {
|
||||
console.error('显示代理详情失败:', error);
|
||||
this.showAlert('显示代理详情失败: ' + error.message, 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
async validateSingleProxy(proxyId) {
|
||||
try {
|
||||
const proxy = this.proxies.find(p => p.id === proxyId);
|
||||
if (!proxy) return;
|
||||
|
||||
// 关闭详情模态框
|
||||
const detailModal = bootstrap.Modal.getInstance(document.getElementById('proxyDetailModal'));
|
||||
if (detailModal) detailModal.hide();
|
||||
|
||||
// 显示验证进度
|
||||
const modal = new bootstrap.Modal(document.getElementById('validationModal'));
|
||||
document.getElementById('validationProgress').style.display = 'block';
|
||||
document.getElementById('validationResults').innerHTML = '';
|
||||
document.getElementById('validationStatus').textContent = `正在验证 ${proxy.ip}:${proxy.port}...`;
|
||||
document.getElementById('validationProgressBar').style.width = '50%';
|
||||
modal.show();
|
||||
|
||||
const response = await fetch('/api/proxies/verify', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
ip: proxy.ip,
|
||||
port: proxy.port
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// 更新进度条
|
||||
document.getElementById('validationProgressBar').style.width = '100%';
|
||||
document.getElementById('validationStatus').textContent = '验证完成';
|
||||
|
||||
// 显示结果
|
||||
document.getElementById('validationResults').innerHTML = `
|
||||
<div class="alert ${result.data.isValid ? 'alert-success' : 'alert-danger'}">
|
||||
<h6>验证结果: ${result.data.isValid ? '成功' : '失败'}</h6>
|
||||
<p><strong>IP:</strong> ${result.data.ip}:${result.data.port}</p>
|
||||
<p><strong>响应时间:</strong> ${result.data.responseTime}ms</p>
|
||||
${result.data.error ? `<p><strong>错误信息:</strong> ${result.data.error}</p>` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 延迟刷新列表
|
||||
setTimeout(() => {
|
||||
this.loadProxies();
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('验证代理失败:', error);
|
||||
this.showAlert('验证代理失败: ' + error.message, 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
async deleteProxy(proxyId) {
|
||||
if (!confirm('确定要删除这个代理吗?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const proxy = this.proxies.find(p => p.id === proxyId);
|
||||
if (!proxy) return;
|
||||
|
||||
// 这里应该调用删除API,但目前我们的API没有单个删除功能
|
||||
// 暂时显示提示
|
||||
this.showAlert(`代理 ${proxy.ip}:${proxy.port} 删除功能待实现`, 'info');
|
||||
|
||||
} catch (error) {
|
||||
console.error('删除代理失败:', error);
|
||||
this.showAlert('删除代理失败: ' + error.message, 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
toggleProxySelection(proxyId) {
|
||||
if (this.selectedProxies.has(proxyId)) {
|
||||
this.selectedProxies.delete(proxyId);
|
||||
} else {
|
||||
this.selectedProxies.add(proxyId);
|
||||
}
|
||||
this.updateSelectAllCheckbox();
|
||||
}
|
||||
|
||||
toggleSelectAll() {
|
||||
const selectAll = document.getElementById('selectAll').checked;
|
||||
const checkboxes = document.querySelectorAll('.proxy-checkbox');
|
||||
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.checked = selectAll;
|
||||
const proxyId = parseInt(checkbox.value);
|
||||
if (selectAll) {
|
||||
this.selectedProxies.add(proxyId);
|
||||
} else {
|
||||
this.selectedProxies.delete(proxyId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateSelectAllCheckbox() {
|
||||
const checkboxes = document.querySelectorAll('.proxy-checkbox');
|
||||
const selectAll = document.getElementById('selectAll');
|
||||
selectAll.checked = checkboxes.length > 0 && this.selectedProxies.size === checkboxes.length;
|
||||
}
|
||||
|
||||
goToPage(page) {
|
||||
this.currentPage = page;
|
||||
this.loadProxies();
|
||||
}
|
||||
|
||||
async validateAll() {
|
||||
if (this.selectedProxies.size === 0) {
|
||||
this.showAlert('请先选择要验证的代理', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const selectedProxiesList = Array.from(this.selectedProxies).map(id =>
|
||||
this.proxies.find(p => p.id === id)
|
||||
).filter(p => p);
|
||||
|
||||
// 显示验证进度
|
||||
const modal = new bootstrap.Modal(document.getElementById('validationModal'));
|
||||
document.getElementById('validationProgress').style.display = 'block';
|
||||
document.getElementById('validationResults').innerHTML = '';
|
||||
document.getElementById('validationProgressBar').style.width = '0%';
|
||||
modal.show();
|
||||
|
||||
const response = await fetch('/api/proxies/verify-batch', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
proxies: selectedProxiesList
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// 更新进度条
|
||||
document.getElementById('validationProgressBar').style.width = '100%';
|
||||
document.getElementById('validationStatus').textContent = '验证完成';
|
||||
|
||||
// 显示结果
|
||||
document.getElementById('validationResults').innerHTML = `
|
||||
<div class="alert alert-info">
|
||||
<h6>批量验证完成</h6>
|
||||
<p><strong>总验证数:</strong> ${result.data.validated}</p>
|
||||
<p><strong>有效代理:</strong> <span class="badge bg-success">${result.data.valid}</span></p>
|
||||
<p><strong>无效代理:</strong> <span class="badge bg-danger">${result.data.invalid}</span></p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 延迟刷新列表
|
||||
setTimeout(() => {
|
||||
this.loadProxies();
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('批量验证失败:', error);
|
||||
this.showAlert('批量验证失败: ' + error.message, 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
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.loadProxies();
|
||||
} else {
|
||||
this.showAlert('清理失败: ' + result.error, 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('清理无效代理失败:', error);
|
||||
this.showAlert('清理无效代理失败: ' + error.message, 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
async exportProxies() {
|
||||
try {
|
||||
const response = await fetch('/api/proxies?limit=1000&validOnly=true');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const csv = this.convertToCSV(result.data);
|
||||
this.downloadCSV(csv, `valid_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');
|
||||
}
|
||||
}
|
||||
|
||||
async startScrape() {
|
||||
try {
|
||||
const response = await fetch('/api/dashboard/actions/scrape', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ pages: 40 })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.showAlert('抓取任务已启动', 'success');
|
||||
// 延迟刷新数据
|
||||
setTimeout(() => this.loadProxies(), 2000);
|
||||
} else {
|
||||
this.showAlert('启动抓取任务失败: ' + result.error, 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('启动抓取任务失败:', error);
|
||||
this.showAlert('启动抓取任务失败: ' + error.message, 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
formatDateTime(dateString) {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
showLoading(show) {
|
||||
const spinner = document.querySelector('#proxyTableBody .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';
|
||||
}
|
||||
|
||||
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.last_check_time || '',
|
||||
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);
|
||||
}
|
||||
|
||||
// 导入代理功能
|
||||
async importProxies(proxies) {
|
||||
try {
|
||||
this.showAlert('开始导入并验证代理...', 'info');
|
||||
|
||||
const response = await fetch('/api/proxies/import', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ proxies })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const { data } = result;
|
||||
let message = `导入完成!`;
|
||||
message += `\n📊 总数: ${data.total}`;
|
||||
message += `\n✅ 格式有效: ${data.format_valid}`;
|
||||
message += `\n❌ 格式无效: ${data.format_invalid}`;
|
||||
message += `\n🔍 验证通过: ${data.valid}`;
|
||||
message += `\n❌ 验证失败: ${data.invalid}`;
|
||||
message += `\n💾 保存成功: ${data.saved}`;
|
||||
|
||||
this.showAlert(message, 'success');
|
||||
|
||||
// 刷新代理列表
|
||||
setTimeout(() => this.loadProxies(), 2000);
|
||||
} else {
|
||||
this.showAlert('导入失败: ' + result.error, 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('导入代理失败:', error);
|
||||
this.showAlert('导入代理失败: ' + error.message, 'danger');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 全局函数(供HTML调用)
|
||||
let proxyManager;
|
||||
|
||||
async function refreshProxies() {
|
||||
if (proxyManager) {
|
||||
await proxyManager.loadProxies();
|
||||
proxyManager.showAlert('代理列表已刷新', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
async function validateAll() {
|
||||
if (proxyManager) {
|
||||
await proxyManager.validateAll();
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupInvalid() {
|
||||
if (proxyManager) {
|
||||
await proxyManager.cleanupInvalid();
|
||||
}
|
||||
}
|
||||
|
||||
async function exportProxies() {
|
||||
if (proxyManager) {
|
||||
await proxyManager.exportProxies();
|
||||
}
|
||||
}
|
||||
|
||||
// 显示导入模态框
|
||||
function showImportModal() {
|
||||
const modal = new bootstrap.Modal(document.getElementById('importModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// 从模态框导入代理
|
||||
async function importProxiesFromModal() {
|
||||
const jsonInput = document.getElementById('proxyJsonInput').value.trim();
|
||||
|
||||
if (!jsonInput) {
|
||||
alert('请输入代理JSON数据');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const proxies = JSON.parse(jsonInput);
|
||||
|
||||
if (!Array.isArray(proxies)) {
|
||||
alert('代理数据必须是数组格式');
|
||||
return;
|
||||
}
|
||||
|
||||
// 关闭模态框
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('importModal'));
|
||||
modal.hide();
|
||||
|
||||
// 清空输入框
|
||||
document.getElementById('proxyJsonInput').value = '';
|
||||
|
||||
// 开始导入
|
||||
if (proxyManager) {
|
||||
await proxyManager.importProxies(proxies);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('JSON格式错误: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理文件导入
|
||||
async function handleImportFile(event) {
|
||||
const file = event.target.files[0];
|
||||
|
||||
if (!file) return;
|
||||
|
||||
if (!file.name.endsWith('.json')) {
|
||||
alert('请选择JSON文件');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const text = await file.text();
|
||||
const proxies = JSON.parse(text);
|
||||
|
||||
if (!Array.isArray(proxies)) {
|
||||
alert('文件内容必须是代理数组');
|
||||
return;
|
||||
}
|
||||
|
||||
// 清空文件输入
|
||||
event.target.value = '';
|
||||
|
||||
// 开始导入
|
||||
if (proxyManager) {
|
||||
await proxyManager.importProxies(proxies);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('文件读取失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
proxyManager = new ProxyManager();
|
||||
});
|
||||
Reference in New Issue
Block a user