子仓库提交
This commit is contained in:
398
src/app/(auth)/login/components/LoginForm.tsx
Normal file
398
src/app/(auth)/login/components/LoginForm.tsx
Normal file
@@ -0,0 +1,398 @@
|
||||
/**
|
||||
* 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()
|
||||
});
|
||||
// 验证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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user