103 lines
5.0 KiB
HTML
103 lines
5.0 KiB
HTML
<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta name="color-scheme" content="light">
|
|
<title>登录 Orbit · 星轨控制台</title>
|
|
<link rel="icon" href="/console/favicon.ico" sizes="16x16 32x32 48x48">
|
|
<link rel="icon" type="image/svg+xml" href="/console/favicon.svg" sizes="any">
|
|
<link rel="stylesheet" href="/console/styles.css">
|
|
</head>
|
|
<body class="login-body">
|
|
<main class="login-page">
|
|
<section class="login-brand" aria-label="Orbit 产品介绍">
|
|
<div class="brand">
|
|
<div class="brand-mark" aria-hidden="true">
|
|
<span>
|
|
</span>
|
|
<span>
|
|
</span>
|
|
<span>
|
|
</span>
|
|
</div>
|
|
<strong>Orbit<span class="brand-separator"> / </span>
|
|
<small>星轨控制台</small>
|
|
</strong>
|
|
</div>
|
|
<div class="login-intro">
|
|
<span class="page-eyebrow">YOUR OPERATIONS, IN ORBIT</span>
|
|
<h1>运行有序,<br>一目了然。</h1>
|
|
<p>任务、调度、流程与结果交付,<br>在一个工作台里管理。</p>
|
|
<div class="login-flow" aria-hidden="true">
|
|
<span>任务</span>
|
|
<i>→</i>
|
|
<span>执行</span>
|
|
<i>→</i>
|
|
<span>结果</span>
|
|
</div>
|
|
</div>
|
|
<small>Pulse · Weave</small>
|
|
</section>
|
|
<section class="login-panel" aria-label="账号登录">
|
|
<div class="login-card">
|
|
<h2>登录控制台</h2>
|
|
<p class="login-copy">欢迎回来,使用管理员账号登录。</p>
|
|
<form class="login-form" id="login-form" autocomplete="on">
|
|
<label class="login-field">
|
|
<span>用户名</span>
|
|
<input id="username" name="username" autocomplete="username" maxlength="128" required autofocus placeholder="请输入用户名">
|
|
</label>
|
|
<label class="login-field">
|
|
<span>密码</span>
|
|
<input id="password" name="password" type="password" autocomplete="current-password" maxlength="1024" required placeholder="请输入密码">
|
|
</label>
|
|
<label class="password-toggle">
|
|
<input type="checkbox" id="show-password">显示密码</label>
|
|
<p class="login-error" id="login-error" role="alert">
|
|
</p>
|
|
<button class="primary-button login-submit" id="login-submit" type="submit">登录 Orbit →</button>
|
|
</form>
|
|
<p class="login-help">忘记密码?请联系部署管理员重置。</p>
|
|
</div>
|
|
<small class="login-footer">Orbit Console · 统一运行管理</small>
|
|
</section>
|
|
</main>
|
|
<script>
|
|
(() => {
|
|
const form = document.getElementById('login-form'); const username = document.getElementById('username'); const password = document.getElementById('password'); const submit = document.getElementById('login-submit'); const error = document.getElementById('login-error');
|
|
document.getElementById('show-password').addEventListener('change', event => { password.type = event.target.checked ? 'text' : 'password'; });
|
|
const params = new URLSearchParams(location.search); let returnTo = params.get('return') || '/console/'; if (!returnTo.startsWith('/console') || returnTo.startsWith('//') || returnTo.startsWith('/console/login')) returnTo = '/console/';
|
|
// 初次访问时 URL 片段不会发给服务端,浏览器会把它保留在跳转后的登录地址上。
|
|
if (!returnTo.includes('#') && /^#\/(overview|tasks|schedules|workflows|subscriptions|retention|archives|applications)$/.test(location.hash)) returnTo += location.hash;
|
|
fetch('/console/auth/session', { headers: { Accept: 'application/json' }, credentials: 'same-origin', cache: 'no-store' }).then(response => response.json()).then(body => { if (body.authenticated) location.replace(returnTo); }).catch(() => {});
|
|
let retryUntil = 0, retryTimer;
|
|
function showRetry() {
|
|
const remaining = Math.max(0, Math.ceil((retryUntil - Date.now()) / 1000));
|
|
submit.disabled = remaining > 0;
|
|
submit.textContent = remaining ? `请等待 ${Math.floor(remaining / 60)}分 ${remaining % 60}秒` : '登录 Orbit →';
|
|
if (!remaining) clearInterval(retryTimer);
|
|
}
|
|
form.addEventListener('submit', async event => {
|
|
event.preventDefault(); if (Date.now() < retryUntil) return;
|
|
error.textContent = ''; submit.disabled = true; submit.textContent = '正在登录…';
|
|
try {
|
|
const response = await fetch('/console/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, credentials: 'same-origin', body: JSON.stringify({ username: username.value.trim(), password: password.value }) });
|
|
const body = await response.json().catch(() => ({}));
|
|
if (!response.ok) {
|
|
const seconds = Number(body?.error?.retry_after_seconds);
|
|
if (response.status === 429 && Number.isFinite(seconds) && seconds > 0) {
|
|
retryUntil = Date.now() + seconds * 1000;
|
|
clearInterval(retryTimer); retryTimer = setInterval(showRetry, 1000);
|
|
}
|
|
throw new Error(body?.error?.message || '登录失败,请稍后重试');
|
|
}
|
|
location.replace(returnTo);
|
|
} catch (loginError) { error.textContent = loginError.message || '登录失败,请稍后重试'; password.select(); }
|
|
finally { showRetry(); }
|
|
});
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|