login.html 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  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">N</div>
  70. <div>
  71. <div class="fw-bold lh-1">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 r = await fetch("/api/auth/me", { credentials: "same-origin" });
  98. const j = await r.json();
  99. if (j && j.code === 200) {
  100. location.replace("/");
  101. return;
  102. }
  103. } catch (_) {}
  104. const form = document.getElementById("login-form");
  105. const err = document.getElementById("login-err");
  106. const btn = document.getElementById("login-btn");
  107. const captchaImg = document.getElementById("login-captcha-img");
  108. const captchaInput = document.getElementById("login-captcha");
  109. function refreshCaptcha() {
  110. captchaImg.src = "/api/auth/captcha?t=" + Date.now();
  111. captchaInput.value = "";
  112. captchaInput.focus();
  113. }
  114. captchaImg.addEventListener("click", refreshCaptcha);
  115. form.addEventListener("submit", async (e) => {
  116. e.preventDefault();
  117. err.classList.remove("is-on");
  118. btn.disabled = true;
  119. try {
  120. const res = await fetch("/api/auth/login", {
  121. method: "POST",
  122. credentials: "same-origin",
  123. headers: { "Content-Type": "application/json", Accept: "application/json" },
  124. body: JSON.stringify({
  125. username: document.getElementById("login-user").value.trim(),
  126. password: document.getElementById("login-pass").value,
  127. captcha: captchaInput.value.trim(),
  128. }),
  129. });
  130. const json = await res.json();
  131. if (!json || json.code !== 200) {
  132. throw new Error((json && json.msg) || "登录失败");
  133. }
  134. location.replace("/");
  135. } catch (ex) {
  136. err.textContent = ex.message || String(ex);
  137. err.classList.add("is-on");
  138. refreshCaptcha();
  139. } finally {
  140. btn.disabled = false;
  141. }
  142. });
  143. })();
  144. </script>
  145. </body>
  146. </html>