生产管理系统前端 开发中心配置系统 所有页面

This commit is contained in:
2025-10-21 18:04:39 +08:00
parent 4a5d278d89
commit 9afc680833
185 changed files with 13677 additions and 4487 deletions

View File

@@ -8,125 +8,125 @@ const centralConfigData = {
navMain: [ navMain: [
{ {
title: "租户管理", title: "租户管理",
url: "/central-config/tenant-management", url: "/central-config/tenant",
icon: "🏢", icon: "🏢",
items: [ items: [
{ {
title: "企业审核", title: "企业审核",
url: "/central-config/tenant-management/enterprise-audit", url: "/central-config/tenant/enterprise-audit",
isActive: false isActive: false
}, },
{ {
title: "审核历史", title: "审核历史",
url: "/central-config/tenant-management/audit-history", url: "/central-config/tenant/audit-history",
isActive: false isActive: false
}, },
{ {
title: "企业信息", title: "企业信息",
url: "/central-config/tenant-management/enterprise-info", url: "/central-config/tenant/enterprise-info",
isActive: false isActive: false
}, },
{ {
title: "平台用户管理", title: "用户管理",
url: "/central-config/tenant-management/platform-user-management", url: "/central-config/tenant/user-management",
isActive: false isActive: false
} }
] ]
}, },
{ {
title: "用户管理", title: "用户管理",
url: "/central-config/user-management", url: "/central-config/user",
icon: "👥", icon: "👥",
items: [ items: [
{ {
title: "员工管理", title: "员工管理",
url: "/central-config/user-management/employee-management", url: "/central-config/user/employee",
isActive: false isActive: false
}, },
{ {
title: "角色管理", title: "角色管理",
url: "/central-config/user-management/role-management", url: "/central-config/user/role",
isActive: false isActive: false
}, },
{ {
title: "菜单管理", title: "菜单管理",
url: "/central-config/user-management/menu-management", url: "/central-config/user/menu",
isActive: false isActive: false
}, },
{ {
title: "权限配置管理", title: "权限配置管理",
url: "/central-config/user-management/permission-config", url: "/central-config/user/permission",
isActive: false isActive: false
} }
] ]
}, },
{ {
title: "系统参数", title: "系统参数",
url: "/central-config/system-parameters", url: "/central-config/system",
icon: "🔧", icon: "🔧",
items: [ items: [
{ {
title: "系统设置", title: "系统设置",
url: "/central-config/system-parameters/system-settings", url: "/central-config/system/settings",
isActive: false isActive: false
}, },
{ {
title: "分类字典", title: "分类字典",
url: "/central-config/system-parameters/category-dictionary", url: "/central-config/system/category",
isActive: false isActive: false
}, },
{ {
title: "数据字典", title: "数据字典",
url: "/central-config/system-parameters/data-dictionary", url: "/central-config/system/dictionary",
isActive: false isActive: false
} }
] ]
}, },
{ {
title: "系统监控", title: "系统监控",
url: "/central-config/system-monitor", url: "/central-config/monitor",
icon: "📈", icon: "📈",
items: [ items: [
{ {
title: "登录日志", title: "登录日志",
url: "/central-config/system-monitor/login-log", url: "/central-config/monitor/login-log",
isActive: false isActive: false
}, },
{ {
title: "操作日志", title: "操作日志",
url: "/central-config/system-monitor/operation-log", url: "/central-config/monitor/operation-log",
isActive: false isActive: false
}, },
{ {
title: "性能监控", title: "性能监控",
url: "/central-config/system-monitor/performance-monitor", url: "/central-config/monitor/performance",
isActive: false isActive: false
}, },
{ {
title: "网络日志", title: "网络日志",
url: "/central-config/system-monitor/network-log", url: "/central-config/monitor/network-log",
isActive: false isActive: false
} }
] ]
}, },
{ {
title: "消息中心", title: "消息中心",
url: "/central-config/message-center", url: "/central-config/message",
icon: "📨", icon: "📨",
items: [ items: [
{ {
title: "消息发送", title: "消息发送",
url: "/central-config/message-center/message-send", url: "/central-config/message/send",
isActive: false isActive: false
}, },
{ {
title: "消息模版", title: "消息模版",
url: "/central-config/message-center/message-template", url: "/central-config/message/template",
isActive: false isActive: false
}, },
{ {
title: "消息日志", title: "消息日志",
url: "/central-config/message-center/message-log", url: "/central-config/message/log",
isActive: false isActive: false
} }
] ]

View File

@@ -1,22 +0,0 @@
import { ReactNode } from 'react'
export default function MessageCenterLayout({
children,
}: {
children: ReactNode
}) {
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white shadow-sm border-b">
<div className="container mx-auto px-4 py-4">
<h1 className="text-2xl font-bold text-green-900">
📨
</h1>
</div>
</header>
<main className="container mx-auto px-4 py-8">
{children}
</main>
</div>
)
}

View File

@@ -1,8 +0,0 @@
export default function MessageLogPage() {
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4"></h1>
<p></p>
</div>
)
}

View File

@@ -1,146 +0,0 @@
import { Metadata } from 'next'
export const metadata: Metadata = {
title: '消息发送 - Crop-X 智慧农业管理系统',
description: '消息推送管理页面',
}
export default function MessageSendPage() {
return (
<div className="space-y-6">
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-2xl font-bold text-gray-800 mb-6">
📤
</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-blue-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-blue-900 mb-4">
</h3>
<form className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<input
type="text"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="请输入消息标题"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<select className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option></option>
<option></option>
<option></option>
<option></option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<select className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option></option>
<option></option>
<option></option>
<option></option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<textarea
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={4}
placeholder="请输入消息内容"
></textarea>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<div className="space-y-2">
<label className="flex items-center">
<input type="checkbox" className="mr-2" defaultChecked />
</label>
<label className="flex items-center">
<input type="checkbox" className="mr-2" />
</label>
<label className="flex items-center">
<input type="checkbox" className="mr-2" />
</label>
</div>
</div>
<button
type="submit"
className="w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
</button>
</form>
</div>
<div className="bg-green-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-green-900 mb-4">
</h3>
<div className="space-y-3">
{[
{ id: 'MSG001', title: '系统维护通知', recipients: '全体用户', time: '2024-10-20 14:30', status: '已发送' },
{ id: 'MSG002', title: '农机任务分配', recipients: '农机操作员', time: '2024-10-20 12:15', status: '已发送' },
{ id: 'MSG003', title: '天气预警', recipients: '农场管理员', time: '2024-10-20 09:45', status: '已发送' },
{ id: 'MSG004', title: '数据备份提醒', recipients: '系统管理员', time: '2024-10-19 23:00', status: '已发送' },
].map((message) => (
<div key={message.id} className="bg-white rounded-lg p-4 shadow-sm">
<div className="flex justify-between items-start">
<div>
<h4 className="font-semibold text-gray-800">{message.title}</h4>
<p className="text-sm text-gray-600">: {message.recipients}</p>
<p className="text-xs text-gray-500">{message.time}</p>
</div>
<span className="inline-block px-2 py-1 text-xs font-medium bg-green-100 text-green-800 rounded-full">
{message.status}
</span>
</div>
</div>
))}
</div>
</div>
</div>
<div className="mt-6 bg-yellow-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-yellow-900 mb-4">
📊
</h3>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-blue-600 mb-2">156</div>
<div className="text-sm text-gray-600"></div>
</div>
<div className="bg-white rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-green-600 mb-2">98.5%</div>
<div className="text-sm text-gray-600"></div>
</div>
<div className="bg-white rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-purple-600 mb-2">1,234</div>
<div className="text-sm text-gray-600"></div>
</div>
<div className="bg-white rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-orange-600 mb-2">8</div>
<div className="text-sm text-gray-600"></div>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -1,8 +0,0 @@
export default function MessageTemplatePage() {
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4"></h1>
<p></p>
</div>
)
}

View File

@@ -1,111 +0,0 @@
import Link from 'next/link'
import { Metadata } from 'next'
export const metadata: Metadata = {
title: '消息中心 - Crop-X 智慧农业管理系统',
description: '消息推送管理页面',
}
export default function MessageCenterPage() {
return (
<div className="space-y-6">
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold text-gray-800 mb-4">
</h2>
<p className="text-gray-600 mb-6">
</p>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<Link
href="/central-config/message-center/message-push-management"
className="block p-4 bg-green-50 rounded-lg hover:bg-green-100 transition-colors"
>
<h3 className="font-semibold text-green-900 mb-2">
📤
</h3>
<p className="text-green-700 text-sm">
</p>
</Link>
<Link
href="/central-config/message-center/message-send"
className="block p-4 bg-blue-50 rounded-lg hover:bg-blue-100 transition-colors"
>
<h3 className="font-semibold text-blue-900 mb-2">
📨
</h3>
<p className="text-blue-700 text-sm">
</p>
</Link>
<Link
href="/central-config/message-center/notification-settings"
className="block p-4 bg-purple-50 rounded-lg hover:bg-purple-100 transition-colors"
>
<h3 className="font-semibold text-purple-900 mb-2">
</h3>
<p className="text-purple-700 text-sm">
</p>
</Link>
<Link
href="/central-config/message-center/feedback-management"
className="block p-4 bg-orange-50 rounded-lg hover:bg-orange-100 transition-colors"
>
<h3 className="font-semibold text-orange-900 mb-2">
💬
</h3>
<p className="text-orange-700 text-sm">
</p>
</Link>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">
📊
</h3>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-gray-600"></span>
<span className="text-green-600 font-semibold">156 </span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600"></span>
<span className="text-orange-600 font-semibold">23 </span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600"></span>
<span className="text-blue-600 font-semibold">8 </span>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">
🔧
</h3>
<div className="space-y-2">
<button className="w-full px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 transition-colors">
</button>
<button className="w-full px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors">
</button>
<button className="w-full px-4 py-2 bg-purple-600 text-white rounded hover:bg-purple-700 transition-colors">
</button>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,64 @@
'use client';
import { Card } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Search } from 'lucide-react';
interface MessageLogFilterProps {
searchKeyword: string;
typeFilter: string;
statusFilter: string;
onSearchChange: (value: string) => void;
onTypeChange: (value: string) => void;
onStatusChange: (value: string) => void;
}
export function MessageLogFilter({
searchKeyword,
typeFilter,
statusFilter,
onSearchChange,
onTypeChange,
onStatusChange,
}: MessageLogFilterProps) {
return (
<Card className="p-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<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={searchKeyword}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-10"
/>
</div>
<Select value={typeFilter} onValueChange={onTypeChange}>
<SelectTrigger>
<SelectValue placeholder="消息类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="sms"></SelectItem>
<SelectItem value="email"></SelectItem>
<SelectItem value="internal"></SelectItem>
<SelectItem value="push"></SelectItem>
</SelectContent>
</Select>
<Select value={statusFilter} onValueChange={onStatusChange}>
<SelectTrigger>
<SelectValue placeholder="发送状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="sent"></SelectItem>
<SelectItem value="read"></SelectItem>
<SelectItem value="failed"></SelectItem>
<SelectItem value="pending"></SelectItem>
</SelectContent>
</Select>
</div>
</Card>
);
}

View File

@@ -0,0 +1,44 @@
'use client';
import { Card } from '@/components/ui/card';
import { MessageLog } from '@/types/message';
interface MessageLogStatsProps {
logs: MessageLog[];
}
export function MessageLogStats({ logs }: MessageLogStatsProps) {
const stats = [
{
label: '总消息数',
value: logs.length,
color: 'text-blue-600',
},
{
label: '已发送',
value: logs.filter(l => l.status === 'sent' || l.status === 'read').length,
color: 'text-green-600',
},
{
label: '已读',
value: logs.filter(l => l.status === 'read').length,
color: 'text-purple-600',
},
{
label: '发送失败',
value: logs.filter(l => l.status === 'failed').length,
color: 'text-red-600',
},
];
return (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{stats.map((stat, index) => (
<Card key={index} className="p-4">
<div className="text-sm text-muted-foreground">{stat.label}</div>
<div className={`mt-2 text-2xl font-bold ${stat.color}`}>{stat.value}</div>
</Card>
))}
</div>
);
}

View File

@@ -0,0 +1,152 @@
'use client';
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { MessageLog } from '@/types/message';
import {
Mail,
MessageSquare,
Smartphone,
Bell,
CheckCircle,
XCircle,
Clock
} from 'lucide-react';
interface MessageLogTableProps {
logs: MessageLog[];
}
export function MessageLogTable({ logs }: MessageLogTableProps) {
const getTypeIcon = (type: string) => {
switch (type) {
case 'sms': return <Smartphone className="w-4 h-4" />;
case 'email': return <Mail className="w-4 h-4" />;
case 'internal': return <MessageSquare className="w-4 h-4" />;
case 'push': return <Bell className="w-4 h-4" />;
default: return <MessageSquare className="w-4 h-4" />;
}
};
const getTypeLabel = (type: string) => {
const labels: Record<string, string> = {
sms: '短信',
email: '邮件',
internal: '站内信',
push: '推送',
};
return labels[type] || type;
};
const getTypeBadge = (type: string) => {
const colors: Record<string, string> = {
sms: 'bg-blue-100 text-blue-700',
email: 'bg-purple-100 text-purple-700',
internal: 'bg-green-100 text-green-700',
push: 'bg-orange-100 text-orange-700',
};
return colors[type] || 'bg-gray-100 text-gray-700';
};
const getStatusBadge = (status: string) => {
switch (status) {
case 'sent':
return { icon: <CheckCircle className="w-3 h-3" />, label: '已发送', className: 'bg-green-100 text-green-700' };
case 'read':
return { icon: <CheckCircle className="w-3 h-3" />, label: '已读', className: 'bg-blue-100 text-blue-700' };
case 'failed':
return { icon: <XCircle className="w-3 h-3" />, label: '失败', className: 'bg-red-100 text-red-700' };
case 'pending':
return { icon: <Clock className="w-3 h-3" />, label: '待发送', className: 'bg-yellow-100 text-yellow-700' };
default:
return { icon: <Clock className="w-3 h-3" />, label: status, className: 'bg-gray-100 text-gray-700' };
}
};
const formatDateTime = (dateTime: string | undefined) => {
if (!dateTime) return '-';
return dateTime;
};
return (
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>/</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{logs.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center text-muted-foreground py-8">
</TableCell>
</TableRow>
) : (
logs.map((log) => {
const statusBadge = getStatusBadge(log.status);
// 调试信息
console.log('Log data:', log);
console.log('Type:', log.type, 'Badge color:', getTypeBadge(log.type));
return (
<TableRow key={log.id}>
<TableCell className="text-sm text-muted-foreground">
{formatDateTime(log.sentTime)}
</TableCell>
<TableCell>
<div className={`inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium ${getTypeBadge(log.type)}`}>
{getTypeIcon(log.type)}
<span>{getTypeLabel(log.type)}</span>
</div>
</TableCell>
<TableCell>
<div>
<div>{log.recipientName || '-'}</div>
<div className="text-xs text-muted-foreground">{log.recipient}</div>
</div>
</TableCell>
<TableCell className="max-w-xs">
{log.subject && (
<div className="mb-1 font-medium">{log.subject}</div>
)}
<div className="text-sm text-muted-foreground truncate">
{log.content}
</div>
</TableCell>
<TableCell>
<div>
<div className={`inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium ${statusBadge.className}`}>
{statusBadge.icon}
<span>{statusBadge.label}</span>
</div>
{log.status === 'read' && log.readTime && (
<p className="text-xs text-muted-foreground mt-1">
{formatDateTime(log.readTime)}
</p>
)}
{log.status === 'failed' && log.failReason && (
<p className="text-xs text-red-600 mt-1">{log.failReason}</p>
)}
</div>
</TableCell>
<TableCell>
<div className="inline-flex items-center px-2 py-1 rounded-full text-xs border border-gray-300 bg-gray-50">
{log.retryCount}
</div>
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</Card>
);
}

View File

@@ -0,0 +1,115 @@
import { MessageLog } from '@/types/message';
export const mockMessageLogs: MessageLog[] = [
{
id: 'msg-1',
templateId: 'tpl-1',
templateName: '任务分配通知',
type: 'internal',
recipient: 'user-2',
recipientName: '张三',
subject: '新任务分配',
content: '您好张三您有新的作业任务小麦播种作业计划执行时间2024-10-15 08:00。请及时查看并准备。',
status: 'sent',
sentTime: '2024-10-14 09:30:00',
readTime: '2024-10-14 10:15:00',
retryCount: 0,
variables: {
username: '张三',
taskName: '小麦播种作业',
executeTime: '2024-10-15 08:00',
},
},
{
id: 'msg-2',
templateId: 'tpl-2',
templateName: '设备预警通知',
type: 'sms',
recipient: '13800138000',
recipientName: '李四',
content: '【智慧农业】设备预警:约翰迪尔拖拉机检测到异常,发动机温度过高,请及时处理。',
status: 'sent',
sentTime: '2024-10-14 09:30:00',
retryCount: 0,
},
{
id: 'msg-3',
templateId: 'tpl-3',
templateName: '保养提醒',
type: 'email',
recipient: 'wangwu@example.com',
recipientName: '王五',
subject: '设备保养提醒',
content: '尊敬的用户:\n\n您的设备约翰迪尔拖拉机编号JD-001已使用500小时建议进行保养维护...',
status: 'sent',
sentTime: '2024-10-14 09:30:00',
retryCount: 0,
},
{
id: 'msg-4',
templateId: 'tpl-4',
templateName: '任务完成通知',
type: 'push',
recipient: 'user-2',
recipientName: '张三',
subject: '任务完成',
content: '作业任务小麦播种作业已完成作业面积50亩耗时3小时。',
status: 'read',
sentTime: '2024-10-14 09:30:00',
readTime: '2024-10-14 10:15:00',
retryCount: 0,
},
{
id: 'msg-5',
templateId: 'tpl-5',
templateName: '验证码',
type: 'sms',
recipient: '13900139000',
recipientName: '赵六',
content: '【智慧农业】验证码123456有效期5分钟。',
status: 'failed',
sentTime: '2024-10-14 09:30:00',
failReason: '手机号码格式错误',
retryCount: 2,
},
{
id: 'msg-6',
templateId: 'tpl-6',
templateName: '系统维护通知',
type: 'internal',
recipient: 'user-3',
recipientName: '钱七',
subject: '系统维护通知',
content: '系统将于今晚22:00-24:00进行维护期间可能影响部分功能使用。',
status: 'pending',
sentTime: '2024-10-15 14:20:00',
retryCount: 0,
},
{
id: 'msg-7',
templateId: 'tpl-7',
templateName: '天气预警',
type: 'push',
recipient: 'user-4',
recipientName: '孙八',
subject: '天气预警',
content: '未来24小时将有暴雨请注意防范做好农田排水工作。',
status: 'sent',
sentTime: '2024-10-15 14:20:00',
retryCount: 0,
},
{
id: 'msg-8',
templateId: 'tpl-8',
templateName: '作业报告',
type: 'email',
recipient: 'manager@example.com',
recipientName: '管理员',
subject: '每日作业报告',
content: '今日完成播种作业100亩施肥作业50亩灌溉作业200亩。',
status: 'read',
sentTime: '2024-10-15 14:20:00',
readTime: '2024-10-15 15:30:00',
retryCount: 0,
}
];

View File

@@ -0,0 +1,139 @@
'use client';
import { useState, useEffect } from 'react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Download, MessageSquare, RefreshCw } from 'lucide-react';
import { MessageLogStats } from './components/MessageLogStats';
import { MessageLogFilter } from './components/MessageLogFilter';
import { MessageLogTable } from './components/MessageLogTable';
import { MessageLog } from '@/types/message';
import { mockMessageLogs } from './mock/mockData';
export default function MessageLogPage() {
const [logs, setLogs] = useState<MessageLog[]>([]);
const [filteredLogs, setFilteredLogs] = useState<MessageLog[]>([]);
const [searchKeyword, setSearchKeyword] = useState('');
const [typeFilter, setTypeFilter] = useState<string>('all');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [loading, setLoading] = useState(false);
useEffect(() => {
loadLogs();
}, []);
useEffect(() => {
applyFilters();
}, [logs, searchKeyword, typeFilter, statusFilter]);
const loadLogs = async () => {
setLoading(true);
try {
// 模拟API延迟
await new Promise(resolve => setTimeout(resolve, 500));
setLogs(mockMessageLogs);
} catch (error) {
console.error('Failed to load message logs:', error);
toast.error('加载消息日志失败');
} finally {
setLoading(false);
}
};
const applyFilters = () => {
let filtered = logs;
// 调试信息
console.log('All logs:', logs);
console.log('Filters:', { searchKeyword, typeFilter, statusFilter });
if (searchKeyword) {
filtered = filtered.filter(log =>
(log.recipientName && log.recipientName.includes(searchKeyword)) ||
log.recipient.includes(searchKeyword) ||
log.content.includes(searchKeyword)
);
}
if (typeFilter !== 'all') {
filtered = filtered.filter(log => log.type === typeFilter);
}
if (statusFilter !== 'all') {
filtered = filtered.filter(log => log.status === statusFilter);
}
filtered.sort((a, b) => b.sentTime.localeCompare(a.sentTime));
setFilteredLogs(filtered);
};
const handleExport = async () => {
try {
const dataStr = JSON.stringify(filteredLogs, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `message_logs_${new Date().getTime()}.json`;
link.click();
toast.success('导出成功');
} catch (error) {
console.error('Failed to export logs:', error);
toast.error('导出失败');
}
};
const handleRefresh = () => {
loadLogs();
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-green-800 text-2xl font-bold"></h2>
<p className="text-muted-foreground"></p>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={handleRefresh} disabled={loading}>
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
<Button onClick={handleExport} disabled={loading}>
<Download className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
<MessageLogStats logs={filteredLogs} />
<MessageLogFilter
searchKeyword={searchKeyword}
typeFilter={typeFilter}
statusFilter={statusFilter}
onSearchChange={setSearchKeyword}
onTypeChange={setTypeFilter}
onStatusChange={setStatusFilter}
/>
<MessageLogTable logs={filteredLogs} />
<Card className="p-4 bg-blue-50 border-blue-200">
<h4 className="text-blue-900 mb-2 font-semibold">
<MessageSquare 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,26 @@
'use client';
import React from 'react';
import Link from 'next/link';
export default function MessagePage() {
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-3 gap-4">
<Link href="/central-config/message/send" 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/message/template" 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/message/log" 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,17 @@
import { Card } from '@/components/ui/card';
export function MessageInstructions() {
return (
<Card className="p-4 bg-blue-50 border-blue-200">
<h4 className="text-blue-900 mb-2"></h4>
<ul className="space-y-1 text-sm text-blue-800">
<li> </li>
<li> </li>
<li> </li>
<li> 使</li>
<li> </li>
<li> </li>
</ul>
</Card>
);
}

View File

@@ -0,0 +1,122 @@
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { format } from 'date-fns';
import { zhCN } from 'date-fns/locale';
import { MessageSendRecord } from '@/types/message';
interface MessagePreviewDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
record: MessageSendRecord | null;
getTypeIcon: (type: string) => JSX.Element;
getTypeLabel: (type: string) => string;
getTypeBadge: (type: string) => string;
getStatusBadge: (status: string) => JSX.Element;
}
export function MessagePreviewDialog({
open,
onOpenChange,
record,
getTypeIcon,
getTypeLabel,
getTypeBadge,
getStatusBadge
}: MessagePreviewDialogProps) {
if (!record) return null;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription className="sr-only">
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label></Label>
<div className="field-value-inline">{record.templateName}</div>
</div>
<div>
<Label></Label>
<div className="mt-2">
<Badge className={getTypeBadge(record.type)}>
<span className="flex items-center gap-1">
{getTypeIcon(record.type)}
{getTypeLabel(record.type)}
</span>
</Badge>
</div>
</div>
<div>
<Label></Label>
<div className="field-value-inline">
{record.sendType === 'immediate' ? '实时发送' : '定时发送'}
</div>
</div>
<div>
<Label></Label>
<div className="mt-2">
{getStatusBadge(record.status)}
</div>
</div>
{record.scheduledTime && (
<div>
<Label></Label>
<div className="field-value-inline">
{format(new Date(record.scheduledTime), 'yyyy-MM-dd HH:mm', { locale: zhCN })}
</div>
</div>
)}
<div>
<Label></Label>
<div className="field-value-inline">
{format(new Date(record.createdAt), 'yyyy-MM-dd HH:mm', { locale: zhCN })}
</div>
</div>
</div>
{record.subject && (
<div>
<Label></Label>
<div className="field-value-inline">{record.subject}</div>
</div>
)}
<div>
<Label> {record.recipientCount} </Label>
<Card className="p-3 bg-gray-50 mt-2">
<div className="flex flex-wrap gap-2">
{record.recipients.map((recipient, index) => (
<Badge key={index} variant="outline">
{recipient}
</Badge>
))}
</div>
</Card>
</div>
<div>
<Label></Label>
<Card className="p-4 bg-blue-50 border-blue-200 mt-2">
<pre className="text-sm whitespace-pre-wrap">
{record.content}
</pre>
</Card>
</div>
</div>
<DialogFooter>
<Button onClick={() => onOpenChange(false)}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,154 @@
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import {
Send,
Clock,
Users,
Eye,
Trash2,
CheckCircle2,
XCircle,
Timer
} from 'lucide-react';
import { format } from 'date-fns';
import { zhCN } from 'date-fns/locale';
import { MessageSendRecord } from '@/types/message';
interface MessageSendTableProps {
sendRecords: MessageSendRecord[];
onPreview: (record: MessageSendRecord) => void;
onCancel: (id: string) => void;
onDelete: (id: string) => void;
getTypeIcon: (type: string) => JSX.Element;
getTypeLabel: (type: string) => string;
getTypeBadge: (type: string) => string;
getStatusBadge: (status: string) => JSX.Element;
}
export function MessageSendTable({
sendRecords,
onPreview,
onCancel,
onDelete,
getTypeIcon,
getTypeLabel,
getTypeBadge,
getStatusBadge
}: MessageSendTableProps) {
return (
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sendRecords.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="text-center text-muted-foreground py-8">
</TableCell>
</TableRow>
) : (
sendRecords.map((record) => (
<TableRow key={record.id}>
<TableCell>
<div>{record.templateName}</div>
{record.subject && (
<p className="text-xs text-muted-foreground">{record.subject}</p>
)}
</TableCell>
<TableCell>
<Badge className={getTypeBadge(record.type)}>
<span className="flex items-center gap-1">
{getTypeIcon(record.type)}
{getTypeLabel(record.type)}
</span>
</Badge>
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Users className="w-4 h-4 text-muted-foreground" />
<span>{record.recipientCount}</span>
</div>
</TableCell>
<TableCell>
{record.sendType === 'immediate' ? (
<Badge variant="outline">
<Send className="w-3 h-3 mr-1" />
</Badge>
) : (
<div>
<Badge variant="outline">
<Clock className="w-3 h-3 mr-1" />
</Badge>
{record.scheduledTime && (
<p className="text-xs text-muted-foreground mt-1">
{format(new Date(record.scheduledTime), 'MM-dd HH:mm', { locale: zhCN })}
</p>
)}
</div>
)}
</TableCell>
<TableCell>
{getStatusBadge(record.status)}
{record.status === 'sent' && (
<p className="text-xs text-muted-foreground mt-1">
{record.sentCount}/{record.recipientCount}
</p>
)}
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{format(new Date(record.createdAt), 'MM-dd HH:mm', { locale: zhCN })}
</TableCell>
<TableCell>
<div className="flex gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => onPreview(record)}
title="查看详情"
>
<Eye className="w-4 h-4" />
</Button>
{record.status === 'pending' && (
<Button
variant="ghost"
size="sm"
onClick={() => onCancel(record.id)}
title="取消发送"
>
<XCircle className="w-4 h-4 text-orange-600" />
</Button>
)}
{(record.status === 'sent' || record.status === 'cancelled') && (
<Button
variant="ghost"
size="sm"
onClick={() => onDelete(record.id)}
title="删除记录"
>
<Trash2 className="w-4 h-4 text-destructive" />
</Button>
)}
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</Card>
);
}

View File

@@ -0,0 +1,42 @@
import { Card } from '@/components/ui/card';
import { MessageSendRecord } from '@/types/message';
interface MessageStatsCardsProps {
sendRecords: MessageSendRecord[];
}
export function MessageStatsCards({ sendRecords }: MessageStatsCardsProps) {
const stats = [
{
label: '总发送数',
value: sendRecords.length,
color: 'text-blue-600',
},
{
label: '已发送',
value: sendRecords.filter(r => r.status === 'sent').length,
color: 'text-green-600',
},
{
label: '待发送',
value: sendRecords.filter(r => r.status === 'pending').length,
color: 'text-yellow-600',
},
{
label: '已取消',
value: sendRecords.filter(r => r.status === 'cancelled').length,
color: 'text-gray-600',
},
];
return (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{stats.map((stat, index) => (
<Card key={index} className="p-4">
<div className="text-sm text-muted-foreground">{stat.label}</div>
<div className={`mt-2 text-2xl font-bold ${stat.color}`}>{stat.value}</div>
</Card>
))}
</div>
);
}

View File

@@ -0,0 +1,258 @@
import { useState } from 'react';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
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 { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Calendar } from '@/components/ui/calendar';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Send, Clock, CalendarIcon } from 'lucide-react';
import { format } from 'date-fns';
import { zhCN } from 'date-fns/locale';
import { MessageTemplate } from '@/types/message';
import { MessageSendFormData } from '../types';
interface SendMessageDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
templates: MessageTemplate[];
formData: MessageSendFormData;
onFormDataChange: (data: MessageSendFormData) => void;
onSend: () => void;
getTypeIcon: (type: string) => JSX.Element;
getTypeLabel: (type: string) => string;
}
export function SendMessageDialog({
open,
onOpenChange,
templates,
formData,
onFormDataChange,
onSend,
getTypeIcon,
getTypeLabel
}: SendMessageDialogProps) {
const replaceVariables = (content: string, variables: Record<string, string>): string => {
let result = content;
Object.entries(variables).forEach(([key, value]) => {
result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value || `{{${key}}}`);
});
return result;
};
const handleTemplateChange = (templateId: string) => {
const template = templates.find(t => t.id === templateId);
if (template) {
// 初始化变量
const vars: Record<string, string> = {};
template.variables.forEach(v => {
vars[v] = '';
});
onFormDataChange({
...formData,
templateId,
type: template.type,
subject: template.subject || '',
content: template.content,
variables: vars,
});
}
};
const selectedTemplate = templates.find(t => t.id === formData.templateId);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>
<div className="flex items-center gap-2">
<Send className="w-5 h-5 text-green-600" />
</div>
</DialogTitle>
<DialogDescription className="sr-only">
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* 选择模版 */}
<div>
<Label> *</Label>
<Select value={formData.templateId} onValueChange={handleTemplateChange}>
<SelectTrigger>
<SelectValue placeholder="请选择消息模版" />
</SelectTrigger>
<SelectContent>
{templates.filter(t => t.isActive).map(template => (
<SelectItem key={template.id} value={template.id}>
<div className="flex items-center gap-2">
{getTypeIcon(template.type)}
<span>{template.name}</span>
<Badge variant="outline" className="text-xs">
{getTypeLabel(template.type)}
</Badge>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 发送方式 */}
<div>
<Label> *</Label>
<Select
value={formData.sendType}
onValueChange={(value: 'immediate' | 'scheduled') => onFormDataChange({ ...formData, sendType: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="immediate">
<div className="flex items-center gap-2">
<Send className="w-4 h-4" />
</div>
</SelectItem>
<SelectItem value="scheduled">
<div className="flex items-center gap-2">
<Clock className="w-4 h-4" />
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
{/* 定时发送设置 */}
{formData.sendType === 'scheduled' && (
<div className="grid grid-cols-2 gap-4">
<div>
<Label> *</Label>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="w-full justify-start">
<CalendarIcon className="w-4 h-4 mr-2" />
{formData.scheduledDate ? (
format(formData.scheduledDate, 'yyyy年MM月dd日', { locale: zhCN })
) : (
'选择日期'
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={formData.scheduledDate}
onSelect={(date) => onFormDataChange({ ...formData, scheduledDate: date })}
locale={zhCN}
disabled={(date) => date < new Date(new Date().setHours(0, 0, 0, 0))}
/>
</PopoverContent>
</Popover>
</div>
<div>
<Label> *</Label>
<Input
type="time"
value={formData.scheduledTime}
onChange={(e) => onFormDataChange({ ...formData, scheduledTime: e.target.value })}
/>
</div>
</div>
)}
{/* 接收人 */}
<div>
<Label> *</Label>
<Textarea
value={formData.recipients}
onChange={(e) => onFormDataChange({ ...formData, recipients: e.target.value })}
placeholder={
formData.type === 'sms' ? '输入手机号,多个用逗号或换行分隔' :
formData.type === 'email' ? '输入邮箱地址,多个用逗号或换行分隔' :
formData.type === 'push' ? '输入设备ID或用户ID多个用逗号或换行分隔' :
'输入用户名,多个用逗号或换行分隔'
}
rows={3}
/>
<p className="text-xs text-muted-foreground mt-1">
使
</p>
</div>
{/* 消息主题(邮件和推送) */}
{(formData.type === 'email' || formData.type === 'push') && (
<div>
<Label></Label>
<Input
value={formData.subject}
onChange={(e) => onFormDataChange({ ...formData, subject: e.target.value })}
placeholder="输入消息主题"
/>
</div>
)}
{/* 变量填写 */}
{selectedTemplate && selectedTemplate.variables.length > 0 && (
<div>
<Label> *</Label>
<Card className="p-4 bg-gray-50">
<div className="grid grid-cols-2 gap-4">
{selectedTemplate.variables.map(variable => (
<div key={variable}>
<Label htmlFor={`var-${variable}`} className="text-xs">
{variable}
</Label>
<Input
id={`var-${variable}`}
value={formData.variables[variable] || ''}
onChange={(e) => onFormDataChange({
...formData,
variables: {
...formData.variables,
[variable]: e.target.value,
},
})}
placeholder={`输入 ${variable}`}
className="mt-1"
/>
</div>
))}
</div>
</Card>
</div>
)}
{/* 消息内容预览 */}
{formData.content && (
<div>
<Label></Label>
<Card className="p-4 bg-blue-50 border-blue-200">
<pre className="text-sm whitespace-pre-wrap">
{replaceVariables(formData.content, formData.variables)}
</pre>
</Card>
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={onSend} className="bg-green-600 hover:bg-green-700">
<Send className="w-4 h-4 mr-2" />
{formData.sendType === 'immediate' ? '立即发送' : '创建定时任务'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,5 @@
export { MessageStatsCards } from './MessageStatsCards';
export { MessageSendTable } from './MessageSendTable';
export { SendMessageDialog } from './SendMessageDialog';
export { MessagePreviewDialog } from './MessagePreviewDialog';
export { MessageInstructions } from './MessageInstructions';

View File

@@ -0,0 +1,493 @@
'use client';
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Send, Mail, MessageSquare, Bell, Smartphone, CheckCircle2, XCircle, Timer } from 'lucide-react';
import { toast } from 'sonner';
import { MessageTemplate, MessageSendRecord } from '@/types/message';
import { MessageSendFormData } from './types';
import {
MessageStatsCards,
MessageSendTable,
SendMessageDialog,
MessagePreviewDialog,
MessageInstructions
} from './components';
// API服务函数
const messageApi = {
// 获取消息模板
getTemplates: async (): Promise<MessageTemplate[]> => {
try {
const response = await fetch('/api/message/templates');
if (!response.ok) {
throw new Error('Failed to fetch message templates');
}
return await response.json();
} catch (error) {
console.warn('Failed to fetch message templates, using mock data:', error);
return getMockTemplates();
}
},
// 获取发送记录
getSendRecords: async (): Promise<MessageSendRecord[]> => {
try {
const response = await fetch('/api/message/send-records');
if (!response.ok) {
throw new Error('Failed to fetch send records');
}
return await response.json();
} catch (error) {
console.warn('Failed to fetch send records, using mock data:', error);
return getMockSendRecords();
}
},
// 发送消息
sendMessage: async (data: MessageSendFormData): Promise<MessageSendRecord> => {
try {
const response = await fetch('/api/message/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error('Failed to send message');
}
return await response.json();
} catch (error) {
console.warn('Failed to send message, using mock response:', error);
return createMockSendRecord(data);
}
},
// 取消定时消息
cancelMessage: async (id: string): Promise<void> => {
try {
const response = await fetch(`/api/message/send/${id}/cancel`, {
method: 'POST',
});
if (!response.ok) {
throw new Error('Failed to cancel message');
}
} catch (error) {
console.warn('Failed to cancel message, updating local state:', error);
// 模拟取消操作
}
},
// 删除发送记录
deleteMessage: async (id: string): Promise<void> => {
try {
const response = await fetch(`/api/message/send/${id}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error('Failed to delete message');
}
} catch (error) {
console.warn('Failed to delete message, updating local state:', error);
// 模拟删除操作
}
}
};
// 模拟数据生成函数
function getMockTemplates(): MessageTemplate[] {
return [
{
id: 'tpl-1',
code: 'TASK_ASSIGNMENT',
name: '任务分配通知',
type: 'internal',
subject: '新任务分配',
content: '您好,{{username}}!您有新的作业任务:{{taskName}},计划执行时间:{{executeTime}}。请及时查看并准备。',
variables: ['username', 'taskName', 'executeTime'],
isActive: true,
description: '向农机操作员分配新任务时发送',
createdAt: '2024-01-01T00:00:00',
updatedAt: '2024-01-01T00:00:00',
createdBy: 'admin',
},
{
id: 'tpl-2',
code: 'EQUIPMENT_WARNING',
name: '设备预警通知',
type: 'sms',
content: '【智慧农业】设备预警:{{equipmentName}}检测到{{warningType}},请及时处理。',
variables: ['equipmentName', 'warningType'],
isActive: true,
description: '设备出现异常时发送预警',
createdAt: '2024-01-01T00:00:00',
updatedAt: '2024-01-01T00:00:00',
createdBy: 'admin',
},
{
id: 'tpl-3',
code: 'MAINTENANCE_REMINDER',
name: '保养提醒',
type: 'email',
subject: '设备保养提醒',
content: '尊敬的用户:\n\n您的设备{{equipmentName}}(编号:{{equipmentNo}})已使用{{usageHours}}小时,建议进行保养维护。\n\n保养周期每{{maintenanceInterval}}小时\n上次保养时间{{lastMaintenanceTime}}\n\n请及时安排保养确保设备正常运行。',
variables: ['equipmentName', 'equipmentNo', 'usageHours', 'maintenanceInterval', 'lastMaintenanceTime'],
isActive: true,
description: '设备需要保养时发送提醒',
createdAt: '2024-01-01T00:00:00',
updatedAt: '2024-01-01T00:00:00',
createdBy: 'admin',
},
];
}
function getMockSendRecords(): MessageSendRecord[] {
return [
{
id: 'send-1',
templateId: 'tpl-1',
templateName: '任务分配通知',
type: 'internal',
recipients: ['张三', '李四', '王五'],
recipientCount: 3,
subject: '新任务分配',
content: '您好张三您有新的作业任务冬小麦播种计划执行时间2024-10-16 08:00。请及时查看并准备。',
sendType: 'immediate',
status: 'sent',
sentCount: 3,
sentAt: '2024-10-15T14:30:00',
createdAt: '2024-10-15T14:30:00',
createdBy: 'admin',
},
{
id: 'send-2',
templateId: 'tpl-2',
templateName: '设备预警通知',
type: 'sms',
recipients: ['13800138001', '13900139002'],
recipientCount: 2,
content: '【智慧农业】设备预警拖拉机01检测到异常油温过高请及时处理。',
sendType: 'immediate',
status: 'sent',
sentCount: 2,
sentAt: '2024-10-15T10:15:00',
createdAt: '2024-10-15T10:15:00',
createdBy: 'admin',
},
{
id: 'send-3',
templateId: 'tpl-3',
templateName: '保养提醒',
type: 'email',
recipients: ['zhangsan@example.com', 'lisi@example.com'],
recipientCount: 2,
subject: '设备保养提醒',
content: '尊敬的用户:\n\n您的设备拖拉机01编号TR001已使用500小时建议进行保养维护。\n\n保养周期每500小时\n上次保养时间2024-09-01\n\n请及时安排保养确保设备正常运行。',
sendType: 'scheduled',
scheduledTime: '2024-10-16T09:00:00',
status: 'pending',
createdAt: '2024-10-15T15:00:00',
createdBy: 'admin',
},
];
}
function createMockSendRecord(data: MessageSendFormData): MessageSendRecord {
const now = new Date().toISOString();
const scheduledDateTime = data.sendType === 'scheduled' && data.scheduledDate
? new Date(data.scheduledDate.getFullYear(), data.scheduledDate.getMonth(), data.scheduledDate.getDate(),
parseInt(data.scheduledTime.split(':')[0]), parseInt(data.scheduledTime.split(':')[1])).toISOString()
: undefined;
// 解析接收人
const recipients = data.recipients.split(/[,;\n]/).map(r => r.trim()).filter(r => r);
// 替换变量生成最终内容
const replaceVariables = (content: string, variables: Record<string, string>): string => {
let result = content;
Object.entries(variables).forEach(([key, value]) => {
result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value || `{{${key}}}`);
});
return result;
};
const finalContent = replaceVariables(data.content, data.variables);
const finalSubject = data.subject ? replaceVariables(data.subject, data.variables) : undefined;
return {
id: `send-${Date.now()}`,
templateId: data.templateId,
templateName: getMockTemplates().find(t => t.id === data.templateId)?.name || '',
type: data.type,
recipients,
recipientCount: recipients.length,
subject: finalSubject,
content: finalContent,
sendType: data.sendType,
scheduledTime: scheduledDateTime,
status: data.sendType === 'immediate' ? 'sent' : 'pending',
sentCount: data.sendType === 'immediate' ? recipients.length : undefined,
sentAt: data.sendType === 'immediate' ? now : undefined,
createdAt: now,
createdBy: 'admin',
};
}
export default function MessageSendPage() {
const [templates, setTemplates] = useState<MessageTemplate[]>([]);
const [sendRecords, setSendRecords] = useState<MessageSendRecord[]>([]);
const [showSendDialog, setShowSendDialog] = useState(false);
const [showPreviewDialog, setShowPreviewDialog] = useState(false);
const [previewRecord, setPreviewRecord] = useState<MessageSendRecord | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [formData, setFormData] = useState<MessageSendFormData>({
templateId: '',
type: 'internal',
recipientType: 'manual',
recipients: '',
subject: '',
content: '',
sendType: 'immediate',
scheduledDate: undefined,
scheduledTime: '09:00',
variables: {},
});
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
setError(null);
setLoading(true);
const [templatesData, recordsData] = await Promise.all([
messageApi.getTemplates(),
messageApi.getSendRecords()
]);
setTemplates(templatesData);
setSendRecords(recordsData);
} catch (err) {
setError(err instanceof Error ? err.message : '加载数据失败');
console.error('Failed to load data:', err);
} finally {
setLoading(false);
}
};
const handleOpenSendDialog = () => {
setFormData({
templateId: '',
type: 'internal',
recipientType: 'manual',
recipients: '',
subject: '',
content: '',
sendType: 'immediate',
scheduledDate: undefined,
scheduledTime: '09:00',
variables: {},
});
setShowSendDialog(true);
};
const handleSend = async () => {
try {
// 验证
if (!formData.templateId) {
toast.error('请选择消息模版');
return;
}
if (!formData.recipients.trim()) {
toast.error('请输入接收人');
return;
}
// 检查变量是否填写
const template = templates.find(t => t.id === formData.templateId);
if (template) {
const emptyVars = template.variables.filter(v => !formData.variables[v]?.trim());
if (emptyVars.length > 0) {
toast.error(`请填写变量:${emptyVars.join(', ')}`);
return;
}
}
if (formData.sendType === 'scheduled' && !formData.scheduledDate) {
toast.error('请选择定时发送日期');
return;
}
// 发送消息
const newRecord = await messageApi.sendMessage(formData);
setSendRecords([newRecord, ...sendRecords]);
if (formData.sendType === 'immediate') {
toast.success(`消息发送成功!已发送 ${newRecord.recipientCount} 条消息`);
} else {
toast.success(`定时消息已创建!将于 ${new Date(newRecord.scheduledTime!).toLocaleString('zh-CN')} 发送`);
}
setShowSendDialog(false);
} catch (err) {
toast.error('发送失败:' + (err instanceof Error ? err.message : '未知错误'));
}
};
const handlePreview = (record: MessageSendRecord) => {
setPreviewRecord(record);
setShowPreviewDialog(true);
};
const handleCancel = async (id: string) => {
if (!confirm('确定要取消该定时消息吗?')) return;
try {
await messageApi.cancelMessage(id);
setSendRecords(sendRecords.map(r =>
r.id === id ? { ...r, status: 'cancelled' as const } : r
));
toast.success('已取消定时消息');
} catch (err) {
toast.error('取消失败:' + (err instanceof Error ? err.message : '未知错误'));
}
};
const handleDelete = async (id: string) => {
if (!confirm('确定要删除该发送记录吗?')) return;
try {
await messageApi.deleteMessage(id);
setSendRecords(sendRecords.filter(r => r.id !== id));
toast.success('删除成功');
} catch (err) {
toast.error('删除失败:' + (err instanceof Error ? err.message : '未知错误'));
}
};
const getTypeIcon = (type: string) => {
switch (type) {
case 'sms': return <Smartphone className="w-4 h-4" />;
case 'email': return <Mail className="w-4 h-4" />;
case 'internal': return <MessageSquare className="w-4 h-4" />;
case 'push': return <Bell className="w-4 h-4" />;
default: return <MessageSquare className="w-4 h-4" />;
}
};
const getTypeLabel = (type: string) => {
const labels: Record<string, string> = {
sms: '短信',
email: '邮件',
internal: '站内信',
push: '推送',
};
return labels[type] || type;
};
const getTypeBadge = (type: string) => {
const colors: Record<string, string> = {
sms: 'bg-blue-100 text-blue-700',
email: 'bg-purple-100 text-purple-700',
internal: 'bg-green-100 text-green-700',
push: 'bg-orange-100 text-orange-700',
};
return colors[type] || 'bg-gray-100 text-gray-700';
};
const getStatusBadge = (status: string) => {
const config: Record<string, { label: string; className: string; icon: any }> = {
pending: { label: '待发送', className: 'bg-yellow-100 text-yellow-700', icon: Timer },
sending: { label: '发送中', className: 'bg-blue-100 text-blue-700', icon: Send },
sent: { label: '已发送', className: 'bg-green-100 text-green-700', icon: CheckCircle2 },
failed: { label: '发送失败', className: 'bg-red-100 text-red-700', icon: XCircle },
cancelled: { label: '已取消', className: 'bg-gray-100 text-gray-700', icon: XCircle },
};
const { label, className, icon: Icon } = config[status] || config.pending;
return (
<div className={`inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium ${className}`}>
<Icon className="w-3 h-3" />
{label}
</div>
);
};
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="w-8 h-8 border-4 border-green-600 border-t-transparent rounded-full animate-spin mx-auto mb-2"></div>
<p className="text-muted-foreground">...</p>
</div>
</div>
);
}
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>
<Button onClick={handleOpenSendDialog} className="bg-green-600 hover:bg-green-700">
<Send className="w-4 h-4 mr-2" />
</Button>
</div>
{error && (
<div className="p-4 border border-yellow-200 bg-yellow-50 rounded-md">
<p className="text-yellow-800 text-sm">
: {error} ()
</p>
</div>
)}
{/* 统计卡片 */}
<MessageStatsCards sendRecords={sendRecords} />
{/* 发送记录列表 */}
<MessageSendTable
sendRecords={sendRecords}
onPreview={handlePreview}
onCancel={handleCancel}
onDelete={handleDelete}
getTypeIcon={getTypeIcon}
getTypeLabel={getTypeLabel}
getTypeBadge={getTypeBadge}
getStatusBadge={getStatusBadge}
/>
{/* 发送消息对话框 */}
<SendMessageDialog
open={showSendDialog}
onOpenChange={setShowSendDialog}
templates={templates}
formData={formData}
onFormDataChange={setFormData}
onSend={handleSend}
getTypeIcon={getTypeIcon}
getTypeLabel={getTypeLabel}
/>
{/* 详情预览对话框 */}
<MessagePreviewDialog
open={showPreviewDialog}
onOpenChange={setShowPreviewDialog}
record={previewRecord}
getTypeIcon={getTypeIcon}
getTypeLabel={getTypeLabel}
getTypeBadge={getTypeBadge}
getStatusBadge={getStatusBadge}
/>
{/* 使用说明 */}
<MessageInstructions />
</div>
);
}

View File

@@ -0,0 +1,12 @@
export interface MessageSendFormData {
templateId: string;
type: 'sms' | 'email' | 'internal' | 'push';
recipientType: 'manual' | 'role' | 'all';
recipients: string;
subject: string;
content: string;
sendType: 'immediate' | 'scheduled';
scheduledDate?: Date;
scheduledTime: string;
variables: Record<string, string>;
}

View File

@@ -0,0 +1,179 @@
'use client';
import { useState, useEffect } from 'react';
import { MessageTemplate } from '../types';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
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 { Switch } from '@/components/ui/switch';
interface MessageTemplateDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
editingTemplate: MessageTemplate | null;
onSave: (formData: FormData) => void;
}
interface FormData {
code: string;
name: string;
type: 'sms' | 'email' | 'internal' | 'push';
subject: string;
content: string;
variables: string[];
description: string;
isActive: boolean;
}
export function MessageTemplateDialog({
open,
onOpenChange,
editingTemplate,
onSave
}: MessageTemplateDialogProps) {
const [formData, setFormData] = useState<FormData>({
code: '',
name: '',
type: 'internal',
subject: '',
content: '',
variables: [],
description: '',
isActive: true,
});
// 使用useEffect来管理表单数据的状态
useEffect(() => {
if (editingTemplate) {
setFormData({
code: editingTemplate.code,
name: editingTemplate.name,
type: editingTemplate.type,
subject: editingTemplate.subject || '',
content: editingTemplate.content,
variables: editingTemplate.variables,
description: editingTemplate.description || '',
isActive: editingTemplate.isActive,
});
} else if (open) {
// 新增模式时重置表单
setFormData({
code: '',
name: '',
type: 'internal',
subject: '',
content: '',
variables: [],
description: '',
isActive: true,
});
}
}, [editingTemplate, open]);
const handleSave = () => {
onSave(formData);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>
{editingTemplate ? '编辑模版' : '新增模版'}
</DialogTitle>
<DialogDescription className="sr-only">
{editingTemplate ? '编辑消息模版' : '添加新消息模版'}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label> *</Label>
<Input
value={formData.code}
onChange={(e) => setFormData({ ...formData, code: e.target.value })}
placeholder="TASK_ASSIGNED"
/>
</div>
<div>
<Label> *</Label>
<Input
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="任务分配通知"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label> *</Label>
<Select
value={formData.type}
onValueChange={(value: any) => setFormData({ ...formData, type: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="sms"></SelectItem>
<SelectItem value="email"></SelectItem>
<SelectItem value="internal"></SelectItem>
<SelectItem value="push"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-between pt-6">
<Label></Label>
<Switch
checked={formData.isActive}
onCheckedChange={(checked) => setFormData({ ...formData, isActive: checked })}
/>
</div>
</div>
{(formData.type === 'email' || formData.type === 'push') && (
<div>
<Label></Label>
<Input
value={formData.subject}
onChange={(e) => setFormData({ ...formData, subject: e.target.value })}
placeholder="输入消息主题"
/>
</div>
)}
<div>
<Label> *</Label>
<Textarea
value={formData.content}
onChange={(e) => setFormData({ ...formData, content: e.target.value })}
placeholder="输入消息内容,使用 {{变量名}} 表示变量"
rows={6}
/>
<p className="text-xs text-muted-foreground mt-1">
使 {'{{'} {'}'} {'{{username}}'}{'{{taskName}}'}
</p>
</div>
<div>
<Label></Label>
<Textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="模板用途说明"
rows={2}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={handleSave}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,30 @@
'use client';
import { Button } from '@/components/ui/button';
import { Plus, Download } from 'lucide-react';
interface MessageTemplateHeaderProps {
onExport: () => void;
onAdd: () => void;
}
export function MessageTemplateHeader({ onExport, onAdd }: MessageTemplateHeaderProps) {
return (
<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={onExport}>
<Download className="w-4 h-4 mr-2" />
</Button>
<Button onClick={onAdd}>
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,22 @@
'use client';
import { Card } from '@/components/ui/card';
import { MessageSquare } from 'lucide-react';
export function MessageTemplateInfo() {
return (
<Card className="p-4 bg-blue-50 border-blue-200">
<h4 className="text-blue-900 mb-2">
<MessageSquare 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>
);
}

View File

@@ -0,0 +1,128 @@
'use client';
import { MessageTemplate } from '../types';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Edit, Trash2, Send, Mail, MessageSquare, Smartphone, Bell } from 'lucide-react';
interface MessageTemplateListProps {
templates: MessageTemplate[];
onEdit: (template: MessageTemplate) => void;
onDelete: (id: string) => void;
onTest: (template: MessageTemplate) => void;
}
export function MessageTemplateList({ templates, onEdit, onDelete, onTest }: MessageTemplateListProps) {
const getTypeIcon = (type: string) => {
switch (type) {
case 'sms': return <Smartphone className="w-4 h-4" />;
case 'email': return <Mail className="w-4 h-4" />;
case 'internal': return <MessageSquare className="w-4 h-4" />;
case 'push': return <Bell className="w-4 h-4" />;
default: return <MessageSquare className="w-4 h-4" />;
}
};
const getTypeLabel = (type: string) => {
const labels: Record<string, string> = {
sms: '短信',
email: '邮件',
internal: '站内信',
push: '推送',
};
return labels[type] || type;
};
const getTypeBadge = (type: string) => {
const colors: Record<string, string> = {
sms: 'bg-blue-100 text-blue-700',
email: 'bg-purple-100 text-purple-700',
internal: 'bg-green-100 text-green-700',
push: 'bg-orange-100 text-orange-700',
};
return colors[type] || 'bg-gray-100 text-gray-700';
};
return (
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{templates.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center text-muted-foreground py-8">
</TableCell>
</TableRow>
) : (
templates.map((template) => (
<TableRow key={template.id}>
<TableCell>
<code className="text-xs bg-gray-100 px-2 py-1 rounded">
{template.code}
</code>
</TableCell>
<TableCell>
<div>
<div>{template.name}</div>
{template.description && (
<p className="text-xs text-muted-foreground">{template.description}</p>
)}
</div>
</TableCell>
<TableCell>
<Badge className={getTypeBadge(template.type)}>
<span className="flex items-center gap-1">
{getTypeIcon(template.type)}
{getTypeLabel(template.type)}
</span>
</Badge>
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
{template.variables.map((variable, index) => (
<Badge key={index} variant="outline" className="text-xs">
{variable}
</Badge>
))}
</div>
</TableCell>
<TableCell>
{template.isActive ? (
<Badge className="bg-green-100 text-green-700"></Badge>
) : (
<Badge variant="outline"></Badge>
)}
</TableCell>
<TableCell>
<div className="flex gap-1">
<Button variant="ghost" size="sm" onClick={() => onTest(template)}>
<Send className="w-4 h-4" />
</Button>
<Button variant="ghost" size="sm" onClick={() => onEdit(template)}>
<Edit className="w-4 h-4" />
</Button>
<Button variant="ghost" size="sm" onClick={() => onDelete(template.id)}>
<Trash2 className="w-4 h-4 text-destructive" />
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</Card>
);
}

View File

@@ -0,0 +1,48 @@
'use client';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Card } from '@/components/ui/card';
import { Search } from 'lucide-react';
interface MessageTemplateSearchProps {
searchKeyword: string;
onSearchChange: (value: string) => void;
typeFilter: string;
onTypeFilterChange: (value: string) => void;
}
export function MessageTemplateSearch({
searchKeyword,
onSearchChange,
typeFilter,
onTypeFilterChange
}: MessageTemplateSearchProps) {
return (
<Card className="p-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<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={searchKeyword}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-10"
/>
</div>
<Select value={typeFilter} onValueChange={onTypeFilterChange}>
<SelectTrigger>
<SelectValue placeholder="消息类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="sms"></SelectItem>
<SelectItem value="email"></SelectItem>
<SelectItem value="internal"></SelectItem>
<SelectItem value="push"></SelectItem>
</SelectContent>
</Select>
</div>
</Card>
);
}

View File

@@ -0,0 +1,121 @@
'use client';
import { useState, useEffect } from 'react';
import { MessageTemplate } from '../types';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Send } from 'lucide-react';
interface MessageTemplateTestDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
testTemplate: MessageTemplate | null;
onSendTest: (testData: TestData) => void;
}
interface TestData {
recipient: string;
variables: Record<string, string>;
}
export function MessageTemplateTestDialog({
open,
onOpenChange,
testTemplate,
onSendTest
}: MessageTemplateTestDialogProps) {
const [testData, setTestData] = useState<TestData>({
recipient: '',
variables: {},
});
// 使用useEffect来管理测试数据的状态
useEffect(() => {
if (testTemplate && open) {
const varsObj: Record<string, string> = {};
testTemplate.variables.forEach(v => {
varsObj[v] = '';
});
setTestData({
recipient: '',
variables: varsObj,
});
}
}, [testTemplate, open]);
const handleSendTest = () => {
onSendTest(testData);
};
const handleVariableChange = (variable: string, value: string) => {
setTestData({
...testData,
variables: { ...testData.variables, [variable]: value }
});
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>
<div className="flex items-center gap-2">
<Send className="w-5 h-5 text-green-600" />
</div>
</DialogTitle>
<DialogDescription className="sr-only">
</DialogDescription>
</DialogHeader>
{testTemplate && (
<div className="space-y-4">
<div className="p-3 bg-blue-50 border border-blue-200 rounded-lg">
<p className="text-sm text-blue-900">
{testTemplate.name}
</p>
</div>
<div>
<Label> *</Label>
<Input
value={testData.recipient}
onChange={(e) => setTestData({ ...testData, recipient: e.target.value })}
placeholder={
testTemplate.type === 'sms' ? '手机号' :
testTemplate.type === 'email' ? '邮箱地址' :
'用户ID'
}
/>
</div>
{testTemplate.variables.length > 0 && (
<div className="space-y-3">
<Label></Label>
{testTemplate.variables.map(variable => (
<div key={variable}>
<Label className="text-sm text-muted-foreground">{variable}</Label>
<Input
value={testData.variables[variable] || ''}
onChange={(e) => handleVariableChange(variable, e.target.value)}
placeholder={`输入 ${variable} 的值`}
/>
</div>
))}
</div>
)}
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={handleSendTest}>
<Send className="w-4 h-4 mr-2" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,6 @@
export { MessageTemplateList } from './MessageTemplateList';
export { MessageTemplateSearch } from './MessageTemplateSearch';
export { MessageTemplateHeader } from './MessageTemplateHeader';
export { MessageTemplateDialog } from './MessageTemplateDialog';
export { MessageTemplateTestDialog } from './MessageTemplateTestDialog';
export { MessageTemplateInfo } from './MessageTemplateInfo';

View File

@@ -0,0 +1,269 @@
'use client';
import { useState, useEffect } from 'react';
import { toast } from 'sonner';
import { MessageTemplate as MessageTemplateType } from './types';
import {
MessageTemplateList,
MessageTemplateSearch,
MessageTemplateHeader,
MessageTemplateDialog,
MessageTemplateTestDialog,
MessageTemplateInfo
} from './components';
export default function MessageTemplatePage() {
const [templates, setTemplates] = useState<MessageTemplateType[]>([]);
const [searchKeyword, setSearchKeyword] = useState('');
const [typeFilter, setTypeFilter] = useState<string>('all');
const [showDialog, setShowDialog] = useState(false);
const [showTestDialog, setShowTestDialog] = useState(false);
const [editingTemplate, setEditingTemplate] = useState<MessageTemplateType | null>(null);
const [testTemplate, setTestTemplate] = useState<MessageTemplateType | null>(null);
useEffect(() => {
loadTemplates();
}, []);
const loadTemplates = () => {
// 清除旧的localStorage数据确保使用最新的mock数据
localStorage.removeItem('smart_agriculture_message_templates');
const data = localStorage.getItem('smart_agriculture_message_templates');
if (data) {
try {
const parsedData = JSON.parse(data);
console.log('Loaded templates from localStorage:', parsedData);
// 确保数据是数组格式
if (Array.isArray(parsedData)) {
setTemplates(parsedData);
} else {
console.error('Loaded data is not an array:', parsedData);
setTemplates([]);
}
} catch (error) {
console.error('Error parsing templates from localStorage:', error);
setTemplates([]);
}
} else {
const mockTemplates: MessageTemplateType[] = [
{
id: 'tpl-1',
code: 'TASK_ASSIGNED',
name: '任务分配通知',
type: 'internal',
subject: '新任务分配',
content: '您好,{{username}}!您有新的作业任务:{{taskName}},计划执行时间:{{executeTime}}。请及时查看并准备。',
variables: ['username', 'taskName', 'executeTime'],
isActive: true,
description: '当任务分配给驾驶员时发送',
createdAt: '2024-01-01T00:00:00',
updatedAt: '2024-01-01T00:00:00',
createdBy: 'admin',
},
{
id: 'tpl-2',
code: 'EQUIPMENT_WARNING',
name: '设备预警通知',
type: 'sms',
content: '【智慧农业】设备预警:{{equipmentName}}检测到异常,{{warningType}},请及时处理。',
variables: ['equipmentName', 'warningType'],
isActive: true,
description: '设备出现异常时发送短信通知',
createdAt: '2024-01-01T00:00:00',
updatedAt: '2024-01-01T00:00:00',
createdBy: 'admin',
},
{
id: 'tpl-3',
code: 'MAINTENANCE_REMINDER',
name: '保养提醒',
type: 'email',
subject: '设备保养提醒',
content: '尊敬的用户:\n\n您的设备{{equipmentName}}(编号:{{equipmentCode}})已使用{{hours}}小时,建议进行保养维护。\n\n保养周期{{maintenanceCycle}}\n上次保养时间{{lastMaintenanceTime}}\n\n请及时安排保养确保设备正常运行。\n\n智慧农业管理系统',
variables: ['equipmentName', 'equipmentCode', 'hours', 'maintenanceCycle', 'lastMaintenanceTime'],
isActive: true,
description: '设备到达保养周期时发送邮件提醒',
createdAt: '2024-01-01T00:00:00',
updatedAt: '2024-01-01T00:00:00',
createdBy: 'admin',
},
{
id: 'tpl-4',
code: 'TASK_COMPLETED',
name: '任务完成通知',
type: 'push',
subject: '任务完成',
content: '作业任务{{taskName}}已完成,作业面积:{{area}}亩,耗时:{{duration}}。',
variables: ['taskName', 'area', 'duration'],
isActive: true,
description: '任务完成后推送通知',
createdAt: '2024-01-01T00:00:00',
updatedAt: '2024-01-01T00:00:00',
createdBy: 'admin',
},
];
console.log('Created mock templates:', mockTemplates);
localStorage.setItem('smart_agriculture_message_templates', JSON.stringify(mockTemplates));
setTemplates(mockTemplates);
}
};
const saveTemplates = (newTemplates: MessageTemplateType[]) => {
localStorage.setItem('smart_agriculture_message_templates', JSON.stringify(newTemplates));
setTemplates(newTemplates);
};
const filteredTemplates = templates.filter(tpl => {
const matchKeyword = !searchKeyword ||
tpl.name.includes(searchKeyword) ||
tpl.code.includes(searchKeyword) ||
tpl.content.includes(searchKeyword);
const matchType = typeFilter === 'all' || tpl.type === typeFilter;
return matchKeyword && matchType;
});
// 调试日志
console.log('Original templates:', templates);
console.log('Filtered templates:', filteredTemplates);
console.log('Search keyword:', searchKeyword);
console.log('Type filter:', typeFilter);
const handleAdd = () => {
setEditingTemplate(null);
setShowDialog(true);
};
const handleEdit = (template: MessageTemplateType) => {
setEditingTemplate(template);
setShowDialog(true);
};
const handleDelete = (id: string) => {
const updated = templates.filter(t => t.id !== id);
saveTemplates(updated);
toast.success('删除成功');
};
const handleSave = (formData: any) => {
if (!formData.code.trim() || !formData.name.trim() || !formData.content.trim()) {
toast.error('请填写必填项');
return;
}
const now = new Date().toISOString();
// 提取变量
const variableRegex = /\{\{(\w+)\}\}/g;
const matches = formData.content.matchAll(variableRegex);
const extractedVars = Array.from(new Set(Array.from(matches, m => m[1])));
if (editingTemplate) {
const updated = templates.map(t =>
t.id === editingTemplate.id
? {
...t,
...formData,
variables: extractedVars,
updatedAt: now,
}
: t
);
saveTemplates(updated);
toast.success('更新成功');
} else {
const newTemplate: MessageTemplateType = {
id: `tpl-${Date.now()}`,
...formData,
variables: extractedVars,
createdAt: now,
updatedAt: now,
createdBy: 'admin',
};
saveTemplates([...templates, newTemplate]);
toast.success('添加成功');
}
setShowDialog(false);
};
const handleTest = (template: MessageTemplateType) => {
setTestTemplate(template);
setShowTestDialog(true);
};
const handleSendTest = (testData: any) => {
if (!testData.recipient.trim()) {
toast.error('请输入接收人');
return;
}
// 检查变量是否都填写了
const emptyVars = Object.entries(testData.variables).filter(([k, v]) => !v.trim());
if (emptyVars.length > 0) {
toast.error('请填写变量:' + emptyVars.map(([k]) => k).join(', '));
return;
}
toast.success('测试消息发送成功');
setShowTestDialog(false);
};
const handleExport = () => {
const dataStr = JSON.stringify(filteredTemplates, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `message_templates_${new Date().getTime()}.json`;
link.click();
toast.success('导出成功');
};
return (
<div className="space-y-6">
{/* 页面头部 */}
<MessageTemplateHeader
onExport={handleExport}
onAdd={handleAdd}
/>
{/* 搜索和筛选 */}
<MessageTemplateSearch
searchKeyword={searchKeyword}
onSearchChange={setSearchKeyword}
typeFilter={typeFilter}
onTypeFilterChange={setTypeFilter}
/>
{/* 模版列表 */}
<MessageTemplateList
templates={filteredTemplates}
onEdit={handleEdit}
onDelete={handleDelete}
onTest={handleTest}
/>
{/* 编辑对话框 */}
<MessageTemplateDialog
open={showDialog}
onOpenChange={setShowDialog}
editingTemplate={editingTemplate}
onSave={handleSave}
/>
{/* 测试对话框 */}
<MessageTemplateTestDialog
open={showTestDialog}
onOpenChange={setShowTestDialog}
testTemplate={testTemplate}
onSendTest={handleSendTest}
/>
{/* 使用说明 */}
<MessageTemplateInfo />
</div>
);
}

View File

@@ -0,0 +1,14 @@
export interface MessageTemplate {
id: string;
code: string;
name: string;
type: 'sms' | 'email' | 'internal' | 'push';
subject: string;
content: string;
variables: string[];
description: string;
isActive: boolean;
createdAt: string;
updatedAt: string;
createdBy: string;
}

View File

@@ -0,0 +1,59 @@
import { Card } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Search } from 'lucide-react'
interface LoginLogFiltersProps {
searchKeyword: string
onSearchChange: (value: string) => void
statusFilter: string
onStatusFilterChange: (value: string) => void
dateRange: string
onDateRangeChange: (value: string) => void
}
export function LoginLogFilters({
searchKeyword,
onSearchChange,
statusFilter,
onStatusFilterChange,
dateRange,
onDateRangeChange
}: LoginLogFiltersProps) {
return (
<Card className="p-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<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="搜索用户名、IP地址、位置..."
value={searchKeyword}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-10"
/>
</div>
<Select value={statusFilter} onValueChange={onStatusFilterChange}>
<SelectTrigger>
<SelectValue placeholder="登录状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="success"></SelectItem>
<SelectItem value="failed"></SelectItem>
</SelectContent>
</Select>
<Select value={dateRange} onValueChange={onDateRangeChange}>
<SelectTrigger>
<SelectValue placeholder="时间范围" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="today"></SelectItem>
<SelectItem value="week">7</SelectItem>
<SelectItem value="month">30</SelectItem>
</SelectContent>
</Select>
</div>
</Card>
)
}

View File

@@ -0,0 +1,20 @@
import { Card } from '@/components/ui/card'
import { Shield } from 'lucide-react'
export function LoginLogInfo() {
return (
<Card className="p-4 bg-blue-50 border-blue-200">
<h4 className="text-blue-900 mb-2">
<Shield className="w-4 h-4 inline mr-2" />
</h4>
<ul className="space-y-1 text-sm text-blue-800">
<li> </li>
<li> IP地址</li>
<li> 便</li>
<li> 访</li>
<li> </li>
</ul>
</Card>
)
}

View File

@@ -0,0 +1,50 @@
import { Card } from '@/components/ui/card'
import { LoginLog } from '@/types/monitor'
interface LoginLogStatsProps {
logs: LoginLog[]
}
export function LoginLogStats({ logs }: LoginLogStatsProps) {
const stats = [
{
label: '总登录次数',
value: logs.length,
color: 'text-blue-600',
bg: 'bg-blue-100',
},
{
label: '成功登录',
value: logs.filter(l => l.status === 'success').length,
color: 'text-green-600',
bg: 'bg-green-100',
},
{
label: '失败登录',
value: logs.filter(l => l.status === 'failed').length,
color: 'text-red-600',
bg: 'bg-red-100',
},
{
label: '今日登录',
value: logs.filter(l => {
const logDate = new Date(l.loginTime)
const today = new Date()
return logDate.toDateString() === today.toDateString()
}).length,
color: 'text-purple-600',
bg: 'bg-purple-100',
},
]
return (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{stats.map((stat, index) => (
<Card key={index} className="p-4">
<div className="text-sm text-muted-foreground">{stat.label}</div>
<div className={`mt-2 ${stat.color}`}>{stat.value}</div>
</Card>
))}
</div>
)
}

View File

@@ -0,0 +1,87 @@
import { Badge } from '@/components/ui/badge'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Card } from '@/components/ui/card'
import { LoginLog } from '@/types/monitor'
import { Shield, Monitor, MapPin } from 'lucide-react'
interface LoginLogTableProps {
logs: LoginLog[]
}
export function LoginLogTable({ logs }: LoginLogTableProps) {
return (
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>IP地址</TableHead>
<TableHead>/</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{logs.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center text-muted-foreground py-8">
</TableCell>
</TableRow>
) : (
logs.map((log) => (
<TableRow key={log.id}>
<TableCell>
<div className="flex items-center gap-2">
<Shield className="w-4 h-4 text-gray-400" />
{log.username}
</div>
</TableCell>
<TableCell className="text-muted-foreground">
{new Date(log.loginTime).toLocaleString('zh-CN')}
</TableCell>
<TableCell>
<code className="text-xs bg-gray-100 px-2 py-1 rounded">
{log.ipAddress}
</code>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Monitor className="w-4 h-4 text-gray-400" />
<div className="text-sm">
<div>{log.device}</div>
{log.browser && (
<div className="text-xs text-muted-foreground">{log.browser}</div>
)}
</div>
</div>
</TableCell>
<TableCell>
{log.location && (
<div className="flex items-center gap-1 text-sm">
<MapPin className="w-3 h-3 text-gray-400" />
{log.location}
</div>
)}
</TableCell>
<TableCell>
{log.status === 'success' ? (
<Badge className="bg-green-100 text-green-700"></Badge>
) : (
<div>
<Badge className="bg-red-100 text-red-700"></Badge>
{log.failReason && (
<p className="text-xs text-red-600 mt-1">{log.failReason}</p>
)}
</div>
)}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</Card>
)
}

View File

@@ -0,0 +1,4 @@
export { LoginLogStats } from './LoginLogStats'
export { LoginLogFilters } from './LoginLogFilters'
export { LoginLogTable } from './LoginLogTable'
export { LoginLogInfo } from './LoginLogInfo'

View File

@@ -0,0 +1,202 @@
'use client'
import { useState, useEffect } from 'react'
import { Button } from '@/components/ui/button'
import { LoginLog } from '@/types/monitor'
import { Download } from 'lucide-react'
import { toast } from 'sonner'
// Import modular components
import {
LoginLogStats,
LoginLogFilters,
LoginLogTable,
LoginLogInfo
} from './components'
export default function LoginLogPage() {
const [logs, setLogs] = useState<LoginLog[]>([])
const [searchKeyword, setSearchKeyword] = useState('')
const [statusFilter, setStatusFilter] = useState<string>('all')
const [dateRange, setDateRange] = useState<string>('all')
useEffect(() => {
loadLogs()
}, [])
const loadLogs = () => {
// 强制重新加载mock数据以解决显示问题
localStorage.removeItem('smart_agriculture_login_logs')
const mockLogs: LoginLog[] = [
{
id: 'log-1',
userId: 'user-1',
username: 'admin',
loginTime: '2024-10-21T09:30:00',
ipAddress: '192.168.1.100',
device: 'Windows 11',
browser: 'Chrome 118',
os: 'Windows',
location: '北京市海淀区',
status: 'success',
sessionId: 'sess-001',
},
{
id: 'log-2',
userId: 'user-2',
username: 'zhangsan',
loginTime: '2024-10-21T10:15:00',
ipAddress: '192.168.1.101',
device: 'macOS 14',
browser: 'Safari 17',
os: 'macOS',
location: '上海市浦东新区',
status: 'success',
sessionId: 'sess-002',
},
{
id: 'log-3',
userId: 'user-3',
username: 'lisi',
loginTime: '2024-10-21T11:20:00',
ipAddress: '192.168.1.102',
device: 'Android 13',
browser: 'Chrome Mobile 118',
os: 'Android',
location: '广州市天河区',
status: 'failed',
failReason: '密码错误',
},
{
id: 'log-4',
userId: 'user-1',
username: 'admin',
loginTime: '2024-10-21T14:45:00',
ipAddress: '192.168.1.100',
device: 'Windows 11',
browser: 'Chrome 118',
os: 'Windows',
location: '北京市海淀区',
status: 'success',
sessionId: 'sess-003',
},
{
id: 'log-5',
userId: 'user-4',
username: 'wangwu',
loginTime: '2024-10-21T15:30:00',
ipAddress: '192.168.1.103',
device: 'iOS 17',
browser: 'Safari 17',
os: 'iOS',
location: '深圳市南山区',
status: 'failed',
failReason: '账号被锁定',
},
{
id: 'log-6',
userId: 'user-5',
username: 'zhaoliu',
loginTime: '2024-10-21T16:20:00',
ipAddress: '192.168.1.104',
device: 'Windows 10',
browser: 'Edge 118',
os: 'Windows',
location: '杭州市西湖区',
status: 'success',
sessionId: 'sess-004',
},
{
id: 'log-7',
userId: 'user-6',
username: 'chenqi',
loginTime: '2024-10-21T17:10:00',
ipAddress: '192.168.1.105',
device: 'Ubuntu 22.04',
browser: 'Firefox 119',
os: 'Linux',
location: '成都市武侯区',
status: 'success',
sessionId: 'sess-005',
},
]
localStorage.setItem('smart_agriculture_login_logs', JSON.stringify(mockLogs))
setLogs(mockLogs)
}
const filteredLogs = logs.filter(log => {
const matchKeyword = !searchKeyword ||
log.username.includes(searchKeyword) ||
log.ipAddress.includes(searchKeyword) ||
(log.location && log.location.includes(searchKeyword))
const matchStatus = statusFilter === 'all' || log.status === statusFilter
let matchDate = true
if (dateRange !== 'all') {
const logDate = new Date(log.loginTime)
const now = new Date()
const diffDays = Math.floor((now.getTime() - logDate.getTime()) / (1000 * 60 * 60 * 24))
switch (dateRange) {
case 'today':
matchDate = diffDays === 0
break
case 'week':
matchDate = diffDays <= 7
break
case 'month':
matchDate = diffDays <= 30
break
}
}
return matchKeyword && matchStatus && matchDate
})
const handleExport = () => {
const dataStr = JSON.stringify(filteredLogs, null, 2)
const dataBlob = new Blob([dataStr], { type: 'application/json' })
const url = URL.createObjectURL(dataBlob)
const link = document.createElement('a')
link.href = url
link.download = `login_logs_${new Date().getTime()}.json`
link.click()
toast.success('导出成功')
}
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>
<Button onClick={handleExport}>
<Download className="w-4 h-4 mr-2" />
</Button>
</div>
{/* 统计卡片 */}
<LoginLogStats logs={logs} />
{/* 搜索和筛选 */}
<LoginLogFilters
searchKeyword={searchKeyword}
onSearchChange={setSearchKeyword}
statusFilter={statusFilter}
onStatusFilterChange={setStatusFilter}
dateRange={dateRange}
onDateRangeChange={setDateRange}
/>
{/* 日志列表 */}
<LoginLogTable logs={filteredLogs} />
{/* 使用说明 */}
<LoginLogInfo />
</div>
)
}

View File

@@ -0,0 +1,191 @@
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { NetworkLog } from '@/types/monitor'
import { Globe } from 'lucide-react'
interface NetworkLogDetailDialogProps {
log: NetworkLog | null
isOpen: boolean
onClose: () => void
isLoading?: boolean
}
export function NetworkLogDetailDialog({
log,
isOpen,
onClose,
isLoading = false
}: NetworkLogDetailDialogProps) {
const getMethodBadge = (method: string) => {
const colors: Record<string, string> = {
GET: 'bg-blue-100 text-blue-700',
POST: 'bg-green-100 text-green-700',
PUT: 'bg-yellow-100 text-yellow-700',
DELETE: 'bg-red-100 text-red-700',
PATCH: 'bg-purple-100 text-purple-700',
}
return colors[method] || 'bg-gray-100 text-gray-700'
}
const getStatusBadge = (status: number) => {
if (status >= 200 && status < 300) {
return 'bg-green-100 text-green-700'
} else if (status >= 400 && status < 500) {
return 'bg-yellow-100 text-yellow-700'
} else if (status >= 500) {
return 'bg-red-100 text-red-700'
}
return 'bg-gray-100 text-gray-700'
}
const formatBytes = (bytes: number) => {
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`
}
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>
<div className="flex items-center gap-2">
<Globe className="w-5 h-5 text-green-600" />
</div>
</DialogTitle>
</DialogHeader>
{isLoading ? (
<div className="space-y-4">
{Array.from({ length: 8 }).map((_, index) => (
<div key={index} className="animate-pulse">
<div className="bg-gray-200 h-4 w-24 rounded mb-2"></div>
<div className="bg-gray-200 h-6 w-40 rounded"></div>
</div>
))}
</div>
) : log ? (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1">{new Date(log.timestamp).toLocaleString('zh-CN')}</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1">
<Badge className={getMethodBadge(log.method)}>
{log.method}
</Badge>
</p>
</div>
<div className="col-span-2">
<p className="text-sm text-muted-foreground">URL</p>
<p className="mt-1">
<code className="text-xs bg-gray-100 px-2 py-1 rounded block break-all">
{log.url}
</code>
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">IP</p>
<p className="mt-1">
<code className="text-xs bg-gray-100 px-2 py-1 rounded">
{log.clientIp}
</code>
</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1">{log.username || '未登录'}</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1">
<Badge className={getStatusBadge(log.responseStatus)}>
{log.responseStatus}
</Badge>
</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1">{log.responseTime}ms</p>
</div>
{log.responseSize && (
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1">{formatBytes(log.responseSize)}</p>
</div>
)}
</div>
{log.requestParams && (
<div>
<p className="text-sm text-muted-foreground"></p>
<pre className="mt-1 p-3 bg-gray-50 rounded text-xs overflow-x-auto">
{log.requestParams}
</pre>
</div>
)}
{log.requestHeaders && (
<div>
<p className="text-sm text-muted-foreground"></p>
<pre className="mt-1 p-3 bg-gray-50 rounded text-xs overflow-x-auto">
{(() => {
try {
return JSON.stringify(JSON.parse(log.requestHeaders), null, 2)
} catch {
return log.requestHeaders
}
})()}
</pre>
</div>
)}
{log.requestBody && (
<div>
<p className="text-sm text-muted-foreground"></p>
<pre className="mt-1 p-3 bg-gray-50 rounded text-xs overflow-x-auto">
{log.requestBody}
</pre>
</div>
)}
{log.responseBody && (
<div>
<p className="text-sm text-muted-foreground"></p>
<pre className="mt-1 p-3 bg-gray-50 rounded text-xs overflow-x-auto max-h-40 overflow-y-auto">
{(() => {
try {
return JSON.stringify(JSON.parse(log.responseBody), null, 2)
} catch {
return log.responseBody
}
})()}
</pre>
</div>
)}
{log.userAgent && (
<div>
<p className="text-sm text-muted-foreground">User Agent</p>
<p className="mt-1 text-xs text-muted-foreground break-all">
{log.userAgent}
</p>
</div>
)}
</div>
) : null}
<DialogFooter>
<Button variant="outline" onClick={onClose}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,62 @@
import { Card } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Search } from 'lucide-react'
interface NetworkLogFiltersProps {
searchKeyword: string
onSearchChange: (value: string) => void
methodFilter: string
onMethodFilterChange: (value: string) => void
statusFilter: string
onStatusFilterChange: (value: string) => void
}
export function NetworkLogFilters({
searchKeyword,
onSearchChange,
methodFilter,
onMethodFilterChange,
statusFilter,
onStatusFilterChange
}: NetworkLogFiltersProps) {
return (
<Card className="p-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<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="搜索URL、用户名..."
value={searchKeyword}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-10"
/>
</div>
<Select value={methodFilter} onValueChange={onMethodFilterChange}>
<SelectTrigger>
<SelectValue placeholder="请求方法" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="GET">GET</SelectItem>
<SelectItem value="POST">POST</SelectItem>
<SelectItem value="PUT">PUT</SelectItem>
<SelectItem value="DELETE">DELETE</SelectItem>
<SelectItem value="PATCH">PATCH</SelectItem>
</SelectContent>
</Select>
<Select value={statusFilter} onValueChange={onStatusFilterChange}>
<SelectTrigger>
<SelectValue placeholder="响应状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="2xx">2xx </SelectItem>
<SelectItem value="4xx">4xx </SelectItem>
<SelectItem value="5xx">5xx </SelectItem>
</SelectContent>
</Select>
</div>
</Card>
)
}

View File

@@ -0,0 +1,20 @@
import { Card } from '@/components/ui/card'
import { Globe } from 'lucide-react'
export function NetworkLogInfo() {
return (
<Card className="p-4 bg-blue-50 border-blue-200">
<h4 className="text-blue-900 mb-2">
<Globe className="w-4 h-4 inline mr-2" />
</h4>
<ul className="space-y-1 text-sm text-blue-800">
<li> HTTP请求的详细信息</li>
<li> 1</li>
<li> 4xx状态码通常表示客户端错误5xx表示服务器错误</li>
<li> </li>
<li> </li>
</ul>
</Card>
)
}

View File

@@ -0,0 +1,338 @@
import { NetworkLog } from '@/types/monitor'
import { ApiResponse, PaginatedResponse, PaginationParams } from '@/types'
export interface NetworkLogFilters {
searchKeyword?: string
method?: string
status?: string
startDate?: string
endDate?: string
minResponseTime?: number
maxResponseTime?: number
}
export interface NetworkLogListParams extends PaginationParams {
filters?: NetworkLogFilters
}
export class NetworkLogService {
private static baseUrl = '/api/monitor/network-logs'
/**
* 获取网络日志列表
*/
static async getNetworkLogs(params: NetworkLogListParams = {}): Promise<PaginatedResponse<NetworkLog>> {
try {
const queryParams = new URLSearchParams()
// 添加分页参数
if (params.page) queryParams.append('page', params.page.toString())
if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString())
// 添加筛选参数
if (params.filters) {
if (params.filters.searchKeyword) queryParams.append('searchKeyword', params.filters.searchKeyword)
if (params.filters.method && params.filters.method !== 'all') queryParams.append('method', params.filters.method)
if (params.filters.status && params.filters.status !== 'all') queryParams.append('status', params.filters.status)
if (params.filters.startDate) queryParams.append('startDate', params.filters.startDate)
if (params.filters.endDate) queryParams.append('endDate', params.filters.endDate)
if (params.filters.minResponseTime) queryParams.append('minResponseTime', params.filters.minResponseTime.toString())
if (params.filters.maxResponseTime) queryParams.append('maxResponseTime', params.filters.maxResponseTime.toString())
}
const response = await fetch(`${this.baseUrl}?${queryParams}`)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
return data
} catch (error) {
console.error('Failed to fetch network logs:', error)
// 降级处理返回mock数据
return this.getMockData(params)
}
}
/**
* 获取网络日志详情
*/
static async getNetworkLogDetail(id: string): Promise<ApiResponse<NetworkLog>> {
try {
const response = await fetch(`${this.baseUrl}/${id}`)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
return data
} catch (error) {
console.error('Failed to fetch network log detail:', error)
throw error
}
}
/**
* 导出网络日志
*/
static async exportNetworkLogs(filters?: NetworkLogFilters): Promise<Blob> {
try {
const queryParams = new URLSearchParams()
if (filters) {
if (filters.searchKeyword) queryParams.append('searchKeyword', filters.searchKeyword)
if (filters.method && filters.method !== 'all') queryParams.append('method', filters.method)
if (filters.status && filters.status !== 'all') queryParams.append('status', filters.status)
if (filters.startDate) queryParams.append('startDate', filters.startDate)
if (filters.endDate) queryParams.append('endDate', filters.endDate)
if (filters.minResponseTime) queryParams.append('minResponseTime', filters.minResponseTime.toString())
if (filters.maxResponseTime) queryParams.append('maxResponseTime', filters.maxResponseTime.toString())
}
const response = await fetch(`${this.baseUrl}/export?${queryParams}`)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return response.blob()
} catch (error) {
console.error('Failed to export network logs:', error)
throw error
}
}
/**
* 获取网络日志统计信息
*/
static async getNetworkLogStats(filters?: NetworkLogFilters): Promise<ApiResponse<{
total: number
success: number
clientError: number
serverError: number
averageResponseTime: number
totalResponseSize: number
methodStats: Array<{ method: string, count: number }>
statusStats: Array<{ status: number, count: number }>
topSlowRequests: Array<{ url: string, responseTime: number, count: number }>
}>> {
try {
const queryParams = new URLSearchParams()
if (filters) {
if (filters.method && filters.method !== 'all') queryParams.append('method', filters.method)
if (filters.status && filters.status !== 'all') queryParams.append('status', filters.status)
if (filters.startDate) queryParams.append('startDate', filters.startDate)
if (filters.endDate) queryParams.append('endDate', filters.endDate)
}
const response = await fetch(`${this.baseUrl}/stats?${queryParams}`)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
return data
} catch (error) {
console.error('Failed to fetch network log stats:', error)
// 降级处理返回mock统计数据
return this.getMockStats()
}
}
/**
* Mock数据 - 用于降级处理
*/
private static getMockData(params: NetworkLogListParams): PaginatedResponse<NetworkLog> {
const mockLogs: NetworkLog[] = [
{
id: 'net-1',
timestamp: '2024-10-21T09:35:00',
method: 'POST',
url: '/api/users',
requestParams: 'username=zhangsan&name=张三',
responseStatus: 200,
responseTime: 150,
responseSize: 256,
clientIp: '192.168.1.100',
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/118.0.0.0',
userId: 'user-1',
username: 'admin',
},
{
id: 'net-2',
timestamp: '2024-10-21T10:20:00',
method: 'GET',
url: '/api/machinery/list',
requestParams: 'page=1&size=10',
responseStatus: 200,
responseTime: 89,
responseSize: 4096,
clientIp: '192.168.1.101',
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Safari/17.0',
userId: 'user-2',
username: 'zhangsan',
},
{
id: 'net-3',
timestamp: '2024-10-21T11:25:00',
method: 'DELETE',
url: '/api/roles/456',
responseStatus: 400,
responseTime: 120,
responseSize: 128,
clientIp: '192.168.1.102',
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) Chrome/118.0.0.0',
userId: 'user-3',
username: 'lisi',
},
{
id: 'net-4',
timestamp: '2024-10-21T14:50:00',
method: 'PUT',
url: '/api/system/settings',
requestParams: 'sessionTimeout=30',
responseStatus: 200,
responseTime: 95,
responseSize: 512,
clientIp: '192.168.1.100',
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/118.0.0.0',
userId: 'user-1',
username: 'admin',
},
{
id: 'net-5',
timestamp: '2024-10-21T15:35:00',
method: 'POST',
url: '/api/tasks',
responseStatus: 201,
responseTime: 180,
responseSize: 1024,
clientIp: '192.168.1.101',
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Safari/17.0',
userId: 'user-2',
username: 'zhangsan',
},
{
id: 'net-6',
timestamp: '2024-10-21T16:15:00',
method: 'GET',
url: '/api/users/export',
requestParams: 'format=excel',
responseStatus: 200,
responseTime: 1250,
responseSize: 102400,
clientIp: '192.168.1.100',
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/118.0.0.0',
userId: 'user-1',
username: 'admin',
},
]
// 应用筛选器
let filteredLogs = mockLogs.filter(log => {
if (params.filters?.searchKeyword) {
const keyword = params.filters.searchKeyword.toLowerCase()
if (!log.url.toLowerCase().includes(keyword) &&
!(log.username && log.username.toLowerCase().includes(keyword))) {
return false
}
}
if (params.filters?.method && params.filters.method !== 'all') {
if (log.method !== params.filters.method) return false
}
if (params.filters?.status && params.filters.status !== 'all') {
const status = log.responseStatus
switch (params.filters.status) {
case '2xx':
if (status < 200 || status >= 300) return false
break
case '4xx':
if (status < 400 || status >= 500) return false
break
case '5xx':
if (status < 500) return false
break
}
}
if (params.filters?.minResponseTime && log.responseTime < params.filters.minResponseTime) {
return false
}
if (params.filters?.maxResponseTime && log.responseTime > params.filters.maxResponseTime) {
return false
}
return true
})
// 应用分页
const page = params.page || 1
const pageSize = params.pageSize || 10
const startIndex = (page - 1) * pageSize
const endIndex = startIndex + pageSize
const paginatedLogs = filteredLogs.slice(startIndex, endIndex)
return {
code: 200,
message: 'success',
success: true,
data: paginatedLogs,
pagination: {
page,
pageSize,
total: filteredLogs.length,
totalPages: Math.ceil(filteredLogs.length / pageSize)
}
}
}
private static getMockStats(): ApiResponse<{
total: number
success: number
clientError: number
serverError: number
averageResponseTime: number
totalResponseSize: number
methodStats: Array<{ method: string, count: number }>
statusStats: Array<{ status: number, count: number }>
topSlowRequests: Array<{ url: string, responseTime: number, count: number }>
}> {
return {
code: 200,
message: 'success',
success: true,
data: {
total: 6,
success: 4,
clientError: 1,
serverError: 1,
averageResponseTime: 314,
totalResponseSize: 108416,
methodStats: [
{ method: 'GET', count: 2 },
{ method: 'POST', count: 2 },
{ method: 'PUT', count: 1 },
{ method: 'DELETE', count: 1 }
],
statusStats: [
{ status: 200, count: 3 },
{ status: 201, count: 1 },
{ status: 400, count: 1 },
{ status: 500, count: 1 }
],
topSlowRequests: [
{ url: '/api/users/export', responseTime: 1250, count: 1 },
{ url: '/api/tasks', responseTime: 180, count: 1 }
]
}
}
}
}

View File

@@ -0,0 +1,51 @@
import { Card } from '@/components/ui/card'
import { NetworkLog } from '@/types/monitor'
interface NetworkLogStatsProps {
logs: NetworkLog[]
isLoading?: boolean
}
export function NetworkLogStats({ logs, isLoading = false }: NetworkLogStatsProps) {
const stats = [
{
label: '总请求数',
value: logs.length,
color: 'text-blue-600',
},
{
label: '成功请求',
value: logs.filter(l => l.responseStatus >= 200 && l.responseStatus < 300).length,
color: 'text-green-600',
},
{
label: '失败请求',
value: logs.filter(l => l.responseStatus >= 400).length,
color: 'text-red-600',
},
{
label: '平均响应时间',
value: logs.length > 0
? Math.round(logs.reduce((sum, l) => sum + l.responseTime, 0) / logs.length) + 'ms'
: '0ms',
color: 'text-purple-600',
},
]
return (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{stats.map((stat, index) => (
<Card key={index} className="p-4">
<div className="text-sm text-muted-foreground">{stat.label}</div>
<div className={`mt-2 text-2xl font-semibold ${stat.color}`}>
{isLoading ? (
<div className="animate-pulse bg-gray-200 h-8 w-16 rounded"></div>
) : (
stat.value
)}
</div>
</Card>
))}
</div>
)
}

View File

@@ -0,0 +1,133 @@
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Card } from '@/components/ui/card'
import { NetworkLog } from '@/types/monitor'
import { Eye, Clock } from 'lucide-react'
interface NetworkLogTableProps {
logs: NetworkLog[]
isLoading?: boolean
onViewDetail: (log: NetworkLog) => void
}
export function NetworkLogTable({ logs, isLoading = false, onViewDetail }: NetworkLogTableProps) {
const getMethodBadge = (method: string) => {
const colors: Record<string, string> = {
GET: 'bg-blue-100 text-blue-700',
POST: 'bg-green-100 text-green-700',
PUT: 'bg-yellow-100 text-yellow-700',
DELETE: 'bg-red-100 text-red-700',
PATCH: 'bg-purple-100 text-purple-700',
}
return colors[method] || 'bg-gray-100 text-gray-700'
}
const getStatusBadge = (status: number) => {
if (status >= 200 && status < 300) {
return 'bg-green-100 text-green-700'
} else if (status >= 400 && status < 500) {
return 'bg-yellow-100 text-yellow-700'
} else if (status >= 500) {
return 'bg-red-100 text-red-700'
}
return 'bg-gray-100 text-gray-700'
}
const formatBytes = (bytes: number) => {
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`
}
if (isLoading) {
return (
<Card>
<div className="p-8 space-y-4">
{Array.from({ length: 5 }).map((_, index) => (
<div key={index} className="animate-pulse">
<div className="flex space-x-4">
<div className="bg-gray-200 h-4 w-20 rounded"></div>
<div className="bg-gray-200 h-4 w-16 rounded"></div>
<div className="bg-gray-200 h-4 w-32 rounded"></div>
<div className="bg-gray-200 h-4 w-20 rounded"></div>
<div className="bg-gray-200 h-4 w-16 rounded"></div>
<div className="bg-gray-200 h-4 w-20 rounded"></div>
<div className="bg-gray-200 h-4 w-16 rounded"></div>
<div className="bg-gray-200 h-8 w-8 rounded"></div>
</div>
</div>
))}
</div>
</Card>
)
}
return (
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>URL</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{logs.length === 0 ? (
<TableRow>
<TableCell colSpan={8} className="text-center text-muted-foreground py-8">
</TableCell>
</TableRow>
) : (
logs.map((log) => (
<TableRow key={log.id}>
<TableCell className="text-sm text-muted-foreground">
{new Date(log.timestamp).toLocaleTimeString('zh-CN')}
</TableCell>
<TableCell>
<Badge className={getMethodBadge(log.method)}>
{log.method}
</Badge>
</TableCell>
<TableCell className="max-w-xs truncate text-sm" title={log.url}>
{log.url}
</TableCell>
<TableCell className="text-sm">{log.username || '-'}</TableCell>
<TableCell>
<Badge className={getStatusBadge(log.responseStatus)}>
{log.responseStatus}
</Badge>
</TableCell>
<TableCell className="text-sm">
<div className="flex items-center gap-1">
<Clock className="w-3 h-3 text-gray-400" />
{log.responseTime}ms
</div>
</TableCell>
<TableCell className="text-sm">
{log.responseSize ? formatBytes(log.responseSize) : '-'}
</TableCell>
<TableCell>
<Button
variant="ghost"
size="sm"
onClick={() => onViewDetail(log)}
>
<Eye className="w-4 h-4" />
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</Card>
)
}

View File

@@ -0,0 +1,8 @@
export { NetworkLogService } from './NetworkLogService'
export { NetworkLogStats } from './NetworkLogStats'
export { NetworkLogFilters } from './NetworkLogFilters'
export { NetworkLogTable } from './NetworkLogTable'
export { NetworkLogDetailDialog } from './NetworkLogDetailDialog'
export { NetworkLogInfo } from './NetworkLogInfo'
export type { NetworkLogFilters as NetworkLogFiltersType, NetworkLogListParams } from './NetworkLogService'

View File

@@ -0,0 +1,139 @@
'use client'
import { useState, useEffect } from 'react'
import { Button } from '@/components/ui/button'
import { NetworkLog } from '@/types/monitor'
import { Download } from 'lucide-react'
import { toast } from 'sonner'
// Import modular components
import {
NetworkLogService,
NetworkLogStats,
NetworkLogFilters,
NetworkLogTable,
NetworkLogDetailDialog,
NetworkLogInfo
} from './components'
export default function NetworkLogPage() {
const [logs, setLogs] = useState<NetworkLog[]>([])
const [isLoading, setIsLoading] = useState(false)
const [searchKeyword, setSearchKeyword] = useState('')
const [methodFilter, setMethodFilter] = useState<string>('all')
const [statusFilter, setStatusFilter] = useState<string>('all')
const [showDetailDialog, setShowDetailDialog] = useState(false)
const [selectedLog, setSelectedLog] = useState<NetworkLog | null>(null)
const [isDetailLoading, setIsDetailLoading] = useState(false)
useEffect(() => {
loadLogs()
}, [searchKeyword, methodFilter, statusFilter])
const loadLogs = async () => {
setIsLoading(true)
try {
const response = await NetworkLogService.getNetworkLogs({
page: 1,
pageSize: 100,
filters: {
searchKeyword,
method: methodFilter,
status: statusFilter
}
})
if (response.success) {
setLogs(response.data)
} else {
throw new Error(response.message || '加载网络日志失败')
}
} catch (error) {
console.error('Failed to load network logs:', error)
toast.error('加载网络日志失败,请稍后重试')
} finally {
setIsLoading(false)
}
}
const handleViewDetail = async (log: NetworkLog) => {
setSelectedLog(log)
setShowDetailDialog(true)
setIsDetailLoading(true)
try {
const response = await NetworkLogService.getNetworkLogDetail(log.id)
if (response.success) {
setSelectedLog(response.data)
}
} catch (error) {
console.error('Failed to fetch log detail:', error)
} finally {
setIsDetailLoading(false)
}
}
const handleExport = async () => {
try {
const blob = await NetworkLogService.exportNetworkLogs({
searchKeyword,
method: methodFilter,
status: statusFilter
})
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `network_logs_${new Date().getTime()}.json`
link.click()
URL.revokeObjectURL(url)
toast.success('导出成功')
} catch (error) {
console.error('Failed to export logs:', error)
toast.error('导出失败,请稍后重试')
}
}
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>
<Button onClick={handleExport} disabled={isLoading || logs.length === 0}>
<Download className="w-4 h-4 mr-2" />
</Button>
</div>
<NetworkLogStats logs={logs} isLoading={isLoading} />
<NetworkLogFilters
searchKeyword={searchKeyword}
onSearchChange={setSearchKeyword}
methodFilter={methodFilter}
onMethodFilterChange={setMethodFilter}
statusFilter={statusFilter}
onStatusFilterChange={setStatusFilter}
/>
<NetworkLogTable
logs={logs}
isLoading={isLoading}
onViewDetail={handleViewDetail}
/>
<NetworkLogDetailDialog
log={selectedLog}
isOpen={showDetailDialog}
onClose={() => setShowDetailDialog(false)}
isLoading={isDetailLoading}
/>
<NetworkLogInfo />
</div>
)
}

View File

@@ -0,0 +1,183 @@
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { OperationLog } from '@/types/monitor'
import { FileText } from 'lucide-react'
interface OperationLogDetailDialogProps {
log: OperationLog | null
isOpen: boolean
onClose: () => void
isLoading?: boolean
}
export function OperationLogDetailDialog({
log,
isOpen,
onClose,
isLoading = false
}: OperationLogDetailDialogProps) {
const getModuleLabel = (module: string) => {
const labels: Record<string, string> = {
user: '用户管理',
role: '角色管理',
permission: '权限管理',
machinery: '农机管理',
driver: '驾驶员管理',
task: '任务管理',
system: '系统配置',
other: '其他',
}
return labels[module] || module
}
const getActionLabel = (action: string) => {
const labels: Record<string, string> = {
create: '新增',
update: '修改',
delete: '删除',
view: '查看',
export: '导出',
import: '导入',
login: '登录',
logout: '登出',
}
return labels[action] || action
}
const getActionBadge = (action: string) => {
const colors: Record<string, string> = {
create: 'bg-green-100 text-green-700',
update: 'bg-blue-100 text-blue-700',
delete: 'bg-red-100 text-red-700',
view: 'bg-gray-100 text-gray-700',
export: 'bg-purple-100 text-purple-700',
import: 'bg-yellow-100 text-yellow-700',
}
return colors[action] || 'bg-gray-100 text-gray-700'
}
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>
<div className="flex items-center gap-2">
<FileText className="w-5 h-5 text-green-600" />
</div>
</DialogTitle>
</DialogHeader>
{isLoading ? (
<div className="space-y-4">
{Array.from({ length: 6 }).map((_, index) => (
<div key={index} className="animate-pulse">
<div className="bg-gray-200 h-4 w-24 rounded mb-2"></div>
<div className="bg-gray-200 h-6 w-40 rounded"></div>
</div>
))}
</div>
) : log ? (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1 font-medium">{log.username}</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1">{new Date(log.operationTime).toLocaleString('zh-CN')}</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1">
<Badge variant="outline">{getModuleLabel(log.module)}</Badge>
</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1">
<Badge className={getActionBadge(log.action)}>
{getActionLabel(log.action)}
</Badge>
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">IP地址</p>
<p className="mt-1">
<code className="text-xs bg-gray-100 px-2 py-1 rounded">
{log.ipAddress}
</code>
</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1">{log.duration ? `${log.duration}ms` : '-'}</p>
</div>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1">{log.description}</p>
</div>
{log.requestUrl && (
<div>
<p className="text-sm text-muted-foreground">URL</p>
<p className="mt-1">
<code className="text-xs bg-gray-100 px-2 py-1 rounded">
{log.requestMethod} {log.requestUrl}
</code>
</p>
</div>
)}
{log.requestParams && (
<div>
<p className="text-sm text-muted-foreground"></p>
<pre className="mt-1 p-3 bg-gray-50 rounded text-xs overflow-x-auto">
{(() => {
try {
return JSON.stringify(JSON.parse(log.requestParams), null, 2)
} catch {
return log.requestParams
}
})()}
</pre>
</div>
)}
{log.responseData && (
<div>
<p className="text-sm text-muted-foreground"></p>
<pre className="mt-1 p-3 bg-gray-50 rounded text-xs overflow-x-auto">
{(() => {
try {
return JSON.stringify(JSON.parse(log.responseData), null, 2)
} catch {
return log.responseData
}
})()}
</pre>
</div>
)}
{log.errorMessage && (
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1 text-red-600 bg-red-50 p-3 rounded">{log.errorMessage}</p>
</div>
)}
</div>
) : null}
<DialogFooter>
<Button variant="outline" onClick={onClose}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,81 @@
import { Card } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Search } from 'lucide-react'
interface OperationLogFiltersProps {
searchKeyword: string
onSearchChange: (value: string) => void
moduleFilter: string
onModuleFilterChange: (value: string) => void
actionFilter: string
onActionFilterChange: (value: string) => void
statusFilter: string
onStatusFilterChange: (value: string) => void
}
export function OperationLogFilters({
searchKeyword,
onSearchChange,
moduleFilter,
onModuleFilterChange,
actionFilter,
onActionFilterChange,
statusFilter,
onStatusFilterChange
}: OperationLogFiltersProps) {
return (
<Card className="p-4">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<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={searchKeyword}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-10"
/>
</div>
<Select value={moduleFilter} onValueChange={onModuleFilterChange}>
<SelectTrigger>
<SelectValue placeholder="操作模块" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="user"></SelectItem>
<SelectItem value="role"></SelectItem>
<SelectItem value="permission"></SelectItem>
<SelectItem value="machinery"></SelectItem>
<SelectItem value="driver"></SelectItem>
<SelectItem value="task"></SelectItem>
<SelectItem value="system"></SelectItem>
</SelectContent>
</Select>
<Select value={actionFilter} onValueChange={onActionFilterChange}>
<SelectTrigger>
<SelectValue placeholder="操作类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="create"></SelectItem>
<SelectItem value="update"></SelectItem>
<SelectItem value="delete"></SelectItem>
<SelectItem value="view"></SelectItem>
<SelectItem value="export"></SelectItem>
<SelectItem value="import"></SelectItem>
</SelectContent>
</Select>
<Select value={statusFilter} onValueChange={onStatusFilterChange}>
<SelectTrigger>
<SelectValue placeholder="操作状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="success"></SelectItem>
<SelectItem value="failed"></SelectItem>
</SelectContent>
</Select>
</div>
</Card>
)
}

View File

@@ -0,0 +1,20 @@
import { Card } from '@/components/ui/card'
import { Activity } from 'lucide-react'
export function OperationLogInfo() {
return (
<Card className="p-4 bg-blue-50 border-blue-200">
<h4 className="text-blue-900 mb-2">
<Activity 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>
)
}

View File

@@ -0,0 +1,314 @@
import { OperationLog } from '@/types/monitor'
import { ApiResponse, PaginatedResponse, PaginationParams } from '@/types'
export interface OperationLogFilters {
searchKeyword?: string
module?: string
action?: string
status?: string
startDate?: string
endDate?: string
}
export interface OperationLogListParams extends PaginationParams {
filters?: OperationLogFilters
}
export class OperationLogService {
private static baseUrl = '/api/monitor/operation-logs'
/**
* 获取操作日志列表
*/
static async getOperationLogs(params: OperationLogListParams = {}): Promise<PaginatedResponse<OperationLog>> {
try {
const queryParams = new URLSearchParams()
// 添加分页参数
if (params.page) queryParams.append('page', params.page.toString())
if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString())
// 添加筛选参数
if (params.filters) {
if (params.filters.searchKeyword) queryParams.append('searchKeyword', params.filters.searchKeyword)
if (params.filters.module && params.filters.module !== 'all') queryParams.append('module', params.filters.module)
if (params.filters.action && params.filters.action !== 'all') queryParams.append('action', params.filters.action)
if (params.filters.status && params.filters.status !== 'all') queryParams.append('status', params.filters.status)
if (params.filters.startDate) queryParams.append('startDate', params.filters.startDate)
if (params.filters.endDate) queryParams.append('endDate', params.filters.endDate)
}
const response = await fetch(`${this.baseUrl}?${queryParams}`)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
return data
} catch (error) {
console.error('Failed to fetch operation logs:', error)
// 降级处理返回mock数据
return this.getMockData(params)
}
}
/**
* 获取操作日志详情
*/
static async getOperationLogDetail(id: string): Promise<ApiResponse<OperationLog>> {
try {
const response = await fetch(`${this.baseUrl}/${id}`)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
return data
} catch (error) {
console.error('Failed to fetch operation log detail:', error)
throw error
}
}
/**
* 导出操作日志
*/
static async exportOperationLogs(filters?: OperationLogFilters): Promise<Blob> {
try {
const queryParams = new URLSearchParams()
if (filters) {
if (filters.searchKeyword) queryParams.append('searchKeyword', filters.searchKeyword)
if (filters.module && filters.module !== 'all') queryParams.append('module', filters.module)
if (filters.action && filters.action !== 'all') queryParams.append('action', filters.action)
if (filters.status && filters.status !== 'all') queryParams.append('status', filters.status)
if (filters.startDate) queryParams.append('startDate', filters.startDate)
if (filters.endDate) queryParams.append('endDate', filters.endDate)
}
const response = await fetch(`${this.baseUrl}/export?${queryParams}`)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return response.blob()
} catch (error) {
console.error('Failed to export operation logs:', error)
throw error
}
}
/**
* 获取操作日志统计信息
*/
static async getOperationLogStats(filters?: OperationLogFilters): Promise<ApiResponse<{
total: number
success: number
failed: number
averageDuration: number
moduleStats: Array<{ module: string, count: number }>
actionStats: Array<{ action: string, count: number }>
}>> {
try {
const queryParams = new URLSearchParams()
if (filters) {
if (filters.module && filters.module !== 'all') queryParams.append('module', filters.module)
if (filters.action && filters.action !== 'all') queryParams.append('action', filters.action)
if (filters.status && filters.status !== 'all') queryParams.append('status', filters.status)
if (filters.startDate) queryParams.append('startDate', filters.startDate)
if (filters.endDate) queryParams.append('endDate', filters.endDate)
}
const response = await fetch(`${this.baseUrl}/stats?${queryParams}`)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
return data
} catch (error) {
console.error('Failed to fetch operation log stats:', error)
// 降级处理返回mock统计数据
return this.getMockStats()
}
}
/**
* Mock数据 - 用于降级处理
*/
private static getMockData(params: OperationLogListParams): PaginatedResponse<OperationLog> {
const mockLogs: OperationLog[] = [
{
id: 'op-1',
userId: 'user-1',
username: 'admin',
operationTime: '2024-10-21T09:35:00',
module: 'user',
action: 'create',
description: '创建用户账号: zhangsan',
ipAddress: '192.168.1.100',
requestUrl: '/api/users',
requestMethod: 'POST',
requestParams: JSON.stringify({ username: 'zhangsan', name: '张三' }),
duration: 150,
status: 'success',
},
{
id: 'op-2',
userId: 'user-2',
username: 'zhangsan',
operationTime: '2024-10-21T10:20:00',
module: 'machinery',
action: 'update',
description: '更新农机信息: 约翰迪尔拖拉机',
ipAddress: '192.168.1.101',
requestUrl: '/api/machinery/123',
requestMethod: 'PUT',
duration: 89,
status: 'success',
},
{
id: 'op-3',
userId: 'user-3',
username: 'lisi',
operationTime: '2024-10-21T11:25:00',
module: 'role',
action: 'delete',
description: '删除角色: 临时操作员',
ipAddress: '192.168.1.102',
requestUrl: '/api/roles/456',
requestMethod: 'DELETE',
duration: 120,
status: 'failed',
errorMessage: '该角色下仍有关联用户,无法删除',
},
{
id: 'op-4',
userId: 'user-1',
username: 'admin',
operationTime: '2024-10-21T14:50:00',
module: 'system',
action: 'update',
description: '修改系统配置: 会话超时时间',
ipAddress: '192.168.1.100',
requestUrl: '/api/system/settings',
requestMethod: 'PUT',
duration: 95,
status: 'success',
},
{
id: 'op-5',
userId: 'user-2',
username: 'zhangsan',
operationTime: '2024-10-21T15:35:00',
module: 'task',
action: 'create',
description: '创建作业任务: 小麦播种作业',
ipAddress: '192.168.1.101',
requestUrl: '/api/tasks',
requestMethod: 'POST',
duration: 180,
status: 'success',
},
{
id: 'op-6',
userId: 'user-1',
username: 'admin',
operationTime: '2024-10-21T16:15:00',
module: 'user',
action: 'export',
description: '导出用户列表数据',
ipAddress: '192.168.1.100',
requestUrl: '/api/users/export',
requestMethod: 'GET',
duration: 1250,
status: 'success',
},
]
// 应用筛选器
let filteredLogs = mockLogs.filter(log => {
if (params.filters?.searchKeyword) {
const keyword = params.filters.searchKeyword.toLowerCase()
if (!log.username.toLowerCase().includes(keyword) &&
!log.description.toLowerCase().includes(keyword) &&
!log.module.toLowerCase().includes(keyword)) {
return false
}
}
if (params.filters?.module && params.filters.module !== 'all') {
if (log.module !== params.filters.module) return false
}
if (params.filters?.action && params.filters.action !== 'all') {
if (log.action !== params.filters.action) return false
}
if (params.filters?.status && params.filters.status !== 'all') {
if (log.status !== params.filters.status) return false
}
return true
})
// 应用分页
const page = params.page || 1
const pageSize = params.pageSize || 10
const startIndex = (page - 1) * pageSize
const endIndex = startIndex + pageSize
const paginatedLogs = filteredLogs.slice(startIndex, endIndex)
return {
code: 200,
message: 'success',
success: true,
data: paginatedLogs,
pagination: {
page,
pageSize,
total: filteredLogs.length,
totalPages: Math.ceil(filteredLogs.length / pageSize)
}
}
}
private static getMockStats(): ApiResponse<{
total: number
success: number
failed: number
averageDuration: number
moduleStats: Array<{ module: string, count: number }>
actionStats: Array<{ action: string, count: number }>
}> {
return {
code: 200,
message: 'success',
success: true,
data: {
total: 6,
success: 5,
failed: 1,
averageDuration: 314,
moduleStats: [
{ module: 'user', count: 2 },
{ module: 'machinery', count: 1 },
{ module: 'role', count: 1 },
{ module: 'system', count: 1 },
{ module: 'task', count: 1 }
],
actionStats: [
{ action: 'create', count: 2 },
{ action: 'update', count: 2 },
{ action: 'delete', count: 1 },
{ action: 'export', count: 1 }
]
}
}
}
}

View File

@@ -0,0 +1,51 @@
import { Card } from '@/components/ui/card'
import { OperationLog } from '@/types/monitor'
interface OperationLogStatsProps {
logs: OperationLog[]
isLoading?: boolean
}
export function OperationLogStats({ logs, isLoading = false }: OperationLogStatsProps) {
const stats = [
{
label: '总操作数',
value: logs.length,
color: 'text-blue-600',
},
{
label: '成功操作',
value: logs.filter(l => l.status === 'success').length,
color: 'text-green-600',
},
{
label: '失败操作',
value: logs.filter(l => l.status === 'failed').length,
color: 'text-red-600',
},
{
label: '平均耗时',
value: logs.length > 0
? Math.round(logs.reduce((sum, l) => sum + (l.duration || 0), 0) / logs.length) + 'ms'
: '0ms',
color: 'text-purple-600',
},
]
return (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{stats.map((stat, index) => (
<Card key={index} className="p-4">
<div className="text-sm text-muted-foreground">{stat.label}</div>
<div className={`mt-2 text-2xl font-semibold ${stat.color}`}>
{isLoading ? (
<div className="animate-pulse bg-gray-200 h-8 w-16 rounded"></div>
) : (
stat.value
)}
</div>
</Card>
))}
</div>
)
}

View File

@@ -0,0 +1,144 @@
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Card } from '@/components/ui/card'
import { OperationLog } from '@/types/monitor'
import { Eye } from 'lucide-react'
interface OperationLogTableProps {
logs: OperationLog[]
isLoading?: boolean
onViewDetail: (log: OperationLog) => void
}
export function OperationLogTable({ logs, isLoading = false, onViewDetail }: OperationLogTableProps) {
const getModuleLabel = (module: string) => {
const labels: Record<string, string> = {
user: '用户管理',
role: '角色管理',
permission: '权限管理',
machinery: '农机管理',
driver: '驾驶员管理',
task: '任务管理',
system: '系统配置',
other: '其他',
}
return labels[module] || module
}
const getActionLabel = (action: string) => {
const labels: Record<string, string> = {
create: '新增',
update: '修改',
delete: '删除',
view: '查看',
export: '导出',
import: '导入',
login: '登录',
logout: '登出',
}
return labels[action] || action
}
const getActionBadge = (action: string) => {
const colors: Record<string, string> = {
create: 'bg-green-100 text-green-700',
update: 'bg-blue-100 text-blue-700',
delete: 'bg-red-100 text-red-700',
view: 'bg-gray-100 text-gray-700',
export: 'bg-purple-100 text-purple-700',
import: 'bg-yellow-100 text-yellow-700',
}
return colors[action] || 'bg-gray-100 text-gray-700'
}
if (isLoading) {
return (
<Card>
<div className="p-8 space-y-4">
{Array.from({ length: 5 }).map((_, index) => (
<div key={index} className="animate-pulse">
<div className="flex space-x-4">
<div className="bg-gray-200 h-4 w-20 rounded"></div>
<div className="bg-gray-200 h-4 w-32 rounded"></div>
<div className="bg-gray-200 h-4 w-16 rounded"></div>
<div className="bg-gray-200 h-4 w-24 rounded"></div>
<div className="bg-gray-200 h-4 w-40 rounded"></div>
<div className="bg-gray-200 h-4 w-16 rounded"></div>
<div className="bg-gray-200 h-4 w-16 rounded"></div>
<div className="bg-gray-200 h-8 w-8 rounded"></div>
</div>
</div>
))}
</div>
</Card>
)
}
return (
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{logs.length === 0 ? (
<TableRow>
<TableCell colSpan={8} className="text-center text-muted-foreground py-8">
</TableCell>
</TableRow>
) : (
logs.map((log) => (
<TableRow key={log.id}>
<TableCell>{log.username}</TableCell>
<TableCell className="text-muted-foreground text-sm">
{new Date(log.operationTime).toLocaleString('zh-CN')}
</TableCell>
<TableCell>
<Badge variant="outline">{getModuleLabel(log.module)}</Badge>
</TableCell>
<TableCell>
<Badge className={getActionBadge(log.action)}>
{getActionLabel(log.action)}
</Badge>
</TableCell>
<TableCell className="max-w-xs truncate" title={log.description}>
{log.description}
</TableCell>
<TableCell className="text-sm">
{log.duration ? `${log.duration}ms` : '-'}
</TableCell>
<TableCell>
{log.status === 'success' ? (
<Badge className="bg-green-100 text-green-700"></Badge>
) : (
<Badge className="bg-red-100 text-red-700"></Badge>
)}
</TableCell>
<TableCell>
<Button
variant="ghost"
size="sm"
onClick={() => onViewDetail(log)}
>
<Eye className="w-4 h-4" />
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</Card>
)
}

View File

@@ -0,0 +1,9 @@
export { OperationLogService } from './OperationLogService'
export { OperationLogStats } from './OperationLogStats'
export { OperationLogFilters } from './OperationLogFilters'
export { OperationLogTable } from './OperationLogTable'
export { OperationLogDetailDialog } from './OperationLogDetailDialog'
export { OperationLogInfo } from './OperationLogInfo'
export type { OperationLogListParams } from './OperationLogService'
export type { OperationLogFilters as OperationLogFiltersType } from './OperationLogService'

View File

@@ -0,0 +1,152 @@
'use client'
import { useState, useEffect } from 'react'
import { Button } from '@/components/ui/button'
import { OperationLog } from '@/types/monitor'
import { Download } from 'lucide-react'
import { toast } from 'sonner'
// Import modular components
import {
OperationLogService,
OperationLogStats,
OperationLogFilters,
OperationLogTable,
OperationLogDetailDialog,
OperationLogInfo
} from './components'
export default function OperationLogPage() {
const [logs, setLogs] = useState<OperationLog[]>([])
const [isLoading, setIsLoading] = useState(false)
const [searchKeyword, setSearchKeyword] = useState('')
const [moduleFilter, setModuleFilter] = useState<string>('all')
const [actionFilter, setActionFilter] = useState<string>('all')
const [statusFilter, setStatusFilter] = useState<string>('all')
const [showDetailDialog, setShowDetailDialog] = useState(false)
const [selectedLog, setSelectedLog] = useState<OperationLog | null>(null)
const [isDetailLoading, setIsDetailLoading] = useState(false)
useEffect(() => {
loadLogs()
}, [searchKeyword, moduleFilter, actionFilter, statusFilter])
const loadLogs = async () => {
setIsLoading(true)
try {
const response = await OperationLogService.getOperationLogs({
page: 1,
pageSize: 100,
filters: {
searchKeyword,
module: moduleFilter,
action: actionFilter,
status: statusFilter
}
})
if (response.success) {
setLogs(response.data)
} else {
throw new Error(response.message || '加载操作日志失败')
}
} catch (error) {
console.error('Failed to load operation logs:', error)
toast.error('加载操作日志失败,请稍后重试')
} finally {
setIsLoading(false)
}
}
const handleViewDetail = async (log: OperationLog) => {
setSelectedLog(log)
setShowDetailDialog(true)
setIsDetailLoading(true)
try {
const response = await OperationLogService.getOperationLogDetail(log.id)
if (response.success) {
setSelectedLog(response.data)
}
} catch (error) {
console.error('Failed to fetch log detail:', error)
} finally {
setIsDetailLoading(false)
}
}
const handleExport = async () => {
try {
const blob = await OperationLogService.exportOperationLogs({
searchKeyword,
module: moduleFilter,
action: actionFilter,
status: statusFilter
})
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `operation_logs_${new Date().getTime()}.json`
link.click()
URL.revokeObjectURL(url)
toast.success('导出成功')
} catch (error) {
console.error('Failed to export logs:', error)
toast.error('导出失败,请稍后重试')
}
}
const handleRefresh = () => {
loadLogs()
}
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={handleRefresh} disabled={isLoading}>
</Button>
<Button onClick={handleExport} disabled={isLoading || logs.length === 0}>
<Download className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
<OperationLogStats logs={logs} isLoading={isLoading} />
<OperationLogFilters
searchKeyword={searchKeyword}
onSearchChange={setSearchKeyword}
moduleFilter={moduleFilter}
onModuleFilterChange={setModuleFilter}
actionFilter={actionFilter}
onActionFilterChange={setActionFilter}
statusFilter={statusFilter}
onStatusFilterChange={setStatusFilter}
/>
<OperationLogTable
logs={logs}
isLoading={isLoading}
onViewDetail={handleViewDetail}
/>
<OperationLogDetailDialog
log={selectedLog}
isOpen={showDetailDialog}
onClose={() => setShowDetailDialog(false)}
isLoading={isDetailLoading}
/>
<OperationLogInfo />
</div>
)
}

View File

@@ -0,0 +1,30 @@
'use client';
import React from 'react';
import Link from 'next/link';
export default function MonitorPage() {
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/monitor/login-log" 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/monitor/operation-log" 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/monitor/performance" 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/monitor/network-log" 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,40 @@
import { Card } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import { Badge } from '@/components/ui/badge';
import { Cpu } from 'lucide-react';
import { SystemPerformance } from '@/types/monitor';
interface CpuMetricCardProps {
performance: SystemPerformance;
getUsageColor: (usage: number) => string;
getUsageStatus: (usage: number) => string;
}
export function CpuMetricCard({ performance, getUsageColor, getUsageStatus }: CpuMetricCardProps) {
return (
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Cpu className="w-5 h-5 text-blue-600" />
<h3>CPU使用率</h3>
</div>
<Badge className={getUsageColor(performance.cpu.usage)}>
{getUsageStatus(performance.cpu.usage)}
</Badge>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">使</span>
<span className={`${getUsageColor(performance.cpu.usage)}`}>
{performance.cpu.usage.toFixed(1)}%
</span>
</div>
<Progress value={performance.cpu.usage} className="h-2" />
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground"></span>
<span>{performance.cpu.cores} </span>
</div>
</div>
</Card>
);
}

View File

@@ -0,0 +1,51 @@
import { Card } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import { Badge } from '@/components/ui/badge';
import { HardDrive } from 'lucide-react';
import { SystemPerformance } from '@/types/monitor';
interface DiskMetricCardProps {
performance: SystemPerformance;
getUsageColor: (usage: number) => string;
getUsageStatus: (usage: number) => string;
formatBytes: (bytes: number) => string;
}
export function DiskMetricCard({ performance, getUsageColor, getUsageStatus, formatBytes }: DiskMetricCardProps) {
return (
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<HardDrive className="w-5 h-5 text-purple-600" />
<h3>使</h3>
</div>
<Badge className={getUsageColor(performance.disk.usage)}>
{getUsageStatus(performance.disk.usage)}
</Badge>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">使</span>
<span className={getUsageColor(performance.disk.usage)}>
{performance.disk.usage.toFixed(1)}%
</span>
</div>
<Progress value={performance.disk.usage} className="h-2" />
<div className="grid grid-cols-3 gap-4 text-sm">
<div>
<span className="text-muted-foreground"></span>
<p>{formatBytes(performance.disk.used)}</p>
</div>
<div>
<span className="text-muted-foreground"></span>
<p>{formatBytes(performance.disk.free)}</p>
</div>
<div>
<span className="text-muted-foreground"></span>
<p>{formatBytes(performance.disk.total)}</p>
</div>
</div>
</div>
</Card>
);
}

View File

@@ -0,0 +1,53 @@
import { Card } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import { Server } from 'lucide-react';
import { SystemPerformance } from '@/types/monitor';
interface JvmInfoCardProps {
performance: SystemPerformance;
formatBytes: (bytes: number) => string;
}
export function JvmInfoCard({ performance, formatBytes }: JvmInfoCardProps) {
if (!performance.jvm) {
return null;
}
return (
<Card className="p-6">
<div className="flex items-center gap-2 mb-4">
<Server className="w-5 h-5 text-orange-600" />
<h3>JVM信息</h3>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<div>
<p className="text-sm text-muted-foreground">使</p>
<p className="mt-1">{performance.jvm.heapUsage.toFixed(1)}%</p>
<Progress value={performance.jvm.heapUsage} className="h-1 mt-2" />
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1">
{formatBytes(performance.jvm.heapUsed)} / {formatBytes(performance.jvm.heapMax)}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1">{formatBytes(performance.jvm.nonHeapUsed)}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">线</p>
<p className="mt-1">{performance.jvm.threadCount}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">GC次数</p>
<p className="mt-1">{performance.jvm.gcCount}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">GC耗时</p>
<p className="mt-1">{performance.jvm.gcTime}ms</p>
</div>
</div>
</Card>
);
}

View File

@@ -0,0 +1,47 @@
import { Card } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import { Badge } from '@/components/ui/badge';
import { MemoryStick } from 'lucide-react';
import { SystemPerformance } from '@/types/monitor';
interface MemoryMetricCardProps {
performance: SystemPerformance;
getUsageColor: (usage: number) => string;
getUsageStatus: (usage: number) => string;
formatBytes: (bytes: number) => string;
}
export function MemoryMetricCard({ performance, getUsageColor, getUsageStatus, formatBytes }: MemoryMetricCardProps) {
return (
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<MemoryStick className="w-5 h-5 text-green-600" />
<h3>使</h3>
</div>
<Badge className={getUsageColor(performance.memory.usage)}>
{getUsageStatus(performance.memory.usage)}
</Badge>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">使</span>
<span className={getUsageColor(performance.memory.usage)}>
{performance.memory.usage.toFixed(1)}%
</span>
</div>
<Progress value={performance.memory.usage} className="h-2" />
<div className="grid grid-cols-2 gap-2 text-sm">
<div>
<span className="text-muted-foreground"></span>
<p>{formatBytes(performance.memory.used)}</p>
</div>
<div>
<span className="text-muted-foreground"></span>
<p>{formatBytes(performance.memory.total)}</p>
</div>
</div>
</div>
</Card>
);
}

View File

@@ -0,0 +1,20 @@
import { Card } from '@/components/ui/card';
import { Activity } from 'lucide-react';
export function PerformanceInstructions() {
return (
<Card className="p-4 bg-blue-50 border-blue-200">
<h4 className="text-blue-900 mb-2">
<Activity className="w-4 h-4 inline mr-2" />
</h4>
<ul className="space-y-1 text-sm text-blue-800">
<li> 5</li>
<li> 使60%80%</li>
<li> 20</li>
<li> 使</li>
<li> JVM和Tomcat信息仅在Java环境下可用</li>
</ul>
</Card>
);
}

View File

@@ -0,0 +1,42 @@
import { Card } from '@/components/ui/card';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import { SystemPerformance } from '@/types/monitor';
interface PerformanceTrendChartProps {
history: SystemPerformance[];
}
export function PerformanceTrendChart({ history }: PerformanceTrendChartProps) {
if (history.length <= 1) {
return null;
}
const chartData = history.map((item) => ({
time: new Date(item.timestamp).toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}),
CPU: item.cpu.usage.toFixed(1),
内存: item.memory.usage.toFixed(1),
磁盘: item.disk.usage.toFixed(1),
}));
return (
<Card className="p-6">
<h3 className="mb-4"></h3>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="time" />
<YAxis />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="CPU" stroke="#3b82f6" strokeWidth={2} />
<Line type="monotone" dataKey="内存" stroke="#10b981" strokeWidth={2} />
<Line type="monotone" dataKey="磁盘" stroke="#a855f7" strokeWidth={2} />
</LineChart>
</ResponsiveContainer>
</Card>
);
}

View File

@@ -0,0 +1,48 @@
import { Card } from '@/components/ui/card';
import { Activity } from 'lucide-react';
import { SystemPerformance } from '@/types/monitor';
interface TomcatInfoCardProps {
performance: SystemPerformance;
}
export function TomcatInfoCard({ performance }: TomcatInfoCardProps) {
if (!performance.tomcat) {
return null;
}
return (
<Card className="p-6">
<div className="flex items-center gap-2 mb-4">
<Activity className="w-5 h-5 text-red-600" />
<h3>Tomcat信息</h3>
</div>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
<div>
<p className="text-sm text-muted-foreground">线</p>
<p className="mt-1">
{performance.tomcat.threadCount} / {performance.tomcat.maxThreads}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1">{performance.tomcat.connectionCount}</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1">{performance.tomcat.requestCount.toLocaleString()}</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1 text-red-600">{performance.tomcat.errorCount}</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1">
{((performance.tomcat.errorCount / performance.tomcat.requestCount) * 100).toFixed(2)}%
</p>
</div>
</div>
</Card>
);
}

View File

@@ -0,0 +1,7 @@
export { CpuMetricCard } from './CpuMetricCard';
export { MemoryMetricCard } from './MemoryMetricCard';
export { DiskMetricCard } from './DiskMetricCard';
export { JvmInfoCard } from './JvmInfoCard';
export { TomcatInfoCard } from './TomcatInfoCard';
export { PerformanceTrendChart } from './PerformanceTrendChart';
export { PerformanceInstructions } from './PerformanceInstructions';

View File

@@ -0,0 +1,274 @@
'use client';
import { useState, useEffect } from 'react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { RefreshCw } from 'lucide-react';
import { SystemPerformance } from '@/types/monitor';
import {
CpuMetricCard,
MemoryMetricCard,
DiskMetricCard,
JvmInfoCard,
TomcatInfoCard,
PerformanceTrendChart,
PerformanceInstructions
} from './components';
// API服务函数
const performanceApi = {
// 获取当前性能数据
getCurrentPerformance: async (): Promise<SystemPerformance> => {
try {
const response = await fetch('/api/monitor/performance/current');
if (!response.ok) {
throw new Error('Failed to fetch performance data');
}
return await response.json();
} catch (error) {
console.warn('Failed to fetch performance data, using mock data:', error);
// 如果API调用失败返回模拟数据
return getMockPerformanceData();
}
},
// 获取历史性能数据
getPerformanceHistory: async (limit: number = 20): Promise<SystemPerformance[]> => {
try {
const response = await fetch(`/api/monitor/performance/history?limit=${limit}`);
if (!response.ok) {
throw new Error('Failed to fetch performance history');
}
return await response.json();
} catch (error) {
console.warn('Failed to fetch performance history, using mock data:', error);
// 如果API调用失败返回模拟历史数据
return Array.from({ length: Math.min(5, limit) }, (_, i) => {
const mockData = getMockPerformanceData();
mockData.timestamp = new Date(Date.now() - (4 - i) * 5000).toISOString();
return mockData;
});
}
}
};
// 模拟数据生成函数
function getMockPerformanceData(): SystemPerformance {
const mockData: SystemPerformance = {
timestamp: new Date().toISOString(),
cpu: {
usage: Math.random() * 60 + 20, // 20-80%
cores: 8,
},
memory: {
total: 16384, // 16GB
used: Math.random() * 8192 + 4096, // 4-12GB
free: 0,
usage: 0,
},
disk: {
total: 512, // 512GB
used: Math.random() * 102 + 204, // 204-306GB
free: 0,
usage: 0,
},
jvm: {
heapUsed: Math.random() * 1024 + 512, // 512-1536MB
heapMax: 2048, // 2GB
heapUsage: 0,
nonHeapUsed: Math.random() * 100 + 50,
threadCount: Math.floor(Math.random() * 50 + 100),
gcCount: Math.floor(Math.random() * 10 + 50),
gcTime: Math.floor(Math.random() * 200 + 100),
},
tomcat: {
threadCount: Math.floor(Math.random() * 50 + 50),
maxThreads: 200,
connectionCount: Math.floor(Math.random() * 100 + 50),
requestCount: Math.floor(Math.random() * 10000 + 50000),
errorCount: Math.floor(Math.random() * 10),
},
};
// 计算百分比
mockData.memory.free = mockData.memory.total - mockData.memory.used;
mockData.memory.usage = (mockData.memory.used / mockData.memory.total) * 100;
mockData.disk.free = mockData.disk.total - mockData.disk.used;
mockData.disk.usage = (mockData.disk.used / mockData.disk.total) * 100;
if (mockData.jvm) {
mockData.jvm.heapUsage = (mockData.jvm.heapUsed / mockData.jvm.heapMax) * 100;
}
return mockData;
}
export default function PerformanceMonitorPage() {
const [performance, setPerformance] = useState<SystemPerformance | null>(null);
const [history, setHistory] = useState<SystemPerformance[]>([]);
const [autoRefresh, setAutoRefresh] = useState(true);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadPerformance();
const interval = setInterval(() => {
if (autoRefresh) {
loadPerformance();
}
}, 5000); // 每5秒刷新一次
return () => clearInterval(interval);
}, [autoRefresh]);
const loadPerformance = async () => {
try {
setError(null);
setLoading(true);
// 获取当前数据
const currentData = await performanceApi.getCurrentPerformance();
setPerformance(currentData);
// 保存历史数据最多保留20条
setHistory(prev => {
const newHistory = [...prev, currentData].slice(-20);
return newHistory;
});
} catch (err) {
setError(err instanceof Error ? err.message : '加载性能数据失败');
console.error('Failed to load performance data:', err);
} finally {
setLoading(false);
}
};
const getUsageColor = (usage: number) => {
if (usage < 60) return 'text-green-600';
if (usage < 80) return 'text-yellow-600';
return 'text-red-600';
};
const getUsageStatus = (usage: number) => {
if (usage < 60) return '正常';
if (usage < 80) return '警告';
return '危险';
};
const formatBytes = (bytes: number) => {
if (bytes < 1024) return `${bytes.toFixed(2)} MB`;
return `${(bytes / 1024).toFixed(2)} GB`;
};
if (loading && !performance) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<RefreshCw className="w-8 h-8 animate-spin text-green-600 mx-auto mb-2" />
<p className="text-muted-foreground">...</p>
</div>
</div>
);
}
if (error && !performance) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<p className="text-red-600 mb-4">: {error}</p>
<Button onClick={loadPerformance} variant="outline">
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
);
}
if (!performance) {
return null;
}
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 items-center gap-2">
<Badge variant={autoRefresh ? 'default' : 'outline'}>
{autoRefresh ? '自动刷新' : '已暂停'}
</Badge>
<Button
variant="outline"
size="sm"
onClick={() => setAutoRefresh(!autoRefresh)}
>
{autoRefresh ? '暂停' : '启动'}
</Button>
<Button
variant="outline"
size="sm"
onClick={loadPerformance}
disabled={loading}
>
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
</Button>
</div>
</div>
{error && (
<div className="p-4 border border-yellow-200 bg-yellow-50 rounded-md">
<p className="text-yellow-800 text-sm">
: {error} ()
</p>
</div>
)}
{/* CPU和内存卡片 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<CpuMetricCard
performance={performance}
getUsageColor={getUsageColor}
getUsageStatus={getUsageStatus}
/>
<MemoryMetricCard
performance={performance}
getUsageColor={getUsageColor}
getUsageStatus={getUsageStatus}
formatBytes={formatBytes}
/>
</div>
{/* 磁盘卡片 */}
<DiskMetricCard
performance={performance}
getUsageColor={getUsageColor}
getUsageStatus={getUsageStatus}
formatBytes={formatBytes}
/>
{/* JVM信息卡片 */}
<JvmInfoCard
performance={performance}
formatBytes={formatBytes}
/>
{/* Tomcat信息卡片 */}
<TomcatInfoCard
performance={performance}
/>
{/* 性能趋势图 */}
<PerformanceTrendChart
history={history}
/>
{/* 使用说明 */}
<PerformanceInstructions />
</div>
);
}

View File

@@ -1,11 +0,0 @@
export default function SystemMonitorLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="system-monitor-layout">
{children}
</div>
)
}

View File

@@ -1,8 +0,0 @@
export default function LoginLogPage() {
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4"></h1>
<p></p>
</div>
)
}

View File

@@ -1,8 +0,0 @@
export default function NetworkLogPage() {
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4"></h1>
<p></p>
</div>
)
}

View File

@@ -1,8 +0,0 @@
export default function OperationLogPage() {
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4"></h1>
<p></p>
</div>
)
}

View File

@@ -1,8 +0,0 @@
export default function SystemMonitorPage() {
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4"></h1>
<p></p>
</div>
)
}

View File

@@ -1,8 +0,0 @@
export default function PerformanceMonitorPage() {
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4"></h1>
<p></p>
</div>
)
}

View File

@@ -1,185 +0,0 @@
import { Metadata } from 'next'
export const metadata: Metadata = {
title: '异常处理 - Crop-X 智慧农业管理系统',
description: '系统异常处理管理页面',
}
export default function ExceptionHandlingPage() {
return (
<div className="space-y-6">
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-2xl font-bold text-gray-800 mb-6">
</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-red-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-red-900 mb-4">
</h3>
<div className="space-y-3">
{[
{
id: 'ERR001',
type: '数据库连接异常',
severity: '高',
time: '2024-10-20 15:30:25',
status: 'active',
description: 'MySQL连接池耗尽无法获取新连接'
},
{
id: 'ERR002',
type: 'API超时异常',
severity: '中',
time: '2024-10-20 15:28:15',
status: 'active',
description: '第三方天气API调用超时'
},
{
id: 'ERR003',
type: '内存溢出警告',
severity: '中',
time: '2024-10-20 15:25:42',
status: 'resolved',
description: 'JVM内存使用率超过85%阈值'
},
].map((error) => (
<div key={error.id} className="bg-white rounded-lg p-4 shadow-sm border-l-4 border-red-500">
<div className="flex justify-between items-start mb-2">
<div>
<h4 className="font-semibold text-gray-800">{error.type}</h4>
<p className="text-sm text-gray-600">{error.description}</p>
<p className="text-xs text-gray-500 mt-1">{error.time}</p>
</div>
<div className="flex flex-col items-end space-y-1">
<span className={`px-2 py-1 text-xs font-medium rounded-full ${
error.severity === '高' ? 'bg-red-100 text-red-800' : 'bg-yellow-100 text-yellow-800'
}`}>
{error.severity}
</span>
<span className={`px-2 py-1 text-xs font-medium rounded-full ${
error.status === 'active' ? 'bg-orange-100 text-orange-800' : 'bg-green-100 text-green-800'
}`}>
{error.status === 'active' ? '活跃' : '已解决'}
</span>
</div>
</div>
<div className="flex space-x-2 mt-3">
<button className="px-3 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700">
</button>
<button className="px-3 py-1 bg-green-600 text-white text-xs rounded hover:bg-green-700">
</button>
<button className="px-3 py-1 bg-purple-600 text-white text-xs rounded hover:bg-purple-700">
</button>
</div>
</div>
))}
</div>
</div>
<div className="bg-yellow-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-yellow-900 mb-4">
</h3>
<div className="space-y-4">
<div className="bg-white rounded-lg p-4">
<h4 className="font-medium text-gray-800 mb-3"></h4>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-gray-600"></span>
<span className="font-semibold text-red-600">23 </span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600"></span>
<span className="font-semibold text-green-600">18 </span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600"></span>
<span className="font-semibold text-orange-600">5 </span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600"></span>
<span className="font-semibold text-blue-600">15</span>
</div>
</div>
</div>
<div className="bg-white rounded-lg p-4">
<h4 className="font-medium text-gray-800 mb-3"></h4>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-gray-600"></span>
<span className="font-semibold text-purple-600">8 </span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600"></span>
<span className="font-semibold text-blue-600">6 </span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600"></span>
<span className="font-semibold text-green-600">5 </span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600"></span>
<span className="font-semibold text-orange-600">4 </span>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="mt-6 bg-green-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-green-900 mb-4">
🔧
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h4 className="font-medium text-gray-800 mb-3"></h4>
<div className="space-y-2">
<div className="flex justify-between items-center p-2 bg-white rounded">
<span className="text-sm text-gray-700"></span>
<span className="text-sm text-green-600"></span>
</div>
<div className="flex justify-between items-center p-2 bg-white rounded">
<span className="text-sm text-gray-700"></span>
<span className="text-sm text-green-600"></span>
</div>
<div className="flex justify-between items-center p-2 bg-white rounded">
<span className="text-sm text-gray-700"></span>
<span className="text-sm text-gray-600"></span>
</div>
<div className="flex justify-between items-center p-2 bg-white rounded">
<span className="text-sm text-gray-700"></span>
<span className="text-sm text-green-600"></span>
</div>
</div>
</div>
<div>
<h4 className="font-medium text-gray-800 mb-3"></h4>
<div className="grid grid-cols-2 gap-2">
<button className="px-3 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors text-sm">
</button>
<button className="px-3 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors text-sm">
</button>
<button className="px-3 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700 transition-colors text-sm">
</button>
<button className="px-3 py-2 bg-orange-600 text-white rounded-md hover:bg-orange-700 transition-colors text-sm">
</button>
</div>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -1,22 +0,0 @@
import { ReactNode } from 'react'
export default function SystemMonitoringLayout({
children,
}: {
children: ReactNode
}) {
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white shadow-sm border-b">
<div className="container mx-auto px-4 py-4">
<h1 className="text-2xl font-bold text-green-900">
📈
</h1>
</div>
</header>
<main className="container mx-auto px-4 py-8">
{children}
</main>
</div>
)
}

View File

@@ -1,160 +0,0 @@
import { Metadata } from 'next'
export const metadata: Metadata = {
title: '日志管理 - Crop-X 智慧农业管理系统',
description: '系统日志管理页面',
}
export default function LogManagementPage() {
return (
<div className="space-y-6">
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-2xl font-bold text-gray-800 mb-6">
📋
</h2>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<div className="bg-gray-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">
</h3>
<div className="mb-4 flex space-x-4">
<select className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option></option>
<option>ERROR</option>
<option>WARN</option>
<option>INFO</option>
<option>DEBUG</option>
</select>
<input
type="text"
placeholder="搜索日志内容..."
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors">
</button>
</div>
<div className="bg-white rounded-lg p-4 max-h-96 overflow-y-auto">
<div className="space-y-3">
{[
{ time: '2024-10-20 15:30:25', level: 'INFO', module: '用户管理', message: '用户登录成功: admin', status: 'normal' },
{ time: '2024-10-20 15:28:15', level: 'WARN', module: '农机管理', message: '农机NJ001离线超时', status: 'warning' },
{ time: '2024-10-20 15:25:42', level: 'ERROR', module: '数据同步', message: 'API调用失败: timeout', status: 'error' },
{ time: '2024-10-20 15:22:18', level: 'INFO', module: '任务调度', message: '定时任务执行完成', status: 'normal' },
{ time: '2024-10-20 15:20:05', level: 'INFO', module: '系统监控', message: '系统性能指标正常', status: 'normal' },
].map((log, index) => (
<div key={index} className="border-l-4 border-gray-300 pl-4 py-2 hover:bg-gray-50">
<div className="flex justify-between items-start">
<div className="flex-1">
<div className="flex items-center space-x-2 mb-1">
<span className={`text-xs font-medium px-2 py-1 rounded ${
log.level === 'ERROR' ? 'bg-red-100 text-red-800' :
log.level === 'WARN' ? 'bg-yellow-100 text-yellow-800' :
log.level === 'INFO' ? 'bg-blue-100 text-blue-800' :
'bg-gray-100 text-gray-800'
}`}>
{log.level}
</span>
<span className="text-sm text-gray-500">{log.time}</span>
<span className="text-sm text-gray-600">[{log.module}]</span>
</div>
<p className="text-sm text-gray-700">{log.message}</p>
</div>
</div>
</div>
))}
</div>
</div>
<div className="mt-4 flex justify-between items-center">
<div className="text-sm text-gray-600">
1-5 1,245
</div>
<div className="flex space-x-2">
<button className="px-3 py-1 border border-gray-300 rounded-md hover:bg-gray-50">
</button>
<button className="px-3 py-1 bg-blue-600 text-white rounded-md">
1
</button>
<button className="px-3 py-1 border border-gray-300 rounded-md hover:bg-gray-50">
2
</button>
<button className="px-3 py-1 border border-gray-300 rounded-md hover:bg-gray-50">
</button>
</div>
</div>
</div>
</div>
<div className="space-y-4">
<div className="bg-green-50 rounded-lg p-4">
<h4 className="font-semibold text-green-900 mb-3"></h4>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-gray-600"></span>
<span className="font-semibold text-green-600">1,245</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600"></span>
<span className="font-semibold text-red-600">12</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600"></span>
<span className="font-semibold text-yellow-600">45</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600"></span>
<span className="font-semibold text-blue-600">1,188</span>
</div>
</div>
</div>
<div className="bg-blue-50 rounded-lg p-4">
<h4 className="font-semibold text-blue-900 mb-3"></h4>
<div className="space-y-2">
<button className="w-full px-3 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors text-sm">
</button>
<button className="w-full px-3 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors text-sm">
</button>
<button className="w-full px-3 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700 transition-colors text-sm">
</button>
</div>
</div>
<div className="bg-purple-50 rounded-lg p-4">
<h4 className="font-semibold text-purple-900 mb-3"></h4>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600"></span>
<select className="text-xs px-2 py-1 border border-gray-300 rounded">
<option>INFO</option>
<option>DEBUG</option>
<option>WARN</option>
<option>ERROR</option>
</select>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600"></span>
<span className="text-sm font-semibold">30</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600"></span>
<span className="text-sm text-green-600"></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -1,103 +0,0 @@
import Link from 'next/link'
import { Metadata } from 'next'
export const metadata: Metadata = {
title: '系统监控 - Crop-X 智慧农业管理系统',
description: '系统监控管理页面',
}
export default function SystemMonitoringPage() {
return (
<div className="space-y-6">
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold text-gray-800 mb-4">
</h2>
<p className="text-gray-600 mb-6">
</p>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<Link
href="/central-config/system-monitoring/performance-monitoring"
className="block p-4 bg-green-50 rounded-lg hover:bg-green-100 transition-colors"
>
<h3 className="font-semibold text-green-900 mb-2">
📊
</h3>
<p className="text-green-700 text-sm">
</p>
</Link>
<Link
href="/central-config/system-monitoring/log-management"
className="block p-4 bg-blue-50 rounded-lg hover:bg-blue-100 transition-colors"
>
<h3 className="font-semibold text-blue-900 mb-2">
📋
</h3>
<p className="text-blue-700 text-sm">
</p>
</Link>
<Link
href="/central-config/system-monitoring/exception-handling"
className="block p-4 bg-purple-50 rounded-lg hover:bg-purple-100 transition-colors"
>
<h3 className="font-semibold text-purple-900 mb-2">
</h3>
<p className="text-purple-700 text-sm">
</p>
</Link>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">
📊
</h3>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-gray-600"></span>
<span className="text-green-600 font-semibold">15 8</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600">CPU使用率</span>
<span className="text-blue-600 font-semibold">45.2%</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600">使</span>
<span className="text-purple-600 font-semibold">68.7%</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600"></span>
<span className="text-orange-600 font-semibold">3 </span>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">
🔧
</h3>
<div className="space-y-2">
<button className="w-full px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 transition-colors">
</button>
<button className="w-full px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors">
</button>
<button className="w-full px-4 py-2 bg-purple-600 text-white rounded hover:bg-purple-700 transition-colors">
</button>
</div>
</div>
</div>
</div>
)
}

View File

@@ -1,119 +0,0 @@
import { Metadata } from 'next'
export const metadata: Metadata = {
title: '性能监控 - Crop-X 智慧农业管理系统',
description: '系统性能监控管理页面',
}
export default function PerformanceMonitoringPage() {
return (
<div className="space-y-6">
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-2xl font-bold text-gray-800 mb-6">
📊
</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-green-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-green-900 mb-4">
</h3>
<div className="space-y-4">
<div>
<div className="flex justify-between items-center mb-2">
<span className="text-gray-700">CPU 使</span>
<span className="font-semibold text-green-600">45.2%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div className="bg-green-600 h-2 rounded-full" style={{ width: '45.2%' }}></div>
</div>
</div>
<div>
<div className="flex justify-between items-center mb-2">
<span className="text-gray-700">使</span>
<span className="font-semibold text-blue-600">68.7%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div className="bg-blue-600 h-2 rounded-full" style={{ width: '68.7%' }}></div>
</div>
</div>
<div>
<div className="flex justify-between items-center mb-2">
<span className="text-gray-700">使</span>
<span className="font-semibold text-purple-600">32.1%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div className="bg-purple-600 h-2 rounded-full" style={{ width: '32.1%' }}></div>
</div>
</div>
<div>
<div className="flex justify-between items-center mb-2">
<span className="text-gray-700"></span>
<span className="font-semibold text-orange-600">28.5%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div className="bg-orange-600 h-2 rounded-full" style={{ width: '28.5%' }}></div>
</div>
</div>
</div>
</div>
<div className="bg-blue-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-blue-900 mb-4">
</h3>
<div className="bg-white rounded-lg p-4 h-64 flex items-center justify-center">
<div className="text-center">
<div className="text-4xl mb-2">📈</div>
<p className="text-gray-600">
</p>
<p className="text-sm text-gray-500">
()
</p>
</div>
</div>
</div>
</div>
<div className="mt-6 grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-white rounded-lg p-4 border-l-4 border-green-500">
<h4 className="font-semibold text-gray-800 mb-2"></h4>
<div className="text-2xl font-bold text-green-600 mb-1">125ms</div>
<div className="text-sm text-gray-600"></div>
</div>
<div className="bg-white rounded-lg p-4 border-l-4 border-blue-500">
<h4 className="font-semibold text-gray-800 mb-2"></h4>
<div className="text-2xl font-bold text-blue-600 mb-1">1,245</div>
<div className="text-sm text-gray-600">/</div>
</div>
<div className="bg-white rounded-lg p-4 border-l-4 border-purple-500">
<h4 className="font-semibold text-gray-800 mb-2"></h4>
<div className="text-2xl font-bold text-purple-600 mb-1">99.9%</div>
<div className="text-sm text-gray-600"></div>
</div>
</div>
<div className="mt-6 bg-yellow-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-yellow-900 mb-4">
</h3>
<div className="space-y-2">
<div className="flex items-center">
<div className="w-2 h-2 bg-yellow-500 rounded-full mr-3"></div>
<span className="text-gray-700">使</span>
</div>
<div className="flex items-center">
<div className="w-2 h-2 bg-green-500 rounded-full mr-3"></div>
<span className="text-gray-700">CPU使用率正常</span>
</div>
<div className="flex items-center">
<div className="w-2 h-2 bg-blue-500 rounded-full mr-3"></div>
<span className="text-gray-700"></span>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -1,223 +0,0 @@
import { Metadata } from 'next'
export const metadata: Metadata = {
title: '基础配置 - Crop-X 智慧农业管理系统',
description: '基础配置管理页面',
}
export default function BasicConfigurationPage() {
return (
<div className="space-y-6">
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-2xl font-bold text-gray-800 mb-6">
</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="space-y-6">
<div className="bg-green-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-green-900 mb-4">
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<input
type="text"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
defaultValue="Crop-X 智慧农业管理系统"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<input
type="text"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
defaultValue="v2.1.0"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<select className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500">
<option>Asia/Shanghai (UTC+8)</option>
<option>Asia/Beijing (UTC+8)</option>
<option>UTC (UTC+0)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<select className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500">
<option></option>
<option>English</option>
</select>
</div>
</div>
</div>
<div className="bg-blue-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-blue-900 mb-4">
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
()
</label>
<input
type="number"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
defaultValue="30"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<input
type="number"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
defaultValue="8"
/>
</div>
<div className="flex items-center">
<input
type="checkbox"
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
defaultChecked
/>
<label className="ml-2 block text-sm text-gray-700">
</label>
</div>
<div className="flex items-center">
<input
type="checkbox"
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
defaultChecked
/>
<label className="ml-2 block text-sm text-gray-700">
HTTPS访问
</label>
</div>
</div>
</div>
</div>
<div className="space-y-6">
<div className="bg-purple-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-purple-900 mb-4">
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<input
type="number"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
defaultValue="20"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
()
</label>
<input
type="number"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
defaultValue="24"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
(MB)
</label>
<input
type="number"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
defaultValue="10"
/>
</div>
<div className="flex items-center">
<input
type="checkbox"
className="h-4 w-4 text-purple-600 focus:ring-purple-500 border-gray-300 rounded"
defaultChecked
/>
<label className="ml-2 block text-sm text-gray-700">
Gzip压缩
</label>
</div>
</div>
</div>
<div className="bg-orange-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-orange-900 mb-4">
</h3>
<div className="space-y-4">
<div className="flex items-center">
<input
type="checkbox"
className="h-4 w-4 text-orange-600 focus:ring-orange-500 border-gray-300 rounded"
defaultChecked
/>
<label className="ml-2 block text-sm text-gray-700">
</label>
</div>
<div className="flex items-center">
<input
type="checkbox"
className="h-4 w-4 text-orange-600 focus:ring-orange-500 border-gray-300 rounded"
defaultChecked
/>
<label className="ml-2 block text-sm text-gray-700">
</label>
</div>
<div className="flex items-center">
<input
type="checkbox"
className="h-4 w-4 text-orange-600 focus:ring-orange-500 border-gray-300 rounded"
/>
<label className="ml-2 block text-sm text-gray-700">
</label>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<input
type="email"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-orange-500"
defaultValue="admin@crop-x.com"
/>
</div>
</div>
</div>
</div>
</div>
<div className="mt-6 flex justify-end space-x-4">
<button className="px-6 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition-colors">
</button>
<button className="px-6 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors">
</button>
</div>
</div>
</div>
)
}

View File

@@ -1,243 +0,0 @@
import { Metadata } from 'next'
export const metadata: Metadata = {
title: '业务规则设置 - Crop-X 智慧农业管理系统',
description: '业务规则设置管理页面',
}
export default function BusinessRuleSettingsPage() {
return (
<div className="space-y-6">
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-2xl font-bold text-gray-800 mb-6">
📋
</h2>
<div className="space-y-6">
<div className="bg-green-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-green-900 mb-4">
🚙
</h3>
<div className="space-y-4">
<div className="bg-white rounded-lg p-4">
<div className="flex justify-between items-center mb-2">
<h4 className="font-medium text-gray-800">线</h4>
<span className="text-sm text-green-600"></span>
</div>
<p className="text-sm text-gray-600 mb-3">线</p>
<div className="flex items-center space-x-4">
<input
type="number"
className="w-20 px-2 py-1 border border-gray-300 rounded"
defaultValue="8"
/>
<span className="text-sm text-gray-600">/</span>
<button className="px-3 py-1 bg-green-600 text-white text-sm rounded hover:bg-green-700">
</button>
</div>
</div>
<div className="bg-white rounded-lg p-4">
<div className="flex justify-between items-center mb-2">
<h4 className="font-medium text-gray-800"></h4>
<span className="text-sm text-green-600"></span>
</div>
<p className="text-sm text-gray-600 mb-3"></p>
<div className="flex items-center space-x-4">
<input
type="number"
className="w-20 px-2 py-1 border border-gray-300 rounded"
defaultValue="30"
/>
<span className="text-sm text-gray-600"></span>
<button className="px-3 py-1 bg-green-600 text-white text-sm rounded hover:bg-green-700">
</button>
</div>
</div>
<div className="bg-white rounded-lg p-4">
<div className="flex justify-between items-center mb-2">
<h4 className="font-medium text-gray-800"></h4>
<span className="text-sm text-green-600"></span>
</div>
<p className="text-sm text-gray-600 mb-3"></p>
<div className="flex items-center space-x-4">
<select className="px-3 py-1 border border-gray-300 rounded">
<option></option>
<option></option>
<option></option>
</select>
<button className="px-3 py-1 bg-green-600 text-white text-sm rounded hover:bg-green-700">
</button>
</div>
</div>
</div>
</div>
<div className="bg-blue-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-blue-900 mb-4">
🌾
</h3>
<div className="space-y-4">
<div className="bg-white rounded-lg p-4">
<div className="flex justify-between items-center mb-2">
<h4 className="font-medium text-gray-800"></h4>
<span className="text-sm text-green-600"></span>
</div>
<p className="text-sm text-gray-600 mb-3"></p>
<div className="flex items-center space-x-4">
<input
type="number"
className="w-24 px-2 py-1 border border-gray-300 rounded"
defaultValue="1000"
/>
<span className="text-sm text-gray-600"></span>
<button className="px-3 py-1 bg-green-600 text-white text-sm rounded hover:bg-green-700">
</button>
</div>
</div>
<div className="bg-white rounded-lg p-4">
<div className="flex justify-between items-center mb-2">
<h4 className="font-medium text-gray-800"></h4>
<span className="text-sm text-green-600"></span>
</div>
<p className="text-sm text-gray-600 mb-3"></p>
<div className="flex items-center space-x-4">
<select className="px-3 py-1 border border-gray-300 rounded">
<option>A/B/C三级</option>
<option>//</option>
<option>//</option>
</select>
<button className="px-3 py-1 bg-green-600 text-white text-sm rounded hover:bg-green-700">
</button>
</div>
</div>
</div>
</div>
<div className="bg-purple-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-purple-900 mb-4">
📋
</h3>
<div className="space-y-4">
<div className="bg-white rounded-lg p-4">
<div className="flex justify-between items-center mb-2">
<h4 className="font-medium text-gray-800"></h4>
<span className="text-sm text-green-600"></span>
</div>
<p className="text-sm text-gray-600 mb-3"></p>
<div className="flex items-center space-x-4">
<select className="px-3 py-1 border border-gray-300 rounded">
<option></option>
<option></option>
<option></option>
</select>
<button className="px-3 py-1 bg-green-600 text-white text-sm rounded hover:bg-green-700">
</button>
</div>
</div>
<div className="bg-white rounded-lg p-4">
<div className="flex justify-between items-center mb-2">
<h4 className="font-medium text-gray-800"></h4>
<span className="text-sm text-green-600"></span>
</div>
<p className="text-sm text-gray-600 mb-3"></p>
<div className="flex items-center space-x-4">
<input
type="number"
className="w-20 px-2 py-1 border border-gray-300 rounded"
defaultValue="2"
/>
<span className="text-sm text-gray-600"></span>
<button className="px-3 py-1 bg-green-600 text-white text-sm rounded hover:bg-green-700">
</button>
</div>
</div>
<div className="bg-white rounded-lg p-4">
<div className="flex justify-between items-center mb-2">
<h4 className="font-medium text-gray-800"></h4>
<span className="text-sm text-orange-600"></span>
</div>
<p className="text-sm text-gray-600 mb-3"></p>
<div className="flex items-center space-x-4">
<input
type="number"
className="w-20 px-2 py-1 border border-gray-300 rounded"
defaultValue="85"
/>
<span className="text-sm text-gray-600">% 线</span>
<button className="px-3 py-1 bg-green-600 text-white text-sm rounded hover:bg-green-700">
</button>
</div>
</div>
</div>
</div>
<div className="bg-orange-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-orange-900 mb-4">
📊
</h3>
<div className="space-y-4">
<div className="bg-white rounded-lg p-4">
<div className="flex justify-between items-center mb-2">
<h4 className="font-medium text-gray-800"></h4>
<span className="text-sm text-green-600"></span>
</div>
<p className="text-sm text-gray-600 mb-3"></p>
<div className="flex items-center space-x-4">
<select className="px-3 py-1 border border-gray-300 rounded">
<option></option>
<option></option>
<option></option>
</select>
<button className="px-3 py-1 bg-green-600 text-white text-sm rounded hover:bg-green-700">
</button>
</div>
</div>
<div className="bg-white rounded-lg p-4">
<div className="flex justify-between items-center mb-2">
<h4 className="font-medium text-gray-800"></h4>
<span className="text-sm text-green-600"></span>
</div>
<p className="text-sm text-gray-600 mb-3"></p>
<div className="flex items-center space-x-4">
<input
type="number"
className="w-20 px-2 py-1 border border-gray-300 rounded"
defaultValue="90"
/>
<span className="text-sm text-gray-600"></span>
<button className="px-3 py-1 bg-green-600 text-white text-sm rounded hover:bg-green-700">
</button>
</div>
</div>
</div>
</div>
</div>
<div className="mt-6 flex justify-end space-x-4">
<button className="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors">
</button>
<button className="px-6 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors">
</button>
</div>
</div>
</div>
)
}

View File

@@ -1,8 +0,0 @@
export default function CategoryDictionaryPage() {
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4"></h1>
<p></p>
</div>
)
}

View File

@@ -1,8 +0,0 @@
export default function DataDictionaryPage() {
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4"></h1>
<p></p>
</div>
)
}

View File

@@ -1,299 +0,0 @@
import { Metadata } from 'next'
export const metadata: Metadata = {
title: '接口配置 - Crop-X 智慧农业管理系统',
description: '接口配置管理页面',
}
export default function InterfaceConfigurationPage() {
return (
<div className="space-y-6">
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-2xl font-bold text-gray-800 mb-6">
🔌
</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="space-y-6">
<div className="bg-green-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-green-900 mb-4">
🌤
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
API提供商
</label>
<select className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500">
<option></option>
<option></option>
<option>OpenWeatherMap</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
API密钥
</label>
<input
type="password"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
defaultValue="••••••••••••••••"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<div className="flex items-center space-x-2">
<input
type="number"
className="w-20 px-2 py-1 border border-gray-300 rounded"
defaultValue="1000"
/>
<span className="text-sm text-gray-600">/</span>
</div>
</div>
<div className="flex items-center">
<input
type="checkbox"
className="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded"
defaultChecked
/>
<label className="ml-2 block text-sm text-gray-700">
</label>
</div>
<button className="w-full px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors">
</button>
</div>
</div>
<div className="bg-blue-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-blue-900 mb-4">
📱
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<select className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option></option>
<option></option>
<option></option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Access Key ID
</label>
<input
type="password"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
defaultValue="••••••••••••••••"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Access Key Secret
</label>
<input
type="password"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
defaultValue="••••••••••••••••"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<input
type="text"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
defaultValue="Crop-X农业"
/>
</div>
<button className="w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors">
</button>
</div>
</div>
</div>
<div className="space-y-6">
<div className="bg-purple-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-purple-900 mb-4">
📧
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
SMTP服务器
</label>
<input
type="text"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
defaultValue="smtp.crop-x.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<input
type="number"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
defaultValue="587"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<input
type="email"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
defaultValue="noreply@crop-x.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<input
type="password"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
defaultValue="••••••••••••••••"
/>
</div>
<div className="flex items-center">
<input
type="checkbox"
className="h-4 w-4 text-purple-600 focus:ring-purple-500 border-gray-300 rounded"
defaultChecked
/>
<label className="ml-2 block text-sm text-gray-700">
SSL/TLS加密
</label>
</div>
<button className="w-full px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700 transition-colors">
</button>
</div>
</div>
<div className="bg-orange-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-orange-900 mb-4">
🗺
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<select className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-orange-500">
<option></option>
<option></option>
<option></option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
API Key
</label>
<input
type="password"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-orange-500"
defaultValue="••••••••••••••••"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<div className="flex space-x-2">
<input
type="text"
className="flex-1 px-2 py-1 border border-gray-300 rounded"
placeholder="经度"
defaultValue="116.397428"
/>
<input
type="text"
className="flex-1 px-2 py-1 border border-gray-300 rounded"
placeholder="纬度"
defaultValue="39.90923"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<select className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-orange-500">
<option>10 - </option>
<option>12 - </option>
<option>14 - </option>
<option>16 - </option>
</select>
</div>
<button className="w-full px-4 py-2 bg-orange-600 text-white rounded-md hover:bg-orange-700 transition-colors">
</button>
</div>
</div>
</div>
</div>
<div className="mt-6 bg-gray-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">
📊
</h3>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white rounded-lg p-4 border-l-4 border-green-500">
<h4 className="font-medium text-gray-800 mb-2"></h4>
<div className="flex items-center justify-between">
<span className="text-sm text-green-600"></span>
<span className="text-xs text-gray-500">响应: 125ms</span>
</div>
</div>
<div className="bg-white rounded-lg p-4 border-l-4 border-green-500">
<h4 className="font-medium text-gray-800 mb-2"></h4>
<div className="flex items-center justify-between">
<span className="text-sm text-green-600"></span>
<span className="text-xs text-gray-500">响应: 89ms</span>
</div>
</div>
<div className="bg-white rounded-lg p-4 border-l-4 border-yellow-500">
<h4 className="font-medium text-gray-800 mb-2"></h4>
<div className="flex items-center justify-between">
<span className="text-sm text-yellow-600"></span>
<span className="text-xs text-gray-500">响应: 456ms</span>
</div>
</div>
<div className="bg-white rounded-lg p-4 border-l-4 border-green-500">
<h4 className="font-medium text-gray-800 mb-2"></h4>
<div className="flex items-center justify-between">
<span className="text-sm text-green-600"></span>
<span className="text-xs text-gray-500">响应: 67ms</span>
</div>
</div>
</div>
</div>
<div className="mt-6 flex justify-end space-x-4">
<button className="px-6 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition-colors">
</button>
<button className="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors">
</button>
<button className="px-6 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors">
</button>
</div>
</div>
</div>
)
}

View File

@@ -1,22 +0,0 @@
import { ReactNode } from 'react'
export default function SystemParametersLayout({
children,
}: {
children: ReactNode
}) {
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white shadow-sm border-b">
<div className="container mx-auto px-4 py-4">
<h1 className="text-2xl font-bold text-green-900">
🔧
</h1>
</div>
</header>
<main className="container mx-auto px-4 py-8">
{children}
</main>
</div>
)
}

View File

@@ -1,103 +0,0 @@
import Link from 'next/link'
import { Metadata } from 'next'
export const metadata: Metadata = {
title: '系统参数 - Crop-X 智慧农业管理系统',
description: '系统参数管理页面',
}
export default function SystemParametersPage() {
return (
<div className="space-y-6">
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold text-gray-800 mb-4">
</h2>
<p className="text-gray-600 mb-6">
</p>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<Link
href="/central-config/system-parameters/basic-configuration"
className="block p-4 bg-green-50 rounded-lg hover:bg-green-100 transition-colors"
>
<h3 className="font-semibold text-green-900 mb-2">
</h3>
<p className="text-green-700 text-sm">
</p>
</Link>
<Link
href="/central-config/system-parameters/business-rule-settings"
className="block p-4 bg-blue-50 rounded-lg hover:bg-blue-100 transition-colors"
>
<h3 className="font-semibold text-blue-900 mb-2">
📋
</h3>
<p className="text-blue-700 text-sm">
</p>
</Link>
<Link
href="/central-config/system-parameters/interface-configuration"
className="block p-4 bg-purple-50 rounded-lg hover:bg-purple-100 transition-colors"
>
<h3 className="font-semibold text-purple-900 mb-2">
🔌
</h3>
<p className="text-purple-700 text-sm">
API接口和第三方服务配置
</p>
</Link>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">
📊
</h3>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-gray-600"></span>
<span className="text-green-600 font-semibold">45 </span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600"></span>
<span className="text-blue-600 font-semibold">23 </span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600"></span>
<span className="text-purple-600 font-semibold">12 </span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600"></span>
<span className="text-orange-600 font-semibold">3 </span>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">
🔧
</h3>
<div className="space-y-2">
<button className="w-full px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 transition-colors">
</button>
<button className="w-full px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors">
</button>
<button className="w-full px-4 py-2 bg-purple-600 text-white rounded hover:bg-purple-700 transition-colors">
</button>
</div>
</div>
</div>
</div>
)
}

View File

@@ -1,8 +0,0 @@
export default function SystemSettingsPage() {
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4"></h1>
<p></p>
</div>
)
}

View File

@@ -0,0 +1,14 @@
'use client';
import React from 'react';
export default function CategoryDictionaryPage() {
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4"></h1>
<div className="bg-white rounded-lg shadow p-4">
<p> - : /config/system/category</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,14 @@
'use client';
import React from 'react';
export default function DataDictionaryPage() {
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4"></h1>
<div className="bg-white rounded-lg shadow p-4">
<p> - : /config/system/dictionary</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,26 @@
'use client';
import React from 'react';
import Link from 'next/link';
export default function SystemPage() {
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-3 gap-4">
<Link href="/central-config/system/settings" 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/system/category" 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/system/dictionary" 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,35 @@
import { Card } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { SystemSettings } from '@/types/system-params'
interface CopyrightInfoCardProps {
settings: SystemSettings
onSettingsChange: (updates: Partial<SystemSettings>) => void
}
export function CopyrightInfoCard({ settings, onSettingsChange }: CopyrightInfoCardProps) {
return (
<Card className="p-6">
<h3 className="mb-4"></h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label>ICP备案号</Label>
<Input
value={settings.icp || ''}
onChange={(e) => onSettingsChange({ icp: e.target.value })}
placeholder="京ICP备12345678号"
/>
</div>
<div>
<Label></Label>
<Input
value={settings.copyright || ''}
onChange={(e) => onSettingsChange({ copyright: e.target.value })}
placeholder="© 2024 公司名称 版权所有"
/>
</div>
</div>
</Card>
)
}

View File

@@ -0,0 +1,39 @@
import { Card } from '@/components/ui/card'
import { Switch } from '@/components/ui/switch'
import { Label } from '@/components/ui/label'
import { SystemSettings } from '@/types/system-params'
interface FeatureToggleCardProps {
settings: SystemSettings
onSettingsChange: (updates: Partial<SystemSettings>) => void
}
export function FeatureToggleCard({ settings, onSettingsChange }: FeatureToggleCardProps) {
return (
<Card className="p-6">
<h3 className="mb-4"></h3>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<Label></Label>
<p className="text-sm text-muted-foreground"></p>
</div>
<Switch
checked={settings.enableRegistration}
onCheckedChange={(checked) => onSettingsChange({ enableRegistration: checked })}
/>
</div>
<div className="flex items-center justify-between">
<div>
<Label>访访</Label>
<p className="text-sm text-muted-foreground">访</p>
</div>
<Switch
checked={settings.enableGuestAccess}
onCheckedChange={(checked) => onSettingsChange({ enableGuestAccess: checked })}
/>
</div>
</div>
</Card>
)
}

View File

@@ -0,0 +1,67 @@
import { Card } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import { SystemSettings } from '@/types/system-params'
interface PasswordPolicyCardProps {
settings: SystemSettings
onSettingsChange: (updates: Partial<SystemSettings>) => void
}
export function PasswordPolicyCard({ settings, onSettingsChange }: PasswordPolicyCardProps) {
const updatePasswordPolicy = (updates: Partial<SystemSettings['passwordPolicy']>) => {
onSettingsChange({
passwordPolicy: { ...settings.passwordPolicy, ...updates }
})
}
return (
<Card className="p-6">
<h3 className="mb-4"></h3>
<div className="space-y-4">
<div>
<Label></Label>
<Input
type="number"
value={settings.passwordPolicy.minLength}
onChange={(e) => updatePasswordPolicy({ minLength: parseInt(e.target.value) || 8 })}
min={6}
max={32}
/>
</div>
<div className="space-y-3">
<Label></Label>
<div className="flex items-center justify-between">
<span className="text-sm"></span>
<Switch
checked={settings.passwordPolicy.requireUppercase}
onCheckedChange={(checked) => updatePasswordPolicy({ requireUppercase: checked })}
/>
</div>
<div className="flex items-center justify-between">
<span className="text-sm"></span>
<Switch
checked={settings.passwordPolicy.requireLowercase}
onCheckedChange={(checked) => updatePasswordPolicy({ requireLowercase: checked })}
/>
</div>
<div className="flex items-center justify-between">
<span className="text-sm"></span>
<Switch
checked={settings.passwordPolicy.requireNumbers}
onCheckedChange={(checked) => updatePasswordPolicy({ requireNumbers: checked })}
/>
</div>
<div className="flex items-center justify-between">
<span className="text-sm"></span>
<Switch
checked={settings.passwordPolicy.requireSpecialChars}
onCheckedChange={(checked) => updatePasswordPolicy({ requireSpecialChars: checked })}
/>
</div>
</div>
</div>
</Card>
)
}

View File

@@ -0,0 +1,60 @@
import { Card } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { SystemSettings } from '@/types/system-params'
interface PlatformInfoCardProps {
settings: SystemSettings
onSettingsChange: (updates: Partial<SystemSettings>) => void
}
export function PlatformInfoCard({ settings, onSettingsChange }: PlatformInfoCardProps) {
return (
<Card className="p-6">
<h3 className="mb-4"></h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label> *</Label>
<Input
value={settings.platformName}
onChange={(e) => onSettingsChange({ platformName: e.target.value })}
placeholder="请输入平台名称"
/>
</div>
<div>
<Label></Label>
<Input
value={settings.companyName || ''}
onChange={(e) => onSettingsChange({ companyName: e.target.value })}
placeholder="请输入公司名称"
/>
</div>
<div>
<Label></Label>
<Input
type="email"
value={settings.contactEmail || ''}
onChange={(e) => onSettingsChange({ contactEmail: e.target.value })}
placeholder="support@example.com"
/>
</div>
<div>
<Label></Label>
<Input
value={settings.contactPhone || ''}
onChange={(e) => onSettingsChange({ contactPhone: e.target.value })}
placeholder="400-888-8888"
/>
</div>
<div className="md:col-span-2">
<Label></Label>
<Input
value={settings.address || ''}
onChange={(e) => onSettingsChange({ address: e.target.value })}
placeholder="请输入公司地址"
/>
</div>
</div>
</Card>
)
}

View File

@@ -0,0 +1,69 @@
import { Card } from '@/components/ui/card'
import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { SystemSettings } from '@/types/system-params'
interface RegionalSettingsCardProps {
settings: SystemSettings
onSettingsChange: (updates: Partial<SystemSettings>) => void
}
export function RegionalSettingsCard({ settings, onSettingsChange }: RegionalSettingsCardProps) {
return (
<Card className="p-6">
<h3 className="mb-4"></h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label></Label>
<Select
value={settings.language}
onValueChange={(value) => onSettingsChange({ language: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="zh-CN"></SelectItem>
<SelectItem value="zh-TW"></SelectItem>
<SelectItem value="en-US">English</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label></Label>
<Select
value={settings.timezone}
onValueChange={(value) => onSettingsChange({ timezone: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="Asia/Shanghai"> (UTC+8)</SelectItem>
<SelectItem value="Asia/Tokyo"> (UTC+9)</SelectItem>
<SelectItem value="America/New_York"> (UTC-5)</SelectItem>
<SelectItem value="Europe/London"> (UTC+0)</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label></Label>
<Select
value={settings.dateFormat}
onValueChange={(value) => onSettingsChange({ dateFormat: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="YYYY-MM-DD">2024-10-14</SelectItem>
<SelectItem value="DD/MM/YYYY">14/10/2024</SelectItem>
<SelectItem value="MM/DD/YYYY">10/14/2024</SelectItem>
<SelectItem value="YYYY年MM月DD日">20241014</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</Card>
)
}

View File

@@ -0,0 +1,45 @@
import { Card } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { SystemSettings } from '@/types/system-params'
interface SessionManagementCardProps {
settings: SystemSettings
onSettingsChange: (updates: Partial<SystemSettings>) => void
}
export function SessionManagementCard({ settings, onSettingsChange }: SessionManagementCardProps) {
return (
<Card className="p-6">
<h3 className="mb-4"></h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label></Label>
<Input
type="number"
value={settings.sessionTimeout}
onChange={(e) => onSettingsChange({ sessionTimeout: parseInt(e.target.value) || 30 })}
min={5}
max={1440}
/>
<p className="text-sm text-muted-foreground mt-1">
退
</p>
</div>
<div>
<Label></Label>
<Input
type="number"
value={settings.maxLoginAttempts}
onChange={(e) => onSettingsChange({ maxLoginAttempts: parseInt(e.target.value) || 5 })}
min={3}
max={10}
/>
<p className="text-sm text-muted-foreground mt-1">
</p>
</div>
</div>
</Card>
)
}

View File

@@ -0,0 +1,20 @@
import { Card } from '@/components/ui/card'
import { Settings } from 'lucide-react'
export function SettingsInfoCard() {
return (
<Card className="p-4 bg-blue-50 border-blue-200">
<h4 className="text-blue-900 mb-2">
<Settings className="w-4 h-4 inline mr-2" />
</h4>
<ul className="space-y-1 text-sm text-blue-800">
<li> Logo将显示在系统导航栏和登录页面</li>
<li> </li>
<li> </li>
<li> </li>
<li> "保存设置"</li>
</ul>
</Card>
)
}

View File

@@ -0,0 +1,22 @@
import { Card } from '@/components/ui/card'
import { Textarea } from '@/components/ui/textarea'
import { SystemSettings } from '@/types/system-params'
interface SystemAnnouncementCardProps {
settings: SystemSettings
onSettingsChange: (updates: Partial<SystemSettings>) => void
}
export function SystemAnnouncementCard({ settings, onSettingsChange }: SystemAnnouncementCardProps) {
return (
<Card className="p-6">
<h3 className="mb-4"></h3>
<Textarea
value={settings.systemAnnouncement || ''}
onChange={(e) => onSettingsChange({ systemAnnouncement: e.target.value })}
placeholder="输入系统公告内容,将显示在登录页面"
rows={4}
/>
</Card>
)
}

View File

@@ -0,0 +1,8 @@
export { PlatformInfoCard } from './PlatformInfoCard'
export { SystemAnnouncementCard } from './SystemAnnouncementCard'
export { CopyrightInfoCard } from './CopyrightInfoCard'
export { FeatureToggleCard } from './FeatureToggleCard'
export { SessionManagementCard } from './SessionManagementCard'
export { PasswordPolicyCard } from './PasswordPolicyCard'
export { RegionalSettingsCard } from './RegionalSettingsCard'
export { SettingsInfoCard } from './SettingsInfoCard'

View File

@@ -0,0 +1,168 @@
'use client'
import { useState, useEffect } from 'react'
import { Button } from '@/components/ui/button'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { SystemSettings } from '@/types/system-params'
import { Save, RefreshCw, Info, Shield, Globe } from 'lucide-react'
import { toast } from 'sonner'
// Import modular components
import {
PlatformInfoCard,
SystemAnnouncementCard,
CopyrightInfoCard,
FeatureToggleCard,
SessionManagementCard,
PasswordPolicyCard,
RegionalSettingsCard,
SettingsInfoCard
} from './components'
export default function SystemSettingsPage() {
const [settings, setSettings] = useState<SystemSettings>({
platformName: '智慧农业生产管理系统',
platformLogo: '',
systemAnnouncement: '欢迎使用智慧农业生产管理系统!',
contactEmail: 'support@smart-agriculture.com',
contactPhone: '400-888-8888',
address: '北京市海淀区中关村大街1号',
companyName: '智慧农业科技有限公司',
icp: '京ICP备12345678号',
copyright: '© 2024 智慧农业科技有限公司 版权所有',
enableRegistration: true,
enableGuestAccess: false,
sessionTimeout: 30,
maxLoginAttempts: 5,
passwordPolicy: {
minLength: 8,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSpecialChars: false,
},
dateFormat: 'YYYY-MM-DD',
timezone: 'Asia/Shanghai',
language: 'zh-CN',
})
const [hasChanges, setHasChanges] = useState(false)
useEffect(() => {
loadSettings()
}, [])
const loadSettings = () => {
const data = localStorage.getItem('smart_agriculture_system_settings')
if (data) {
setSettings(JSON.parse(data))
} else {
saveSettings(settings)
}
}
const saveSettings = (newSettings: SystemSettings) => {
localStorage.setItem('smart_agriculture_system_settings', JSON.stringify(newSettings))
setSettings(newSettings)
setHasChanges(false)
toast.success('系统设置已保存')
}
const handleSave = () => {
saveSettings(settings)
}
const handleReset = () => {
loadSettings()
setHasChanges(false)
toast.info('已恢复到上次保存的设置')
}
const updateSettings = (updates: Partial<SystemSettings>) => {
setSettings({ ...settings, ...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">
{hasChanges && (
<Button variant="outline" onClick={handleReset}>
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
)}
<Button onClick={handleSave} disabled={!hasChanges}>
<Save className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
<Tabs defaultValue="basic" className="space-y-4">
<TabsList>
<TabsTrigger value="basic">
<Info className="w-4 h-4 mr-2" />
</TabsTrigger>
<TabsTrigger value="security">
<Shield className="w-4 h-4 mr-2" />
</TabsTrigger>
<TabsTrigger value="regional">
<Globe className="w-4 h-4 mr-2" />
</TabsTrigger>
</TabsList>
{/* 基本设置 */}
<TabsContent value="basic" className="space-y-4">
<PlatformInfoCard
settings={settings}
onSettingsChange={updateSettings}
/>
<SystemAnnouncementCard
settings={settings}
onSettingsChange={updateSettings}
/>
<CopyrightInfoCard
settings={settings}
onSettingsChange={updateSettings}
/>
<FeatureToggleCard
settings={settings}
onSettingsChange={updateSettings}
/>
</TabsContent>
{/* 安全设置 */}
<TabsContent value="security" className="space-y-4">
<SessionManagementCard
settings={settings}
onSettingsChange={updateSettings}
/>
<PasswordPolicyCard
settings={settings}
onSettingsChange={updateSettings}
/>
</TabsContent>
{/* 区域设置 */}
<TabsContent value="regional" className="space-y-4">
<RegionalSettingsCard
settings={settings}
onSettingsChange={updateSettings}
/>
</TabsContent>
</Tabs>
{/* 设置预览 */}
<SettingsInfoCard />
</div>
)
}

View File

@@ -1,8 +0,0 @@
export default function AuditHistoryPage() {
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4"></h1>
<p></p>
</div>
)
}

View File

@@ -1,65 +0,0 @@
'use client'
import { Label } from '@/components/ui/label'
import { Image as ImageIcon } from 'lucide-react'
import { Enterprise } from '@/types/user-management'
interface EnterpriseBankInfoProps {
enterprise: Enterprise
}
export function EnterpriseBankInfo({ enterprise }: EnterpriseBankInfoProps) {
return (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-6">
<div>
<Label></Label>
<div className="field-value mt-1 p-2 bg-gray-50 rounded">
{enterprise.bankAccount ? (
<code className="text-sm font-mono">
{enterprise.bankAccount}
</code>
) : '-'}
</div>
</div>
<div>
<Label></Label>
<div className="field-value mt-1 p-2 bg-gray-50 rounded">
{enterprise.bankName || '-'}
</div>
</div>
<div className="col-span-2">
<Label></Label>
<div className="field-value mt-1 p-2 bg-gray-50 rounded">
{enterprise.bankFullName || '-'}
</div>
</div>
<div className="col-span-2">
<Label></Label>
<div className="field-value mt-1 p-2 bg-gray-50 rounded">
{enterprise.bankAddress || '-'}
</div>
</div>
<div className="col-span-2">
<Label></Label>
<div className="mt-2">
{enterprise.bankLicense ? (
<div className="border rounded-lg p-2 inline-block">
<img
src={enterprise.bankLicense}
alt="开户许可证"
className="w-64 h-auto"
/>
</div>
) : (
<div className="flex items-center gap-2 text-muted-foreground p-4 border-2 border-dashed rounded-lg">
<ImageIcon className="w-6 h-6" />
<span></span>
</div>
)}
</div>
</div>
</div>
</div>
)
}

View File

@@ -1,43 +0,0 @@
'use client'
import { Label } from '@/components/ui/label'
import { Enterprise } from '@/types/user-management'
interface EnterpriseBasicInfoProps {
enterprise: Enterprise
}
export function EnterpriseBasicInfo({ enterprise }: EnterpriseBasicInfoProps) {
return (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-6">
<div>
<Label></Label>
<div className="field-value mt-1 p-2 bg-gray-50 rounded">{enterprise.name}</div>
</div>
<div>
<Label></Label>
<div className="field-value mt-1 p-2 bg-gray-50 rounded">{enterprise.type}</div>
</div>
<div>
<Label></Label>
<div className="field-value mt-1 p-2 bg-gray-50 rounded">
{enterprise.province} {enterprise.city} {enterprise.district}
</div>
</div>
<div>
<Label></Label>
<div className="field-value mt-1 p-2 bg-gray-50 rounded">{enterprise.address}</div>
</div>
<div>
<Label></Label>
<div className="field-value mt-1 p-2 bg-gray-50 rounded">{enterprise.registrant}</div>
</div>
<div>
<Label></Label>
<div className="field-value mt-1 p-2 bg-gray-50 rounded">{enterprise.contactPhone}</div>
</div>
</div>
</div>
)
}

View File

@@ -1,192 +0,0 @@
'use client'
import { useState } from 'react'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Card } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Building, FileText, CreditCard, User, CheckCircle, XCircle } from 'lucide-react'
import { Enterprise, AuditStatus } from '@/types/user-management'
import { EnterpriseBasicInfo } from './EnterpriseBasicInfo'
import { EnterpriseOtherInfo } from './EnterpriseOtherInfo'
import { EnterpriseBankInfo } from './EnterpriseBankInfo'
import { EnterpriseLegalInfo } from './EnterpriseLegalInfo'
interface EnterpriseDetailDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
enterprise: Enterprise | null
onApprove: (auditReason: string) => void
onReject: (auditReason: string) => void
}
export function EnterpriseDetailDialog({
open,
onOpenChange,
enterprise,
onApprove,
onReject
}: EnterpriseDetailDialogProps) {
const [auditReason, setAuditReason] = useState('')
const getAuditStatusBadge = (status: AuditStatus) => {
switch (status) {
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>{status}</Badge>
}
}
const handleApprove = () => {
onApprove(auditReason)
setAuditReason('')
}
const handleReject = () => {
onReject(auditReason)
setAuditReason('')
}
const handleClose = () => {
onOpenChange(false)
setAuditReason('')
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-5xl max-h-[90vh]">
<DialogHeader>
<div className="flex items-center justify-between pr-8">
<DialogTitle></DialogTitle>
{enterprise && getAuditStatusBadge(enterprise.auditStatus)}
</div>
<DialogDescription className="sr-only">
</DialogDescription>
</DialogHeader>
{enterprise && (
<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">
<Building 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>
<TabsContent value="basic">
<EnterpriseBasicInfo enterprise={enterprise} />
</TabsContent>
<TabsContent value="other">
<EnterpriseOtherInfo enterprise={enterprise} />
</TabsContent>
<TabsContent value="bank">
<EnterpriseBankInfo enterprise={enterprise} />
</TabsContent>
<TabsContent value="legal">
<EnterpriseLegalInfo enterprise={enterprise} />
</TabsContent>
</Tabs>
{/* 审核信息 */}
<div className="mt-6 pt-6 border-t">
<h4 className="mb-4 font-bold"></h4>
<Card className="p-6 bg-gray-50 border">
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
<div>
<Label className="text-xs"></Label>
<div className="mt-1.5 text-base">
{new Date(enterprise.createdAt).toLocaleString('zh-CN')}
</div>
</div>
{enterprise.auditTime && (
<div>
<Label className="text-xs"></Label>
<div className="mt-1.5 text-base">
{new Date(enterprise.auditTime).toLocaleString('zh-CN')}
</div>
</div>
)}
{enterprise.auditor && (
<div>
<Label className="text-xs"></Label>
<div className="mt-1.5 text-base">
{enterprise.auditor}
</div>
</div>
)}
{enterprise.auditReason && (
<div className="col-span-2 pt-4 mt-2 border-t">
<Label className="text-xs"></Label>
<div className="mt-1.5 text-base">
{enterprise.auditReason}
</div>
</div>
)}
</div>
</Card>
{/* 审核操作区 - 仅待审核状态显示 */}
{enterprise.auditStatus === 'pending' && (
<div className="mt-6">
<Label></Label>
<Textarea
value={auditReason}
onChange={(e) => setAuditReason(e.target.value)}
rows={3}
placeholder="请填写审核意见(驳回时必填)..."
className="mt-2"
/>
</div>
)}
</div>
</ScrollArea>
)}
<DialogFooter className="border-t pt-4">
<Button variant="outline" onClick={handleClose}>
</Button>
{enterprise?.auditStatus === 'pending' && (
<>
<Button
variant="destructive"
onClick={handleReject}
>
<XCircle className="w-4 h-4 mr-2" />
</Button>
<Button onClick={handleApprove}>
<CheckCircle className="w-4 h-4 mr-2" />
</Button>
</>
)}
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -1,62 +0,0 @@
'use client'
import { Label } from '@/components/ui/label'
import { Image as ImageIcon } from 'lucide-react'
import { Enterprise } from '@/types/user-management'
interface EnterpriseLegalInfoProps {
enterprise: Enterprise
}
export function EnterpriseLegalInfo({ enterprise }: EnterpriseLegalInfoProps) {
return (
<div className="space-y-4">
<div className="grid grid-cols-1 gap-6">
<div>
<Label></Label>
<div className="field-value mt-1 p-2 bg-gray-50 rounded">
{enterprise.legalPerson || '-'}
</div>
</div>
<div>
<Label></Label>
<div className="mt-2">
{enterprise.idCardFront ? (
<div className="border rounded-lg p-2 inline-block">
<img
src={enterprise.idCardFront}
alt="身份证正面"
className="w-80 h-auto"
/>
</div>
) : (
<div className="flex items-center gap-2 text-muted-foreground p-4 border-2 border-dashed rounded-lg">
<ImageIcon className="w-6 h-6" />
<span></span>
</div>
)}
</div>
</div>
<div>
<Label></Label>
<div className="mt-2">
{enterprise.idCardBack ? (
<div className="border rounded-lg p-2 inline-block">
<img
src={enterprise.idCardBack}
alt="身份证反面"
className="w-80 h-auto"
/>
</div>
) : (
<div className="flex items-center gap-2 text-muted-foreground p-4 border-2 border-dashed rounded-lg">
<ImageIcon className="w-6 h-6" />
<span></span>
</div>
)}
</div>
</div>
</div>
</div>
)
}

View File

@@ -1,75 +0,0 @@
'use client'
import { Label } from '@/components/ui/label'
import { Image as ImageIcon } from 'lucide-react'
import { Enterprise } from '@/types/user-management'
interface EnterpriseOtherInfoProps {
enterprise: Enterprise
}
export function EnterpriseOtherInfo({ enterprise }: EnterpriseOtherInfoProps) {
return (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-6">
<div>
<Label></Label>
<div className="field-value mt-1 p-2 bg-gray-50 rounded">
{enterprise.companySize || '-'}
</div>
</div>
<div>
<Label></Label>
<div className="field-value mt-1 p-2 bg-gray-50 rounded">
{enterprise.registeredCapital || '-'}
</div>
</div>
<div>
<Label></Label>
<div className="field-value mt-1 p-2 bg-gray-50 rounded">
{enterprise.establishmentDate || '-'}
</div>
</div>
<div>
<Label></Label>
<div className="field-value mt-1 p-2 bg-gray-50 rounded">
{enterprise.invoiceType || '-'}
</div>
</div>
<div className="col-span-2">
<Label></Label>
<div className="field-value mt-1 p-2 bg-gray-50 rounded">
<code className="text-sm font-mono">
{enterprise.socialCreditCode}
</code>
</div>
</div>
<div className="col-span-2">
<Label></Label>
<div className="field-value mt-1 p-2 bg-gray-50 rounded">
{enterprise.businessScope || '-'}
</div>
</div>
<div className="col-span-2">
<Label></Label>
<div className="mt-2">
{enterprise.businessLicense ? (
<div className="border rounded-lg p-2 inline-block">
<img
src={enterprise.businessLicense}
alt="营业执照"
className="w-64 h-auto"
/>
</div>
) : (
<div className="flex items-center gap-2 text-muted-foreground p-4 border-2 border-dashed rounded-lg">
<ImageIcon className="w-6 h-6" />
<span></span>
</div>
)}
</div>
</div>
</div>
</div>
)
}

View File

@@ -1,8 +0,0 @@
export { AuditStatsCard } from './AuditStatsCard'
export { SearchAndFilter } from './SearchAndFilter'
export { EnterpriseTable } from './EnterpriseTable'
export { EnterpriseDetailDialog } from './EnterpriseDetailDialog'
export { EnterpriseBasicInfo } from './EnterpriseBasicInfo'
export { EnterpriseOtherInfo } from './EnterpriseOtherInfo'
export { EnterpriseBankInfo } from './EnterpriseBankInfo'
export { EnterpriseLegalInfo } from './EnterpriseLegalInfo'

Some files were not shown because too many files have changed in this diff Show More