FPSMS-frontend
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

89 lines
2.4 KiB

  1. "use client";
  2. import { useEffect, useMemo, useState } from "react";
  3. const STORAGE_KEY = "fpsms_server_wait_until_ms";
  4. const WAIT_SECONDS = 30;
  5. const RELOAD_INTERVAL_SECONDS = 5;
  6. function formatSeconds(s: number) {
  7. const v = Math.max(0, Math.floor(s));
  8. return `${v} 秒`;
  9. }
  10. /**
  11. * Catches root-level errors (e.g. backend down during deploy).
  12. * Shows a reconnect countdown and retries, then forwards to login if still failing.
  13. * Must define <html> and <body> because this replaces the root layout.
  14. */
  15. export default function GlobalError({
  16. error,
  17. reset,
  18. }: {
  19. error: Error & { digest?: string };
  20. reset: () => void;
  21. }) {
  22. const [remaining, setRemaining] = useState(WAIT_SECONDS);
  23. const waitUntilMs = useMemo(() => {
  24. const existing = Number(window.localStorage.getItem(STORAGE_KEY) || "0");
  25. const now = Date.now();
  26. if (existing && existing > now) return existing;
  27. const next = now + WAIT_SECONDS * 1000;
  28. window.localStorage.setItem(STORAGE_KEY, String(next));
  29. return next;
  30. }, []);
  31. useEffect(() => {
  32. const start = Date.now();
  33. const tick = () => {
  34. const now = Date.now();
  35. const msLeft = Math.max(0, waitUntilMs - now);
  36. setRemaining(msLeft / 1000);
  37. if (msLeft <= 0) {
  38. window.localStorage.removeItem(STORAGE_KEY);
  39. window.location.href = "/login";
  40. }
  41. };
  42. tick();
  43. const interval = window.setInterval(tick, 250);
  44. const reloadTimer = window.setInterval(() => {
  45. const elapsedSec = (Date.now() - start) / 1000;
  46. if (elapsedSec >= WAIT_SECONDS) return;
  47. window.location.reload();
  48. }, RELOAD_INTERVAL_SECONDS * 1000);
  49. return () => {
  50. window.clearInterval(interval);
  51. window.clearInterval(reloadTimer);
  52. };
  53. }, [waitUntilMs]);
  54. return (
  55. <html lang="zh-TW">
  56. <body>
  57. <div
  58. style={{
  59. display: "flex",
  60. minHeight: "100vh",
  61. alignItems: "center",
  62. justifyContent: "center",
  63. fontFamily: "system-ui, sans-serif",
  64. padding: "1rem",
  65. }}
  66. >
  67. <p style={{ color: "#334155", fontSize: "0.95rem", fontWeight: 700 }}>
  68. 連線異常,伺服器暫停中
  69. </p>
  70. <p style={{ color: "#64748b", fontSize: "0.875rem", marginTop: 8 }}>
  71. 系統會在後台恢復後自動重試。倒數中:{formatSeconds(remaining)}
  72. </p>
  73. </div>
  74. </body>
  75. </html>
  76. );
  77. }