File size: 3,658 Bytes
ce0719e 4312f62 ce0719e 02a8414 ce0719e 3d6b7f2 ce0719e 02a8414 ce0719e 02a8414 cf289c1 02a8414 3d6b7f2 02a8414 3d6b7f2 02a8414 ce0719e 3d6b7f2 4312f62 cf289c1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | <!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ page_title or app_name }} · {{ app_name }}</title>
<link rel="icon" href="/static/favicon.ico?v={{ static_asset_version('favicon.ico') }}" sizes="any" />
<link rel="stylesheet" href="/static/style.css?v={{ static_asset_version('style.css') }}" />
</head>
<body class="{% if admin %}admin-mode{% else %}user-mode{% endif %}">
<div class="page-backdrop"></div>
<div class="shell">
<header class="topbar">
<div class="brand-block">
<div class="brand-mark">春</div>
<div>
<p class="eyebrow">Spring Check-In</p>
<h1 class="brand-title">{{ app_name }}</h1>
</div>
</div>
<nav class="topnav">
{% if user %}
<a href="/dashboard">活动广场</a>
<a href="/account">账号中心</a>
<a href="/logout">退出登录</a>
{% elif admin %}
<a href="/admin/dashboard">总览</a>
<a href="/admin/users">用户</a>
<a href="/admin/groups">小组</a>
<a href="/admin/activities">活动</a>
<a href="/admin/images">图片</a>
<a href="/admin/reviews">审核</a>
<a href="/account">账号中心</a>
{% if admin.role == 'superadmin' %}
<a href="/admin/admins">管理员</a>
{% endif %}
<a href="/admin/logout">退出</a>
{% endif %}
</nav>
</header>
{% include 'partials/flash.html' %}
<main class="content-shell">
{% block content %}{% endblock %}
</main>
</div>
{% if admin %}
<script>
(() => {
const PRESENCE_INTERVAL_MS = 5000;
const INITIAL_JITTER_MS = 1200;
let timerId = null;
let inFlight = false;
const shouldPing = () => !document.hidden && navigator.onLine;
const schedule = (delay = PRESENCE_INTERVAL_MS) => {
if (timerId) {
window.clearTimeout(timerId);
}
timerId = window.setTimeout(runPing, delay);
};
const runPing = async () => {
if (!shouldPing()) {
timerId = null;
return;
}
if (inFlight) {
schedule(1000);
return;
}
inFlight = true;
try {
await fetch('/api/presence/ping', {
method: 'POST',
keepalive: true,
credentials: 'same-origin',
cache: 'no-store',
headers: { 'X-Requested-With': 'fetch' },
});
} catch (error) {
console.debug('presence ping skipped', error);
} finally {
inFlight = false;
schedule();
}
};
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
if (timerId) {
window.clearTimeout(timerId);
timerId = null;
}
return;
}
runPing();
});
window.addEventListener('focus', () => {
if (!document.hidden) {
runPing();
}
});
window.addEventListener('online', runPing);
schedule(500 + Math.floor(Math.random() * INITIAL_JITTER_MS));
})();
</script>
{% endif %}
</body>
</html>
|