124 lines
4.6 KiB
HTML
124 lines
4.6 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>客户端详情 - meshgo</title>
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
<style>
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f0f2f5; color: #333; }
|
|
header { background: #1677ff; color: #fff; padding: 16px 24px; font-size: 18px; font-weight: 600; display: flex; align-items: center; gap: 12px; }
|
|
.back-btn { color: #fff; text-decoration: none; font-size: 14px; opacity: 0.8; }
|
|
.back-btn:hover { opacity: 1; }
|
|
.container { max-width: 1000px; margin: 24px auto; padding: 0 16px; }
|
|
|
|
.card { background: #fff; border-radius: 8px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,.08); margin-bottom: 16px; }
|
|
.card h3 { font-size: 15px; color: #333; margin-bottom: 16px; border-bottom: 1px solid #eee; padding-bottom: 10px; }
|
|
.info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; }
|
|
.info-item { }
|
|
.info-item .label { font-size: 12px; color: #888; margin-bottom: 4px; }
|
|
.info-item .value { font-size: 14px; color: #333; font-weight: 500; }
|
|
|
|
.sub-list { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
.sub-tag { display: inline-block; padding: 4px 12px; background: #e6f4ff; color: #1677ff; border-radius: 4px; font-size: 13px; font-family: monospace; }
|
|
|
|
.online-dot { display: inline-block; width: 8px; height: 8px; background: #52c41a; border-radius: 50%; margin-right: 6px; }
|
|
.status-online { color: #52c41a; }
|
|
.status-offline { color: #ff4d4f; }
|
|
|
|
.loading, .error, .not-found { text-align: center; padding: 48px; color: #888; }
|
|
.error { color: #ff4d4f; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header>
|
|
<a href="/admin/mqtt" class="back-btn">← 返回</a>
|
|
客户端详情
|
|
</header>
|
|
|
|
<div class="container">
|
|
<div id="app">
|
|
<div class="loading">加载中...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const clientId = window.location.pathname.split('/').pop();
|
|
|
|
function renderClient(client, subs) {
|
|
if (!client) {
|
|
document.getElementById('app').innerHTML = '<div class="card"><div class="not-found">客户端不存在或已离线</div></div>';
|
|
return;
|
|
}
|
|
|
|
const time = new Date(client.connected_at).toLocaleString('zh-CN', { hour12: false });
|
|
const subsHtml = subs.length > 0
|
|
? subs.map(s => `<span class="sub-tag">${esc(s)}</span>`).join('')
|
|
: '<span style="color:#aaa">暂无订阅</span>';
|
|
|
|
document.getElementById('app').innerHTML = `
|
|
<div class="card">
|
|
<h3>基本信息</h3>
|
|
<div class="info-grid">
|
|
<div class="info-item">
|
|
<div class="label">状态</div>
|
|
<div class="value"><span class="online-dot"></span><span class="status-online">在线</span></div>
|
|
</div>
|
|
<div class="info-item">
|
|
<div class="label">客户端 ID</div>
|
|
<div class="value" style="font-family:monospace;font-size:13px">${esc(client.id)}</div>
|
|
</div>
|
|
<div class="info-item">
|
|
<div class="label">IP 地址</div>
|
|
<div class="value">${esc(client.remote_addr)}</div>
|
|
</div>
|
|
<div class="info-item">
|
|
<div class="label">用户名</div>
|
|
<div class="value">${esc(client.username)}</div>
|
|
</div>
|
|
<div class="info-item">
|
|
<div class="label">连接时间</div>
|
|
<div class="value">${time}</div>
|
|
</div>
|
|
<div class="info-item">
|
|
<div class="label">订阅数</div>
|
|
<div class="value">${client.subs_count}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>订阅主题 (${subs.length})</h3>
|
|
<div class="sub-list">${subsHtml}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function esc(s) {
|
|
if (!s) return '';
|
|
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
}
|
|
|
|
function load() {
|
|
Promise.all([
|
|
fetch('/admin/mqtt/api/client/' + encodeURIComponent(clientId)).then(r => r.json()),
|
|
fetch('/admin/mqtt/api/client/' + encodeURIComponent(clientId) + '/subs').then(r => r.json())
|
|
]).then(([clientRes, subsRes]) => {
|
|
if (clientRes.code !== 0) {
|
|
document.getElementById('app').innerHTML = '<div class="card"><div class="error">获取客户端信息失败</div></div>';
|
|
return;
|
|
}
|
|
renderClient(clientRes.data, subsRes.data || []);
|
|
}).catch(() => {
|
|
document.getElementById('app').innerHTML = '<div class="card"><div class="error">网络错误</div></div>';
|
|
});
|
|
}
|
|
|
|
load();
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|