整理文件

This commit is contained in:
CN-JS-HuiBai
2026-04-07 18:06:42 +08:00
parent f83ab6745c
commit 26fffef653
12 changed files with 489 additions and 1996 deletions

View File

@@ -0,0 +1,381 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>User Online Devices</title>
<style>
:root {
--bg: #eff5f4;
--panel: #ffffff;
--line: #d9e3e1;
--text: #172126;
--muted: #5f6f74;
--accent: #0f766e;
--accent-soft: rgba(15, 118, 110, 0.10);
--danger-soft: rgba(185, 28, 28, 0.08);
--shadow: 0 18px 46px rgba(23, 33, 38, 0.08);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
color: var(--text);
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
background:
radial-gradient(circle at top right, rgba(15, 118, 110, 0.12), transparent 28%),
linear-gradient(180deg, #f6fbfa 0%, #edf3f1 100%);
}
.wrap {
max-width: 1360px;
margin: 0 auto;
padding: 28px 20px 40px;
}
.hero {
margin-bottom: 20px;
}
.eyebrow {
font-size: 12px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
}
h1 {
margin: 10px 0 12px;
font-size: clamp(30px, 4vw, 50px);
line-height: 1.06;
}
.subtitle {
max-width: 860px;
color: var(--muted);
line-height: 1.8;
font-size: 15px;
}
.summary {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 14px;
margin-bottom: 18px;
}
.card {
background: var(--panel);
border: 1px solid rgba(217, 227, 225, 0.94);
border-radius: 24px;
box-shadow: var(--shadow);
}
.stat {
padding: 22px;
}
.stat-label {
font-size: 12px;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.08em;
}
.stat-value {
margin-top: 10px;
font-size: 34px;
font-weight: 800;
}
.stat-note {
margin-top: 8px;
color: var(--muted);
font-size: 14px;
line-height: 1.6;
}
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
margin-bottom: 18px;
}
.input,
.select,
.btn {
min-height: 46px;
border-radius: 14px;
border: 1px solid var(--line);
background: #fff;
font-size: 14px;
}
.input,
.select {
padding: 12px 14px;
color: var(--text);
}
.input {
min-width: 280px;
flex: 1 1 320px;
}
.select {
min-width: 130px;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 16px;
text-decoration: none;
color: var(--text);
font-weight: 700;
cursor: pointer;
}
.btn.primary {
color: #fff;
border-color: var(--accent);
background: linear-gradient(135deg, #0f766e, #115e59);
}
.panel {
padding: 12px;
}
.table-wrap {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
padding: 14px 12px;
text-align: left;
border-bottom: 1px solid var(--line);
vertical-align: top;
font-size: 14px;
}
th {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
}
tr:last-child td {
border-bottom: 0;
}
.count {
display: inline-flex;
min-width: 42px;
justify-content: center;
align-items: center;
padding: 6px 10px;
border-radius: 999px;
background: var(--accent-soft);
color: var(--accent);
font-weight: 800;
}
.count.zero {
background: #eef2f2;
color: #6b7280;
}
.ips {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.ip {
display: inline-flex;
align-items: center;
padding: 6px 10px;
border-radius: 999px;
background: #f3f5f5;
color: #374151;
font-size: 12px;
}
.muted {
color: var(--muted);
}
.empty {
margin: 10px;
padding: 16px 18px;
border-radius: 18px;
border: 1px dashed var(--line);
color: var(--muted);
background: #f9fbfb;
font-size: 14px;
line-height: 1.7;
}
.footer {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 14px 4px 2px;
color: var(--muted);
font-size: 14px;
}
.pager {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.pager .btn[aria-disabled="true"] {
opacity: 0.45;
pointer-events: none;
}
@media (max-width: 1080px) {
.summary {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 720px) {
.summary {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="wrap">
<section class="hero">
<div class="eyebrow">Xboard Admin Plugin</div>
<h1>All Users Online IP Monitor</h1>
<div class="subtitle">
This plugin page is rendered on the server side for administrators. It reads live device state from Xboard nodes and shows each user's current online IP count and active IP list without relying on browser-stored admin tokens.
</div>
</section>
<section class="summary">
<article class="card stat">
<div class="stat-label">Users On Current Page</div>
<div class="stat-value">{{ $summary['page_users'] }}</div>
<div class="stat-note">Rows currently displayed after filtering</div>
</article>
<article class="card stat">
<div class="stat-label">Users With Online IP</div>
<div class="stat-value">{{ $summary['users_with_online_ip'] }}</div>
<div class="stat-note">Users on this page whose live IP count is greater than zero</div>
</article>
<article class="card stat">
<div class="stat-label">Total Online IP Count</div>
<div class="stat-value">{{ $summary['total_online_ips'] }}</div>
<div class="stat-note">Sum of all active IPs on the current page</div>
</article>
<article class="card stat">
<div class="stat-label">Current Page</div>
<div class="stat-value">{{ $summary['current_page'] }}</div>
<div class="stat-note">Page {{ $users->currentPage() }} of {{ $users->lastPage() }}</div>
</article>
</section>
<form class="toolbar" method="GET" action="">
<input class="input" name="keyword" type="text" value="{{ $filters['keyword'] }}" placeholder="Search by user ID or email">
<select class="select" name="per_page">
@foreach ([20, 50, 100] as $size)
<option value="{{ $size }}" @selected((int) $filters['per_page'] === $size)>{{ $size }} / page</option>
@endforeach
</select>
<button class="btn primary" type="submit">Search</button>
<a class="btn" href="{{ request()->url() }}">Reset</a>
<a class="btn" href="{{ $adminHomeUrl }}">Back Admin</a>
</form>
<section class="card panel">
<div class="table-wrap">
<table>
<thead>
<tr>
<th>ID</th>
<th>Email</th>
<th>Subscription</th>
<th>Online IP Count</th>
<th>Online IP List</th>
<th>Last Online</th>
<th>Created</th>
</tr>
</thead>
<tbody>
@forelse ($users as $user)
<tr>
<td>{{ $user->id }}</td>
<td>{{ $user->email }}</td>
<td>{{ $user->subscription_name }}</td>
<td>
<span class="count {{ ($user->online_count_live ?? 0) > 0 ? '' : 'zero' }}">
{{ $user->online_count_live ?? 0 }}
</span>
</td>
<td>
@if (!empty($user->online_devices))
<div class="ips">
@foreach ($user->online_devices as $ip)
<span class="ip">{{ $ip }}</span>
@endforeach
</div>
@else
<span class="muted">No active IP</span>
@endif
</td>
<td>{{ $user->last_online_text }}</td>
<td>{{ $user->created_text }}</td>
</tr>
@empty
<tr>
<td colspan="7">
<div class="empty">No users found for the current filter.</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="footer">
<div>Total {{ $users->total() }} users, showing page {{ $users->currentPage() }} / {{ $users->lastPage() }}</div>
<div class="pager">
@php
$prevUrl = $users->previousPageUrl();
$nextUrl = $users->nextPageUrl();
@endphp
<a class="btn" href="{{ $prevUrl ?: '#' }}" aria-disabled="{{ $prevUrl ? 'false' : 'true' }}">Prev</a>
<a class="btn" href="{{ $nextUrl ?: '#' }}" aria-disabled="{{ $nextUrl ? 'false' : 'true' }}">Next</a>
</div>
</div>
</section>
</div>
</body>
</html>