156 lines
4.7 KiB
TypeScript
156 lines
4.7 KiB
TypeScript
"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>
|
||
);
|
||
}
|