| <!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> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|