Files
smart-crop-ui/crop-x/src/app/(auth)/login/components/LoginForm.tsx

399 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* filekorolheader: 登录表单组件 - 用户密码登录和手机号登录功能
* 功能:密码登录、手机号登录、表单验证、验证码处理
* 路径:/login/components/LoginForm.tsx
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn/ui语义化样式支持暗色主题
*/
'use client';
import React, { useEffect, useState } from 'react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { CaptchaInput } from '@/components/auth/CaptchaInput';
import {
Lock,
User,
Phone,
MessageSquare,
Eye,
EyeOff,
LogIn,
Loader2,
Smartphone,
} from 'lucide-react';
import { toast } from 'sonner';
import { useAuth } from '@/components/auth/AuthContext';
import { authReducer, initialAuthState, AuthState, AuthAction } from './authReducer';
import { getCaptchaApiV1AuthCaptchaGet, loginApiV1AuthLoginPost } from '@/lib/api/sdk.gen';
import type { CaptchaResponse } from '@/lib/api/types.gen';
import {PERSONAL_CELTRAL_PAGE} from "@/config/constants"
interface LoginFormProps {
onRegisterClick: () => void;
}
export function LoginForm({ onRegisterClick }: LoginFormProps) {
const { login } = useAuth();
const [state, dispatch] = React.useReducer(authReducer, initialAuthState);
const [loginType, setLoginType] = useState<'password' | 'phone'>('password');
const [passwordCaptchaData, setPasswordCaptchaData] = useState<CaptchaResponse | null>(null);
const [phoneCaptchaData, setPhoneCaptchaData] = useState<CaptchaResponse | null>(null);
// 倒计时效果
useEffect(() => {
if (state.countdown > 0) {
const timer = setTimeout(() => {
dispatch({ type: 'SET_COUNTDOWN', payload: state.countdown - 1 });
}, 1000);
return () => clearTimeout(timer);
}
}, [state.countdown]);
// 发送验证码
const handleSendCode = async () => {
if (!state.phoneForm.phone) {
toast.error('请输入手机号');
return;
}
if (!/^1[3-9]\d{9}$/.test(state.phoneForm.phone)) {
toast.error('请输入正确的手机号');
return;
}
dispatch({ type: 'SET_SENDING_CODE', payload: true });
dispatch({ type: 'SET_ERROR', payload: '' });
try {
// 模拟发送验证码
await new Promise(resolve => setTimeout(resolve, 1000));
toast.success('验证码发送成功测试验证码123456');
dispatch({ type: 'SET_COUNTDOWN', payload: 60 });
} catch (err) {
dispatch({ type: 'SET_ERROR', payload: '发送验证码失败,请稍后重试' });
toast.error('发送验证码失败');
} finally {
dispatch({ type: 'SET_SENDING_CODE', payload: false });
}
};
// 密码登录
const handlePasswordLogin = async (e: React.FormEvent) => {
e.preventDefault();
dispatch({ type: 'SET_ERROR', payload: '' });
if (!state.passwordForm.username || !state.passwordForm.password) {
dispatch({ type: 'SET_ERROR', payload: '请输入用户名和密码' });
toast.error('请输入用户名和密码');
return;
}
if (!state.passwordForm.captcha) {
dispatch({ type: 'SET_ERROR', payload: '请输入图形验证码' });
toast.error('请输入图形验证码');
return;
}
if (!passwordCaptchaData?.captcha_id) {
dispatch({ type: 'SET_ERROR', payload: '验证码数据未获取,请刷新验证码' });
toast.error('验证码数据未获取,请刷新验证码');
return;
}
dispatch({ type: 'SET_LOADING', payload: true });
try {
const response = await loginApiV1AuthLoginPost({
body: {
identifier: state.passwordForm.username,
password: state.passwordForm.password,
captcha_id: passwordCaptchaData.captcha_id,
captcha_text: state.passwordForm.captcha,
},
});
if (response.data) {
// 登录成功,提取用户信息
const userData = {
id: response.data.user_id || '1',
username: state.passwordForm.username,
realName: response.data.real_name || state.passwordForm.username,
phone: response.data.phone || '',
email: response.data.email || '',
role: response.data.role || 'user',
permissions: response.data.permissions || [],
enterpriseId: response.data.enterprise_id || '',
enterpriseName: response.data.enterprise_name || '',
createdAt: response.data.created_at || new Date().toISOString(),
// 重要存储token到用户对象中
token: response.data.access_token || response.data.token || null,
refreshToken:response.data.refresh_token || ''
};
// 打印登录成功日志
console.log('🎉 登录成功!', {
user: userData,
apiResponse: response.data,
timestamp: new Date().toISOString()
});
debugger
// 验证token是否正确存储
if (userData.token) {
console.log('🔑 Token已存储:', userData.token.substring(0, 20) + '...');
} else {
console.warn('⚠️ 未找到token请检查API响应格式');
}
login(userData);
toast.success('登录成功!正在跳转...');
// 跳转到个人中心页面
window.location.href = PERSONAL_CELTRAL_PAGE;
} else {
dispatch({ type: 'SET_ERROR', payload: '登录失败,请检查用户名和密码' });
toast.error('登录失败,请检查用户名和密码');
}
} catch (err: any) {
console.error('登录失败:', err);
const errorMessage = err?.response?.data?.message || err?.message || '登录失败,请稍后重试';
dispatch({ type: 'SET_ERROR', payload: errorMessage });
toast.error(errorMessage);
} finally {
dispatch({ type: 'SET_LOADING', payload: false });
}
};
// 手机号登录
const handlePhoneLogin = async (e: React.FormEvent) => {
e.preventDefault();
dispatch({ type: 'SET_ERROR', payload: '' });
if (!state.phoneForm.phone || !state.phoneForm.code) {
dispatch({ type: 'SET_ERROR', payload: '请输入手机号和验证码' });
return;
}
if (!state.phoneForm.captcha) {
dispatch({ type: 'SET_ERROR', payload: '请输入图形验证码' });
return;
}
dispatch({ type: 'SET_LOADING', payload: true });
try {
// 模拟手机号登录
await new Promise(resolve => setTimeout(resolve, 1000));
if (state.phoneForm.code === '123456') {
login({
id: '2',
username: 'user_' + state.phoneForm.phone.slice(-4),
realName: '用户',
phone: state.phoneForm.phone,
email: '',
role: 'user',
permissions: [],
enterpriseId: '',
enterpriseName: '',
createdAt: new Date().toISOString(),
});
toast.success('登录成功!');
window.location.href = '/';
} else {
dispatch({ type: 'SET_ERROR', payload: '验证码错误' });
toast.error('验证码错误');
}
} catch (err) {
dispatch({ type: 'SET_ERROR', payload: '登录失败,请稍后重试' });
toast.error('登录失败');
} finally {
dispatch({ type: 'SET_LOADING', payload: false });
}
};
return (
<Card className="p-8 shadow-2xl bg-white/95 backdrop-blur-md border-white/20">
<Tabs value={loginType} onValueChange={(v) => setLoginType(v as 'password' | 'phone')}>
<TabsList className="grid w-full grid-cols-2 mb-6">
<TabsTrigger value="password" className="flex items-center gap-2">
<Lock className="w-4 h-4" />
</TabsTrigger>
<TabsTrigger value="phone" className="flex items-center gap-2">
<Smartphone className="w-4 h-4" />
</TabsTrigger>
</TabsList>
{state.error && (
<div className="mb-4 p-3 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded-lg">
<p className="text-sm text-red-700 dark:text-red-300">{state.error}</p>
</div>
)}
{/* 密码登录 */}
<TabsContent value="password">
<form onSubmit={handlePasswordLogin} className="space-y-4">
<div>
<Label></Label>
<div className="relative mt-2">
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
type="text"
placeholder="请输入用户名"
value={state.passwordForm.username}
onChange={(e) => dispatch({ type: 'UPDATE_PASSWORD_FORM', payload: { username: e.target.value } })}
className="pl-10"
disabled={state.loading}
/>
</div>
</div>
<div>
<Label></Label>
<div className="relative mt-2">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
type={state.showPassword ? 'text' : 'password'}
placeholder="请输入密码"
value={state.passwordForm.password}
onChange={(e) => dispatch({ type: 'UPDATE_PASSWORD_FORM', payload: { password: e.target.value } })}
className="pl-10 pr-10"
disabled={state.loading}
/>
<button
type="button"
onClick={() => dispatch({ type: 'TOGGLE_PASSWORD_VISIBILITY' })}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{state.showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<div>
<Label></Label>
<CaptchaInput
value={state.passwordForm.captcha}
onChange={(value) => dispatch({ type: 'UPDATE_PASSWORD_FORM', payload: { captcha: value } })}
onCaptchaChange={(captchaData) => setPasswordCaptchaData(captchaData)}
instanceId="password-login"
className="mt-2"
/>
</div>
<Button type="submit" className="w-full bg-green-600 hover:bg-green-700" disabled={state.loading}>
{state.loading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
<>
<LogIn className="w-4 h-4 mr-2" />
</>
)}
</Button>
</form>
</TabsContent>
{/* 手机号登录 */}
<TabsContent value="phone">
<form onSubmit={handlePhoneLogin} className="space-y-4">
<div>
<Label></Label>
<div className="relative mt-2">
<Phone className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
type="tel"
placeholder="请输入手机号"
value={state.phoneForm.phone}
onChange={(e) => dispatch({ type: 'UPDATE_PHONE_FORM', payload: { phone: e.target.value } })}
className="pl-10"
maxLength={11}
disabled={state.loading}
/>
</div>
</div>
<div>
<Label></Label>
<div className="flex gap-2 mt-2">
<div className="relative flex-1">
<MessageSquare className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
type="text"
placeholder="请输入验证码"
value={state.phoneForm.code}
onChange={(e) => dispatch({ type: 'UPDATE_PHONE_FORM', payload: { code: e.target.value } })}
className="pl-10"
maxLength={6}
disabled={state.loading}
/>
</div>
<Button
type="button"
variant="outline"
onClick={handleSendCode}
disabled={state.sendingCode || state.countdown > 0 || state.loading}
className="min-w-[120px]"
>
{state.sendingCode ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : state.countdown > 0 ? (
`${state.countdown}秒后重试`
) : (
'发送验证码'
)}
</Button>
</div>
</div>
<div>
<Label></Label>
<CaptchaInput
value={state.phoneForm.captcha}
onChange={(value) => dispatch({ type: 'UPDATE_PHONE_FORM', payload: { captcha: value } })}
instanceId="phone-login"
className="mt-2"
/>
</div>
<Button type="submit" className="w-full bg-green-600 hover:bg-green-700" disabled={state.loading}>
{state.loading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
<>
<LogIn className="w-4 h-4 mr-2" />
</>
)}
</Button>
</form>
</TabsContent>
</Tabs>
{/* 注册跳转 */}
<div className="mt-6 text-center">
<p className="text-sm text-muted-foreground">
<button
type="button"
onClick={onRegisterClick}
className="ml-1 text-green-600 hover:text-green-700 font-medium"
>
</button>
</p>
</div>
</Card>
);
}