| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1" />
- <title>NGS · 登录</title>
- <link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css" />
- <link rel="stylesheet" href="/assets/bootstrap-icons/bootstrap-icons.css" />
- <link rel="stylesheet" href="/assets/app.css" />
- <style>
- .login-page {
- min-height: 100vh;
- display: grid;
- place-items: center;
- padding: 1.5rem;
- background:
- radial-gradient(1200px 600px at 10% -10%, rgba(13,148,136,.22), transparent 55%),
- radial-gradient(900px 500px at 100% 0%, rgba(30,41,59,.18), transparent 50%),
- #f1f5f9;
- }
- .login-card {
- width: min(400px, 100%);
- background: #fff;
- border: 1px solid #e2e8f0;
- border-radius: 14px;
- padding: 1.5rem 1.4rem 1.25rem;
- box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08);
- }
- .login-brand {
- display: flex;
- align-items: center;
- gap: 0.75rem;
- margin-bottom: 1.15rem;
- }
- .login-err {
- display: none;
- font-size: 0.85rem;
- color: #b91c1c;
- background: #fef2f2;
- border: 1px solid #fecaca;
- border-radius: 8px;
- padding: 0.5rem 0.7rem;
- margin-bottom: 0.85rem;
- }
- .login-err.is-on { display: block; }
- .login-captcha {
- display: flex;
- gap: 0.6rem;
- align-items: stretch;
- }
- .login-captcha input { flex: 1; min-width: 0; }
- .login-captcha-img {
- width: 120px;
- height: 40px;
- border: 1px solid #e2e8f0;
- border-radius: 8px;
- background: #f8fafc;
- cursor: pointer;
- flex-shrink: 0;
- object-fit: fill;
- }
- .login-captcha-img:hover { border-color: #94a3b8; }
- </style>
- </head>
- <body>
- <div class="login-page">
- <form class="login-card" id="login-form" autocomplete="on">
- <div class="login-brand">
- <div class="brand-mark" id="login-brand-mark">N</div>
- <div>
- <div class="fw-bold lh-1" id="login-brand-name">NGS</div>
- <div class="small text-secondary">管理面板登录</div>
- </div>
- </div>
- <div class="login-err" id="login-err"></div>
- <div class="mb-3">
- <label class="form-label">账号</label>
- <input class="form-control" name="username" id="login-user" required autofocus placeholder="admin" />
- </div>
- <div class="mb-3">
- <label class="form-label">密码</label>
- <input class="form-control" type="password" name="password" id="login-pass" required placeholder="密码" />
- </div>
- <div class="mb-3">
- <label class="form-label">验证码</label>
- <div class="login-captcha">
- <input class="form-control" name="captcha" id="login-captcha" required maxlength="8" autocomplete="off" placeholder="验证码" />
- <img class="login-captcha-img" id="login-captcha-img" src="/api/auth/captcha" alt="验证码" title="点击刷新" />
- </div>
- </div>
- <button type="submit" class="btn btn-primary w-100" id="login-btn">登录</button>
- </form>
- </div>
- <script>
- (async () => {
- try {
- const ping = await fetch("/api/ping", { credentials: "same-origin" });
- const pj = await ping.json();
- const name = String((pj && pj.data && pj.data.panel_name) || "").trim() || "NGS";
- document.title = name + " · 登录";
- const mark = document.getElementById("login-brand-mark");
- const brand = document.getElementById("login-brand-name");
- if (mark) mark.textContent = Array.from(name)[0] || "N";
- if (brand) brand.textContent = name;
- } catch (_) {}
- try {
- const r = await fetch("/api/auth/me", { credentials: "same-origin" });
- const j = await r.json();
- if (j && j.code === 200) {
- location.replace("/");
- return;
- }
- } catch (_) {}
- const form = document.getElementById("login-form");
- const err = document.getElementById("login-err");
- const btn = document.getElementById("login-btn");
- const captchaImg = document.getElementById("login-captcha-img");
- const captchaInput = document.getElementById("login-captcha");
- function refreshCaptcha() {
- captchaImg.src = "/api/auth/captcha?t=" + Date.now();
- captchaInput.value = "";
- captchaInput.focus();
- }
- captchaImg.addEventListener("click", refreshCaptcha);
- form.addEventListener("submit", async (e) => {
- e.preventDefault();
- err.classList.remove("is-on");
- btn.disabled = true;
- try {
- const res = await fetch("/api/auth/login", {
- method: "POST",
- credentials: "same-origin",
- headers: { "Content-Type": "application/json", Accept: "application/json" },
- body: JSON.stringify({
- username: document.getElementById("login-user").value.trim(),
- password: document.getElementById("login-pass").value,
- captcha: captchaInput.value.trim(),
- }),
- });
- const json = await res.json();
- if (!json || json.code !== 200) {
- throw new Error((json && json.msg) || "登录失败");
- }
- location.replace("/");
- } catch (ex) {
- err.textContent = ex.message || String(ex);
- err.classList.add("is-on");
- refreshCaptcha();
- } finally {
- btn.disabled = false;
- }
- });
- })();
- </script>
- </body>
- </html>
|