// 代理管理页面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 = `
|
暂无代理数据
|
`;
return;
}
tbody.innerHTML = this.proxies.map(proxy => `
|
|
${proxy.ip}
|
${proxy.port} |
${proxy.location || '-'}
|
${proxy.is_valid ? '可用' : '不可用'}
|
${proxy.response_time ? proxy.response_time + 'ms' : '-'}
|
${proxy.last_check_time ? this.formatDateTime(proxy.last_check_time) : '-'}
|
${this.formatDateTime(proxy.created_at)}
|
|
`).join('');
}
renderPagination() {
const pagination = document.getElementById('pagination');
const totalPages = Math.ceil(this.totalCount / this.pageSize);
if (totalPages <= 1) {
pagination.innerHTML = '';
return;
}
let paginationHTML = '';
// 上一页
paginationHTML += `
`;
// 页码
const startPage = Math.max(1, this.currentPage - 2);
const endPage = Math.min(totalPages, this.currentPage + 2);
if (startPage > 1) {
paginationHTML += `
1
`;
if (startPage > 2) {
paginationHTML += '...';
}
}
for (let i = startPage; i <= endPage; i++) {
paginationHTML += `
${i}
`;
}
if (endPage < totalPages) {
if (endPage < totalPages - 1) {
paginationHTML += '...';
}
paginationHTML += `
${totalPages}
`;
}
// 下一页
paginationHTML += `
`;
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 = `
IP地址:
${proxy.ip}
端口:
${proxy.port}
位置:
${proxy.location || '-'}
状态:
${proxy.is_valid ? '可用' : '不可用'}
响应时间:
${proxy.response_time ? proxy.response_time + 'ms' : '-'}
最后验证:
${proxy.last_check_time ? this.formatDateTime(proxy.last_check_time) : '-'}
创建时间:
${this.formatDateTime(proxy.created_at)}
更新时间:
${this.formatDateTime(proxy.updated_at)}
`;
// 设置验证按钮的数据属性
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 = `
响应结果:
状态码: ${result.data.responseStatus}
`;
if (result.data.responseData) {
// 限制显示长度,避免内容过长
let displayData = result.data.responseData;
// const maxLength = 300;
// if (displayData.length > maxLength) {
// displayData = displayData.substring(0, maxLength) + '... (内容已截断,完整内容请查看日志)';
// }
responseResultHtml += `
响应内容:
${this.escapeHtml(displayData)}
`;
}
if (result.data.testUrl) {
responseResultHtml += `
测试URL: ${result.data.testUrl}
`;
}
} else if (result.data.error) {
responseResultHtml = `
响应结果:
错误: ${result.data.error}
`;
}
document.getElementById('validationResults').innerHTML = `
验证结果: ${result.data.isValid ? '成功' : '失败'}
IP: ${result.data.ip}:${result.data.port}
响应时间: ${result.data.responseTime}ms
${result.data.error ? `
错误信息: ${result.data.error}
` : ''}
${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 = `
批量验证完成
总验证数: ${result.data.validated}
有效代理: ${result.data.valid}
无效代理: ${result.data.invalid}
`;
// 延迟刷新列表
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 = `
${message}
`;
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();
});