131 lines
4.5 KiB
TypeScript
131 lines
4.5 KiB
TypeScript
"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,
|
|
title = "账户登录",
|
|
submitLabel = "登录"
|
|
}: {
|
|
next: string;
|
|
configured: boolean;
|
|
message?: string | null;
|
|
missing?: string[];
|
|
title?: string;
|
|
submitLabel?: string;
|
|
}) {
|
|
const [phone, setPhone] = 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;
|
|
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(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>{title}</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"
|
|
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() : "";
|
|
}
|