生产管理系统前端 开发中心配置系统 所有页面
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
115
crop-x/src/app/(app)/central-config/message/log/mock/mockData.ts
Normal file
115
crop-x/src/app/(app)/central-config/message/log/mock/mockData.ts
Normal 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,
|
||||||
|
}
|
||||||
|
];
|
||||||
139
crop-x/src/app/(app)/central-config/message/log/page.tsx
Normal file
139
crop-x/src/app/(app)/central-config/message/log/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
crop-x/src/app/(app)/central-config/message/page.tsx
Normal file
26
crop-x/src/app/(app)/central-config/message/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export { MessageStatsCards } from './MessageStatsCards';
|
||||||
|
export { MessageSendTable } from './MessageSendTable';
|
||||||
|
export { SendMessageDialog } from './SendMessageDialog';
|
||||||
|
export { MessagePreviewDialog } from './MessagePreviewDialog';
|
||||||
|
export { MessageInstructions } from './MessageInstructions';
|
||||||
493
crop-x/src/app/(app)/central-config/message/send/page.tsx
Normal file
493
crop-x/src/app/(app)/central-config/message/send/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
crop-x/src/app/(app)/central-config/message/send/types.ts
Normal file
12
crop-x/src/app/(app)/central-config/message/send/types.ts
Normal 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>;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
269
crop-x/src/app/(app)/central-config/message/template/page.tsx
Normal file
269
crop-x/src/app/(app)/central-config/message/template/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export { LoginLogStats } from './LoginLogStats'
|
||||||
|
export { LoginLogFilters } from './LoginLogFilters'
|
||||||
|
export { LoginLogTable } from './LoginLogTable'
|
||||||
|
export { LoginLogInfo } from './LoginLogInfo'
|
||||||
202
crop-x/src/app/(app)/central-config/monitor/login-log/page.tsx
Normal file
202
crop-x/src/app/(app)/central-config/monitor/login-log/page.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
139
crop-x/src/app/(app)/central-config/monitor/network-log/page.tsx
Normal file
139
crop-x/src/app/(app)/central-config/monitor/network-log/page.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
30
crop-x/src/app/(app)/central-config/monitor/page.tsx
Normal file
30
crop-x/src/app/(app)/central-config/monitor/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
274
crop-x/src/app/(app)/central-config/monitor/performance/page.tsx
Normal file
274
crop-x/src/app/(app)/central-config/monitor/performance/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
export default function SystemMonitorLayout({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="system-monitor-layout">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
14
crop-x/src/app/(app)/central-config/system/category/page.tsx
Normal file
14
crop-x/src/app/(app)/central-config/system/category/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
crop-x/src/app/(app)/central-config/system/page.tsx
Normal file
26
crop-x/src/app/(app)/central-config/system/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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日">2024年10月14日</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
168
crop-x/src/app/(app)/central-config/system/settings/page.tsx
Normal file
168
crop-x/src/app/(app)/central-config/system/settings/page.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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
Reference in New Issue
Block a user