634 lines
27 KiB
TypeScript
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>
|
|
);
|
|
} |