Login.tsx 6.9 KB

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