生产管理系统 - 登录,二维码功能集成

This commit is contained in:
2025-10-31 11:49:11 +08:00
parent 2fa64e66c9
commit 46ff61eaed
11 changed files with 1867 additions and 185 deletions

View File

@@ -0,0 +1,387 @@
/**
* 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 { loginApiV1AuthLoginPost } from '@/lib/api/sdk.gen';
import type { CaptchaResponse } from '@/lib/api/types.gen';
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(),
};
// 打印登录成功日志
console.log('🎉 登录成功!', {
user: userData,
apiResponse: response.data,
timestamp: new Date().toISOString()
});
login(userData);
toast.success('登录成功!');
// 暂时不实现页面跳转
console.log('✅ 登录流程完成,等待后续页面跳转实现');
} 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)}
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 } })}
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>
);
}