/** * 通用算法结果渲染(Route B) * ------------------------------------------------------------ * 输入:一条 task_analysis_results.resultData(PluginResultData)记录。 * 输出:统一的「误差统计 + 测量值表 + 图片缩略图 + 3D 叠加按钮 + 原始字段折叠」。 * * 设计目标:新增算法不需要为它写新的 React 组件;只要 result.json 字段对齐 * PluginResult,就能自动渲染。 */ import { useState } from 'react'; import { Box, ChevronDown, ChevronRight, Image as ImageIcon } from 'lucide-react'; interface Measurement { name: string; value: number; unit?: string; tolerance?: number; status?: 'pass' | 'warning' | 'fail' | 'info'; } interface ErrorStats { min: number; max: number; mean: number; stdDev: number; totalPoints?: number; withinTolerance?: number; outOfTolerance?: number; } type Overlay3D = | { kind: 'none' } | { kind: 'vertexColors'; file: string; vertexCount: number; dispMin: number; dispMax: number } | { kind: 'ply'; file: string }; interface PluginResultLike { pluginId?: string; schemaVersion?: number; message?: string; measurements?: Measurement[]; errorStats?: ErrorStats | null; overlay3d?: Overlay3D; images?: string[]; extra?: Record; error?: string; } interface Props { taskId: number; resultId: number; pluginId?: string; pluginName?: string; data: PluginResultLike; /** 用户点击「在 3D 中查看」时回调,把叠加层信息传给 Home → PointCloudViewer */ onViewOverlay?: (overlay: { taskId: number; resultId: number; overlay: Overlay3D }) => void; } function statusClass(s?: Measurement['status']) { switch (s) { case 'pass': return 'bg-green-900/40 text-green-400'; case 'warning': return 'bg-amber-900/40 text-amber-400'; case 'fail': return 'bg-red-900/40 text-red-400'; default: return 'bg-secondary text-muted-foreground'; } } function statusLabel(s?: Measurement['status']) { switch (s) { case 'pass': return '合格'; case 'warning': return '警告'; case 'fail': return '超差'; default: return '—'; } } function formatValue(v: number): string { if (!Number.isFinite(v)) return '—'; const abs = Math.abs(v); if (abs !== 0 && (abs < 0.001 || abs >= 1e6)) return v.toExponential(3); return v.toFixed(3); } export default function GenericResultView({ taskId, resultId, pluginName, data, onViewOverlay, }: Props) { const [showExtra, setShowExtra] = useState(false); const errorStats = data.errorStats ?? null; const measurements = data.measurements ?? []; const images = data.images ?? []; const overlay = data.overlay3d ?? { kind: 'none' }; return (
{pluginName && (
{pluginName} {data.pluginId ? ` · ${data.pluginId}` : ''} {data.schemaVersion != null ? ` · schema v${data.schemaVersion}` : ''}
)} {data.message && (
{data.message}
)} {errorStats && (
{[ { label: '最小误差', value: formatValue(errorStats.min) + ' mm' }, { label: '最大误差', value: formatValue(errorStats.max) + ' mm' }, { label: '平均误差', value: formatValue(errorStats.mean) + ' mm' }, { label: '标准差', value: formatValue(errorStats.stdDev) + ' mm' }, ].map(({ label, value }) => (
{label}
{value}
))}
)} {measurements.length > 0 && (
测量值
{measurements.map((m, i) => ( ))}
项目 值 公差 结果
{m.name} {formatValue(m.value)} {m.unit ?? ''} {m.tolerance != null ? `±${m.tolerance}` : '—'} {m.status === 'info' ? '参考' : statusLabel(m.status)}
)} {overlay.kind !== 'none' && onViewOverlay && ( )} {images.length > 0 && (
产物图片
{images.map((rel) => { const url = `/api/files/read/analysis/${taskId}/${resultId}/${encodeURI(rel)}`; return ( {rel} ); })}
)} {data.extra && Object.keys(data.extra).length > 0 && (
{showExtra && (
              {JSON.stringify(data.extra, null, 2)}
            
)}
)} {data.error && (
{data.error}
)}
); } export type { PluginResultLike, Overlay3D, Measurement, ErrorStats };