casualroom/apps/fenghuo/web/app/[locale]/auth/login/page.tsx

265 lines
8.2 KiB
TypeScript
Raw Normal View History

2025-07-28 07:50:50 +08:00
'use client';
import { useState, useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useForm } from 'react-hook-form';
import { useAuth } from '@/components/providers/auth-provider';
import { Button } from '@nice/ui/components/button';
import { Input } from '@nice/ui/components/input';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@nice/ui/components/form';
import { Alert, AlertDescription } from '@nice/ui/components/alert';
import Link from 'next/link';
import { toast } from '@nice/ui/components/sonner';
import React from 'react';
import { Checkbox } from '@nice/ui/components/checkbox';
import { oidcClient } from '@/lib/auth/oidc-client';
import { Eye, EyeOff } from 'lucide-react';
interface LoginForm {
username: string;
password: string;
remember: boolean;
}
export default function LoginPage() {
const router = useRouter();
const searchParams = useSearchParams();
const { login, isLoading, isAuthenticated } = useAuth();
const [isRedirecting, setIsRedirecting] = useState(false);
const [redirectUri, setRedirectUri] = useState('');
const [passwordVisible, setPasswordVisible] = useState(false);
const [error, setError] = useState<string | null>(null);
const redirectUrl = searchParams.get('redirectUrl') || '/dashboard';
const message = searchParams.get('message');
useEffect(() => {
// 设置回调 URI
setRedirectUri(`${window.location.origin}/auth/callback`);
}, []);
const handleSSOLogin = () => {
if (!redirectUri) return;
// 构建授权 URL
const authUrl = oidcClient.buildAuthorizationUrl({
redirectUri,
state: 'test-state',
scope: 'openid profile email',
});
// 保存状态到 sessionStorage包括 redirectUrl
sessionStorage.setItem('oauth_state', 'test-state');
sessionStorage.setItem('oauth_redirect_uri', redirectUri);
sessionStorage.setItem('oauth_return_url', redirectUrl);
// 跳转到授权页面
window.location.href = authUrl;
};
const form = useForm<LoginForm>({
defaultValues: {
username: '',
password: '',
remember: false,
},
});
// 组件加载时检查是否有保存的凭据
useEffect(() => {
const savedCredentials = localStorage.getItem('loginCredentials');
if (savedCredentials) {
const { username, password } = JSON.parse(savedCredentials);
form.setValue('username', username);
form.setValue('password', password);
form.setValue('remember', true);
}
}, [form]);
// 处理已登录用户的重定向
React.useEffect(() => {
if (isAuthenticated && !isRedirecting) {
setIsRedirecting(true);
router.push(redirectUrl);
}
}, [isAuthenticated, redirectUrl, router, isRedirecting]);
const handleSubmit = async (data: LoginForm) => {
setError(null);
try {
await login(data.username, data.password);
// 处理记住密码
if (data.remember) {
localStorage.setItem(
'loginCredentials',
JSON.stringify({
username: data.username,
password: data.password,
}),
);
} else {
localStorage.removeItem('loginCredentials');
}
toast.success('登录成功');
setIsRedirecting(true);
router.replace(redirectUrl);
} catch (err) {
setError(err instanceof Error ? err.message : '登录失败,请重试');
toast.error('登录失败,请重试');
}
};
// 如果正在重定向,显示加载状态而不是空白
if (isRedirecting) {
return (
<div className="flex min-h-[428px] items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"></div>
<p className="text-gray-600">...</p>
</div>
</div>
);
}
return (
<div className="flex">
<div className="w-full">
{/* 欢迎标题 */}
<div className="text-center mb-12">
<h1 className="text-3xl font-bold text-gray-900 mb-3">使FengHuo</h1>
{/* 成功消息 */}
{message === 'register_success' && (
<Alert className="mb-4 border-green-200 bg-green-50">
<AlertDescription className="text-green-800">使</AlertDescription>
</Alert>
)}
{/* 错误消息 */}
{error && (
<Alert className="mb-4 border-red-200 bg-red-50">
<AlertDescription className="text-red-800">{error}</AlertDescription>
</Alert>
)}
</div>
{/* 登录表单 */}
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
{/* 用户名输入 */}
<FormField
control={form.control}
name="username"
rules={{
required: '请输入用户名',
minLength: { value: 3, message: '用户名至少3个字符' },
}}
render={({ field }) => (
<FormItem>
<FormLabel className="text-base text-gray-700 font-medium"></FormLabel>
<FormControl>
<Input
type="text"
placeholder="请输入用户名"
className="h-12 text-base focus-visible:ring-1 focus-visible:ring-blue-500 focus-visible:border-blue-500"
autoComplete="username"
disabled={isLoading}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* 密码输入 */}
<FormField
control={form.control}
name="password"
rules={{
required: '请输入密码',
minLength: { value: 4, message: '密码至少4个字符' },
}}
render={({ field }) => (
<FormItem>
<FormLabel className="text-base text-gray-700 font-medium"></FormLabel>
<FormControl>
<div className='relative'>
<Input
type={passwordVisible ? 'text' : 'password'} // 根据状态切换输入类型
placeholder="请输入密码"
className="h-12 text-base focus-visible:ring-1 focus-visible:ring-blue-500 focus-visible:border-blue-500 pr-10" // 增加右侧内边距
autoComplete="current-password"
disabled={isLoading}
{...field}
/>
<button
type="button"
className="absolute right-3 top-1/2 transform -translate-y-1/2"
onClick={() => setPasswordVisible(!passwordVisible)} // 点击切换密码可见性
>
{passwordVisible ? (
<Eye className="w-5 h-5 text-gray-600" />
) : (
<EyeOff className="w-5 h-5 text-gray-600" />
)}
</button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* 添加记住密码复选框 */}
<FormField
control={form.control}
name="remember"
render={({ field }) => (
<FormItem className="flex flex-row justify-end items-center space-x-3">
<FormControl>
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
<FormLabel className="text-sm text-gray-600 font-normal"></FormLabel>
</FormItem>
)}
/>
{/* 登录按钮 */}
<Button
type="submit"
className="w-full h-12 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium text-base transition-colors mt-4 cursor-pointer"
disabled={isLoading}
>
{isLoading ? '登录中...' : '登录'}
</Button>
</form>
</Form>
<div className="mt-10">
<div className="text-center text-sm text-gray-500 mb-6"></div>
<Button
variant="outline"
className="w-full h-12 border-green-300 text-green-700 hover:bg-green-50 rounded-lg font-medium text-base transition-colors cursor-pointer"
onClick={handleSSOLogin}
>
<svg className="w-5 h-5 mr-3" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
</svg>
SSO
</Button>
</div>
{/* 注册链接 */}
<div className="text-center mt-8">
<span className="text-base text-gray-600"></span>
<Link href="/auth/register" className="text-base text-blue-600 hover:underline ml-2">
</Link>
</div>
</div>
</div>
);
}