132 lines
5.8 KiB
HTML
132 lines
5.8 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>IP 替换历史</title>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<style>
|
||
:root {
|
||
--bg: linear-gradient(135deg, #0f172a, #1e293b);
|
||
--card: #0b1224;
|
||
--accent: #22d3ee;
|
||
--accent-2: #a855f7;
|
||
--text: #e2e8f0;
|
||
--muted: #94a3b8;
|
||
--danger: #f87171;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
body {
|
||
margin: 0;
|
||
min-height: 100vh;
|
||
font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
padding: 28px;
|
||
}
|
||
.shell {
|
||
max-width: 900px;
|
||
margin: 0 auto;
|
||
background: var(--card);
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
border-radius: 14px;
|
||
padding: 24px;
|
||
box-shadow: 0 25px 60px rgba(0, 0, 0, 0.45);
|
||
}
|
||
h1 { margin: 0 0 10px; letter-spacing: 0.6px; }
|
||
label { font-weight: 600; color: #cbd5e1; display: block; margin-bottom: 6px; }
|
||
input { width: 100%; padding: 11px 12px; border-radius: 10px; border: 1px solid rgba(255,255,255,0.08); background: rgba(255,255,255,0.04); color: var(--text); }
|
||
button { margin-top: 12px; padding: 10px 14px; border-radius: 10px; border: none; cursor: pointer; background: linear-gradient(135deg, var(--accent), var(--accent-2)); color: var(--text); font-weight: 700; }
|
||
.muted { color: var(--muted); }
|
||
.status { margin-top: 12px; padding: 12px; border-radius: 10px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); }
|
||
.history-item { padding: 10px 0; border-bottom: 1px solid rgba(255,255,255,0.06); }
|
||
.history-item:last-child { border-bottom: none; }
|
||
.badge { display: inline-block; padding: 2px 8px; background: rgba(255,255,255,0.08); border-radius: 999px; margin-right: 6px; font-size: 12px; }
|
||
.mono { font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; }
|
||
.row { display: flex; gap: 10px; }
|
||
.row > div { flex: 1; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="shell">
|
||
<h1>IP 替换历史</h1>
|
||
<p class="muted" style="margin-top:0;">按 IP 或 group_id 查看完整链路(a→b→c)。group_id 默认继承上一跳,否则用旧 IP。</p>
|
||
<div class="row">
|
||
<div>
|
||
<label for="ip">IP(旧/新任意一个)</label>
|
||
<input id="ip" placeholder="例如:18.133.222.207">
|
||
</div>
|
||
<div>
|
||
<label for="group">group_id(可选)</label>
|
||
<input id="group" placeholder="默认使用旧 IP 作为 group_id">
|
||
</div>
|
||
</div>
|
||
<button id="search-btn">搜索</button>
|
||
<div id="result" class="status" style="display:none;"></div>
|
||
<div id="list"></div>
|
||
</div>
|
||
|
||
<script>
|
||
const ipInput = document.getElementById('ip');
|
||
const groupInput = document.getElementById('group');
|
||
const searchBtn = document.getElementById('search-btn');
|
||
const result = document.getElementById('result');
|
||
const list = document.getElementById('list');
|
||
|
||
function showMsg(msg, isError=false) {
|
||
result.style.display = 'block';
|
||
result.className = 'status' + (isError ? ' error' : '');
|
||
result.textContent = msg;
|
||
}
|
||
|
||
function render(items) {
|
||
if (!items || !items.length) {
|
||
list.innerHTML = '<div class="muted" style="margin-top:10px;">暂无记录</div>';
|
||
return;
|
||
}
|
||
list.innerHTML = items.map(group => {
|
||
const chainStr = (group.chain || []).join(' ➜ ');
|
||
const detail = (group.items || []).map(item => `
|
||
<div class="history-item">
|
||
<div><span class="badge">旧 IP</span><span class="mono">${item.old_ip}</span></div>
|
||
<div><span class="badge">新 IP</span><span class="mono">${item.new_ip}</span></div>
|
||
<div class="muted" style="margin-top:4px;">${item.account_name} | ${item.created_at} | 流量(30天): ${item.terminated_network_out_mb ?? '-' } MB</div>
|
||
</div>
|
||
`).join('');
|
||
return `
|
||
<div class="status" style="margin-top:12px;">
|
||
<div><strong>Group</strong>: <span class="mono">${group.group_id || '-'}</span></div>
|
||
<div class="muted" style="margin:4px 0;">首台开机时间:${group.first_ip_start || '-'}</div>
|
||
<div class="muted" style="margin:6px 0;">链路:${chainStr}</div>
|
||
<div>${detail}</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
async function loadChains() {
|
||
const ip = ipInput.value.trim();
|
||
const group = groupInput.value.trim();
|
||
searchBtn.disabled = true;
|
||
showMsg('查询中...');
|
||
try {
|
||
const params = new URLSearchParams();
|
||
if (ip) params.append('ip', ip);
|
||
if (group) params.append('group', group);
|
||
const resp = await fetch('/history/chains?' + params.toString());
|
||
const data = await resp.json();
|
||
if (!resp.ok) throw new Error(data.error || '查询失败');
|
||
result.style.display = 'none';
|
||
render(data.items || []);
|
||
} catch (err) {
|
||
showMsg(err.message, true);
|
||
} finally {
|
||
searchBtn.disabled = false;
|
||
}
|
||
}
|
||
|
||
searchBtn.addEventListener('click', loadChains);
|
||
loadChains();
|
||
</script>
|
||
</body>
|
||
</html>
|