first commit
This commit is contained in:
352
Xboard/plugins/UserOnlineDevices/resources/views/panel.blade.php
Normal file
352
Xboard/plugins/UserOnlineDevices/resources/views/panel.blade.php
Normal file
@@ -0,0 +1,352 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>在线设备与在线 IP</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f3f0e8;
|
||||
--card: rgba(255, 252, 246, 0.94);
|
||||
--line: #d8ccb6;
|
||||
--text: #2f261a;
|
||||
--muted: #786b58;
|
||||
--accent: #1f7a5a;
|
||||
--accent-soft: #d9efe5;
|
||||
--warning: #8f4c2d;
|
||||
--shadow: 0 18px 50px rgba(77, 56, 27, 0.12);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
font-family: Georgia, "Times New Roman", serif;
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(255, 255, 255, 0.75), transparent 34%),
|
||||
linear-gradient(135deg, #efe4d0 0%, #f9f5ee 45%, #e6dccb 100%);
|
||||
}
|
||||
|
||||
.wrap {
|
||||
max-width: 1120px;
|
||||
margin: 0 auto;
|
||||
padding: 32px 20px 48px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(30px, 5vw, 54px);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
max-width: 720px;
|
||||
font-size: 16px;
|
||||
line-height: 1.7;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid rgba(216, 204, 182, 0.8);
|
||||
border-radius: 24px;
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.stat {
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
margin-top: 12px;
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.stat-note {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 0.8fr;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.panel h2 {
|
||||
margin: 0 0 16px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.device-list,
|
||||
.session-list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.row {
|
||||
padding: 16px;
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.65);
|
||||
border: 1px solid rgba(216, 204, 182, 0.7);
|
||||
}
|
||||
|
||||
.row-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.row-title {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.row-meta {
|
||||
margin-top: 8px;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 28px 20px;
|
||||
border: 1px dashed var(--line);
|
||||
border-radius: 18px;
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
background: rgba(255, 255, 255, 0.48);
|
||||
}
|
||||
|
||||
.footer-note {
|
||||
margin-top: 18px;
|
||||
color: var(--warning);
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<section class="hero">
|
||||
<div class="eyebrow">Xboard Plugin Dashboard</div>
|
||||
<h1>在线设备与在线 IP</h1>
|
||||
<div class="subtitle">
|
||||
当前页面由 User Online Devices 插件生成,会自动刷新并显示该账号最近上报到 Xboard 的在线 IP 列表。
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="summary">
|
||||
<article class="card stat">
|
||||
<div class="stat-label">账号</div>
|
||||
<div class="stat-value" id="user-email">{{ $payload['user']['email'] }}</div>
|
||||
<div class="stat-note">用户 ID: <span id="user-id">{{ $payload['user']['id'] }}</span></div>
|
||||
</article>
|
||||
<article class="card stat">
|
||||
<div class="stat-label">在线设备数</div>
|
||||
<div class="stat-value" id="online-count">{{ $payload['user']['online_count'] }}</div>
|
||||
<div class="stat-note">按实时在线 IP 去重统计</div>
|
||||
</article>
|
||||
<article class="card stat">
|
||||
<div class="stat-label">最后活跃</div>
|
||||
<div class="stat-value" id="last-online">{{ $payload['user']['last_online_at'] ? date('Y-m-d H:i:s', $payload['user']['last_online_at']) : '暂无记录' }}</div>
|
||||
<div class="stat-note">数据来源于 Xboard 设备状态服务</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="grid">
|
||||
<article class="card panel">
|
||||
<div class="panel-head">
|
||||
<h2>在线 IP 列表</h2>
|
||||
<div class="badge">每 {{ $refreshInterval }} 秒刷新</div>
|
||||
</div>
|
||||
<div class="device-list" id="device-list"></div>
|
||||
<div class="footer-note">
|
||||
说明:这里展示的是节点最近上报的在线 IP,Xboard 本体按 IP 去重,所以“在线设备”本质上是“当前唯一在线 IP 数”。
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="card panel">
|
||||
<div class="panel-head">
|
||||
<h2>登录会话</h2>
|
||||
<div class="badge" id="session-badge">{{ $payload['meta']['show_active_sessions'] ? '已开启' : '已关闭' }}</div>
|
||||
</div>
|
||||
<div class="session-list" id="session-list"></div>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const snapshotUrl = @json($snapshotUrl);
|
||||
const refreshInterval = {{ (int) $refreshInterval }};
|
||||
const initialPayload = @json($payload);
|
||||
|
||||
function formatTime(timestamp) {
|
||||
if (!timestamp) return '暂无记录';
|
||||
const date = new Date(timestamp * 1000);
|
||||
const pad = (value) => String(value).padStart(2, '0');
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
||||
}
|
||||
|
||||
function createEmpty(message) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'empty';
|
||||
div.textContent = message;
|
||||
return div;
|
||||
}
|
||||
|
||||
function renderDevices(devices) {
|
||||
const root = document.getElementById('device-list');
|
||||
root.innerHTML = '';
|
||||
|
||||
if (!devices.length) {
|
||||
root.appendChild(createEmpty('当前没有检测到在线设备。'));
|
||||
return;
|
||||
}
|
||||
|
||||
devices.forEach((device) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'row';
|
||||
row.innerHTML = `
|
||||
<div class="row-top">
|
||||
<div class="row-title">${device.name}</div>
|
||||
<div class="badge">在线</div>
|
||||
</div>
|
||||
<div class="row-meta">IP: ${device.ip}</div>
|
||||
`;
|
||||
root.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function renderSessions(sessions, enabled) {
|
||||
const root = document.getElementById('session-list');
|
||||
root.innerHTML = '';
|
||||
|
||||
if (!enabled) {
|
||||
root.appendChild(createEmpty('管理员已关闭登录会话展示。'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sessions.length) {
|
||||
root.appendChild(createEmpty('当前没有可展示的登录会话。'));
|
||||
return;
|
||||
}
|
||||
|
||||
sessions.forEach((session) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'row';
|
||||
row.innerHTML = `
|
||||
<div class="row-top">
|
||||
<div class="row-title">${session.device}</div>
|
||||
<div class="badge">#${session.id}</div>
|
||||
</div>
|
||||
<div class="row-meta">
|
||||
最近使用: ${formatTime(session.last_used_at)}<br>
|
||||
创建时间: ${formatTime(session.created_at)}<br>
|
||||
过期时间: ${formatTime(session.expires_at)}
|
||||
</div>
|
||||
`;
|
||||
root.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function render(payload) {
|
||||
document.getElementById('user-email').textContent = payload.user.email;
|
||||
document.getElementById('user-id').textContent = payload.user.id;
|
||||
document.getElementById('online-count').textContent = payload.user.online_count;
|
||||
document.getElementById('last-online').textContent = formatTime(payload.user.last_online_at);
|
||||
document.getElementById('session-badge').textContent = payload.meta.show_active_sessions ? '已开启' : '已关闭';
|
||||
renderDevices(payload.online_devices || []);
|
||||
renderSessions(payload.active_sessions || [], payload.meta.show_active_sessions);
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
try {
|
||||
const response = await fetch(snapshotUrl, {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
if (!response.ok) return;
|
||||
const result = await response.json();
|
||||
if (result && result.data) {
|
||||
render(result.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh online devices snapshot.', error);
|
||||
}
|
||||
}
|
||||
|
||||
render(initialPayload);
|
||||
window.setInterval(refresh, refreshInterval * 1000);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user