Files
IronSteelNon-ferrousMetallu…/src/app/pages/common/MonitoringCenter.tsx
2026-04-08 23:50:01 +08:00

634 lines
27 KiB
TypeScript

import { Activity, AlertTriangle, CheckCircle, TrendingUp, TrendingDown, RefreshCw, Bell, Settings, Zap, Thermometer, Gauge, Clock } from "lucide-react";
import { motion, AnimatePresence } from "motion/react";
import { LineChart, Line, BarChart, Bar, AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts";
import { useState, useEffect, useRef } from "react";
import { toast } from "sonner";
const steelProcessStages = [
{ name: "炼铁", status: "normal", load: 85, temp: 1250 },
{ name: "炼钢", status: "normal", load: 92, temp: 1650 },
{ name: "连铸", status: "normal", load: 88, temp: 1520 },
{ name: "热轧", status: "normal", load: 90, temp: 1180 },
{ name: "冷轧", status: "normal", load: 78, temp: 850 },
];
const aluminaProcessStages = [
{ name: "铝土矿破碎", status: "normal", load: 82, temp: 25 },
{ name: "拜耳法溶出", status: "normal", load: 88, temp: 260 },
{ name: "沉降分离", status: "normal", load: 85, temp: 100 },
{ name: "晶种分解", status: "warning", load: 78, temp: 80 },
{ name: "焙烧", status: "normal", load: 90, temp: 1100 },
];
const steelTemperatureTrend = [
{ time: "08:00", t1: 1245, t2: 1648, t3: 1518 },
{ time: "10:00", t1: 1248, t2: 1650, t3: 1520 },
{ time: "12:00", t1: 1250, t2: 1652, t3: 1522 },
{ time: "14:00", t1: 1252, t2: 1649, t3: 1519 },
{ time: "16:00", t1: 1250, t2: 1650, t3: 1520 },
];
const aluminaTemperatureTrend = [
{ time: "08:00", t1: 24, t2: 258, t3: 98 },
{ time: "10:00", t1: 25, t2: 260, t3: 100 },
{ time: "12:00", t1: 26, t2: 262, t3: 102 },
{ time: "14:00", t1: 25, t2: 259, t3: 99 },
{ time: "16:00", t1: 25, t2: 260, t3: 100 },
];
const steelProductionData = [
{ hour: "00", value: 820 },
{ hour: "04", value: 780 },
{ hour: "08", value: 950 },
{ hour: "12", value: 1020 },
{ hour: "16", value: 980 },
{ hour: "20", value: 890 },
];
const aluminaProductionData = [
{ hour: "00", value: 420 },
{ hour: "04", value: 380 },
{ hour: "08", value: 480 },
{ hour: "12", value: 520 },
{ hour: "16", value: 490 },
{ hour: "20", value: 450 },
];
const steelAlerts = [
{ id: 1, type: "warning", message: "高炉1#冷却水温差偏高", time: "10:35", handled: false },
{ id: 2, type: "info", message: "连铸2#定期维护提醒", time: "10:20", handled: false },
{ id: 3, type: "success", message: "炼钢3#工艺优化已完成", time: "10:15", handled: true },
{ id: 4, type: "warning", message: "热轧设备温度波动", time: "10:05", handled: true },
];
const aluminaAlerts = [
{ id: 1, type: "warning", message: "分解槽搅拌强度偏低", time: "10:35", handled: false },
{ id: 2, type: "info", message: "蒸发器定期维护提醒", time: "10:20", handled: false },
{ id: 3, type: "success", message: "铝酸钠溶液浓度优化完成", time: "10:15", handled: true },
{ id: 4, type: "warning", message: "晶种分解效率波动", time: "10:05", handled: true },
];
const steelEquipmentStatus = [
{ name: "高炉 #1", status: "running", efficiency: 94, uptime: "99.2%" },
{ name: "高炉 #2", status: "running", efficiency: 92, uptime: "98.8%" },
{ name: "转炉 #1", status: "running", efficiency: 96, uptime: "99.5%" },
{ name: "转炉 #2", status: "maintenance", efficiency: 0, uptime: "—" },
{ name: "连铸 #1", status: "running", efficiency: 91, uptime: "98.3%" },
{ name: "连铸 #2", status: "running", efficiency: 93, uptime: "99.1%" },
];
const aluminaEquipmentStatus = [
{ name: "磨机 #1", status: "running", efficiency: 94, uptime: "99.2%" },
{ name: "磨机 #2", status: "running", efficiency: 92, uptime: "98.8%" },
{ name: "溶出器 #1", status: "running", efficiency: 96, uptime: "99.5%" },
{ name: "溶出器 #2", status: "running", efficiency: 91, uptime: "98.3%" },
{ name: "沉降槽 #1", status: "running", efficiency: 93, uptime: "99.1%" },
{ name: "分解槽 #1", status: "maintenance", efficiency: 0, uptime: "—" },
];
export function MonitoringCenter() {
const [currentTime, setCurrentTime] = useState(new Date());
const [alertsList, setAlertsList] = useState(steelAlerts);
const [processData, setProcessData] = useState(steelProcessStages);
const [tempTrendData, setTempTrendData] = useState(steelTemperatureTrend);
const [industry, setIndustry] = useState<"steel" | "alumina">("steel");
const currentProductionData = industry === "steel" ? steelProductionData : aluminaProductionData;
const currentEquipmentStatus = industry === "steel" ? steelEquipmentStatus : aluminaEquipmentStatus;
const currentProcessStages = industry === "steel" ? steelProcessStages : aluminaProcessStages;
const currentTempTrend = industry === "steel" ? steelTemperatureTrend : aluminaTemperatureTrend;
const currentAlerts = industry === "steel" ? steelAlerts : aluminaAlerts;
// Reset data when industry changes
useEffect(() => {
setProcessData(currentProcessStages);
setTempTrendData(currentTempTrend);
setAlertsList(currentAlerts);
}, [industry, currentProcessStages, currentTempTrend, currentAlerts]);
// Update current time
useEffect(() => {
const interval = setInterval(() => {
setCurrentTime(new Date());
}, 1000);
return () => clearInterval(interval);
}, []);
// Simulate real-time data updates
useEffect(() => {
const interval = setInterval(() => {
setProcessData(prev => prev.map(stage => ({
...stage,
load: Math.max(70, Math.min(100, stage.load + (Math.random() - 0.5) * 5)),
temp: stage.temp + (Math.random() - 0.5) * 10
})));
setTempTrendData(prev => {
const newData = [...prev];
newData.shift();
const lastPoint = newData[newData.length - 1];
newData.push({
time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
t1: lastPoint.t1 + (Math.random() - 0.5) * 5,
t2: lastPoint.t2 + (Math.random() - 0.5) * 5,
t3: lastPoint.t3 + (Math.random() - 0.5) * 5,
});
return newData;
});
}, 3000);
return () => clearInterval(interval);
}, []);
const handleAlertAction = (id: number) => {
setAlertsList(prev => prev.map(alert =>
alert.id === id ? { ...alert, handled: !alert.handled } : alert
));
toast.success("报警状态已更新", { duration: 2000 });
};
const handleRefresh = () => {
toast.promise(
new Promise((resolve) => setTimeout(resolve, 1000)),
{
loading: '刷新监控数据...',
success: '数据已更新!',
error: '刷新失败',
}
);
};
const handleNotification = () => {
const unhandled = alertsList.filter(a => !a.handled).length;
if (unhandled > 0) {
toast.warning(`您有 ${unhandled} 条未处理报警`, { duration: 3000 });
} else {
toast.success("暂无未处理报警", { duration: 2000 });
}
};
return (
<div className="h-full p-6 space-y-6 overflow-y-auto">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-white mb-1"></h1>
<p className="text-sm text-slate-400"></p>
</div>
<div className="flex gap-3">
<button
onClick={() => setIndustry("steel")}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${
industry === "steel"
? "bg-orange-500/20 text-orange-400 border border-orange-500/50"
: "bg-slate-800/50 text-slate-400 border border-slate-700/50 hover:bg-slate-800"
}`}
>
</button>
<button
onClick={() => setIndustry("alumina")}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${
industry === "alumina"
? "bg-cyan-500/20 text-cyan-400 border border-cyan-500/50"
: "bg-slate-800/50 text-slate-400 border border-slate-700/50 hover:bg-slate-800"
}`}
>
</button>
</div>
<div className="flex items-center gap-3">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={handleNotification}
className="p-2 rounded-lg bg-slate-800/50 hover:bg-slate-800 transition-colors relative"
>
<Bell className="w-5 h-5 text-slate-300" />
{alertsList.filter(a => !a.handled).length > 0 && (
<span className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 rounded-full text-xs flex items-center justify-center text-white">
{alertsList.filter(a => !a.handled).length}
</span>
)}
</motion.button>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={handleRefresh}
className="p-2 rounded-lg bg-slate-800/50 hover:bg-slate-800 transition-colors"
>
<RefreshCw className="w-5 h-5 text-slate-300" />
</motion.button>
<motion.div
animate={{
boxShadow: [
"0 0 20px rgba(16, 185, 129, 0.3)",
"0 0 40px rgba(16, 185, 129, 0.5)",
"0 0 20px rgba(16, 185, 129, 0.3)"
]
}}
transition={{ duration: 2, repeat: Infinity }}
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-green-500/10 border border-green-500/30"
>
<Activity className="w-4 h-4 text-green-400" />
<span className="text-sm text-green-400"></span>
</motion.div>
<div className="text-right">
<div className="text-sm text-slate-400"></div>
<div className="text-lg font-semibold text-white font-mono">
{currentTime.toLocaleTimeString('zh-CN')}
</div>
</div>
</div>
</div>
{/* Process Flow Monitor */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="rounded-xl bg-slate-900/50 border border-slate-800/50 backdrop-blur-sm p-6"
>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 8, repeat: Infinity, ease: "linear" }}
>
<Zap className="w-5 h-5 text-blue-400" />
</motion.div>
</h2>
<div className="flex items-center gap-4 text-xs">
<motion.div
className="flex items-center gap-2 text-slate-400"
animate={{ opacity: [0.5, 1, 0.5] }}
transition={{ duration: 2, repeat: Infinity }}
>
<Clock className="w-4 h-4" />
<span></span>
</motion.div>
</div>
</div>
<div className="relative">
{/* Data Flow Background Animation */}
<div className="absolute inset-0 overflow-hidden pointer-events-none rounded-lg">
{[...Array(8)].map((_, i) => (
<motion.div
key={i}
className="absolute h-0.5 bg-gradient-to-r from-transparent via-blue-500/30 to-transparent"
initial={{ left: '-20%', top: `${15 + i * 12}%`, width: '10%' }}
animate={{ left: '100%' }}
transition={{
duration: 3 + i * 0.5,
repeat: Infinity,
delay: i * 0.4,
ease: "linear"
}}
/>
))}
</div>
{/* Process Flow */}
<div className="flex items-center justify-between gap-4">
{processData.map((stage, index) => (
<div key={stage.name} className="flex items-center flex-1">
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: index * 0.1 }}
whileHover={{ scale: 1.05, boxShadow: "0 0 30px rgba(59, 130, 246, 0.3)" }}
className="flex-1 p-4 rounded-lg bg-slate-800/50 border border-slate-700/50 cursor-pointer hover:border-blue-500/50 transition-all relative overflow-hidden"
onClick={() => toast.info(`${stage.name}阶段详情`, { duration: 2000 })}
>
{/* Animated Background Gradient */}
<motion.div
className="absolute inset-0 opacity-20"
animate={{
background: [
`radial-gradient(circle at 20% 50%, rgba(59, 130, 246, 0.3) 0%, transparent 50%)`,
`radial-gradient(circle at 80% 50%, rgba(59, 130, 246, 0.3) 0%, transparent 50%)`,
`radial-gradient(circle at 20% 50%, rgba(59, 130, 246, 0.3) 0%, transparent 50%)`,
]
}}
transition={{ duration: 4, repeat: Infinity }}
/>
<div className="flex items-center justify-between mb-2 relative">
<span className="font-semibold text-white">{stage.name}</span>
<motion.div
animate={{
scale: [1, 1.2, 1],
boxShadow: stage.status === "normal"
? ["0 0 5px rgba(34, 197, 94, 0.5)", "0 0 15px rgba(34, 197, 94, 0.8)", "0 0 5px rgba(34, 197, 94, 0.5)"]
: ["0 0 5px rgba(234, 179, 8, 0.5)", "0 0 15px rgba(234, 179, 8, 0.8)", "0 0 5px rgba(234, 179, 8, 0.5)"]
}}
transition={{ duration: 1.5, repeat: Infinity }}
className={`w-3 h-3 rounded-full ${
stage.status === "normal" ? "bg-green-500" : "bg-yellow-500"
}`}
/>
</div>
<div className="space-y-1 relative">
<div className="flex justify-between text-sm">
<span className="text-slate-400 flex items-center gap-1">
<Gauge className="w-3 h-3" />
</span>
<motion.span
key={stage.load}
initial={{ scale: 1.2, color: "#60a5fa" }}
animate={{ scale: 1, color: "#60a5fa" }}
className="font-medium"
>
{Math.round(stage.load)}%
</motion.span>
</div>
<div className="w-full h-2 bg-slate-700/50 rounded-full overflow-hidden">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${stage.load}%` }}
transition={{ duration: 0.5 }}
className={`h-full rounded-full relative ${
stage.load > 90 ? 'bg-red-500' :
stage.load > 75 ? 'bg-yellow-500' :
'bg-green-500'
}`}
>
<motion.div
className="absolute inset-0 bg-white/30"
animate={{ x: ["-100%", "200%"] }}
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
/>
</motion.div>
</div>
<div className="flex justify-between text-sm mt-2">
<span className="text-slate-400 flex items-center gap-1">
<Thermometer className="w-3 h-3" />
</span>
<motion.span
key={stage.temp}
initial={{ scale: 1.1 }}
animate={{ scale: 1 }}
className="text-orange-400 font-medium"
>
{Math.round(stage.temp)}°C
</motion.span>
</div>
</div>
</motion.div>
{index < processData.length - 1 && (
<div className="mx-2 flex items-center relative">
<motion.div
animate={{
x: [0, 8, 0],
opacity: [0.3, 1, 0.3]
}}
transition={{ duration: 1.5, repeat: Infinity }}
className="text-slate-600 relative"
>
</motion.div>
{/* Flow Particles */}
<div className="absolute -top-2 left-1/2">
<motion.div
className="w-1.5 h-1.5 rounded-full bg-blue-400/50"
animate={{ x: [0, 20, 0], y: [0, -5, 0] }}
transition={{ duration: 1, repeat: Infinity }}
/>
</div>
</div>
)}
</div>
))}
</div>
</div>
</motion.div>
{/* Charts Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Temperature Trends */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="rounded-xl bg-slate-900/50 border border-slate-800/50 backdrop-blur-sm p-6"
>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-white"></h2>
<div className="flex gap-3 text-xs">
<div className="flex items-center gap-1">
<div className="w-3 h-3 rounded-full bg-blue-500"></div>
<span className="text-slate-400"></span>
</div>
<div className="flex items-center gap-1">
<div className="w-3 h-3 rounded-full bg-purple-500"></div>
<span className="text-slate-400"></span>
</div>
<div className="flex items-center gap-1">
<div className="w-3 h-3 rounded-full bg-green-500"></div>
<span className="text-slate-400"></span>
</div>
</div>
</div>
<ResponsiveContainer width="100%" height={200}>
<LineChart data={tempTrendData}>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" opacity={0.3} />
<XAxis dataKey="time" stroke="#94a3b8" fontSize={12} />
<YAxis stroke="#94a3b8" fontSize={12} />
<Tooltip
contentStyle={{
backgroundColor: "#1e293b",
border: "1px solid #334155",
borderRadius: "8px",
color: "#fff"
}}
/>
<Line type="monotone" dataKey="t1" stroke="#3b82f6" strokeWidth={2} dot={{ r: 3 }} />
<Line type="monotone" dataKey="t2" stroke="#8b5cf6" strokeWidth={2} dot={{ r: 3 }} />
<Line type="monotone" dataKey="t3" stroke="#10b981" strokeWidth={2} dot={{ r: 3 }} />
</LineChart>
</ResponsiveContainer>
</motion.div>
{/* Production Output */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="rounded-xl bg-slate-900/50 border border-slate-800/50 backdrop-blur-sm p-6"
>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-white"></h2>
<span className="text-sm text-slate-400">单位: /</span>
</div>
<ResponsiveContainer width="100%" height={200}>
<BarChart data={currentProductionData}>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" opacity={0.3} />
<XAxis dataKey="hour" stroke="#94a3b8" fontSize={12} />
<YAxis stroke="#94a3b8" fontSize={12} />
<Tooltip
contentStyle={{
backgroundColor: "#1e293b",
border: "1px solid #334155",
borderRadius: "8px",
color: "#fff"
}}
/>
<Bar dataKey="value" fill="#10b981" radius={[8, 8, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</motion.div>
</div>
{/* Bottom Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Alerts Panel */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="lg:col-span-2 rounded-xl bg-slate-900/50 border border-slate-800/50 backdrop-blur-sm p-6"
>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-white"></h2>
<div className="flex gap-3 text-sm">
<span className="px-3 py-1 rounded-full bg-yellow-500/20 text-yellow-400">
{alertsList.filter(a => !a.handled).length}
</span>
<span className="px-3 py-1 rounded-full bg-green-500/20 text-green-400">
{alertsList.filter(a => a.handled).length}
</span>
</div>
</div>
<div className="space-y-2">
<AnimatePresence mode="popLayout">
{alertsList.map((alert) => (
<motion.div
key={alert.id}
layout
initial={{ opacity: 0, x: -50, scale: 0.8 }}
animate={{
opacity: 1,
x: 0,
scale: 1,
backgroundColor: !alert.handled && alert.type === "warning"
? ["rgba(234, 179, 8, 0.05)", "rgba(234, 179, 8, 0.1)", "rgba(234, 179, 8, 0.05)"]
: "rgba(30, 41, 59, 0.5)"
}}
exit={{ opacity: 0, x: 50, scale: 0.8 }}
transition={{ duration: 0.4 }}
className={`flex items-center justify-between p-4 rounded-lg border ${
alert.handled ? "bg-slate-800/30" : "bg-slate-800/50"
} ${
alert.type === "warning"
? "border-yellow-500/30"
: alert.type === "success"
? "border-green-500/30"
: "border-blue-500/30"
}`}
>
<div className="flex items-center gap-3 flex-1">
<motion.div
animate={alert.type === "warning" ? {
rotate: [-5, 5, -5, 5, 0],
scale: [1, 1.1, 1]
} : {}}
transition={{ duration: 0.5 }}
>
{alert.type === "warning" && <AlertTriangle className="w-5 h-5 text-yellow-400" />}
{alert.type === "success" && <CheckCircle className="w-5 h-5 text-green-400" />}
{alert.type === "info" && <Activity className="w-5 h-5 text-blue-400" />}
</motion.div>
<div className="flex-1">
<motion.div
className={`font-medium ${alert.handled ? "text-slate-500" : "text-white"}`}
animate={!alert.handled ? { opacity: [0.8, 1, 0.8] } : {}}
transition={{ duration: 2, repeat: Infinity }}
>
{alert.message}
</motion.div>
<div className="text-xs text-slate-500 mt-1 flex items-center gap-1">
<Clock className="w-3 h-3" />
{alert.time}
</div>
</div>
</div>
{!alert.handled ? (
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="px-4 py-2 rounded-lg bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium transition-colors"
onClick={() => handleAlertAction(alert.id)}
>
</motion.button>
) : (
<motion.span
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-sm text-slate-500 flex items-center gap-1"
>
<CheckCircle className="w-4 h-4" />
</motion.span>
)}
</motion.div>
))}
</AnimatePresence>
</div>
</motion.div>
{/* Equipment Status */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="rounded-xl bg-slate-900/50 border border-slate-800/50 backdrop-blur-sm p-6"
>
<h2 className="text-lg font-semibold text-white mb-4"></h2>
<div className="space-y-3">
{currentEquipmentStatus.map((equipment: typeof steelEquipmentStatus[0], index: number) => (
<div
key={index}
className="p-3 rounded-lg bg-slate-800/50 border border-slate-700/50"
>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-white">{equipment.name}</span>
<span className={`flex items-center gap-1 text-xs px-2 py-1 rounded-full ${
equipment.status === "running"
? "bg-green-500/20 text-green-400"
: "bg-orange-500/20 text-orange-400"
}`}>
<div className={`w-1.5 h-1.5 rounded-full ${
equipment.status === "running" ? "bg-green-500" : "bg-orange-500"
}`}></div>
{equipment.status === "running" ? "运行中" : "维护中"}
</span>
</div>
{equipment.status === "running" && (
<div className="space-y-1">
<div className="flex justify-between text-xs">
<span className="text-slate-400"></span>
<span className="text-white">{equipment.efficiency}%</span>
</div>
<div className="flex justify-between text-xs">
<span className="text-slate-400"></span>
<span className="text-white">{equipment.uptime}</span>
</div>
</div>
)}
</div>
))}
</div>
</motion.div>
</div>
</div>
);
}