login.html 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1" />
  6. <title>NGS · 登录</title>
  7. <link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css" />
  8. <link rel="stylesheet" href="/assets/bootstrap-icons/bootstrap-icons.css" />
  9. <link rel="stylesheet" href="/assets/app.css" />
  10. <style>
  11. .login-page {
  12. min-height: 100vh;
  13. display: grid;
  14. place-items: center;
  15. padding: 1.5rem;
  16. background:
  17. radial-gradient(1200px 600px at 10% -10%, rgba(13,148,136,.22), transparent 55%),
  18. radial-gradient(900px 500px at 100% 0%, rgba(30,41,59,.18), transparent 50%),
  19. #f1f5f9;
  20. }
  21. .login-card {
  22. width: min(400px, 100%);
  23. background: #fff;
  24. border: 1px solid #e2e8f0;
  25. border-radius: 14px;
  26. padding: 1.5rem 1.4rem 1.25rem;
  27. box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08);
  28. }
  29. .login-brand {
  30. display: flex;
  31. align-items: center;
  32. gap: 0.75rem;
  33. margin-bottom: 1.15rem;
  34. }
  35. .login-err {
  36. display: none;
  37. font-size: 0.85rem;
  38. color: #b91c1c;
  39. background: #fef2f2;
  40. border: 1px solid #fecaca;
  41. border-radius: 8px;
  42. padding: 0.5rem 0.7rem;
  43. margin-bottom: 0.85rem;
  44. }
  45. .login-err.is-on { display: block; }
  46. .login-captcha {
  47. display: flex;
  48. gap: 0.6rem;
  49. align-items: stretch;
  50. }
  51. .login-captcha input { flex: 1; min-width: 0; }
  52. .login-captcha-img {
  53. width: 120px;
  54. height: 40px;
  55. border: 1px solid #e2e8f0;
  56. border-radius: 8px;
  57. background: #f8fafc;
  58. cursor: pointer;
  59. flex-shrink: 0;
  60. object-fit: fill;
  61. }
  62. .login-captcha-img:hover { border-color: #94a3b8; }
  63. </style>
  64. </head>
  65. <body>
  66. <div class="login-page">
  67. <form class="login-card" id="login-form" autocomplete="on">
  68. <div class="login-brand">
  69. <div class="brand-mark" id="login-brand-mark">N</div>
  70. <div>
  71. <div class="fw-bold lh-1" id="login-brand-name">NGS</div>
  72. <div class="small text-secondary">管理面板登录</div>
  73. </div>
  74. </div>
  75. <div class="login-err" id="login-err"></div>
  76. <div class="mb-3">
  77. <label class="form-label">账号</label>
  78. <input class="form-control" name="username" id="login-user" required autofocus placeholder="admin" />
  79. </div>
  80. <div class="mb-3">
  81. <label class="form-label">密码</label>
  82. <input class="form-control" type="password" name="password" id="login-pass" required placeholder="密码" />
  83. </div>
  84. <div class="mb-3">
  85. <label class="form-label">验证码</label>
  86. <div class="login-captcha">
  87. <input class="form-control" name="captcha" id="login-captcha" required maxlength="8" autocomplete="off" placeholder="验证码" />
  88. <img class="login-captcha-img" id="login-captcha-img" src="/api/auth/captcha" alt="验证码" title="点击刷新" />
  89. </div>
  90. </div>
  91. <button type="submit" class="btn btn-primary w-100" id="login-btn">登录</button>
  92. </form>
  93. </div>
  94. <script>
  95. (async () => {
  96. try {
  97. const ping = await fetch("/api/ping", { credentials: "same-origin" });
  98. const pj = await ping.json();
  99. const name = String((pj && pj.data && pj.data.panel_name) || "").trim() || "NGS";
  100. document.title = name + " · 登录";
  101. const mark = document.getElementById("login-brand-mark");
  102. const brand = document.getElementById("login-brand-name");
  103. if (mark) mark.textContent = Array.from(name)[0] || "N";
  104. if (brand) brand.textContent = name;
  105. } catch (_) {}
  106. try {
  107. const r = await fetch("/api/auth/me", { credentials: "same-origin" });
  108. const j = await r.json();
  109. if (j && j.code === 200) {
  110. location.replace("/");
  111. return;
  112. }
  113. } catch (_) {}
  114. const form = document.getElementById("login-form");
  115. const err = document.getElementById("login-err");
  116. const btn = document.getElementById("login-btn");
  117. const captchaImg = document.getElementById("login-captcha-img");
  118. const captchaInput = document.getElementById("login-captcha");
  119. function refreshCaptcha() {
  120. captchaImg.src = "/api/auth/captcha?t=" + Date.now();
  121. captchaInput.value = "";
  122. captchaInput.focus();
  123. }
  124. captchaImg.addEventListener("click", refreshCaptcha);
  125. form.addEventListener("submit", async (e) => {
  126. e.preventDefault();
  127. err.classList.remove("is-on");
  128. btn.disabled = true;
  129. try {
  130. const res = await fetch("/api/auth/login", {
  131. method: "POST",
  132. credentials: "same-origin",
  133. headers: { "Content-Type": "application/json", Accept: "application/json" },
  134. body: JSON.stringify({
  135. username: document.getElementById("login-user").value.trim(),
  136. password: document.getElementById("login-pass").value,
  137. captcha: captchaInput.value.trim(),
  138. }),
  139. });
  140. const json = await res.json();
  141. if (!json || json.code !== 200) {
  142. throw new Error((json && json.msg) || "登录失败");
  143. }
  144. location.replace("/");
  145. } catch (ex) {
  146. err.textContent = ex.message || String(ex);
  147. err.classList.add("is-on");
  148. refreshCaptcha();
  149. } finally {
  150. btn.disabled = false;
  151. }
  152. });
  153. })();
  154. </script>
  155. </body>
  156. </html>