Files
NianAIGC/components/auth-login-panel.tsx
2026-06-04 12:14:13 +08:00

111 lines
3.8 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 { useEffect, useRef, useState } from "react";
import type { FormEvent } from "react";
import Image from "next/image";
import { Loader2, LogIn } from "lucide-react";
import { pulseFeedback, revealChildren, runScopedMotion } from "@/lib/ui/motion";
export function AuthLoginPanel({
next,
configured,
message,
missing
}: {
next: string;
configured: boolean;
message?: string | null;
missing?: string[];
}) {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const screenRef = useRef<HTMLDivElement | null>(null);
const feedbackRef = useRef<HTMLDivElement | null>(null);
const hasMissingConfig = !configured && Boolean(missing?.length);
useEffect(() => {
return runScopedMotion(screenRef, (scope) => revealChildren(scope));
}, []);
useEffect(() => {
pulseFeedback(feedbackRef.current);
}, [error, message, missing?.join(",")]);
async function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!configured || submitting) return;
setSubmitting(true);
setError(null);
try {
const payload: Record<string, string> = { username, password, next };
const response = await fetch("/api/auth/password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
const result = await response.json().catch(() => ({})) as { error?: string; redirectTo?: string };
if (!response.ok) throw new Error(result.error || "登录失败");
window.location.assign(result.redirectTo || next || "/create");
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
} finally {
setSubmitting(false);
}
}
return (
<div className="auth-page" ref={screenRef}>
<section className="auth-brand-panel" aria-label="智念AIGC平台" data-animate>
<Image className="auth-brand-logo" src="/logo/zhinian-logo.png" alt="" width={168} height={38} priority />
<h1>AIGC平台</h1>
</section>
<section className="panel auth-panel" data-animate>
<h2></h2>
{message || hasMissingConfig || error ? (
<div ref={feedbackRef}>
{message ? <div className="callout" role="alert">{message}</div> : null}
{hasMissingConfig ? (
<div className="callout" role="alert">
{missing?.join("、")}
</div>
) : null}
{error ? <div className="callout" role="alert">{error}</div> : null}
</div>
) : null}
<form className="auth-password-form" onSubmit={submit}>
<label className="field" data-animate>
<span></span>
<input
autoComplete="username"
value={username}
onChange={(event) => setUsername(event.target.value)}
disabled={!configured || submitting}
placeholder="请输入账号"
/>
</label>
<label className="field" data-animate>
<span></span>
<input
autoComplete="current-password"
type="password"
value={password}
onChange={(event) => setPassword(event.target.value)}
disabled={!configured || submitting}
placeholder="请输入密码"
/>
</label>
<button className="button primary auth-submit" type="submit" disabled={!configured || submitting || !username.trim() || !password} data-animate>
{submitting ? <Loader2 className="spin" size={18} /> : <LogIn size={18} />}
</button>
</form>
</section>
</div>
);
}