feat(login): add password visibility toggle and confirm password field

This commit is contained in:
2025-11-18 11:15:09 +08:00
parent fd640631fc
commit fc0b7b2cc9
4 changed files with 77 additions and 4 deletions

View File

@@ -169,6 +169,7 @@ export interface RegisterFormData {
email: string; email: string;
password: string; password: string;
nickname: string; nickname: string;
confirmPassword: string;
} }
/** /**
@@ -315,7 +316,8 @@ export const useLoginForm = () => {
defaultValues: { defaultValues: {
email: '', email: '',
password: '', password: '',
nickname: '' nickname: '',
confirmPassword: ''
} }
}); });
@@ -395,6 +397,13 @@ export const useLoginForm = () => {
value: 8, value: 8,
message: t('setting.passwordMinLength') message: t('setting.passwordMinLength')
} }
},
confirmPassword: {
required: t('confirmPasswordRequired'),
validate: (value: string) => {
const pwd = registerForm.getValues('password');
return value === pwd || t('setting.passwordMismatch');
}
} }
} }
}; };

View File

@@ -258,6 +258,7 @@ export default {
registerDescription: 'Glad to have you on board!', registerDescription: 'Glad to have you on board!',
emailLabel: 'Email', emailLabel: 'Email',
emailPlaceholder: 'Please input email', emailPlaceholder: 'Please input email',
emailInvalid: 'Invalid email address',
passwordLabel: 'Password', passwordLabel: 'Password',
passwordPlaceholder: 'Please input password', passwordPlaceholder: 'Please input password',
rememberMe: 'Remember me', rememberMe: 'Remember me',

View File

@@ -240,6 +240,7 @@ export default {
registerDescription: '很高兴您加入!', registerDescription: '很高兴您加入!',
emailLabel: '邮箱', emailLabel: '邮箱',
emailPlaceholder: '请输入邮箱地址', emailPlaceholder: '请输入邮箱地址',
emailInvalid: '无效的邮箱地址',
passwordLabel: '密码', passwordLabel: '密码',
passwordPlaceholder: '请输入密码', passwordPlaceholder: '请输入密码',
rememberMe: '记住我', rememberMe: '记住我',

View File

@@ -1,4 +1,5 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import React, { useState } from 'react';
import { import {
Box, Box,
Button, Button,
@@ -14,13 +15,20 @@ import {
CardContent, CardContent,
Alert, Alert,
Tabs, Tabs,
Tab Tab,
InputAdornment,
IconButton,
} from '@mui/material'; } from '@mui/material';
import Visibility from '@mui/icons-material/Visibility';
import VisibilityOff from '@mui/icons-material/VisibilityOff';
import LanguageSwitcher from '../../components/LanguageSwitcher'; import LanguageSwitcher from '../../components/LanguageSwitcher';
import { useLoginPage } from '../../hooks/login-hooks'; import { useLoginPage } from '../../hooks/login-hooks';
const Login = () => { const Login = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const [showLoginPassword, setShowLoginPassword] = useState(false);
const [showRegisterPassword, setShowRegisterPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const { const {
// 表单状态 // 表单状态
tabValue, tabValue,
@@ -110,13 +118,27 @@ const Login = () => {
<TextField <TextField
fullWidth fullWidth
id="login-password" id="login-password"
type="password" type={showLoginPassword ? 'text' : 'password'}
placeholder={t('login.passwordPlaceholder')} placeholder={t('login.passwordPlaceholder')}
autoComplete="current-password" autoComplete="current-password"
{...loginForm.register('password', loginValidation.password)} {...loginForm.register('password', loginValidation.password)}
error={!!loginForm.formState.errors.password} error={!!loginForm.formState.errors.password}
helperText={loginForm.formState.errors.password?.message} helperText={loginForm.formState.errors.password?.message}
sx={{ mb: 2 }} sx={{ mb: 2 }}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label={showLoginPassword ? 'Hide password' : 'Show password'}
onClick={() => setShowLoginPassword((v) => !v)}
onMouseDown={(e) => e.preventDefault()}
edge="end"
>
{showLoginPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
/> />
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
@@ -175,13 +197,53 @@ const Login = () => {
<TextField <TextField
fullWidth fullWidth
id="register-password" id="register-password"
type="password" type={showRegisterPassword ? 'text' : 'password'}
placeholder={t('login.passwordPlaceholder')} placeholder={t('login.passwordPlaceholder')}
autoComplete="new-password" autoComplete="new-password"
{...registerForm.register('password', registerValidation.password)} {...registerForm.register('password', registerValidation.password)}
error={!!registerForm.formState.errors.password} error={!!registerForm.formState.errors.password}
helperText={registerForm.formState.errors.password?.message} helperText={registerForm.formState.errors.password?.message}
sx={{ mb: 2 }} sx={{ mb: 2 }}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label={showRegisterPassword ? 'Hide password' : 'Show password'}
onClick={() => setShowRegisterPassword((v) => !v)}
onMouseDown={(e) => e.preventDefault()}
edge="end"
>
{showRegisterPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
/>
<TextField
fullWidth
id="register-confirm-password"
type={showConfirmPassword ? 'text' : 'password'}
placeholder={t('confirmPassword')}
autoComplete="new-password"
{...registerForm.register('confirmPassword', registerValidation.confirmPassword)}
error={!!registerForm.formState.errors.confirmPassword}
helperText={registerForm.formState.errors.confirmPassword?.message}
sx={{ mb: 2 }}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label={showConfirmPassword ? 'Hide password' : 'Show password'}
onClick={() => setShowConfirmPassword((v) => !v)}
onMouseDown={(e) => e.preventDefault()}
edge="end"
>
{showConfirmPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
/> />
<Button <Button