import { useEffect, useState, type ReactNode } from 'react'; import { AlertTriangle, CheckCircle2, Copy, FileBox, RefreshCw, Search, ShieldAlert, XCircle } from 'lucide-react'; import { motion } from 'motion/react'; import { AdminSelect } from '@/src/components/admin/AdminSelect'; import { cn } from '@/src/lib/utils'; import { formatBytes, formatDateTime } from '@/src/lib/format'; import { getAdminFileBlobs, type AdminFileBlobEntityType, type AdminFileBlobResponse, } from '@/src/lib/admin-fileblobs'; const container = { hidden: { opacity: 0 }, show: { opacity: 1, transition: { staggerChildren: 0.05, }, }, }; const itemVariants = { hidden: { y: 12, opacity: 0 }, show: { y: 0, opacity: 1 }, }; const DEFAULT_FILTERS = { userQuery: '', storagePolicyId: '', objectKey: '', entityType: '' as AdminFileBlobEntityType | '', }; const ENTITY_TYPE_LABELS: Record = { VERSION: '版本', THUMBNAIL: '缩略图', LIVE_PHOTO: '实况照片', TRANSCODE: '转码产物', AVATAR: '头像', }; function statusPill(active: boolean, trueLabel: string, falseLabel: string, tone: 'red' | 'amber' | 'purple' = 'red') { const toneClass = tone === 'amber' ? active ? 'border-amber-500/20 bg-amber-500/10 text-amber-600 dark:text-amber-400' : 'border-white/10 bg-white/5 text-gray-500 dark:text-gray-300' : tone === 'purple' ? active ? 'border-purple-500/20 bg-purple-500/10 text-purple-600 dark:text-purple-400' : 'border-white/10 bg-white/5 text-gray-500 dark:text-gray-300' : active ? 'border-red-500/20 bg-red-500/10 text-red-600 dark:text-red-400' : 'border-white/10 bg-white/5 text-gray-500 dark:text-gray-300'; return ( {active ? : } {active ? trueLabel : falseLabel} ); } function titleBlock(title: string, subtitle: string) { return (

{title}

{subtitle}

); } function metricCard({ label, value, icon, tone, }: { label: string; value: string; icon: ReactNode; tone: 'blue' | 'green' | 'amber' | 'red' | 'purple'; }) { return (
{icon}

{value}

{label}

); } function riskRow(label: string, active: boolean, tone: 'red' | 'amber' | 'purple') { return statusPill(active, label, `无${label}`, tone); } export default function AdminFileBlobs() { const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [error, setError] = useState(''); const [notice, setNotice] = useState(''); const [filters, setFilters] = useState(DEFAULT_FILTERS); const [page, setPage] = useState<{ items: AdminFileBlobResponse[]; total: number; page: number; size: number; } | null>(null); async function loadFileBlobs(nextFilters = filters, isRefresh = false) { const trimmedStoragePolicyId = nextFilters.storagePolicyId.trim(); if (trimmedStoragePolicyId) { const parsedStoragePolicyId = Number(trimmedStoragePolicyId); if (!Number.isInteger(parsedStoragePolicyId) || parsedStoragePolicyId <= 0) { setError('存储策略 ID 必须是正整数'); return; } } if (isRefresh) { setRefreshing(true); } else { setLoading(true); } setError(''); try { const result = await getAdminFileBlobs(0, 100, { userQuery: nextFilters.userQuery, storagePolicyId: trimmedStoragePolicyId ? Number(trimmedStoragePolicyId) : null, objectKey: nextFilters.objectKey, entityType: nextFilters.entityType, }); setPage(result); } catch (err) { setError(err instanceof Error ? err.message : '加载对象实体失败'); } finally { setLoading(false); setRefreshing(false); } } useEffect(() => { void loadFileBlobs(); }, []); async function copyText(value: string) { if (!value) { return; } try { await navigator.clipboard.writeText(value); setNotice('对象键已复制'); setError(''); } catch { setError('复制失败,请手动复制。'); setNotice(''); } } const items = page?.items ?? []; const blobMissingCount = items.filter((item) => item.blobMissing).length; const orphanRiskCount = items.filter((item) => item.orphanRisk).length; const referenceMismatchCount = items.filter((item) => item.referenceMismatch).length; const anyRiskCount = items.filter((item) => item.blobMissing || item.orphanRisk || item.referenceMismatch).length; const activeFilterLabels = [ filters.userQuery.trim() ? `用户: ${filters.userQuery.trim()}` : '', filters.storagePolicyId.trim() ? `策略: #${filters.storagePolicyId.trim()}` : '', filters.objectKey.trim() ? `对象键: ${filters.objectKey.trim()}` : '', filters.entityType ? `实体类型: ${ENTITY_TYPE_LABELS[filters.entityType]}` : '', ].filter(Boolean); const isInitialLoading = loading && !page; const pageLabel = page ? `第 ${page.page + 1} 页 / 每页 ${page.size}` : '-'; return (

对象实体

`GET /api/admin/file-blobs` / 用户检索 / 策略过滤 / 风险巡检

{notice ? (
{notice}
) : null} {metricCard({ label: '当前页对象数', value: page ? `${items.length}` : '-', icon: , tone: 'blue', })} {metricCard({ label: 'blobMissing', value: `${blobMissingCount}`, icon: , tone: 'red', })} {metricCard({ label: 'orphanRisk', value: `${orphanRiskCount}`, icon: , tone: 'amber', })} {metricCard({ label: 'referenceMismatch', value: `${referenceMismatchCount}`, icon: , tone: 'purple', })}
{ event.preventDefault(); void loadFileBlobs(filters); }} className="mb-8 glass-panel-no-hover rounded-lg border border-white/10 p-6 shadow-3xl" > {titleBlock('筛选器', '只保留服务端支持的查询参数,避免前端做额外猜测')}
{activeFilterLabels.length ? ( activeFilterLabels.map((label) => ( {label} )) ) : ( 当前没有启用筛选条件 )}
{error ? (
{error}
) : null} {page ? (
共 {page.total} 条记录 / {pageLabel} 风险对象 {anyRiskCount} 条
) : null}
{isInitialLoading ? (
正在加载对象实体快照...
) : page ? (
{items.map((item) => { const rowClassName = cn( 'group transition-colors', item.blobMissing && 'bg-red-500/5 hover:bg-red-500/10', !item.blobMissing && item.orphanRisk && 'bg-amber-500/5 hover:bg-amber-500/10', !item.blobMissing && !item.orphanRisk && item.referenceMismatch && 'bg-purple-500/5 hover:bg-purple-500/10', ); return ( ); })} {items.length === 0 ? ( ) : null}
对象键 实体 / Blob 存储策略 大小 / 类型 关联信息 风险 时间
{item.objectKey}
创建者 {item.createdByUsername || `#${item.createdByUserId ?? '-'}`}
样本所有者 {item.sampleOwnerUsername || '-'} {item.sampleOwnerEmail ? ` / ${item.sampleOwnerEmail}` : ''}
实体 #{item.entityId}
Blob #{item.blobId}
{ENTITY_TYPE_LABELS[item.entityType]}
策略 #{item.storagePolicyId}
{item.createdByUserId != null ? `创建者 ID ${item.createdByUserId}` : '创建者 ID -'}
{formatBytes(item.size)}
{item.contentType || '-'}
引用 {item.referenceCount ?? '-'}
关联文件 {item.linkedStoredFileCount}
关联所有者 {item.linkedOwnerCount}
{riskRow('blobMissing', item.blobMissing, 'red')} {riskRow('orphanRisk', item.orphanRisk, 'amber')} {riskRow('referenceMismatch', item.referenceMismatch, 'purple')}
{formatDateTime(item.createdAt)}
Blob {formatDateTime(item.blobCreatedAt)}
没有匹配的对象实体
) : (
暂无对象实体数据
)}
); }