2026-04-29-21-51-19 - 全栈系统改造:FastAPI后端+SAM2+PostgreSQL+Redis+MinIO+前端Zustand重构
This commit is contained in:
135
src/lib/api.ts
Normal file
135
src/lib/api.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import axios, { AxiosError } from 'axios';
|
||||
import type { Project, Template } from '../store/useStore';
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: 'http://localhost:8000',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
// Request interceptor: attach token
|
||||
apiClient.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
// Response interceptor: handle errors
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error: AxiosError) => {
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
window.location.reload();
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Auth
|
||||
export async function login(username: string, password: string): Promise<{ token: string }> {
|
||||
const response = await apiClient.post('/api/auth/login', { username, password });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Projects
|
||||
export async function getProjects(): Promise<Project[]> {
|
||||
const response = await apiClient.get('/api/projects');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function createProject(payload: {
|
||||
name: string;
|
||||
description?: string;
|
||||
}): Promise<Project> {
|
||||
const response = await apiClient.post('/api/projects', payload);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function updateProject(id: string, payload: Partial<Project>): Promise<Project> {
|
||||
const response = await apiClient.put(`/api/projects/${id}`, payload);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function deleteProject(id: string): Promise<void> {
|
||||
await apiClient.delete(`/api/projects/${id}`);
|
||||
}
|
||||
|
||||
// Templates
|
||||
export async function getTemplates(): Promise<Template[]> {
|
||||
const response = await apiClient.get('/api/templates');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function createTemplate(payload: {
|
||||
name: string;
|
||||
description?: string;
|
||||
classes?: { name: string; color: string; zIndex: number; category?: string }[];
|
||||
}): Promise<Template> {
|
||||
const response = await apiClient.post('/api/templates', payload);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function updateTemplate(id: string, payload: Partial<Template>): Promise<Template> {
|
||||
const response = await apiClient.put(`/api/templates/${id}`, payload);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function deleteTemplate(id: string): Promise<void> {
|
||||
await apiClient.delete(`/api/templates/${id}`);
|
||||
}
|
||||
|
||||
// Media
|
||||
export async function uploadMedia(file: File, projectId?: string): Promise<{ url: string; id: string }> {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
if (projectId) {
|
||||
formData.append('project_id', projectId);
|
||||
}
|
||||
const response = await apiClient.post('/api/media/upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// AI Prediction
|
||||
export async function predictMask(payload: {
|
||||
imageUrl: string;
|
||||
points?: { x: number; y: number; type: 'pos' | 'neg' }[];
|
||||
box?: { x1: number; y1: number; x2: number; y2: number };
|
||||
text?: string;
|
||||
modelSize?: string;
|
||||
}): Promise<{
|
||||
masks: Array<{
|
||||
id: string;
|
||||
pathData: string;
|
||||
label: string;
|
||||
color: string;
|
||||
segmentation: number[][];
|
||||
bbox: [number, number, number, number];
|
||||
area: number;
|
||||
confidence: number;
|
||||
}>;
|
||||
}> {
|
||||
const response = await apiClient.post('/api/ai/predict', payload);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Export
|
||||
export async function exportCoco(projectId: string): Promise<Blob> {
|
||||
const response = await apiClient.get(`/api/export/coco/${projectId}`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export default apiClient;
|
||||
104
src/lib/websocket.ts
Normal file
104
src/lib/websocket.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
type ProgressCallback = (data: ProgressMessage) => void;
|
||||
|
||||
interface ProgressMessage {
|
||||
type: 'progress' | 'status' | 'error' | 'complete';
|
||||
taskId?: string;
|
||||
filename?: string;
|
||||
progress?: number;
|
||||
status?: string;
|
||||
message?: string;
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
class ProgressWebSocket {
|
||||
private ws: WebSocket | null = null;
|
||||
private url: string;
|
||||
private callbacks: Set<ProgressCallback> = new Set();
|
||||
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private reconnectInterval = 3000;
|
||||
private maxReconnectInterval = 30000;
|
||||
private shouldReconnect = false;
|
||||
private currentInterval = 3000;
|
||||
|
||||
constructor(url = 'ws://localhost:8000/ws/progress') {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.shouldReconnect = true;
|
||||
|
||||
try {
|
||||
this.ws = new WebSocket(this.url);
|
||||
|
||||
this.ws.onopen = () => {
|
||||
this.currentInterval = this.reconnectInterval;
|
||||
console.log('[WebSocket] Connected to progress stream');
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const data: ProgressMessage = JSON.parse(event.data);
|
||||
this.callbacks.forEach((cb) => cb(data));
|
||||
} catch (err) {
|
||||
console.error('[WebSocket] Failed to parse message:', err);
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
console.log('[WebSocket] Connection closed');
|
||||
if (this.shouldReconnect) {
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onerror = (err) => {
|
||||
console.error('[WebSocket] Error:', err);
|
||||
this.ws?.close();
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('[WebSocket] Failed to connect:', err);
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.shouldReconnect = false;
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
}
|
||||
|
||||
onProgress(callback: ProgressCallback) {
|
||||
this.callbacks.add(callback);
|
||||
return () => {
|
||||
this.callbacks.delete(callback);
|
||||
};
|
||||
}
|
||||
|
||||
private scheduleReconnect() {
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
}
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
console.log(`[WebSocket] Reconnecting in ${this.currentInterval}ms...`);
|
||||
this.connect();
|
||||
this.currentInterval = Math.min(this.currentInterval * 1.5, this.maxReconnectInterval);
|
||||
}, this.currentInterval);
|
||||
}
|
||||
|
||||
isConnected(): boolean {
|
||||
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
||||
}
|
||||
}
|
||||
|
||||
export const progressWS = new ProgressWebSocket();
|
||||
export type { ProgressMessage };
|
||||
Reference in New Issue
Block a user