- Add hardware fingerprinting with cross-platform support - Implement secure device registration flow with X.509 certificates - Add WebSocket real-time communication for device status - Create comprehensive device management dashboard - Establish zero-trust security architecture with multi-layer protection - Add database migrations for device registration entities - Implement Rust edge client with hardware identification - Add certificate management and automated provisioning system 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
183 lines
5.2 KiB
TypeScript
183 lines
5.2 KiB
TypeScript
import React from 'react'
|
|
import {
|
|
MoreVertical,
|
|
Wifi,
|
|
WifiOff,
|
|
Settings,
|
|
Trash2,
|
|
Edit,
|
|
Activity,
|
|
Calendar
|
|
} from 'lucide-react'
|
|
import { formatDistanceToNow } from 'date-fns'
|
|
import { zhCN } from 'date-fns/locale'
|
|
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'
|
|
import { Button } from '../ui/button'
|
|
import { Badge } from '../ui/badge'
|
|
import { StatusIndicator } from '../ui/status-indicator'
|
|
import { DeviceDto, DeviceStatus } from '../../types/device'
|
|
|
|
interface DeviceCardProps {
|
|
device: DeviceDto
|
|
onEdit?: (device: DeviceDto) => void
|
|
onDelete?: (device: DeviceDto) => void
|
|
onConfigure?: (device: DeviceDto) => void
|
|
className?: string
|
|
}
|
|
|
|
export function DeviceCard({
|
|
device,
|
|
onEdit,
|
|
onDelete,
|
|
onConfigure,
|
|
className
|
|
}: DeviceCardProps) {
|
|
const getStatusConfig = (status: DeviceStatus) => {
|
|
switch (status) {
|
|
case DeviceStatus.ONLINE:
|
|
return {
|
|
variant: 'success' as const,
|
|
icon: <Wifi className="w-3 h-3" />,
|
|
text: '在线'
|
|
}
|
|
case DeviceStatus.OFFLINE:
|
|
return {
|
|
variant: 'destructive' as const,
|
|
icon: <WifiOff className="w-3 h-3" />,
|
|
text: '离线'
|
|
}
|
|
case DeviceStatus.ACTIVE:
|
|
return {
|
|
variant: 'success' as const,
|
|
icon: <Activity className="w-3 h-3" />,
|
|
text: '活跃'
|
|
}
|
|
case DeviceStatus.INACTIVE:
|
|
return {
|
|
variant: 'secondary' as const,
|
|
icon: <Activity className="w-3 h-3" />,
|
|
text: '非活跃'
|
|
}
|
|
case DeviceStatus.MAINTENANCE:
|
|
return {
|
|
variant: 'warning' as const,
|
|
icon: <Settings className="w-3 h-3" />,
|
|
text: '维护中'
|
|
}
|
|
default:
|
|
return {
|
|
variant: 'secondary' as const,
|
|
icon: <Activity className="w-3 h-3" />,
|
|
text: '未知'
|
|
}
|
|
}
|
|
}
|
|
|
|
const statusConfig = getStatusConfig(device.status)
|
|
|
|
const getLastSeenText = () => {
|
|
if (!device.lastSeenAt) {
|
|
return '从未连接'
|
|
}
|
|
|
|
return formatDistanceToNow(new Date(device.lastSeenAt), { addSuffix: true, locale: zhCN })
|
|
}
|
|
|
|
const getRegisteredText = () => {
|
|
return formatDistanceToNow(new Date(device.registeredAt), { addSuffix: true, locale: zhCN })
|
|
}
|
|
|
|
return (
|
|
<Card className={`hover:shadow-md transition-shadow ${className}`}>
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1 min-w-0">
|
|
<CardTitle className="text-lg truncate">
|
|
{device.deviceName || '未命名设备'}
|
|
</CardTitle>
|
|
<div className="flex items-center space-x-2 mt-1">
|
|
<Badge variant={statusConfig.variant} className="flex items-center space-x-1">
|
|
{statusConfig.icon}
|
|
<span>{statusConfig.text}</span>
|
|
</Badge>
|
|
<StatusIndicator
|
|
status={device.status === DeviceStatus.ONLINE ? 'online' : 'offline'}
|
|
size="sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="relative">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-8 w-8 p-0"
|
|
>
|
|
<MoreVertical className="w-4 h-4" />
|
|
</Button>
|
|
{/* Dropdown menu would go here in a real implementation */}
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
|
|
<CardContent className="space-y-4">
|
|
{/* Device Details */}
|
|
<div className="space-y-2 text-sm">
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-500">硬件ID</span>
|
|
<span className="font-mono text-xs">{device.hardwareId}</span>
|
|
</div>
|
|
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-500">最后连接</span>
|
|
<span>{getLastSeenText()}</span>
|
|
</div>
|
|
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-500">注册时间</span>
|
|
<span>{getRegisteredText()}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Action Buttons */}
|
|
<div className="flex space-x-2 pt-2">
|
|
{onConfigure && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => onConfigure(device)}
|
|
className="flex-1"
|
|
>
|
|
<Settings className="w-4 h-4 mr-2" />
|
|
配置
|
|
</Button>
|
|
)}
|
|
|
|
{onEdit && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => onEdit(device)}
|
|
className="flex-1"
|
|
>
|
|
<Edit className="w-4 h-4 mr-2" />
|
|
编辑
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{onDelete && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => onDelete(device)}
|
|
className="w-full text-red-600 border-red-200 hover:bg-red-50"
|
|
>
|
|
<Trash2 className="w-4 h-4 mr-2" />
|
|
删除设备
|
|
</Button>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
} |