156 lines
5.0 KiB
TypeScript
156 lines
5.0 KiB
TypeScript
"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>
|
||
);
|
||
}
|