Login.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import { useState } from 'react';
  2. import { useNavigate } from 'react-router';
  3. import { useAuth } from '../hooks/AuthProvider';
  4. import { useForm } from 'react-hook-form';
  5. import { zodResolver } from '@hookform/resolvers/zod';
  6. import { z } from 'zod';
  7. import { toast } from 'sonner';
  8. import { Eye, EyeOff, User, Lock } from 'lucide-react';
  9. import { Button } from '@/client/components/ui/button';
  10. import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/client/components/ui/card';
  11. import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
  12. import { Input } from '@/client/components/ui/input';
  13. // 表单验证Schema
  14. const loginSchema = z.object({
  15. username: z.string().min(1, '请输入用户名'),
  16. password: z.string().min(1, '请输入密码'),
  17. });
  18. type LoginFormData = z.infer<typeof loginSchema>;
  19. // 登录页面
  20. export const LoginPage = () => {
  21. const { login } = useAuth();
  22. const [isLoading, setIsLoading] = useState(false);
  23. const [showPassword, setShowPassword] = useState(false);
  24. const navigate = useNavigate();
  25. const form = useForm<LoginFormData>({
  26. resolver: zodResolver(loginSchema),
  27. defaultValues: {
  28. username: '',
  29. password: '',
  30. },
  31. });
  32. const handleSubmit = async (data: LoginFormData) => {
  33. try {
  34. setIsLoading(true);
  35. // 获取地理位置
  36. let latitude: number | undefined;
  37. let longitude: number | undefined;
  38. try {
  39. if (navigator.geolocation) {
  40. const position = await new Promise<GeolocationPosition>((resolve, reject) => {
  41. navigator.geolocation.getCurrentPosition(resolve, reject);
  42. });
  43. latitude = position.coords.latitude;
  44. longitude = position.coords.longitude;
  45. }
  46. } catch (geoError) {
  47. console.warn('获取地理位置失败:', geoError);
  48. }
  49. await login(data.username, data.password, latitude, longitude);
  50. // 登录成功后跳转到管理后台首页
  51. navigate('/admin/dashboard');
  52. toast.success('登录成功!欢迎回来');
  53. } catch (error: any) {
  54. toast.error(error instanceof Error ? error.message : '登录失败');
  55. } finally {
  56. setIsLoading(false);
  57. }
  58. };
  59. return (
  60. <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50 py-12 px-4 sm:px-6 lg:px-8">
  61. <div className="max-w-md w-full space-y-8">
  62. <div className="text-center">
  63. <div className="mx-auto w-16 h-16 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-full flex items-center justify-center shadow-lg">
  64. <User className="h-8 w-8 text-white" />
  65. </div>
  66. <h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
  67. 管理后台登录
  68. </h2>
  69. <p className="mt-2 text-center text-sm text-gray-600">
  70. 请输入您的账号和密码继续操作
  71. </p>
  72. </div>
  73. <Card className="shadow-xl border-0">
  74. <CardHeader className="text-center">
  75. <CardTitle className="text-2xl">欢迎登录</CardTitle>
  76. <CardDescription>
  77. 使用您的账户信息登录系统
  78. </CardDescription>
  79. </CardHeader>
  80. <CardContent>
  81. <Form {...form}>
  82. <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
  83. <FormField
  84. control={form.control}
  85. name="username"
  86. render={({ field }) => (
  87. <FormItem>
  88. <FormLabel>用户名</FormLabel>
  89. <FormControl>
  90. <div className="relative">
  91. <User className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
  92. <Input
  93. placeholder="请输入用户名"
  94. className="pl-10"
  95. {...field}
  96. />
  97. </div>
  98. </FormControl>
  99. <FormMessage />
  100. </FormItem>
  101. )}
  102. />
  103. <FormField
  104. control={form.control}
  105. name="password"
  106. render={({ field }) => (
  107. <FormItem>
  108. <FormLabel>密码</FormLabel>
  109. <FormControl>
  110. <div className="relative">
  111. <Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
  112. <Input
  113. type={showPassword ? 'text' : 'password'}
  114. placeholder="请输入密码"
  115. className="pl-10 pr-10"
  116. {...field}
  117. />
  118. <button
  119. type="button"
  120. className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
  121. onClick={() => setShowPassword(!showPassword)}
  122. >
  123. {showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
  124. </button>
  125. </div>
  126. </FormControl>
  127. <FormMessage />
  128. </FormItem>
  129. )}
  130. />
  131. <Button
  132. type="submit"
  133. className="w-full"
  134. disabled={isLoading}
  135. >
  136. {isLoading ? (
  137. <>
  138. <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
  139. 登录中...
  140. </>
  141. ) : (
  142. '登录'
  143. )}
  144. </Button>
  145. </form>
  146. </Form>
  147. </CardContent>
  148. <CardFooter className="flex flex-col items-center space-y-2">
  149. <div className="text-sm text-gray-500">
  150. 测试账号: <span className="font-medium text-gray-700">admin</span> / <span className="font-medium text-gray-700">admin123</span>
  151. </div>
  152. <div className="text-xs text-gray-400">
  153. © {new Date().getFullYear()} 管理系统. 保留所有权利.
  154. </div>
  155. </CardFooter>
  156. </Card>
  157. <div className="text-center">
  158. <p className="text-sm text-gray-500">
  159. 遇到问题?<a href="#" className="font-medium text-indigo-600 hover:text-indigo-500">联系管理员</a>
  160. </p>
  161. </div>
  162. </div>
  163. </div>
  164. );
  165. };