enterprise-saa-s-dashboard-.../src/app/pages/Dashboard.tsx

187 lines
7.0 KiB
TypeScript

import {
TrendingUp,
TrendingDown,
Server,
Wifi,
CheckCircle,
PackageCheck,
Wrench,
Target,
Clock,
Upload
} from "lucide-react";
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell } from "recharts";
interface MetricCardProps {
label: string;
value: string;
trend?: "up" | "down";
trendValue?: string;
color?: string;
icon: React.ElementType;
}
function MetricCard({ label, value, trend, trendValue, color = "#1890FF", icon: Icon }: MetricCardProps) {
return (
<div className="bg-white p-6 rounded-lg" style={{ boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)' }}>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="text-sm mb-2" style={{ color: 'rgba(0, 0, 0, 0.65)' }}>{label}</div>
<div className="text-3xl font-semibold mb-2">{value}</div>
{trend && trendValue && (
<div className="flex items-center gap-1" style={{ color: trend === "up" ? "#52C41A" : "#FF4D4F" }}>
{trend === "up" ? <TrendingUp size={14} /> : <TrendingDown size={14} />}
<span className="text-sm">{trendValue}</span>
</div>
)}
</div>
<div className="w-12 h-12 rounded-lg flex items-center justify-center" style={{ backgroundColor: `${color}15` }}>
<Icon size={24} style={{ color }} />
</div>
</div>
</div>
);
}
interface TaskItemProps {
deviceSN: string;
description: string;
time?: string;
}
function TaskItem({ deviceSN, description, time }: TaskItemProps) {
return (
<div className="flex items-start justify-between py-3" style={{ borderBottom: '1px solid #F0F0F0' }}>
<div className="flex-1">
<div className="text-sm mb-1">{deviceSN}</div>
<div className="text-sm" style={{ color: 'rgba(0, 0, 0, 0.45)' }}>{description}</div>
</div>
<div className="flex items-center gap-3">
{time && <span className="text-xs" style={{ color: 'rgba(0, 0, 0, 0.45)' }}>{time}</span>}
<button className="text-sm" style={{ color: '#1890FF' }}></button>
</div>
</div>
);
}
export default function Dashboard() {
const metrics = [
{ label: "设备总数", value: "5,234", trend: "up" as const, trendValue: "+5.2%", color: "#1890FF", icon: Server },
{ label: "在线设备", value: "4,856", trend: "up" as const, trendValue: "+2.8%", color: "#52C41A", icon: Wifi },
{ label: "已激活", value: "4,912", trend: "up" as const, trendValue: "+1.5%", color: "#1890FF", icon: CheckCircle },
{ label: "有新版本", value: "156", color: "#722ED1", icon: PackageCheck },
{ label: "维修中", value: "23", trend: "down" as const, trendValue: "-12.3%", color: "#FF4D4F", icon: Wrench },
{ label: "待校准", value: "56", color: "#FA8C16", icon: Target },
{ label: "授权即将到期", value: "45", color: "#FAAD14", icon: Clock },
{ label: "升级中", value: "8", color: "#13C2C2", icon: Upload },
];
const deviceStatusData = [
{ name: "在线", value: 4856, color: "#52C41A" },
{ name: "离线", value: 378, color: "#FF4D4F" },
{ name: "维修", value: 23, color: "#FAAD14" },
{ name: "报废", value: 77, color: "#8C8C8C" },
];
const taskGroups = [
{
title: "待校准设备",
count: 12,
tasks: [
{ deviceSN: "SN2024030801", description: "温度传感器校准到期", time: "2小时前" },
{ deviceSN: "SN2024030802", description: "压力传感器校准到期", time: "3小时前" },
{ deviceSN: "SN2024030803", description: "湿度传感器校准到期", time: "5小时前" },
],
},
{
title: "固件升级通知",
count: 8,
tasks: [
{ deviceSN: "SN2024030710", description: "固件版本v2.3.5可用", time: "1天前" },
{ deviceSN: "SN2024030711", description: "固件版本v2.3.5可用", time: "1天前" },
],
},
{
title: "待授权审批",
count: 15,
tasks: [
{ deviceSN: "SN2024030620", description: "设备授权申请待审批", time: "30分钟前" },
{ deviceSN: "SN2024030621", description: "设备授权申请待审批", time: "1小时前" },
{ deviceSN: "SN2024030622", description: "设备授权延期申请", time: "2小时前" },
],
},
{
title: "维修工单",
count: 5,
tasks: [
{ deviceSN: "SN2024030530", description: "设备故障报修", time: "4小时前" },
{ deviceSN: "SN2024030531", description: "定期维护到期", time: "6小时前" },
],
},
];
return (
<div className="p-6">
{/* Page Header */}
<div className="mb-6">
<h2 className="text-2xl font-semibold mb-1"></h2>
<p className="text-sm" style={{ color: 'rgba(0, 0, 0, 0.45)' }}></p>
</div>
{/* Metric Cards */}
<div className="grid grid-cols-4 gap-6 mb-6">
{metrics.map((metric, index) => (
<MetricCard key={index} {...metric} />
))}
</div>
{/* Device Status Chart */}
<div className="bg-white p-6 rounded-lg mb-6" style={{ boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)' }}>
<h3 className="text-lg font-semibold mb-6"></h3>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={deviceStatusData} layout="vertical">
<CartesianGrid strokeDasharray="3 3" stroke="#F0F0F0" />
<XAxis type="number" />
<YAxis dataKey="name" type="category" width={60} />
<Tooltip />
<Bar dataKey="value" radius={[0, 4, 4, 0]}>
{deviceStatusData.map((entry) => (
<Cell key={`bar-cell-${entry.name}`} fill={entry.color} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
{/* Pending Tasks */}
<div className="bg-white p-6 rounded-lg" style={{ boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)' }}>
<h3 className="text-lg font-semibold mb-6"></h3>
<div className="grid grid-cols-2 gap-6">
{taskGroups.map((group, groupIndex) => (
<div key={groupIndex}>
<div className="flex items-center justify-between mb-4">
<h4 className="text-base font-medium">{group.title}</h4>
<span
className="px-2 py-1 rounded text-xs"
style={{ backgroundColor: '#F0F2F5', color: 'rgba(0, 0, 0, 0.65)' }}
>
{group.count}
</span>
</div>
<div>
{group.tasks.map((task, taskIndex) => (
<TaskItem key={taskIndex} {...task} />
))}
</div>
{group.tasks.length < group.count && (
<button className="w-full mt-3 text-center text-sm" style={{ color: '#1890FF' }}>
{group.count}
</button>
)}
</div>
))}
</div>
</div>
</div>
);
}