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

265 lines
8.2 KiB
TypeScript
Executable File
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.

'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>
);
}