生产管理系统前端 提交个人中心2个页面开发

This commit is contained in:
2025-10-28 15:22:54 +08:00
parent 2c3227fb64
commit 26213aaa76
18 changed files with 2866 additions and 1 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,549 @@
'use client';
import { useReducer } from 'react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
import { Shield, Lock, Key, CheckCircle, XCircle, Eye, EyeOff, AlertTriangle } from 'lucide-react';
import { toast } from 'sonner';
// Types
interface PasswordForm {
oldPassword: string;
newPassword: string;
confirmPassword: string;
}
interface SecurityState {
user: {
username: string;
phone: string;
email: string;
lastLoginTime: string;
lastLoginDevice: string;
lastLoginIp: string;
};
showPasswordDialog: boolean;
showOldPassword: boolean;
showNewPassword: boolean;
showConfirmPassword: boolean;
passwordForm: PasswordForm;
passwordStrength: {
checks: {
length: boolean;
hasUpper: boolean;
hasLower: boolean;
hasNumber: boolean;
hasSpecial: boolean;
};
strength: 'weak' | 'medium' | 'strong';
passedCount: number;
};
}
type SecurityAction =
| { type: 'TOGGLE_PASSWORD_DIALOG'; payload: boolean }
| { type: 'TOGGLE_OLD_PASSWORD_VISIBILITY' }
| { type: 'TOGGLE_NEW_PASSWORD_VISIBILITY' }
| { type: 'TOGGLE_CONFIRM_PASSWORD_VISIBILITY' }
| { type: 'UPDATE_PASSWORD_FORM'; payload: Partial<PasswordForm> }
| { type: 'UPDATE_PASSWORD_STRENGTH'; payload: SecurityState['passwordStrength'] };
// Initial state
const initialState: SecurityState = {
user: {
username: 'admin',
phone: '13800138000',
email: 'admin@smart-agriculture.com',
lastLoginTime: '2024-10-14 09:30:00',
lastLoginDevice: 'Windows PC - Chrome 120.0',
lastLoginIp: '192.168.1.100'
},
showPasswordDialog: false,
showOldPassword: false,
showNewPassword: false,
showConfirmPassword: false,
passwordForm: {
oldPassword: '',
newPassword: '',
confirmPassword: ''
},
passwordStrength: {
checks: {
length: false,
hasUpper: false,
hasLower: false,
hasNumber: false,
hasSpecial: false
},
strength: 'weak',
passedCount: 0
}
};
// Reducer
function securityReducer(state: SecurityState, action: SecurityAction): SecurityState {
switch (action.type) {
case 'TOGGLE_PASSWORD_DIALOG':
return {
...state,
showPasswordDialog: action.payload,
...(action.payload === false ? {
passwordForm: {
oldPassword: '',
newPassword: '',
confirmPassword: ''
},
passwordStrength: {
checks: {
length: false,
hasUpper: false,
hasLower: false,
hasNumber: false,
hasSpecial: false
},
strength: 'weak' as const,
passedCount: 0
}
} : {})
};
case 'TOGGLE_OLD_PASSWORD_VISIBILITY':
return { ...state, showOldPassword: !state.showOldPassword };
case 'TOGGLE_NEW_PASSWORD_VISIBILITY':
return { ...state, showNewPassword: !state.showNewPassword };
case 'TOGGLE_CONFIRM_PASSWORD_VISIBILITY':
return { ...state, showConfirmPassword: !state.showConfirmPassword };
case 'UPDATE_PASSWORD_FORM':
return {
...state,
passwordForm: { ...state.passwordForm, ...action.payload }
};
case 'UPDATE_PASSWORD_STRENGTH':
return {
...state,
passwordStrength: action.payload
};
default:
return state;
}
}
// Password strength checker
const checkPasswordStrength = (password: string): SecurityState['passwordStrength'] => {
const checks = {
length: password.length >= 8,
hasUpper: /[A-Z]/.test(password),
hasLower: /[a-z]/.test(password),
hasNumber: /[0-9]/.test(password),
hasSpecial: /[!@#$%^&*(),.?":{}|<>]/.test(password),
};
const passedCount = Object.values(checks).filter(Boolean).length;
let strength: 'weak' | 'medium' | 'strong' = 'weak';
if (passedCount >= 4) strength = 'strong';
else if (passedCount >= 3) strength = 'medium';
return { checks, strength, passedCount };
};
// Utility functions
const getStrengthColor = (strength: 'weak' | 'medium' | 'strong') => {
switch (strength) {
case 'strong':
return 'text-green-600';
case 'medium':
return 'text-yellow-600';
case 'weak':
return 'text-red-600';
}
};
const getStrengthBg = (strength: 'weak' | 'medium' | 'strong') => {
switch (strength) {
case 'strong':
return 'bg-green-100';
case 'medium':
return 'bg-yellow-100';
case 'weak':
return 'bg-red-100';
}
};
const getStrengthText = (strength: 'weak' | 'medium' | 'strong') => {
switch (strength) {
case 'strong':
return '强';
case 'medium':
return '中';
case 'weak':
return '弱';
}
};
export default function AccountSecurity() {
const [state, dispatch] = useReducer(securityReducer, initialState);
const handleChangePassword = () => {
dispatch({ type: 'UPDATE_PASSWORD_FORM', payload: { oldPassword: '', newPassword: '', confirmPassword: '' } });
dispatch({ type: 'TOGGLE_PASSWORD_DIALOG', payload: true });
};
const handleConfirmChangePassword = () => {
const { passwordForm } = state;
// Validation
if (!passwordForm.oldPassword) {
toast.error('请输入原密码');
return;
}
if (!passwordForm.newPassword) {
toast.error('请输入新密码');
return;
}
if (passwordForm.newPassword.length < 8) {
toast.error('密码长度至少为8位');
return;
}
if (state.passwordStrength.strength === 'weak') {
toast.error('密码强度过弱,请使用更复杂的密码');
return;
}
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
toast.error('两次输入的密码不一致');
return;
}
if (passwordForm.oldPassword === passwordForm.newPassword) {
toast.error('新密码不能与原密码相同');
return;
}
// Mock password validation (assuming old password is "123456")
if (passwordForm.oldPassword !== '123456') {
toast.error('原密码错误');
return;
}
// Success
toast.success('密码修改成功,请使用新密码重新登录');
dispatch({ type: 'TOGGLE_PASSWORD_DIALOG', payload: false });
};
const updatePassword = (field: keyof PasswordForm, value: string) => {
const updatePayload = {
[field]: value || ''
};
dispatch({ type: 'UPDATE_PASSWORD_FORM', payload: updatePayload });
if (field === 'newPassword') {
const strength = checkPasswordStrength(value || '');
dispatch({ type: 'UPDATE_PASSWORD_STRENGTH', payload: strength });
}
};
return (
<div className="space-y-6">
{/* Page Header */}
<Card className="p-6 bg-gradient-to-r from-blue-50 to-indigo-50 border-blue-200">
<div className="flex items-start gap-3">
<Shield className="w-6 h-6 text-blue-600 flex-shrink-0 mt-1" />
<div className="flex-1">
<h2 className="text-green-800 mb-2"></h2>
<p className="text-sm text-muted-foreground mb-3">
</p>
<div className="flex flex-wrap gap-2">
<Badge variant="outline" className="bg-white">
<Lock className="w-3 h-3 mr-1" />
</Badge>
<Badge variant="outline" className="bg-white">
<Key className="w-3 h-3 mr-1" />
</Badge>
<Badge variant="outline" className="bg-white">
<Shield className="w-3 h-3 mr-1" />
</Badge>
</div>
</div>
</div>
</Card>
{/* Account Overview */}
<Card className="p-6">
<h3 className="mb-4"></h3>
<div className="grid grid-cols-2 gap-6">
<div className="p-4 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground mb-2"></div>
<div className="font-medium">{state.user.username}</div>
</div>
<div className="p-4 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground mb-2"></div>
<div className="font-medium">{state.user.phone}</div>
</div>
<div className="p-4 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground mb-2"></div>
<div className="font-medium">{state.user.email}</div>
</div>
<div className="p-4 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground mb-2"></div>
<div className="font-medium text-sm">{state.user.lastLoginTime}</div>
</div>
</div>
</Card>
{/* Password Management */}
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="mb-1"></h3>
<p className="text-sm text-muted-foreground"></p>
</div>
<Button onClick={handleChangePassword} className="gap-2">
<Key className="w-4 h-4" />
</Button>
</div>
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
<div className="flex items-start gap-3">
<Shield className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<h4 className="text-sm font-medium text-blue-900 mb-2"></h4>
<ul className="text-sm text-blue-800 space-y-1">
<li> 8</li>
<li> </li>
<li> 使</li>
<li> 3</li>
<li> 使</li>
</ul>
</div>
</div>
</div>
</Card>
{/* Security Settings */}
<Card className="p-6">
<h3 className="mb-4"></h3>
<div className="space-y-4">
<div className="flex items-center justify-between p-4 bg-muted rounded-lg">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
<CheckCircle className="w-5 h-5 text-green-600" />
</div>
<div>
<div className="font-medium"></div>
<div className="text-sm text-muted-foreground">{state.user.phone}</div>
</div>
</div>
<Badge className="bg-green-100 text-green-700"></Badge>
</div>
<div className="flex items-center justify-between p-4 bg-muted rounded-lg">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
<Lock className="w-5 h-5 text-blue-600" />
</div>
<div>
<div className="font-medium"></div>
<div className="text-sm text-muted-foreground"></div>
</div>
</div>
<Badge className="bg-blue-100 text-blue-700"></Badge>
</div>
<div className="flex items-center justify-between p-4 bg-muted rounded-lg">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-yellow-100 rounded-full flex items-center justify-center">
<AlertTriangle className="w-5 h-5 text-yellow-600" />
</div>
<div>
<div className="font-medium"></div>
<div className="text-sm text-muted-foreground">{state.user.email}</div>
</div>
</div>
<Badge className="bg-green-100 text-green-700"></Badge>
</div>
</div>
</Card>
{/* Recent Login Records */}
<Card className="p-6">
<h3 className="mb-4"></h3>
<div className="space-y-3">
<div className="flex items-center justify-between p-3 bg-muted rounded-lg">
<div className="flex items-center gap-3">
<CheckCircle className="w-5 h-5 text-green-600" />
<div>
<div className="text-sm font-medium"></div>
<div className="text-xs text-muted-foreground">
{state.user.lastLoginDevice} · {state.user.lastLoginIp}
</div>
</div>
</div>
<div className="text-sm text-muted-foreground">
{state.user.lastLoginTime}
</div>
</div>
</div>
</Card>
{/* Change Password Dialog */}
<Dialog open={state.showPasswordDialog} onOpenChange={(open) => dispatch({ type: 'TOGGLE_PASSWORD_DIALOG', payload: open })}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>
<div className="flex items-center gap-2">
<Lock className="w-5 h-5 text-blue-600" />
</div>
</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* Old Password */}
<div>
<Label> *</Label>
<div className="relative mt-2">
<Input
type={state.showOldPassword ? 'text' : 'password'}
value={state.passwordForm.oldPassword}
onChange={(e) => updatePassword('oldPassword', e.target.value)}
placeholder="请输入原密码"
className="pr-10"
/>
<button
type="button"
onClick={() => dispatch({ type: 'TOGGLE_OLD_PASSWORD_VISIBILITY' })}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{state.showOldPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
{/* New Password */}
<div>
<Label> *</Label>
<div className="relative mt-2">
<Input
type={state.showNewPassword ? 'text' : 'password'}
value={state.passwordForm.newPassword}
onChange={(e) => updatePassword('newPassword', e.target.value)}
placeholder="请输入新密码至少8位"
className="pr-10"
/>
<button
type="button"
onClick={() => dispatch({ type: 'TOGGLE_NEW_PASSWORD_VISIBILITY' })}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{state.showNewPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
{/* Password Strength Indicator */}
{state.passwordForm.newPassword && (
<div className="mt-3 space-y-2">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground"></span>
<Badge className={`${getStrengthBg(state.passwordStrength.strength)} ${getStrengthColor(state.passwordStrength.strength)}`}>
{getStrengthText(state.passwordStrength.strength)}
</Badge>
</div>
<div className="space-y-1.5">
<div className={`flex items-center gap-2 text-sm ${state.passwordStrength.checks.length ? 'text-green-600' : 'text-muted-foreground'}`}>
{state.passwordStrength.checks.length ? <CheckCircle className="w-4 h-4" /> : <XCircle className="w-4 h-4" />}
<span>8</span>
</div>
<div className={`flex items-center gap-2 text-sm ${state.passwordStrength.checks.hasUpper ? 'text-green-600' : 'text-muted-foreground'}`}>
{state.passwordStrength.checks.hasUpper ? <CheckCircle className="w-4 h-4" /> : <XCircle className="w-4 h-4" />}
<span></span>
</div>
<div className={`flex items-center gap-2 text-sm ${state.passwordStrength.checks.hasLower ? 'text-green-600' : 'text-muted-foreground'}`}>
{state.passwordStrength.checks.hasLower ? <CheckCircle className="w-4 h-4" /> : <XCircle className="w-4 h-4" />}
<span></span>
</div>
<div className={`flex items-center gap-2 text-sm ${state.passwordStrength.checks.hasNumber ? 'text-green-600' : 'text-muted-foreground'}`}>
{state.passwordStrength.checks.hasNumber ? <CheckCircle className="w-4 h-4" /> : <XCircle className="w-4 h-4" />}
<span></span>
</div>
<div className={`flex items-center gap-2 text-sm ${state.passwordStrength.checks.hasSpecial ? 'text-green-600' : 'text-muted-foreground'}`}>
{state.passwordStrength.checks.hasSpecial ? <CheckCircle className="w-4 h-4" /> : <XCircle className="w-4 h-4" />}
<span></span>
</div>
</div>
</div>
)}
</div>
{/* Confirm Password */}
<div>
<Label> *</Label>
<div className="relative mt-2">
<Input
type={state.showConfirmPassword ? 'text' : 'password'}
value={state.passwordForm.confirmPassword}
onChange={(e) => updatePassword('confirmPassword', e.target.value)}
placeholder="请再次输入新密码"
className="pr-10"
/>
<button
type="button"
onClick={() => dispatch({ type: 'TOGGLE_CONFIRM_PASSWORD_VISIBILITY' })}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{state.showConfirmPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
{state.passwordForm.confirmPassword && state.passwordForm.newPassword !== state.passwordForm.confirmPassword && (
<p className="text-sm text-red-600 mt-2 flex items-center gap-1">
<XCircle className="w-4 h-4" />
</p>
)}
</div>
{/* Security Notice */}
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<div className="flex items-start gap-2">
<AlertTriangle className="w-4 h-4 text-yellow-600 flex-shrink-0 mt-0.5" />
<p className="text-sm text-yellow-800">
</p>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => dispatch({ type: 'TOGGLE_PASSWORD_DIALOG', payload: false })}>
</Button>
<Button onClick={handleConfirmChangePassword}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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