Files
massive-cloud/client/src/components/GenericResultView.tsx

232 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 通用算法结果渲染(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<string, unknown>;
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 (
<div className="space-y-3">
{pluginName && (
<div className="text-[10px] text-muted-foreground">
{pluginName}
{data.pluginId ? ` · ${data.pluginId}` : ''}
{data.schemaVersion != null ? ` · schema v${data.schemaVersion}` : ''}
</div>
)}
{data.message && (
<div className="text-[10px] text-muted-foreground italic">{data.message}</div>
)}
{errorStats && (
<div className="grid grid-cols-2 gap-1.5">
{[
{ 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 }) => (
<div key={label} className="industrial-card p-2">
<div className="text-[10px] text-muted-foreground">{label}</div>
<div className="text-xs font-mono mt-0.5">{value}</div>
</div>
))}
</div>
)}
{measurements.length > 0 && (
<div>
<div className="text-[10px] text-muted-foreground mb-1">测量值</div>
<table className="w-full text-[10px] border-collapse">
<thead>
<tr className="text-muted-foreground">
<th className="text-left py-1 font-normal">项目</th>
<th className="text-right py-1 font-normal">值</th>
<th className="text-right py-1 font-normal">公差</th>
<th className="text-right py-1 font-normal">结果</th>
</tr>
</thead>
<tbody>
{measurements.map((m, i) => (
<tr key={`${m.name}-${i}`} className="border-t border-border/40">
<td className="py-1 truncate max-w-[140px]" title={m.name}>
{m.name}
</td>
<td className="text-right font-mono">
{formatValue(m.value)} {m.unit ?? ''}
</td>
<td className="text-right text-muted-foreground">
{m.tolerance != null ? `±${m.tolerance}` : '—'}
</td>
<td className="text-right">
<span className={`px-1 py-0.5 rounded text-[9px] ${statusClass(m.status)}`}>
{m.status === 'info' ? '参考' : statusLabel(m.status)}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{overlay.kind !== 'none' && onViewOverlay && (
<button
type="button"
className="industrial-btn industrial-btn-primary py-1 text-[10px] flex items-center gap-1 w-full justify-center"
onClick={() => onViewOverlay({ taskId, resultId, overlay })}
>
<Box className="w-3 h-3" />
在 3D 中查看叠加层
</button>
)}
{images.length > 0 && (
<div>
<div className="text-[10px] text-muted-foreground mb-1">产物图片</div>
<div className="grid grid-cols-2 gap-1.5">
{images.map((rel) => {
const url = `/api/files/read/analysis/${taskId}/${resultId}/${encodeURI(rel)}`;
return (
<a
key={rel}
href={url}
target="_blank"
rel="noreferrer"
className="industrial-card p-1 flex items-center gap-1 hover:bg-secondary/40 text-[10px]"
title={rel}
>
<ImageIcon className="w-3 h-3 flex-shrink-0" />
<span className="truncate">{rel}</span>
</a>
);
})}
</div>
</div>
)}
{data.extra && Object.keys(data.extra).length > 0 && (
<div>
<button
type="button"
className="flex items-center gap-1 text-[10px] text-muted-foreground hover:text-foreground"
onClick={() => setShowExtra((v) => !v)}
>
{showExtra ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
原始字段 (extra)
</button>
{showExtra && (
<pre className="industrial-card p-2 text-[9px] font-mono whitespace-pre-wrap break-all max-h-48 overflow-y-auto mt-1">
{JSON.stringify(data.extra, null, 2)}
</pre>
)}
</div>
)}
{data.error && (
<div className="industrial-card p-2 border border-red-900/40">
<div className="text-[10px] text-red-400/90">{data.error}</div>
</div>
)}
</div>
);
}
export type { PluginResultLike, Overlay3D, Measurement, ErrorStats };