diff --git a/client/src/components/AlgorithmRunPanel.tsx b/client/src/components/AlgorithmRunPanel.tsx index 6e1ac8c..16482b4 100644 --- a/client/src/components/AlgorithmRunPanel.tsx +++ b/client/src/components/AlgorithmRunPanel.tsx @@ -11,10 +11,17 @@ */ import { useEffect, useState } from 'react'; import { trpc } from '@/lib/trpc'; -import { AlertTriangle, CheckCircle2, ChevronDown, ChevronRight, Cpu, Play, RefreshCw, Scan } from 'lucide-react'; +import { AlertTriangle, CheckCircle2, ChevronDown, ChevronRight, Cpu, Play, RefreshCw, Scan, Square } from 'lucide-react'; import { toast } from 'sonner'; import TrainMeasurementPanel from './TrainMeasurementPanel'; +const DEMO_MEASUREMENT_PLUGIN_IDS = [ + 'train-measurement', + 'coupler-panel', + 'flatness-suite', + 'underframe-deflection', +]; + interface PluginInputField { name: string; label: string; @@ -53,6 +60,7 @@ interface PointcloudFileSummary { interface Props { taskId: number | null; + modelId: number | null; /** 是否已加载点云(3D 视图,仅用于提示,不再作为执行前置条件) */ pointCloudLoaded: boolean; /** 当前任务下的点云文件总数(>0 即可执行算法,无需先在 3D 视图加载) */ @@ -86,7 +94,7 @@ function defaultParamsFor(plugin: PluginSummary): Record { return out; } -export default function AlgorithmRunPanel({ taskId, pointCloudLoaded: _pointCloudLoaded, taskPcFileCount, taskPointcloudFiles = [], selectedPcFileIds, hasRegisteredCloud, selectedCadFileIds, roiPickedPoint, onStartRoiPick, activeRoiPickTarget }: Props) { +export default function AlgorithmRunPanel({ taskId, modelId, pointCloudLoaded: _pointCloudLoaded, taskPcFileCount, taskPointcloudFiles = [], selectedPcFileIds, hasRegisteredCloud, selectedCadFileIds, roiPickedPoint, onStartRoiPick, activeRoiPickTarget }: Props) { const utils = trpc.useUtils(); const { data, isLoading } = trpc.algorithm.list.useQuery(undefined, { staleTime: 60_000, @@ -98,12 +106,51 @@ export default function AlgorithmRunPanel({ taskId, pointCloudLoaded: _pointClou void utils.analysisResult.list.invalidate({ taskId }); void utils.pointcloudFile.list.invalidate({ taskId }); } + if (modelId != null) { + void utils.inspectionTask.listByModel.invalidate({ modelId }); + } }, onError: (e) => toast.error(e.message || '提交失败'), }); + const runDemoAll = trpc.algorithm.runDemoAll.useMutation({ + onSuccess: ({ jobs, registrationJob, queuedMeasurementCount }) => { + const submittedCount = jobs.length + (registrationJob ? 1 : 0); + if (registrationJob) { + toast.success(`已提交整车配准;完成后将自动提交 ${queuedMeasurementCount} 个测量算法`); + } else { + toast.success(`已提交 ${submittedCount} 个算法到并发池`); + } + if (taskId != null) { + void utils.analysisResult.list.invalidate({ taskId }); + void utils.pointcloudFile.list.invalidate({ taskId }); + } + if (modelId != null) { + void utils.inspectionTask.listByModel.invalidate({ modelId }); + } + }, + onError: (e) => toast.error(e.message || '一键全流程演示提交失败'), + }); + const cancelDemoAll = trpc.algorithm.cancelDemoAll.useMutation({ + onSuccess: ({ cancelledIds }) => { + toast.success(cancelledIds.length > 0 ? `已停止 ${cancelledIds.length} 个运行任务` : '当前没有运行中的全流程任务'); + if (taskId != null) { + void utils.analysisResult.list.invalidate({ taskId }); + void utils.pointcloudFile.list.invalidate({ taskId }); + } + if (modelId != null) { + void utils.inspectionTask.listByModel.invalidate({ modelId }); + } + void utils.algorithm.list.invalidate(); + }, + onError: (e) => toast.error(e.message || '停止全流程失败'), + }); const plugins = data?.plugins ?? []; const scheduler = data?.scheduler; + const { data: analysisResults = [] } = trpc.analysisResult.list.useQuery( + { taskId: taskId ?? 0 }, + { enabled: taskId != null, refetchInterval: 3000 }, + ); const [selected, setSelected] = useState>(new Set()); const [expanded, setExpanded] = useState>(new Set()); @@ -149,6 +196,18 @@ export default function AlgorithmRunPanel({ taskId, pointCloudLoaded: _pointClou 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); + const demoMeasurementPluginIds = DEMO_MEASUREMENT_PLUGIN_IDS.filter((id) => measPlugins.some((p) => p.id === id)); + const hasDemoRegistrationPlugin = regPlugins.some((p) => p.id === 'vehicle-registration'); + const demoAllPluginIds = [ + ...(hasDemoRegistrationPlugin ? ['vehicle-registration'] : []), + ...demoMeasurementPluginIds, + ]; + const demoAlgorithmCount = demoAllPluginIds.length; + const demoMeasurementAlgorithmCount = demoMeasurementPluginIds.length; + const demoRunTitle = `先执行整车配准,完成后自动提交 ${demoMeasurementAlgorithmCount} 个测量算法`; + const demoPluginIds = new Set(demoAllPluginIds); + const runningDemoResults = analysisResults.filter((r) => (r.status === 'processing' || r.status === 'pending') && r.type === 'plugin' && r.pluginId && demoPluginIds.has(r.pluginId)); + const demoFlowRunning = runningDemoResults.length > 0; const selectedPcCount = selectedPcFileIds?.length ?? 0; const selectedCadCount = selectedCadFileIds?.length ?? 0; @@ -171,7 +230,8 @@ export default function AlgorithmRunPanel({ taskId, pointCloudLoaded: _pointClou const baseRunBlockReason = taskId == null ? '请先选择检测任务' : (taskPcFileCount ?? 0) === 0 ? '请先上传点云文件' - : runBatch.isPending ? '算法提交中,请稍候' + : demoFlowRunning ? '一键全流程运行中,请先停止或等待完成' + : (runBatch.isPending || runDemoAll.isPending || cancelDemoAll.isPending) ? '算法提交中,请稍候' : null; const registrationBlockReason = baseRunBlockReason @@ -187,11 +247,36 @@ export default function AlgorithmRunPanel({ taskId, pointCloudLoaded: _pointClou ?? (selectedMeasIds.length === 0 ? '请至少勾选一个测量算法' : null); const canRunRegistration = registrationBlockReason == null; const canRunMeasurement = measurementBlockReason == null; + const demoBlockReason = + baseRunBlockReason + ?? (selectedPcCount === 0 ? '一键全流程演示必须选择 1 个原始点云' + : selectedPcCount > 1 ? '一键全流程演示一次只能选择 1 个原始点云' + : selectedRegistrationPointcloud?.source === 'registered' ? '请选择原始点云,系统会自动找到对应配准产物' + : selectedCadCount === 0 ? '一键全流程演示必须选择 1 个 CAD 文件' + : selectedCadCount > 1 ? '一键全流程演示一次只能选择 1 个 CAD 文件' + : !hasDemoRegistrationPlugin ? '暂无可执行的整车配准算法' + : demoMeasurementPluginIds.length === 0 ? '暂无可执行的测量算法' + : null); + const canRunDemoAll = demoBlockReason == null; + const demoRouteHint = + selectedPcCount === 1 && selectedRegistrationPointcloud?.source === 'registered' + ? '请选择原始点云,系统会自动找到对应配准产物。' + : selectedPcCount === 1 && selectedRegistrationPointcloud && selectedRegistrationPointcloud.source !== 'registered' && selectedCadCount === 1 + ? `将先执行整车配准,完成后自动提交 ${demoMeasurementAlgorithmCount} 个测量算法` + : null; const duplicateRegistrationWarning = latestRegistrationChild && selectedRegistrationPointcloud ? `点云「${selectedRegistrationPointcloud.name}」已有配准产物「${latestRegistrationChild.name}」(${formatDateTime(latestRegistrationChild.createdAt)}),再次执行会生成新产物。` : null; + const handleSelectAllMeasurement = () => { + setSelected((prev) => { + const next = new Set(prev); + for (const plugin of measPlugins) next.add(plugin.id); + return next; + }); + }; + const handleRun = (ids: string[], mode: 'registration' | 'measurement') => { const blockReason = mode === 'registration' ? registrationBlockReason : measurementBlockReason; if (blockReason) { @@ -232,10 +317,84 @@ export default function AlgorithmRunPanel({ taskId, pointCloudLoaded: _pointClou }); }; + const handleRunDemoAll = () => { + if (demoBlockReason) { + toast.error(demoBlockReason); + return; + } + if (taskId == null || !selectedPcFileIds?.[0] || !selectedCadFileIds?.[0]) return; + setSelected((prev) => { + const next = new Set(prev); + for (const pluginId of demoAllPluginIds) next.add(pluginId); + return next; + }); + runDemoAll.mutate({ + taskId, + sourcePointCloudFileId: selectedPcFileIds[0], + cadFileId: selectedCadFileIds[0], + measurementPluginIds: demoMeasurementPluginIds, + }); + }; + + const handleCancelDemoAll = () => { + if (taskId == null || cancelDemoAll.isPending) return; + cancelDemoAll.mutate({ taskId }); + }; + // 不再需要 showRegWarning 变量,已直接内联到 JSX 里 return (
+ {/* ── 一键演示:整车配准 → 测量算法 ───────────────────────────── */} +
+
+ + + 一键全流程演示 + +
+ {demoFlowRunning && ( + + 运行中 {runningDemoResults.length} + + )} + {demoFlowRunning ? ( + + ) : ( + + )} +
+
+ {demoRouteHint && ( +
+ + {demoRouteHint} +
+ )} +
+ {/* ── 步骤 1:整车配准 ────────────────────────────────── */}
@@ -275,7 +434,7 @@ export default function AlgorithmRunPanel({ taskId, pointCloudLoaded: _pointClou 第 2 步 · 测量分析(多选并发) -
+
{scheduler && ( )} + + )} + {isCancellableStatus(r.status) && ( + + )} + + +
+
+ + {formatStartTime(r.createdAt)} + + {getStatusLabel(r.status)} + + {(r.status === 'processing' || r.status === 'pending') && + (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 ? ( + {pct}% + ) : null; + })()} +
+
+ + {/* 展开内容 */} + {expanded && r.status === 'completed' && r.type !== 'smooth_heatmap' && r.type !== 'assembly_inspection' && r.type !== 'obj_deviation' && r.type !== 'plugin' && ( +
+ {errorStats && } + {measurements && measurements.length > 0 && ( +
+
尺寸测量
+ +
+ )} +
+ )} + + {expanded && + (r.type === 'smooth_heatmap' || r.type === 'assembly_inspection' || r.type === 'obj_deviation' || r.type === 'plugin') && + (r.status === 'processing' || r.status === 'pending') && + (() => { + const hm = r.resultData as SmoothHeatmapMeta | null; + const p = hm?.progress ?? 0; + const stage = hm?.stage ?? '排队或启动中…'; + return ( +
+
{stage}
+
+
+
+

+ 后台子进程执行,页面可继续操作;进度约每 1.2s 刷新。 +

+
+ ); + })()} + + {expanded && r.type === 'smooth_heatmap' && r.status === 'completed' && ( +
+ {(() => { + const hm = r.resultData as SmoothHeatmapMeta | null; + return ( + <> +
+ {hm?.vertexCount != null && ( +
网格顶点约 {hm.vertexCount.toLocaleString()}
+ )} + {hm?.dispMin != null && hm?.dispMax != null && ( +
+ 色标误差约 {hm.dispMin.toFixed(4)} ~ {hm.dispMax.toFixed(4)} mm +
+ )} +
+ + + ); + })()} +
+ )} + + {expanded && r.type === 'smooth_heatmap' && r.status === 'error' && ( +
+

+ {(r.resultData as SmoothHeatmapMeta | null)?.error ?? '分析失败'} +

+
+ )} + + {expanded && r.type === 'assembly_inspection' && r.status === 'completed' && ( +
+ {(() => { + const am = r.resultData as AssemblyMeta | null; + const tol = am?.toleranceMm ?? 0.3; + const parts = am?.parts ?? []; + return ( + <> +
+ 阈值 < {tol} mm 为合格;测点归属为「到各零件网格距离最小」的零件。 +
+
+ + + + + + + + + + + + + {parts.map((p) => ( + + + + + + + + + ))} + +
零件点数minmaxmean合格率
+ {p.name} + {p.pointCount}{p.min.toFixed(4)}{p.max.toFixed(4)}{p.mean.toFixed(4)} + = 0.95 + ? 'text-green-400' + : p.passRate >= 0.85 + ? 'text-amber-400' + : 'text-red-400' + } + > + {(p.passRate * 100).toFixed(1)}% + +
+
+ + ); + })()} +
+ )} + + {expanded && r.type === 'assembly_inspection' && r.status === 'error' && ( +
+

+ {(r.resultData as AssemblyMeta | null)?.error ?? '装配检测失败'} +

+
+ )} + + {expanded && r.type === 'obj_deviation' && r.status === 'completed' && ( +
+ {(() => { + const hm = r.resultData as SmoothHeatmapMeta | null; + return ( + <> +
+
按顶点到最近点云点的距离着色(蓝=近 → 红=远)
+ {hm?.vertexCount != null && ( +
网格顶点约 {hm.vertexCount.toLocaleString()}
+ )} + {hm?.dispMin != null && hm?.dispMax != null && ( +
+ 偏差范围约 {hm.dispMin.toFixed(4)} ~ {hm.dispMax.toFixed(4)} +
+ )} +
+ + + ); + })()} +
+ )} + + {expanded && r.type === 'obj_deviation' && r.status === 'error' && ( +
+

+ {(r.resultData as SmoothHeatmapMeta | null)?.error ?? '偏差分析失败'} +

+
+ )} + + {expanded && r.type === 'plugin' && (r.status === 'completed' || r.status === 'error' || r.status === 'cancelled') && ( +
+ [0]['data']) ?? {}} + onViewOverlay={onViewPluginOverlay} + /> +
+ )} +
+ ); + } + + function renderDemoFlowCard(group: Extract, variant: ResultListVariant) { + const expanded = expandedFlowIds.has(group.runId); + const status = getDemoFlowStatus(group.results); + const total = getDemoFlowTotal(group.results); + const completed = group.results.filter((r) => r.status === 'completed').length; + const createdAt = group.results.reduce((min, r) => Math.min(min, resultTimeMs(r)), Number.POSITIVE_INFINITY); + const runningItems = group.results + .filter((r) => r.status === 'processing' || r.status === 'pending') + .slice(0, 2) + .map((r) => { + const data = asRecord(r.resultData); + const progress = numberFromUnknown(data?.progress); + return `${getResultTitle(r, pluginNameById)}${progress != null ? ` ${Math.round(progress * 100)}%` : ''}`; + }); + + return ( +
+
toggleFlowExpand(group.runId)} + > + {expanded ? : } + + 全流程算法结果 + + {completed}/{total} 完成 + + + {Number.isFinite(createdAt) ? new Date(createdAt).toLocaleDateString('zh-CN') : ''} + + {getStatusLabel(status)} +
+ {expanded && ( +
+ {runningItems.length > 0 && ( +
+ 运行中:{runningItems.join('、')} +
+ )} +
+ {group.results.map((result) => renderResultCard(result))} +
+
+ )} +
+ ); + } + + function renderResultList(variant: ResultListVariant) { + return ( +
+ {listItems.map((item) => ( + item.kind === 'demoFlow' + ? renderDemoFlowCard(item, variant) + : renderResultCard(item.result) + ))} +
+ ); + } return (
@@ -205,6 +744,14 @@ export default function AnalysisPanel({ taskId, onReplay, onViewSmoothHeatmap, o 分析结果
+
)} -
- {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 }); + {!isLoading && results.length > 0 && renderResultList('panel')} - return ( -
- {/* 标题行 */} -
toggleExpand(r.id)} - > - {expanded ? : } - {r.type === 'error_analysis' && } - {r.type === 'measurement' && } - {r.type === 'compression' && } - {r.type === 'smooth_heatmap' && } - {r.type === 'assembly_inspection' && } - {r.type === 'obj_deviation' && } - {r.type === 'plugin' && } - - - {resultTitle} - - - {resultTitle} - - - - {new Date(r.createdAt).toLocaleDateString('zh-CN')} - - {r.status} - - {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 ? ( - {pct}% - ) : null; - })()} - - - {onReplay && r.status === 'completed' && ( - - )} - - -
- - {/* 展开内容 */} - {expanded && r.status === 'completed' && r.type !== 'smooth_heatmap' && r.type !== 'assembly_inspection' && r.type !== 'obj_deviation' && r.type !== 'plugin' && ( -
- {errorStats && } - {measurements && measurements.length > 0 && ( -
-
尺寸测量
- -
- )} -
- )} - - {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 ( -
-
{stage}
-
-
-
-

- 后台子进程执行,页面可继续操作;进度约每 1.2s 刷新。 -

-
- ); - })()} - - {expanded && r.type === 'smooth_heatmap' && r.status === 'completed' && ( -
- {(() => { - const hm = r.resultData as SmoothHeatmapMeta | null; - return ( - <> -
- {hm?.vertexCount != null && ( -
网格顶点约 {hm.vertexCount.toLocaleString()}
- )} - {hm?.dispMin != null && hm?.dispMax != null && ( -
- 色标误差约 {hm.dispMin.toFixed(4)} ~ {hm.dispMax.toFixed(4)} mm -
- )} -
- - - ); - })()} -
- )} - - {expanded && r.type === 'smooth_heatmap' && r.status === 'error' && ( -
-

- {(r.resultData as SmoothHeatmapMeta | null)?.error ?? '分析失败'} -

-
- )} - - {expanded && r.type === 'assembly_inspection' && r.status === 'completed' && ( -
- {(() => { - const am = r.resultData as AssemblyMeta | null; - const tol = am?.toleranceMm ?? 0.3; - const parts = am?.parts ?? []; - return ( - <> -
- 阈值 < {tol} mm 为合格;测点归属为「到各零件网格距离最小」的零件。 -
-
- - - - - - - - - - - - - {parts.map((p) => ( - - - - - - - - - ))} - -
零件点数minmaxmean合格率
- {p.name} - {p.pointCount}{p.min.toFixed(4)}{p.max.toFixed(4)}{p.mean.toFixed(4)} - = 0.95 - ? 'text-green-400' - : p.passRate >= 0.85 - ? 'text-amber-400' - : 'text-red-400' - } - > - {(p.passRate * 100).toFixed(1)}% - -
-
- - ); - })()} -
- )} - - {expanded && r.type === 'assembly_inspection' && r.status === 'error' && ( -
-

- {(r.resultData as AssemblyMeta | null)?.error ?? '装配检测失败'} -

-
- )} - - {expanded && r.type === 'obj_deviation' && r.status === 'completed' && ( -
- {(() => { - const hm = r.resultData as SmoothHeatmapMeta | null; - return ( - <> -
-
按顶点到最近点云点的距离着色(蓝=近 → 红=远)
- {hm?.vertexCount != null && ( -
网格顶点约 {hm.vertexCount.toLocaleString()}
- )} - {hm?.dispMin != null && hm?.dispMax != null && ( -
- 偏差范围约 {hm.dispMin.toFixed(4)} ~ {hm.dispMax.toFixed(4)} -
- )} -
- - - ); - })()} -
- )} - - {expanded && r.type === 'obj_deviation' && r.status === 'error' && ( -
-

- {(r.resultData as SmoothHeatmapMeta | null)?.error ?? '偏差分析失败'} -

-
- )} - - {expanded && r.type === 'plugin' && (r.status === 'completed' || r.status === 'error') && ( -
- [0]['data']) ?? {}} - onViewOverlay={onViewPluginOverlay} - /> -
- )} -
- ); - })} -
+ + + + + + 分析结果 + + +
+ {isLoading && ( +
加载中…
+ )} + {!isLoading && results.length === 0 && ( +
暂无分析结果
+ )} + {!isLoading && results.length > 0 && renderResultList('maximized')} +
+
+
); } diff --git a/client/src/components/GenericResultView.tsx b/client/src/components/GenericResultView.tsx index 6fbbfa1..b61abe2 100644 --- a/client/src/components/GenericResultView.tsx +++ b/client/src/components/GenericResultView.tsx @@ -100,6 +100,7 @@ function formatValue(v: number): string { export default function GenericResultView({ taskId, resultId, + pluginId, pluginName, data, onViewOverlay, @@ -112,13 +113,13 @@ export default function GenericResultView({ typeof f === 'string' ? f : (f as { name?: string; path?: string }).name ?? String(f) ); const overlay = data.overlay3d ?? { kind: 'none' }; + const displayPluginName = pluginName ?? pluginId ?? data.pluginId; return (
- {pluginName && ( + {displayPluginName && (
- {pluginName} - {data.pluginId ? ` · ${data.pluginId}` : ''} + {displayPluginName} {data.schemaVersion != null ? ` · schema v${data.schemaVersion}` : ''}
)} diff --git a/client/src/components/TaskManager.tsx b/client/src/components/TaskManager.tsx index dbee685..020e9cc 100644 --- a/client/src/components/TaskManager.tsx +++ b/client/src/components/TaskManager.tsx @@ -16,6 +16,7 @@ import { Library, } from 'lucide-react'; import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip'; +import { toast } from 'sonner'; interface Props { modelId: number; @@ -46,6 +47,31 @@ function formatDateTime(value?: string | Date) { return date.toLocaleString('zh-CN', { hour12: false }); } +type InspectionTaskStatus = 'created' | 'pending' | 'processing' | 'completed' | 'error'; + +const taskStatusLabels: Record = { + created: '未开始', + pending: '待处理', + processing: '处理中', + completed: '已完成', + error: '异常', +}; + +function taskStatusClass(status: InspectionTaskStatus) { + switch (status) { + case 'completed': + return 'bg-green-900/40 text-green-400'; + case 'processing': + case 'pending': + return 'bg-blue-900/40 text-blue-400'; + case 'error': + return 'bg-red-900/40 text-red-400'; + case 'created': + default: + return 'bg-secondary text-muted-foreground'; + } +} + export default function TaskManager({ modelId, activeTaskId, @@ -82,9 +108,21 @@ export default function TaskManager({ const [selectedModelCadFileIds, setSelectedModelCadFileIds] = useState>(new Set()); const [uploadingModelCad, setUploadingModelCad] = useState(false); - const { data: tasks = [] } = trpc.inspectionTask.listByModel.useQuery({ modelId }); + const { data: tasks = [] } = trpc.inspectionTask.listByModel.useQuery( + { modelId }, + { + refetchInterval: 3000, + refetchIntervalInBackground: false, + }, + ); const createTask = trpc.inspectionTask.create.useMutation({ - onSuccess: () => utils.inspectionTask.listByModel.invalidate({ modelId }), + onSuccess: () => { + utils.inspectionTask.listByModel.invalidate({ modelId }); + setTaskName(''); + setAdding(false); + onEndCreate?.(); + }, + onError: (e) => toast.error(e.message || '新建任务失败'), }); const deleteTask = trpc.inspectionTask.delete.useMutation({ onSuccess: () => utils.inspectionTask.listByModel.invalidate({ modelId }), @@ -187,6 +225,12 @@ export default function TaskManager({ fileRef.current?.click(); } + function submitCreateTask() { + const name = taskName.trim(); + if (!name || createTask.isPending) return; + createTask.mutate({ modelId, name }); + } + async function handleUpload(files: FileList) { const taskId = pendingTaskId.current; if (!taskId) return; @@ -373,10 +417,7 @@ export default function TaskManager({ onChange={(e) => setTaskName(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter' && taskName.trim()) { - createTask.mutate({ modelId, name: taskName.trim() }); - setTaskName(''); - setAdding(false); - onEndCreate?.(); + submitCreateTask(); } if (e.key === 'Escape') { setAdding(false); @@ -386,11 +427,8 @@ export default function TaskManager({ /> @@ -407,52 +445,47 @@ export default function TaskManager({ )}
- {tasks.map((task) => ( -
onSelectTask(task.id)} - > - - {task.name} - { + const status = task.status as InspectionTaskStatus; + return ( +
onSelectTask(task.id)} > - {task.status} - - - -
- ))} + + {task.name} + + {taskStatusLabels[status] ?? status} + + + +
+ ); + })}
{tasks.length === 0 && ( diff --git a/client/src/components/WorkflowProgress.tsx b/client/src/components/WorkflowProgress.tsx deleted file mode 100644 index 0d18f9d..0000000 --- a/client/src/components/WorkflowProgress.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { type ElementType } from 'react'; -import { CheckCircle2, Loader2, FolderOpen, Cloud, Box, BarChart3, Save } from 'lucide-react'; - -export type WorkflowStepId = 'cad' | 'pointcloud' | 'render' | 'analysis' | 'save'; - -export type StepStatus = 'done' | 'active' | 'pending'; - -export interface WorkflowState { - cad: StepStatus; - pointcloud: StepStatus; - render: StepStatus; - analysis: StepStatus; - save: StepStatus; -} - -interface StepDef { - id: WorkflowStepId; - label: string; - icon: ElementType; -} - -const STEPS: StepDef[] = [ - { id: 'cad', label: 'CAD 模型', icon: Box }, - { id: 'pointcloud', label: '点云采集', icon: Cloud }, - { id: 'render', label: '渲染完成', icon: FolderOpen }, - { id: 'analysis', label: '分析完成', icon: BarChart3 }, - { id: 'save', label: '保存完成', icon: Save }, -]; - -function StepNode({ - step, - status, - onClick, -}: { - step: StepDef; - status: StepStatus; - onClick?: () => void; -}) { - const Icon = step.icon; - - const circleClass = - status === 'done' - ? 'bg-[oklch(0.55_0.18_145)] border-[oklch(0.55_0.18_145)] text-white' - : status === 'active' - ? 'bg-primary/20 border-primary text-primary' - : 'bg-transparent border-border text-muted-foreground/40'; - - const labelClass = - status === 'done' - ? 'text-[oklch(0.75_0.14_145)]' - : status === 'active' - ? 'text-primary' - : 'text-muted-foreground/40'; - - return ( - - ); -} - -function Connector({ done }: { done: boolean }) { - return ( -
-
-
- ); -} - -interface Props { - state: WorkflowState; - onStepClick?: (id: WorkflowStepId) => void; -} - -export default function WorkflowProgress({ state, onStepClick }: Props) { - return ( -
- - 工作流程 - -
- {STEPS.map((step, i) => ( -
- onStepClick(step.id) : undefined} - /> - {i < STEPS.length - 1 && } -
- ))} -
-
- ); -} diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx index 00c2762..12d073f 100644 --- a/client/src/pages/Home.tsx +++ b/client/src/pages/Home.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useEffect, useRef, useMemo } from 'react'; +import { useState, useCallback, useEffect, useMemo } from 'react'; import { useAuth } from '@/_core/hooks/useAuth'; import { getLoginUrl } from '@/const'; import PointCloudViewer from '@/components/PointCloudViewer'; @@ -7,7 +7,6 @@ import TaskManager from '@/components/TaskManager'; import AnalysisPanel from '@/components/AnalysisPanel'; import AlgorithmRunPanel from '@/components/AlgorithmRunPanel'; import type { Overlay3D } from '@/components/GenericResultView'; -import WorkflowProgress, { type WorkflowState, type WorkflowStepId, type StepStatus } from '@/components/WorkflowProgress'; import { trpc } from '@/lib/trpc'; import { LogIn, @@ -90,11 +89,6 @@ export default function Home() { overlay: Overlay3D; } | null>(null); const [errorPickEnabled, setErrorPickEnabled] = useState(false); - const [isCreatingTask, setIsCreatingTask] = useState(false); - // 触发分析时的 completed 结果基线数(key=taskId, value=触发时已有的 completed 数) - const [analysisBaselines, setAnalysisBaselines] = useState>(new Map()); - // 用 ref 在 onSuccess 闭包中读取最新 analysisResults,避免过时闭包 - const analysisResultsRef = useRef([]); const [showLeftPanel, setShowLeftPanel] = useState(true); const [showRightPanel, setShowRightPanel] = useState(true); @@ -122,13 +116,6 @@ export default function Home() { toast.success('已提交偏差热力分析(STL/OBJ + 点云距离),完成后点击「在 3D 中查看」'); } void utils.analysisResult.list.invalidate({ taskId: variables.taskId }); - // 记录此时已有的 completed 数作为基线,只有新增结果才算本次分析完成 - const baselineCount = analysisResultsRef.current.filter((r: any) => r.status === 'completed').length; - setAnalysisBaselines(prev => { - const next = new Map(prev); - if (!next.has(variables.taskId)) next.set(variables.taskId, baselineCount); - return next; - }); }, onError: (e) => toast.error(e.message || '提交失败'), }); @@ -146,7 +133,6 @@ export default function Home() { setHeatmapViewResultId(null); setPluginOverlay(null); setPointCloudRenderMode('points'); - setIsCreatingTask(false); setSelectedPcFileIds([]); setSelectedTaskCadFileIds([]); setRoiPickMode(false); @@ -253,57 +239,6 @@ export default function Home() { // ── 把 __task:N 格式的 URL 转成 batch 请求参数 ────────────────────────────── const batchTaskId = pointCloudTaskId; - // ── 工作流进度条状态推导 ─────────────────────────────────────────────────── - const { data: analysisResults = [] } = trpc.analysisResult.list.useQuery( - { taskId: selection.taskId! }, - { enabled: selection.taskId != null, refetchInterval: 5000 }, - ); - - useEffect(() => { analysisResultsRef.current = analysisResults; }, [analysisResults]); - - const workflowState = useMemo(() => { - const hasCad = !!cadModelUrl; - const hasPc = pointCloudTaskId != null; - const rendered = hasPc && !pcLoading && pcProgress >= 100; - const baseline = (!isCreatingTask && selection.taskId != null) - ? (analysisBaselines.get(selection.taskId) ?? -1) - : -1; - const triggered = baseline >= 0; - const currentCompleted = (analysisResults as any[]).filter((r: any) => r.status === 'completed').length; - const analyzed = triggered && currentCompleted > baseline; - - function step(cond: boolean, prev: boolean): 'done' | 'active' | 'pending' { - if (cond) return 'done'; - if (prev) return 'active'; - return 'pending'; - } - - // 分析步骤:只在 触发后未完成 时转圈;保存步骤:分析完成即完成,无中间态 - const analysisStatus: StepStatus = analyzed ? 'done' : triggered ? 'active' : 'pending'; - const saveStatus: StepStatus = analyzed ? 'done' : 'pending'; - - return { - cad: step(hasCad, true), - pointcloud: step(hasPc, hasCad), - render: step(rendered, hasPc), - analysis: analysisStatus, - save: saveStatus, - }; - }, [cadModelUrl, pointCloudTaskId, pcLoading, pcProgress, analysisResults, isCreatingTask, analysisBaselines, selection.taskId]); - - const rightPanelRef = useRef(null); - - const handleWorkflowStepClick = useCallback((id: WorkflowStepId) => { - if (id === 'cad' || id === 'pointcloud') { - setShowLeftPanel(true); - } else if (id === 'render') { - if (selection.taskId != null && pointCloudTaskId !== selection.taskId) { - handleLoadPointClouds(selection.taskId, []); - } - } else if (id === 'analysis' || id === 'save') { - setShowRightPanel(true); - } - }, [selection.taskId, pointCloudTaskId, handleLoadPointClouds]); // 必须稳定数组引用:若每次 render 都 new [],PointCloudViewer 会以为点云源变了而反复全量请求 const pointCloudUrlsStable = useMemo( () => (pointCloudTaskId != null ? [`__task:${pointCloudTaskId}`] : undefined), @@ -329,8 +264,6 @@ export default function Home() { onPcFilesSelectionChange={setSelectedPcFileIds} onCadFilesSelectionChange={setSelectedTaskCadFileIds} onSelectTaskCad={handleSelectCad} - onStartCreate={() => setIsCreatingTask(true)} - onEndCreate={() => setIsCreatingTask(false)} /> ); @@ -350,6 +283,7 @@ export default function Home() {
0} taskPcFileCount={taskPcFiles.length} taskPointcloudFiles={taskPcFiles} @@ -481,13 +415,6 @@ export default function Home() { {/* 中央 3D 视图 */}
- {/* 工作流进度条(仅在选中任务时显示) */} - {selection.taskId != null && ( - - )}
@@ -670,7 +597,7 @@ export default function Home() { {/* 右侧面板 - 分析结果 */} {showRightPanel && ( -