// 代理管理页面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 = ` `; 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(); });