feat: update task file selection UI
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
* - 多选执行时调用 `algorithm.runBatch` 一次性把所有作业丢进后端并发池
|
||||
* (默认 4 并发),20 分钟内出结果(工信部验收要求)。
|
||||
*/
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { trpc } from '@/lib/trpc';
|
||||
import { AlertTriangle, CheckCircle2, ChevronDown, ChevronRight, Cpu, Play, RefreshCw, Scan } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
@@ -42,12 +42,23 @@ interface PluginSummary {
|
||||
runtimeKind: 'process' | 'mock';
|
||||
}
|
||||
|
||||
interface PointcloudFileSummary {
|
||||
id: number;
|
||||
name: string;
|
||||
source?: string;
|
||||
sourceResultId?: number | null;
|
||||
sourcePointcloudFileId?: number | null;
|
||||
createdAt?: string | Date;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
taskId: number | null;
|
||||
/** 是否已加载点云(3D 视图,仅用于提示,不再作为执行前置条件) */
|
||||
pointCloudLoaded: boolean;
|
||||
/** 当前任务下的点云文件总数(>0 即可执行算法,无需先在 3D 视图加载) */
|
||||
taskPcFileCount?: number;
|
||||
/** 当前任务下的点云文件元数据(用于配准输入校验与重复配准确认) */
|
||||
taskPointcloudFiles?: PointcloudFileSummary[];
|
||||
/** 当前任务中用户选中的点云文件 ID(空/undefined = 全部) */
|
||||
selectedPcFileIds?: number[];
|
||||
/** 当前任务中选中的点云中是否包含配准后的点云 */
|
||||
@@ -60,6 +71,13 @@ interface Props {
|
||||
activeRoiPickTarget?: { taskIdx: number; roiKey: string; corner: string } | null;
|
||||
}
|
||||
|
||||
function formatDateTime(value?: string | Date) {
|
||||
if (!value) return '未知时间';
|
||||
const date = value instanceof Date ? value : new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return '未知时间';
|
||||
return date.toLocaleString('zh-CN', { hour12: false });
|
||||
}
|
||||
|
||||
function defaultParamsFor(plugin: PluginSummary): Record<string, unknown> {
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const field of plugin.inputs) {
|
||||
@@ -68,7 +86,7 @@ function defaultParamsFor(plugin: PluginSummary): Record<string, unknown> {
|
||||
return out;
|
||||
}
|
||||
|
||||
export default function AlgorithmRunPanel({ taskId, pointCloudLoaded: _pointCloudLoaded, taskPcFileCount, selectedPcFileIds, hasRegisteredCloud, selectedCadFileIds, roiPickedPoint, onStartRoiPick, activeRoiPickTarget }: Props) {
|
||||
export default function AlgorithmRunPanel({ taskId, 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,
|
||||
@@ -78,6 +96,7 @@ export default function AlgorithmRunPanel({ taskId, pointCloudLoaded: _pointClou
|
||||
toast.success(`已提交 ${jobs.length} 个算法到并发池,进度请查看下方「分析结果」`);
|
||||
if (taskId != null) {
|
||||
void utils.analysisResult.list.invalidate({ taskId });
|
||||
void utils.pointcloudFile.list.invalidate({ taskId });
|
||||
}
|
||||
},
|
||||
onError: (e) => toast.error(e.message || '提交失败'),
|
||||
@@ -125,14 +144,81 @@ export default function AlgorithmRunPanel({ taskId, pointCloudLoaded: _pointClou
|
||||
}));
|
||||
};
|
||||
|
||||
const canRun = useMemo(
|
||||
// 任务下有点云文件(taskPcFileCount > 0)即可执行,无需先在 3D 视图加载
|
||||
() => taskId != null && (taskPcFileCount ?? 0) > 0 && selected.size > 0 && !runBatch.isPending,
|
||||
[taskId, taskPcFileCount, selected, runBatch.isPending],
|
||||
);
|
||||
// 区分配准插件 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);
|
||||
|
||||
const handleRun = (ids: string[]) => {
|
||||
if (!canRun || taskId == null) return;
|
||||
const selectedPcCount = selectedPcFileIds?.length ?? 0;
|
||||
const selectedCadCount = selectedCadFileIds?.length ?? 0;
|
||||
const selectedRegistrationPointcloud =
|
||||
selectedPcCount === 1
|
||||
? taskPointcloudFiles.find((f) => f.id === selectedPcFileIds?.[0])
|
||||
: undefined;
|
||||
const latestRegistrationChild = selectedRegistrationPointcloud?.source !== 'registered'
|
||||
? taskPointcloudFiles
|
||||
.filter((f) => (
|
||||
f.source === 'registered'
|
||||
&& f.sourcePointcloudFileId === selectedRegistrationPointcloud?.id
|
||||
))
|
||||
.sort((a, b) => {
|
||||
const aTime = a.createdAt ? new Date(a.createdAt).getTime() : 0;
|
||||
const bTime = b.createdAt ? new Date(b.createdAt).getTime() : 0;
|
||||
return bTime - aTime;
|
||||
})[0]
|
||||
: undefined;
|
||||
const baseRunBlockReason =
|
||||
taskId == null ? '请先选择检测任务'
|
||||
: (taskPcFileCount ?? 0) === 0 ? '请先上传点云文件'
|
||||
: runBatch.isPending ? '算法提交中,请稍候'
|
||||
: null;
|
||||
const registrationBlockReason =
|
||||
baseRunBlockReason
|
||||
?? (selectedRegIds.length === 0 ? '请勾选配准算法'
|
||||
: selectedPcCount === 0 ? '整车配准必须选择 1 个点云文件'
|
||||
: selectedPcCount > 1 ? '整车配准一次只能选择 1 个点云文件'
|
||||
: selectedRegistrationPointcloud?.source === 'registered' ? '请选择原始点云进行配准,已配准点云不能再次配准'
|
||||
: selectedCadCount === 0 ? '整车配准必须选择 1 个 CAD 文件'
|
||||
: selectedCadCount > 1 ? '整车配准一次只能选择 1 个 CAD 文件'
|
||||
: null);
|
||||
const measurementBlockReason =
|
||||
baseRunBlockReason
|
||||
?? (selectedMeasIds.length === 0 ? '请至少勾选一个测量算法' : null);
|
||||
const canRunRegistration = registrationBlockReason == null;
|
||||
const canRunMeasurement = measurementBlockReason == null;
|
||||
const duplicateRegistrationWarning =
|
||||
latestRegistrationChild && selectedRegistrationPointcloud
|
||||
? `点云「${selectedRegistrationPointcloud.name}」已有配准产物「${latestRegistrationChild.name}」(${formatDateTime(latestRegistrationChild.createdAt)}),再次执行会生成新产物。`
|
||||
: null;
|
||||
|
||||
const handleRun = (ids: string[], mode: 'registration' | 'measurement') => {
|
||||
const blockReason = mode === 'registration' ? registrationBlockReason : measurementBlockReason;
|
||||
if (blockReason) {
|
||||
toast.error(blockReason);
|
||||
return;
|
||||
}
|
||||
if (taskId == null) return;
|
||||
const allowDuplicateRegistration =
|
||||
mode === 'registration' && duplicateRegistrationWarning
|
||||
? window.confirm(`${duplicateRegistrationWarning}\n\n是否确认重新配准?`)
|
||||
: false;
|
||||
if (mode === 'registration' && duplicateRegistrationWarning && !allowDuplicateRegistration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pointCloudFileIds =
|
||||
mode === 'registration'
|
||||
? selectedPcFileIds
|
||||
: selectedPcFileIds && selectedPcFileIds.length > 0
|
||||
? selectedPcFileIds
|
||||
: undefined;
|
||||
const cadFileIds =
|
||||
mode === 'registration'
|
||||
? selectedCadFileIds
|
||||
: selectedCadFileIds && selectedCadFileIds.length > 0
|
||||
? selectedCadFileIds
|
||||
: undefined;
|
||||
const jobs = ids.map((pluginId) => ({
|
||||
pluginId,
|
||||
params: paramsById[pluginId] ?? {},
|
||||
@@ -140,21 +226,12 @@ export default function AlgorithmRunPanel({ taskId, pointCloudLoaded: _pointClou
|
||||
runBatch.mutate({
|
||||
taskId,
|
||||
jobs,
|
||||
...(selectedPcFileIds && selectedPcFileIds.length > 0
|
||||
? { pointCloudFileIds: selectedPcFileIds }
|
||||
: {}),
|
||||
...(selectedCadFileIds && selectedCadFileIds.length > 0
|
||||
? { cadFileIds: selectedCadFileIds }
|
||||
: {}),
|
||||
...(pointCloudFileIds ? { pointCloudFileIds } : {}),
|
||||
...(cadFileIds ? { cadFileIds } : {}),
|
||||
...(allowDuplicateRegistration ? { allowDuplicateRegistration } : {}),
|
||||
});
|
||||
};
|
||||
|
||||
// 区分配准插件 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 (
|
||||
@@ -169,11 +246,11 @@ export default function AlgorithmRunPanel({ taskId, pointCloudLoaded: _pointClou
|
||||
<button
|
||||
type="button"
|
||||
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'
|
||||
canRunRegistration ? '' : 'opacity-50 cursor-not-allowed'
|
||||
}`}
|
||||
disabled={!canRun || selectedRegIds.length === 0}
|
||||
onClick={() => handleRun(selectedRegIds)}
|
||||
title={selectedRegIds.length === 0 ? '请勾选配准算法' : '执行配准'}
|
||||
disabled={!canRunRegistration}
|
||||
onClick={() => handleRun(selectedRegIds, 'registration')}
|
||||
title={registrationBlockReason ?? '执行配准'}
|
||||
>
|
||||
{runBatch.isPending ? <RefreshCw className="w-3 h-3 animate-spin" /> : <Play className="w-3 h-3" />}
|
||||
执行({selectedRegIds.length})
|
||||
@@ -182,6 +259,18 @@ export default function AlgorithmRunPanel({ taskId, pointCloudLoaded: _pointClou
|
||||
{regPlugins.length === 0 && !isLoading && (
|
||||
<div className="text-[10px] text-muted-foreground italic px-1">暂无配准插件</div>
|
||||
)}
|
||||
{selectedRegIds.length > 0 && registrationBlockReason && !baseRunBlockReason && (
|
||||
<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">{registrationBlockReason}</span>
|
||||
</div>
|
||||
)}
|
||||
{selectedRegIds.length > 0 && !registrationBlockReason && duplicateRegistrationWarning && (
|
||||
<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">{duplicateRegistrationWarning}</span>
|
||||
</div>
|
||||
)}
|
||||
{regPlugins.map((p) => (
|
||||
<PluginCard
|
||||
key={p.id}
|
||||
@@ -219,16 +308,11 @@ export default function AlgorithmRunPanel({ taskId, pointCloudLoaded: _pointClou
|
||||
<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'
|
||||
canRunMeasurement ? '' : 'opacity-50 cursor-not-allowed'
|
||||
}`}
|
||||
disabled={!canRun || selectedMeasIds.length === 0}
|
||||
onClick={() => handleRun(selectedMeasIds)}
|
||||
title={
|
||||
taskId == null ? '请先选择检测任务'
|
||||
: (taskPcFileCount ?? 0) === 0 ? '请先上传点云文件'
|
||||
: selectedMeasIds.length === 0 ? '请至少勾选一个测量算法'
|
||||
: '并发执行已勾选算法'
|
||||
}
|
||||
disabled={!canRunMeasurement}
|
||||
onClick={() => handleRun(selectedMeasIds, 'measurement')}
|
||||
title={measurementBlockReason ?? '并发执行已勾选算法'}
|
||||
>
|
||||
{runBatch.isPending ? <RefreshCw className="w-3 h-3 animate-spin" /> : <Play className="w-3 h-3" />}
|
||||
执行({selectedMeasIds.length})
|
||||
|
||||
@@ -2,6 +2,7 @@ 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 {
|
||||
@@ -162,6 +163,16 @@ type AssemblyMeta = SmoothHeatmapMeta & {
|
||||
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());
|
||||
@@ -243,12 +254,13 @@ export default function AnalysisPanel({ taskId, onReplay, onViewSmoothHeatmap, o
|
||||
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">
|
||||
<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"
|
||||
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" />}
|
||||
@@ -259,21 +271,18 @@ export default function AnalysisPanel({ taskId, onReplay, onViewSmoothHeatmap, o
|
||||
{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" />}
|
||||
<span className="text-xs flex-1 truncate">
|
||||
{r.type === 'error_analysis'
|
||||
? '误差分析'
|
||||
: r.type === 'measurement'
|
||||
? '尺寸测量'
|
||||
: r.type === 'smooth_heatmap'
|
||||
? '平滑热力'
|
||||
: r.type === 'assembly_inspection'
|
||||
? '装配检测'
|
||||
: r.type === 'obj_deviation'
|
||||
? '偏差热力'
|
||||
: r.type === 'plugin'
|
||||
? `算法 · ${(r as { pluginId?: string }).pluginId ?? '插件'}`
|
||||
: '压缩'}
|
||||
</span>
|
||||
<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>
|
||||
|
||||
@@ -13,7 +13,9 @@ import {
|
||||
Square,
|
||||
FileBox,
|
||||
Eye,
|
||||
Library,
|
||||
} from 'lucide-react';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
|
||||
|
||||
interface Props {
|
||||
modelId: number;
|
||||
@@ -37,6 +39,13 @@ function formatSize(bytes: number) {
|
||||
return (bytes / 1e9).toFixed(2) + ' GB';
|
||||
}
|
||||
|
||||
function formatDateTime(value?: string | Date) {
|
||||
if (!value) return '未知时间';
|
||||
const date = value instanceof Date ? value : new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return '未知时间';
|
||||
return date.toLocaleString('zh-CN', { hour12: false });
|
||||
}
|
||||
|
||||
export default function TaskManager({
|
||||
modelId,
|
||||
activeTaskId,
|
||||
@@ -54,7 +63,11 @@ export default function TaskManager({
|
||||
const [uploadingFor, setUploadingFor] = useState<number | null>(null);
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
const modelPointcloudFileRef = useRef<HTMLInputElement>(null);
|
||||
const pendingTaskId = useRef<number | null>(null);
|
||||
const [pcLibraryOpen, setPcLibraryOpen] = useState(false);
|
||||
const [selectedModelPcFileIds, setSelectedModelPcFileIds] = useState<Set<number>>(new Set());
|
||||
const [uploadingModelPointcloud, setUploadingModelPointcloud] = useState(false);
|
||||
|
||||
// ── 点云文件多选状态 ──────────────────────────────────────────────────────
|
||||
const [selectedFileIds, setSelectedFileIds] = useState<Set<number>>(new Set());
|
||||
@@ -62,8 +75,12 @@ export default function TaskManager({
|
||||
// ── 任务级 CAD 文件多选状态 ───────────────────────────────────────────────
|
||||
const [selectedCadFileIds, setSelectedCadFileIds] = useState<Set<number>>(new Set());
|
||||
const cadFileRef = useRef<HTMLInputElement>(null);
|
||||
const modelCadFileRef = useRef<HTMLInputElement>(null);
|
||||
const [uploadingCadFor, setUploadingCadFor] = useState<number | null>(null);
|
||||
const [cadUploadProgress, setCadUploadProgress] = useState(0);
|
||||
const [cadLibraryOpen, setCadLibraryOpen] = useState(false);
|
||||
const [selectedModelCadFileIds, setSelectedModelCadFileIds] = useState<Set<number>>(new Set());
|
||||
const [uploadingModelCad, setUploadingModelCad] = useState(false);
|
||||
|
||||
const { data: tasks = [] } = trpc.inspectionTask.listByModel.useQuery({ modelId });
|
||||
const createTask = trpc.inspectionTask.create.useMutation({
|
||||
@@ -81,6 +98,16 @@ export default function TaskManager({
|
||||
const deletePcFile = trpc.pointcloudFile.delete.useMutation({
|
||||
onSuccess: () => utils.pointcloudFile.list.invalidate({ taskId: activeTaskId! }),
|
||||
});
|
||||
const { data: modelPcFiles = [] } = trpc.modelPointcloudFile.list.useQuery(
|
||||
{ modelId },
|
||||
{ enabled: activeTaskId != null },
|
||||
);
|
||||
const linkPcFilesFromModel = trpc.pointcloudFile.linkFromModel.useMutation({
|
||||
onSuccess: () => {
|
||||
if (activeTaskId != null) utils.pointcloudFile.list.invalidate({ taskId: activeTaskId });
|
||||
setSelectedModelPcFileIds(new Set());
|
||||
},
|
||||
});
|
||||
|
||||
// 当前选中任务下的 CAD 文件
|
||||
const { data: taskCadFiles = [] } = trpc.taskCadFile.list.useQuery(
|
||||
@@ -90,6 +117,16 @@ export default function TaskManager({
|
||||
const deleteTaskCadFile = trpc.taskCadFile.delete.useMutation({
|
||||
onSuccess: () => utils.taskCadFile.list.invalidate({ taskId: activeTaskId! }),
|
||||
});
|
||||
const { data: modelCadFiles = [] } = trpc.cadFile.list.useQuery(
|
||||
{ modelId },
|
||||
{ enabled: activeTaskId != null },
|
||||
);
|
||||
const linkCadFilesFromModel = trpc.taskCadFile.linkFromModel.useMutation({
|
||||
onSuccess: () => {
|
||||
if (activeTaskId != null) utils.taskCadFile.list.invalidate({ taskId: activeTaskId });
|
||||
setSelectedModelCadFileIds(new Set());
|
||||
},
|
||||
});
|
||||
|
||||
// CAD 三角化采样缓存状态(key: cadFileId, value: 是否已缓存)
|
||||
const cadFileIds = taskCadFiles.map((f) => f.id);
|
||||
@@ -102,6 +139,10 @@ export default function TaskManager({
|
||||
useEffect(() => {
|
||||
setSelectedFileIds(new Set());
|
||||
setSelectedCadFileIds(new Set());
|
||||
setSelectedModelPcFileIds(new Set());
|
||||
setSelectedModelCadFileIds(new Set());
|
||||
setPcLibraryOpen(false);
|
||||
setCadLibraryOpen(false);
|
||||
}, [activeTaskId]);
|
||||
|
||||
// 通知父组件点云选中变化
|
||||
@@ -133,6 +174,14 @@ export default function TaskManager({
|
||||
});
|
||||
}
|
||||
|
||||
function toggleModelPcFile(id: number) {
|
||||
setSelectedModelPcFileIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.has(id) ? next.delete(id) : next.add(id);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
function openUpload(taskId: number) {
|
||||
pendingTaskId.current = taskId;
|
||||
fileRef.current?.click();
|
||||
@@ -172,6 +221,27 @@ export default function TaskManager({
|
||||
}
|
||||
}
|
||||
|
||||
async function handleModelPointcloudUpload(files: FileList) {
|
||||
setUploadingModelPointcloud(true);
|
||||
try {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const fd = new FormData();
|
||||
fd.append('file', files[i]);
|
||||
const res = await fetch(`/api/files/upload/model-pointcloud/${modelId}`, {
|
||||
method: 'POST',
|
||||
body: fd,
|
||||
});
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
}
|
||||
utils.modelPointcloudFile.list.invalidate({ modelId });
|
||||
} catch (e) {
|
||||
console.error('Model pointcloud upload error', e);
|
||||
alert('上传失败: ' + (e as Error).message);
|
||||
} finally {
|
||||
setUploadingModelPointcloud(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载按钮:有勾选就只加载勾选的,否则加载全部
|
||||
function handleLoad() {
|
||||
if (activeTaskId == null || pcFiles.length === 0) return;
|
||||
@@ -198,6 +268,28 @@ export default function TaskManager({
|
||||
});
|
||||
}
|
||||
|
||||
function toggleModelCadFile(id: number) {
|
||||
setSelectedModelCadFileIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.has(id) ? next.delete(id) : next.add(id);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
function taskCadReadUrl(file: { name: string; source?: string }) {
|
||||
const encoded = encodeURIComponent(file.name);
|
||||
if (file.source === 'model_library') {
|
||||
return `/api/files/read/cad/${modelId}/${encoded}`;
|
||||
}
|
||||
return `/api/files/read/task-cad/${activeTaskId}/${encoded}`;
|
||||
}
|
||||
|
||||
function canPreviewCadIn3D(file: { name: string; format?: string }) {
|
||||
const format = file.format?.toLowerCase();
|
||||
const ext = file.name.split('.').pop()?.toLowerCase();
|
||||
return format === 'stl' || format === 'obj' || ext === 'stl' || ext === 'obj';
|
||||
}
|
||||
|
||||
async function handleCadUpload(files: FileList) {
|
||||
if (activeTaskId == null) return;
|
||||
setUploadingCadFor(activeTaskId);
|
||||
@@ -231,6 +323,27 @@ export default function TaskManager({
|
||||
}
|
||||
}
|
||||
|
||||
async function handleModelCadUpload(files: FileList) {
|
||||
setUploadingModelCad(true);
|
||||
try {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const fd = new FormData();
|
||||
fd.append('file', files[i]);
|
||||
const res = await fetch(`/api/files/upload/cad/${modelId}`, {
|
||||
method: 'POST',
|
||||
body: fd,
|
||||
});
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
}
|
||||
utils.cadFile.list.invalidate({ modelId });
|
||||
} catch (e) {
|
||||
console.error('Model CAD upload error', e);
|
||||
alert('上传失败: ' + (e as Error).message);
|
||||
} finally {
|
||||
setUploadingModelCad(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{/* 任务列表 */}
|
||||
@@ -353,40 +466,50 @@ export default function TaskManager({
|
||||
{activeTaskId != null && (
|
||||
<div>
|
||||
{/* 标题栏 */}
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
{/* 全选复选框 */}
|
||||
{pcFiles.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
title={allSelected ? '取消全选' : '全选'}
|
||||
onClick={toggleAllFiles}
|
||||
>
|
||||
{allSelected ? (
|
||||
<CheckSquare className="w-3.5 h-3.5 text-primary" />
|
||||
) : (
|
||||
<Square className="w-3.5 h-3.5" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
<span className="industrial-label flex items-center gap-1">
|
||||
<Cloud className="w-3.5 h-3.5" /> 点云文件
|
||||
<span className="text-[10px] text-muted-foreground font-normal">
|
||||
({someSelected ? `${selectedFileIds.size}/` : ''}{pcFiles.length})
|
||||
<div className="mb-1.5 space-y-1.5">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
{/* 全选复选框 */}
|
||||
{pcFiles.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-muted-foreground hover:text-foreground flex-shrink-0"
|
||||
title={allSelected ? '取消全选' : '全选'}
|
||||
onClick={toggleAllFiles}
|
||||
>
|
||||
{allSelected ? (
|
||||
<CheckSquare className="w-3.5 h-3.5 text-primary" />
|
||||
) : (
|
||||
<Square className="w-3.5 h-3.5" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
<span className="industrial-label flex items-center gap-1 whitespace-nowrap">
|
||||
<Cloud className="w-3.5 h-3.5" /> 点云文件
|
||||
<span className="text-[10px] text-muted-foreground font-normal">
|
||||
({someSelected ? `${selectedFileIds.size}/` : ''}{pcFiles.length})
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
className="industrial-btn py-0.5 text-[10px] flex items-center gap-0.5"
|
||||
className="industrial-btn flex-1 min-w-0 justify-center whitespace-nowrap py-0.5 text-[10px] flex items-center gap-0.5"
|
||||
onClick={() => openUpload(activeTaskId)}
|
||||
>
|
||||
<Upload className="w-3 h-3" /> 上传
|
||||
<Upload className="w-3 h-3" /> 上传到任务
|
||||
</button>
|
||||
<button
|
||||
className={`industrial-btn flex-1 min-w-0 justify-center whitespace-nowrap py-0.5 text-[10px] flex items-center gap-0.5 ${
|
||||
pcLibraryOpen ? 'text-primary border-primary/50' : ''
|
||||
}`}
|
||||
onClick={() => setPcLibraryOpen((open) => !open)}
|
||||
>
|
||||
<Library className="w-3 h-3" /> 从车型库选择
|
||||
</button>
|
||||
{pcFiles.length > 0 && (
|
||||
<button
|
||||
className="industrial-btn industrial-btn-primary py-0.5 text-[10px] flex items-center gap-0.5"
|
||||
className="industrial-btn industrial-btn-primary flex-1 min-w-0 justify-center whitespace-nowrap py-0.5 text-[10px] flex items-center gap-0.5"
|
||||
title={
|
||||
someSelected
|
||||
? `加载选中的 ${selectedFileIds.size} 个文件`
|
||||
@@ -412,9 +535,84 @@ export default function TaskManager({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pcLibraryOpen && (
|
||||
<div className="industrial-card rounded p-2 mb-2 space-y-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
车型点云库 ({modelPcFiles.length})
|
||||
</span>
|
||||
<button
|
||||
className="industrial-btn py-0.5 text-[10px] flex items-center gap-0.5"
|
||||
disabled={uploadingModelPointcloud}
|
||||
onClick={() => modelPointcloudFileRef.current?.click()}
|
||||
>
|
||||
<Upload className="w-3 h-3" />
|
||||
{uploadingModelPointcloud ? '上传中' : '上传到车型库'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 max-h-40 overflow-auto">
|
||||
{modelPcFiles.map((f) => {
|
||||
const isChecked = selectedModelPcFileIds.has(f.id);
|
||||
return (
|
||||
<button
|
||||
key={f.id}
|
||||
type="button"
|
||||
className={`w-full flex items-center gap-2 px-2 py-1.5 rounded text-xs text-left min-w-0 ${
|
||||
isChecked ? 'bg-primary/10 border border-primary/30' : 'bg-secondary/40 hover:bg-secondary/70'
|
||||
}`}
|
||||
onClick={() => toggleModelPcFile(f.id)}
|
||||
>
|
||||
{isChecked ? (
|
||||
<CheckSquare className="w-3.5 h-3.5 text-primary flex-shrink-0" />
|
||||
) : (
|
||||
<Square className="w-3.5 h-3.5 text-muted-foreground flex-shrink-0" />
|
||||
)}
|
||||
<Cloud className="w-3.5 h-3.5 text-cyan-400 flex-shrink-0" />
|
||||
<span className="flex-1 min-w-0 truncate" title={f.name}>{f.name}</span>
|
||||
<span className="text-muted-foreground text-[10px] flex-shrink-0">
|
||||
{formatSize(f.fileSize)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{modelPcFiles.length === 0 && (
|
||||
<div className="text-xs text-muted-foreground text-center py-3">
|
||||
车型点云库为空
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="industrial-btn industrial-btn-primary w-full py-1 text-[10px] flex items-center justify-center gap-1"
|
||||
disabled={activeTaskId == null || selectedModelPcFileIds.size === 0 || linkPcFilesFromModel.isPending}
|
||||
onClick={() => {
|
||||
if (activeTaskId == null || selectedModelPcFileIds.size === 0) return;
|
||||
linkPcFilesFromModel.mutate({
|
||||
taskId: activeTaskId,
|
||||
modelPointcloudFileIds: Array.from(selectedModelPcFileIds),
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
添加到当前任务{selectedModelPcFileIds.size > 0 ? ` (${selectedModelPcFileIds.size})` : ''}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-1">
|
||||
{pcFiles.map((f) => {
|
||||
const isChecked = selectedFileIds.has(f.id);
|
||||
const meta = f as {
|
||||
source?: string;
|
||||
sourceResultId?: number | null;
|
||||
sourcePointcloudFileId?: number | null;
|
||||
createdAt?: string | Date;
|
||||
};
|
||||
const source = meta.source;
|
||||
const sourcePointcloud = meta.sourcePointcloudFileId
|
||||
? pcFiles.find((row) => row.id === meta.sourcePointcloudFileId)
|
||||
: undefined;
|
||||
return (
|
||||
<div
|
||||
key={f.id}
|
||||
@@ -440,14 +638,50 @@ export default function TaskManager({
|
||||
<Cloud
|
||||
className={`w-3.5 h-3.5 flex-shrink-0 ${isChecked ? 'text-primary' : 'text-cyan-400'}`}
|
||||
/>
|
||||
<span className="flex-1 truncate" title={f.name}>
|
||||
{f.name}
|
||||
</span>
|
||||
{(f as { source?: string }).source === 'registered' && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="flex-1 min-w-0 truncate">{f.name}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
align="start"
|
||||
className="max-w-[min(32rem,calc(100vw-2rem))]"
|
||||
>
|
||||
<div className="space-y-1 text-[11px]">
|
||||
<div className="break-all">{f.name}</div>
|
||||
{source === 'registered' && (
|
||||
<>
|
||||
<div>类型:已配准点云</div>
|
||||
{meta.sourceResultId != null && <div>结果编号:R{meta.sourceResultId}</div>}
|
||||
<div>生成时间:{formatDateTime(meta.createdAt)}</div>
|
||||
<div>
|
||||
来源:
|
||||
{sourcePointcloud
|
||||
? sourcePointcloud.name
|
||||
: meta.sourcePointcloudFileId != null
|
||||
? `点云 #${meta.sourcePointcloudFileId}`
|
||||
: '旧记录未记录来源'}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{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>
|
||||
)}
|
||||
{source === 'registered' && meta.sourceResultId != null && (
|
||||
<span className="text-[9px] px-1 py-0.5 rounded bg-sky-500/15 text-sky-300 border border-sky-500/30 flex-shrink-0 whitespace-nowrap">
|
||||
R{meta.sourceResultId}
|
||||
</span>
|
||||
)}
|
||||
{source === 'model_library' && (
|
||||
<span className="text-[9px] px-1 py-0.5 rounded bg-amber-500/15 text-amber-300 border border-amber-500/30 flex-shrink-0 whitespace-nowrap">
|
||||
车型库
|
||||
</span>
|
||||
)}
|
||||
<span className="text-muted-foreground text-[10px] flex-shrink-0">
|
||||
{formatSize(f.fileSize)}
|
||||
</span>
|
||||
@@ -483,17 +717,19 @@ export default function TaskManager({
|
||||
{/* 任务级 CAD 文件 */}
|
||||
{activeTaskId != null && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<span className="industrial-label flex items-center gap-1">
|
||||
<FileBox className="w-3.5 h-3.5" /> 任务 CAD
|
||||
<span className="text-[10px] text-muted-foreground font-normal">
|
||||
({taskCadFiles.length})
|
||||
<div className="mb-1.5 space-y-1.5">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="industrial-label flex items-center gap-1 whitespace-nowrap">
|
||||
<FileBox className="w-3.5 h-3.5" /> 任务 CAD
|
||||
<span className="text-[10px] text-muted-foreground font-normal">
|
||||
({taskCadFiles.length})
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{taskCadFiles.length > 0 && (
|
||||
<button
|
||||
className="industrial-btn py-0.5 text-[10px] flex items-center gap-0.5"
|
||||
className="industrial-btn flex-1 min-w-0 justify-center whitespace-nowrap py-0.5 text-[10px] flex items-center gap-0.5"
|
||||
onClick={toggleAllCadFiles}
|
||||
title={allCadSelected ? '取消全选' : '全选'}
|
||||
>
|
||||
@@ -504,10 +740,18 @@ export default function TaskManager({
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="industrial-btn py-0.5 text-[10px] flex items-center gap-0.5"
|
||||
className="industrial-btn flex-1 min-w-0 justify-center whitespace-nowrap py-0.5 text-[10px] flex items-center gap-0.5"
|
||||
onClick={() => cadFileRef.current?.click()}
|
||||
>
|
||||
<Upload className="w-3 h-3" /> 上传
|
||||
<Upload className="w-3 h-3" /> 上传到任务
|
||||
</button>
|
||||
<button
|
||||
className={`industrial-btn flex-1 min-w-0 justify-center whitespace-nowrap py-0.5 text-[10px] flex items-center gap-0.5 ${
|
||||
cadLibraryOpen ? 'text-primary border-primary/50' : ''
|
||||
}`}
|
||||
onClick={() => setCadLibraryOpen((open) => !open)}
|
||||
>
|
||||
<Library className="w-3 h-3" /> 从车型库选择
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -523,13 +767,80 @@ export default function TaskManager({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{cadLibraryOpen && (
|
||||
<div className="industrial-card rounded p-2 mb-2 space-y-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
车型 CAD 库 ({modelCadFiles.length})
|
||||
</span>
|
||||
<button
|
||||
className="industrial-btn py-0.5 text-[10px] flex items-center gap-0.5"
|
||||
disabled={uploadingModelCad}
|
||||
onClick={() => modelCadFileRef.current?.click()}
|
||||
>
|
||||
<Upload className="w-3 h-3" />
|
||||
{uploadingModelCad ? '上传中' : '上传到车型库'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 max-h-40 overflow-auto">
|
||||
{modelCadFiles.map((f) => {
|
||||
const isChecked = selectedModelCadFileIds.has(f.id);
|
||||
return (
|
||||
<button
|
||||
key={f.id}
|
||||
type="button"
|
||||
className={`w-full flex items-center gap-2 px-2 py-1.5 rounded text-xs text-left min-w-0 ${
|
||||
isChecked ? 'bg-primary/10 border border-primary/30' : 'bg-secondary/40 hover:bg-secondary/70'
|
||||
}`}
|
||||
onClick={() => toggleModelCadFile(f.id)}
|
||||
>
|
||||
{isChecked ? (
|
||||
<CheckSquare className="w-3.5 h-3.5 text-primary flex-shrink-0" />
|
||||
) : (
|
||||
<Square className="w-3.5 h-3.5 text-muted-foreground flex-shrink-0" />
|
||||
)}
|
||||
<FileBox className="w-3.5 h-3.5 text-blue-400 flex-shrink-0" />
|
||||
<span className="flex-1 min-w-0 truncate" title={f.name}>{f.name}</span>
|
||||
<span className="text-muted-foreground text-[10px] flex-shrink-0">
|
||||
{f.format.toUpperCase()} · {formatSize(f.fileSize)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{modelCadFiles.length === 0 && (
|
||||
<div className="text-xs text-muted-foreground text-center py-3">
|
||||
车型 CAD 库为空
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="industrial-btn industrial-btn-primary w-full py-1 text-[10px] flex items-center justify-center gap-1"
|
||||
disabled={activeTaskId == null || selectedModelCadFileIds.size === 0 || linkCadFilesFromModel.isPending}
|
||||
onClick={() => {
|
||||
if (activeTaskId == null || selectedModelCadFileIds.size === 0) return;
|
||||
linkCadFilesFromModel.mutate({
|
||||
taskId: activeTaskId,
|
||||
modelCadFileIds: Array.from(selectedModelCadFileIds),
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
添加到当前任务{selectedModelCadFileIds.size > 0 ? ` (${selectedModelCadFileIds.size})` : ''}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-1">
|
||||
{taskCadFiles.map((f) => {
|
||||
const isChecked = selectedCadFileIds.has(f.id);
|
||||
const source = (f as { source?: string }).source;
|
||||
const canPreview = canPreviewCadIn3D(f);
|
||||
return (
|
||||
<div
|
||||
key={f.id}
|
||||
className={`flex items-center gap-2 px-2 py-1.5 rounded text-xs group cursor-pointer
|
||||
className={`flex items-center gap-2 px-2 py-1.5 rounded text-xs group cursor-pointer min-w-0
|
||||
${isChecked ? 'bg-primary/10 border border-primary/30' : 'industrial-card'}`}
|
||||
onClick={() => toggleCadFile(f.id)}
|
||||
>
|
||||
@@ -545,7 +856,23 @@ export default function TaskManager({
|
||||
)}
|
||||
</button>
|
||||
<FileBox className={`w-3.5 h-3.5 flex-shrink-0 ${isChecked ? 'text-primary' : 'text-blue-400'}`} />
|
||||
<span className="flex-1 truncate" title={f.name}>{f.name}</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="flex-1 min-w-0 truncate">{f.name}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
align="start"
|
||||
className="max-w-[min(32rem,calc(100vw-2rem))] break-all"
|
||||
>
|
||||
{f.name}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{source === 'model_library' && (
|
||||
<span className="text-[9px] px-1 py-0.5 rounded bg-amber-500/15 text-amber-300 border border-amber-500/30 flex-shrink-0 whitespace-nowrap">
|
||||
车型库
|
||||
</span>
|
||||
)}
|
||||
{cadSampleCacheMap[f.id] && (
|
||||
<span
|
||||
className="text-[9px] px-1 py-0.5 rounded bg-green-500/15 text-green-400 border border-green-500/30 flex-shrink-0"
|
||||
@@ -557,14 +884,14 @@ export default function TaskManager({
|
||||
<span className="text-muted-foreground text-[10px] flex-shrink-0">
|
||||
{formatSize(f.fileSize)}
|
||||
</span>
|
||||
{onSelectTaskCad && (
|
||||
{onSelectTaskCad && canPreview && (
|
||||
<button
|
||||
className="hidden group-hover:block p-0.5 rounded hover:bg-secondary flex-shrink-0"
|
||||
title="在 3D 视图中显示"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSelectTaskCad(
|
||||
`/api/files/read/task-cad/${activeTaskId}/${encodeURIComponent(f.name)}`,
|
||||
taskCadReadUrl({ name: f.name, source }),
|
||||
f.format,
|
||||
f.name,
|
||||
);
|
||||
@@ -600,11 +927,14 @@ export default function TaskManager({
|
||||
</p>
|
||||
{selectedCadFileIds.size === 1 && onSelectTaskCad && (() => {
|
||||
const f = taskCadFiles.find(f => selectedCadFileIds.has(f.id));
|
||||
return f ? (
|
||||
return f && canPreviewCadIn3D(f) ? (
|
||||
<button
|
||||
className="industrial-btn w-full py-0.5 text-[10px] flex items-center justify-center gap-1"
|
||||
onClick={() => onSelectTaskCad(
|
||||
`/api/files/read/task-cad/${activeTaskId}/${encodeURIComponent(f.name)}`,
|
||||
taskCadReadUrl({
|
||||
name: f.name,
|
||||
source: (f as { source?: string }).source,
|
||||
}),
|
||||
f.format,
|
||||
f.name,
|
||||
)}
|
||||
@@ -629,6 +959,16 @@ export default function TaskManager({
|
||||
e.target.files && e.target.files.length > 0 && handleUpload(e.target.files)
|
||||
}
|
||||
/>
|
||||
<input
|
||||
ref={modelPointcloudFileRef}
|
||||
type="file"
|
||||
multiple
|
||||
accept=".txt,.xyz,.pts,.ply,.pcd,.las,.laz"
|
||||
className="hidden"
|
||||
onChange={(e) =>
|
||||
e.target.files && e.target.files.length > 0 && handleModelPointcloudUpload(e.target.files)
|
||||
}
|
||||
/>
|
||||
<input
|
||||
ref={cadFileRef}
|
||||
type="file"
|
||||
@@ -639,6 +979,16 @@ export default function TaskManager({
|
||||
e.target.files && e.target.files.length > 0 && handleCadUpload(e.target.files)
|
||||
}
|
||||
/>
|
||||
<input
|
||||
ref={modelCadFileRef}
|
||||
type="file"
|
||||
multiple
|
||||
accept=".obj,.stl,.step,.stp,.iges,.igs"
|
||||
className="hidden"
|
||||
onChange={(e) =>
|
||||
e.target.files && e.target.files.length > 0 && handleModelCadUpload(e.target.files)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -352,6 +352,7 @@ export default function Home() {
|
||||
taskId={selection.taskId}
|
||||
pointCloudLoaded={pointCloudTaskId === selection.taskId && pointCloudUrls.length > 0}
|
||||
taskPcFileCount={taskPcFiles.length}
|
||||
taskPointcloudFiles={taskPcFiles}
|
||||
selectedPcFileIds={selectedPcFileIds}
|
||||
hasRegisteredCloud={hasRegisteredCloud}
|
||||
selectedCadFileIds={selectedTaskCadFileIds}
|
||||
|
||||
Reference in New Issue
Block a user