- bookmark management with dnd-kit reordering, bulk edit, search, category filter/rename, and meta auto-fetch - migrate /bookmarks → /dashboard/bookmarks under (main) layout - homepage redesign with category grid, /-key search, dock tooltips - theme toggle + use-theme, sonner toasts, alert-dialog/skeleton, visual refresh of auth pages Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
99 lines
2.7 KiB
TypeScript
99 lines
2.7 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { signIn } from "next-auth/react";
|
|
import { useSearchParams } from "next/navigation";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
export function LoginForm({
|
|
hasKeycloak,
|
|
callbackUrl: callbackUrlProp,
|
|
onSuccess,
|
|
}: {
|
|
hasKeycloak: boolean;
|
|
callbackUrl?: string;
|
|
onSuccess?: () => void;
|
|
}) {
|
|
const searchParams = useSearchParams();
|
|
const callbackUrl = callbackUrlProp ?? searchParams.get("callbackUrl") ?? "/dashboard";
|
|
const [username, setUsername] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [error, setError] = useState("");
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
setError("");
|
|
|
|
const res = await signIn("credentials", {
|
|
username,
|
|
password,
|
|
redirect: false,
|
|
callbackUrl,
|
|
});
|
|
|
|
if (res?.error) {
|
|
setError("登录失败,请检查用户名和密码");
|
|
} else {
|
|
onSuccess?.();
|
|
const redirectUrl = callbackUrl.startsWith("http")
|
|
? callbackUrl
|
|
: window.location.origin + callbackUrl;
|
|
window.location.href = redirectUrl;
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div>
|
|
<Label htmlFor="username">用户名</Label>
|
|
<Input
|
|
id="username"
|
|
value={username}
|
|
onChange={(e) => setUsername(e.target.value)}
|
|
required
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="password">密码</Label>
|
|
<Input
|
|
id="password"
|
|
type="password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
required
|
|
/>
|
|
</div>
|
|
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
<Button type="submit" className="w-full">
|
|
本地账号登录
|
|
</Button>
|
|
</form>
|
|
|
|
{hasKeycloak && (
|
|
<>
|
|
<div className="relative">
|
|
<div className="absolute inset-0 flex items-center">
|
|
<span className="w-full border-t border-border" />
|
|
</div>
|
|
<div className="relative flex justify-center text-xs uppercase">
|
|
<span className="bg-popover px-2 text-muted-foreground">或者</span>
|
|
</div>
|
|
</div>
|
|
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
className="w-full"
|
|
onClick={() => signIn("keycloak", { callbackUrl })}
|
|
>
|
|
Keycloak 登录
|
|
</Button>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|