cam / app /templates /admin_admins.html
cacode's picture
Upload 67 files
cf289c1 verified
{% extends 'base.html' %}
{% block content %}
<section class="page-grid admin-page-grid presence-admin-grid">
<article class="glass-card form-panel">
<div class="section-head">
<div>
<p class="eyebrow">Create Admin</p>
<h3>新增管理员</h3>
</div>
</div>
<form method="post" action="/admin/admins" class="form-stack">
<div class="form-grid cols-2">
<label>
<span>登录账号</span>
<input type="text" name="username" required />
</label>
<label>
<span>显示名称</span>
<input type="text" name="display_name" required />
</label>
<label>
<span>登录密码</span>
<input type="text" name="password" required />
</label>
<label>
<span>角色</span>
<select name="role">
<option value="admin">管理员</option>
<option value="superadmin">超级管理员</option>
</select>
</label>
</div>
<button class="btn btn-primary" type="submit">创建管理员</button>
</form>
</article>
<article class="glass-card table-panel">
<div class="section-head">
<div>
<p class="eyebrow">Admins</p>
<h3>管理员列表</h3>
</div>
</div>
<div class="stack-list">
{% for admin_item in admins %}
<div class="stack-item">
<div>
<strong>{{ admin_item.display_name }}</strong>
<p class="muted">{{ admin_item.username }} · 创建于 {{ admin_item.created_at|datetime_local }}</p>
</div>
<span class="status-badge {% if admin_item.role == 'superadmin' %}status-approved{% endif %}">
{{ '超级管理员' if admin_item.role == 'superadmin' else '管理员' }}
</span>
</div>
{% else %}
<p class="muted">暂无管理员记录。</p>
{% endfor %}
</div>
</article>
</section>
<section class="page-grid admin-page-grid presence-admin-grid">
<article class="glass-card table-panel">
<div class="section-head">
<div>
<p class="eyebrow">Presence</p>
<h3>管理员在线状态</h3>
<p class="mini-note">在线心跳按 5 秒同步,连续 15 秒未收到心跳时判定为离线,状态文本按秒自动更新。</p>
</div>
</div>
<div class="stack-list" id="admin-presence-list">
{% for item in admin_statuses %}
<div class="stack-item presence-item">
<div>
<strong>{{ item.name }}</strong>
<p class="muted">{{ item.username }} · {{ item.role_label }}</p>
</div>
<span
class="status-badge {% if item.is_online %}status-approved{% else %}status-rejected{% endif %}"
data-presence-badge
data-last-seen-ts="{{ item.last_seen_ts or '' }}"
title="最近心跳 {{ item.last_seen_at }}"
>
{{ '在线' if item.is_online else '离线' }} · {{ item.last_seen_at }}
</span>
</div>
{% else %}
<p class="muted">暂无管理员在线状态。</p>
{% endfor %}
</div>
</article>
<article class="glass-card form-panel">
<div class="section-head">
<div>
<p class="eyebrow">Notice</p>
<h3>在线检测说明</h3>
</div>
</div>
<div class="info-box">
<div class="stack-item stack-item-block">
<strong>普通用户在线心跳已关闭</strong>
<p class="muted">为降低数据库连接占用与轮询压力,系统不再维护普通用户的实时在线状态,只保留管理员在线检测与审核分发所需的心跳。</p>
</div>
<div class="stack-item stack-item-block">
<strong>当前策略</strong>
<p class="muted">管理员登录时会立即标记在线,之后仅由管理员端每 5 秒心跳续期;页面隐藏或离线时会自动暂停心跳,减少无效请求。</p>
</div>
</div>
</article>
</section>
<script>
(() => {
const adminList = document.getElementById('admin-presence-list');
if (!adminList) return;
let serverTs = Number('{{ presence_server_ts or 0 }}') || Math.floor(Date.now() / 1000);
let syncClientTs = Date.now();
let onlineWindowSeconds = Number('{{ online_window_seconds or 15 }}') || 15;
let snapshotInFlight = false;
const escapeHtml = (value) => String(value ?? '').replace(/[&<>"']/g, (char) => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[char]));
const estimatedNowTs = () => serverTs + Math.floor((Date.now() - syncClientTs) / 1000);
const formatElapsed = (seconds) => {
if (seconds <= 1) return '刚刚';
if (seconds < 60) return `${seconds}秒前`;
if (seconds < 3600) return `${Math.floor(seconds / 60)}分钟前`;
if (seconds < 86400) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return minutes ? `${hours}小时${minutes}分钟前` : `${hours}小时前`;
}
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
return hours ? `${days}${hours}小时前` : `${days}天前`;
};
const getStatusMeta = (lastSeenTs) => {
if (!lastSeenTs) {
return {
online: false,
text: '离线 · 暂无心跳',
};
}
const elapsed = Math.max(0, estimatedNowTs() - lastSeenTs);
const online = elapsed <= onlineWindowSeconds;
return {
online,
text: `${online ? '在线' : '离线'} · ${formatElapsed(elapsed)}`,
};
};
const renderAdminItems = (items) => {
if (!items.length) {
adminList.innerHTML = '<p class="muted">暂无管理员在线状态。</p>';
return;
}
adminList.innerHTML = items.map((item) => {
const status = getStatusMeta(Number(item.last_seen_ts || 0));
return `
<div class="stack-item presence-item">
<div>
<strong>${escapeHtml(item.name)}</strong>
<p class="muted">${escapeHtml(item.username)} · ${escapeHtml(item.role_label)}</p>
</div>
<span
class="status-badge ${status.online ? 'status-approved' : 'status-rejected'}"
data-presence-badge
data-last-seen-ts="${item.last_seen_ts || ''}"
title="最近心跳 ${escapeHtml(item.last_seen_at || '-') }"
>
${status.text}
</span>
</div>`;
}).join('');
};
const refreshPresenceBadges = () => {
if (document.hidden) return;
document.querySelectorAll('[data-presence-badge]').forEach((badge) => {
const lastSeenTs = Number(badge.dataset.lastSeenTs || 0);
const status = getStatusMeta(lastSeenTs);
badge.textContent = status.text;
badge.classList.toggle('status-approved', status.online);
badge.classList.toggle('status-rejected', !status.online);
});
};
const refreshPresence = async () => {
if (document.hidden || snapshotInFlight) return;
snapshotInFlight = true;
try {
const response = await fetch('/api/admin/presence/overview', {
credentials: 'same-origin',
cache: 'no-store',
headers: { 'X-Requested-With': 'fetch' },
});
if (!response.ok) return;
const payload = await response.json();
serverTs = Number(payload.server_ts || 0) || Math.floor(Date.now() / 1000);
syncClientTs = Date.now();
onlineWindowSeconds = Number(payload.online_window_seconds || 0) || onlineWindowSeconds;
renderAdminItems(payload.admins || []);
refreshPresenceBadges();
} catch (error) {
console.debug('presence refresh skipped', error);
} finally {
snapshotInFlight = false;
}
};
refreshPresenceBadges();
refreshPresence();
window.setInterval(refreshPresenceBadges, 1000);
window.setInterval(refreshPresence, 5000);
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
refreshPresence();
}
});
window.addEventListener('focus', refreshPresence);
})();
</script>
{% endblock %}