feat: initial commit - massive-cloud point cloud processing system

This commit is contained in:
2026-05-27 09:51:42 +00:00
commit 16811100d1
391 changed files with 78338 additions and 0 deletions

View File

@@ -0,0 +1,291 @@
import { useMemo, useState } from 'react';
import { trpc } from '@/lib/trpc';
import {
ChevronRight,
ChevronDown,
Plus,
Trash2,
Train,
Box,
Check,
X,
} from 'lucide-react';
export type TreeSelection = {
seriesId: number | null;
modelId: number | null;
taskId: number | null;
};
interface Props {
selection: TreeSelection;
onSelect: (s: TreeSelection) => void;
}
function EditableText({
value,
onSave,
}: {
value: string;
onSave: (v: string) => void;
}) {
const [editing, setEditing] = useState(false);
const [draft, setDraft] = useState(value);
if (!editing) {
return (
<span
className="flex-1 cursor-pointer hover:text-foreground truncate"
onDoubleClick={() => { setDraft(value); setEditing(true); }}
>
{value}
</span>
);
}
return (
<span className="flex items-center gap-1 flex-1">
<input
autoFocus
className="flex-1 bg-secondary text-xs px-1 py-0.5 rounded outline-none border border-primary"
value={draft}
onChange={e => setDraft(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter') { onSave(draft); setEditing(false); }
if (e.key === 'Escape') setEditing(false);
}}
/>
<button onClick={() => { onSave(draft); setEditing(false); }} className="text-green-400"><Check className="w-3 h-3" /></button>
<button onClick={() => setEditing(false)} className="text-muted-foreground"><X className="w-3 h-3" /></button>
</span>
);
}
export default function VehicleTree({ selection, onSelect }: Props) {
const utils = trpc.useUtils();
const [expandedSeries, setExpandedSeries] = useState<Set<number>>(new Set());
const [newSeriesName, setNewSeriesName] = useState('');
const [addingSeries, setAddingSeries] = useState(false);
const [addingModelFor, setAddingModelFor] = useState<number | null>(null);
const [newModelName, setNewModelName] = useState('');
const { data: seriesList = [] } = trpc.vehicleSeries.list.useQuery();
// 固定次数的 Hooks:用 trpc.useQueries(内部是单个 useQueries),禁止在 map 里调 useQuery
const modelQueryResults = trpc.useQueries((t) =>
seriesList.map((s) =>
t.vehicleModel.listBySeries(
{ seriesId: s.id },
{ enabled: expandedSeries.has(s.id) }
)
)
);
const createSeries = trpc.vehicleSeries.create.useMutation({
onSuccess: () => utils.vehicleSeries.list.invalidate(),
});
const deleteSeries = trpc.vehicleSeries.delete.useMutation({
onSuccess: () => utils.vehicleSeries.list.invalidate(),
});
const updateSeries = trpc.vehicleSeries.update.useMutation({
onSuccess: () => utils.vehicleSeries.list.invalidate(),
});
const createModel = trpc.vehicleModel.create.useMutation({
onSuccess: (_, vars) => utils.vehicleModel.listBySeries.invalidate({ seriesId: vars.seriesId }),
});
const deleteModel = trpc.vehicleModel.delete.useMutation({
onSuccess: () => {
seriesList.forEach(s => utils.vehicleModel.listBySeries.invalidate({ seriesId: s.id }));
},
});
const updateModel = trpc.vehicleModel.update.useMutation({
onSuccess: () => {
seriesList.forEach(s => utils.vehicleModel.listBySeries.invalidate({ seriesId: s.id }));
},
});
function toggleSeries(id: number) {
setExpandedSeries(prev => {
const n = new Set(prev);
n.has(id) ? n.delete(id) : n.add(id);
return n;
});
}
return (
<div className="industrial-panel p-2 space-y-1">
<div className="flex items-center justify-between mb-2">
<span className="industrial-label">车系 / 车型 / 任务</span>
<button
className="industrial-btn py-0.5 text-[10px] flex items-center gap-0.5"
onClick={() => setAddingSeries(true)}
>
<Plus className="w-3 h-3" /> 车系
</button>
</div>
{addingSeries && (
<div className="flex items-center gap-1 pl-1">
<input
autoFocus
className="flex-1 bg-secondary text-xs px-2 py-1 rounded outline-none border border-primary"
placeholder="车系名称"
value={newSeriesName}
onChange={e => setNewSeriesName(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter' && newSeriesName.trim()) {
createSeries.mutate({ name: newSeriesName.trim() });
setNewSeriesName('');
setAddingSeries(false);
}
if (e.key === 'Escape') setAddingSeries(false);
}}
/>
<button
className="text-green-400"
onClick={() => {
if (newSeriesName.trim()) {
createSeries.mutate({ name: newSeriesName.trim() });
setNewSeriesName('');
}
setAddingSeries(false);
}}
>
<Check className="w-3.5 h-3.5" />
</button>
<button className="text-muted-foreground" onClick={() => setAddingSeries(false)}>
<X className="w-3.5 h-3.5" />
</button>
</div>
)}
{seriesList.map((series, si) => {
const expanded = expandedSeries.has(series.id);
const models = modelQueryResults[si]?.data ?? [];
const isSeriesSelected = selection.seriesId === series.id && !selection.modelId;
return (
<div key={series.id}>
{/* 车系行 */}
<div
className={`flex items-center gap-1 px-1 py-1 rounded cursor-pointer group text-xs
${isSeriesSelected ? 'bg-primary/20 text-primary' : 'hover:bg-secondary/60'}`}
onClick={() => {
toggleSeries(series.id);
onSelect({ seriesId: series.id, modelId: null, taskId: null });
}}
>
{expanded ? <ChevronDown className="w-3 h-3 flex-shrink-0 text-muted-foreground" /> : <ChevronRight className="w-3 h-3 flex-shrink-0 text-muted-foreground" />}
<Train className="w-3 h-3 flex-shrink-0 text-muted-foreground" />
<EditableText
value={series.name}
onSave={name => updateSeries.mutate({ id: series.id, name })}
/>
<span className="hidden group-hover:flex items-center gap-0.5 flex-shrink-0">
<button
className="p-0.5 rounded hover:bg-secondary"
title="新增车型"
onClick={e => { e.stopPropagation(); setAddingModelFor(series.id); setNewModelName(''); }}
>
<Plus className="w-3 h-3" />
</button>
<button
className="p-0.5 rounded hover:bg-secondary text-destructive"
title="删除车系"
onClick={e => {
e.stopPropagation();
if (confirm(`删除车系「${series.name}」?`)) deleteSeries.mutate({ id: series.id });
}}
>
<Trash2 className="w-3 h-3" />
</button>
</span>
</div>
{/* 新增车型输入 */}
{addingModelFor === series.id && (
<div className="flex items-center gap-1 pl-7 pr-1 py-0.5">
<input
autoFocus
className="flex-1 bg-secondary text-xs px-2 py-1 rounded outline-none border border-primary"
placeholder="车型名称"
value={newModelName}
onChange={e => setNewModelName(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter' && newModelName.trim()) {
createModel.mutate({ seriesId: series.id, name: newModelName.trim() });
setNewModelName('');
setAddingModelFor(null);
setExpandedSeries(prev => {
const n = new Set(prev);
n.add(series.id);
return n;
});
}
if (e.key === 'Escape') setAddingModelFor(null);
}}
/>
<button
className="text-green-400"
onClick={() => {
if (newModelName.trim()) {
createModel.mutate({ seriesId: series.id, name: newModelName.trim() });
setExpandedSeries(prev => {
const n = new Set(prev);
n.add(series.id);
return n;
});
}
setAddingModelFor(null);
}}
>
<Check className="w-3.5 h-3.5" />
</button>
<button className="text-muted-foreground" onClick={() => setAddingModelFor(null)}>
<X className="w-3.5 h-3.5" />
</button>
</div>
)}
{/* 车型列表 */}
{expanded && models.map(model => {
const isModelSelected = selection.modelId === model.id;
return (
<div key={model.id}>
<div
className={`flex items-center gap-1 px-1 py-1 pl-5 rounded cursor-pointer group text-xs
${isModelSelected ? 'bg-primary/20 text-primary' : 'hover:bg-secondary/60'}`}
onClick={() => onSelect({ seriesId: series.id, modelId: model.id, taskId: null })}
>
<Box className="w-3 h-3 flex-shrink-0 text-blue-400" />
<EditableText
value={model.name}
onSave={name => updateModel.mutate({ id: model.id, name })}
/>
<span className="hidden group-hover:flex items-center gap-0.5 flex-shrink-0">
<button
className="p-0.5 rounded hover:bg-secondary text-destructive"
title="删除车型"
onClick={e => {
e.stopPropagation();
if (confirm(`删除车型「${model.name}」?`)) deleteModel.mutate({ id: model.id });
}}
>
<Trash2 className="w-3 h-3" />
</button>
</span>
</div>
</div>
);
})}
</div>
);
})}
{seriesList.length === 0 && (
<div className="text-center py-8 text-xs text-muted-foreground">
暂无车系,点击右上角新增
</div>
)}
</div>
);
}