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

148 lines
5.6 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";
import { useBrowserAuth } from "@/components/browser-auth";
import { safeBrowserNext } from "@/lib/client/browser-auth";
export function AuthLoginPanel({
title = "账户登录",
submitLabel = "登录"
}: {
title?: string;
submitLabel?: string;
}) {
const auth = useBrowserAuth();
const [phone, setPhone] = useState("");
const [password, setPassword] = useState("");
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [next, setNext] = useState("/create");
const [message, setMessage] = useState<string | null>(null);
const screenRef = useRef<HTMLDivElement | null>(null);
const feedbackRef = useRef<HTMLDivElement | null>(null);
const configured = auth.status === "ready" && auth.authConfigured;
const hasMissingConfig = auth.status === "ready" && auth.authRequired && !auth.authConfigured;
useEffect(() => {
return runScopedMotion(screenRef, (scope) => revealChildren(scope));
}, []);
useEffect(() => {
const params = new URLSearchParams(window.location.search);
setNext(safeBrowserNext(params.get("next")));
const errorCode = params.get("error");
if (errorCode) setMessage(errorMessages[errorCode] || "登录失败,请重新登录。");
else if (params.get("loggedOut") === "1") setMessage("已退出登录。");
}, []);
useEffect(() => {
if (auth.status === "ready" && auth.user) window.location.replace(next);
}, [auth.status, auth.user, next]);
useEffect(() => {
pulseFeedback(feedbackRef.current);
}, [error, message, hasMissingConfig]);
async function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!configured || submitting) return;
const formData = new FormData(event.currentTarget);
const submittedPhone = formStringValue(formData.get("phone"));
const submittedPassword = formStringValue(formData.get("password"));
if (!submittedPhone || !submittedPassword) {
setError("手机号和密码不能为空。");
return;
}
setSubmitting(true);
setError(null);
try {
const payload: Record<string, string> = { phone: submittedPhone, password: submittedPassword, 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(safeBrowserNext(result.redirectTo, next));
} 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>{title}</h2>
{message || hasMissingConfig || auth.status === "error" || error ? (
<div ref={feedbackRef}>
{message ? <div className="callout" role="alert">{message}</div> : null}
{hasMissingConfig ? (
<div className="callout" role="alert">
账号认证配置不完整,请联系管理员。
</div>
) : null}
{auth.status === "error" ? <div className="callout" role="alert">{auth.error || "无法读取认证状态。"}</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"
inputMode="tel"
name="phone"
required
value={phone}
onChange={(event) => setPhone(event.target.value)}
disabled={!configured || submitting}
placeholder="请输入手机号"
/>
</label>
<label className="field" data-animate>
<span>密码</span>
<input
autoComplete="current-password"
name="password"
required
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} data-animate>
{submitting ? <Loader2 className="spin" size={18} /> : <LogIn size={18} />}
{submitLabel}
</button>
</form>
</section>
</div>
);
}
function formStringValue(value: FormDataEntryValue | null): string {
return typeof value === "string" ? value.trim() : "";
}
const errorMessages: Record<string, string> = {
auth_not_configured: "账号认证配置不完整,请联系管理员。",
callback_failed: "登录失败,请重新登录。",
state_invalid: "登录状态已失效,请重新登录。"
};