648 lines
23 KiB
TypeScript
648 lines
23 KiB
TypeScript
/**
|
||
* filekorolheader: 账户安全页面 - 用户账户安全设置管理
|
||
* 功能:密码修改、安全验证、账户信息展示、登录记录查看
|
||
* 路径:/central-config/personal-center/account-security
|
||
* 规范:遵循crop-x-new/docs/开发项目规范.md,使用useReducer状态管理,集成真实用户数据
|
||
*/
|
||
'use client';
|
||
|
||
import { useReducer, useEffect } from 'react';
|
||
import { useAuthStore } from '@/stores/modules/auth';
|
||
import { Card } from '@/components/ui/card';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Input } from '@/components/ui/input';
|
||
import { Label } from '@/components/ui/label';
|
||
import { Badge } from '@/components/ui/badge';
|
||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
|
||
import { Shield, Lock, Key, CheckCircle, XCircle, Eye, EyeOff, AlertTriangle } from 'lucide-react';
|
||
import { toast } from 'sonner';
|
||
|
||
// Types
|
||
interface PasswordForm {
|
||
oldPassword: string;
|
||
newPassword: string;
|
||
confirmPassword: string;
|
||
}
|
||
|
||
interface SecurityState {
|
||
user: {
|
||
username: string;
|
||
phone: string;
|
||
email: string;
|
||
lastLoginTime: string;
|
||
lastLoginDevice: string;
|
||
lastLoginIp: string;
|
||
};
|
||
showPasswordDialog: boolean;
|
||
isLoading: boolean;
|
||
showOldPassword: boolean;
|
||
showNewPassword: boolean;
|
||
showConfirmPassword: boolean;
|
||
passwordForm: PasswordForm;
|
||
passwordStrength: {
|
||
checks: {
|
||
length: boolean;
|
||
hasUpper: boolean;
|
||
hasLower: boolean;
|
||
hasNumber: boolean;
|
||
hasSpecial: boolean;
|
||
};
|
||
strength: 'weak' | 'medium' | 'strong';
|
||
passedCount: number;
|
||
};
|
||
}
|
||
|
||
type SecurityAction =
|
||
| { type: 'TOGGLE_PASSWORD_DIALOG'; payload: boolean }
|
||
| { type: 'TOGGLE_OLD_PASSWORD_VISIBILITY' }
|
||
| { type: 'TOGGLE_NEW_PASSWORD_VISIBILITY' }
|
||
| { type: 'TOGGLE_CONFIRM_PASSWORD_VISIBILITY' }
|
||
| { type: 'UPDATE_PASSWORD_FORM'; payload: Partial<PasswordForm> }
|
||
| { type: 'UPDATE_PASSWORD_STRENGTH'; payload: SecurityState['passwordStrength'] }
|
||
| { type: 'UPDATE_USER_DATA'; payload: SecurityState['user'] }
|
||
| { type: 'SET_LOADING'; payload: boolean };
|
||
|
||
// Initial state
|
||
const initialState: SecurityState = {
|
||
user: {
|
||
username: '',
|
||
phone: '',
|
||
email: '',
|
||
lastLoginTime: '',
|
||
lastLoginDevice: '',
|
||
lastLoginIp: ''
|
||
},
|
||
showPasswordDialog: false,
|
||
isLoading: true,
|
||
showOldPassword: false,
|
||
showNewPassword: false,
|
||
showConfirmPassword: false,
|
||
passwordForm: {
|
||
oldPassword: '',
|
||
newPassword: '',
|
||
confirmPassword: ''
|
||
},
|
||
passwordStrength: {
|
||
checks: {
|
||
length: false,
|
||
hasUpper: false,
|
||
hasLower: false,
|
||
hasNumber: false,
|
||
hasSpecial: false
|
||
},
|
||
strength: 'weak',
|
||
passedCount: 0
|
||
}
|
||
};
|
||
|
||
// Reducer
|
||
function securityReducer(state: SecurityState, action: SecurityAction): SecurityState {
|
||
switch (action.type) {
|
||
case 'TOGGLE_PASSWORD_DIALOG':
|
||
return {
|
||
...state,
|
||
showPasswordDialog: action.payload,
|
||
...(action.payload === false ? {
|
||
passwordForm: {
|
||
oldPassword: '',
|
||
newPassword: '',
|
||
confirmPassword: ''
|
||
},
|
||
passwordStrength: {
|
||
checks: {
|
||
length: false,
|
||
hasUpper: false,
|
||
hasLower: false,
|
||
hasNumber: false,
|
||
hasSpecial: false
|
||
},
|
||
strength: 'weak' as const,
|
||
passedCount: 0
|
||
}
|
||
} : {})
|
||
};
|
||
|
||
case 'TOGGLE_OLD_PASSWORD_VISIBILITY':
|
||
return { ...state, showOldPassword: !state.showOldPassword };
|
||
|
||
case 'TOGGLE_NEW_PASSWORD_VISIBILITY':
|
||
return { ...state, showNewPassword: !state.showNewPassword };
|
||
|
||
case 'TOGGLE_CONFIRM_PASSWORD_VISIBILITY':
|
||
return { ...state, showConfirmPassword: !state.showConfirmPassword };
|
||
|
||
case 'UPDATE_PASSWORD_FORM':
|
||
return {
|
||
...state,
|
||
passwordForm: { ...state.passwordForm, ...action.payload }
|
||
};
|
||
|
||
case 'UPDATE_PASSWORD_STRENGTH':
|
||
return {
|
||
...state,
|
||
passwordStrength: action.payload
|
||
};
|
||
|
||
case 'UPDATE_USER_DATA':
|
||
return {
|
||
...state,
|
||
user: action.payload
|
||
};
|
||
|
||
case 'SET_LOADING':
|
||
return {
|
||
...state,
|
||
isLoading: action.payload
|
||
};
|
||
|
||
default:
|
||
return state;
|
||
}
|
||
}
|
||
|
||
// Password strength checker
|
||
const checkPasswordStrength = (password: string): SecurityState['passwordStrength'] => {
|
||
const checks = {
|
||
length: password.length >= 8,
|
||
hasUpper: /[A-Z]/.test(password),
|
||
hasLower: /[a-z]/.test(password),
|
||
hasNumber: /[0-9]/.test(password),
|
||
hasSpecial: /[!@#$%^&*(),.?":{}|<>]/.test(password),
|
||
};
|
||
|
||
const passedCount = Object.values(checks).filter(Boolean).length;
|
||
|
||
let strength: 'weak' | 'medium' | 'strong' = 'weak';
|
||
if (passedCount >= 4) strength = 'strong';
|
||
else if (passedCount >= 3) strength = 'medium';
|
||
|
||
return { checks, strength, passedCount };
|
||
};
|
||
|
||
// Utility functions
|
||
const getStrengthColor = (strength: 'weak' | 'medium' | 'strong') => {
|
||
switch (strength) {
|
||
case 'strong':
|
||
return 'text-green-600';
|
||
case 'medium':
|
||
return 'text-yellow-600';
|
||
case 'weak':
|
||
return 'text-red-600';
|
||
}
|
||
};
|
||
|
||
const getStrengthBg = (strength: 'weak' | 'medium' | 'strong') => {
|
||
switch (strength) {
|
||
case 'strong':
|
||
return 'bg-green-100';
|
||
case 'medium':
|
||
return 'bg-yellow-100';
|
||
case 'weak':
|
||
return 'bg-red-100';
|
||
}
|
||
};
|
||
|
||
const getStrengthText = (strength: 'weak' | 'medium' | 'strong') => {
|
||
switch (strength) {
|
||
case 'strong':
|
||
return '强';
|
||
case 'medium':
|
||
return '中';
|
||
case 'weak':
|
||
return '弱';
|
||
}
|
||
};
|
||
|
||
export default function AccountSecurity() {
|
||
const [state, dispatch] = useReducer(securityReducer, initialState);
|
||
const { getAuthUser } = useAuthStore();
|
||
|
||
// 加载用户数据
|
||
useEffect(() => {
|
||
const loadUserData = () => {
|
||
const authUser = getAuthUser();
|
||
if (authUser) {
|
||
// 格式化最后登录时间
|
||
const formatLastLoginTime = (loginTime: string) => {
|
||
try {
|
||
const date = new Date(loginTime);
|
||
return date.toLocaleString('zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit'
|
||
});
|
||
} catch {
|
||
return loginTime;
|
||
}
|
||
};
|
||
|
||
// 更新用户数据到状态
|
||
dispatch({
|
||
type: 'UPDATE_USER_DATA',
|
||
payload: {
|
||
username: authUser.username || '',
|
||
phone: authUser.phone || '',
|
||
email: authUser.email || '',
|
||
lastLoginTime: authUser.last_login_at ? formatLastLoginTime(authUser.last_login_at) : '',
|
||
lastLoginDevice: 'Web Browser', // 可以从user-agent或其他地方获取
|
||
lastLoginIp: '192.168.1.100' // 可以从登录记录中获取
|
||
}
|
||
});
|
||
}
|
||
|
||
// 数据加载完成,设置加载状态为false
|
||
dispatch({ type: 'SET_LOADING', payload: false });
|
||
};
|
||
|
||
loadUserData();
|
||
}, [getAuthUser]);
|
||
|
||
const handleChangePassword = () => {
|
||
dispatch({ type: 'UPDATE_PASSWORD_FORM', payload: { oldPassword: '', newPassword: '', confirmPassword: '' } });
|
||
dispatch({ type: 'TOGGLE_PASSWORD_DIALOG', payload: true });
|
||
};
|
||
|
||
const handleConfirmChangePassword = () => {
|
||
const { passwordForm } = state;
|
||
|
||
// Validation
|
||
if (!passwordForm.oldPassword) {
|
||
toast.error('请输入原密码');
|
||
return;
|
||
}
|
||
|
||
if (!passwordForm.newPassword) {
|
||
toast.error('请输入新密码');
|
||
return;
|
||
}
|
||
|
||
if (passwordForm.newPassword.length < 8) {
|
||
toast.error('密码长度至少为8位');
|
||
return;
|
||
}
|
||
|
||
if (state.passwordStrength.strength === 'weak') {
|
||
toast.error('密码强度过弱,请使用更复杂的密码');
|
||
return;
|
||
}
|
||
|
||
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
|
||
toast.error('两次输入的密码不一致');
|
||
return;
|
||
}
|
||
|
||
if (passwordForm.oldPassword === passwordForm.newPassword) {
|
||
toast.error('新密码不能与原密码相同');
|
||
return;
|
||
}
|
||
|
||
// Mock password validation (assuming old password is "123456")
|
||
if (passwordForm.oldPassword !== '123456') {
|
||
toast.error('原密码错误');
|
||
return;
|
||
}
|
||
|
||
// Success
|
||
toast.success('密码修改成功,请使用新密码重新登录');
|
||
dispatch({ type: 'TOGGLE_PASSWORD_DIALOG', payload: false });
|
||
};
|
||
|
||
const updatePassword = (field: keyof PasswordForm, value: string) => {
|
||
const updatePayload = {
|
||
[field]: value || ''
|
||
};
|
||
dispatch({ type: 'UPDATE_PASSWORD_FORM', payload: updatePayload });
|
||
|
||
if (field === 'newPassword') {
|
||
const strength = checkPasswordStrength(value || '');
|
||
dispatch({ type: 'UPDATE_PASSWORD_STRENGTH', payload: strength });
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* Loading State */}
|
||
{state.isLoading && (
|
||
<Card className="p-6">
|
||
<div className="flex items-center justify-center py-8">
|
||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||
<span className="ml-3 text-muted-foreground">加载用户数据中...</span>
|
||
</div>
|
||
</Card>
|
||
)}
|
||
|
||
{/* Page Header */}
|
||
{!state.isLoading && (
|
||
<Card className="p-6 bg-gradient-to-r from-blue-50 to-indigo-50 border-blue-200">
|
||
<div className="flex items-start gap-3">
|
||
<Shield className="w-6 h-6 text-blue-600 flex-shrink-0 mt-1" />
|
||
<div className="flex-1">
|
||
<h2 className="text-green-800 mb-2">账户安全</h2>
|
||
<p className="text-sm text-muted-foreground mb-3">
|
||
管理您的账户安全设置,包括密码修改、安全验证等,确保账户安全
|
||
</p>
|
||
<div className="flex flex-wrap gap-2">
|
||
<Badge variant="outline" className="bg-white">
|
||
<Lock className="w-3 h-3 mr-1" />
|
||
密码管理
|
||
</Badge>
|
||
<Badge variant="outline" className="bg-white">
|
||
<Key className="w-3 h-3 mr-1" />
|
||
安全验证
|
||
</Badge>
|
||
<Badge variant="outline" className="bg-white">
|
||
<Shield className="w-3 h-3 mr-1" />
|
||
隐私保护
|
||
</Badge>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
)}
|
||
|
||
{/* Account Overview */}
|
||
{!state.isLoading && (
|
||
<Card className="p-6">
|
||
<h3 className="mb-4">账户信息</h3>
|
||
<div className="grid grid-cols-2 gap-6">
|
||
<div className="p-4 bg-muted rounded-lg">
|
||
<div className="text-sm text-muted-foreground mb-2">用户名</div>
|
||
<div className="font-medium">
|
||
{state.user.username || (state.isLoading ? '加载中...' : '未设置')}
|
||
</div>
|
||
</div>
|
||
<div className="p-4 bg-muted rounded-lg">
|
||
<div className="text-sm text-muted-foreground mb-2">手机号</div>
|
||
<div className="font-medium">
|
||
{state.user.phone || (state.isLoading ? '加载中...' : '未绑定')}
|
||
</div>
|
||
</div>
|
||
<div className="p-4 bg-muted rounded-lg">
|
||
<div className="text-sm text-muted-foreground mb-2">邮箱</div>
|
||
<div className="font-medium">
|
||
{state.user.email || (state.isLoading ? '加载中...' : '未设置')}
|
||
</div>
|
||
</div>
|
||
<div className="p-4 bg-muted rounded-lg">
|
||
<div className="text-sm text-muted-foreground mb-2">最后登录时间</div>
|
||
<div className="font-medium text-sm">
|
||
{state.user.lastLoginTime || (state.isLoading ? '加载中...' : '暂无登录记录')}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
)}
|
||
|
||
{/* Password Management */}
|
||
{!state.isLoading && (
|
||
<Card className="p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div>
|
||
<h3 className="mb-1">登录密码</h3>
|
||
<p className="text-sm text-muted-foreground">定期修改密码可以提高账户安全性</p>
|
||
</div>
|
||
<Button onClick={handleChangePassword} className="gap-2">
|
||
<Key className="w-4 h-4" />
|
||
修改密码
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||
<div className="flex items-start gap-3">
|
||
<Shield className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
|
||
<div className="flex-1">
|
||
<h4 className="text-sm font-medium text-blue-900 mb-2">密码安全提示</h4>
|
||
<ul className="text-sm text-blue-800 space-y-1">
|
||
<li>• 密码长度至少8位</li>
|
||
<li>• 包含大小写字母、数字和特殊字符</li>
|
||
<li>• 不要使用过于简单的密码</li>
|
||
<li>• 定期更换密码(建议3个月更换一次)</li>
|
||
<li>• 不要在多个平台使用相同密码</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
)}
|
||
|
||
{/* Security Settings */}
|
||
{!state.isLoading && (
|
||
<Card className="p-6">
|
||
<h3 className="mb-4">安全设置</h3>
|
||
<div className="space-y-4">
|
||
<div className="flex items-center justify-between p-4 bg-muted rounded-lg">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
|
||
<CheckCircle className="w-5 h-5 text-green-600" />
|
||
</div>
|
||
<div>
|
||
<div className="font-medium">手机验证</div>
|
||
<div className="text-sm text-muted-foreground">
|
||
已绑定手机号:{state.user.phone || '未绑定'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<Badge className="bg-green-100 text-green-700">已启用</Badge>
|
||
</div>
|
||
|
||
<div className="flex items-center justify-between p-4 bg-muted rounded-lg">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
|
||
<Lock className="w-5 h-5 text-blue-600" />
|
||
</div>
|
||
<div>
|
||
<div className="font-medium">登录保护</div>
|
||
<div className="text-sm text-muted-foreground">异常登录时需要额外验证</div>
|
||
</div>
|
||
</div>
|
||
<Badge className="bg-blue-100 text-blue-700">已启用</Badge>
|
||
</div>
|
||
|
||
<div className="flex items-center justify-between p-4 bg-muted rounded-lg">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-10 h-10 bg-yellow-100 rounded-full flex items-center justify-center">
|
||
<AlertTriangle className="w-5 h-5 text-yellow-600" />
|
||
</div>
|
||
<div>
|
||
<div className="font-medium">邮箱验证</div>
|
||
<div className="text-sm text-muted-foreground">
|
||
已绑定邮箱:{state.user.email || '未设置'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<Badge className="bg-green-100 text-green-700">已启用</Badge>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
)}
|
||
|
||
{/* Recent Login Records */}
|
||
{!state.isLoading && (
|
||
<Card className="p-6">
|
||
<h3 className="mb-4">最近登录记录</h3>
|
||
<div className="space-y-3">
|
||
<div className="flex items-center justify-between p-3 bg-muted rounded-lg">
|
||
<div className="flex items-center gap-3">
|
||
<CheckCircle className="w-5 h-5 text-green-600" />
|
||
<div>
|
||
<div className="text-sm font-medium">当前会话</div>
|
||
<div className="text-xs text-muted-foreground">
|
||
{state.user.lastLoginDevice || 'Web Browser'} · {state.user.lastLoginIp || '局域网'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="text-sm text-muted-foreground">
|
||
{state.user.lastLoginTime || '首次登录'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
)} {/* End of loading state check */}
|
||
|
||
{/* Change Password Dialog - always visible regardless of loading state */}
|
||
<Dialog open={state.showPasswordDialog} onOpenChange={(open) => dispatch({ type: 'TOGGLE_PASSWORD_DIALOG', payload: open })}>
|
||
<DialogContent className="max-w-lg">
|
||
<DialogHeader>
|
||
<DialogTitle>
|
||
<div className="flex items-center gap-2">
|
||
<Lock className="w-5 h-5 text-blue-600" />
|
||
修改登录密码
|
||
</div>
|
||
</DialogTitle>
|
||
<DialogDescription>
|
||
为了您的账户安全,请定期更换密码并设置复杂密码
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
<div className="space-y-4">
|
||
{/* Old Password */}
|
||
<div>
|
||
<Label>原密码 *</Label>
|
||
<div className="relative mt-2">
|
||
<Input
|
||
type={state.showOldPassword ? 'text' : 'password'}
|
||
value={state.passwordForm.oldPassword}
|
||
onChange={(e) => updatePassword('oldPassword', e.target.value)}
|
||
placeholder="请输入原密码"
|
||
className="pr-10"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => dispatch({ type: 'TOGGLE_OLD_PASSWORD_VISIBILITY' })}
|
||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||
>
|
||
{state.showOldPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* New Password */}
|
||
<div>
|
||
<Label>新密码 *</Label>
|
||
<div className="relative mt-2">
|
||
<Input
|
||
type={state.showNewPassword ? 'text' : 'password'}
|
||
value={state.passwordForm.newPassword}
|
||
onChange={(e) => updatePassword('newPassword', e.target.value)}
|
||
placeholder="请输入新密码(至少8位)"
|
||
className="pr-10"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => dispatch({ type: 'TOGGLE_NEW_PASSWORD_VISIBILITY' })}
|
||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||
>
|
||
{state.showNewPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||
</button>
|
||
</div>
|
||
|
||
{/* Password Strength Indicator */}
|
||
{state.passwordForm.newPassword && (
|
||
<div className="mt-3 space-y-2">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-sm text-muted-foreground">密码强度:</span>
|
||
<Badge className={`${getStrengthBg(state.passwordStrength.strength)} ${getStrengthColor(state.passwordStrength.strength)}`}>
|
||
{getStrengthText(state.passwordStrength.strength)}
|
||
</Badge>
|
||
</div>
|
||
|
||
<div className="space-y-1.5">
|
||
<div className={`flex items-center gap-2 text-sm ${state.passwordStrength.checks.length ? 'text-green-600' : 'text-muted-foreground'}`}>
|
||
{state.passwordStrength.checks.length ? <CheckCircle className="w-4 h-4" /> : <XCircle className="w-4 h-4" />}
|
||
<span>至少8个字符</span>
|
||
</div>
|
||
<div className={`flex items-center gap-2 text-sm ${state.passwordStrength.checks.hasUpper ? 'text-green-600' : 'text-muted-foreground'}`}>
|
||
{state.passwordStrength.checks.hasUpper ? <CheckCircle className="w-4 h-4" /> : <XCircle className="w-4 h-4" />}
|
||
<span>包含大写字母</span>
|
||
</div>
|
||
<div className={`flex items-center gap-2 text-sm ${state.passwordStrength.checks.hasLower ? 'text-green-600' : 'text-muted-foreground'}`}>
|
||
{state.passwordStrength.checks.hasLower ? <CheckCircle className="w-4 h-4" /> : <XCircle className="w-4 h-4" />}
|
||
<span>包含小写字母</span>
|
||
</div>
|
||
<div className={`flex items-center gap-2 text-sm ${state.passwordStrength.checks.hasNumber ? 'text-green-600' : 'text-muted-foreground'}`}>
|
||
{state.passwordStrength.checks.hasNumber ? <CheckCircle className="w-4 h-4" /> : <XCircle className="w-4 h-4" />}
|
||
<span>包含数字</span>
|
||
</div>
|
||
<div className={`flex items-center gap-2 text-sm ${state.passwordStrength.checks.hasSpecial ? 'text-green-600' : 'text-muted-foreground'}`}>
|
||
{state.passwordStrength.checks.hasSpecial ? <CheckCircle className="w-4 h-4" /> : <XCircle className="w-4 h-4" />}
|
||
<span>包含特殊字符</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Confirm Password */}
|
||
<div>
|
||
<Label>确认新密码 *</Label>
|
||
<div className="relative mt-2">
|
||
<Input
|
||
type={state.showConfirmPassword ? 'text' : 'password'}
|
||
value={state.passwordForm.confirmPassword}
|
||
onChange={(e) => updatePassword('confirmPassword', e.target.value)}
|
||
placeholder="请再次输入新密码"
|
||
className="pr-10"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => dispatch({ type: 'TOGGLE_CONFIRM_PASSWORD_VISIBILITY' })}
|
||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||
>
|
||
{state.showConfirmPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||
</button>
|
||
</div>
|
||
{state.passwordForm.confirmPassword && state.passwordForm.newPassword !== state.passwordForm.confirmPassword && (
|
||
<p className="text-sm text-red-600 mt-2 flex items-center gap-1">
|
||
<XCircle className="w-4 h-4" />
|
||
两次输入的密码不一致
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* Security Notice */}
|
||
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||
<div className="flex items-start gap-2">
|
||
<AlertTriangle className="w-4 h-4 text-yellow-600 flex-shrink-0 mt-0.5" />
|
||
<p className="text-sm text-yellow-800">
|
||
修改密码后需要重新登录,请确保记住新密码
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<DialogFooter>
|
||
<Button variant="outline" onClick={() => dispatch({ type: 'TOGGLE_PASSWORD_DIALOG', payload: false })}>
|
||
取消
|
||
</Button>
|
||
<Button onClick={handleConfirmChangePassword}>
|
||
确认修改
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
</div>
|
||
);
|
||
} |