子仓库提交
This commit is contained in:
@@ -0,0 +1,549 @@
|
||||
'use client';
|
||||
|
||||
import { useReducer } from 'react';
|
||||
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;
|
||||
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'] };
|
||||
|
||||
// Initial state
|
||||
const initialState: SecurityState = {
|
||||
user: {
|
||||
username: 'admin',
|
||||
phone: '13800138000',
|
||||
email: 'admin@smart-agriculture.com',
|
||||
lastLoginTime: '2024-10-14 09:30:00',
|
||||
lastLoginDevice: 'Windows PC - Chrome 120.0',
|
||||
lastLoginIp: '192.168.1.100'
|
||||
},
|
||||
showPasswordDialog: false,
|
||||
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
|
||||
};
|
||||
|
||||
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 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">
|
||||
{/* Page Header */}
|
||||
<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 */}
|
||||
<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}</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}</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}</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}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Password Management */}
|
||||
<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 */}
|
||||
<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 */}
|
||||
<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} · {state.user.lastLoginIp}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{state.user.lastLoginTime}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Change Password Dialog */}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user