Files
helios-evcs/apps/web/app/login/page.tsx

156 lines
5.0 KiB
TypeScript
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 { Suspense, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { Alert, Button, Card, CloseButton, Input, Label, TextField } from "@heroui/react";
import { Fingerprint, Thunderbolt } from "@gravity-ui/icons";
import { authClient } from "@/lib/auth-client";
function LoginForm() {
const router = useRouter();
const searchParams = useSearchParams();
const justSetup = searchParams.get("setup") === "1";
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const [passkeyLoading, setPasskeyLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setLoading(true);
try {
const res = await authClient.signIn.username({
username,
password,
fetchOptions: { credentials: "include" },
});
if (res.error) {
setError(res.error.message ?? "登录失败,请检查用户名和密码");
} else {
router.push("/dashboard");
router.refresh();
}
} catch {
setError("网络错误,请稍后重试");
} finally {
setLoading(false);
}
};
const handlePasskey = async () => {
setError("");
setPasskeyLoading(true);
try {
const res = await authClient.signIn.passkey();
if (res?.error) {
setError(res.error.message ?? "Passkey 登录失败");
} else {
router.push("/dashboard");
router.refresh();
}
} catch {
setError("Passkey 登录失败,请重试");
} finally {
setPasskeyLoading(false);
}
};
return (
<div className="flex min-h-dvh flex-col items-center justify-center bg-background px-4">
{/* Brand */}
<div className="mb-8 flex flex-col items-center gap-3">
<div className="flex size-14 items-center justify-center rounded-2xl bg-accent shadow-lg">
<Thunderbolt className="size-7 text-accent-foreground" />
</div>
<div className="text-center">
<h1 className="text-2xl font-bold tracking-tight text-foreground">Helios EVCS</h1>
<p className="mt-1 text-sm text-muted"></p>
</div>
</div>
<Card className="w-full max-w-sm">
<Card.Content>
<form onSubmit={handleSubmit} className="space-y-4">
{justSetup && (
<Alert status="success">
<Alert.Indicator />
<Alert.Content>
<Alert.Title></Alert.Title>
<Alert.Description></Alert.Description>
</Alert.Content>
</Alert>
)}
<TextField fullWidth>
<Label className="text-sm font-medium"></Label>
<Input
required
autoComplete="username"
placeholder="admin"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</TextField>
<TextField fullWidth>
<Label className="text-sm font-medium"></Label>
<Input
required
autoComplete="current-password"
placeholder="••••••••"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</TextField>
{error && (
<Alert status="danger">
<Alert.Indicator />
<Alert.Content>
<Alert.Title></Alert.Title>
<Alert.Description>{error}</Alert.Description>
</Alert.Content>
<CloseButton onPress={() => setError("")} />
</Alert>
)}
<Button className="mt-2 w-full" isDisabled={loading} type="submit">
{loading ? "登录中…" : "登录"}
</Button>
<div className="relative flex items-center gap-3">
<div className="h-px flex-1 bg-border" />
<span className="text-xs text-muted"></span>
<div className="h-px flex-1 bg-border" />
</div>
<Button
className="w-full"
variant="secondary"
isDisabled={passkeyLoading}
type="button"
onPress={handlePasskey}
>
{passkeyLoading ? (
"验证中…"
) : (
<>
<Fingerprint className="size-4" />
使 Passkey
</>
)}
</Button>
</form>
</Card.Content>
</Card>
<p className="mt-6 text-xs text-muted/60">Helios EVCS</p>
</div>
);
}
export default function LoginPage() {
return (
<Suspense>
<LoginForm />
</Suspense>
);
}