Files
NianAIGC/components/auth-login-panel.tsx

151 lines
5.2 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, useMemo, useRef, useState } from "react";
import type { FormEvent } from "react";
import Image from "next/image";
import { Loader2, LogIn, RefreshCw } 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 [code, setCode] = useState("");
const [randomStr, setRandomStr] = 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(",")]);
const captchaSrc = useMemo(() => {
if (!randomStr) return "";
return `/api/auth/captcha?randomStr=${encodeURIComponent(randomStr)}`;
}, [randomStr]);
function refreshCaptcha() {
setCode("");
setRandomStr(crypto.randomUUID());
}
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 };
if (code.trim() && randomStr) {
payload.code = code.trim();
payload.randomStr = randomStr;
}
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));
refreshCaptcha();
} 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>
<label className="field" data-animate>
<span></span>
<div className="auth-captcha-row">
<input
autoComplete="off"
value={code}
onChange={(event) => setCode(event.target.value)}
disabled={!configured || submitting}
placeholder="需要时填写"
/>
<button
className="auth-captcha-button"
type="button"
onClick={refreshCaptcha}
disabled={!configured || submitting}
aria-label="刷新验证码"
title="刷新验证码"
>
{captchaSrc ? <img src={captchaSrc} alt="验证码" /> : <RefreshCw size={18} />}
</button>
</div>
</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>
);
}