frontend: convert login and register to homepage modals

- Remove register link from login form
- Redirect /login and /register to /?login=1 and /?register=1
- Open login/register as dialogs on homepage instead of separate pages
This commit is contained in:
2026-04-16 16:55:29 +00:00
parent 9f9f57b379
commit f9499c0795
3 changed files with 104 additions and 167 deletions

View File

@@ -6,11 +6,18 @@ import { useSearchParams } from "next/navigation";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export function LoginForm({ hasKeycloak }: { hasKeycloak: boolean }) { export function LoginForm({
hasKeycloak,
callbackUrl: callbackUrlProp,
onSuccess,
}: {
hasKeycloak: boolean;
callbackUrl?: string;
onSuccess?: () => void;
}) {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const callbackUrl = searchParams.get("callbackUrl") || "/dashboard"; const callbackUrl = callbackUrlProp ?? searchParams.get("callbackUrl") ?? "/dashboard";
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [error, setError] = useState(""); const [error, setError] = useState("");
@@ -29,6 +36,7 @@ export function LoginForm({ hasKeycloak }: { hasKeycloak: boolean }) {
if (res?.error) { if (res?.error) {
setError("登录失败,请检查用户名和密码"); setError("登录失败,请检查用户名和密码");
} else { } else {
onSuccess?.();
const redirectUrl = callbackUrl.startsWith("http") const redirectUrl = callbackUrl.startsWith("http")
? callbackUrl ? callbackUrl
: window.location.origin + callbackUrl; : window.location.origin + callbackUrl;
@@ -37,65 +45,53 @@ export function LoginForm({ hasKeycloak }: { hasKeycloak: boolean }) {
} }
return ( return (
<Card className="w-full max-w-md"> <div className="space-y-4">
<CardHeader> <form onSubmit={handleSubmit} className="space-y-4">
<CardTitle className="text-center"></CardTitle> <div>
</CardHeader> <Label htmlFor="username"></Label>
<CardContent className="space-y-4"> <Input
<form onSubmit={handleSubmit} className="space-y-4"> id="username"
<div> value={username}
<Label htmlFor="username"></Label> onChange={(e) => setUsername(e.target.value)}
<Input required
id="username" />
value={username} </div>
onChange={(e) => setUsername(e.target.value)} <div>
required <Label htmlFor="password"></Label>
/> <Input
</div> id="password"
<div> type="password"
<Label htmlFor="password"></Label> value={password}
<Input onChange={(e) => setPassword(e.target.value)}
id="password" required
type="password" />
value={password} </div>
onChange={(e) => setPassword(e.target.value)} {error && <p className="text-sm text-red-500">{error}</p>}
required <Button type="submit" className="w-full">
/>
</div> </Button>
{error && <p className="text-sm text-red-500">{error}</p>} </form>
<Button type="submit" className="w-full">
</Button>
</form>
{hasKeycloak && ( {hasKeycloak && (
<> <>
<div className="relative"> <div className="relative">
<div className="absolute inset-0 flex items-center"> <div className="absolute inset-0 flex items-center">
<span className="w-full border-t" /> <span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-white px-2 text-gray-500"></span>
</div>
</div> </div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-white px-2 text-gray-500"></span>
</div>
</div>
<Button <Button
variant="outline" variant="outline"
className="w-full" className="w-full"
onClick={() => signIn("keycloak", { callbackUrl })} onClick={() => signIn("keycloak", { callbackUrl })}
> >
Keycloak Keycloak
</Button> </Button>
</> </>
)} )}
</div>
<p className="text-center text-sm text-gray-500">
{" "}
<a href="/register" className="text-blue-600 hover:underline">
</a>
</p>
</CardContent>
</Card>
); );
} }

View File

@@ -1,14 +1,50 @@
import { Suspense } from "react"; "use client";
import { Suspense, useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { LoginForm } from "./login-form"; import { LoginForm } from "./login-form";
const hasKeycloak = !!process.env.AUTH_KEYCLOAK_ISSUER; const hasKeycloak = !!process.env.AUTH_KEYCLOAK_ISSUER;
export default function LoginPage() { function LoginPageContent() {
const router = useRouter();
const searchParams = useSearchParams();
const callbackUrl = searchParams.get("callbackUrl");
const error = searchParams.get("error");
useEffect(() => {
// If not an OAuth callback, redirect to home with login modal open
if (!callbackUrl && !error) {
router.replace("/?login=1");
}
}, [callbackUrl, error, router]);
return ( return (
<div className="flex min-h-screen items-center justify-center bg-gray-50 p-4"> <div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100 p-4">
<Suspense fallback={<div>...</div>}> <Dialog defaultOpen>
<LoginForm hasKeycloak={hasKeycloak} /> <DialogContent className="sm:max-w-md">
</Suspense> <DialogHeader>
<DialogTitle className="text-center"></DialogTitle>
</DialogHeader>
<Suspense fallback={<div>...</div>}>
<LoginForm hasKeycloak={hasKeycloak} />
</Suspense>
</DialogContent>
</Dialog>
</div> </div>
); );
} }
export default function LoginPage() {
return (
<Suspense fallback={<div className="flex min-h-screen items-center justify-center">...</div>}>
<LoginPageContent />
</Suspense>
);
}

View File

@@ -1,113 +1,18 @@
"use client"; "use client";
import { useState } from "react"; import { useEffect } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export default function RegisterPage() { export default function RegisterPage() {
const router = useRouter(); const router = useRouter();
const [form, setForm] = useState({
username: "",
email: "",
password: "",
confirmPassword: "",
});
const [error, setError] = useState("");
async function handleSubmit(e: React.FormEvent) { useEffect(() => {
e.preventDefault(); router.replace("/");
setError(""); }, [router]);
if (form.password !== form.confirmPassword) {
setError("两次输入的密码不一致");
return;
}
const res = await fetch("/api/proxy/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: form.username,
email: form.email,
password: form.password,
}),
});
if (!res.ok) {
const data = await res.json();
setError(data.error || "注册失败");
return;
}
router.push("/login");
}
return ( return (
<div className="flex min-h-screen items-center justify-center bg-gray-50 p-4"> <div className="flex min-h-screen items-center justify-center">
<Card className="w-full max-w-md"> ...
<CardHeader>
<CardTitle className="text-center"></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<Label htmlFor="username"></Label>
<Input
id="username"
value={form.username}
onChange={(e) => setForm({ ...form, username: e.target.value })}
required
/>
</div>
<div>
<Label htmlFor="email"></Label>
<Input
id="email"
type="email"
value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
required
/>
</div>
<div>
<Label htmlFor="password"></Label>
<Input
id="password"
type="password"
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
required
/>
</div>
<div>
<Label htmlFor="confirmPassword"></Label>
<Input
id="confirmPassword"
type="password"
value={form.confirmPassword}
onChange={(e) =>
setForm({ ...form, confirmPassword: e.target.value })
}
required
/>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
<Button type="submit" className="w-full">
</Button>
</form>
<p className="text-center text-sm text-gray-500">
{" "}
<a href="/login" className="text-blue-600 hover:underline">
</a>
</p>
</CardContent>
</Card>
</div> </div>
); );
} }