import { useState, useEffect } from 'react'; import { trpc } from '@/lib/trpc'; import { BarChart2, Ruler, Zap, Play, Trash2, ChevronDown, ChevronRight, RefreshCw, Flame, Box, Layers, Crosshair, Cpu, Timer } from 'lucide-react'; import GenericResultView, { type Overlay3D } from './GenericResultView'; import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip'; /** 将毫秒数格式化为可读时长,如 2m 34s / 47s / 1h 5m */ function fmtDuration(ms: number): string { const s = Math.floor(ms / 1000); if (s < 60) return `${s}s`; const m = Math.floor(s / 60); if (m < 60) return `${m}m ${s % 60}s`; return `${Math.floor(m / 60)}h ${m % 60}m`; } /** 运行中的任务:实时更新计时;已结束:显示静态 updatedAt - createdAt */ function ElapsedTime({ status, createdAt, updatedAt }: { status: string; createdAt: string | Date; updatedAt: string | Date; }) { const start = new Date(createdAt).getTime(); const end = new Date(updatedAt).getTime(); const [now, setNow] = useState(() => Date.now()); useEffect(() => { if (status !== 'processing' && status !== 'pending') return; const id = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(id); }, [status]); const ms = (status === 'processing' || status === 'pending') ? now - start : end - start; if (ms < 500) return null; return ( {fmtDuration(ms)} ); } interface Props { taskId: number; onReplay?: (resultId: number) => void; /** 在 3D 视口显示该条平滑热力分析结果 */ onViewSmoothHeatmap?: (resultId: number) => void; /** 触发装配检测(来自 Home.tsx 的 runAnalysisJob) */ onRunAssembly?: () => void; /** 触发偏差热力(来自 Home.tsx 的 runAnalysisJob) */ onRunDeviation?: () => void; /** 外部分析任务是否正在提交中 */ externalPending?: boolean; /** 插件叠加层:用户点击「在 3D 中查看叠加层」时回调 Home → PointCloudViewer */ onViewPluginOverlay?: (args: { taskId: number; resultId: number; overlay: Overlay3D }) => void; } type Measurement = { name: string; value: number; unit: string; tolerance: number; status: string }; type ErrorStats = { min: number; max: number; mean: number; stdDev: number; totalPoints: number; withinTolerance: number; outOfTolerance: number }; function ErrorStatCard({ stats }: { stats: ErrorStats }) { const pct = stats.totalPoints > 0 ? ((stats.withinTolerance / stats.totalPoints) * 100).toFixed(1) : '0'; return (
{[ { label: '最小误差', value: stats.min.toFixed(4) + ' mm' }, { label: '最大误差', value: stats.max.toFixed(4) + ' mm' }, { label: '平均误差', value: stats.mean.toFixed(4) + ' mm' }, { label: '标准差', value: stats.stdDev.toFixed(4) + ' mm' }, ].map(({ label, value }) => (
{label}
{value}
))}
合格率 = 95 ? 'text-green-400' : parseFloat(pct) >= 85 ? 'text-amber-400' : 'text-red-400'}> {pct}%
= 95 ? 'oklch(0.62 0.16 145)' : parseFloat(pct) >= 85 ? 'oklch(0.72 0.16 85)' : 'oklch(0.55 0.20 28)' }} />
合格 {stats.withinTolerance.toLocaleString()} 超差 {stats.outOfTolerance.toLocaleString()}
); } function MeasurementTable({ items }: { items: Measurement[] }) { return ( {items.map((m, i) => ( ))}
项目 值 公差 结果
{m.name} {m.value.toFixed(3)} {m.unit} ±{m.tolerance} {m.status === 'pass' ? '合格' : m.status === 'warning' ? '警告' : '超差'}
); } type SmoothHeatmapMeta = { kind?: string; /** 0–1,processing 时子进程写入 */ progress?: number; stage?: string; vertexCount?: number; dispMin?: number; dispMax?: number; error?: string; }; type AssemblyPartRow = { partId: string; name: string; min: number; max: number; mean: number; stdDev: number; passRate: number; pointCount: number; vertexHeatmapBin?: string; }; type AssemblyMeta = SmoothHeatmapMeta & { toleranceMm?: number; samplePoints?: number; parts?: AssemblyPartRow[]; }; function getResultTitle(r: { type: string; pluginId?: string }) { if (r.type === 'error_analysis') return '误差分析'; if (r.type === 'measurement') return '尺寸测量'; if (r.type === 'smooth_heatmap') return '平滑热力'; if (r.type === 'assembly_inspection') return '装配检测'; if (r.type === 'obj_deviation') return '偏差热力'; if (r.type === 'plugin') return `算法 · ${r.pluginId ?? '插件'}`; return '压缩'; } export default function AnalysisPanel({ taskId, onReplay, onViewSmoothHeatmap, onRunAssembly, onRunDeviation, externalPending, onViewPluginOverlay }: Props) { const utils = trpc.useUtils(); const [expandedIds, setExpandedIds] = useState>(new Set()); const { data: results = [], isLoading } = trpc.analysisResult.list.useQuery( { taskId }, { refetchInterval: 3000 }, ); const runAnalysis = trpc.analysisResult.runAnalysis.useMutation({ onSuccess: () => utils.analysisResult.list.invalidate({ taskId }), }); const deleteResult = trpc.analysisResult.delete.useMutation({ onSuccess: () => utils.analysisResult.list.invalidate({ taskId }), }); function toggleExpand(id: number) { setExpandedIds(prev => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; }); } const running = runAnalysis.isPending; return (
分析结果
{onRunAssembly && ( )} {onRunDeviation && ( )}
{isLoading && (
加载中…
)} {!isLoading && results.length === 0 && (
暂无分析结果,点击运行分析
)}
{results.map(r => { const expanded = expandedIds.has(r.id); const errorStats = r.errorStats as ErrorStats | null; const measurements = r.measurements as Measurement[] | null; const resultTitle = getResultTitle(r as { type: string; pluginId?: string }); return (
{/* 标题行 */}
toggleExpand(r.id)} > {expanded ? : } {r.type === 'error_analysis' && } {r.type === 'measurement' && } {r.type === 'compression' && } {r.type === 'smooth_heatmap' && } {r.type === 'assembly_inspection' && } {r.type === 'obj_deviation' && } {r.type === 'plugin' && } {resultTitle} {resultTitle} {new Date(r.createdAt).toLocaleDateString('zh-CN')} {r.status} {r.status === 'processing' && (r.type === 'smooth_heatmap' || r.type === 'assembly_inspection' || r.type === 'obj_deviation' || r.type === 'plugin') && (() => { const hm = r.resultData as SmoothHeatmapMeta | null; const pct = hm?.progress != null ? Math.round(hm.progress * 100) : null; return pct != null ? ( {pct}% ) : null; })()} {onReplay && r.status === 'completed' && ( )}
{/* 展开内容 */} {expanded && r.status === 'completed' && r.type !== 'smooth_heatmap' && r.type !== 'assembly_inspection' && r.type !== 'obj_deviation' && r.type !== 'plugin' && (
{errorStats && } {measurements && measurements.length > 0 && (
尺寸测量
)}
)} {expanded && (r.type === 'smooth_heatmap' || r.type === 'assembly_inspection' || r.type === 'obj_deviation' || r.type === 'plugin') && r.status === 'processing' && (() => { const hm = r.resultData as SmoothHeatmapMeta | null; const p = hm?.progress ?? 0; const stage = hm?.stage ?? '排队或启动中…'; return (
{stage}

后台子进程执行,页面可继续操作;进度约每 1.2s 刷新。

); })()} {expanded && r.type === 'smooth_heatmap' && r.status === 'completed' && (
{(() => { const hm = r.resultData as SmoothHeatmapMeta | null; return ( <>
{hm?.vertexCount != null && (
网格顶点约 {hm.vertexCount.toLocaleString()}
)} {hm?.dispMin != null && hm?.dispMax != null && (
色标误差约 {hm.dispMin.toFixed(4)} ~ {hm.dispMax.toFixed(4)} mm
)}
); })()}
)} {expanded && r.type === 'smooth_heatmap' && r.status === 'error' && (

{(r.resultData as SmoothHeatmapMeta | null)?.error ?? '分析失败'}

)} {expanded && r.type === 'assembly_inspection' && r.status === 'completed' && (
{(() => { const am = r.resultData as AssemblyMeta | null; const tol = am?.toleranceMm ?? 0.3; const parts = am?.parts ?? []; return ( <>
阈值 < {tol} mm 为合格;测点归属为「到各零件网格距离最小」的零件。
{parts.map((p) => ( ))}
零件 点数 min max mean 合格率
{p.name} {p.pointCount} {p.min.toFixed(4)} {p.max.toFixed(4)} {p.mean.toFixed(4)} = 0.95 ? 'text-green-400' : p.passRate >= 0.85 ? 'text-amber-400' : 'text-red-400' } > {(p.passRate * 100).toFixed(1)}%
); })()}
)} {expanded && r.type === 'assembly_inspection' && r.status === 'error' && (

{(r.resultData as AssemblyMeta | null)?.error ?? '装配检测失败'}

)} {expanded && r.type === 'obj_deviation' && r.status === 'completed' && (
{(() => { const hm = r.resultData as SmoothHeatmapMeta | null; return ( <>
按顶点到最近点云点的距离着色(蓝=近 → 红=远)
{hm?.vertexCount != null && (
网格顶点约 {hm.vertexCount.toLocaleString()}
)} {hm?.dispMin != null && hm?.dispMax != null && (
偏差范围约 {hm.dispMin.toFixed(4)} ~ {hm.dispMax.toFixed(4)}
)}
); })()}
)} {expanded && r.type === 'obj_deviation' && r.status === 'error' && (

{(r.resultData as SmoothHeatmapMeta | null)?.error ?? '偏差分析失败'}

)} {expanded && r.type === 'plugin' && (r.status === 'completed' || r.status === 'error') && (
[0]['data']) ?? {}} onViewOverlay={onViewPluginOverlay} />
)}
); })}
); }