265 lines
8.2 KiB
TypeScript
Executable File
265 lines
8.2 KiB
TypeScript
Executable File
'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>
|
||
);
|
||
}
|