AdminLoginGate.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. /**
  2. * 管理后台登录门 - 对接真实 API(演示环境默认 localhost:8081)
  3. * 已登录(localStorage 持有 admin token)渲染管理界面;否则显示管理员登录表单
  4. */
  5. 'use client';
  6. import { useState, type ReactNode } from 'react';
  7. import { AdminShell } from '@/components/admin/AdminShell';
  8. /** 演示环境 API 地址(生产替换为真实网关域名) */
  9. const API_BASE = process.env['NEXT_PUBLIC_API_URL'] || 'http://localhost:8081/v1';
  10. export function AdminGate({ children }: { children: ReactNode }) {
  11. const [token, setToken] = useState<string | null>(() => {
  12. try {
  13. return localStorage.getItem('dotouch_admin_token');
  14. } catch {
  15. return null;
  16. }
  17. });
  18. const [email, setEmail] = useState('');
  19. const [password, setPassword] = useState('');
  20. const [msg, setMsg] = useState<string | null>(null);
  21. const [msgType, setMsgType] = useState<'err' | 'ok'>('err');
  22. const [loading, setLoading] = useState(false);
  23. const logout = () => {
  24. try {
  25. localStorage.removeItem('dotouch_admin_token');
  26. localStorage.removeItem('dotouch_admin_user');
  27. } catch { /* 忽略 */ }
  28. setToken(null);
  29. };
  30. const login = async () => {
  31. if (!email || !password) {
  32. setMsg('请输入账号和密码');
  33. setMsgType('err');
  34. return;
  35. }
  36. setLoading(true);
  37. setMsg(null);
  38. try {
  39. const resp = await fetch(`${API_BASE}/auth/login`, {
  40. method: 'POST',
  41. headers: { 'Content-Type': 'application/json' },
  42. body: JSON.stringify({ email, password }),
  43. });
  44. const data = await resp.json().catch(() => ({}));
  45. if (resp.ok && data.accessToken && data.user?.role === 'admin') {
  46. try {
  47. localStorage.setItem('dotouch_admin_token', data.accessToken);
  48. localStorage.setItem('dotouch_admin_user', email);
  49. } catch { /* 忽略 */ }
  50. setToken(data.accessToken);
  51. } else if (resp.ok && data.accessToken) {
  52. setMsg('该账号无管理员权限(需要 role=admin)');
  53. setMsgType('err');
  54. } else {
  55. setMsg(data.message || '登录失败,请检查账号密码');
  56. setMsgType('err');
  57. }
  58. } catch {
  59. setMsg('无法连接 API,请确认演示服务已启动(localhost:8081)');
  60. setMsgType('err');
  61. } finally {
  62. setLoading(false);
  63. }
  64. };
  65. if (token) {
  66. return (
  67. <AdminShell>
  68. <button
  69. type="button"
  70. onClick={logout}
  71. className="fixed right-4 top-2 z-[500] rounded-lg border border-white/[0.08] bg-brand-card px-3 py-1 text-xs text-brand-muted transition-colors hover:border-[rgba(255,107,107,.4)] hover:text-[#FF8A8A]"
  72. >
  73. 退出登录
  74. </button>
  75. {children}
  76. </AdminShell>
  77. );
  78. }
  79. return (
  80. <div className="flex min-h-screen items-center justify-center bg-brand-bg px-5">
  81. <div className="w-[380px] max-w-full rounded-[20px] border border-[rgba(124,92,255,.4)] bg-brand-card px-[30px] py-[34px] shadow-[0_24px_80px_rgba(0,0,0,.6)]">
  82. <div className="mb-6 text-center">
  83. <div className="text-xl font-extrabold tracking-wide">
  84. DoTouch<span className="text-brand-purple-l">.AI</span>
  85. </div>
  86. <div className="mt-1 text-[12.5px] text-brand-muted">平台管理后台 · 管理员登录</div>
  87. </div>
  88. {msg && (
  89. <div
  90. className={`mb-3 rounded-[9px] border px-3 py-[9px] text-[12.5px] ${
  91. msgType === 'err'
  92. ? 'border-[rgba(255,107,107,.35)] bg-[rgba(255,107,107,.1)] text-[#FF8A8A]'
  93. : 'border-[rgba(0,229,160,.3)] bg-[rgba(0,229,160,.1)] text-brand-green-l'
  94. }`}
  95. >
  96. {msg}
  97. </div>
  98. )}
  99. <label className="mb-1.5 block text-[12.5px] text-brand-muted">账号(邮箱)</label>
  100. <input
  101. type="email"
  102. value={email}
  103. onChange={(e) => setEmail(e.target.value)}
  104. placeholder="admin@dotouch.ai"
  105. className="mb-4 w-full rounded-[10px] border border-white/[0.08] bg-brand-card2 px-[14px] py-[11px] text-sm text-brand-text outline-none focus:border-brand-purple"
  106. />
  107. <label className="mb-1.5 block text-[12.5px] text-brand-muted">密码</label>
  108. <input
  109. type="password"
  110. value={password}
  111. onChange={(e) => setPassword(e.target.value)}
  112. onKeyDown={(e) => e.key === 'Enter' && login()}
  113. placeholder="••••••••"
  114. className="mb-6 w-full rounded-[10px] border border-white/[0.08] bg-brand-card2 px-[14px] py-[11px] text-sm text-brand-text outline-none focus:border-brand-purple"
  115. />
  116. <button
  117. type="button"
  118. onClick={login}
  119. disabled={loading}
  120. className="w-full justify-center rounded-[10px] bg-gradient-to-br from-brand-purple to-[#5C3DF0] px-[18px] py-[11px] text-sm font-semibold text-white shadow-[0_4px_20px_rgba(124,92,255,.35)] transition-all hover:-translate-y-px hover:shadow-[0_6px_28px_rgba(124,92,255,.5)] disabled:cursor-not-allowed disabled:opacity-60"
  121. >
  122. {loading ? '登录中…' : '登 录'}
  123. </button>
  124. <div className="mt-5 text-center text-[11.5px] text-brand-muted">
  125. 演示账号:admin@dotouch.ai / Admin2026!
  126. </div>
  127. </div>
  128. </div>
  129. );
  130. }