login.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. import React, { useState } from 'react';
  2. import { useNavigate } from 'react-router';
  3. import { ArrowRightIcon, LockClosedIcon, UserIcon } from '@heroicons/react/24/outline';
  4. import { useAuth } from '../hooks.tsx';
  5. import { handleApiError } from '../utils.ts';
  6. // 登录页面组件
  7. const LoginPage: React.FC = () => {
  8. const { login } = useAuth();
  9. const navigate = useNavigate();
  10. const [username, setUsername] = useState('');
  11. const [password, setPassword] = useState('');
  12. const [loading, setLoading] = useState(false);
  13. const [error, setError] = useState<string | null>(null);
  14. const handleLogin = async (e: React.FormEvent) => {
  15. e.preventDefault();
  16. if (!username.trim() || !password.trim()) {
  17. setError('用户名和密码不能为空');
  18. return;
  19. }
  20. setLoading(true);
  21. setError(null);
  22. try {
  23. await login(username, password);
  24. navigate('/');
  25. } catch (err) {
  26. setError(handleApiError(err));
  27. } finally {
  28. setLoading(false);
  29. }
  30. };
  31. return (
  32. <div className="min-h-screen flex flex-col bg-gradient-to-b from-blue-500 to-blue-700 p-6">
  33. {/* 顶部Logo和标题 */}
  34. <div className="flex flex-col items-center justify-center mt-10 mb-8">
  35. <div className="w-20 h-20 bg-white rounded-2xl flex items-center justify-center shadow-lg mb-4">
  36. <svg className="w-12 h-12 text-blue-600" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  37. <path d="M12 2L2 7L12 12L22 7L12 2Z" fill="currentColor" />
  38. <path d="M2 17L12 22L22 17" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
  39. <path d="M2 12L12 17L22 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
  40. </svg>
  41. </div>
  42. <h1 className="text-3xl font-bold text-white">
  43. {window.CONFIG?.APP_NAME || '移动应用'}
  44. </h1>
  45. <p className="text-blue-100 mt-2">登录您的账户</p>
  46. </div>
  47. {/* 登录表单 */}
  48. <div className="bg-white rounded-xl shadow-xl p-6 w-full">
  49. {error && (
  50. <div className="bg-red-50 text-red-700 p-3 rounded-lg mb-4 text-sm">
  51. {error}
  52. </div>
  53. )}
  54. <form onSubmit={handleLogin}>
  55. <div className="mb-4">
  56. <label className="block text-gray-700 text-sm font-medium mb-2" htmlFor="username">
  57. 用户名
  58. </label>
  59. <div className="relative">
  60. <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
  61. <UserIcon className="h-5 w-5 text-gray-400" />
  62. </div>
  63. <input
  64. id="username"
  65. type="text"
  66. value={username}
  67. onChange={(e) => setUsername(e.target.value)}
  68. className="w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
  69. placeholder="请输入用户名"
  70. />
  71. </div>
  72. </div>
  73. <div className="mb-6">
  74. <label className="block text-gray-700 text-sm font-medium mb-2" htmlFor="password">
  75. 密码
  76. </label>
  77. <div className="relative">
  78. <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
  79. <LockClosedIcon className="h-5 w-5 text-gray-400" />
  80. </div>
  81. <input
  82. id="password"
  83. type="password"
  84. value={password}
  85. onChange={(e) => setPassword(e.target.value)}
  86. className="w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
  87. placeholder="请输入密码"
  88. />
  89. </div>
  90. </div>
  91. <button
  92. type="submit"
  93. disabled={loading}
  94. className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 flex items-center justify-center"
  95. >
  96. {loading ? (
  97. <svg className="animate-spin -ml-1 mr-2 h-5 w-5 text-white" fill="none" viewBox="0 0 24 24">
  98. <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
  99. <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
  100. </svg>
  101. ) : (
  102. <ArrowRightIcon className="h-5 w-5 mr-2" />
  103. )}
  104. {loading ? '登录中...' : '登录'}
  105. </button>
  106. </form>
  107. <div className="mt-6 flex items-center justify-between">
  108. <button
  109. type="button"
  110. className="text-sm text-blue-600 hover:text-blue-700"
  111. onClick={() => navigate('/register')}
  112. >
  113. 注册账号
  114. </button>
  115. <button
  116. type="button"
  117. className="text-sm text-blue-600 hover:text-blue-700"
  118. >
  119. 忘记密码?
  120. </button>
  121. </div>
  122. </div>
  123. {/* 底部文本 */}
  124. <div className="mt-auto pt-8 text-center text-blue-100 text-sm">
  125. &copy; {new Date().getFullYear()} {window.CONFIG?.APP_NAME || '移动应用'}
  126. <p className="mt-1">保留所有权利</p>
  127. </div>
  128. </div>
  129. );
  130. };
  131. export default LoginPage;