生产管理系统前端 提交个人中心2个页面开发
This commit is contained in:
@@ -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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>登录历史</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<p>登录历史功能开发中</p>
|
||||
<p className="text-sm">将显示详细的登录记录</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -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<SecuritySettings>) => void;
|
||||
}
|
||||
|
||||
export function NotificationSettings({ securitySettings, onUpdate }: NotificationSettingsProps) {
|
||||
const handleToggle = (field: keyof SecuritySettings) => {
|
||||
onUpdate({ [field]: !securitySettings?.[field] });
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>通知设置</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium">邮件通知</p>
|
||||
<p className="text-sm text-gray-600">接收安全相关的邮件通知</p>
|
||||
</div>
|
||||
<Button
|
||||
variant={securitySettings?.emailNotification ? "default" : "outline"}
|
||||
onClick={() => handleToggle('emailNotification')}
|
||||
>
|
||||
{securitySettings?.emailNotification ? '已启用' : '已禁用'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium">短信通知</p>
|
||||
<p className="text-sm text-gray-600">接收安全相关的短信通知</p>
|
||||
</div>
|
||||
<Button
|
||||
variant={securitySettings?.smsNotification ? "default" : "outline"}
|
||||
onClick={() => handleToggle('smsNotification')}
|
||||
>
|
||||
{securitySettings?.smsNotification ? '已启用' : '已禁用'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium">登录提醒</p>
|
||||
<p className="text-sm text-gray-600">新设备登录时接收通知</p>
|
||||
</div>
|
||||
<Button
|
||||
variant={securitySettings?.loginAlert ? "default" : "outline"}
|
||||
onClick={() => handleToggle('loginAlert')}
|
||||
>
|
||||
{securitySettings?.loginAlert ? '已启用' : '已禁用'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -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<SecuritySettings>) => void;
|
||||
}
|
||||
|
||||
export function PasswordSecurity({ securitySettings, onUpdate }: PasswordSecurityProps) {
|
||||
const handlePasswordChange = () => {
|
||||
// TODO: 实现密码修改功能
|
||||
onUpdate({ lastPasswordChange: new Date().toISOString() });
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>密码安全</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<Label>当前密码强度</Label>
|
||||
<div className={`mt-1 p-2 rounded ${
|
||||
securitySettings?.passwordStrength === 'strong' ? 'bg-green-100 text-green-800' :
|
||||
securitySettings?.passwordStrength === 'medium' ? 'bg-yellow-100 text-yellow-800' :
|
||||
'bg-red-100 text-red-800'
|
||||
}`}>
|
||||
{securitySettings?.passwordStrength === 'strong' ? '强' :
|
||||
securitySettings?.passwordStrength === 'medium' ? '中等' : '弱'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>上次修改时间</Label>
|
||||
<p className="text-sm text-gray-600">
|
||||
{securitySettings?.lastPasswordChange ?
|
||||
new Date(securitySettings.lastPasswordChange).toLocaleString('zh-CN') :
|
||||
'未记录'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button onClick={handlePasswordChange} className="w-full">
|
||||
修改密码
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="space-y-6">
|
||||
{/* 安全评分卡片 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<Shield className="h-5 w-5" />
|
||||
<span>安全评分</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-center">
|
||||
<div className={`inline-flex items-center justify-center w-24 h-24 rounded-full ${securityLevel.bg} mb-4`}>
|
||||
<span className={`text-3xl font-bold ${securityLevel.color}`}>
|
||||
{securityScore}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className={`text-lg font-semibold ${securityLevel.color}`}>
|
||||
安全等级: {securityLevel.level}
|
||||
</h3>
|
||||
<Progress value={securityScore} className="mt-4" />
|
||||
<p className="text-sm text-gray-500 mt-2">
|
||||
基于密码强度、双因素认证、安全问题和通知设置综合评估
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 安全项列表 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{securityItems.map((item, index) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<Card key={index}>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`p-2 rounded-full ${
|
||||
item.status === 'good' ? 'bg-green-100' :
|
||||
item.status === 'warning' ? 'bg-yellow-100' : 'bg-red-100'
|
||||
}`}>
|
||||
<Icon className={`h-5 w-5 ${
|
||||
item.status === 'good' ? 'text-green-600' :
|
||||
item.status === 'warning' ? 'text-yellow-600' : 'text-red-600'
|
||||
}`} />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold">{item.title}</h4>
|
||||
<p className="text-sm text-gray-600">{item.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
{item.status === 'good' && <CheckCircle className="h-4 w-4 text-green-500" />}
|
||||
{item.status === 'warning' && <AlertTriangle className="h-4 w-4 text-yellow-500" />}
|
||||
{item.status === 'danger' && <AlertTriangle className="h-4 w-4 text-red-500" />}
|
||||
{item.status !== 'good' && (
|
||||
<Button variant="outline" size="sm" onClick={item.action}>
|
||||
设置
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 最近活动 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>最近登录活动</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-3 bg-green-50 rounded">
|
||||
<div className="flex items-center space-x-3">
|
||||
<CheckCircle className="h-5 w-5 text-green-500" />
|
||||
<div>
|
||||
<p className="font-medium">成功登录</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
{securitySettings?.lastLoginTime ?
|
||||
new Date(securitySettings.lastLoginTime).toLocaleString('zh-CN') :
|
||||
'未知时间'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-medium">{securitySettings?.lastLoginIp}</p>
|
||||
<p className="text-xs text-gray-500">当前设备</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center mt-4">
|
||||
<Button variant="outline" onClick={() => onTabChange('history')}>
|
||||
查看完整登录历史
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<SecuritySettings>) => void;
|
||||
}
|
||||
|
||||
export function SecurityQuestions({ questions, onUpdate }: SecurityQuestionsProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>安全问题</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{questions.map((question, index) => (
|
||||
<div key={question.id} className="p-4 border rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium">问题 {index + 1}: {question.question}</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
{question.isEnabled ? '已启用' : '已禁用'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{questions.length === 0 && (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<p>尚未设置安全问题</p>
|
||||
<p className="text-sm">设置安全问题可以在忘记密码时恢复账户</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -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<SecuritySettings>) => void;
|
||||
}
|
||||
|
||||
export function TrustedDevices({ devices, onUpdate }: TrustedDevicesProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>信任设备</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{devices.map((device) => (
|
||||
<div
|
||||
key={device.id}
|
||||
className={`p-4 border rounded-lg ${
|
||||
device.isCurrent ? 'border-blue-500 bg-blue-50' : 'border-gray-200'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className="font-medium">{device.deviceName}</p>
|
||||
{device.isCurrent && (
|
||||
<span className="text-xs bg-blue-500 text-white px-2 py-1 rounded">
|
||||
当前设备
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">
|
||||
{device.browser} • {device.os}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{device.ipAddress} • {device.location}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-xs text-gray-500">
|
||||
最后活动: {new Date(device.lastActive).toLocaleString('zh-CN')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -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<SecuritySettings>) => void;
|
||||
}
|
||||
|
||||
export function TwoFactorAuth({ securitySettings, onUpdate }: TwoFactorAuthProps) {
|
||||
const handleToggle = () => {
|
||||
onUpdate({ twoFactorEnabled: !securitySettings?.twoFactorEnabled });
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>双因素认证</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium">
|
||||
{securitySettings?.twoFactorEnabled ? '已启用' : '未启用'}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
为您的账户添加额外的安全保护
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant={securitySettings?.twoFactorEnabled ? "destructive" : "default"}
|
||||
onClick={handleToggle}
|
||||
>
|
||||
{securitySettings?.twoFactorEnabled ? '禁用' : '启用'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
22
crop-x/src/app/(app)/central-config/personal-center/page.tsx
Normal file
22
crop-x/src/app/(app)/central-config/personal-center/page.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function PersonalCenterPage() {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-bold mb-4">个人中心</h1>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Link href="/central-config/personal-center/personal-info" className="bg-white rounded-lg shadow p-4 hover:shadow-md transition-shadow">
|
||||
<h3 className="text-lg font-semibold mb-2">个人信息</h3>
|
||||
<p className="text-gray-600 text-sm">管理个人信息</p>
|
||||
</Link>
|
||||
<Link href="/central-config/personal-center/account-security" className="bg-white rounded-lg shadow p-4 hover:shadow-md transition-shadow">
|
||||
<h3 className="text-lg font-semibold mb-2">账户安全</h3>
|
||||
<p className="text-gray-600 text-sm">管理账户安全设置</p>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<Record<string, string>>({});
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>基本信息</CardTitle>
|
||||
{editing && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button variant="outline" size="sm" onClick={onCancel}>
|
||||
<X className="h-4 w-4 mr-1" />
|
||||
取消
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleSave}>
|
||||
<Save className="h-4 w-4 mr-1" />
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="realName" className="flex items-center space-x-2">
|
||||
<User className="h-4 w-4" />
|
||||
<span>真实姓名 *</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="realName"
|
||||
value={data.realName}
|
||||
onChange={(e) => handleInputChange('realName', e.target.value)}
|
||||
disabled={!editing}
|
||||
className={errors.realName ? 'border-red-500' : ''}
|
||||
placeholder="请输入真实姓名"
|
||||
/>
|
||||
{errors.realName && (
|
||||
<p className="text-sm text-red-500">{errors.realName}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email" className="flex items-center space-x-2">
|
||||
<Mail className="h-4 w-4" />
|
||||
<span>邮箱地址 *</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={data.email}
|
||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||
disabled={!editing}
|
||||
className={errors.email ? 'border-red-500' : ''}
|
||||
placeholder="请输入邮箱地址"
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="text-sm text-red-500">{errors.email}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="phone" className="flex items-center space-x-2">
|
||||
<Phone className="h-4 w-4" />
|
||||
<span>手机号码 *</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="phone"
|
||||
value={data.phone}
|
||||
onChange={(e) => handleInputChange('phone', e.target.value)}
|
||||
disabled={!editing}
|
||||
className={errors.phone ? 'border-red-500' : ''}
|
||||
placeholder="请输入手机号码"
|
||||
/>
|
||||
{errors.phone && (
|
||||
<p className="text-sm text-red-500">{errors.phone}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="gender">性别</Label>
|
||||
<Select
|
||||
value={data.gender || ''}
|
||||
onValueChange={(value) => handleInputChange('gender', value)}
|
||||
disabled={!editing}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="请选择性别" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="male">男</SelectItem>
|
||||
<SelectItem value="female">女</SelectItem>
|
||||
<SelectItem value="other">其他</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="birthday">出生日期</Label>
|
||||
<Input
|
||||
id="birthday"
|
||||
type="date"
|
||||
value={data.birthday || ''}
|
||||
onChange={(e) => handleInputChange('birthday', e.target.value)}
|
||||
disabled={!editing}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="address" className="flex items-center space-x-2">
|
||||
<MapPin className="h-4 w-4" />
|
||||
<span>联系地址</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="address"
|
||||
value={data.address || ''}
|
||||
onChange={(e) => handleInputChange('address', e.target.value)}
|
||||
disabled={!editing}
|
||||
placeholder="请输入联系地址"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="bio" className="flex items-center space-x-2">
|
||||
<FileText className="h-4 w-4" />
|
||||
<span>个人简介</span>
|
||||
</Label>
|
||||
<Textarea
|
||||
id="bio"
|
||||
value={data.bio || ''}
|
||||
onChange={(e) => handleInputChange('bio', e.target.value)}
|
||||
disabled={!editing}
|
||||
placeholder="请输入个人简介"
|
||||
rows={4}
|
||||
maxLength={200}
|
||||
/>
|
||||
<p className="text-sm text-gray-500">
|
||||
{data.bio?.length || 0}/200 字符
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!editing && (
|
||||
<div className="text-sm text-gray-500 bg-gray-50 p-3 rounded">
|
||||
💡 点击右上角的"编辑信息"按钮来修改个人信息
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { User, Edit, Upload, Camera } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import type { PersonalInfo } from '../types';
|
||||
|
||||
interface PersonalInfoHeaderProps {
|
||||
personalInfo: PersonalInfo | null;
|
||||
onEdit: () => void;
|
||||
onAvatarChange: (file: File) => void;
|
||||
}
|
||||
|
||||
export function PersonalInfoHeader({ personalInfo, onEdit, onAvatarChange }: PersonalInfoHeaderProps) {
|
||||
const [avatarPreview, setAvatarPreview] = useState<string | null>(null);
|
||||
|
||||
const handleAvatarChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setAvatarPreview(reader.result as string);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
onAvatarChange(file);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-6">
|
||||
<div className="relative">
|
||||
<Avatar className="h-20 w-20">
|
||||
<AvatarImage src={avatarPreview || personalInfo?.avatar} alt="头像" />
|
||||
<AvatarFallback>
|
||||
<User className="h-10 w-10" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<label className="absolute bottom-0 right-0 cursor-pointer">
|
||||
<div className="bg-blue-500 text-white rounded-full p-1 hover:bg-blue-600 transition-colors">
|
||||
<Camera className="h-4 w-4" />
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleAvatarChange}
|
||||
className="hidden"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">
|
||||
{personalInfo?.realName || '未设置姓名'}
|
||||
</h1>
|
||||
<p className="text-gray-600">
|
||||
@{personalInfo?.username || '未设置用户名'}
|
||||
</p>
|
||||
<div className="flex items-center space-x-4 mt-2 text-sm text-gray-500">
|
||||
<span>{personalInfo?.department || '未设置部门'}</span>
|
||||
<span>•</span>
|
||||
<span>{personalInfo?.position || '未设置职位'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button onClick={onEdit} className="flex items-center space-x-2">
|
||||
<Edit className="h-4 w-4" />
|
||||
<span>编辑信息</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
{personalInfo?.email || '未设置邮箱'}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">邮箱地址</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{personalInfo?.phone || '未设置手机号'}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">手机号码</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-purple-600">
|
||||
{personalInfo?.gender === 'male' ? '男' :
|
||||
personalInfo?.gender === 'female' ? '女' :
|
||||
personalInfo?.gender === 'other' ? '其他' : '未设置'}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">性别</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Users, UserCheck, UserX, RefreshCw } from 'lucide-react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import type { PersonalInfoStats as PersonalInfoStatsType } from '../types';
|
||||
|
||||
export function PersonalInfoStats() {
|
||||
const [stats, setStats] = useState<PersonalInfoStatsType>({
|
||||
totalUsers: 0,
|
||||
activeUsers: 0,
|
||||
inactiveUsers: 0,
|
||||
recentlyUpdated: 0
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// 模拟API调用
|
||||
const fetchStats = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 模拟网络延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// 模拟统计数据
|
||||
const mockStats: PersonalInfoStatsType = {
|
||||
totalUsers: 1523,
|
||||
activeUsers: 1245,
|
||||
inactiveUsers: 278,
|
||||
recentlyUpdated: 89
|
||||
};
|
||||
|
||||
setStats(mockStats);
|
||||
} catch (error) {
|
||||
console.error('获取统计数据失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchStats();
|
||||
}, []);
|
||||
|
||||
const handleRefresh = () => {
|
||||
// 刷新统计数据
|
||||
setLoading(true);
|
||||
setTimeout(() => {
|
||||
setStats({
|
||||
totalUsers: Math.floor(Math.random() * 2000) + 1000,
|
||||
activeUsers: Math.floor(Math.random() * 1500) + 800,
|
||||
inactiveUsers: Math.floor(Math.random() * 300) + 100,
|
||||
recentlyUpdated: Math.floor(Math.random() * 100) + 50
|
||||
});
|
||||
setLoading(false);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg">个人信息统计</CardTitle>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleRefresh}
|
||||
disabled={loading}
|
||||
className="flex items-center space-x-1"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
<span>刷新</span>
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="text-center p-4 bg-blue-50 rounded-lg">
|
||||
<div className="flex items-center justify-center mb-2">
|
||||
<Users className="h-6 w-6 text-blue-600" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
{loading ? '--' : stats.totalUsers.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-sm text-blue-600">总用户数</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-4 bg-green-50 rounded-lg">
|
||||
<div className="flex items-center justify-center mb-2">
|
||||
<UserCheck className="h-6 w-6 text-green-600" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{loading ? '--' : stats.activeUsers.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-sm text-green-600">活跃用户</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-4 bg-orange-50 rounded-lg">
|
||||
<div className="flex items-center justify-center mb-2">
|
||||
<UserX className="h-6 w-6 text-orange-600" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-orange-600">
|
||||
{loading ? '--' : stats.inactiveUsers.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-sm text-orange-600">非活跃用户</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-4 bg-purple-50 rounded-lg">
|
||||
<div className="flex items-center justify-center mb-2">
|
||||
<RefreshCw className="h-6 w-6 text-purple-600" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-purple-600">
|
||||
{loading ? '--' : stats.recentlyUpdated.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-sm text-purple-600">最近更新</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 p-3 bg-gray-50 rounded">
|
||||
<h4 className="font-semibold text-sm mb-2">活跃度分析</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>活跃率</span>
|
||||
<span className="font-medium">
|
||||
{loading ? '--' : `${((stats.activeUsers / stats.totalUsers) * 100).toFixed(1)}%`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-green-500 h-2 rounded-full transition-all duration-500"
|
||||
style={{
|
||||
width: loading ? '0%' : `${(stats.activeUsers / stats.totalUsers) * 100}%`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-500 text-center mt-4">
|
||||
数据更新时间: {new Date().toLocaleString('zh-CN')}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } 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 { Textarea } from '@/components/ui/textarea';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { UserProfile, PasswordChange } from '@/types/profile';
|
||||
import { User, Mail, Phone, Building, Briefcase, Lock, Save, Shield } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function PersonalInfo() {
|
||||
const [profile, setProfile] = useState<UserProfile>({
|
||||
id: 'user-1',
|
||||
username: 'admin',
|
||||
name: '系统管理员',
|
||||
email: 'admin@smart-agriculture.com',
|
||||
phone: '13800138000',
|
||||
avatar: '',
|
||||
gender: 'male',
|
||||
birthday: '1990-01-01',
|
||||
department: '技术部',
|
||||
position: '系统管理员',
|
||||
enterpriseId: 'ent-1',
|
||||
enterpriseName: '智慧农业科技有限公司',
|
||||
roleIds: ['role-1'],
|
||||
roleNames: ['超级管理员'],
|
||||
bio: '负责系统整体架构和技术管理',
|
||||
address: '北京市海淀区中关村大街1号',
|
||||
createdAt: '2024-01-01T00:00:00',
|
||||
lastLoginTime: '2024-10-14T09:30:00',
|
||||
lastLoginIp: '192.168.1.100',
|
||||
});
|
||||
|
||||
const [showPasswordDialog, setShowPasswordDialog] = useState(false);
|
||||
const [passwordForm, setPasswordForm] = useState<PasswordChange>({
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
});
|
||||
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadProfile();
|
||||
}, []);
|
||||
|
||||
const loadProfile = () => {
|
||||
const data = localStorage.getItem('smart_agriculture_user_profile');
|
||||
if (data) {
|
||||
setProfile(JSON.parse(data));
|
||||
} else {
|
||||
saveProfile(profile);
|
||||
}
|
||||
};
|
||||
|
||||
const saveProfile = (newProfile: UserProfile) => {
|
||||
localStorage.setItem('smart_agriculture_user_profile', JSON.stringify(newProfile));
|
||||
setProfile(newProfile);
|
||||
setHasChanges(false);
|
||||
toast.success('个人信息已保存');
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!profile.name.trim() || !profile.email.trim()) {
|
||||
toast.error('请填写必填项');
|
||||
return;
|
||||
}
|
||||
|
||||
// 简单的邮箱格式验证
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(profile.email)) {
|
||||
toast.error('邮箱格式不正确');
|
||||
return;
|
||||
}
|
||||
|
||||
saveProfile(profile);
|
||||
};
|
||||
|
||||
const handlePasswordChange = () => {
|
||||
if (!passwordForm.oldPassword || !passwordForm.newPassword || !passwordForm.confirmPassword) {
|
||||
toast.error('请填写所有密码字段');
|
||||
return;
|
||||
}
|
||||
|
||||
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
|
||||
toast.error('两次输入的新密码不一致');
|
||||
return;
|
||||
}
|
||||
|
||||
if (passwordForm.newPassword.length < 8) {
|
||||
toast.error('新密码长度不能少于8位');
|
||||
return;
|
||||
}
|
||||
|
||||
// 模拟密码修改
|
||||
toast.success('密码修改成功,请重新登录');
|
||||
setShowPasswordDialog(false);
|
||||
setPasswordForm({
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
});
|
||||
};
|
||||
|
||||
const updateProfile = (updates: Partial<UserProfile>) => {
|
||||
setProfile({ ...profile, ...updates });
|
||||
setHasChanges(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-green-800">个人信息</h2>
|
||||
<p className="text-muted-foreground">查看和维护个人账户信息</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={() => setShowPasswordDialog(true)}>
|
||||
<Lock className="w-4 h-4 mr-2" />
|
||||
修改密码
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={!hasChanges}
|
||||
className={hasChanges ? 'bg-green-600 hover:bg-green-700' : ''}
|
||||
>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
保存修改
|
||||
{hasChanges && <span className="ml-1">(有未保存更改)</span>}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="basic" className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="basic">
|
||||
<User className="w-4 h-4 mr-2" />
|
||||
基本信息
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="work">
|
||||
<Briefcase className="w-4 h-4 mr-2" />
|
||||
工作信息
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="security">
|
||||
<Shield className="w-4 h-4 mr-2" />
|
||||
安全信息
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 基本信息 */}
|
||||
<TabsContent value="basic" className="space-y-4">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-start gap-6">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<Avatar className="w-24 h-24">
|
||||
<AvatarImage src={profile.avatar} />
|
||||
<AvatarFallback className="text-2xl bg-green-100 text-green-700">
|
||||
{profile.name?.substring(0, 2) || '用户'}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<Button variant="outline" size="sm">
|
||||
更换头像
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>用户名</Label>
|
||||
<Input
|
||||
value={profile.username}
|
||||
disabled
|
||||
className="bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>姓名 *</Label>
|
||||
<Input
|
||||
value={profile.name}
|
||||
onChange={(e) => updateProfile({ name: e.target.value })}
|
||||
placeholder="请输入姓名"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>邮箱 *</Label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="email"
|
||||
value={profile.email}
|
||||
onChange={(e) => updateProfile({ email: e.target.value })}
|
||||
placeholder="example@email.com"
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>手机号</Label>
|
||||
<div className="relative">
|
||||
<Phone className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
value={profile.phone}
|
||||
onChange={(e) => updateProfile({ phone: e.target.value })}
|
||||
placeholder="13800138000"
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>性别</Label>
|
||||
<Select
|
||||
value={profile.gender}
|
||||
onValueChange={(value: any) => updateProfile({ gender: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="male">男</SelectItem>
|
||||
<SelectItem value="female">女</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>生日</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={profile.birthday}
|
||||
onChange={(e) => updateProfile({ birthday: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<Label>个人简介</Label>
|
||||
<Textarea
|
||||
value={profile.bio}
|
||||
onChange={(e) => updateProfile({ bio: e.target.value })}
|
||||
placeholder="介绍一下自己"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<Label>地址</Label>
|
||||
<Input
|
||||
value={profile.address}
|
||||
onChange={(e) => updateProfile({ address: e.target.value })}
|
||||
placeholder="请输入地址"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* 工作信息 */}
|
||||
<TabsContent value="work" className="space-y-4">
|
||||
<Card className="p-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>企业名称</Label>
|
||||
<div className="relative">
|
||||
<Building className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
value={profile.enterpriseName}
|
||||
disabled
|
||||
className="pl-10 bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>部门</Label>
|
||||
<Input
|
||||
value={profile.department}
|
||||
onChange={(e) => updateProfile({ department: e.target.value })}
|
||||
placeholder="请输入部门"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>职位</Label>
|
||||
<Input
|
||||
value={profile.position}
|
||||
onChange={(e) => updateProfile({ position: e.target.value })}
|
||||
placeholder="请输入职位"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>角色</Label>
|
||||
<div className="flex flex-wrap gap-2 pt-2">
|
||||
{profile.roleNames.map((role, index) => (
|
||||
<div key={index} className="px-3 py-1 bg-green-100 text-green-700 rounded-full text-sm">
|
||||
{role}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* 安全信息 */}
|
||||
<TabsContent value="security" className="space-y-4">
|
||||
<Card className="p-6">
|
||||
<h3 className="mb-4">账户安全</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div>
|
||||
<h4>登录密码</h4>
|
||||
<p className="text-sm text-muted-foreground">定期修改密码可以提高账户安全性</p>
|
||||
</div>
|
||||
<Button variant="outline" onClick={() => setShowPasswordDialog(true)}>
|
||||
修改密码
|
||||
</Button>
|
||||
</div>
|
||||
<div className="p-4 border rounded-lg">
|
||||
<h4 className="mb-2">账户信息</h4>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">账号创建时间</span>
|
||||
<span>{new Date(profile.createdAt).toLocaleString('zh-CN')}</span>
|
||||
</div>
|
||||
{profile.lastLoginTime && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">最后登录时间</span>
|
||||
<span>{new Date(profile.lastLoginTime).toLocaleString('zh-CN')}</span>
|
||||
</div>
|
||||
)}
|
||||
{profile.lastLoginIp && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">最后登录IP</span>
|
||||
<span>
|
||||
<code className="text-xs bg-gray-100 px-2 py-1 rounded">
|
||||
{profile.lastLoginIp}
|
||||
</code>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* 修改密码对话框 */}
|
||||
<Dialog open={showPasswordDialog} onOpenChange={setShowPasswordDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<Lock className="w-5 h-5 text-green-600" />
|
||||
修改密码
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
修改账户密码
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>原密码 *</Label>
|
||||
<Input
|
||||
type="password"
|
||||
value={passwordForm.oldPassword}
|
||||
onChange={(e) => setPasswordForm({ ...passwordForm, oldPassword: e.target.value })}
|
||||
placeholder="请输入原密码"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>新密码 *</Label>
|
||||
<Input
|
||||
type="password"
|
||||
value={passwordForm.newPassword}
|
||||
onChange={(e) => setPasswordForm({ ...passwordForm, newPassword: e.target.value })}
|
||||
placeholder="请输入新密码(至少8位)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>确认新密码 *</Label>
|
||||
<Input
|
||||
type="password"
|
||||
value={passwordForm.confirmPassword}
|
||||
onChange={(e) => setPasswordForm({ ...passwordForm, confirmPassword: e.target.value })}
|
||||
placeholder="请再次输入新密码"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-3 bg-blue-50 border border-blue-200 rounded-lg text-sm text-blue-800">
|
||||
<p>密码要求:</p>
|
||||
<ul className="list-disc list-inside mt-1 space-y-1">
|
||||
<li>长度至少8位</li>
|
||||
<li>建议包含大小写字母、数字和特殊字符</li>
|
||||
<li>不要使用过于简单的密码</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowPasswordDialog(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handlePasswordChange}>
|
||||
确认修改
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 使用说明 */}
|
||||
<Card className="p-4 bg-blue-50 border-blue-200">
|
||||
<h4 className="text-blue-900 mb-2">
|
||||
<User className="w-4 h-4 inline mr-2" />
|
||||
个人信息说明
|
||||
</h4>
|
||||
<ul className="space-y-1 text-sm text-blue-800">
|
||||
<li>• 修改个人信息后需要点击"保存修改"按钮才会生效</li>
|
||||
<li>• 用户名和企业信息由系统管理员管理,个人无法修改</li>
|
||||
<li>• 定期修改密码可以提高账户安全性</li>
|
||||
<li>• 密码修改成功后需要重新登录</li>
|
||||
<li>• 所有个人信息修改都会记录到安全日志中</li>
|
||||
</ul>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// 个人信息页面类型定义
|
||||
|
||||
export interface PersonalInfo {
|
||||
id: string;
|
||||
username: string;
|
||||
realName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
avatar?: string;
|
||||
department?: string;
|
||||
position?: string;
|
||||
gender?: 'male' | 'female' | 'other';
|
||||
birthday?: string;
|
||||
address?: string;
|
||||
bio?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface PersonalInfoForm {
|
||||
realName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
gender?: 'male' | 'female' | 'other';
|
||||
birthday?: string;
|
||||
address?: string;
|
||||
bio?: string;
|
||||
}
|
||||
|
||||
export interface PersonalInfoStats {
|
||||
totalUsers: number;
|
||||
activeUsers: number;
|
||||
inactiveUsers: number;
|
||||
recentlyUpdated: number;
|
||||
}
|
||||
@@ -0,0 +1,807 @@
|
||||
'use client';
|
||||
|
||||
import { useReducer, useMemo } from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Building2, Plus, Eye, Power, PowerOff, Search, Hash, FileText, CreditCard, User } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
// Types
|
||||
interface Enterprise {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
type: string;
|
||||
status: 'active' | 'inactive';
|
||||
auditStatus: 'not_submitted' | 'pending' | 'approved' | 'rejected';
|
||||
createdAt: string;
|
||||
contact?: string;
|
||||
phone?: string;
|
||||
contactPhone?: string;
|
||||
province?: string;
|
||||
city?: string;
|
||||
district?: string;
|
||||
address?: string;
|
||||
registrant?: string;
|
||||
companySize?: string;
|
||||
registeredCapital?: string;
|
||||
establishmentDate?: string;
|
||||
invoiceType?: string;
|
||||
socialCreditCode?: string;
|
||||
businessScope?: string;
|
||||
businessLicense?: string;
|
||||
bankAccount?: string;
|
||||
bankName?: string;
|
||||
bankFullName?: string;
|
||||
bankAddress?: string;
|
||||
bankLicense?: string;
|
||||
legalPerson?: string;
|
||||
idCardFront?: string;
|
||||
idCardBack?: string;
|
||||
}
|
||||
|
||||
interface FormData {
|
||||
name: string;
|
||||
code: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface EnterpriseState {
|
||||
enterprises: Enterprise[];
|
||||
showAddDialog: boolean;
|
||||
showViewDialog: boolean;
|
||||
showStatusDialog: boolean;
|
||||
selectedEnterprise: Enterprise | null;
|
||||
searchText: string;
|
||||
statusAction: 'enable' | 'disable';
|
||||
formData: FormData;
|
||||
}
|
||||
|
||||
type EnterpriseAction =
|
||||
| { type: 'SET_ENTERPRISES'; payload: Enterprise[] }
|
||||
| { type: 'TOGGLE_ADD_DIALOG'; payload: boolean }
|
||||
| { type: 'TOGGLE_VIEW_DIALOG'; payload: boolean }
|
||||
| { type: 'TOGGLE_STATUS_DIALOG'; payload: boolean }
|
||||
| { type: 'SET_SELECTED_ENTERPRISE'; payload: Enterprise | null }
|
||||
| { type: 'SET_SEARCH_TEXT'; payload: string }
|
||||
| { type: 'SET_STATUS_ACTION'; payload: 'enable' | 'disable' }
|
||||
| { type: 'UPDATE_FORM_DATA'; payload: Partial<FormData> }
|
||||
| { type: 'RESET_FORM_DATA' }
|
||||
| { type: 'ADD_ENTERPRISE'; payload: Enterprise }
|
||||
| { type: 'UPDATE_ENTERPRISE_STATUS'; payload: { id: string; status: 'active' | 'inactive' } };
|
||||
|
||||
// Mock data
|
||||
const mockEnterprises: Enterprise[] = [
|
||||
{
|
||||
id: 'ent-001',
|
||||
name: '智慧农业科技有限公司',
|
||||
code: 'ZHNY001',
|
||||
type: '科技企业',
|
||||
status: 'active',
|
||||
auditStatus: 'approved',
|
||||
createdAt: '2024-01-15 10:30:00',
|
||||
contact: '张三',
|
||||
phone: '13800138000',
|
||||
contactPhone: '13800138000',
|
||||
province: '北京市',
|
||||
city: '北京市',
|
||||
district: '海淀区',
|
||||
address: '中关村大街1号',
|
||||
registrant: '李四',
|
||||
companySize: '100-500人',
|
||||
registeredCapital: '1000万元',
|
||||
establishmentDate: '2020-01-01',
|
||||
invoiceType: '增值税专用发票',
|
||||
socialCreditCode: '91110108MA01XXXXXX',
|
||||
businessScope: '技术开发、技术服务、技术咨询',
|
||||
legalPerson: '王五'
|
||||
},
|
||||
{
|
||||
id: 'ent-002',
|
||||
name: '绿色农业合作社',
|
||||
code: 'LSNY002',
|
||||
type: '合作社',
|
||||
status: 'active',
|
||||
auditStatus: 'pending',
|
||||
createdAt: '2024-02-20 14:15:00',
|
||||
contact: '赵六',
|
||||
phone: '13900139000'
|
||||
},
|
||||
{
|
||||
id: 'ent-003',
|
||||
name: '现代农业发展有限公司',
|
||||
code: 'XDNY003',
|
||||
type: '农业企业',
|
||||
status: 'inactive',
|
||||
auditStatus: 'not_submitted',
|
||||
createdAt: '2024-03-10 09:45:00',
|
||||
contact: '钱七',
|
||||
phone: '13700137000'
|
||||
}
|
||||
];
|
||||
|
||||
// Initial state
|
||||
const initialState: EnterpriseState = {
|
||||
enterprises: mockEnterprises,
|
||||
showAddDialog: false,
|
||||
showViewDialog: false,
|
||||
showStatusDialog: false,
|
||||
selectedEnterprise: null,
|
||||
searchText: '',
|
||||
statusAction: 'enable',
|
||||
formData: {
|
||||
name: '',
|
||||
code: '',
|
||||
type: ''
|
||||
}
|
||||
};
|
||||
|
||||
// Reducer
|
||||
function enterpriseReducer(state: EnterpriseState, action: EnterpriseAction): EnterpriseState {
|
||||
switch (action.type) {
|
||||
case 'SET_ENTERPRISES':
|
||||
return { ...state, enterprises: action.payload };
|
||||
|
||||
case 'TOGGLE_ADD_DIALOG':
|
||||
return {
|
||||
...state,
|
||||
showAddDialog: action.payload,
|
||||
...(action.payload === false ? { formData: initialState.formData } : {})
|
||||
};
|
||||
|
||||
case 'TOGGLE_VIEW_DIALOG':
|
||||
return { ...state, showViewDialog: action.payload };
|
||||
|
||||
case 'TOGGLE_STATUS_DIALOG':
|
||||
return { ...state, showStatusDialog: action.payload };
|
||||
|
||||
case 'SET_SELECTED_ENTERPRISE':
|
||||
return { ...state, selectedEnterprise: action.payload };
|
||||
|
||||
case 'SET_SEARCH_TEXT':
|
||||
return { ...state, searchText: action.payload };
|
||||
|
||||
case 'SET_STATUS_ACTION':
|
||||
return { ...state, statusAction: action.payload };
|
||||
|
||||
case 'UPDATE_FORM_DATA':
|
||||
return {
|
||||
...state,
|
||||
formData: { ...state.formData, ...action.payload }
|
||||
};
|
||||
|
||||
case 'RESET_FORM_DATA':
|
||||
return { ...state, formData: initialState.formData };
|
||||
|
||||
case 'ADD_ENTERPRISE':
|
||||
return {
|
||||
...state,
|
||||
enterprises: [...state.enterprises, action.payload]
|
||||
};
|
||||
|
||||
case 'UPDATE_ENTERPRISE_STATUS':
|
||||
return {
|
||||
...state,
|
||||
enterprises: state.enterprises.map(ent =>
|
||||
ent.id === action.payload.id
|
||||
? { ...ent, status: action.payload.status }
|
||||
: ent
|
||||
)
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
const getStatusBadge = (status: 'active' | 'inactive') => {
|
||||
if (status === 'active') {
|
||||
return <Badge className="bg-green-100 text-green-800">启用</Badge>;
|
||||
}
|
||||
return <Badge className="bg-gray-100 text-gray-800">禁用</Badge>;
|
||||
};
|
||||
|
||||
const getAuditStatusBadge = (auditStatus?: 'not_submitted' | 'pending' | 'approved' | 'rejected') => {
|
||||
switch (auditStatus) {
|
||||
case 'not_submitted':
|
||||
return <Badge className="bg-gray-100 text-gray-700">未提交</Badge>;
|
||||
case 'pending':
|
||||
return <Badge className="bg-yellow-100 text-yellow-700">待审核</Badge>;
|
||||
case 'approved':
|
||||
return <Badge className="bg-green-100 text-green-700">审核通过</Badge>;
|
||||
case 'rejected':
|
||||
return <Badge className="bg-red-100 text-red-700">已驳回</Badge>;
|
||||
default:
|
||||
return <Badge className="bg-gray-100 text-gray-700">未提交</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
export default function EnterpriseManagement() {
|
||||
const [state, dispatch] = useReducer(enterpriseReducer, initialState);
|
||||
|
||||
// Computed values
|
||||
const filteredEnterprises = useMemo(() => {
|
||||
return state.enterprises.filter(ent => {
|
||||
if (!state.searchText) return true;
|
||||
const searchLower = state.searchText.toLowerCase();
|
||||
return (
|
||||
ent.name.toLowerCase().includes(searchLower) ||
|
||||
ent.code.toLowerCase().includes(searchLower) ||
|
||||
(ent.type && ent.type.toLowerCase().includes(searchLower))
|
||||
);
|
||||
});
|
||||
}, [state.enterprises, state.searchText]);
|
||||
|
||||
const stats = useMemo(() => ({
|
||||
total: state.enterprises.length,
|
||||
active: state.enterprises.filter(e => e.status === 'active').length,
|
||||
inactive: state.enterprises.filter(e => e.status === 'inactive').length,
|
||||
}), [state.enterprises]);
|
||||
|
||||
// Event handlers
|
||||
const handleAdd = () => {
|
||||
dispatch({ type: 'RESET_FORM_DATA' });
|
||||
dispatch({ type: 'TOGGLE_ADD_DIALOG', payload: true });
|
||||
};
|
||||
|
||||
const handleView = (enterprise: Enterprise) => {
|
||||
dispatch({ type: 'SET_SELECTED_ENTERPRISE', payload: enterprise });
|
||||
dispatch({ type: 'TOGGLE_VIEW_DIALOG', payload: true });
|
||||
};
|
||||
|
||||
const handleStatusChange = (enterprise: Enterprise, action: 'enable' | 'disable') => {
|
||||
dispatch({ type: 'SET_SELECTED_ENTERPRISE', payload: enterprise });
|
||||
dispatch({ type: 'SET_STATUS_ACTION', payload: action });
|
||||
dispatch({ type: 'TOGGLE_STATUS_DIALOG', payload: true });
|
||||
};
|
||||
|
||||
const confirmAdd = () => {
|
||||
const { formData } = state;
|
||||
|
||||
if (!formData.name.trim()) {
|
||||
toast.error('请输入企业名称');
|
||||
return;
|
||||
}
|
||||
if (!formData.code.trim()) {
|
||||
toast.error('请输入企业编码');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查编码是否重复
|
||||
if (state.enterprises.some(e => e.code === formData.code)) {
|
||||
toast.error('企业编码已存在');
|
||||
return;
|
||||
}
|
||||
|
||||
const newEnterprise: Enterprise = {
|
||||
id: `ent-${Date.now()}`,
|
||||
name: formData.name,
|
||||
code: formData.code,
|
||||
type: formData.type || '未分类',
|
||||
status: 'active',
|
||||
auditStatus: 'not_submitted',
|
||||
createdAt: new Date().toISOString().replace('T', ' ').substring(0, 19),
|
||||
};
|
||||
|
||||
dispatch({ type: 'ADD_ENTERPRISE', payload: newEnterprise });
|
||||
dispatch({ type: 'TOGGLE_ADD_DIALOG', payload: false });
|
||||
toast.success('企业创建成功');
|
||||
};
|
||||
|
||||
const confirmStatusChange = () => {
|
||||
if (!state.selectedEnterprise) return;
|
||||
|
||||
const newStatus = state.statusAction === 'enable' ? 'active' : 'inactive';
|
||||
|
||||
dispatch({
|
||||
type: 'UPDATE_ENTERPRISE_STATUS',
|
||||
payload: { id: state.selectedEnterprise.id, status: newStatus }
|
||||
});
|
||||
|
||||
dispatch({ type: 'TOGGLE_STATUS_DIALOG', payload: false });
|
||||
toast.success(state.statusAction === 'enable' ? '企业已启用' : '企业已禁用');
|
||||
};
|
||||
|
||||
const updateFormData = (field: keyof FormData, value: string) => {
|
||||
dispatch({ type: 'UPDATE_FORM_DATA', payload: { [field]: value || '' } });
|
||||
};
|
||||
|
||||
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">
|
||||
<Building2 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">
|
||||
<Plus className="w-3 h-3 mr-1" />
|
||||
快速创建
|
||||
</Badge>
|
||||
<Badge variant="outline" className="bg-white">
|
||||
<Power className="w-3 h-3 mr-1" />
|
||||
状态管理
|
||||
</Badge>
|
||||
<Badge variant="outline" className="bg-white">
|
||||
<Eye className="w-3 h-3 mr-1" />
|
||||
详情查看
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Statistics Cards */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-sm text-muted-foreground">企业总数</div>
|
||||
<Building2 className="w-5 h-5 text-blue-500" />
|
||||
</div>
|
||||
<div className="text-3xl font-bold mb-1">{stats.total}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
全部企业数量
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-sm text-muted-foreground">启用企业</div>
|
||||
<Power className="w-5 h-5 text-green-500" />
|
||||
</div>
|
||||
<div className="text-3xl font-bold mb-1">{stats.active}</div>
|
||||
<div className="text-xs text-green-600">
|
||||
正常运营中
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-sm text-muted-foreground">禁用企业</div>
|
||||
<PowerOff className="w-5 h-5 text-gray-500" />
|
||||
</div>
|
||||
<div className="text-3xl font-bold mb-1">{stats.inactive}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
已暂停使用
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Enterprise List */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3>企业列表</h3>
|
||||
<div className="flex gap-2">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="搜索企业名称、编码..."
|
||||
value={state.searchText}
|
||||
onChange={(e) => dispatch({ type: 'SET_SEARCH_TEXT', payload: e.target.value || '' })}
|
||||
className="pl-10 w-64"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={handleAdd}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
新建企业
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>企业编码</TableHead>
|
||||
<TableHead>企业名称</TableHead>
|
||||
<TableHead>企业类型</TableHead>
|
||||
<TableHead>联系人</TableHead>
|
||||
<TableHead>联系电话</TableHead>
|
||||
<TableHead>创建时间</TableHead>
|
||||
<TableHead>审核状态</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredEnterprises.map((enterprise) => (
|
||||
<TableRow key={enterprise.id}>
|
||||
<TableCell className="font-medium">{enterprise.code}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<Building2 className="w-4 h-4 text-blue-500" />
|
||||
<span className="font-medium">{enterprise.name}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{enterprise.type || '未分类'}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{enterprise.contact || '-'}</TableCell>
|
||||
<TableCell>{enterprise.phone || '-'}</TableCell>
|
||||
<TableCell className="text-sm">{enterprise.createdAt}</TableCell>
|
||||
<TableCell>{getAuditStatusBadge(enterprise.auditStatus)}</TableCell>
|
||||
<TableCell>{getStatusBadge(enterprise.status)}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleView(enterprise)}
|
||||
>
|
||||
<Eye className="w-3 h-3 mr-1" />
|
||||
查看
|
||||
</Button>
|
||||
{enterprise.status === 'active' ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-gray-600 border-gray-300"
|
||||
onClick={() => handleStatusChange(enterprise, 'disable')}
|
||||
>
|
||||
<PowerOff className="w-3 h-3 mr-1" />
|
||||
禁用
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-green-600 border-green-300"
|
||||
onClick={() => handleStatusChange(enterprise, 'enable')}
|
||||
>
|
||||
<Power className="w-3 h-3 mr-1" />
|
||||
启用
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{filteredEnterprises.length === 0 && (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
<Building2 className="w-12 h-12 mx-auto mb-4 opacity-20" />
|
||||
<p>暂无企业数据</p>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Add Enterprise Dialog */}
|
||||
<Dialog open={state.showAddDialog} onOpenChange={(open) => dispatch({ type: 'TOGGLE_ADD_DIALOG', payload: open })}>
|
||||
<DialogContent className="w-[80vw] max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>新建企业</DialogTitle>
|
||||
<DialogDescription>
|
||||
创建新企业账号,管理员仅需填写基本信息,详细信息由企业登录后自行完善
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>企业名称 *</Label>
|
||||
<div className="relative mt-2">
|
||||
<Building2 className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="请输入企业全称"
|
||||
value={state.formData.name}
|
||||
onChange={(e) => updateFormData('name', e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>企业编码 *</Label>
|
||||
<div className="relative mt-2">
|
||||
<Hash className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="请输入企业唯一编码,如:LYNY001"
|
||||
value={state.formData.code}
|
||||
onChange={(e) => updateFormData('code', e.target.value.toUpperCase())}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">编码创建后不可修改,请谨慎填写</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>企业类型</Label>
|
||||
<div className="relative mt-2">
|
||||
<Building2 className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="如:种植企业、养殖企业、合作社等"
|
||||
value={state.formData.type}
|
||||
onChange={(e) => updateFormData('type', e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<p className="text-sm text-blue-800">
|
||||
<strong>温馨提示:</strong>
|
||||
</p>
|
||||
<ul className="text-sm text-blue-700 mt-2 space-y-1">
|
||||
<li>• 企业创建后默认为启用状态,审核状态为"未提交"</li>
|
||||
<li>• 联系人、电话、地址等详细信息由企业登录后自行填写</li>
|
||||
<li>• 企业编码创建后不可修改,请确保准确无误</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => dispatch({ type: 'TOGGLE_ADD_DIALOG', payload: false })}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={confirmAdd}>创建企业</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* View Enterprise Details Dialog */}
|
||||
<Dialog open={state.showViewDialog} onOpenChange={(open) => dispatch({ type: 'TOGGLE_VIEW_DIALOG', payload: open })}>
|
||||
<DialogContent className="w-[80vw] max-w-6xl max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center justify-between pr-8">
|
||||
<DialogTitle>企业详情</DialogTitle>
|
||||
{state.selectedEnterprise && (
|
||||
<div className="flex gap-2">
|
||||
{getAuditStatusBadge(state.selectedEnterprise.auditStatus)}
|
||||
{getStatusBadge(state.selectedEnterprise.status)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogDescription className="sr-only">
|
||||
查看企业的详细信息
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{state.selectedEnterprise && (
|
||||
<ScrollArea className="max-h-[calc(90vh-200px)]">
|
||||
<Tabs defaultValue="basic" className="space-y-4">
|
||||
<TabsList className="grid grid-cols-4 w-full">
|
||||
<TabsTrigger value="basic">
|
||||
<Building2 className="w-4 h-4 mr-2" />
|
||||
基本信息
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="other">
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
其他信息
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="bank">
|
||||
<CreditCard className="w-4 h-4 mr-2" />
|
||||
开户信息
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="legal">
|
||||
<User className="w-4 h-4 mr-2" />
|
||||
法人信息
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Basic Information */}
|
||||
<TabsContent value="basic" className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label>企业名称</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{state.selectedEnterprise.name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>企业编码</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{state.selectedEnterprise.code}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>企业类型</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{state.selectedEnterprise.type || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>所在地区</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">
|
||||
{state.selectedEnterprise.province || '-'} {state.selectedEnterprise.city || ''} {state.selectedEnterprise.district || ''}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label>详细地址</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{state.selectedEnterprise.address || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>登记人</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{state.selectedEnterprise.registrant || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>联系电话</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{state.selectedEnterprise.contactPhone || state.selectedEnterprise.phone || '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Other Information */}
|
||||
<TabsContent value="other" className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label>公司规模</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{state.selectedEnterprise.companySize || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>注册资本</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{state.selectedEnterprise.registeredCapital || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>成立时间</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{state.selectedEnterprise.establishmentDate || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>发票类型</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{state.selectedEnterprise.invoiceType || '-'}</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label>社会信用代码</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">
|
||||
{state.selectedEnterprise.socialCreditCode ? (
|
||||
<code className="text-sm font-mono">
|
||||
{state.selectedEnterprise.socialCreditCode}
|
||||
</code>
|
||||
) : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label>经营范围</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{state.selectedEnterprise.businessScope || '-'}</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label>营业执照</Label>
|
||||
<div className="mt-2">
|
||||
{state.selectedEnterprise.businessLicense ? (
|
||||
<img
|
||||
src={state.selectedEnterprise.businessLicense}
|
||||
alt="营业执照"
|
||||
className="w-64 h-auto border rounded-lg"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-muted-foreground">未上传</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Bank Information */}
|
||||
<TabsContent value="bank" className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label>银行账号</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">
|
||||
{state.selectedEnterprise.bankAccount ? (
|
||||
<code className="text-sm font-mono">
|
||||
{state.selectedEnterprise.bankAccount}
|
||||
</code>
|
||||
) : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>开户行</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{state.selectedEnterprise.bankName || '-'}</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label>开户行全称</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{state.selectedEnterprise.bankFullName || '-'}</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label>开户行地址</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{state.selectedEnterprise.bankAddress || '-'}</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label>开户许可证</Label>
|
||||
<div className="mt-2">
|
||||
{state.selectedEnterprise.bankLicense ? (
|
||||
<img
|
||||
src={state.selectedEnterprise.bankLicense}
|
||||
alt="开户许可证"
|
||||
className="w-64 h-auto border rounded-lg"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-muted-foreground">未上传</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Legal Person Information */}
|
||||
<TabsContent value="legal" className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label>法人姓名</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{state.selectedEnterprise.legalPerson || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>联系人</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{state.selectedEnterprise.contact || '-'}</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label>身份证正面</Label>
|
||||
<div className="mt-2">
|
||||
{state.selectedEnterprise.idCardFront ? (
|
||||
<img
|
||||
src={state.selectedEnterprise.idCardFront}
|
||||
alt="身份证正面"
|
||||
className="w-64 h-auto border rounded-lg"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-muted-foreground">未上传</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label>身份证反面</Label>
|
||||
<div className="mt-2">
|
||||
{state.selectedEnterprise.idCardBack ? (
|
||||
<img
|
||||
src={state.selectedEnterprise.idCardBack}
|
||||
alt="身份证反面"
|
||||
className="w-64 h-auto border rounded-lg"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-muted-foreground">未上传</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</ScrollArea>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => dispatch({ type: 'TOGGLE_VIEW_DIALOG', payload: false })}>
|
||||
关闭
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Status Change Confirmation Dialog */}
|
||||
<AlertDialog open={state.showStatusDialog} onOpenChange={(open) => dispatch({ type: 'TOGGLE_STATUS_DIALOG', payload: open })}>
|
||||
<AlertDialogContent className="w-[80vw] max-w-md">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
确认{state.statusAction === 'enable' ? '启用' : '禁用'}企业
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{state.statusAction === 'enable' ? (
|
||||
<>
|
||||
启用企业 <strong>{state.selectedEnterprise?.name}</strong> 后,该企业用户将恢复正常登录和使用权限。
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
禁用企业 <strong>{state.selectedEnterprise?.name}</strong> 后,该企业所有用户将无法登录系统。此操作不会删除企业数据,可随时重新启用。
|
||||
</>
|
||||
)}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmStatusChange}
|
||||
className={state.statusAction === 'enable' ? 'bg-green-600 hover:bg-green-700' : 'bg-gray-600 hover:bg-gray-700'}
|
||||
>
|
||||
确认{state.statusAction === 'enable' ? '启用' : '禁用'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,15 +2,36 @@ import {Navbar1} from "@/components/layouts/Navbar"
|
||||
import {SideBarOld} from '@/components/layouts/SideBar/SideBarOld'
|
||||
import '@/styles/globals.css'
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import { Building2, Users, Cog, Activity, Mail } from 'lucide-react'
|
||||
import { Building2, Users, Cog, Activity, Mail, UserCircle } from 'lucide-react'
|
||||
|
||||
const centralConfigData = {
|
||||
navMain: [
|
||||
{
|
||||
title: '个人中心',
|
||||
url: "/central-config/personal-center",
|
||||
icon: <UserCircle className="w-4 h-4" />,
|
||||
items: [
|
||||
{
|
||||
title: "个人信息",
|
||||
url: "/central-config/personal-center/personal-info",
|
||||
isActive: false
|
||||
},{
|
||||
title: "账户安全",
|
||||
url: "/central-config/personal-center/account-security",
|
||||
isActive: false
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "租户管理",
|
||||
url: "/central-config/tenant",
|
||||
icon: <Building2 className="w-4 h-4" />,
|
||||
items: [
|
||||
{
|
||||
title: "企业管理",
|
||||
url: "/central-config/tenant/enterprise-management",
|
||||
isActive: false
|
||||
},
|
||||
{
|
||||
title: "企业审核",
|
||||
url: "/central-config/tenant/enterprise-audit",
|
||||
|
||||
27
crop-x/src/types/profile.ts
Normal file
27
crop-x/src/types/profile.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export interface UserProfile {
|
||||
id: string;
|
||||
username: string;
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
avatar: string;
|
||||
gender: 'male' | 'female';
|
||||
birthday: string;
|
||||
department: string;
|
||||
position: string;
|
||||
enterpriseId: string;
|
||||
enterpriseName: string;
|
||||
roleIds: string[];
|
||||
roleNames: string[];
|
||||
bio: string;
|
||||
address: string;
|
||||
createdAt: string;
|
||||
lastLoginTime?: string;
|
||||
lastLoginIp?: string;
|
||||
}
|
||||
|
||||
export interface PasswordChange {
|
||||
oldPassword: string;
|
||||
newPassword: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
Reference in New Issue
Block a user