From caa658cabb9efb7d0511b40f698a2c9333b8d7d0 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sun, 25 Jan 2026 12:02:06 +0800 Subject: [PATCH] feat: refine registration UX and migrate middleware CORS --- frontend.log | Bin 0 -> 57438 bytes next.config.mjs | 14 + src/app/(auth)/register/RegisterContent.tsx | 952 ++++++++------------ src/i18n/translations.ts | 6 + src/middleware.ts | 50 - 5 files changed, 374 insertions(+), 648 deletions(-) create mode 100644 frontend.log delete mode 100644 src/middleware.ts diff --git a/frontend.log b/frontend.log new file mode 100644 index 0000000000000000000000000000000000000000..998053007d428e02ca6e17a4d799081234e2a2cd GIT binary patch literal 57438 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 literal 0 HcmV?d00001 diff --git a/next.config.mjs b/next.config.mjs index 1c7175a..df1417a 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -62,6 +62,20 @@ const nextConfig = { return config; }, + async headers() { + return [ + { + source: "/api/:path*", + headers: [ + { key: "Access-Control-Allow-Credentials", value: "true" }, + { key: "Access-Control-Allow-Origin", value: process.env.CORS_ALLOWED_ORIGINS || "https://console.svc.plus,http://localhost:3000" }, + { key: "Access-Control-Allow-Methods", value: "GET,POST,PUT,PATCH,DELETE,OPTIONS" }, + { key: "Access-Control-Allow-Headers", value: "Content-Type, Authorization, X-Requested-With, X-Account-Session" }, + ], + }, + ]; + }, + reactStrictMode: true, typedRoutes: false, turbopack: { diff --git a/src/app/(auth)/register/RegisterContent.tsx b/src/app/(auth)/register/RegisterContent.tsx index 215a341..162027c 100644 --- a/src/app/(auth)/register/RegisterContent.tsx +++ b/src/app/(auth)/register/RegisterContent.tsx @@ -28,6 +28,7 @@ const VERIFICATION_CODE_LENGTH = 6 const RESEND_COOLDOWN_SECONDS = 60 const EMAIL_PATTERN = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/ const PASSWORD_STRENGTH_PATTERN = /^(?=.*[A-Za-z])(?=.*\d).{8,}$/ +const USERNAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9]{3,15}$/ export default function RegisterContent() { const { language } = useLanguage() @@ -118,19 +119,22 @@ export default function RegisterContent() { const [alert, setAlert] = useState(initialAlert) const [isSubmitting, setIsSubmitting] = useState(false) + + // Wizard Step State: 0 = Info, 1 = Verification, 2 = Success (Processing/Redirecting) + const [currentStep, setCurrentStep] = useState<0 | 1 | 2>(0) + const [codeDigits, setCodeDigits] = useState(() => Array(VERIFICATION_CODE_LENGTH).fill('')) - const [hasRequestedCode, setHasRequestedCode] = useState(false) - const [pendingEmail, setPendingEmail] = useState('') - const [pendingPassword, setPendingPassword] = useState('') - const [isResending, setIsResending] = useState(false) const [resendCooldown, setResendCooldown] = useState(0) - const [isVerified, setIsVerified] = useState(false) + const [isResending, setIsResending] = useState(false) + const [formValues, setFormValues] = useState({ + username: '', email: '', password: '', confirmPassword: '', agreement: false, }) + const [isFormReady, setIsFormReady] = useState(false) const formRef = useRef(null) const codeInputRefs = useRef<(HTMLInputElement | null)[]>([]) @@ -168,7 +172,7 @@ export default function RegisterContent() { }, []) const handleInputChange = useCallback( - (field: 'email' | 'password' | 'confirmPassword') => + (field: 'username' | 'email' | 'password' | 'confirmPassword') => (event: ChangeEvent) => { const { value } = event.target setFormValues((previous) => ({ ...previous, [field]: value })) @@ -191,6 +195,13 @@ export default function RegisterContent() { if (sanitized && index < VERIFICATION_CODE_LENGTH - 1) { focusCodeInput(index + 1) + } else if (sanitized && index === VERIFICATION_CODE_LENGTH - 1) { + // Auto-submit when the last digit is entered + // We use a timeout to let the state update first + setTimeout(() => { + const form = formRef.current + if (form) form.requestSubmit() + }, 100) } }, [focusCodeInput], @@ -249,340 +260,239 @@ export default function RegisterContent() { [focusCodeInput], ) - const handleSubmit = useCallback( - async (event: FormEvent) => { - event.preventDefault() + const showError = (message: string) => { + setAlert({ type: 'error', message }) + } - if (isSubmitting) { - return - } + const showStatus = (message: string) => { + setAlert({ type: 'info', message }) + } - formRef.current = event.currentTarget + // Step 1: Request Verification Code + const handleRequestVerification = async () => { + const { username, email, password, confirmPassword, agreement } = formValues - const formData = new FormData(event.currentTarget) - const emailInput = String(formData.get('email') ?? '').trim() - const normalizedEmail = emailInput.toLowerCase() - const password = String(formData.get('password') ?? '') - const confirmPassword = String(formData.get('confirmPassword') ?? '') - const agreementAccepted = formData.get('agreement') === 'on' - const verificationCode = codeDigits.join('') + if (!username.trim() || !USERNAME_PATTERN.test(username.trim())) { + showError(alerts.invalidName ?? alerts.missingFields) + return + } - setFormValues((previous) => ({ - ...previous, - email: emailInput, - password, - confirmPassword, - agreement: agreementAccepted, - })) + if (!email || !EMAIL_PATTERN.test(email)) { + showError(alerts.invalidEmail) + return + } - const showError = (message: string) => { - setAlert({ type: 'error', message }) - } + if (!password || !confirmPassword) { + showError(alerts.missingFields) + return + } - const showStatus = (message: string) => { - setAlert({ type: 'info', message }) - } + if (!PASSWORD_STRENGTH_PATTERN.test(password)) { + showError(alerts.weakPassword ?? alerts.genericError) + return + } - if (!hasRequestedCode) { - if (!emailInput || !EMAIL_PATTERN.test(emailInput)) { - showError(alerts.invalidEmail) - return - } + if (password !== confirmPassword) { + showError(alerts.passwordMismatch) + return + } - if (!password || !confirmPassword) { - showError(alerts.missingFields) - return - } + if (!agreement) { + showError(alerts.agreementRequired ?? alerts.missingFields) + return + } - if (!PASSWORD_STRENGTH_PATTERN.test(password)) { - showError(alerts.weakPassword ?? alerts.genericError) - return - } + setIsSubmitting(true) + showStatus( + t.form.validation?.submitting ?? + t.form.submitting ?? + 'Submitting registration request…', + ) - if (password !== confirmPassword) { - showError(alerts.passwordMismatch) - return - } - - if (!agreementAccepted) { - showError(alerts.agreementRequired ?? alerts.missingFields) - return - } - - setIsSubmitting(true) - showStatus( - t.form.validation?.submitting ?? - t.form.submitting ?? - 'Submitting registration request…', - ) + try { + const response = await fetch('/api/auth/register/send', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email: email.trim() }), + }) + if (!response.ok) { + // ... (error handling) + let errorCode = 'generic_error' try { - const response = await fetch('/api/auth/register/send', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ email: emailInput }), - }) - - if (!response.ok) { - let errorCode = 'generic_error' - try { - const data = await response.json() - if (typeof data?.error === 'string') { - errorCode = data.error - } - } catch (error) { - console.error('Failed to parse verification send response', error) - } - - const errorMap: Record = { - invalid_request: alerts.genericError, - invalid_email: alerts.invalidEmail, - verification_failed: alerts.verificationFailed ?? alerts.genericError, - email_already_exists: alerts.userExists, - account_service_unreachable: alerts.genericError, - } - - showError(errorMap[normalize(errorCode)] ?? alerts.genericError) - setIsSubmitting(false) - return + const data = await response.json() + if (typeof data?.error === 'string') { + errorCode = data.error } - - setPendingEmail(normalizedEmail) - setPendingPassword(password) - setHasRequestedCode(true) - setIsVerified(false) - resetCodeDigits() - focusCodeInput(0) - setResendCooldown(RESEND_COOLDOWN_SECONDS) - - const successMessage = alerts.verificationSent ?? alerts.genericError - setAlert({ type: 'success', message: successMessage }) } catch (error) { - console.error('Failed to request verification code', error) - showError(alerts.genericError) - } finally { - setIsSubmitting(false) - } - return - } - - const emailForVerification = pendingEmail || normalizedEmail - if (!emailForVerification) { - showError(alerts.invalidEmail) - return - } - - if (!isVerified) { - if (verificationCode.length !== VERIFICATION_CODE_LENGTH) { - showError(alerts.codeRequired ?? alerts.invalidCode ?? alerts.missingFields) - return + console.error('Failed to parse verification send response', error) } - setIsSubmitting(true) - showStatus( - t.form.validation?.verifying ?? - t.form.verifying ?? - t.form.verifySubmit ?? - t.form.submit, - ) - - try { - const response = await fetch('/api/auth/register/verify', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ email: emailForVerification, code: verificationCode }), - }) - - if (!response.ok) { - let errorCode = 'generic_error' - try { - const data = await response.json() - if (typeof data?.error === 'string') { - errorCode = data.error - } - } catch (error) { - console.error('Failed to parse verification response', error) - } - - const errorMap: Record = { - invalid_request: alerts.genericError, - missing_verification: alerts.codeRequired ?? alerts.missingFields, - invalid_code: - alerts.verificationFailed ?? alerts.invalidCode ?? alerts.genericError, - verification_failed: alerts.verificationFailed ?? alerts.genericError, - account_service_unreachable: alerts.genericError, - } - - showError(errorMap[normalize(errorCode)] ?? alerts.genericError) - setIsSubmitting(false) - return - } - - setIsVerified(true) - const successMessage = alerts.verificationReady ?? alerts.success - setAlert({ type: 'success', message: successMessage }) - } catch (error) { - console.error('Failed to verify email', error) - showError(alerts.genericError) - } finally { - setIsSubmitting(false) + const errorMap: Record = { + invalid_request: alerts.genericError, + invalid_email: alerts.invalidEmail, + verification_failed: alerts.verificationFailed ?? alerts.genericError, + email_already_exists: alerts.userExists, + account_service_unreachable: alerts.genericError, } + + showError(errorMap[normalize(errorCode)] ?? alerts.genericError) return } - if (!pendingPassword) { - showError(alerts.genericError) - return - } + // Success: Move to Step 2 + setCurrentStep(1) + setResendCooldown(RESEND_COOLDOWN_SECONDS) + resetCodeDigits() - if (verificationCode.length !== VERIFICATION_CODE_LENGTH) { - showError(alerts.codeRequired ?? alerts.invalidCode ?? alerts.genericError) - return - } + const successMessage = alerts.verificationSent ?? alerts.genericError + setAlert({ type: 'success', message: successMessage }) - setIsSubmitting(true) - showStatus( - t.form.validation?.completing ?? - t.form.completing ?? - t.form.completeSubmit ?? - t.form.submit, - ) + // Focus code input after a short delay for state transition + setTimeout(() => focusCodeInput(0), 100) + } catch (error) { + console.error('Failed to request verification code', error) + showError(alerts.genericError) + } finally { + setIsSubmitting(false) + } + } + + // Step 2: Verify Code & Register + const handleCompleteRegistration = async () => { + const verificationCode = codeDigits.join('') + if (verificationCode.length !== VERIFICATION_CODE_LENGTH) { + showError(alerts.codeRequired ?? alerts.invalidCode ?? alerts.missingFields) + return + } + + setIsSubmitting(true) + showStatus( + t.form.validation?.completing ?? + t.form.completing ?? + t.form.completeSubmit ?? + t.form.submit, + ) + + try { + const { username, email, password } = formValues + + const registerResponse = await fetch('/api/auth/register', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: username.trim(), + email: email.trim(), + password, + code: verificationCode, + }), + }) + + let registerData: { success?: boolean; error?: string } | null = null try { - const registerResponse = await fetch('/api/auth/register', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - email: emailForVerification, - password: pendingPassword, - confirmPassword: pendingPassword, - code: verificationCode, - }), - }) + registerData = await registerResponse.json() + } catch (error) { + registerData = null + } - let registerData: { success?: boolean; error?: string } | null = null - try { - registerData = await registerResponse.json() - } catch (error) { - registerData = null + if (!registerResponse.ok || registerData?.success === false) { + // ... (error handling) + const errorCode = + typeof registerData?.error === 'string' ? registerData.error : 'registration_failed' + const errorMap: Record = { + invalid_request: alerts.genericError, + missing_credentials: alerts.missingFields, + invalid_email: alerts.invalidEmail, + password_too_short: alerts.weakPassword, + email_already_exists: alerts.userExists, + name_already_exists: alerts.usernameExists ?? alerts.userExists, + invalid_name: alerts.invalidName ?? alerts.genericError, + name_required: alerts.invalidName ?? alerts.genericError, + hash_failure: alerts.genericError, + user_creation_failed: alerts.genericError, + credentials_in_query: alerts.genericError, + verification_required: alerts.codeRequired ?? alerts.genericError, + invalid_code: + alerts.verificationFailed ?? alerts.invalidCode ?? alerts.genericError, + account_service_unreachable: alerts.genericError, } - if (!registerResponse.ok || registerData?.success === false) { - const errorCode = - typeof registerData?.error === 'string' ? registerData.error : 'registration_failed' - const errorMap: Record = { - invalid_request: alerts.genericError, - missing_credentials: alerts.missingFields, - invalid_email: alerts.invalidEmail, - password_too_short: alerts.weakPassword, - email_already_exists: alerts.userExists, - name_already_exists: alerts.usernameExists ?? alerts.userExists, - invalid_name: alerts.invalidName ?? alerts.genericError, - name_required: alerts.invalidName ?? alerts.genericError, - hash_failure: alerts.genericError, - user_creation_failed: alerts.genericError, - credentials_in_query: alerts.genericError, - verification_required: alerts.codeRequired ?? alerts.genericError, - invalid_code: - alerts.verificationFailed ?? alerts.invalidCode ?? alerts.genericError, - account_service_unreachable: alerts.genericError, - } + showError(errorMap[normalize(errorCode)] ?? alerts.genericError) + return + } - showError(errorMap[normalize(errorCode)] ?? alerts.genericError) - setIsSubmitting(false) - return - } + // 2. Login + const loginResponse = await fetch('/api/auth/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email: email.trim(), + password, + }), + }) - const loginResponse = await fetch('/api/auth/login', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - email: emailForVerification, - password: pendingPassword, - }), - }) - - let loginData: - | { success?: boolean; needMfa?: boolean; error?: string; redirectTo?: string } - | null = null - try { - loginData = await loginResponse.json() - } catch (error) { - loginData = null - } - - if (!loginResponse.ok || !loginData?.success) { - const errorCode = typeof loginData?.error === 'string' ? loginData.error : 'generic_error' - const errorMap: Record = { - invalid_credentials: alerts.genericError, - missing_credentials: alerts.missingFields, - account_service_unreachable: alerts.genericError, - authentication_failed: alerts.genericError, - } - - if (loginData?.needMfa) { - router.push('/login?needMfa=1') - router.refresh() - setIsSubmitting(false) - return - } - - showError(errorMap[normalize(errorCode)] ?? alerts.genericError) - setIsSubmitting(false) - return - } + let loginData: + | { success?: boolean; needMfa?: boolean; error?: string; redirectTo?: string } + | null = null + try { + loginData = await loginResponse.json() + } catch (error) { + loginData = null + } + if (!loginResponse.ok || !loginData?.success) { + // Login failed but registration succeeded const successMessage = alerts.registrationComplete ?? alerts.success setAlert({ type: 'success', message: successMessage }) - - router.push(loginData?.redirectTo || '/') - router.refresh() - } catch (error) { - console.error('Failed to complete registration', error) - showError(alerts.genericError) - } finally { - setIsSubmitting(false) + router.push('/login') + return } - }, - [ - alerts, - codeDigits, - focusCodeInput, - hasRequestedCode, - isSubmitting, - isVerified, - normalize, - pendingEmail, - pendingPassword, - resetCodeDigits, - router, - t.form, - ], - ) + + if (loginData?.needMfa) { + router.push('/login?needMfa=1') + router.refresh() + return + } + + // Success + setCurrentStep(2) + const successMessage = alerts.registrationComplete ?? alerts.success + setAlert({ type: 'success', message: successMessage }) + + router.push(loginData?.redirectTo || '/') + router.refresh() + + } catch (error) { + console.error('Failed to complete registration', error) + showError(alerts.genericError) + } finally { + setIsSubmitting(false) + } + } + + const handleSubmit = (event: FormEvent) => { + event.preventDefault() + if (isSubmitting) return + + if (currentStep === 0) { + handleRequestVerification() + } else if (currentStep === 1) { + handleCompleteRegistration() + } + } const handleResend = useCallback(async () => { - if (isResending || resendCooldown > 0 || isVerified) { - return - } + if (isResending || resendCooldown > 0) return - const emailFromFormRaw = - pendingEmail || - (formRef.current ? String(new FormData(formRef.current).get('email') ?? '').trim() : '') - - if (!emailFromFormRaw) { - setAlert({ type: 'error', message: alerts.invalidEmail }) - return - } - - const emailFromForm = emailFromFormRaw.trim() + const { email } = formValues + if (!email) return setIsResending(true) const resendStatusMessage = @@ -593,206 +503,47 @@ export default function RegisterContent() { try { const response = await fetch('/api/auth/register/send', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ email: emailFromForm }), + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: email.trim() }), }) if (!response.ok) { - let errorCode = 'generic_error' - try { - const data = await response.json() - if (typeof data?.error === 'string') { - errorCode = data.error - } - } catch (error) { - console.error('Failed to parse resend response', error) - } - - const errorMap: Record = { - invalid_request: alerts.genericError, - invalid_email: alerts.invalidEmail, - verification_failed: alerts.verificationFailed ?? alerts.genericError, - already_verified: alerts.verificationFailed ?? alerts.genericError, - account_service_unreachable: alerts.genericError, - email_already_exists: alerts.userExists, - } - - setAlert({ type: 'error', message: errorMap[normalize(errorCode)] ?? alerts.genericError }) - setIsResending(false) + setAlert({ type: 'error', message: alerts.genericError }) return } - setPendingEmail(emailFromForm.toLowerCase()) - setHasRequestedCode(true) - setIsVerified(false) - resetCodeDigits() - focusCodeInput(0) setResendCooldown(RESEND_COOLDOWN_SECONDS) const message = alerts.verificationResent ?? alerts.verificationSent ?? 'Verification code resent.' setAlert({ type: 'success', message }) - setIsResending(false) } catch (error) { - console.error('Failed to resend verification code', error) setAlert({ type: 'error', message: alerts.genericError }) + } finally { setIsResending(false) } - }, [ - alerts, - focusCodeInput, - isResending, - isVerified, - normalize, - pendingEmail, - resetCodeDigits, - resendCooldown, - t.form.verificationCodeResend, - t.form.verificationCodeResending, - ]) + }, [alerts, formValues, isResending, resendCooldown, t.form]) + + // Render Helpers const aboveForm = t.uuidNote ? (
{t.uuidNote}
) : null - const isVerificationStep = hasRequestedCode && !isVerified - const submitLabel = isVerified - ? isSubmitting - ? t.form.completing ?? t.form.completeSubmit ?? t.form.submit - : t.form.completeSubmit ?? t.form.submit - : isVerificationStep - ? isSubmitting - ? t.form.verifying ?? t.form.verifySubmit ?? t.form.submit - : t.form.verifySubmit ?? t.form.submit - : isSubmitting - ? t.form.submitting ?? t.form.submit - : t.form.submit + const submitLabel = useMemo(() => { + if (isSubmitting) { + if (currentStep === 0) return t.form.submitting ?? t.form.submit + return t.form.completing ?? t.form.submit + } + if (currentStep === 0) return '下一步 (获取验证码)' + return t.form.completeSubmit ?? '完成注册' + }, [isSubmitting, currentStep, t.form]) + const resendLabel = isResending ? t.form.verificationCodeResending ?? t.form.verificationCodeResend : resendCooldown > 0 ? `${t.form.verificationCodeResend} (${resendCooldown}s)` : t.form.verificationCodeResend - const verificationDescriptionId = useId() - const validationHints = t.form.validation - const validationState = useMemo(() => { - const messages: string[] = [] - - if (!isFormReady && validationHints?.initializing) { - return { disabled: true, messages: [validationHints.initializing] } - } - - if (isSubmitting) { - if (isVerified) { - messages.push( - validationHints?.completing ?? - t.form.completing ?? - t.form.completeSubmit ?? - t.form.submit, - ) - } else if (isVerificationStep) { - messages.push( - validationHints?.verifying ?? - t.form.verifying ?? - t.form.verifySubmit ?? - t.form.submit, - ) - } else { - messages.push(validationHints?.submitting ?? t.form.submitting ?? t.form.submit) - } - - return { disabled: true, messages } - } - - if (!hasRequestedCode) { - const emailValue = formValues.email.trim() - - if (!emailValue) { - messages.push(validationHints?.emailMissing ?? alerts.invalidEmail) - } else if (!EMAIL_PATTERN.test(emailValue)) { - messages.push(validationHints?.emailInvalid ?? alerts.invalidEmail) - } - - if (!formValues.password) { - messages.push(validationHints?.passwordMissing ?? alerts.missingFields) - } - - if (!formValues.confirmPassword) { - messages.push(validationHints?.confirmPasswordMissing ?? alerts.missingFields) - } - - if (formValues.password && !PASSWORD_STRENGTH_PATTERN.test(formValues.password)) { - messages.push(validationHints?.passwordWeak ?? alerts.weakPassword ?? alerts.genericError) - } - - if ( - formValues.password && - formValues.confirmPassword && - formValues.password !== formValues.confirmPassword - ) { - messages.push(validationHints?.passwordMismatch ?? alerts.passwordMismatch) - } - - if (!formValues.agreement) { - messages.push( - validationHints?.agreementRequired ?? alerts.agreementRequired ?? alerts.missingFields, - ) - } - - const uniqueMessages = Array.from(new Set(messages.filter(Boolean))) - return { disabled: uniqueMessages.length > 0, messages: uniqueMessages } - } - - if (!isVerified) { - if (codeDigits.some((digit) => !digit)) { - messages.push( - validationHints?.codeIncomplete ?? - alerts.codeRequired ?? - alerts.invalidCode ?? - alerts.missingFields, - ) - } - - const uniqueMessages = Array.from(new Set(messages.filter(Boolean))) - return { disabled: uniqueMessages.length > 0, messages: uniqueMessages } - } - - if (codeDigits.some((digit) => !digit)) { - messages.push( - validationHints?.codeIncomplete ?? - alerts.codeRequired ?? - alerts.invalidCode ?? - alerts.missingFields, - ) - } - - if (!pendingPassword) { - messages.push(validationHints?.passwordUnavailable ?? alerts.genericError) - } - - const uniqueMessages = Array.from(new Set(messages.filter(Boolean))) - return { disabled: uniqueMessages.length > 0, messages: uniqueMessages } - }, [ - alerts, - codeDigits, - formValues, - hasRequestedCode, - isFormReady, - isSubmitting, - isVerificationStep, - isVerified, - pendingPassword, - t.form.completeSubmit, - t.form.completing, - t.form.submit, - t.form.submitting, - t.form.verifySubmit, - t.form.verifying, - validationHints, - ]) - const isSubmitDisabled = validationState.disabled - const validationMessages = validationState.messages return ( -
- - -
-
-
- - -
-
- - -
-
-
- - {t.form.verificationCodeDescription ? ( -

- {t.form.verificationCodeDescription} -

- ) : null} - {hasRequestedCode && !isVerified ? ( -
-
- 我们已向你的邮箱发送一封验证邮件,点击邮件中的链接即可完成注册。 - 验证链接有效期 10 分钟。 -
- 若未收到邮件,请检查垃圾箱或稍后重试。 -
+ {currentStep === 0 && ( + <> +
+ + +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ + + + )} + + {currentStep === 1 && ( +
+
+ 我们已向你的邮箱 {formValues.email} 发送一封验证邮件。 +
+ 验证链接有效期 10 分钟。 +
+ +
+
{codeDigits.map((digit, index) => ( { codeInputRefs.current[index] = el }} - id={`verification-code-${index}`} - name={`verification-code-${index}`} type="text" inputMode="numeric" autoComplete="one-time-code" - pattern="\d{1}" maxLength={1} className="h-12 w-full rounded-xl border border-slate-200 bg-white/90 text-center text-lg font-semibold text-slate-900 shadow-sm transition focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200" value={digit} @@ -910,53 +686,33 @@ export default function RegisterContent() { /> ))}
- -
- -
- ) : null} -
- - {validationMessages.length > 0 ? ( -
-
    - {validationMessages.map((message) => ( -
  • {message}
  • - ))} -
+ +
+ + + +
- ) : null} + )} +