jdeip/app/templates/index.html
2025-10-30 14:34:05 +08:00

499 lines
16 KiB
HTML
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.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EIP IP轮换控制面板</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
font-weight: 300;
}
.header p {
font-size: 1.1em;
opacity: 0.9;
}
.content {
padding: 40px;
}
.form-group {
margin-bottom: 30px;
}
.form-group label {
display: block;
margin-bottom: 10px;
font-weight: 600;
color: #333;
font-size: 1.1em;
}
.checkbox-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
max-height: 300px;
overflow-y: auto;
border: 2px solid #e9ecef;
border-radius: 8px;
padding: 20px;
background: #f8f9fa;
}
.checkbox-item {
display: flex;
align-items: center;
padding: 8px 0;
}
.checkbox-item input[type="checkbox"] {
margin-right: 10px;
transform: scale(1.2);
}
.checkbox-item label {
margin: 0;
font-weight: 500;
cursor: pointer;
color: #495057;
}
.select-all-container {
margin-bottom: 15px;
padding: 10px;
background: #e3f2fd;
border-radius: 6px;
border-left: 4px solid #2196f3;
}
.select-all-container input[type="checkbox"] {
transform: scale(1.3);
margin-right: 10px;
}
.select-all-container label {
font-weight: 600;
color: #1976d2;
}
.table-container {
background: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.table-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
text-align: center;
}
.table-header h2 {
font-size: 1.5em;
font-weight: 300;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 15px;
text-align: center;
border-bottom: 1px solid #e9ecef;
}
th {
background: #f8f9fa;
font-weight: 600;
color: #495057;
font-size: 1.1em;
}
tr:hover {
background: #f8f9fa;
}
.btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 8px 16px;
font-size: 0.9em;
border-radius: 20px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
}
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn.rotating {
background: #6c757d;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.result {
margin-top: 20px;
padding: 15px;
border-radius: 8px;
display: none;
}
.result.success {
background: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.result.error {
background: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
.loading {
display: none;
text-align: center;
margin: 20px 0;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.status-active {
color: #28a745;
font-weight: 600;
}
.status-inactive {
color: #dc3545;
font-weight: 600;
}
.ip-address {
font-family: 'Courier New', monospace;
background: #e9ecef;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.9em;
}
.rotate-count {
background: #e3f2fd;
color: #1976d2;
padding: 4px 8px;
border-radius: 4px;
font-weight: 600;
font-size: 0.9em;
}
.last-rotate-time {
font-size: 0.85em;
color: #666;
font-family: 'Courier New', monospace;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🌐 EIP IP轮换控制面板</h1>
<p>智能IP地址轮换管理系统</p>
</div>
<div class="content">
<!-- 省份选择 -->
<div class="form-group">
<label>选择省份(默认全选)</label>
<div class="select-all-container">
<input type="checkbox" id="select-all" checked>
<label for="select-all">全选/取消全选</label>
</div>
<div class="checkbox-container" id="provinces-container">
<!-- 省份选项将通过JavaScript动态加载 -->
</div>
</div>
<!-- 线路信息表格 -->
<div class="table-container">
<div class="table-header">
<h2>线路信息管理</h2>
</div>
<table>
<thead>
<tr>
<th>序号</th>
<th>端口号</th>
<th>当前IP</th>
<th>状态</th>
<th>轮换次数</th>
<th>最后轮换时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="lines-table-body">
<!-- 线路数据将通过JavaScript动态加载 -->
</tbody>
</table>
</div>
<!-- 加载状态 -->
<div class="loading" id="loading">
<div class="spinner"></div>
<p>正在加载线路信息...</p>
</div>
<!-- 结果显示 -->
<div class="result" id="result"></div>
</div>
</div>
<script>
let linesData = [];
let allProvinces = [];
let selectedProvinces = [];
// 页面加载时初始化
document.addEventListener('DOMContentLoaded', function() {
loadProvinces();
loadLines();
setupSelectAll();
});
// 加载省份列表
async function loadProvinces() {
try {
const response = await fetch('/proxy/cities');
const data = await response.json();
allProvinces = data.data;
selectedProvinces = [...allProvinces]; // 默认全选
renderProvinces();
} catch (error) {
console.error('加载省份失败:', error);
showResult('加载省份列表失败: ' + error.message, 'error');
}
}
// 渲染省份选项
function renderProvinces() {
const container = document.getElementById('provinces-container');
container.innerHTML = '';
allProvinces.forEach(province => {
const div = document.createElement('div');
div.className = 'checkbox-item';
div.innerHTML = `
<input type="checkbox" id="province-${province}" value="${province}"
${selectedProvinces.includes(province) ? 'checked' : ''}
onchange="updateSelection()">
<label for="province-${province}">${province}</label>
`;
container.appendChild(div);
});
}
// 设置全选功能
function setupSelectAll() {
const selectAllCheckbox = document.getElementById('select-all');
selectAllCheckbox.addEventListener('change', function() {
const checkboxes = document.querySelectorAll('#provinces-container input[type="checkbox"]');
checkboxes.forEach(checkbox => {
checkbox.checked = this.checked;
});
updateSelection();
});
}
// 更新选择状态
function updateSelection() {
const checkboxes = document.querySelectorAll('#provinces-container input[type="checkbox"]');
selectedProvinces = Array.from(checkboxes)
.filter(cb => cb.checked)
.map(cb => cb.value);
// 更新全选状态
const selectAllCheckbox = document.getElementById('select-all');
selectAllCheckbox.checked = selectedProvinces.length === allProvinces.length;
}
// 加载线路列表
async function loadLines() {
try {
document.getElementById('loading').style.display = 'block';
const response = await fetch('/proxy/lines');
const data = await response.json();
linesData = data.data;
renderLines();
} catch (error) {
console.error('加载线路失败:', error);
showResult('加载线路信息失败: ' + error.message, 'error');
} finally {
document.getElementById('loading').style.display = 'none';
}
}
// 渲染线路表格
function renderLines() {
const tbody = document.getElementById('lines-table-body');
tbody.innerHTML = '';
linesData.forEach((line, index) => {
const row = document.createElement('tr');
const lastRotateTime = line.last_rotate_time ?
new Date(line.last_rotate_time).toLocaleString('zh-CN') : '从未轮换';
row.innerHTML = `
<td>${index + 1}</td>
<td>${line.port}</td>
<td><span class="ip-address">${line.ip}</span></td>
<td><span class="status-${line.status}">${line.status === 'active' ? '活跃' : '离线'}</span></td>
<td><span class="rotate-count">${line.rotate_count || 0}</span></td>
<td><span class="last-rotate-time">${lastRotateTime}</span></td>
<td>
<button class="btn" onclick="rotateLine(${line.id})" id="btn-${line.id}">
🔄 换IP
</button>
</td>
`;
tbody.appendChild(row);
});
}
// 执行单条线路IP轮换
async function rotateLine(lineId) {
const button = document.getElementById(`btn-${lineId}`);
const originalText = button.innerHTML;
// 显示加载状态
button.disabled = true;
button.classList.add('rotating');
button.innerHTML = '⏳ 轮换中...';
try {
const response = await fetch('/proxy/rotate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: lineId,
citys: selectedProvinces.join(',') // 使用选中的省份
})
});
const result = await response.json();
if (result.changed) {
showResult(`
<h3>✅ 线路 ${lineId} IP轮换成功</h3>
<p><strong>新IP地址:</strong> ${result.ip}</p>
<p><strong>边缘设备:</strong> ${result.edge}</p>
<p><strong>地理位置:</strong> ${result.geo}</p>
`, 'success');
// 更新表格中的IP地址
const line = linesData.find(l => l.id === lineId);
if (line) {
line.ip = result.ip;
line.rotate_count = line.rotate_count+1;
renderLines(); // 重新渲染表格
}
} else {
showResult(`
<h3>⚠️ 线路 ${lineId} IP轮换失败</h3>
<p><strong>原因:</strong> ${result.reason}</p>
`, 'error');
}
} catch (error) {
console.error('轮换失败:', error);
showResult('线路 ' + lineId + ' IP轮换失败: ' + error.message, 'error');
} finally {
// 恢复按钮状态
button.disabled = false;
button.classList.remove('rotating');
button.innerHTML = originalText;
}
}
// 显示结果
function showResult(message, type) {
const resultDiv = document.getElementById('result');
resultDiv.innerHTML = message;
resultDiv.className = `result ${type}`;
resultDiv.style.display = 'block';
// 3秒后自动隐藏成功消息
if (type === 'success') {
setTimeout(() => {
resultDiv.style.display = 'none';
}, 3000);
}
}
</script>
</body>
</html>