feat: standardize algorithm measurement results

This commit is contained in:
2026-08-14 00:12:53 +08:00
parent ac1387debc
commit 185dd36bf3
15 changed files with 1273 additions and 19 deletions

View File

@@ -2,7 +2,7 @@ import { useState, useEffect, useMemo, useRef } from 'react';
import { trpc } from '@/lib/trpc';
import { useAuth } from '@/_core/hooks/useAuth';
import { BarChart2, Ruler, Zap, Play, Trash2, ChevronDown, ChevronRight, RefreshCw, Flame, Box, Layers, Crosshair, Cpu, Timer, Square, Maximize2, FileSpreadsheet, Image as ImageIcon } from 'lucide-react';
import GenericResultView, { filterVisibleMeasurements, formatValue, normalizeOverlay3D, numberArrayFromUnknown, statusClass, type Overlay3D } from './GenericResultView';
import GenericResultView, { filterVisibleMeasurements, formatValue, normalizeOverlay3D, numberArrayFromUnknown, sortMeasurements, statusClass, type Overlay3D } from './GenericResultView';
import { aggregateDemoFlowResults, demoFlowStatusLabel, type DemoFlowArtifactGroup, type DemoFlowStatusNotice } from './demoFlowAggregation';
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog';
@@ -66,7 +66,19 @@ interface Props {
}) => void;
}
type Measurement = { name: string; value: number; unit: string; tolerance: number; status: string };
type Measurement = {
taskNo?: string;
metricCode?: string;
name: string;
position?: string;
value: number;
unit: string;
tolerance: number;
status: string;
baseline?: number;
legacyUnmapped?: boolean;
legacyOriginalName?: string;
};
type ErrorStats = { min: number; max: number; mean: number; stdDev: number; totalPoints: number; withinTolerance: number; outOfTolerance: number };
function ErrorStatCard({ stats }: { stats: ErrorStats }) {
@@ -112,10 +124,13 @@ function ErrorStatCard({ stats }: { stats: ErrorStats }) {
function MeasurementTable({ items, displayMode }: { items: Measurement[]; displayMode: ResultListVariant }) {
const showDetails = displayMode === 'maximized';
const sortedItems = sortMeasurements(items);
return (
<table className="w-full text-[10px] border-collapse">
<div className="overflow-x-auto">
<table className="w-full min-w-[360px] text-[10px] border-collapse">
<thead>
<tr className="text-muted-foreground">
<th className="text-left py-1 pr-2 font-normal whitespace-nowrap">任务号</th>
<th className="text-left py-1 font-normal">项目</th>
{showDetails && <th className="text-right py-1 font-normal">值</th>}
{showDetails && <th className="text-right py-1 font-normal">公差</th>}
@@ -123,9 +138,23 @@ function MeasurementTable({ items, displayMode }: { items: Measurement[]; displa
</tr>
</thead>
<tbody>
{items.map((m, i) => (
{sortedItems.map((m, i) => (
<tr key={i} className="border-t border-border/40">
<td className="py-1">{m.name}</td>
<td className="py-1 pr-2 whitespace-nowrap">
{m.legacyUnmapped || !m.taskNo ? (
<span className="text-amber-400 underline decoration-dotted underline-offset-2 cursor-help" title={`旧算法项目尚未确认任务号:${m.legacyOriginalName ?? m.name}`}>
待确认
</span>
) : m.taskNo}
</td>
<td className="py-1">
{m.name}
{m.position && (
<span className="ml-1.5 inline-flex rounded bg-secondary px-1 py-0.5 text-[9px] text-muted-foreground align-middle">
{m.position}
</span>
)}
</td>
{showDetails && <td className="text-right font-mono">{m.value.toFixed(3)} {m.unit}</td>}
{showDetails && <td className="text-right text-muted-foreground">±{m.tolerance}</td>}
<td className="text-right">
@@ -141,6 +170,7 @@ function MeasurementTable({ items, displayMode }: { items: Measurement[]; displa
))}
</tbody>
</table>
</div>
);
}
@@ -861,6 +891,13 @@ export default function AnalysisPanel({ taskId, onReplay, onViewSmoothHeatmap, o
(result) => getPluginDisplayName(result.pluginId, pluginNameById, getResultTitle(result, pluginNameById)),
{ includeAlgorithmGroups: SHOW_DEMO_FLOW_ALGORITHM_GROUP_ROWS },
);
const sortedTableRows = SHOW_DEMO_FLOW_ALGORITHM_GROUP_ROWS
? aggregated.tableRows
: sortMeasurements(aggregated.measurementRows.map((row) => ({
...row,
taskNo: row.measurement.taskNo,
position: row.measurement.position,
})));
const showMeasurementDetails = variant === 'maximized';
return (
@@ -907,6 +944,7 @@ export default function AnalysisPanel({ taskId, onReplay, onViewSmoothHeatmap, o
<table className="w-full text-[10px] border-collapse">
<thead>
<tr className="text-muted-foreground border-b border-border/60">
<th className="text-left px-2 py-1.5 font-normal whitespace-nowrap">任务号</th>
<th className="text-left px-2 py-1.5 font-normal">项目</th>
{showMeasurementDetails && <th className="text-right px-2 py-1.5 font-normal">值</th>}
{showMeasurementDetails && <th className="text-right px-2 py-1.5 font-normal">公差</th>}
@@ -914,13 +952,30 @@ export default function AnalysisPanel({ taskId, onReplay, onViewSmoothHeatmap, o
</tr>
</thead>
<tbody>
{aggregated.tableRows.map((row) => row.kind === 'algorithm' ? (
{sortedTableRows.map((row) => row.kind === 'algorithm' ? (
<tr key={`group-${row.resultId}`} className="bg-secondary/40">
<td colSpan={showMeasurementDetails ? 4 : 2} className="px-2 py-1 font-medium">{row.algorithmName}</td>
<td colSpan={showMeasurementDetails ? 5 : 3} className="px-2 py-1 font-medium">{row.algorithmName}</td>
</tr>
) : (
<tr key={`${row.resultId}-${row.measurementIndex}`} className="border-t border-border/40">
<td className="px-2 py-1 truncate max-w-[140px]" title={row.measurement.name}>{row.measurement.name}</td>
<td className="px-2 py-1 whitespace-nowrap">
{row.measurement.legacyUnmapped || !row.measurement.taskNo ? (
<span
className="text-amber-400 underline decoration-dotted underline-offset-2 cursor-help"
title={`旧算法项目尚未确认任务号:${row.measurement.legacyOriginalName ?? row.measurement.name}`}
>
待确认
</span>
) : row.measurement.taskNo}
</td>
<td className="px-2 py-1 truncate max-w-[140px]" title={row.measurement.name}>
{row.measurement.name}
{row.measurement.position && (
<span className="ml-1.5 inline-flex rounded bg-secondary px-1 py-0.5 text-[9px] text-muted-foreground align-middle">
{row.measurement.position}
</span>
)}
</td>
{showMeasurementDetails && <td className="px-2 text-right font-mono whitespace-nowrap">
{row.measurement.baseline != null
? `${formatValue(row.measurement.value)}/${row.measurement.baseline}mm`

View File

@@ -11,7 +11,10 @@ import { useState } from 'react';
import { Box, ChevronDown, ChevronRight, FileSpreadsheet, Image as ImageIcon } from 'lucide-react';
interface Measurement {
taskNo?: string;
metricCode?: string;
name: string;
position?: string;
value: number | null;
unit?: string;
tolerance?: number;
@@ -24,6 +27,8 @@ interface Measurement {
passLabel?: string;
warningLabel?: string;
failLabel?: string;
legacyUnmapped?: boolean;
legacyOriginalName?: string;
}
interface ErrorStats {
@@ -113,6 +118,24 @@ export function filterVisibleMeasurements<T extends { status?: unknown }>(measur
return measurements.filter((measurement) => !isReferenceMeasurement(measurement));
}
export function sortMeasurements<T extends { taskNo?: unknown; position?: unknown }>(measurements: readonly T[]): T[] {
const positionOrder = (position: unknown) => position === '左梁' ? 0 : position === '右梁' ? 1 : 2;
return measurements
.map((measurement, index) => ({ measurement, index }))
.sort((a, b) => {
const aNo = typeof a.measurement.taskNo === 'string' && /^\d+$/.test(a.measurement.taskNo)
? Number(a.measurement.taskNo)
: Number.POSITIVE_INFINITY;
const bNo = typeof b.measurement.taskNo === 'string' && /^\d+$/.test(b.measurement.taskNo)
? Number(b.measurement.taskNo)
: Number.POSITIVE_INFINITY;
if (aNo !== bNo) return aNo - bNo;
const positionDelta = positionOrder(a.measurement.position) - positionOrder(b.measurement.position);
return positionDelta || a.index - b.index;
})
.map(({ measurement }) => measurement);
}
export function formatValue(v: number | null): string {
if (v == null) return '—';
if (!Number.isFinite(v)) return '—';
@@ -174,7 +197,7 @@ export default function GenericResultView({
}: Props) {
const [showExtra, setShowExtra] = useState(false);
const errorStats = data.errorStats ?? null;
const measurements = filterVisibleMeasurements(data.measurements ?? []);
const measurements = sortMeasurements(filterVisibleMeasurements(data.measurements ?? []));
const showMeasurementDetails = displayMode === 'maximized';
const images = data.images ?? [];
const files = (data.files ?? []).map((f) =>
@@ -216,9 +239,11 @@ export default function GenericResultView({
{measurements.length > 0 && (
<div>
<div className="text-[10px] text-muted-foreground mb-1">测量值</div>
<table className="w-full text-[10px] border-collapse">
<div className="overflow-x-auto">
<table className="w-full min-w-[360px] text-[10px] border-collapse">
<thead>
<tr className="text-muted-foreground">
<th className="text-left py-1 pr-2 font-normal whitespace-nowrap">任务号</th>
<th className="text-left py-1 font-normal">项目</th>
{showMeasurementDetails && <th className="text-right py-1 font-normal">值</th>}
{showMeasurementDetails && <th className="text-right py-1 font-normal">公差</th>}
@@ -228,8 +253,23 @@ export default function GenericResultView({
<tbody>
{measurements.map((m, i) => (
<tr key={`${m.name}-${i}`} className="border-t border-border/40">
<td className="py-1 pr-2 whitespace-nowrap">
{m.legacyUnmapped || !m.taskNo ? (
<span
className="text-amber-400 underline decoration-dotted underline-offset-2 cursor-help"
title={`旧算法项目尚未确认任务号:${m.legacyOriginalName ?? m.name}`}
>
待确认
</span>
) : m.taskNo}
</td>
<td className="py-1 truncate max-w-[140px]" title={m.name}>
{m.name}
{m.position && (
<span className="ml-1.5 inline-flex rounded bg-secondary px-1 py-0.5 text-[9px] text-muted-foreground align-middle">
{m.position}
</span>
)}
</td>
{showMeasurementDetails && <td className="text-right font-mono">
{m.baseline != null
@@ -254,6 +294,7 @@ export default function GenericResultView({
))}
</tbody>
</table>
</div>
</div>
)}