dailiip/public/js/proxies.js

804 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 代理管理页面JavaScript
class 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 = '验证完成';
// 显示结果
let responseResultHtml = '';
if (result.data.responseStatus !== null && result.data.responseStatus !== undefined) {
responseResultHtml = `
<hr>
<h6>响应结果:</h6>
<p><strong>状态码:</strong> <span class="badge ${result.data.responseStatus >= 200 && result.data.responseStatus < 300 ? 'bg-success' : 'bg-warning'}">${result.data.responseStatus}</span></p>
`;
if (result.data.responseData) {
// 限制显示长度,避免内容过长
let displayData = result.data.responseData;
// const maxLength = 300;
// if (displayData.length > maxLength) {
// displayData = displayData.substring(0, maxLength) + '... (内容已截断,完整内容请查看日志)';
// }
responseResultHtml += `
<p><strong>响应内容:</strong></p>
<pre class="bg-light p-2 border rounded" style="max-height: 200px; overflow-y: auto; font-size: 12px; white-space: pre-wrap; word-wrap: break-word;">${this.escapeHtml(displayData)}</pre>
`;
}
if (result.data.testUrl) {
responseResultHtml += `
<p class="text-muted small"><strong>测试URL:</strong> ${result.data.testUrl}</p>
`;
}
} else if (result.data.error) {
responseResultHtml = `
<hr>
<h6>响应结果:</h6>
<p class="text-danger"><strong>错误:</strong> ${result.data.error}</p>
`;
}
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>
${responseResultHtml}
`;
// 延迟刷新列表
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';
}
escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
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();
});