feat: standardize algorithm measurement results
This commit is contained in:
@@ -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`
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user