From 26213aaa7602129c7bb7f489725807dcfb5fb818 Mon Sep 17 00:00:00 2001 From: peng Date: Tue, 28 Oct 2025 15:22:54 +0800 Subject: [PATCH] =?UTF-8?q?=E7=94=9F=E4=BA=A7=E7=AE=A1=E7=90=86=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E5=89=8D=E7=AB=AF=20=E6=8F=90=E4=BA=A4=E4=B8=AA?= =?UTF-8?q?=E4=BA=BA=E4=B8=AD=E5=BF=832=E4=B8=AA=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/LoginHistory.tsx | 23 + .../components/NotificationSettings.tsx | 64 ++ .../components/PasswordSecurity.tsx | 54 ++ .../components/SecurityOverview.tsx | 188 ++++ .../components/SecurityQuestions.tsx | 42 + .../components/TrustedDevices.tsx | 55 ++ .../components/TwoFactorAuth.tsx | 42 + .../personal-center/account-security/page.tsx | 549 ++++++++++++ .../personal-center/account-security/types.ts | 58 ++ .../central-config/personal-center/page.tsx | 22 + .../components/PersonalInfoForm.tsx | 207 +++++ .../components/PersonalInfoHeader.tsx | 102 +++ .../components/PersonalInfoStats.tsx | 144 ++++ .../personal-center/personal-info/page.tsx | 425 +++++++++ .../personal-center/personal-info/types.ts | 35 + .../tenant/enterprise-management/page.tsx | 807 ++++++++++++++++++ crop-x/src/app/layout.tsx | 23 +- crop-x/src/types/profile.ts | 27 + 18 files changed, 2866 insertions(+), 1 deletion(-) create mode 100644 crop-x/src/app/(app)/central-config/personal-center/account-security/components/LoginHistory.tsx create mode 100644 crop-x/src/app/(app)/central-config/personal-center/account-security/components/NotificationSettings.tsx create mode 100644 crop-x/src/app/(app)/central-config/personal-center/account-security/components/PasswordSecurity.tsx create mode 100644 crop-x/src/app/(app)/central-config/personal-center/account-security/components/SecurityOverview.tsx create mode 100644 crop-x/src/app/(app)/central-config/personal-center/account-security/components/SecurityQuestions.tsx create mode 100644 crop-x/src/app/(app)/central-config/personal-center/account-security/components/TrustedDevices.tsx create mode 100644 crop-x/src/app/(app)/central-config/personal-center/account-security/components/TwoFactorAuth.tsx create mode 100644 crop-x/src/app/(app)/central-config/personal-center/account-security/page.tsx create mode 100644 crop-x/src/app/(app)/central-config/personal-center/account-security/types.ts create mode 100644 crop-x/src/app/(app)/central-config/personal-center/page.tsx create mode 100644 crop-x/src/app/(app)/central-config/personal-center/personal-info/components/PersonalInfoForm.tsx create mode 100644 crop-x/src/app/(app)/central-config/personal-center/personal-info/components/PersonalInfoHeader.tsx create mode 100644 crop-x/src/app/(app)/central-config/personal-center/personal-info/components/PersonalInfoStats.tsx create mode 100644 crop-x/src/app/(app)/central-config/personal-center/personal-info/page.tsx create mode 100644 crop-x/src/app/(app)/central-config/personal-center/personal-info/types.ts create mode 100644 crop-x/src/app/(app)/central-config/tenant/enterprise-management/page.tsx create mode 100644 crop-x/src/types/profile.ts diff --git a/crop-x/src/app/(app)/central-config/personal-center/account-security/components/LoginHistory.tsx b/crop-x/src/app/(app)/central-config/personal-center/account-security/components/LoginHistory.tsx new file mode 100644 index 0000000..ae582b3 --- /dev/null +++ b/crop-x/src/app/(app)/central-config/personal-center/account-security/components/LoginHistory.tsx @@ -0,0 +1,23 @@ +'use client'; + +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; + +interface LoginHistoryProps { + userId: string; +} + +export function LoginHistory({ userId }: LoginHistoryProps) { + return ( + + + 登录历史 + + +
+

登录历史功能开发中

+

将显示详细的登录记录

+
+
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/central-config/personal-center/account-security/components/NotificationSettings.tsx b/crop-x/src/app/(app)/central-config/personal-center/account-security/components/NotificationSettings.tsx new file mode 100644 index 0000000..a552a4b --- /dev/null +++ b/crop-x/src/app/(app)/central-config/personal-center/account-security/components/NotificationSettings.tsx @@ -0,0 +1,64 @@ +'use client'; + +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import type { SecuritySettings } from '../types'; + +interface NotificationSettingsProps { + securitySettings: SecuritySettings | null; + onUpdate: (updates: Partial) => void; +} + +export function NotificationSettings({ securitySettings, onUpdate }: NotificationSettingsProps) { + const handleToggle = (field: keyof SecuritySettings) => { + onUpdate({ [field]: !securitySettings?.[field] }); + }; + + return ( + + + 通知设置 + + +
+
+

邮件通知

+

接收安全相关的邮件通知

+
+ +
+ +
+
+

短信通知

+

接收安全相关的短信通知

+
+ +
+ +
+
+

登录提醒

+

新设备登录时接收通知

+
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/central-config/personal-center/account-security/components/PasswordSecurity.tsx b/crop-x/src/app/(app)/central-config/personal-center/account-security/components/PasswordSecurity.tsx new file mode 100644 index 0000000..acef744 --- /dev/null +++ b/crop-x/src/app/(app)/central-config/personal-center/account-security/components/PasswordSecurity.tsx @@ -0,0 +1,54 @@ +'use client'; + +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import type { SecuritySettings } from '../types'; + +interface PasswordSecurityProps { + securitySettings: SecuritySettings | null; + onUpdate: (updates: Partial) => void; +} + +export function PasswordSecurity({ securitySettings, onUpdate }: PasswordSecurityProps) { + const handlePasswordChange = () => { + // TODO: 实现密码修改功能 + onUpdate({ lastPasswordChange: new Date().toISOString() }); + }; + + return ( + + + 密码安全 + + +
+ +
+ {securitySettings?.passwordStrength === 'strong' ? '强' : + securitySettings?.passwordStrength === 'medium' ? '中等' : '弱'} +
+
+ +
+ +

+ {securitySettings?.lastPasswordChange ? + new Date(securitySettings.lastPasswordChange).toLocaleString('zh-CN') : + '未记录' + } +

+
+ + +
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/central-config/personal-center/account-security/components/SecurityOverview.tsx b/crop-x/src/app/(app)/central-config/personal-center/account-security/components/SecurityOverview.tsx new file mode 100644 index 0000000..1fd8e78 --- /dev/null +++ b/crop-x/src/app/(app)/central-config/personal-center/account-security/components/SecurityOverview.tsx @@ -0,0 +1,188 @@ +'use client'; + +import { Shield, Key, Smartphone, AlertTriangle, CheckCircle, Clock } from 'lucide-react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Progress } from '@/components/ui/progress'; +import { Button } from '@/components/ui/button'; +import type { SecuritySettings } from '../types'; + +interface SecurityOverviewProps { + securitySettings: SecuritySettings | null; + onTabChange: (tab: string) => void; +} + +export function SecurityOverview({ securitySettings, onTabChange }: SecurityOverviewProps) { + const securityScore = calculateSecurityScore(securitySettings); + + function calculateSecurityScore(settings: SecuritySettings | null): number { + if (!settings) return 0; + + let score = 0; + + // 密码强度 (30%) + if (settings.passwordStrength === 'strong') score += 30; + else if (settings.passwordStrength === 'medium') score += 20; + else score += 10; + + // 双因素认证 (25%) + if (settings.twoFactorEnabled) score += 25; + + // 安全问题设置 (20%) + if (settings.securityQuestions.length > 0) score += 20; + + // 登录提醒 (15%) + if (settings.loginAlert) score += 15; + + // 信任设备管理 (10%) + if (settings.trustedDevices.length <= 3) score += 10; + + return score; + } + + const getSecurityLevel = (score: number) => { + if (score >= 80) return { level: '高', color: 'text-green-600', bg: 'bg-green-50' }; + if (score >= 60) return { level: '中', color: 'text-yellow-600', bg: 'bg-yellow-50' }; + return { level: '低', color: 'text-red-600', bg: 'bg-red-50' }; + }; + + const securityLevel = getSecurityLevel(securityScore); + + const securityItems = [ + { + title: '密码强度', + icon: Key, + status: securitySettings?.passwordStrength === 'strong' ? 'good' : + securitySettings?.passwordStrength === 'medium' ? 'warning' : 'danger', + description: securitySettings?.passwordStrength === 'strong' ? '强密码' : + securitySettings?.passwordStrength === 'medium' ? '中等强度' : '弱密码', + action: () => onTabChange('password') + }, + { + title: '双因素认证', + icon: Smartphone, + status: securitySettings?.twoFactorEnabled ? 'good' : 'warning', + description: securitySettings?.twoFactorEnabled ? '已启用' : '未启用', + action: () => onTabChange('twoFactor') + }, + { + title: '安全问题', + icon: Shield, + status: securitySettings?.securityQuestions.length > 0 ? 'good' : 'warning', + description: securitySettings?.securityQuestions.length > 0 ? + `已设置 ${securitySettings.securityQuestions.length} 个问题` : '未设置', + action: () => onTabChange('questions') + }, + { + title: '登录提醒', + icon: Clock, + status: securitySettings?.loginAlert ? 'good' : 'warning', + description: securitySettings?.loginAlert ? '已启用' : '未启用', + action: () => onTabChange('notifications') + } + ]; + + return ( +
+ {/* 安全评分卡片 */} + + + + + 安全评分 + + + +
+
+ + {securityScore} + +
+

+ 安全等级: {securityLevel.level} +

+ +

+ 基于密码强度、双因素认证、安全问题和通知设置综合评估 +

+
+
+
+ + {/* 安全项列表 */} +
+ {securityItems.map((item, index) => { + const Icon = item.icon; + return ( + + +
+
+
+ +
+
+

{item.title}

+

{item.description}

+
+
+
+ {item.status === 'good' && } + {item.status === 'warning' && } + {item.status === 'danger' && } + {item.status !== 'good' && ( + + )} +
+
+
+
+ ); + })} +
+ + {/* 最近活动 */} + + + 最近登录活动 + + +
+
+
+ +
+

成功登录

+

+ {securitySettings?.lastLoginTime ? + new Date(securitySettings.lastLoginTime).toLocaleString('zh-CN') : + '未知时间' + } +

+
+
+
+

{securitySettings?.lastLoginIp}

+

当前设备

+
+
+
+ +
+ +
+
+
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/central-config/personal-center/account-security/components/SecurityQuestions.tsx b/crop-x/src/app/(app)/central-config/personal-center/account-security/components/SecurityQuestions.tsx new file mode 100644 index 0000000..d3edbcd --- /dev/null +++ b/crop-x/src/app/(app)/central-config/personal-center/account-security/components/SecurityQuestions.tsx @@ -0,0 +1,42 @@ +'use client'; + +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import type { SecuritySettings, SecurityQuestion } from '../types'; + +interface SecurityQuestionsProps { + questions: SecurityQuestion[]; + onUpdate: (updates: Partial) => void; +} + +export function SecurityQuestions({ questions, onUpdate }: SecurityQuestionsProps) { + return ( + + + 安全问题 + + +
+ {questions.map((question, index) => ( +
+
+
+

问题 {index + 1}: {question.question}

+

+ {question.isEnabled ? '已启用' : '已禁用'} +

+
+
+
+ ))} + + {questions.length === 0 && ( +
+

尚未设置安全问题

+

设置安全问题可以在忘记密码时恢复账户

+
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/central-config/personal-center/account-security/components/TrustedDevices.tsx b/crop-x/src/app/(app)/central-config/personal-center/account-security/components/TrustedDevices.tsx new file mode 100644 index 0000000..207e37a --- /dev/null +++ b/crop-x/src/app/(app)/central-config/personal-center/account-security/components/TrustedDevices.tsx @@ -0,0 +1,55 @@ +'use client'; + +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import type { SecuritySettings, TrustedDevice } from '../types'; + +interface TrustedDevicesProps { + devices: TrustedDevice[]; + onUpdate: (updates: Partial) => void; +} + +export function TrustedDevices({ devices, onUpdate }: TrustedDevicesProps) { + return ( + + + 信任设备 + + +
+ {devices.map((device) => ( +
+
+
+
+

{device.deviceName}

+ {device.isCurrent && ( + + 当前设备 + + )} +
+

+ {device.browser} • {device.os} +

+

+ {device.ipAddress} • {device.location} +

+
+
+

+ 最后活动: {new Date(device.lastActive).toLocaleString('zh-CN')} +

+
+
+
+ ))} +
+
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/central-config/personal-center/account-security/components/TwoFactorAuth.tsx b/crop-x/src/app/(app)/central-config/personal-center/account-security/components/TwoFactorAuth.tsx new file mode 100644 index 0000000..67a752b --- /dev/null +++ b/crop-x/src/app/(app)/central-config/personal-center/account-security/components/TwoFactorAuth.tsx @@ -0,0 +1,42 @@ +'use client'; + +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import type { SecuritySettings } from '../types'; + +interface TwoFactorAuthProps { + securitySettings: SecuritySettings | null; + onUpdate: (updates: Partial) => void; +} + +export function TwoFactorAuth({ securitySettings, onUpdate }: TwoFactorAuthProps) { + const handleToggle = () => { + onUpdate({ twoFactorEnabled: !securitySettings?.twoFactorEnabled }); + }; + + return ( + + + 双因素认证 + + +
+
+

+ {securitySettings?.twoFactorEnabled ? '已启用' : '未启用'} +

+

+ 为您的账户添加额外的安全保护 +

+
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/central-config/personal-center/account-security/page.tsx b/crop-x/src/app/(app)/central-config/personal-center/account-security/page.tsx new file mode 100644 index 0000000..ca19f5a --- /dev/null +++ b/crop-x/src/app/(app)/central-config/personal-center/account-security/page.tsx @@ -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 } + | { 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 ( +
+ {/* Page Header */} + +
+ +
+

账户安全

+

+ 管理您的账户安全设置,包括密码修改、安全验证等,确保账户安全 +

+
+ + + 密码管理 + + + + 安全验证 + + + + 隐私保护 + +
+
+
+
+ + {/* Account Overview */} + +

账户信息

+
+
+
用户名
+
{state.user.username}
+
+
+
手机号
+
{state.user.phone}
+
+
+
邮箱
+
{state.user.email}
+
+
+
最后登录时间
+
{state.user.lastLoginTime}
+
+
+
+ + {/* Password Management */} + +
+
+

登录密码

+

定期修改密码可以提高账户安全性

+
+ +
+ +
+
+ +
+

密码安全提示

+
    +
  • • 密码长度至少8位
  • +
  • • 包含大小写字母、数字和特殊字符
  • +
  • • 不要使用过于简单的密码
  • +
  • • 定期更换密码(建议3个月更换一次)
  • +
  • • 不要在多个平台使用相同密码
  • +
+
+
+
+
+ + {/* Security Settings */} + +

安全设置

+
+
+
+
+ +
+
+
手机验证
+
已绑定手机号:{state.user.phone}
+
+
+ 已启用 +
+ +
+
+
+ +
+
+
登录保护
+
异常登录时需要额外验证
+
+
+ 已启用 +
+ +
+
+
+ +
+
+
邮箱验证
+
已绑定邮箱:{state.user.email}
+
+
+ 已启用 +
+
+
+ + {/* Recent Login Records */} + +

最近登录记录

+
+
+
+ +
+
当前会话
+
+ {state.user.lastLoginDevice} · {state.user.lastLoginIp} +
+
+
+
+ {state.user.lastLoginTime} +
+
+
+
+ + {/* Change Password Dialog */} + dispatch({ type: 'TOGGLE_PASSWORD_DIALOG', payload: open })}> + + + +
+ + 修改登录密码 +
+
+ + 为了您的账户安全,请定期更换密码并设置复杂密码 + +
+ +
+ {/* Old Password */} +
+ +
+ updatePassword('oldPassword', e.target.value)} + placeholder="请输入原密码" + className="pr-10" + /> + +
+
+ + {/* New Password */} +
+ +
+ updatePassword('newPassword', e.target.value)} + placeholder="请输入新密码(至少8位)" + className="pr-10" + /> + +
+ + {/* Password Strength Indicator */} + {state.passwordForm.newPassword && ( +
+
+ 密码强度: + + {getStrengthText(state.passwordStrength.strength)} + +
+ +
+
+ {state.passwordStrength.checks.length ? : } + 至少8个字符 +
+
+ {state.passwordStrength.checks.hasUpper ? : } + 包含大写字母 +
+
+ {state.passwordStrength.checks.hasLower ? : } + 包含小写字母 +
+
+ {state.passwordStrength.checks.hasNumber ? : } + 包含数字 +
+
+ {state.passwordStrength.checks.hasSpecial ? : } + 包含特殊字符 +
+
+
+ )} +
+ + {/* Confirm Password */} +
+ +
+ updatePassword('confirmPassword', e.target.value)} + placeholder="请再次输入新密码" + className="pr-10" + /> + +
+ {state.passwordForm.confirmPassword && state.passwordForm.newPassword !== state.passwordForm.confirmPassword && ( +

+ + 两次输入的密码不一致 +

+ )} +
+ + {/* Security Notice */} +
+
+ +

+ 修改密码后需要重新登录,请确保记住新密码 +

+
+
+
+ + + + + +
+
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/central-config/personal-center/account-security/types.ts b/crop-x/src/app/(app)/central-config/personal-center/account-security/types.ts new file mode 100644 index 0000000..dfa6bd7 --- /dev/null +++ b/crop-x/src/app/(app)/central-config/personal-center/account-security/types.ts @@ -0,0 +1,58 @@ +// 账户安全页面类型定义 + +export interface SecuritySettings { + id: string; + userId: string; + twoFactorEnabled: boolean; + emailNotification: boolean; + smsNotification: boolean; + loginAlert: boolean; + passwordStrength: 'weak' | 'medium' | 'strong'; + lastPasswordChange: string; + loginAttempts: number; + lastLoginTime: string; + lastLoginIp: string; + trustedDevices: TrustedDevice[]; + securityQuestions: SecurityQuestion[]; +} + +export interface TrustedDevice { + id: string; + deviceName: string; + deviceType: 'desktop' | 'mobile' | 'tablet'; + browser: string; + os: string; + ipAddress: string; + location: string; + lastActive: string; + isCurrent: boolean; +} + +export interface SecurityQuestion { + id: string; + question: string; + answer: string; + isEnabled: boolean; +} + +export interface PasswordChangeForm { + currentPassword: string; + newPassword: string; + confirmPassword: string; +} + +export interface SecurityQuestionForm { + question: string; + answer: string; + confirmPassword: string; +} + +export interface LoginHistory { + id: string; + loginTime: string; + ipAddress: string; + location: string; + device: string; + status: 'success' | 'failed'; + failureReason?: string; +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/central-config/personal-center/page.tsx b/crop-x/src/app/(app)/central-config/personal-center/page.tsx new file mode 100644 index 0000000..e9b077a --- /dev/null +++ b/crop-x/src/app/(app)/central-config/personal-center/page.tsx @@ -0,0 +1,22 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; + +export default function PersonalCenterPage() { + return ( +
+

个人中心

+
+ +

个人信息

+

管理个人信息

+ + +

账户安全

+

管理账户安全设置

+ +
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(app)/central-config/personal-center/personal-info/components/PersonalInfoForm.tsx b/crop-x/src/app/(app)/central-config/personal-center/personal-info/components/PersonalInfoForm.tsx new file mode 100644 index 0000000..586703e --- /dev/null +++ b/crop-x/src/app/(app)/central-config/personal-center/personal-info/components/PersonalInfoForm.tsx @@ -0,0 +1,207 @@ +'use client'; + +import { useState } from 'react'; +import { Save, X, User, Mail, Phone, MapPin, FileText } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { Label } from '@/components/ui/label'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import type { PersonalInfoForm as PersonalInfoFormType } from '../types'; + +interface PersonalInfoFormProps { + data: PersonalInfoFormType; + editing: boolean; + onSave: (data: PersonalInfoFormType) => void; + onCancel: () => void; + onChange: (data: PersonalInfoFormType) => void; +} + +export function PersonalInfoForm({ data, editing, onSave, onCancel, onChange }: PersonalInfoFormProps) { + const [errors, setErrors] = useState>({}); + + const validateForm = (): boolean => { + const newErrors: Record = {}; + + if (!data.realName.trim()) { + newErrors.realName = '真实姓名不能为空'; + } + + if (!data.email.trim()) { + newErrors.email = '邮箱不能为空'; + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) { + newErrors.email = '请输入有效的邮箱地址'; + } + + if (!data.phone.trim()) { + newErrors.phone = '手机号不能为空'; + } else if (!/^1[3-9]\d{9}$/.test(data.phone)) { + newErrors.phone = '请输入有效的手机号码'; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSave = () => { + if (validateForm()) { + onSave(data); + } + }; + + const handleInputChange = (field: keyof PersonalInfoFormType, value: string) => { + onChange({ ...data, [field]: value }); + if (errors[field]) { + setErrors({ ...errors, [field]: '' }); + } + }; + + return ( + + +
+ 基本信息 + {editing && ( +
+ + +
+ )} +
+
+ +
+
+ + handleInputChange('realName', e.target.value)} + disabled={!editing} + className={errors.realName ? 'border-red-500' : ''} + placeholder="请输入真实姓名" + /> + {errors.realName && ( +

{errors.realName}

+ )} +
+ +
+ + handleInputChange('email', e.target.value)} + disabled={!editing} + className={errors.email ? 'border-red-500' : ''} + placeholder="请输入邮箱地址" + /> + {errors.email && ( +

{errors.email}

+ )} +
+ +
+ + handleInputChange('phone', e.target.value)} + disabled={!editing} + className={errors.phone ? 'border-red-500' : ''} + placeholder="请输入手机号码" + /> + {errors.phone && ( +

{errors.phone}

+ )} +
+ +
+ + +
+ +
+ + handleInputChange('birthday', e.target.value)} + disabled={!editing} + /> +
+ +
+ + handleInputChange('address', e.target.value)} + disabled={!editing} + placeholder="请输入联系地址" + /> +
+
+ +
+ +