子仓库提交

This commit is contained in:
2025-11-10 09:19:56 +08:00
parent 62f92213f7
commit 5feb24e4e2
733 changed files with 141413 additions and 0 deletions

View File

@@ -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>
);
}