import React from 'react'; import { Modal, Typography, Spin, theme, Card, Descriptions, Divider, Badge, Space, message, Collapse, Tag } from 'antd'; import { FileOutlined, FolderOutlined, CameraOutlined, InfoCircleOutlined, DatabaseOutlined } from '@ant-design/icons'; import { useI18n } from '../../../i18n'; import type { VfsEntry } from '../../../api/client'; interface Props { entry: VfsEntry | null; loading: boolean; data: any; onClose: () => void; } function getExifFieldMap(t: (k: string)=>string): Record string }> { return { '271': { label: t('Camera Make') }, '272': { label: t('Camera Model') }, '306': { label: t('Capture Time') }, '282': { label: t('X Resolution'), format: v => `${v} dpi` }, '283': { label: t('Y Resolution'), format: v => `${v} dpi` }, '33434': { label: t('Exposure Time'), format: v => `${v} s` }, '33437': { label: t('Aperture'), format: v => `f/${v}` }, '34855': { label: 'ISO' }, '37377': { label: t('Focal Length'), format: v => `${v} mm` }, '40962': { label: t('Width'), format: v => `${v} px` }, '40963': { label: t('Height'), format: v => `${v} px` }, }; } function renderExif(exif: Record, t: (k: string)=>string) { const exifFieldMap = getExifFieldMap(t); const items = Object.entries(exifFieldMap) .filter(([key]) => exif[key] !== undefined) .map(([key, { label, format }]) => ({ key, label, value: format ? format(exif[key]) : exif[key] })); if (items.length === 0) { return (
{t('No common EXIF info')}
); } return ( ({ key: item.key, label: {item.label}, children: {item.value} }))} contentStyle={{ padding: '8px 12px' }} labelStyle={{ padding: '8px 12px', backgroundColor: 'var(--ant-color-fill-tertiary, #fafafa)', width: '30%' }} /> ); } function formatFileSize(size: number | string, t: (k: string)=>string): string { if (typeof size !== 'number') return String(size); const units = [t('Bytes'), 'KB', 'MB', 'GB']; let index = 0; let fileSize = size; while (fileSize >= 1024 && index < units.length - 1) { fileSize /= 1024; index++; } return `${fileSize.toFixed(index === 0 ? 0 : 1)} ${units[index]}`; } export const FileDetailModal: React.FC = ({ entry, loading, data, onClose }) => { const { token } = theme.useToken(); const { t } = useI18n(); const vectorIndex = data?.vector_index; const vectorEntries = Array.isArray(vectorIndex?.entries) ? vectorIndex.entries : []; const primaryIndexEntries = vectorEntries.slice(0, 3); const remainingIndexEntries = vectorEntries.slice(3); const renderIndexEntry = (entry: any, idx: number, total: number) => { const key = entry?.chunk_id ?? entry?.vector_id ?? idx; const hasOffsets = entry?.start_offset !== undefined || entry?.end_offset !== undefined; const previewText = entry?.preview; const previewTruncated = Boolean(entry?.preview_truncated && previewText); return (
{entry?.chunk_id && ( {t('Chunk ID')}: {entry.chunk_id} )} {entry?.type && ( {entry.type} )} {entry?.mime && ( {entry.mime} )} {entry?.name && !previewText && ( {entry.name} )} {hasOffsets && ( {t('Offset Range')}: {entry?.start_offset ?? '-'} ~ {entry?.end_offset ?? '-'} )} {entry?.vector_id && ( {t('Vector ID')}: {entry.vector_id} )} {previewText && ( {previewText} )}
); }; return ( {t('File Properties')} {entry && ( - {entry.name} )} } open={!!entry} onCancel={onClose} footer={null} width={800} styles={{ body: { padding: '20px 0px' } }} > {loading ? (
{t('Loading file info...')}
) : data ? ( data.error ? (
{data.error}
) : (
{/* 左侧:基本信息 */}
{data.is_dir ? : } {t('Basic Info')} } style={{ borderRadius: 8, height: 'fit-content' }} > {data.name} }, { key: 'type', label: t('Type'), children: ( ) }, { key: 'size', label: t('Size'), children: formatFileSize(data.size, t) }, { key: 'mtime', label: t('Modified Time'), children: data.mtime ? ( typeof data.mtime === 'number' ? new Date(data.mtime * 1000).toLocaleString() : data.mtime ) : '-' }, { key: 'path', label: t('Path'), children: ( { e.preventDefault(); try { if (navigator.clipboard) { navigator.clipboard.writeText(data.path).then(() => { message.success(t('Path copied to clipboard')); }).catch(() => { message.error(t('Copy failed')); }); } else { const textarea = document.createElement('textarea'); textarea.value = data.path; document.body.appendChild(textarea); textarea.select(); const ok = document.execCommand('copy'); document.body.removeChild(textarea); message[ok ? 'success' : 'error'](ok ? t('Path copied to clipboard') : t('Copy failed')); } } catch { message.error(t('Copy failed')); } }} style={{ fontSize: 12, wordBreak: 'break-all', backgroundColor: token.colorFillAlter, padding: '4px 8px', borderRadius: 4, display: 'inline-block' }} > {data.path} ) } ]} contentStyle={{ fontSize: 14, color: token.colorText }} labelStyle={{ fontWeight: 500, color: token.colorTextSecondary, width: '30%' }} /> {data.mode !== undefined && ( <>
{t('Permissions')}: {data.mode.toString(8)}
)}
{!data.is_dir && vectorIndex && ( {t('Index Info')} } > 0 ? ( {Object.entries(vectorIndex.by_type || {}).map(([type, count]) => ( {type} ({count as number}) ))} ) : ( {t('No index data')} ), }, ]} contentStyle={{ fontSize: 14 }} labelStyle={{ fontWeight: 500, color: token.colorTextSecondary, width: '30%' }} /> {vectorIndex.total ? (
{t('Indexed Chunks')}
{primaryIndexEntries.map((entry: any, idx: number) => renderIndexEntry(entry, idx, primaryIndexEntries.length))} {remainingIndexEntries.length > 0 && ( {remainingIndexEntries.map((entry: any, idx: number) => renderIndexEntry(entry, idx, remainingIndexEntries.length))}
), }]} style={{ background: 'transparent' }} /> )}
{vectorIndex.has_more && ( {t('Showing first {count} entries', { count: vectorEntries.length })} )}
) : (
{t('No index data')}
)} )}
{/* 右侧:EXIF 信息 */} {data.exif && (
{t('EXIF Info')} } style={{ borderRadius: 8, height: 'fit-content' }} > {renderExif(data.exif, t)}
)} ) ) : null}
); }; export default FileDetailModal;