532 lines
24 KiB
TypeScript
532 lines
24 KiB
TypeScript
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 (
|
||
<span className="flex items-center gap-0.5 text-[9px] tabular-nums text-muted-foreground flex-shrink-0" title="执行时长">
|
||
<Timer className="w-2.5 h-2.5" />
|
||
{fmtDuration(ms)}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
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 (
|
||
<div className="space-y-2">
|
||
<div className="grid grid-cols-2 gap-1.5">
|
||
{[
|
||
{ 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 }) => (
|
||
<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>
|
||
<div className="industrial-card p-2">
|
||
<div className="flex items-center justify-between text-[10px] mb-1">
|
||
<span className="text-muted-foreground">合格率</span>
|
||
<span className={parseFloat(pct) >= 95 ? 'text-green-400' : parseFloat(pct) >= 85 ? 'text-amber-400' : 'text-red-400'}>
|
||
{pct}%
|
||
</span>
|
||
</div>
|
||
<div className="industrial-progress">
|
||
<div
|
||
className="industrial-progress-bar"
|
||
style={{ width: `${pct}%`, background: parseFloat(pct) >= 95 ? 'oklch(0.62 0.16 145)' : parseFloat(pct) >= 85 ? 'oklch(0.72 0.16 85)' : 'oklch(0.55 0.20 28)' }}
|
||
/>
|
||
</div>
|
||
<div className="flex justify-between text-[10px] text-muted-foreground mt-1">
|
||
<span>合格 {stats.withinTolerance.toLocaleString()}</span>
|
||
<span>超差 {stats.outOfTolerance.toLocaleString()}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function MeasurementTable({ items }: { items: Measurement[] }) {
|
||
return (
|
||
<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>
|
||
{items.map((m, i) => (
|
||
<tr key={i} className="border-t border-border/40">
|
||
<td className="py-1">{m.name}</td>
|
||
<td className="text-right font-mono">{m.value.toFixed(3)} {m.unit}</td>
|
||
<td className="text-right text-muted-foreground">±{m.tolerance}</td>
|
||
<td className="text-right">
|
||
<span className={`px-1 py-0.5 rounded text-[9px] ${
|
||
m.status === 'pass' ? 'bg-green-900/40 text-green-400' :
|
||
m.status === 'warning' ? 'bg-amber-900/40 text-amber-400' :
|
||
'bg-red-900/40 text-red-400'
|
||
}`}>
|
||
{m.status === 'pass' ? '合格' : m.status === 'warning' ? '警告' : '超差'}
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
);
|
||
}
|
||
|
||
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<Set<number>>(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 (
|
||
<div className="space-y-3">
|
||
<div className="flex items-center justify-between gap-1 flex-wrap">
|
||
<span className="industrial-label flex items-center gap-1.5">
|
||
<BarChart2 className="w-3.5 h-3.5" /> 分析结果
|
||
</span>
|
||
<div className="flex items-center gap-1">
|
||
<button
|
||
className={`industrial-btn industrial-btn-primary py-0.5 text-[10px] flex items-center gap-0.5 ${running ? 'opacity-60 cursor-not-allowed' : ''}`}
|
||
onClick={() => !running && runAnalysis.mutate({ taskId, type: 'error_analysis' })}
|
||
disabled={running}
|
||
title="误差统计分析"
|
||
>
|
||
{running ? <RefreshCw className="w-3 h-3 animate-spin" /> : <Play className="w-3 h-3" />}
|
||
{running ? '分析中…' : '误差分析'}
|
||
</button>
|
||
{onRunAssembly && (
|
||
<button
|
||
className={`industrial-btn py-0.5 text-[10px] flex items-center gap-0.5 ${externalPending ? 'opacity-60 cursor-wait' : ''}`}
|
||
onClick={onRunAssembly}
|
||
disabled={externalPending}
|
||
title="需车型下已上传 STEP;后台解析零件并做最近零件归属与误差统计"
|
||
>
|
||
<Layers className="w-3 h-3" />
|
||
装配检测
|
||
</button>
|
||
)}
|
||
{onRunDeviation && (
|
||
<button
|
||
className={`industrial-btn py-0.5 text-[10px] flex items-center gap-0.5 ${externalPending ? 'opacity-60 cursor-wait' : ''}`}
|
||
onClick={onRunDeviation}
|
||
disabled={externalPending}
|
||
title="在 STL/OBJ 网格上按顶点到最近点云点的距离着色"
|
||
>
|
||
<Crosshair className="w-3 h-3" />
|
||
偏差热力
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{isLoading && (
|
||
<div className="text-xs text-muted-foreground text-center py-4">加载中…</div>
|
||
)}
|
||
|
||
{!isLoading && results.length === 0 && (
|
||
<div className="text-xs text-muted-foreground text-center py-4">
|
||
暂无分析结果,点击运行分析
|
||
</div>
|
||
)}
|
||
|
||
<div className="space-y-1.5">
|
||
{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 (
|
||
<div key={r.id} className="industrial-card rounded overflow-hidden min-w-0">
|
||
{/* 标题行 */}
|
||
<div
|
||
className="flex items-center gap-2 px-2 py-1.5 cursor-pointer hover:bg-secondary/40 group min-w-0"
|
||
onClick={() => toggleExpand(r.id)}
|
||
>
|
||
{expanded ? <ChevronDown className="w-3 h-3 text-muted-foreground" /> : <ChevronRight className="w-3 h-3 text-muted-foreground" />}
|
||
{r.type === 'error_analysis' && <BarChart2 className="w-3.5 h-3.5 text-blue-400 flex-shrink-0" />}
|
||
{r.type === 'measurement' && <Ruler className="w-3.5 h-3.5 text-green-400 flex-shrink-0" />}
|
||
{r.type === 'compression' && <Zap className="w-3.5 h-3.5 text-amber-400 flex-shrink-0" />}
|
||
{r.type === 'smooth_heatmap' && <Flame className="w-3.5 h-3.5 text-orange-400 flex-shrink-0" />}
|
||
{r.type === 'assembly_inspection' && <Layers className="w-3.5 h-3.5 text-cyan-400 flex-shrink-0" />}
|
||
{r.type === 'obj_deviation' && <Crosshair className="w-3.5 h-3.5 text-rose-400 flex-shrink-0" />}
|
||
{r.type === 'plugin' && <Cpu className="w-3.5 h-3.5 text-violet-400 flex-shrink-0" />}
|
||
<Tooltip>
|
||
<TooltipTrigger asChild>
|
||
<span className="text-xs flex-1 min-w-0 truncate">{resultTitle}</span>
|
||
</TooltipTrigger>
|
||
<TooltipContent
|
||
side="left"
|
||
align="start"
|
||
className="max-w-[min(28rem,calc(100vw-2rem))] break-all"
|
||
>
|
||
{resultTitle}
|
||
</TooltipContent>
|
||
</Tooltip>
|
||
<span className="text-[10px] text-muted-foreground">
|
||
{new Date(r.createdAt).toLocaleDateString('zh-CN')}
|
||
</span>
|
||
<span className={`text-[9px] px-1.5 py-0.5 rounded flex-shrink-0 ${
|
||
r.status === 'completed' ? 'bg-green-900/40 text-green-400' :
|
||
r.status === 'processing' ? 'bg-blue-900/40 text-blue-400 animate-pulse' :
|
||
r.status === 'error' ? 'bg-red-900/40 text-red-400' :
|
||
'bg-secondary text-muted-foreground'
|
||
}`}>{r.status}</span>
|
||
<ElapsedTime status={r.status} createdAt={r.createdAt} updatedAt={r.updatedAt} />
|
||
{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 ? (
|
||
<span className="text-[9px] font-mono text-primary tabular-nums">{pct}%</span>
|
||
) : null;
|
||
})()}
|
||
|
||
<span className="hidden group-hover:flex items-center gap-0.5">
|
||
{onReplay && r.status === 'completed' && (
|
||
<button
|
||
className="p-0.5 rounded hover:bg-secondary"
|
||
title="回放此结果"
|
||
onClick={e => { e.stopPropagation(); onReplay(r.id); }}
|
||
>
|
||
<Play className="w-3 h-3 text-primary" />
|
||
</button>
|
||
)}
|
||
<button
|
||
className="p-0.5 rounded hover:bg-secondary text-destructive"
|
||
title="删除"
|
||
onClick={e => { e.stopPropagation(); deleteResult.mutate({ id: r.id }); }}
|
||
>
|
||
<Trash2 className="w-3 h-3" />
|
||
</button>
|
||
</span>
|
||
</div>
|
||
|
||
{/* 展开内容 */}
|
||
{expanded && r.status === 'completed' && r.type !== 'smooth_heatmap' && r.type !== 'assembly_inspection' && r.type !== 'obj_deviation' && r.type !== 'plugin' && (
|
||
<div className="px-3 pb-3 pt-1 border-t border-border/40 space-y-3">
|
||
{errorStats && <ErrorStatCard stats={errorStats} />}
|
||
{measurements && measurements.length > 0 && (
|
||
<div>
|
||
<div className="text-[10px] text-muted-foreground mb-1">尺寸测量</div>
|
||
<MeasurementTable items={measurements} />
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{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 (
|
||
<div className="px-3 pb-3 pt-1 border-t border-border/40 space-y-2">
|
||
<div className="text-[10px] text-muted-foreground">{stage}</div>
|
||
<div className="industrial-progress h-1.5">
|
||
<div
|
||
className="industrial-progress-bar transition-[width] duration-300"
|
||
style={{
|
||
width: `${Math.min(100, Math.max(0, p * 100))}%`,
|
||
background: 'oklch(0.55 0.14 250)',
|
||
}}
|
||
/>
|
||
</div>
|
||
<p className="text-[9px] text-muted-foreground/80">
|
||
后台子进程执行,页面可继续操作;进度约每 1.2s 刷新。
|
||
</p>
|
||
</div>
|
||
);
|
||
})()}
|
||
|
||
{expanded && r.type === 'smooth_heatmap' && r.status === 'completed' && (
|
||
<div className="px-3 pb-3 pt-1 border-t border-border/40 space-y-2">
|
||
{(() => {
|
||
const hm = r.resultData as SmoothHeatmapMeta | null;
|
||
return (
|
||
<>
|
||
<div className="text-[10px] text-muted-foreground space-y-0.5">
|
||
{hm?.vertexCount != null && (
|
||
<div>网格顶点约 {hm.vertexCount.toLocaleString()}</div>
|
||
)}
|
||
{hm?.dispMin != null && hm?.dispMax != null && (
|
||
<div>
|
||
色标误差约 {hm.dispMin.toFixed(4)} ~ {hm.dispMax.toFixed(4)} mm
|
||
</div>
|
||
)}
|
||
</div>
|
||
<button
|
||
type="button"
|
||
className="industrial-btn industrial-btn-primary py-1 text-[10px] flex items-center gap-1 w-full justify-center"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
onViewSmoothHeatmap?.(r.id);
|
||
}}
|
||
>
|
||
<Box className="w-3 h-3" />
|
||
在 3D 中查看
|
||
</button>
|
||
</>
|
||
);
|
||
})()}
|
||
</div>
|
||
)}
|
||
|
||
{expanded && r.type === 'smooth_heatmap' && r.status === 'error' && (
|
||
<div className="px-3 pb-3 pt-1 border-t border-border/40">
|
||
<p className="text-[10px] text-red-400/90">
|
||
{(r.resultData as SmoothHeatmapMeta | null)?.error ?? '分析失败'}
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{expanded && r.type === 'assembly_inspection' && r.status === 'completed' && (
|
||
<div className="px-3 pb-3 pt-1 border-t border-border/40 space-y-2">
|
||
{(() => {
|
||
const am = r.resultData as AssemblyMeta | null;
|
||
const tol = am?.toleranceMm ?? 0.3;
|
||
const parts = am?.parts ?? [];
|
||
return (
|
||
<>
|
||
<div className="text-[10px] text-muted-foreground">
|
||
阈值 < {tol} mm 为合格;测点归属为「到各零件网格距离最小」的零件。
|
||
</div>
|
||
<div className="overflow-x-auto max-h-48 overflow-y-auto">
|
||
<table className="w-full text-[9px] border-collapse">
|
||
<thead>
|
||
<tr className="text-muted-foreground text-left">
|
||
<th className="py-0.5 pr-1">零件</th>
|
||
<th className="py-0.5 pr-1 text-right">点数</th>
|
||
<th className="py-0.5 pr-1 text-right">min</th>
|
||
<th className="py-0.5 pr-1 text-right">max</th>
|
||
<th className="py-0.5 pr-1 text-right">mean</th>
|
||
<th className="py-0.5 text-right">合格率</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{parts.map((p) => (
|
||
<tr key={p.partId} className="border-t border-border/30">
|
||
<td className="py-0.5 pr-1 font-mono truncate max-w-[100px]" title={p.name}>
|
||
{p.name}
|
||
</td>
|
||
<td className="py-0.5 pr-1 text-right">{p.pointCount}</td>
|
||
<td className="py-0.5 pr-1 text-right">{p.min.toFixed(4)}</td>
|
||
<td className="py-0.5 pr-1 text-right">{p.max.toFixed(4)}</td>
|
||
<td className="py-0.5 pr-1 text-right">{p.mean.toFixed(4)}</td>
|
||
<td className="py-0.5 text-right">
|
||
<span
|
||
className={
|
||
p.passRate >= 0.95
|
||
? 'text-green-400'
|
||
: p.passRate >= 0.85
|
||
? 'text-amber-400'
|
||
: 'text-red-400'
|
||
}
|
||
>
|
||
{(p.passRate * 100).toFixed(1)}%
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</>
|
||
);
|
||
})()}
|
||
</div>
|
||
)}
|
||
|
||
{expanded && r.type === 'assembly_inspection' && r.status === 'error' && (
|
||
<div className="px-3 pb-3 pt-1 border-t border-border/40">
|
||
<p className="text-[10px] text-red-400/90">
|
||
{(r.resultData as AssemblyMeta | null)?.error ?? '装配检测失败'}
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{expanded && r.type === 'obj_deviation' && r.status === 'completed' && (
|
||
<div className="px-3 pb-3 pt-1 border-t border-border/40 space-y-2">
|
||
{(() => {
|
||
const hm = r.resultData as SmoothHeatmapMeta | null;
|
||
return (
|
||
<>
|
||
<div className="text-[10px] text-muted-foreground space-y-0.5">
|
||
<div>按顶点到最近点云点的距离着色(蓝=近 → 红=远)</div>
|
||
{hm?.vertexCount != null && (
|
||
<div>网格顶点约 {hm.vertexCount.toLocaleString()}</div>
|
||
)}
|
||
{hm?.dispMin != null && hm?.dispMax != null && (
|
||
<div>
|
||
偏差范围约 {hm.dispMin.toFixed(4)} ~ {hm.dispMax.toFixed(4)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
<button
|
||
type="button"
|
||
className="industrial-btn industrial-btn-primary py-1 text-[10px] flex items-center gap-1 w-full justify-center"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
onViewSmoothHeatmap?.(r.id);
|
||
}}
|
||
>
|
||
<Box className="w-3 h-3" />
|
||
在 3D 中查看
|
||
</button>
|
||
</>
|
||
);
|
||
})()}
|
||
</div>
|
||
)}
|
||
|
||
{expanded && r.type === 'obj_deviation' && r.status === 'error' && (
|
||
<div className="px-3 pb-3 pt-1 border-t border-border/40">
|
||
<p className="text-[10px] text-red-400/90">
|
||
{(r.resultData as SmoothHeatmapMeta | null)?.error ?? '偏差分析失败'}
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{expanded && r.type === 'plugin' && (r.status === 'completed' || r.status === 'error') && (
|
||
<div className="px-3 pb-3 pt-1 border-t border-border/40">
|
||
<GenericResultView
|
||
taskId={taskId}
|
||
resultId={r.id}
|
||
pluginId={(r as { pluginId?: string }).pluginId ?? undefined}
|
||
pluginName={(r as { pluginId?: string }).pluginId ?? '算法插件'}
|
||
data={(r.resultData as Parameters<typeof GenericResultView>[0]['data']) ?? {}}
|
||
onViewOverlay={onViewPluginOverlay}
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|