2026-04-29-21-51-19 - 全栈系统改造:FastAPI后端+SAM2+PostgreSQL+Redis+MinIO+前端Zustand重构

This commit is contained in:
2026-04-29 22:17:25 +08:00
parent c8f8686097
commit fd4b5e5b3d
39 changed files with 3816 additions and 211 deletions

View File

@@ -1,6 +1,9 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useRef, useState, useCallback } from 'react';
import { Stage, Layer, Image as KonvaImage, Circle, Rect, Path, Group } from 'react-konva';
import useImage from 'use-image';
import { useStore } from '../store/useStore';
import { predictMask } from '../lib/api';
import { cn } from '../lib/utils';
interface CanvasAreaProps {
activeTool: string;
@@ -13,6 +16,17 @@ export function CanvasArea({ activeTool }: CanvasAreaProps) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const [points, setPoints] = useState<{ x: number, y: number, type: 'pos'|'neg' }[]>([]);
const [cursorPos, setCursorPos] = useState({ x: 0, y: 0 });
const [boxStart, setBoxStart] = useState<{ x: number, y: number } | null>(null);
const [boxCurrent, setBoxCurrent] = useState<{ x: number, y: number } | null>(null);
const [isInferencing, setIsInferencing] = useState(false);
const masks = useStore((state) => state.masks);
const addMask = useStore((state) => state.addMask);
const clearMasks = useStore((state) => state.clearMasks);
const storeActiveTool = useStore((state) => state.activeTool);
const setActiveTool = useStore((state) => state.setActiveTool);
const effectiveTool = activeTool || storeActiveTool;
// We load a mock image representing a frame
const [image] = useImage('https://images.unsplash.com/photo-1549317661-bd32c8ce0be2?q=80&w=2070&auto=format&fit=crop');
@@ -56,35 +70,120 @@ export function CanvasArea({ activeTool }: CanvasAreaProps) {
if (!stage) return;
const pos = stage.getPointerPosition();
if (pos) {
// Convert to image coordinates
const imageX = (pos.x - position.x) / scale;
const imageY = (pos.y - position.y) / scale;
setCursorPos({ x: imageX, y: imageY });
}
};
const handleStageClick = (e: any) => {
if (activeTool === 'move') return;
if (activeTool === 'point_pos' || activeTool === 'point_neg') {
const stage = e.target.getStage();
const pos = stage.getRelativePointerPosition();
setPoints([...points, { x: pos.x, y: pos.y, type: activeTool === 'point_pos' ? 'pos' : 'neg' }]);
if (boxStart && effectiveTool === 'box_select') {
const relPos = stage.getRelativePointerPosition();
if (relPos) {
setBoxCurrent({ x: relPos.x, y: relPos.y });
}
}
};
const runInference = useCallback(async (promptPoints?: typeof points, promptBox?: { x1: number, y1: number, x2: number, y2: number }) => {
setIsInferencing(true);
try {
const result = await predictMask({
imageUrl: 'https://images.unsplash.com/photo-1549317661-bd32c8ce0be2?q=80&w=2070&auto=format&fit=crop',
points: promptPoints?.map((p) => ({ x: p.x, y: p.y, type: p.type })),
box: promptBox,
});
result.masks.forEach((m) => {
addMask({
id: m.id,
frameId: 'frame-1',
pathData: m.pathData,
label: m.label,
color: m.color,
segmentation: m.segmentation,
bbox: m.bbox,
area: m.area,
});
});
} catch (err) {
console.error('Inference failed:', err);
} finally {
setIsInferencing(false);
}
}, [addMask]);
const handleStageMouseDown = (e: any) => {
if (effectiveTool === 'box_select') {
const stage = e.target.getStage();
const pos = stage.getRelativePointerPosition();
if (pos) {
setBoxStart({ x: pos.x, y: pos.y });
setBoxCurrent({ x: pos.x, y: pos.y });
}
}
};
const handleStageMouseUp = (e: any) => {
if (effectiveTool === 'box_select' && boxStart && boxCurrent) {
const x1 = Math.min(boxStart.x, boxCurrent.x);
const y1 = Math.min(boxStart.y, boxCurrent.y);
const x2 = Math.max(boxStart.x, boxCurrent.x);
const y2 = Math.max(boxStart.y, boxCurrent.y);
if (Math.abs(x2 - x1) > 5 && Math.abs(y2 - y1) > 5) {
runInference(undefined, { x1, y1, x2, y2 });
}
setBoxStart(null);
setBoxCurrent(null);
}
};
const handleStageClick = (e: any) => {
if (effectiveTool === 'move') return;
if (effectiveTool === 'box_select') return; // handled by mouseup
if (effectiveTool === 'point_pos' || effectiveTool === 'point_neg') {
const stage = e.target.getStage();
const pos = stage.getRelativePointerPosition();
if (pos) {
const newPoints = [...points, { x: pos.x, y: pos.y, type: effectiveTool === 'point_pos' ? 'pos' : 'neg' as 'pos'|'neg' }];
setPoints(newPoints);
// Auto-trigger inference after point selection
runInference(newPoints);
}
}
};
const boxRect = React.useMemo(() => {
if (!boxStart || !boxCurrent) return null;
const x = Math.min(boxStart.x, boxCurrent.x);
const y = Math.min(boxStart.y, boxCurrent.y);
const width = Math.abs(boxCurrent.x - boxStart.x);
const height = Math.abs(boxCurrent.y - boxStart.y);
return { x, y, width, height };
}, [boxStart, boxCurrent]);
return (
<div ref={containerRef} className="w-full h-full relative cursor-crosshair overflow-hidden rounded-sm">
{isInferencing && (
<div className="absolute top-4 right-4 z-20 flex items-center gap-2 bg-[#111] border border-white/10 px-3 py-2 rounded-lg shadow-xl">
<div className="w-3 h-3 border-2 border-cyan-500 border-t-transparent rounded-full animate-spin" />
<span className="text-xs text-cyan-400 font-mono">AI ...</span>
</div>
)}
<Stage
width={stageSize.width}
height={stageSize.height}
onWheel={handleWheel}
onMouseMove={handleMouseMove}
onMouseDown={handleStageMouseDown}
onMouseUp={handleStageMouseUp}
scaleX={scale}
scaleY={scale}
x={position.x}
y={position.y}
draggable={activeTool === 'move'}
draggable={effectiveTool === 'move'}
onClick={handleStageClick}
>
<Layer>
@@ -98,24 +197,38 @@ export function CanvasArea({ activeTool }: CanvasAreaProps) {
/>
)}
{/* Mock Instance Mask overlapping */}
<Group opacity={0.4}>
<Path
data="M 300 200 Q 400 150 450 250 T 400 350 Q 250 350 280 250 Z"
fill="#06b6d4" // cyan-500
{/* AI Returned Masks */}
{masks.map((mask) => (
<Group key={mask.id} opacity={0.5}>
<Path
data={mask.pathData}
fill={mask.color}
stroke={mask.color}
strokeWidth={1 / scale}
/>
</Group>
))}
{/* Box selection preview */}
{boxRect && effectiveTool === 'box_select' && (
<Rect
x={boxRect.x}
y={boxRect.y}
width={boxRect.width}
height={boxRect.height}
fill="rgba(6, 182, 212, 0.1)"
stroke="#06b6d4"
strokeWidth={2 / scale}
dash={[4 / scale, 4 / scale]}
/>
<Path
data="M 600 400 Q 700 350 750 450 T 650 550 Q 550 550 580 450 Z"
fill="#a855f7" // purple-500
/>
</Group>
)}
{/* AI Prompts Point Regions */}
{points.map((p, i) => (
<Group key={i} x={p.x} y={p.y}>
<Circle
radius={6 / scale}
fill={p.type === 'pos' ? '#22c55e' : '#ef4444'} // green or red
fill={p.type === 'pos' ? '#22c55e' : '#ef4444'}
stroke="#ffffff"
strokeWidth={2 / scale}
shadowColor="black"
@@ -134,7 +247,17 @@ export function CanvasArea({ activeTool }: CanvasAreaProps) {
<span>: {cursorPos.x.toFixed(2)}, {cursorPos.y.toFixed(2)}</span>
<span>当前图层树: OBJECT_VEHICLE_01</span>
<span>: {(scale * 100).toFixed(0)}%</span>
<span>: {masks.length}</span>
</div>
{masks.length > 0 && (
<button
onClick={clearMasks}
className="absolute bottom-4 right-4 text-xs bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/20 px-3 py-1.5 rounded transition-colors"
>
</button>
)}
</div>
);
}