Files
UserOnlineStatus/Xboard/plugins/UserOnlineDevices/resources/views/panel.blade.php
CN-JS-HuiBai 2c6a38c80d first commit
2026-04-07 16:54:24 +08:00

353 lines
11 KiB
PHP
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>在线设备与在线 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">
说明:这里展示的是节点最近上报的在线 IPXboard 本体按 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>