pages_login.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  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. // 获取地理位置
  24. const position = await new Promise<GeolocationPosition>((resolve, reject) => {
  25. if (!navigator.geolocation) {
  26. reject(new Error('浏览器不支持地理位置功能'));
  27. return;
  28. }
  29. navigator.geolocation.getCurrentPosition(
  30. resolve,
  31. (err) => reject(new Error(`获取位置失败: ${err.message}`)),
  32. { timeout: 5000 }
  33. );
  34. });
  35. const { latitude, longitude } = position.coords;
  36. await login(username, password, latitude, longitude);
  37. navigate('/');
  38. } catch (err) {
  39. // 如果获取位置失败,仍然允许登录但不带位置信息
  40. const error = err instanceof Error ? err : new Error(String(err));
  41. if (error.message.includes('获取位置失败')) {
  42. console.warn('获取位置失败:', err);
  43. try {
  44. await login(username, password);
  45. navigate('/');
  46. } catch (loginErr) {
  47. setError(handleApiError(loginErr));
  48. }
  49. } else {
  50. setError(handleApiError(err));
  51. }
  52. } finally {
  53. setLoading(false);
  54. }
  55. };
  56. return (
  57. <div className="min-h-screen flex flex-col bg-gradient-to-b from-blue-500 to-blue-700 p-6">
  58. {/* 顶部Logo和标题 */}
  59. <div className="flex flex-col items-center justify-center mt-10 mb-8">
  60. <div className="w-20 h-20 bg-white rounded-2xl flex items-center justify-center shadow-lg mb-4">
  61. <svg className="w-12 h-12 text-blue-600" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  62. <path d="M12 2L2 7L12 12L22 7L12 2Z" fill="currentColor" />
  63. <path d="M2 17L12 22L22 17" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
  64. <path d="M2 12L12 17L22 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
  65. </svg>
  66. </div>
  67. <h1 className="text-3xl font-bold text-white">
  68. {window.CONFIG?.APP_NAME || '移动应用'}
  69. </h1>
  70. <p className="text-blue-100 mt-2">登录您的账户</p>
  71. </div>
  72. {/* 登录表单 */}
  73. <div className="bg-white rounded-xl shadow-xl p-6 w-full">
  74. {error && (
  75. <div className="bg-red-50 text-red-700 p-3 rounded-lg mb-4 text-sm">
  76. {error}
  77. </div>
  78. )}
  79. <form onSubmit={handleLogin}>
  80. <div className="mb-4">
  81. <label className="block text-gray-700 text-sm font-medium mb-2" htmlFor="username">
  82. 用户名
  83. </label>
  84. <div className="relative">
  85. <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
  86. <UserIcon className="h-5 w-5 text-gray-400" />
  87. </div>
  88. <input
  89. id="username"
  90. type="text"
  91. value={username}
  92. onChange={(e) => setUsername(e.target.value)}
  93. 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"
  94. placeholder="请输入用户名"
  95. />
  96. </div>
  97. </div>
  98. <div className="mb-6">
  99. <label className="block text-gray-700 text-sm font-medium mb-2" htmlFor="password">
  100. 密码
  101. </label>
  102. <div className="relative">
  103. <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
  104. <LockClosedIcon className="h-5 w-5 text-gray-400" />
  105. </div>
  106. <input
  107. id="password"
  108. type="password"
  109. value={password}
  110. onChange={(e) => setPassword(e.target.value)}
  111. 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"
  112. placeholder="请输入密码"
  113. />
  114. </div>
  115. </div>
  116. <button
  117. type="submit"
  118. disabled={loading}
  119. 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"
  120. >
  121. {loading ? (
  122. <svg className="animate-spin -ml-1 mr-2 h-5 w-5 text-white" fill="none" viewBox="0 0 24 24">
  123. <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
  124. <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>
  125. </svg>
  126. ) : (
  127. <ArrowRightIcon className="h-5 w-5 mr-2" />
  128. )}
  129. {loading ? '登录中...' : '登录'}
  130. </button>
  131. </form>
  132. <div className="mt-6 flex items-center justify-between">
  133. <button
  134. type="button"
  135. className="text-sm text-blue-600 hover:text-blue-700"
  136. onClick={() => navigate('/register')}
  137. >
  138. 注册账号
  139. </button>
  140. <button
  141. type="button"
  142. className="text-sm text-blue-600 hover:text-blue-700"
  143. >
  144. 忘记密码?
  145. </button>
  146. </div>
  147. </div>
  148. {/* 底部文本 */}
  149. <div className="mt-auto pt-8 text-center text-blue-100 text-sm">
  150. &copy; {new Date().getFullYear()} {window.CONFIG?.APP_NAME || '移动应用'}
  151. <p className="mt-1">保留所有权利</p>
  152. </div>
  153. </div>
  154. );
  155. };
  156. export default LoginPage;