feat: 算法升级、整车配准集成与性能分析文档
算法更新: delivery v1.2.1、平整度v1.5、测距v2.2、整车配准v1.3 适配器: vehicle-registration 新增、各算法 manifest 和 adapter 更新 后端: taskCadFiles/source 字段迁移、derivedPointClouds 注册、取消任务联动 前端: ElapsedTime 时长显示、两步算法流程、canRun 逻辑修复 文档: 新增5份算法性能分析报告
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
*/
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { trpc } from '@/lib/trpc';
|
||||
import { CheckCircle2, ChevronDown, ChevronRight, Cpu, Play, RefreshCw } from 'lucide-react';
|
||||
import { AlertTriangle, CheckCircle2, ChevronDown, ChevronRight, Cpu, Play, RefreshCw, Scan } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import TrainMeasurementPanel from './TrainMeasurementPanel';
|
||||
|
||||
@@ -44,10 +44,14 @@ interface PluginSummary {
|
||||
|
||||
interface Props {
|
||||
taskId: number | null;
|
||||
/** 是否已加载点云(用于按钮 disabled 提示) */
|
||||
/** 是否已加载点云(3D 视图,仅用于提示,不再作为执行前置条件) */
|
||||
pointCloudLoaded: boolean;
|
||||
/** 当前任务下的点云文件总数(>0 即可执行算法,无需先在 3D 视图加载) */
|
||||
taskPcFileCount?: number;
|
||||
/** 当前任务中用户选中的点云文件 ID(空/undefined = 全部) */
|
||||
selectedPcFileIds?: number[];
|
||||
/** 当前任务中选中的点云中是否包含配准后的点云 */
|
||||
hasRegisteredCloud?: boolean;
|
||||
/** 当前任务中用户选中的 CAD 文件 ID(空/undefined = 不传 CAD) */
|
||||
selectedCadFileIds?: number[];
|
||||
/** ROI 拾取相关(train-measurement 专用) */
|
||||
@@ -64,7 +68,7 @@ function defaultParamsFor(plugin: PluginSummary): Record<string, unknown> {
|
||||
return out;
|
||||
}
|
||||
|
||||
export default function AlgorithmRunPanel({ taskId, pointCloudLoaded, selectedPcFileIds, selectedCadFileIds, roiPickedPoint, onStartRoiPick, activeRoiPickTarget }: Props) {
|
||||
export default function AlgorithmRunPanel({ taskId, pointCloudLoaded: _pointCloudLoaded, taskPcFileCount, selectedPcFileIds, hasRegisteredCloud, selectedCadFileIds, roiPickedPoint, onStartRoiPick, activeRoiPickTarget }: Props) {
|
||||
const utils = trpc.useUtils();
|
||||
const { data, isLoading } = trpc.algorithm.list.useQuery(undefined, {
|
||||
staleTime: 60_000,
|
||||
@@ -122,14 +126,13 @@ export default function AlgorithmRunPanel({ taskId, pointCloudLoaded, selectedPc
|
||||
};
|
||||
|
||||
const canRun = useMemo(
|
||||
() => taskId != null && pointCloudLoaded && selected.size > 0 && !runBatch.isPending,
|
||||
[taskId, pointCloudLoaded, selected, runBatch.isPending],
|
||||
// 任务下有点云文件(taskPcFileCount > 0)即可执行,无需先在 3D 视图加载
|
||||
() => taskId != null && (taskPcFileCount ?? 0) > 0 && selected.size > 0 && !runBatch.isPending,
|
||||
[taskId, taskPcFileCount, selected, runBatch.isPending],
|
||||
);
|
||||
|
||||
const handleRun = () => {
|
||||
const handleRun = (ids: string[]) => {
|
||||
if (!canRun || taskId == null) return;
|
||||
const ids: string[] = [];
|
||||
selected.forEach((id) => ids.push(id));
|
||||
const jobs = ids.map((pluginId) => ({
|
||||
pluginId,
|
||||
params: paramsById[pluginId] ?? {},
|
||||
@@ -146,137 +149,202 @@ export default function AlgorithmRunPanel({ taskId, pointCloudLoaded, selectedPc
|
||||
});
|
||||
};
|
||||
|
||||
// 区分配准插件 vs 测量插件
|
||||
const regPlugins = plugins.filter((p) => p.category === '配准');
|
||||
const measPlugins = plugins.filter((p) => p.category !== '配准');
|
||||
const selectedMeasIds = measPlugins.filter((p) => selected.has(p.id)).map((p) => p.id);
|
||||
const selectedRegIds = regPlugins.filter((p) => selected.has(p.id)).map((p) => p.id);
|
||||
|
||||
// 不再需要 showRegWarning 变量,已直接内联到 JSX 里
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<span className="industrial-label flex items-center gap-1.5">
|
||||
<Cpu className="w-3.5 h-3.5" /> 算法运行(多选并发)
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
{scheduler && (
|
||||
<span
|
||||
className="text-[9px] text-muted-foreground tabular-nums"
|
||||
title={`并发上限 ${scheduler.limit},运行中 ${scheduler.running},排队 ${scheduler.queued}`}
|
||||
>
|
||||
{scheduler.running}/{scheduler.limit} · 排队 {scheduler.queued}
|
||||
</span>
|
||||
)}
|
||||
<div className="space-y-3">
|
||||
{/* ── 步骤 1:整车配准 ────────────────────────────────── */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<span className="industrial-label flex items-center gap-1.5 text-blue-400">
|
||||
<Scan className="w-3.5 h-3.5" />
|
||||
第 1 步 · 整车配准
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className={`industrial-btn industrial-btn-primary py-0.5 text-[10px] flex items-center gap-0.5 ${canRun ? '' : 'opacity-50 cursor-not-allowed'}`}
|
||||
disabled={!canRun}
|
||||
onClick={handleRun}
|
||||
title={
|
||||
taskId == null
|
||||
? '请先选择检测任务'
|
||||
: !pointCloudLoaded
|
||||
? '请先在 3D 视图加载点云'
|
||||
: selected.size === 0
|
||||
? '请至少勾选一个算法'
|
||||
: '并发执行已勾选的算法'
|
||||
}
|
||||
className={`industrial-btn industrial-btn-primary py-0.5 text-[10px] flex items-center gap-0.5 ${
|
||||
canRun && selectedRegIds.length > 0 ? '' : 'opacity-50 cursor-not-allowed'
|
||||
}`}
|
||||
disabled={!canRun || selectedRegIds.length === 0}
|
||||
onClick={() => handleRun(selectedRegIds)}
|
||||
title={selectedRegIds.length === 0 ? '请勾选配准算法' : '执行配准'}
|
||||
>
|
||||
{runBatch.isPending ? (
|
||||
<RefreshCw className="w-3 h-3 animate-spin" />
|
||||
) : (
|
||||
<Play className="w-3 h-3" />
|
||||
)}
|
||||
执行({selected.size})
|
||||
{runBatch.isPending ? <RefreshCw className="w-3 h-3 animate-spin" /> : <Play className="w-3 h-3" />}
|
||||
执行({selectedRegIds.length})
|
||||
</button>
|
||||
</div>
|
||||
{regPlugins.length === 0 && !isLoading && (
|
||||
<div className="text-[10px] text-muted-foreground italic px-1">暂无配准插件</div>
|
||||
)}
|
||||
{regPlugins.map((p) => (
|
||||
<PluginCard
|
||||
key={p.id}
|
||||
p={p}
|
||||
isSelected={selected.has(p.id)}
|
||||
isExpanded={expanded.has(p.id)}
|
||||
params={paramsById[p.id] ?? {}}
|
||||
onToggleSelect={toggleSelect}
|
||||
onToggleExpand={toggleExpand}
|
||||
onUpdateParam={updateParam}
|
||||
taskId={taskId}
|
||||
roiPickedPoint={roiPickedPoint}
|
||||
onStartRoiPick={onStartRoiPick}
|
||||
activeRoiPickTarget={activeRoiPickTarget}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{isLoading && (
|
||||
<div className="text-xs text-muted-foreground text-center py-2">加载插件列表…</div>
|
||||
)}
|
||||
{!isLoading && plugins.length === 0 && (
|
||||
<div className="text-xs text-muted-foreground text-center py-2">
|
||||
暂无已注册算法插件
|
||||
{/* ── 步骤 2:测量分析 ────────────────────────────────── */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<span className="industrial-label flex items-center gap-1.5">
|
||||
<Cpu className="w-3.5 h-3.5" />
|
||||
第 2 步 · 测量分析(多选并发)
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
{scheduler && (
|
||||
<span
|
||||
className="text-[9px] text-muted-foreground tabular-nums"
|
||||
title={`并发上限 ${scheduler.limit},运行中 ${scheduler.running},排队 ${scheduler.queued}`}
|
||||
>
|
||||
{scheduler.running}/{scheduler.limit} · 排队 {scheduler.queued}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={`industrial-btn industrial-btn-primary py-0.5 text-[10px] flex items-center gap-0.5 ${
|
||||
canRun && selectedMeasIds.length > 0 ? '' : 'opacity-50 cursor-not-allowed'
|
||||
}`}
|
||||
disabled={!canRun || selectedMeasIds.length === 0}
|
||||
onClick={() => handleRun(selectedMeasIds)}
|
||||
title={
|
||||
taskId == null ? '请先选择检测任务'
|
||||
: (taskPcFileCount ?? 0) === 0 ? '请先上传点云文件'
|
||||
: selectedMeasIds.length === 0 ? '请至少勾选一个测量算法'
|
||||
: '并发执行已勾选算法'
|
||||
}
|
||||
>
|
||||
{runBatch.isPending ? <RefreshCw className="w-3 h-3 animate-spin" /> : <Play className="w-3 h-3" />}
|
||||
执行({selectedMeasIds.length})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 未配准提示:有点云文件且选了测量算法,但所选点云均未配准 */}
|
||||
{selectedMeasIds.length > 0 && !hasRegisteredCloud && (taskPcFileCount ?? 0) > 0 && (
|
||||
<div className="flex items-start gap-1.5 bg-amber-900/20 border border-amber-700/40 rounded p-2">
|
||||
<AlertTriangle className="w-3 h-3 text-amber-400 flex-shrink-0 mt-0.5" />
|
||||
<span className="text-[10px] text-amber-300">
|
||||
当前选中的点云未经整车配准,测量结果可能不准确。建议先完成第 1 步配准,再选用已配准点云。
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading && (
|
||||
<div className="text-xs text-muted-foreground text-center py-2">加载插件列表…</div>
|
||||
)}
|
||||
{!isLoading && measPlugins.length === 0 && (
|
||||
<div className="text-xs text-muted-foreground text-center py-2">暂无测量算法插件</div>
|
||||
)}
|
||||
{measPlugins.map((p) => (
|
||||
<PluginCard
|
||||
key={p.id}
|
||||
p={p}
|
||||
isSelected={selected.has(p.id)}
|
||||
isExpanded={expanded.has(p.id)}
|
||||
params={paramsById[p.id] ?? {}}
|
||||
onToggleSelect={toggleSelect}
|
||||
onToggleExpand={toggleExpand}
|
||||
onUpdateParam={updateParam}
|
||||
taskId={taskId}
|
||||
roiPickedPoint={roiPickedPoint}
|
||||
onStartRoiPick={onStartRoiPick}
|
||||
activeRoiPickTarget={activeRoiPickTarget}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PluginCard({
|
||||
p, isSelected, isExpanded, params,
|
||||
onToggleSelect, onToggleExpand, onUpdateParam,
|
||||
taskId, roiPickedPoint, onStartRoiPick, activeRoiPickTarget,
|
||||
}: {
|
||||
p: PluginSummary;
|
||||
isSelected: boolean;
|
||||
isExpanded: boolean;
|
||||
params: Record<string, unknown>;
|
||||
onToggleSelect: (id: string) => void;
|
||||
onToggleExpand: (id: string) => void;
|
||||
onUpdateParam: (pluginId: string, name: string, value: unknown) => void;
|
||||
taskId: number | null;
|
||||
roiPickedPoint?: { x: number; y: number; z: number } | null;
|
||||
onStartRoiPick?: (taskIdx: number, roiKey: 'source_roi' | 'target_roi', corner: 'min' | 'max') => void;
|
||||
activeRoiPickTarget?: { taskIdx: number; roiKey: string; corner: string } | null;
|
||||
}) {
|
||||
return (
|
||||
<div className={`industrial-card rounded overflow-hidden ${isSelected ? 'border-primary/50' : ''}`}>
|
||||
<div className="flex items-center gap-2 px-2 py-1.5">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
onChange={() => onToggleSelect(p.id)}
|
||||
className="w-3.5 h-3.5 accent-primary"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="flex-1 flex items-center gap-1.5 text-left"
|
||||
onClick={() => onToggleExpand(p.id)}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="w-3 h-3 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronRight className="w-3 h-3 text-muted-foreground" />
|
||||
)}
|
||||
<span className="text-xs flex-1 truncate" title={p.description}>{p.name}</span>
|
||||
<span className="text-[9px] text-muted-foreground tabular-nums flex-shrink-0">v{p.version}</span>
|
||||
{p.runtimeKind === 'mock' && (
|
||||
<span className="text-[9px] px-1 py-0.5 rounded bg-amber-900/40 text-amber-400 flex-shrink-0" title="Mock 模式">mock</span>
|
||||
)}
|
||||
{p.runtimeKind === 'process' && (
|
||||
<CheckCircle2 className="w-3 h-3 text-green-400 flex-shrink-0" aria-label="已就绪" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<div className="px-3 pb-2 pt-1 border-t border-border/40 space-y-2">
|
||||
<p className="text-[10px] text-muted-foreground">{p.description}</p>
|
||||
{p.id === 'train-measurement' ? (
|
||||
<TrainMeasurementPanel
|
||||
taskId={taskId}
|
||||
pickedPoint={roiPickedPoint}
|
||||
onStartRoiPick={onStartRoiPick}
|
||||
activePickTarget={activeRoiPickTarget}
|
||||
/>
|
||||
) : p.inputs.length === 0 ? (
|
||||
<p className="text-[10px] text-muted-foreground italic">该算法无可调参数。</p>
|
||||
) : (
|
||||
<div className="space-y-1.5">
|
||||
{p.inputs.map((field) => (
|
||||
<FieldEditor
|
||||
key={field.name}
|
||||
field={field}
|
||||
value={params[field.name]}
|
||||
onChange={(v) => onUpdateParam(p.id, field.name, v)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-1.5">
|
||||
{plugins.map((p) => {
|
||||
const isSelected = selected.has(p.id);
|
||||
const isExpanded = expanded.has(p.id);
|
||||
const params = paramsById[p.id] ?? {};
|
||||
return (
|
||||
<div
|
||||
key={p.id}
|
||||
className={`industrial-card rounded overflow-hidden ${isSelected ? 'border-primary/50' : ''}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 px-2 py-1.5">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
onChange={() => toggleSelect(p.id)}
|
||||
className="w-3.5 h-3.5 accent-primary"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="flex-1 flex items-center gap-1.5 text-left"
|
||||
onClick={() => toggleExpand(p.id)}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="w-3 h-3 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronRight className="w-3 h-3 text-muted-foreground" />
|
||||
)}
|
||||
<span className="text-xs flex-1 truncate" title={p.description}>
|
||||
{p.name}
|
||||
</span>
|
||||
<span className="text-[9px] text-muted-foreground tabular-nums flex-shrink-0">
|
||||
v{p.version}
|
||||
</span>
|
||||
{p.runtimeKind === 'mock' && (
|
||||
<span
|
||||
className="text-[9px] px-1 py-0.5 rounded bg-amber-900/40 text-amber-400 flex-shrink-0"
|
||||
title="Mock 模式:返回预设结果,算法 CLI 就绪后切换为真实执行"
|
||||
>
|
||||
mock
|
||||
</span>
|
||||
)}
|
||||
{p.runtimeKind === 'process' && (
|
||||
<CheckCircle2
|
||||
className="w-3 h-3 text-green-400 flex-shrink-0"
|
||||
aria-label="已就绪"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="px-3 pb-2 pt-1 border-t border-border/40 space-y-2">
|
||||
<p className="text-[10px] text-muted-foreground">{p.description}</p>
|
||||
{p.id === 'train-measurement' ? (
|
||||
<TrainMeasurementPanel
|
||||
taskId={taskId}
|
||||
pickedPoint={roiPickedPoint}
|
||||
onStartRoiPick={onStartRoiPick}
|
||||
activePickTarget={activeRoiPickTarget}
|
||||
/>
|
||||
) : p.inputs.length === 0 ? (
|
||||
<p className="text-[10px] text-muted-foreground italic">
|
||||
该算法无可调参数。
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-1.5">
|
||||
{p.inputs.map((field) => (
|
||||
<FieldEditor
|
||||
key={field.name}
|
||||
field={field}
|
||||
value={params[field.name]}
|
||||
onChange={(v) => updateParam(p.id, field.name, v)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,46 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { trpc } from '@/lib/trpc';
|
||||
import { BarChart2, Ruler, Zap, Play, Trash2, ChevronDown, ChevronRight, RefreshCw, Flame, Box, Layers, Crosshair, Cpu } from 'lucide-react';
|
||||
import { BarChart2, Ruler, Zap, Play, Trash2, ChevronDown, ChevronRight, RefreshCw, Flame, Box, Layers, Crosshair, Cpu, Timer } from 'lucide-react';
|
||||
import GenericResultView, { type Overlay3D } from './GenericResultView';
|
||||
|
||||
/** 将毫秒数格式化为可读时长,如 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;
|
||||
@@ -245,6 +283,7 @@ export default function AnalysisPanel({ taskId, onReplay, onViewSmoothHeatmap, o
|
||||
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') &&
|
||||
(() => {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* PluginResult,就能自动渲染。
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { Box, ChevronDown, ChevronRight, Image as ImageIcon } from 'lucide-react';
|
||||
import { Box, ChevronDown, ChevronRight, FileSpreadsheet, Image as ImageIcon } from 'lucide-react';
|
||||
|
||||
interface Measurement {
|
||||
name: string;
|
||||
@@ -41,6 +41,8 @@ interface PluginResultLike {
|
||||
errorStats?: ErrorStats | null;
|
||||
overlay3d?: Overlay3D;
|
||||
images?: string[];
|
||||
/** outDir 下的报表产物(相对路径,如 report.xlsx),前端展示下载入口 */
|
||||
files?: string[];
|
||||
extra?: Record<string, unknown>;
|
||||
error?: string;
|
||||
}
|
||||
@@ -99,6 +101,7 @@ export default function GenericResultView({
|
||||
const errorStats = data.errorStats ?? null;
|
||||
const measurements = data.measurements ?? [];
|
||||
const images = data.images ?? [];
|
||||
const files = data.files ?? [];
|
||||
const overlay = data.overlay3d ?? { kind: 'none' };
|
||||
|
||||
return (
|
||||
@@ -177,6 +180,29 @@ export default function GenericResultView({
|
||||
</button>
|
||||
)}
|
||||
|
||||
{files.length > 0 && (
|
||||
<div>
|
||||
<div className="text-[10px] text-muted-foreground mb-1">报表下载</div>
|
||||
<div className="space-y-1">
|
||||
{files.map((rel) => {
|
||||
const url = `/api/files/read/analysis/${taskId}/${resultId}/${encodeURI(rel)}`;
|
||||
return (
|
||||
<a
|
||||
key={rel}
|
||||
href={url}
|
||||
download
|
||||
className="industrial-card p-1.5 flex items-center gap-1.5 hover:bg-secondary/40 text-[10px]"
|
||||
title={rel}
|
||||
>
|
||||
<FileSpreadsheet className="w-3 h-3 flex-shrink-0 text-green-500" />
|
||||
<span className="truncate">{rel}</span>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{images.length > 0 && (
|
||||
<div>
|
||||
<div className="text-[10px] text-muted-foreground mb-1">产物图片</div>
|
||||
|
||||
@@ -436,6 +436,11 @@ export default function TaskManager({
|
||||
<span className="flex-1 truncate" title={f.name}>
|
||||
{f.name}
|
||||
</span>
|
||||
{(f as { source?: string }).source === 'registered' && (
|
||||
<span className="text-[9px] px-1 py-0.5 rounded bg-blue-900/40 text-blue-300 flex-shrink-0 whitespace-nowrap">
|
||||
已配准
|
||||
</span>
|
||||
)}
|
||||
<span className="text-muted-foreground text-[10px] flex-shrink-0">
|
||||
{formatSize(f.fileSize)}
|
||||
</span>
|
||||
|
||||
@@ -49,6 +49,18 @@ export default function Home() {
|
||||
const [pcLoadKey, setPcLoadKey] = useState(0);
|
||||
/** 当前任务下用户勾选的点云文件 ID(空 = 加载全部) */
|
||||
const [selectedPcFileIds, setSelectedPcFileIds] = useState<number[]>([]);
|
||||
/** 当前任务下所有点云文件列表(用于判断是否含已配准点云) */
|
||||
const { data: taskPcFiles = [] } = trpc.pointcloudFile.list.useQuery(
|
||||
{ taskId: selection.taskId! },
|
||||
{ enabled: selection.taskId != null },
|
||||
);
|
||||
/** 已选中的点云文件中,是否包含配准算法产出的点云 */
|
||||
const hasRegisteredCloud =
|
||||
selectedPcFileIds.length === 0
|
||||
? taskPcFiles.some((f) => (f as { source?: string }).source === 'registered')
|
||||
: taskPcFiles
|
||||
.filter((f) => selectedPcFileIds.includes(f.id))
|
||||
.some((f) => (f as { source?: string }).source === 'registered');
|
||||
/** 当前任务下用户勾选的 CAD 文件 ID(空 = 不传 CAD) */
|
||||
const [selectedTaskCadFileIds, setSelectedTaskCadFileIds] = useState<number[]>([]);
|
||||
/** ROI 坐标拾取状态(train-measurement 专用) */
|
||||
@@ -339,7 +351,9 @@ export default function Home() {
|
||||
<AlgorithmRunPanel
|
||||
taskId={selection.taskId}
|
||||
pointCloudLoaded={pointCloudTaskId === selection.taskId && pointCloudUrls.length > 0}
|
||||
taskPcFileCount={taskPcFiles.length}
|
||||
selectedPcFileIds={selectedPcFileIds}
|
||||
hasRegisteredCloud={hasRegisteredCloud}
|
||||
selectedCadFileIds={selectedTaskCadFileIds}
|
||||
roiPickedPoint={roiPickedPoint}
|
||||
onStartRoiPick={(taskIdx, roiKey, corner) => {
|
||||
|
||||
Reference in New Issue
Block a user