2026-01-05 12:02:14 +08:00

374 lines
8.6 KiB
HTML
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" />
<title>管理后台 - 学生列表</title>
<style>
:root {
color-scheme: light;
--bg: #ffffff;
--fg: #111827;
--muted: #6b7280;
--border: #e5e7eb;
--panel: #f9fafb;
--danger: #b91c1c;
--ok: #065f46;
--accent: #2563eb;
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji",
"Segoe UI Emoji";
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: var(--sans);
background: var(--bg);
color: var(--fg);
}
header {
position: sticky;
top: 0;
z-index: 10;
background: var(--bg);
border-bottom: 1px solid var(--border);
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 12px 16px;
}
.row {
display: flex;
gap: 12px;
flex-wrap: wrap;
align-items: center;
}
.spacer {
flex: 1;
}
input,
button {
font: inherit;
padding: 8px 10px;
border: 1px solid var(--border);
border-radius: 8px;
background: #fff;
color: var(--fg);
}
button {
cursor: pointer;
}
button.primary {
border-color: var(--accent);
background: var(--accent);
color: #fff;
}
button.ghost {
background: #fff;
}
.badge {
font-size: 12px;
padding: 3px 8px;
border-radius: 999px;
border: 1px solid var(--border);
background: var(--panel);
color: var(--muted);
}
.badge.ok {
color: var(--ok);
border-color: rgba(6, 95, 70, 0.25);
background: rgba(6, 95, 70, 0.06);
}
.badge.err {
color: var(--danger);
border-color: rgba(185, 28, 28, 0.25);
background: rgba(185, 28, 28, 0.06);
}
main {
max-width: 1200px;
margin: 0 auto;
padding: 16px;
}
.panel {
border: 1px solid var(--border);
border-radius: 12px;
background: var(--panel);
}
.panel .panel-hd {
padding: 12px 12px 8px;
border-bottom: 1px solid var(--border);
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
.panel .panel-bd {
padding: 12px;
}
h2 {
font-size: 14px;
margin: 0;
color: var(--muted);
letter-spacing: 0.02em;
text-transform: uppercase;
}
table {
width: 100%;
border-collapse: collapse;
background: #fff;
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
}
th,
td {
text-align: left;
padding: 10px 10px;
border-bottom: 1px solid var(--border);
vertical-align: top;
font-size: 13px;
}
th {
color: var(--muted);
font-weight: 600;
background: #fff;
}
tr:hover td {
background: rgba(37, 99, 235, 0.04);
}
.mono {
font-family: var(--mono);
font-size: 12px;
}
.small {
font-size: 12px;
color: var(--muted);
}
.error {
color: var(--danger);
}
</style>
</head>
<body>
<header>
<div class="container">
<div class="row">
<strong>声速测定 - 学生列表</strong>
<span id="status" class="badge">未连接</span>
<div class="spacer"></div>
<a href="/admin" class="small" style="text-decoration: none; color: var(--accent)">返回管理后台</a>
</div>
<div class="row" style="margin-top: 10px">
<label class="small">ADMIN_TOKEN</label>
<input id="token" style="min-width: 260px" placeholder="粘贴 ADMIN_TOKEN" />
<button id="saveToken" class="primary">保存</button>
<button id="clearToken" class="ghost">清除</button>
<button id="refresh" class="ghost">刷新</button>
</div>
</div>
</header>
<main>
<section class="panel">
<div class="panel-hd">
<h2>做过实验的学生</h2>
<div class="spacer"></div>
<label class="small">From</label>
<input id="from" type="datetime-local" />
<label class="small">To</label>
<input id="to" type="datetime-local" />
</div>
<div class="panel-bd">
<div class="row">
<input id="studentId" placeholder="studentId模糊" style="min-width: 240px" />
<button id="apply" class="primary">查询</button>
<button id="reset" class="ghost">重置</button>
<span class="small" id="summary"></span>
</div>
<div id="error" class="small error" style="margin: 10px 0"></div>
<div style="overflow: auto">
<table>
<thead>
<tr>
<th>studentId</th>
<th>count</th>
<th>lastAt</th>
</tr>
</thead>
<tbody id="tbody"></tbody>
</table>
</div>
</div>
</section>
</main>
<script type="module">
const $ = (id) => document.getElementById(id);
function setStatus(ok, text) {
const el = $('status');
el.textContent = text;
el.className = 'badge ' + (ok ? 'ok' : 'err');
}
function getToken() {
return (localStorage.getItem('ADMIN_TOKEN') || '').trim();
}
function setToken(v) {
localStorage.setItem('ADMIN_TOKEN', (v || '').trim());
}
async function api(path, params = undefined) {
const token = getToken();
if (!token) throw new Error('未设置 ADMIN_TOKEN');
let url = path;
if (params) {
const u = new URL(location.origin + path);
for (const [k, v] of Object.entries(params)) {
if (v === undefined || v === null || v === '') continue;
u.searchParams.set(k, String(v));
}
url = u.pathname + u.search;
}
const res = await fetch(url, {
headers: {
Authorization: 'Bearer ' + token,
},
});
const json = await res.json().catch(() => ({}));
if (!res.ok || !json.ok) {
throw new Error(json?.error || '请求失败:' + res.status);
}
return json;
}
function toISOFromLocal(value) {
if (!value) return '';
const d = new Date(value);
if (Number.isNaN(d.getTime())) return '';
return d.toISOString();
}
function setError(msg) {
$('error').textContent = msg || '';
}
function renderRows(rows) {
const tbody = $('tbody');
tbody.innerHTML = '';
for (const r of rows) {
const tr = document.createElement('tr');
const cols = [
r.studentId || '',
String(r.count ?? ''),
r.lastAt ? new Date(r.lastAt).toLocaleString() : '',
];
for (let i = 0; i < cols.length; i++) {
const td = document.createElement('td');
if (i === 0) td.className = 'mono';
td.textContent = cols[i];
tr.appendChild(td);
}
tbody.appendChild(tr);
}
}
async function loadList() {
setError('');
const from = toISOFromLocal($('from').value);
const to = toISOFromLocal($('to').value);
const params = {
from,
to,
studentId: $('studentId').value.trim(),
};
const data = await api('/api/admin/students', params);
$('summary').textContent = `${data.total} 名学生`;
renderRows(data.rows || []);
}
$('saveToken').addEventListener('click', async () => {
setToken($('token').value);
try {
await loadList();
setStatus(true, '已连接');
} catch (e) {
setStatus(false, '未连接');
setError(e?.message || String(e));
}
});
$('clearToken').addEventListener('click', () => {
setToken('');
$('token').value = '';
setStatus(false, '未连接');
setError('已清除 token');
$('summary').textContent = '';
$('tbody').innerHTML = '';
});
$('refresh').addEventListener('click', async () => {
try {
await loadList();
setStatus(true, '已连接');
} catch (e) {
setStatus(false, '未连接');
setError(e?.message || String(e));
}
});
$('apply').addEventListener('click', async () => {
try {
await loadList();
setStatus(true, '已连接');
} catch (e) {
setStatus(false, '未连接');
setError(e?.message || String(e));
}
});
$('reset').addEventListener('click', async () => {
$('studentId').value = '';
$('from').value = '';
$('to').value = '';
try {
await loadList();
setStatus(true, '已连接');
} catch (e) {
setStatus(false, '未连接');
setError(e?.message || String(e));
}
});
// init
$('token').value = getToken();
(async () => {
try {
if (getToken()) {
await loadList();
setStatus(true, '已连接');
} else {
setStatus(false, '未连接');
setError('请先在顶部设置 ADMIN_TOKEN');
}
} catch (e) {
setStatus(false, '未连接');
setError(e?.message || String(e));
}
})();
</script>
</body>
</html>