Files
cozsweet-frontend-nextjs/src/app/auth/components/email-register-form.tsx
T

156 lines
4.7 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";
/**
* 邮箱注册表单
*
*
*
* 视觉规格(与 Dart 对齐):
* - 4 字段:username / email / password / confirmPassword(顺序与 Dart 一致)
* - 字段无 label(仅 placeholder
* - 错误仅以红色横幅显示(AuthErrorMessage),无字段级错误
* - 切换链接由父级 AuthEmailPanel 渲染
* - 提交派发 `AuthEmailRegisterSubmitted`
*/
import { useState } from "react";
import { useAuthDispatch } from "@/stores/auth/auth-context";
import {
validateConfirmPassword,
validateEmail,
validatePassword,
validateUsername,
} from "@/app/auth/components/auth-validators";
import { AuthTextField } from "./auth-text-field";
import { AuthPrimaryButton } from "./auth-primary-button";
import { AuthErrorMessage } from "./auth-error-message";
import { Logger } from "@/utils";
const log = new Logger("AppAuthComponentsEmailRegisterForm");
export interface EmailRegisterFormProps {
globalError?: string | null;
isLoading?: boolean;
}
export function EmailRegisterForm({
globalError,
isLoading,
}: EmailRegisterFormProps) {
const dispatch = useAuthDispatch();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [username, setUsername] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
// 本地校验错误(client-side)—— 校验失败时展示第一个非 null 的错误
// 优先级:local validationError > server globalError
const [validationError, setValidationError] = useState<string | null>(null);
// 用户在任一字段开始输入 → 清掉旧校验错误(标准 UX:边改边消)
// 直接在 onChange 调 setState —— React useState 对相同值自动 bail-out 不触发额外渲染
// 避免 useEffect 内 setState 的"级联渲染"反模式
const clearValidationOnType = () => setValidationError(null);
const onUsernameChange = (v: string) => {
setUsername(v);
clearValidationOnType();
};
const onEmailChange = (v: string) => {
setEmail(v);
clearValidationOnType();
};
const onPasswordChange = (v: string) => {
setPassword(v);
clearValidationOnType();
};
const onConfirmPasswordChange = (v: string) => {
setConfirmPassword(v);
clearValidationOnType();
};
const submit = () => {
log.debug("[email-register-form] submit ENTRY", {
emailLength: email.length,
emailPreview: email.slice(0, 8),
passwordLength: password.length,
usernameLength: username.length,
confirmPasswordLength: confirmPassword.length,
});
const emailErr = validateEmail(email);
const passwordErr = validatePassword(password);
const usernameErr = validateUsername(username);
const confirmErr = validateConfirmPassword(password, confirmPassword);
if (emailErr || passwordErr || usernameErr || confirmErr) {
// 按 username → email → password → confirm 顺序挑第一个错(与字段顺序一致)
const firstErr =
usernameErr ?? emailErr ?? passwordErr ?? confirmErr ?? null;
log.warn(
"[email-register-form] submit VALIDATION FAILED (silent return)",
{
emailErr,
passwordErr,
usernameErr,
confirmErr,
surfaced: firstErr,
},
);
setValidationError(firstErr);
return;
}
setValidationError(null);
log.debug(
"[email-register-form] submit dispatching AuthEmailRegisterSubmitted",
);
dispatch({
type: "AuthEmailRegisterSubmitted",
email,
password,
username,
confirmPassword,
});
};
return (
<form
className="flex w-full flex-col gap-md"
onSubmit={(e) => {
e.preventDefault();
submit();
}}
>
<AuthTextField
value={username}
onChange={onUsernameChange}
placeholder="Username"
autoComplete="username"
/>
<AuthTextField
type="email"
value={email}
onChange={onEmailChange}
placeholder="Email"
autoComplete="email"
/>
<AuthTextField
type="password"
value={password}
onChange={onPasswordChange}
placeholder="Password"
autoComplete="new-password"
/>
<AuthTextField
type="password"
value={confirmPassword}
onChange={onConfirmPasswordChange}
placeholder="Confirm Password"
autoComplete="new-password"
onSubmit={submit}
/>
{/* 优先级:本地校验错误 > 后端 globalError */}
<AuthErrorMessage message={validationError ?? globalError ?? null} />
<AuthPrimaryButton type="submit" isLoading={isLoading}>
Register
</AuthPrimaryButton>
</form>
);
}