totp_view.html 3.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
  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. <meta name="color-scheme" content="light">
  7. <title>FWH 验证码</title>
  8. <style>
  9. :root { font-family: system-ui, -apple-system, "Segoe UI", sans-serif; color: #25313b; background: #f6f8fa; }
  10. body { margin: 0; min-height: 100vh; display: grid; place-items: center; }
  11. main { box-sizing: border-box; width: min(360px, calc(100% - 32px)); padding: 32px; background: white; border: 1px solid #e5e9ed; border-radius: 14px; box-shadow: 0 8px 30px #1d34420a; text-align: center; }
  12. h1 { margin: 0 0 24px; font-size: 18px; font-weight: 600; }
  13. #code { min-height: 58px; font-size: clamp(36px, 11vw, 48px); font-weight: 650; letter-spacing: .12em; font-variant-numeric: tabular-nums; white-space: nowrap; }
  14. #status { margin: 18px 0 0; color: #687783; font-size: 14px; }
  15. .track { height: 4px; margin-top: 18px; background: #e9eef1; border-radius: 4px; overflow: hidden; }
  16. #progress { height: 100%; width: 0; background: #568baa; transition: width .2s linear; }
  17. </style>
  18. </head>
  19. <body>
  20. <main>
  21. <h1>FWH 验证码</h1>
  22. <div id="code" aria-live="polite">··· ···</div>
  23. <p id="status">正在获取验证码…</p>
  24. <div class="track" aria-hidden="true"><div id="progress"></div></div>
  25. </main>
  26. <script>
  27. const codeElement = document.getElementById('code');
  28. const statusElement = document.getElementById('status');
  29. const progressElement = document.getElementById('progress');
  30. let expiresAt = 0;
  31. let clockOffset = 0;
  32. let loading = false;
  33. let lastFetch = 0;
  34. async function refreshCode() {
  35. if (loading) return;
  36. loading = true;
  37. lastFetch = Date.now();
  38. try {
  39. const codePath = location.pathname.replace(/\/?$/, '/') + 'code';
  40. const response = await fetch(codePath, { cache: 'no-store', credentials: 'same-origin' });
  41. if (!response.ok) throw new Error('request failed');
  42. const data = await response.json();
  43. if (!/^\d{6}$/.test(data.code)) throw new Error('invalid code');
  44. clockOffset = data.serverTime - Date.now();
  45. expiresAt = data.expiresAt;
  46. codeElement.textContent = data.code.slice(0, 3) + ' ' + data.code.slice(3);
  47. updateCountdown();
  48. } catch (_) {
  49. expiresAt = 0;
  50. codeElement.textContent = '··· ···';
  51. statusElement.textContent = '获取失败,正在重试…';
  52. progressElement.style.width = '0%';
  53. } finally {
  54. loading = false;
  55. }
  56. }
  57. function updateCountdown() {
  58. if (!expiresAt) return;
  59. const remaining = Math.max(0, expiresAt - (Date.now() + clockOffset));
  60. statusElement.textContent = remaining > 0 ? Math.ceil(remaining / 1000) + ' 秒后更新' : '正在更新…';
  61. progressElement.style.width = (remaining / 30000 * 100) + '%';
  62. if ((remaining === 0 || Date.now() - lastFetch >= 5000) && !loading) refreshCode();
  63. }
  64. refreshCode();
  65. setInterval(() => {
  66. if (expiresAt) updateCountdown();
  67. else if (!loading && Date.now() - lastFetch >= 3000) refreshCode();
  68. }, 250);
  69. </script>
  70. </body>
  71. </html>