({ file }: FileCardProps)
| 44 | } |
| 45 | |
| 46 | export function FileCard({ file }: FileCardProps) { |
| 47 | const { t } = useTranslation('data') |
| 48 | const [previewOpen, setPreviewOpen] = useState(false) |
| 49 | |
| 50 | const Icon = fileIcons[file.type] || FileText |
| 51 | const styles = fileStyles[file.type] || { |
| 52 | icon: 'text-cyber-text-muted', |
| 53 | border: 'hover:border-cyber-neon-cyan/50', |
| 54 | badge: 'border-cyber-border-DEFAULT bg-cyber-bg-tertiary text-cyber-text-secondary' |
| 55 | } |
| 56 | |
| 57 | // 检查是否支持预览 |
| 58 | const isPreviewable = ['json', 'csv', 'xlsx', 'xls'].includes(file.type.toLowerCase()) |
| 59 | |
| 60 | const handleDownload = () => { |
| 61 | const url = dataApi.getDownloadUrl(file.path) |
| 62 | window.open(url, '_blank') |
| 63 | } |
| 64 | |
| 65 | return ( |
| 66 | <> |
| 67 | <Card className={`relative overflow-hidden card-scan group transition-all ${styles.border} hover:shadow-[0_0_15px_rgb(var(--cyber-neon-cyan)/0.15)]`}> |
| 68 | {/* Scan effect overlay */} |
| 69 | <div className="absolute inset-0 bg-gradient-to-r from-transparent via-cyber-neon-cyan/5 to-transparent -translate-x-full group-hover:translate-x-full transition-transform duration-700 pointer-events-none" /> |
| 70 | |
| 71 | <CardContent className="p-4 relative"> |
| 72 | <div className="flex items-start gap-3"> |
| 73 | <div className={`p-2 rounded bg-cyber-bg-panel border border-cyber-border-DEFAULT ${styles.icon}`}> |
| 74 | <Icon className="w-6 h-6" /> |
| 75 | </div> |
| 76 | <div className="flex-1 min-w-0"> |
| 77 | <h3 className="font-mono font-medium text-sm text-cyber-text-primary truncate" title={file.name}> |
| 78 | {file.name} |
| 79 | </h3> |
| 80 | <p className="text-xs text-cyber-text-muted mt-1 font-mono"> |
| 81 | {formatFileSize(file.size)} |
| 82 | {file.record_count !== null && ( |
| 83 | <span className="text-cyber-neon-green"> | {t('file.entries', { count: file.record_count })}</span> |
| 84 | )} |
| 85 | </p> |
| 86 | <p className="text-xs text-cyber-text-muted mt-1 font-mono"> |
| 87 | {formatDateTime(file.modified_at)} |
| 88 | </p> |
| 89 | </div> |
| 90 | </div> |
| 91 | |
| 92 | <div className="flex items-center justify-between mt-3 pt-3 border-t border-cyber-border-subtle"> |
| 93 | <Badge variant="outline" className={`text-[10px] font-mono ${styles.badge}`}> |
| 94 | .{file.type.toUpperCase()} |
| 95 | </Badge> |
| 96 | <div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity"> |
| 97 | {isPreviewable && ( |
| 98 | <Button |
| 99 | variant="ghost" |
| 100 | size="sm" |
| 101 | className="h-7 px-2 font-mono text-cyber-neon-cyan hover:text-cyber-neon-cyan hover:bg-cyber-neon-cyan/10" |
| 102 | onClick={() => setPreviewOpen(true)} |
| 103 | > |
nothing calls this directly
no test coverage detected